diff --git a/web/src/components/add.test.tsx b/web/src/components/add.test.tsx index 2bc02dcb..3859e9eb 100644 --- a/web/src/components/add.test.tsx +++ b/web/src/components/add.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import * as redux from 'react-redux'; -import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import { screen, fireEvent, act, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { renderWithBrowserRouter } from '../../tests/test-utils'; import Add from './add'; const languages = [{ @@ -39,7 +40,7 @@ test('should submit sentences including review', async () => { })); (redux.useDispatch as jest.Mock).mockImplementation(() => dispatchMock); - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') } }); @@ -91,7 +92,7 @@ test('should submit sentences including review - with errors', async () => { })); (redux.useDispatch as jest.Mock).mockImplementation(() => dispatchMock); - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') } }); @@ -136,7 +137,7 @@ test('should submit sentences including review - with unexpected server response })); (redux.useDispatch as jest.Mock).mockImplementation(() => dispatchMock); - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') } }); diff --git a/web/src/components/confirm-form.test.tsx b/web/src/components/confirm-form.test.tsx index 873592f1..dcf44819 100644 --- a/web/src/components/confirm-form.test.tsx +++ b/web/src/components/confirm-form.test.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { renderWithBrowserRouter } from '../../tests/test-utils'; import ConfirmForm from './confirm-form'; const submitted = ['This is a test444.', 'This too!', 'Hi.']; @@ -12,7 +13,7 @@ const onSubmit = jest.fn(); const onReview = jest.fn(); test('should render full form', () => { - render( + renderWithBrowserRouter( { }); test('should not render review if none to review', () => { - render( + renderWithBrowserRouter( { }); test('should not render invalidated if there are none', () => { - render( + renderWithBrowserRouter( { }); test('should not render already validated if there are none', () => { - render( + renderWithBrowserRouter( { event.preventDefault(); }); - render( + renderWithBrowserRouter( { }); test('should disable submit button if no sentences', async () => { - render( + renderWithBrowserRouter( { }); test('should not show submit button while uploading sentences', async () => { - render( + renderWithBrowserRouter( { }); test('should show submission notice while uploading', async () => { - render( + renderWithBrowserRouter( + +

Confirm New Sentences

{`${submitted.length} sentences found.`}

- { invalidated.length > 0 && ( -

+ {invalidated.length > 0 && ( +

{`${invalidated.length} rejected by you`}

)} - { validated.length + invalidated.length > 0 && ( + {validated.length + invalidated.length > 0 && (

{`-- ${validated.length + invalidated.length} sentences are already reviewed. Great job!`}

@@ -47,7 +50,7 @@ export default function ConfirmForm(props: Props) {

{`${readyCount} sentences ready for submission!`}

- { unreviewed.length > 0 && ( + {unreviewed.length > 0 && (

{`-- ${unreviewed.length} of these sentences are unreviewed. If you want, you can also review your sentences now before submitting them.`}  - { isUploadingSentences ? + {isUploadingSentences ? : } - { isUploadingSentences && ( + {isUploadingSentences && (

Sentences are being uploaded. This can take several minutes depending on the number of sentences added. diff --git a/web/src/components/prompt.tsx b/web/src/components/prompt.tsx new file mode 100644 index 00000000..d7361cf2 --- /dev/null +++ b/web/src/components/prompt.tsx @@ -0,0 +1,33 @@ +import React, { useEffect } from 'react'; +import { Prompt as ReactRouterPrompt } from 'react-router-dom'; + +interface PromptProps { + when: boolean; + message: string; +} + +/** + * Wrapper around react-router Prompt adding confirmation on tab close and external navigation. + */ +export const Prompt: React.FC = (props) => { + const { when } = props; + useEffect(() => { + if (!when) { + return () => null; + } + + function listener(event: BeforeUnloadEvent) { + // Cancel the event as stated by the standard. + event.preventDefault(); + // Chrome requires returnValue to be set. + event.returnValue = ''; + } + + window.addEventListener('beforeunload', listener); + return () => window.removeEventListener('beforeunload', listener); + }, [when]); + + return ( + + ); +}; diff --git a/web/src/components/review-form.test.tsx b/web/src/components/review-form.test.tsx index 85472d63..88d9efda 100644 --- a/web/src/components/review-form.test.tsx +++ b/web/src/components/review-form.test.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import { screen, fireEvent, act, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { renderWithBrowserRouter } from '../../tests/test-utils'; import ReviewForm from './review-form'; const allSentences = [{ @@ -22,7 +23,7 @@ beforeEach(() => { }); test('should approve and reject sentences using buttons', async () => { - render( + renderWithBrowserRouter( { }); test('should approve and reject sentences using shortcuts', async () => { - render( + renderWithBrowserRouter( { }); test('should skip sentences using button', async () => { - render( + renderWithBrowserRouter( { }); test('should skip sentence using shortcut', async () => { - render( + renderWithBrowserRouter( { }); test('should not mark anything as validated or invalidated if no review done', async () => { - render( + renderWithBrowserRouter( { - render( + renderWithBrowserRouter( { }); test('should return empty component if no sentences', async () => { - const { container } = render( + const { container } = renderWithBrowserRouter( { }); test('should show message', async () => { - render( + renderWithBrowserRouter( +

+ 0 } + message="Reviewed sentences not submitted, are sure?" + /> +

Swipe right to approve the sentence. Swipe left to reject it. Swipe up to skip it.

-

You have reviewed {reviewedSentencesCount} sentences. Do not forget to submit your review by clicking on the "Finish Review" button below!

+

You have reviewed { reviewedSentencesCount } sentences. Do not forget to submit your review by clicking on the "Finish Review" button below!

- + - {message && (

{message}

)} + { message && (

{ message }

) }
- {sentences.map((sentence, i) => ( + { sentences.map((sentence, i) => ( handleSwipe(i, direction)} - preventSwipe={['down']} + ref={ cardsRefs[i] } + onSwipe={ (direction) => handleSwipe(i, direction) } + preventSwipe={ ['down'] } >
- {sentence.sentence || sentence} - {sentence.source ? `Source: ${sentence.source}` : ''} + { sentence.sentence || sentence } + { sentence.source ? `Source: ${ sentence.source }` : '' }
- ))} + )) }
- - - + + +

You can also use Keyboard Shortcuts: Y to Approve, N to Reject, S to Skip

@@ -181,4 +187,4 @@ function mapSentencesAccordingToState(sentences: SentenceRecord[], reviewApprova return acc; }, { validated: [], invalidated: [], unreviewed: [] }); -} \ No newline at end of file +} diff --git a/web/src/components/review.test.tsx b/web/src/components/review.test.tsx index 0a92d025..b7038849 100644 --- a/web/src/components/review.test.tsx +++ b/web/src/components/review.test.tsx @@ -1,8 +1,8 @@ import React from 'react'; import * as redux from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; +import { renderWithBrowserRouter } from '../../tests/test-utils'; import LanguageSelector from './language-selector'; import ReviewForm from './review-form'; import ReviewCriteria from './review-criteria'; @@ -33,7 +33,7 @@ test('should set language from single user language', () => { sentencesLoading: false, sentences: [] })); - render(); + renderWithBrowserRouter(); expect(screen.queryByText('Please select a language to review sentences.')).toBeNull(); }); @@ -44,7 +44,7 @@ test('should ask to set language', () => { sentencesLoading: false, sentences: [] })); - render(); + renderWithBrowserRouter(); expect(screen.getByText(/You have not selected any languages./)).toBeTruthy(); }); @@ -55,7 +55,7 @@ test('should render loading', () => { sentencesLoading: true, sentences: [] })); - render(); + renderWithBrowserRouter(); expect(screen.getByText('Loading sentences...')).toBeTruthy(); }); @@ -66,7 +66,7 @@ test('should render no language selected', () => { sentencesLoading: false, sentences: [] })); - render(); + renderWithBrowserRouter(); expect(screen.getByText('Please select a language to review sentences.')).toBeTruthy(); }); @@ -77,7 +77,7 @@ test('should render no sentences found', () => { sentencesLoading: false, sentences: [] })); - render(); + renderWithBrowserRouter(); expect(screen.getByText(/No sentences to review./)).toBeTruthy(); }); @@ -92,22 +92,22 @@ test('should render no sentences found if all sentences are skipped', () => { }], skippedSentences: [1], })); - render(); + renderWithBrowserRouter(); expect(screen.getByText(/No sentences to review./)).toBeTruthy(); }); test('should render language selector', () => { - render(); + renderWithBrowserRouter(); expect(screen.getByText(/LanguageSelector/)).toBeTruthy(); }); test('should render review criteria', () => { - render(); + renderWithBrowserRouter(); expect(screen.getByText(/ReviewCriteria/)).toBeTruthy(); }); test('should dispatch load', () => { - render(); + renderWithBrowserRouter(); expect(dispatchMock).toHaveBeenCalled(); }); @@ -119,7 +119,7 @@ test('should only render form', () => { sentences: ['Hi'], reviewMessage: 'Hi', })); - render(); + renderWithBrowserRouter(); expect(screen.getByText(/ReviewForm/)).toBeTruthy(); expect(screen.queryByText(/You have not selected any languages./)).toBeNull(); expect(screen.queryByText('Loading sentences...')).toBeNull(); diff --git a/web/src/components/submit-form.test.tsx b/web/src/components/submit-form.test.tsx index 83f1ecad..269c67df 100644 --- a/web/src/components/submit-form.test.tsx +++ b/web/src/components/submit-form.test.tsx @@ -3,6 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter } from 'react-router-dom'; +import { renderWithBrowserRouter } from '../../tests/test-utils'; import SubmitForm from './submit-form'; const languages = [{ @@ -30,24 +31,24 @@ beforeEach(() => { }); test('should render submit button', () => { - render(); + renderWithBrowserRouter(); expect(screen.getByText('Submit')).toBeTruthy(); }); test('should render message', () => { const message = 'Hi'; - render(); + renderWithBrowserRouter(); expect(screen.getByText(message)).toBeTruthy(); }); test('should render error', () => { const error = 'Oh no!'; - render(); + renderWithBrowserRouter(); expect(screen.getByText(error)).toBeTruthy(); }); test('should submit form if valid', async () => { - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') }}); @@ -63,7 +64,7 @@ test('should submit form if valid', async () => { }); test('should show error if no language', async () => { - render(); + renderWithBrowserRouter(); await userEvent.click(screen.getByText('Submit')); expect(onSubmit.mock.calls.length).toBe(0); @@ -71,7 +72,7 @@ test('should show error if no language', async () => { }); test('should show error if no sentences', async () => { - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); @@ -81,7 +82,7 @@ test('should show error if no sentences', async () => { }); test('should show error if no source', async () => { - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') }}); @@ -92,7 +93,7 @@ test('should show error if no source', async () => { }); test('should show error if not confirmed', async () => { - render(); + renderWithBrowserRouter(); fireEvent.change(screen.getByRole('combobox'), { target: { value: 'en' } }); fireEvent.input(screen.getByRole('textbox', { name: /Add public domain sentences/i }), { target: { value: sentences.join('\n') }}); diff --git a/web/src/components/submit-form.tsx b/web/src/components/submit-form.tsx index 3fb1f950..9e9a8d52 100644 --- a/web/src/components/submit-form.tsx +++ b/web/src/components/submit-form.tsx @@ -5,6 +5,7 @@ import type { Language, SubmissionFailures } from '../types'; import LanguageSelector from './language-selector'; import Sentence from './sentence'; import SubmitButton from './submit-button'; +import { Prompt } from './prompt'; const SPLIT_ON = '\n'; @@ -99,6 +100,11 @@ export default function SubmitForm({ languages, onSubmit, message, error, senten return ( + +

Add Sentences

{ message && (
{ message }
)} diff --git a/web/tests/test-utils.tsx b/web/tests/test-utils.tsx new file mode 100644 index 00000000..f33c9bb4 --- /dev/null +++ b/web/tests/test-utils.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; + + +export function renderWithBrowserRouter(component: React.ReactNode) { + return render( + + {component} + + ); +}