From 2e4447b03ffbf35d864abac207e1678e8b5467d2 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 26 Nov 2024 23:17:29 +0100 Subject: [PATCH] :alien: [#724] Update tests and stories to work with Vitest * Replaced the jest mocks with vi mocks (which is the equivalent in vitest) * Replaced useFakeTimers with just mocking the system date/time, which is simpler in vitest. Note that using fake timers seems to have some challenges, but ultimately there shouldn't really be a need for that anymore. * Updated the stories to deal with the index.js -> index.jsx renames, which don't automatically resolve when doing directory imports * Rewrite more tests from legacy react test utils to testing-library/ react style tests, which are more pleasant to read and write and do encourage proper accessible queries --- src/components/Anchor/Anchor.stories.jsx | 2 +- src/components/ButtonsToolbar/test.spec.jsx | 12 +- src/components/CoSign/test.spec.jsx | 49 ++---- src/components/EditGrid/EditGrid.stories.jsx | 2 +- .../EditGrid/EditGridItem.stories.jsx | 2 +- .../EmailVerificationForm.stories.jsx | 2 +- .../EmailVerificationModal.stories.jsx | 2 +- src/components/Form.spec.jsx | 6 +- src/components/FormStart/tests.spec.jsx | 26 +-- src/components/FormStep/FormStep.stories.jsx | 2 +- .../FormStepSummary.stories.jsx | 2 +- .../IntroductionPage.stories.jsx | 2 +- .../LanguageSelection.stories.jsx | 2 +- .../LoginOptions/LoginOptions.stories.jsx | 2 +- src/components/LoginOptions/index.jsx | 4 +- src/components/LoginOptions/tests.spec.jsx | 6 +- src/components/Map/Map.stories.jsx | 2 +- .../ProgressIndicator.stories.jsx | 2 +- src/components/Summary/test.spec.jsx | 16 +- .../SummaryConfirmation/test.spec.jsx | 6 +- .../SummaryProgress.stories.jsx | 2 +- .../CreateAppointment.spec.jsx | 4 +- .../CreateAppointment/Summary.spec.jsx | 3 +- .../appointments/fields/DateSelect.spec.jsx | 13 +- .../appointments/fields/TimeSelect.spec.jsx | 29 +--- .../steps/LocationAndTimeStep.spec.jsx | 10 +- .../auth/AuthenticationErrors/tests.spec.jsx | 134 +++++++-------- src/components/formio/index.stories.jsx | 2 +- src/formio/components/Selectboxes.spec.js | 7 +- .../{useZodErrorMap.js => useZodErrorMap.jsx} | 0 .../formio/components/currency.spec.js | 41 ++--- src/jstests/formio/components/date.spec.js | 24 +-- .../formio/components/datetime.spec.js | 4 +- src/jstests/formio/components/file.spec.js | 83 ++++------ .../formio/components/ibanfield.spec.js | 100 +++++------- .../formio/components/licenseplate.spec.js | 60 +++---- src/jstests/formio/components/map.spec.js | 10 +- src/jstests/formio/components/number.spec.js | 24 ++- .../formio/components/textfield.spec.js | 153 +++++++----------- src/jstests/formio/components/time.spec.js | 130 ++++++--------- src/sdk.spec.jsx | 8 +- 41 files changed, 384 insertions(+), 606 deletions(-) rename src/hooks/{useZodErrorMap.js => useZodErrorMap.jsx} (100%) diff --git a/src/components/Anchor/Anchor.stories.jsx b/src/components/Anchor/Anchor.stories.jsx index f6f11885a..0263be677 100644 --- a/src/components/Anchor/Anchor.stories.jsx +++ b/src/components/Anchor/Anchor.stories.jsx @@ -1,4 +1,4 @@ -import Anchor, {ANCHOR_MODIFIERS} from '.'; +import Anchor, {ANCHOR_MODIFIERS} from './index'; export default { title: 'Pure React components / Anchor', diff --git a/src/components/ButtonsToolbar/test.spec.jsx b/src/components/ButtonsToolbar/test.spec.jsx index eefa0d0e2..46f0ab835 100644 --- a/src/components/ButtonsToolbar/test.spec.jsx +++ b/src/components/ButtonsToolbar/test.spec.jsx @@ -45,7 +45,7 @@ const Wrap = ({children}) => ( ); it('Last step of submittable form, button is present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); act(() => { root.render( @@ -76,7 +76,7 @@ it('Last step of submittable form, button is present', () => { }); it('Last step of non-submittable form with overview, button is present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); act(() => { root.render( @@ -107,7 +107,7 @@ it('Last step of non-submittable form with overview, button is present', () => { }); it('Last step of non-submittable form without overview, button is NOT present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); act(() => { root.render( @@ -137,7 +137,7 @@ it('Last step of non-submittable form without overview, button is NOT present', }); it('Non-last step of non-submittable form without overview, button IS present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); act(() => { root.render( @@ -168,7 +168,7 @@ it('Non-last step of non-submittable form without overview, button IS present', }); it('Suspending form allowed, button is present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); renderTest( @@ -191,7 +191,7 @@ it('Suspending form allowed, button is present', () => { }); it('Suspending form not allowed, button is NOT present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); renderTest( diff --git a/src/components/CoSign/test.spec.jsx b/src/components/CoSign/test.spec.jsx index 4ec84261c..3702f95ea 100644 --- a/src/components/CoSign/test.spec.jsx +++ b/src/components/CoSign/test.spec.jsx @@ -1,32 +1,12 @@ +import {render, screen} from '@testing-library/react'; import messagesNL from 'i18n/compiled/nl.json'; import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {act} from 'react-dom/test-utils'; import {IntlProvider} from 'react-intl'; import {testLoginForm} from 'components/FormStart/fixtures'; import {CoSignAuthentication} from './index'; -let container = null; -let root = null; -beforeEach(() => { - // setup a DOM element as a render target - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); -}); - -afterEach(() => { - // cleanup on exiting - act(() => { - root.unmount(); - container.remove(); - root = null; - container = null; - }); -}); - it('CoSign component constructs the right auth URL', () => { // Control the location that the test will use const {location} = window; @@ -35,25 +15,22 @@ it('CoSign component constructs the right auth URL', () => { href: 'https://openforms.nl/form-name/step/step-name', }; - act(() => { - root.render( - - {}} - /> - - ); - }); + render( + + {}} + /> + + ); // Reset location window.location = location; - const loginButton = container.getElementsByTagName('a')[0]; - - expect(loginButton.textContent).toEqual('Inloggen met DigiD'); + const loginButton = screen.getByRole('link', {name: 'Inloggen met DigiD'}); + expect(loginButton).toBeVisible(); const loginUrl = new URL(loginButton.href); diff --git a/src/components/EditGrid/EditGrid.stories.jsx b/src/components/EditGrid/EditGrid.stories.jsx index af7e7a818..5547ac299 100644 --- a/src/components/EditGrid/EditGrid.stories.jsx +++ b/src/components/EditGrid/EditGrid.stories.jsx @@ -3,7 +3,7 @@ import {fn} from '@storybook/test'; import Body from 'components/Body'; import {OFButton} from 'components/Button'; -import {EditGrid, EditGridButtonGroup, EditGridItem} from '.'; +import {EditGrid, EditGridButtonGroup, EditGridItem} from './index'; export default { title: 'Pure React components / EditGrid / EditGrid', diff --git a/src/components/EditGrid/EditGridItem.stories.jsx b/src/components/EditGrid/EditGridItem.stories.jsx index d63ceca57..0cf294383 100644 --- a/src/components/EditGrid/EditGridItem.stories.jsx +++ b/src/components/EditGrid/EditGridItem.stories.jsx @@ -1,7 +1,7 @@ import Body from 'components/Body'; import {OFButton} from 'components/Button'; -import {EditGridButtonGroup, EditGridItem as EditGridItemComponent} from '.'; +import {EditGridButtonGroup, EditGridItem as EditGridItemComponent} from './index'; export default { title: 'Pure React components / EditGrid / Item', diff --git a/src/components/EmailVerification/EmailVerificationForm.stories.jsx b/src/components/EmailVerification/EmailVerificationForm.stories.jsx index 3bc0f4f10..405f6e706 100644 --- a/src/components/EmailVerification/EmailVerificationForm.stories.jsx +++ b/src/components/EmailVerification/EmailVerificationForm.stories.jsx @@ -4,7 +4,7 @@ import {withRouter} from 'storybook-addon-remix-react-router'; import {BASE_URL} from 'api-mocks'; import {ConfigDecorator} from 'story-utils/decorators'; -import {EmailVerificationForm} from '.'; +import {EmailVerificationForm} from './index'; import { mockEmailVerificationErrorPost, mockEmailVerificationPost, diff --git a/src/components/EmailVerification/EmailVerificationModal.stories.jsx b/src/components/EmailVerification/EmailVerificationModal.stories.jsx index 42299cfee..b403f72c7 100644 --- a/src/components/EmailVerification/EmailVerificationModal.stories.jsx +++ b/src/components/EmailVerification/EmailVerificationModal.stories.jsx @@ -3,7 +3,7 @@ import {fn} from '@storybook/test'; import {BASE_URL} from 'api-mocks'; import {ConfigDecorator} from 'story-utils/decorators'; -import {EmailVerificationModal} from '.'; +import {EmailVerificationModal} from './index'; import {mockEmailVerificationPost, mockEmailVerificationVerifyCodePost} from './mocks'; export default { diff --git a/src/components/Form.spec.jsx b/src/components/Form.spec.jsx index c57f70faf..14ffe3e1a 100644 --- a/src/components/Form.spec.jsx +++ b/src/components/Form.spec.jsx @@ -12,7 +12,7 @@ import {routes} from 'components/App'; import {START_FORM_QUERY_PARAM} from './constants'; -window.scrollTo = jest.fn(); +window.scrollTo = vi.fn(); beforeEach(() => { localStorage.clear(); @@ -23,7 +23,7 @@ afterEach(() => { }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); const Wrapper = ({form = buildForm(), initialEntry = '/startpagina'}) => { @@ -54,7 +54,7 @@ const Wrapper = ({form = buildForm(), initialEntry = '/startpagina'}) => { test('Start form anonymously', async () => { const user = userEvent.setup(); - mswServer.use(mockSubmissionPost(), mockAnalyticsToolConfigGet(), mockSubmissionStepGet); + mswServer.use(mockSubmissionPost(), mockAnalyticsToolConfigGet(), mockSubmissionStepGet()); let startSubmissionRequest; mswServer.events.on('request:match', async ({request}) => { const url = new URL(request.url); diff --git a/src/components/FormStart/tests.spec.jsx b/src/components/FormStart/tests.spec.jsx index 72ab89450..11cbf4a19 100644 --- a/src/components/FormStart/tests.spec.jsx +++ b/src/components/FormStart/tests.spec.jsx @@ -7,11 +7,11 @@ import {MemoryRouter} from 'react-router-dom'; import {buildSubmission} from 'api-mocks'; import useQuery from 'hooks/useQuery'; -import FormStart from '.'; import {testForm, testLoginForm} from './fixtures'; +import FormStart from './index'; -jest.mock('hooks/useQuery'); -let scrollIntoViewMock = jest.fn(); +vi.mock('hooks/useQuery'); +let scrollIntoViewMock = vi.fn(); window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; const Wrap = ({children}) => ( @@ -24,8 +24,8 @@ it('Form start page start if _start parameter is present', async () => { const testLocation = new URLSearchParams('?_start=1'); useQuery.mockReturnValue(testLocation); - const onFormStart = jest.fn(); - const onDestroySession = jest.fn(); + const onFormStart = vi.fn(); + const onDestroySession = vi.fn(); render( @@ -55,8 +55,8 @@ it.each([ ])( 'Form start does not start if there are auth errors / %s', async (testQuery, expectedMessage) => { - const onFormStart = jest.fn(); - const onDestroySession = jest.fn(); + const onFormStart = vi.fn(); + const onDestroySession = vi.fn(); const testLocation = new URLSearchParams(`?_start=1&${testQuery}`); useQuery.mockReturnValue(testLocation); @@ -74,8 +74,8 @@ it.each([ it('Form start page does not show login buttons if an active submission is present', async () => { useQuery.mockReturnValue(new URLSearchParams()); - const onFormStart = jest.fn(); - const onDestroySession = jest.fn(); + const onFormStart = vi.fn(); + const onDestroySession = vi.fn(); render( @@ -95,8 +95,8 @@ it('Form start page does not show login buttons if an active submission is prese it('Form start page with initial_data_reference', async () => { useQuery.mockReturnValue(new URLSearchParams()); - const onFormStart = jest.fn(); - const onDestroySession = jest.fn(); + const onFormStart = vi.fn(); + const onDestroySession = vi.fn(); render( @@ -118,8 +118,8 @@ it('Form start page with initial_data_reference', async () => { it('Form start page without initial_data_reference', async () => { useQuery.mockReturnValue(new URLSearchParams()); - const onFormStart = jest.fn(); - const onDestroySession = jest.fn(); + const onFormStart = vi.fn(); + const onDestroySession = vi.fn(); render( diff --git a/src/components/FormStep/FormStep.stories.jsx b/src/components/FormStep/FormStep.stories.jsx index b1cfb717e..f5fb10926 100644 --- a/src/components/FormStep/FormStep.stories.jsx +++ b/src/components/FormStep/FormStep.stories.jsx @@ -12,7 +12,7 @@ import { import {AnalyticsToolsDecorator, ConfigDecorator} from 'story-utils/decorators'; import {sleep} from 'utils'; -import FormStep from '.'; +import FormStep from './index'; import { getSubmissionStepDetail, mockSubmissionLogicCheckPost, diff --git a/src/components/FormStepSummary/FormStepSummary.stories.jsx b/src/components/FormStepSummary/FormStepSummary.stories.jsx index 9c834320c..eac37a508 100644 --- a/src/components/FormStepSummary/FormStepSummary.stories.jsx +++ b/src/components/FormStepSummary/FormStepSummary.stories.jsx @@ -1,7 +1,7 @@ import {expect, within} from '@storybook/test'; import {withRouter} from 'storybook-addon-remix-react-router'; -import FormStepSummary from '.'; +import FormStepSummary from './index'; export default { title: 'Private API / FormStepSummary', diff --git a/src/components/IntroductionPage/IntroductionPage.stories.jsx b/src/components/IntroductionPage/IntroductionPage.stories.jsx index 1fb5ad3d4..baf53848e 100644 --- a/src/components/IntroductionPage/IntroductionPage.stories.jsx +++ b/src/components/IntroductionPage/IntroductionPage.stories.jsx @@ -4,7 +4,7 @@ import {withRouter} from 'storybook-addon-remix-react-router'; import {buildForm} from 'api-mocks'; import {withForm} from 'story-utils/decorators'; -import IntroductionPage from '.'; +import IntroductionPage from './index'; const DEFAULT_CONTENT = `

Voorwaarden

diff --git a/src/components/LanguageSelection/LanguageSelection.stories.jsx b/src/components/LanguageSelection/LanguageSelection.stories.jsx index ea44a058e..b24929459 100644 --- a/src/components/LanguageSelection/LanguageSelection.stories.jsx +++ b/src/components/LanguageSelection/LanguageSelection.stories.jsx @@ -7,7 +7,7 @@ import ErrorBoundary from 'components/Errors/ErrorBoundary'; import {I18NContext} from 'i18n'; import {ConfigDecorator} from 'story-utils/decorators'; -import {LanguageSelection, LanguageSelectionDisplay} from '.'; +import {LanguageSelection, LanguageSelectionDisplay} from './index'; import { DEFAULT_LANGUAGES, mockInvalidLanguageChoicePut, diff --git a/src/components/LoginOptions/LoginOptions.stories.jsx b/src/components/LoginOptions/LoginOptions.stories.jsx index 96a8789bf..a0753b01b 100644 --- a/src/components/LoginOptions/LoginOptions.stories.jsx +++ b/src/components/LoginOptions/LoginOptions.stories.jsx @@ -4,8 +4,8 @@ import {withRouter} from 'storybook-addon-remix-react-router'; import {buildForm} from 'api-mocks'; import {LiteralDecorator} from 'story-utils/decorators'; -import LoginOptions from '.'; import LoginOptionsDisplay from './LoginOptionsDisplay'; +import LoginOptions from './index'; export default { title: 'Composites / Login Options', diff --git a/src/components/LoginOptions/index.jsx b/src/components/LoginOptions/index.jsx index fc20ca24b..2ac2758fb 100644 --- a/src/components/LoginOptions/index.jsx +++ b/src/components/LoginOptions/index.jsx @@ -66,9 +66,9 @@ const LoginOptions = ({form, onFormStart, extraNextParams = {}, isolateCosignOpt const containerProps = form.loginRequired ? {} : { - onSubmit: e => { + onSubmit: async e => { e.preventDefault(); - onFormStart(e, true); + await onFormStart(e, true); }, 'data-testid': 'start-form', }; diff --git a/src/components/LoginOptions/tests.spec.jsx b/src/components/LoginOptions/tests.spec.jsx index 1e947afbe..8da223d7e 100644 --- a/src/components/LoginOptions/tests.spec.jsx +++ b/src/components/LoginOptions/tests.spec.jsx @@ -36,7 +36,7 @@ const Wrapper = ({form, onFormStart}) => { it('Login not required, options wrapped in form tag', async () => { const user = userEvent.setup(); const form = buildForm({loginRequired: false, loginOptions: [], cosignLoginOptions: []}); - const onFormStart = jest.fn(e => e.preventDefault()); + const onFormStart = vi.fn(e => e.preventDefault()); render(); @@ -69,7 +69,7 @@ it('Login required, options not wrapped in form tag', async () => { ], cosignLoginOptions: [], }); - const onFormStart = jest.fn(e => e.preventDefault()); + const onFormStart = vi.fn(e => e.preventDefault()); const {location} = window; delete window.location; @@ -112,7 +112,7 @@ it('Login button has the right URL after cancelling log in', async () => { cosignLoginOptions: [], }); - const onFormStart = jest.fn(e => e.preventDefault()); + const onFormStart = vi.fn(e => e.preventDefault()); const {location} = window; delete window.location; diff --git a/src/components/Map/Map.stories.jsx b/src/components/Map/Map.stories.jsx index 578cbe13c..3d9c8f100 100644 --- a/src/components/Map/Map.stories.jsx +++ b/src/components/Map/Map.stories.jsx @@ -2,7 +2,7 @@ import {userEvent, within} from '@storybook/test'; import {ConfigDecorator} from 'story-utils/decorators'; -import LeafletMap from '.'; +import LeafletMap from './index'; import {mockAddressSearchGet, mockLatLngSearchEmptyGet, mockLatLngSearchGet} from './mocks'; const withMapLayout = Story => ( diff --git a/src/components/ProgressIndicator/ProgressIndicator.stories.jsx b/src/components/ProgressIndicator/ProgressIndicator.stories.jsx index 79cf6a6d1..a68802977 100644 --- a/src/components/ProgressIndicator/ProgressIndicator.stories.jsx +++ b/src/components/ProgressIndicator/ProgressIndicator.stories.jsx @@ -2,7 +2,7 @@ import {MINIMAL_VIEWPORTS} from '@storybook/addon-viewport'; import {expect, userEvent, within} from '@storybook/test'; import {withRouter} from 'storybook-addon-remix-react-router'; -import ProgressIndicator from '.'; +import ProgressIndicator from './index'; export default { title: 'Private API / ProgressIndicator', diff --git a/src/components/Summary/test.spec.jsx b/src/components/Summary/test.spec.jsx index 70191e9f0..bc8b07887 100644 --- a/src/components/Summary/test.spec.jsx +++ b/src/components/Summary/test.spec.jsx @@ -31,8 +31,8 @@ const SUBMISSION = { isAuthenticated: false, }; -jest.mock('react-use'); -jest.mock('hooks/useRefreshSubmission'); +vi.mock('react-use'); +vi.mock('hooks/useRefreshSubmission'); let container = null; let root = null; @@ -64,8 +64,8 @@ it('Summary displays logout button if isAuthenticated is true', () => { ...SUBMISSION, isAuthenticated: true, }; - const onDestroySession = jest.fn(); - const onConfirm = jest.fn(); + const onDestroySession = vi.fn(); + const onConfirm = vi.fn(); useAsync.mockReturnValue({loading: false, value: []}); useRefreshSubmission.mockReturnValue(submissionIsAuthenticated); @@ -92,8 +92,8 @@ it('Summary does not display logout button if loginRequired is false', () => { ...testForm, loginRequired: false, }; - const onDestroySession = jest.fn(); - const onConfirm = jest.fn(); + const onDestroySession = vi.fn(); + const onConfirm = vi.fn(); useAsync.mockReturnValue({loading: false, value: []}); useRefreshSubmission.mockReturnValue({...SUBMISSION, isAuthenticated: false}); @@ -116,8 +116,8 @@ it('Summary does not display logout button if loginRequired is false', () => { }); it('Summary displays abort button if isAuthenticated is false', () => { - const onDestroySession = jest.fn(); - const onConfirm = jest.fn(); + const onDestroySession = vi.fn(); + const onConfirm = vi.fn(); useAsync.mockReturnValue({loading: false, value: []}); useRefreshSubmission.mockReturnValue({...SUBMISSION, isAuthenticated: false}); diff --git a/src/components/SummaryConfirmation/test.spec.jsx b/src/components/SummaryConfirmation/test.spec.jsx index f0deea168..ec086a9e8 100644 --- a/src/components/SummaryConfirmation/test.spec.jsx +++ b/src/components/SummaryConfirmation/test.spec.jsx @@ -39,7 +39,7 @@ const Wrapper = ({children}) => ( {children} @@ -48,7 +48,7 @@ const Wrapper = ({children}) => ( ); it('Summary of non-submittable form, button is NOT present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); renderTest( @@ -66,7 +66,7 @@ it('Summary of non-submittable form, button is NOT present', () => { }); it('Summary of submittable form, button IS present', () => { - const mockFunction = jest.fn(); + const mockFunction = vi.fn(); renderTest( diff --git a/src/components/SummaryProgress/SummaryProgress.stories.jsx b/src/components/SummaryProgress/SummaryProgress.stories.jsx index 206aadb25..8545f0e55 100644 --- a/src/components/SummaryProgress/SummaryProgress.stories.jsx +++ b/src/components/SummaryProgress/SummaryProgress.stories.jsx @@ -1,6 +1,6 @@ import {withRouter} from 'storybook-addon-remix-react-router'; -import SummaryProgress from '.'; +import SummaryProgress from './index'; export default { title: 'Private API / SummaryProgress', diff --git a/src/components/appointments/CreateAppointment/CreateAppointment.spec.jsx b/src/components/appointments/CreateAppointment/CreateAppointment.spec.jsx index 1c7143328..4f0c9998e 100644 --- a/src/components/appointments/CreateAppointment/CreateAppointment.spec.jsx +++ b/src/components/appointments/CreateAppointment/CreateAppointment.spec.jsx @@ -24,8 +24,8 @@ import { } from '../mocks'; import {SESSION_STORAGE_KEY as APPOINTMENT_SESSION_STORAGE_KEY} from './CreateAppointmentState'; -// scrollIntoView is not not supported in Jest -let scrollIntoViewMock = jest.fn(); +// scrollIntoView is not not supported in jest-dom +let scrollIntoViewMock = vi.fn(); window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; const routes = [ diff --git a/src/components/appointments/CreateAppointment/Summary.spec.jsx b/src/components/appointments/CreateAppointment/Summary.spec.jsx index 9e4a19f4c..efaf979ab 100644 --- a/src/components/appointments/CreateAppointment/Summary.spec.jsx +++ b/src/components/appointments/CreateAppointment/Summary.spec.jsx @@ -1,4 +1,3 @@ -import {jest} from '@jest/globals'; import {render as realRender, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import messagesEN from 'i18n/compiled/en.json'; @@ -164,7 +163,7 @@ describe('The appointment summary', () => { it('processes backend validation errors', async () => { mswServer.use(mockAppointmentErrorPost); const user = userEvent.setup({delay: null}); - const errorHandler = jest.fn(); + const errorHandler = vi.fn(); renderSummary(errorHandler); diff --git a/src/components/appointments/fields/DateSelect.spec.jsx b/src/components/appointments/fields/DateSelect.spec.jsx index e34ebba4a..9de909333 100644 --- a/src/components/appointments/fields/DateSelect.spec.jsx +++ b/src/components/appointments/fields/DateSelect.spec.jsx @@ -1,4 +1,3 @@ -import {jest} from '@jest/globals'; import {act, render as realRender, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {Formik} from 'formik'; @@ -41,20 +40,12 @@ const render = (comp, locationId) => ); beforeEach(() => { - jest.useFakeTimers({ - advanceTimers: true, - now: new Date('2023-06-12T14:00:00Z'), - }); -}); - -afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); + vi.setSystemTime(new Date('2023-06-12T14:00:00Z')); }); describe('The appointment date select', () => { it('disables dates before and after the available dates range', async () => { - const user = userEvent.setup({delay: null}); + const user = userEvent.setup(); mswServer.use(mockAppointmentDatesGet); render(, '1396f17c'); diff --git a/src/components/appointments/fields/TimeSelect.spec.jsx b/src/components/appointments/fields/TimeSelect.spec.jsx index 998e1511a..440fff077 100644 --- a/src/components/appointments/fields/TimeSelect.spec.jsx +++ b/src/components/appointments/fields/TimeSelect.spec.jsx @@ -1,4 +1,3 @@ -import {jest} from '@jest/globals'; import {act, render as realRender, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {Formik} from 'formik'; @@ -38,26 +37,14 @@ const render = (comp, locationId) => ); -beforeEach(() => { - jest.useFakeTimers({ - advanceTimers: true, - now: new Date('2023-06-12T14:00:00Z'), - }); -}); - -afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); -}); - describe('The appointment time select', () => { it('makes sure times are localized', async () => { - const user = userEvent.setup({delay: null}); + const user = userEvent.setup(); mswServer.use(mockAppointmentTimesGet); render(, '1396f17c'); + const timeSelect = await screen.findByLabelText('Time'); - expect(timeSelect).toBeVisible(); // open the options dropdown act(() => { @@ -66,12 +53,10 @@ describe('The appointment time select', () => { await user.keyboard('[ArrowDown]'); // see mocks.js for the returned times - await waitFor(() => { - expect(screen.getByText('08:00')).toBeVisible(); - }); - expect(screen.getByText('08:10')).toBeVisible(); - expect(screen.getByText('10:00')).toBeVisible(); - expect(screen.getByText('10:30')).toBeVisible(); - expect(screen.getByText('14:30')).toBeVisible(); + expect(await screen.findByRole('option', {name: '08:00'})).toBeVisible(); + expect(screen.getByRole('option', {name: '08:10'})).toBeVisible(); + expect(screen.getByRole('option', {name: '10:00'})).toBeVisible(); + expect(screen.getByRole('option', {name: '10:30'})).toBeVisible(); + expect(screen.getByRole('option', {name: '14:30'})).toBeVisible(); }); }); diff --git a/src/components/appointments/steps/LocationAndTimeStep.spec.jsx b/src/components/appointments/steps/LocationAndTimeStep.spec.jsx index daf42d0bd..620d4d43e 100644 --- a/src/components/appointments/steps/LocationAndTimeStep.spec.jsx +++ b/src/components/appointments/steps/LocationAndTimeStep.spec.jsx @@ -1,4 +1,3 @@ -import {jest} from '@jest/globals'; import {act, render as realRender, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import messagesEN from 'i18n/compiled/en.json'; @@ -59,15 +58,10 @@ const render = initialValues => { }; beforeEach(() => { - jest.useFakeTimers({ - advanceTimers: true, - now: new Date('2023-06-12T14:00:00Z'), - }); + vi.setSystemTime(new Date('2023-06-12T14:00:00Z')); }); afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); window.sessionStorage.clear(); }); @@ -112,7 +106,7 @@ describe('The location and time step', () => { }); it('retains focus on the date input', async () => { - const user = userEvent.setup({delay: null}); + const user = userEvent.setup(); mswServer.use( mockAppointmentProductsGet, mockAppointmentLocationsGet, diff --git a/src/components/auth/AuthenticationErrors/tests.spec.jsx b/src/components/auth/AuthenticationErrors/tests.spec.jsx index 5fd2a7e25..dd985a056 100644 --- a/src/components/auth/AuthenticationErrors/tests.spec.jsx +++ b/src/components/auth/AuthenticationErrors/tests.spec.jsx @@ -1,110 +1,84 @@ +import {render, screen} from '@testing-library/react'; import messagesNL from 'i18n/compiled/nl.json'; import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {act} from 'react-dom/test-utils'; import {IntlProvider} from 'react-intl'; -import {AuthenticationErrors} from '.'; +import {AuthenticationErrors} from './index'; -let scrollIntoViewMock = jest.fn(); +let scrollIntoViewMock = vi.fn(); window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; -let container = null; -let root = null; -beforeEach(() => { - // setup a DOM element as a render target - container = document.createElement('div'); - document.body.appendChild(container); - root = createRoot(container); -}); - -afterEach(() => { - // cleanup on exiting - act(() => { - root.unmount(); - container.remove(); - root = null; - container = null; - }); -}); - it('Renders DigiD default error', () => { - act(() => { - root.render( - - - - ); - }); - - expect(container.textContent).toBe( - 'Inloggen bij deze organisatie is niet gelukt. Probeert u ' + - 'het later nog een keer. Lukt het nog steeds niet? Log in bij Mijn DigiD. Zo ' + - 'controleert u of uw DigiD goed werkt. Mogelijk is er een storing bij ' + - 'de organisatie waar u inlogt.' + render( + + + ); + + expect( + screen.getByText( + 'Inloggen bij deze organisatie is niet gelukt. Probeert u ' + + 'het later nog een keer. Lukt het nog steeds niet? Log in bij Mijn DigiD. Zo ' + + 'controleert u of uw DigiD goed werkt. Mogelijk is er een storing bij ' + + 'de organisatie waar u inlogt.' + ) + ).toBeVisible(); }); it('Renders DigiD cancel login error', () => { - act(() => { - root.render( - - - - ); - }); + render( + + + + ); - expect(container.textContent).toBe('Je hebt het inloggen met DigiD geannuleerd.'); + expect(screen.getByText('Je hebt het inloggen met DigiD geannuleerd.')).toBeVisible(); }); it('Renders EHerkenning default error', () => { - act(() => { - root.render( - - - - ); - }); - - expect(container.textContent).toBe( - 'Er is een fout opgetreden bij het inloggen met EHerkenning. Probeer het later opnieuw.' + render( + + + ); + + expect( + screen.getByText( + 'Er is een fout opgetreden bij het inloggen met EHerkenning. Probeer het later opnieuw.' + ) + ).toBeVisible(); }); it('Renders EHerkenning cancel login error', () => { - act(() => { - root.render( - - - - ); - }); + render( + + + + ); - expect(container.textContent).toBe('Je hebt het inloggen met EHerkenning geannuleerd.'); + expect(screen.getByText('Je hebt het inloggen met EHerkenning geannuleerd.')).toBeVisible(); }); it('Renders eIDAS default error', () => { - act(() => { - root.render( - - - - ); - }); - - expect(container.textContent).toBe( - 'Er is een fout opgetreden bij het inloggen met eIDAS. Probeer het later opnieuw.' + render( + + + ); + + expect( + screen.getByText( + 'Er is een fout opgetreden bij het inloggen met eIDAS. Probeer het later opnieuw.' + ) + ).toBeVisible(); }); it('Renders eIDAS cancel login error', () => { - act(() => { - root.render( - - - - ); - }); + render( + + + + ); - expect(container.textContent).toBe('Je hebt het inloggen met eIDAS geannuleerd.'); + expect(screen.getByText('Je hebt het inloggen met eIDAS geannuleerd.')).toBeVisible(); }); diff --git a/src/components/formio/index.stories.jsx b/src/components/formio/index.stories.jsx index 9881ce31a..c4d8a64e1 100644 --- a/src/components/formio/index.stories.jsx +++ b/src/components/formio/index.stories.jsx @@ -1,6 +1,6 @@ import {FormikDecorator} from 'story-utils/decorators'; -import {FormioComponent} from '.'; +import {FormioComponent} from './index'; export default { title: 'Private API / Formio', diff --git a/src/formio/components/Selectboxes.spec.js b/src/formio/components/Selectboxes.spec.js index b3a85ce41..d803a0226 100644 --- a/src/formio/components/Selectboxes.spec.js +++ b/src/formio/components/Selectboxes.spec.js @@ -1,5 +1,4 @@ -import {expect} from '@storybook/test'; -import {screen} from '@testing-library/dom'; +import {screen, waitFor} from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import _ from 'lodash'; import {Formio} from 'react-formio'; @@ -84,7 +83,9 @@ describe('The selectboxes component', () => { await user.click(selectboxB); // All checkboxes are again marked as valid and without aria-describedby and aria-invalid - expect(selectboxA).not.toHaveClass('is-invalid'); + await waitFor(() => { + expect(selectboxA).not.toHaveClass('is-invalid'); + }); expect(selectboxA).not.toHaveAttribute('aria-describedby'); expect(selectboxA).not.toHaveAttribute('aria-invalid'); expect(selectboxB).not.toHaveClass('is-invalid'); diff --git a/src/hooks/useZodErrorMap.js b/src/hooks/useZodErrorMap.jsx similarity index 100% rename from src/hooks/useZodErrorMap.js rename to src/hooks/useZodErrorMap.jsx diff --git a/src/jstests/formio/components/currency.spec.js b/src/jstests/formio/components/currency.spec.js index 145316e21..65f14da5c 100644 --- a/src/jstests/formio/components/currency.spec.js +++ b/src/jstests/formio/components/currency.spec.js @@ -1,5 +1,4 @@ import _ from 'lodash'; -import React from 'react'; import {Formio} from 'react-formio'; import OpenFormsModule from 'formio/module'; @@ -10,44 +9,34 @@ import {currencyForm} from './fixtures/currency'; Formio.use(OpenFormsModule); describe('Currency Component', () => { - test('Currency component with 0 decimalLimit formatted correctly', done => { + test('Currency component with 0 decimalLimit formatted correctly', async () => { let formJSON = _.cloneDeep(currencyForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('currency'); - const formattedValue = component.getValueAsString(1); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('currency'); + const formattedValue = component.getValueAsString(1); - expect(formattedValue).toEqual('€1'); - - done(); - }) - .catch(done); + expect(formattedValue).toEqual('€1'); }); - test('#2903 - Emptying currency component results in null value in data', done => { + test('#2903 - Emptying currency component results in null value in data', async () => { let formJSON = _.cloneDeep(currencyForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('currency'); - component.setValue(13); - - expect(form._data['currency']).toEqual(13); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('currency'); + component.setValue(13); - component.dataValue = null; + expect(form._data['currency']).toEqual(13); - // null, instead of undefined (default Formio behaviour which removes the key from the data) - expect(form._data['currency']).toEqual(null); + component.dataValue = null; - done(); - }) - .catch(done); + // null, instead of undefined (default Formio behaviour which removes the key from the data) + expect(form._data['currency']).toEqual(null); }); }); diff --git a/src/jstests/formio/components/date.spec.js b/src/jstests/formio/components/date.spec.js index b9ce0b14b..dcdcd5d5e 100644 --- a/src/jstests/formio/components/date.spec.js +++ b/src/jstests/formio/components/date.spec.js @@ -6,7 +6,7 @@ import {MinMaxDateValidator} from 'formio/validators/minMaxDateAndDatetimeValida const FormioComponent = Formio.Components.components.component; describe('Date Component', () => { - test('Date validator: check min date', done => { + test('Date validator: check min date', () => { const component = { label: 'date', key: 'date', @@ -33,11 +33,9 @@ describe('Date Component', () => { const isValid2 = MinMaxDateValidator.check(componentInstance, {}, '2024-01-01'); expect(isValid2).toBeTruthy(); - - done(); }); - test('Date validator: check max date', done => { + test('Date validator: check max date', () => { const component = { label: 'date', key: 'date', @@ -64,11 +62,9 @@ describe('Date Component', () => { const isValid2 = MinMaxDateValidator.check(componentInstance, {}, '2020-01-01'); expect(isValid2).toBeTruthy(); - - done(); }); - test('Date validator: check max date including the current one', done => { + test('Date validator: check max date including the current one', () => { const component = { label: 'date', key: 'date', @@ -88,11 +84,9 @@ describe('Date Component', () => { const isValid1 = MinMaxDateValidator.check(componentInstance, {}, '2023-09-08'); expect(isValid1).toBeTruthy(); - - done(); }); - test('Date validator: error message', done => { + test('Date validator: error message', () => { const component = { label: 'date', key: 'date', @@ -110,12 +104,12 @@ describe('Date Component', () => { }, }; - const mockTranslation = jest.fn((message, values) => message); + const mockTranslation = vi.fn((message, values) => message); const componentInstance = new FormioComponent(component, {}, {}); componentInstance.t = mockTranslation; componentInstance.options.intl = { - formatDate: jest.fn((date, options) => 'formatted date'), + formatDate: vi.fn((date, options) => 'formatted date'), }; set( @@ -137,11 +131,9 @@ describe('Date Component', () => { MinMaxDateValidator.message(componentInstance); expect(mockTranslation.mock.calls[1][0]).toEqual('maxDate'); - - done(); }); - test('Date validator: check max date AND min date', done => { + test('Date validator: check max date AND min date', () => { const component = { label: 'date', key: 'date', @@ -171,7 +163,5 @@ describe('Date Component', () => { expect( componentInstance.openForms.validationErrorContext.minMaxDateAndDatetimeValidatorErrorKey ).toContain('minDate'); - - done(); }); }); diff --git a/src/jstests/formio/components/datetime.spec.js b/src/jstests/formio/components/datetime.spec.js index 2ea46ff7c..5bee2b172 100644 --- a/src/jstests/formio/components/datetime.spec.js +++ b/src/jstests/formio/components/datetime.spec.js @@ -150,12 +150,12 @@ describe('Datetime Component', () => { }, }; - const mockTranslation = jest.fn((message, values) => message); + const mockTranslation = vi.fn((message, values) => message); const componentInstance = new FormioComponent(component, {}, {}); componentInstance.t = mockTranslation; componentInstance.options.intl = { - formatDate: jest.fn((date, options) => 'formatted date'), + formatDate: vi.fn((date, options) => 'formatted date'), }; set( diff --git a/src/jstests/formio/components/file.spec.js b/src/jstests/formio/components/file.spec.js index f1dc23484..15970352c 100644 --- a/src/jstests/formio/components/file.spec.js +++ b/src/jstests/formio/components/file.spec.js @@ -25,71 +25,58 @@ const maxNFilesForm = { }; describe('Multiple File Component', () => { - test('Uploading 1 file gives no error', done => { + test('Uploading 1 file gives no error', async () => { let formJSON = _.cloneDeep(maxNFilesForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('multipleFiles'); - component.upload([{name: 'File 1', size: '50'}]); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('multipleFiles'); + component.upload([{name: 'File 1', size: '50'}]); - expect(!!component.error).toBeFalsy(); - - done(); - }) - .catch(done); + expect(!!component.error).toBeFalsy(); }); - test('Uploading 3 files gives an error', done => { + test('Uploading 3 files gives an error', async () => { let formJSON = _.cloneDeep(maxNFilesForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('multipleFiles'); - component.upload([ - {name: 'File 1', size: '50'}, - {name: 'File 2', size: '50'}, - {name: 'File 3', size: '50'}, - ]); - - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual( - 'Too many files added. The maximum allowed number of files is 2.' - ); - done(); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('multipleFiles'); + component.upload([ + {name: 'File 1', size: '50'}, + {name: 'File 2', size: '50'}, + {name: 'File 3', size: '50'}, + ]); + + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual( + 'Too many files added. The maximum allowed number of files is 2.' + ); }); // GH-4222 - test('Uploading 1 file then 2 files gives an error', done => { + test('Uploading 1 file then 2 files gives an error', async () => { let formJSON = _.cloneDeep(maxNFilesForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('multipleFiles'); - component.dataValue.push({name: 'File 1', size: '50'}); - - component.upload([ - {name: 'File 2', size: '50'}, - {name: 'File 3', size: '50'}, - ]); - - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual( - 'Too many files added. The maximum allowed number of files is 2.' - ); - done(); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('multipleFiles'); + component.dataValue.push({name: 'File 1', size: '50'}); + + component.upload([ + {name: 'File 2', size: '50'}, + {name: 'File 3', size: '50'}, + ]); + + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual( + 'Too many files added. The maximum allowed number of files is 2.' + ); }); }); diff --git a/src/jstests/formio/components/ibanfield.spec.js b/src/jstests/formio/components/ibanfield.spec.js index 07ebe0ec6..c0ca88043 100644 --- a/src/jstests/formio/components/ibanfield.spec.js +++ b/src/jstests/formio/components/ibanfield.spec.js @@ -1,8 +1,8 @@ import _ from 'lodash'; -import React from 'react'; import {Formio} from 'react-formio'; import OpenFormsModule from 'formio/module'; +import {sleep} from 'utils'; import {iban, twoComponentForm} from './fixtures/iban'; @@ -10,78 +10,52 @@ import {iban, twoComponentForm} from './fixtures/iban'; Formio.use(OpenFormsModule); describe('IBAN Component', () => { - test('IBAN component validation', done => { + test.each([ + // valid values + ['BR15 0000 0000 0000 1093 2840 814 P2', true], + ['FR76 3000 6000 0112 3456 7890 189', true], + ['MU43 BOMM 0101 1234 5678 9101 000 MUR', true], + ['BR1500000000000010932840814P2', true], + ['FR76-3000-6000-0112-3456-7890-189', true], + // invalid values + ['BR15 0000 0000 0000 1093 2840 814 00', false], + ['FR76 3000 6000 0112 3456 7890 000', false], + ['MU43 BOMM 0101 1234 5678 9101 000 MUR 00', false], + ['BR150000000000001093284081400', false], + ['FR76-3000-6000-0112-3456-7890-000', false], + ])('IBAN component validation (%s, valid: %s)', async (value, valid) => { const formJSON = _.cloneDeep(iban); - const validValues = [ - 'BR15 0000 0000 0000 1093 2840 814 P2', - 'FR76 3000 6000 0112 3456 7890 189', - 'MU43 BOMM 0101 1234 5678 9101 000 MUR', - 'BR1500000000000010932840814P2', - 'FR76-3000-6000-0112-3456-7890-189', - ]; + const element = document.createElement('div'); - const invalidValues = [ - 'BR15 0000 0000 0000 1093 2840 814 00', - 'FR76 3000 6000 0112 3456 7890 000', - 'MU43 BOMM 0101 1234 5678 9101 000 MUR 00', - 'BR150000000000001093284081400', - 'FR76-3000-6000-0112-3456-7890-000', - ]; + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('iban'); + const changed = component.setValue(value); + expect(changed).toBeTruthy(); - const testValidity = (values, valid) => { - values.forEach(value => { - const element = document.createElement('div'); + await sleep(300); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('iban'); - const changed = component.setValue(value); - expect(changed).toBeTruthy(); - - setTimeout(() => { - if (valid) { - expect(!!component.error).toBeFalsy(); - } else { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Invalid IBAN'); - } - - if (value === invalidValues[4]) { - done(); - } - }, 300); - }) - .catch(done); - }); - }; - - testValidity(validValues, true); - testValidity(invalidValues, false); + if (valid) { + expect(!!component.error).toBeFalsy(); + } else { + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Invalid IBAN'); + } }); - test('IBAN validation not triggered by other components', done => { + test('IBAN validation not triggered by other components', async () => { const formJSON = _.cloneDeep(twoComponentForm); - const testValidity = () => { - const element = document.createElement('div'); - - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('name'); - const changed = component.setValue('John'); - expect(changed).toBeTruthy(); + const element = document.createElement('div'); - setTimeout(() => { - expect(!!component.error).toBeFalsy(); - done(); - }, 300); - }) - .catch(done); - }; + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('name'); + const changed = component.setValue('John'); + expect(changed).toBeTruthy(); - testValidity(); + await sleep(300); + expect(!!component.error).toBeFalsy(); }); }); diff --git a/src/jstests/formio/components/licenseplate.spec.js b/src/jstests/formio/components/licenseplate.spec.js index 37a40069e..25620c9da 100644 --- a/src/jstests/formio/components/licenseplate.spec.js +++ b/src/jstests/formio/components/licenseplate.spec.js @@ -3,6 +3,7 @@ import {Formio} from 'react-formio'; import LicensePlateField from 'formio/components/LicensePlateField'; import OpenFormsModule from 'formio/module'; +import {sleep} from 'utils'; import {licenseplate} from './fixtures/licenseplate'; @@ -10,45 +11,36 @@ import {licenseplate} from './fixtures/licenseplate'; Formio.use(OpenFormsModule); describe('License plate Component', () => { - test('License plate component validation', done => { + test.each([ + // valid values + ['GF-CP-51', true], + ['123-123-123', true], + ['J-206-FV', true], + // invalid values + ['123123123', false], + ['- - -', false], + ['abcabcabc', false], + ])('License plate component validation (%s, valid: %s)', async (value, valid) => { const formJSON = _.cloneDeep(licenseplate); const componentSchema = LicensePlateField.schema(); formJSON.components[0].validate.pattern = componentSchema.validate.pattern; formJSON.components[0].errors.pattern = componentSchema.errors.pattern; - const validValues = ['GF-CP-51', '123-123-123', 'J-206-FV']; - - const invalidValues = ['123123123', '- - -', 'abcabcabc']; - - const testValidity = (values, valid) => { - values.forEach(value => { - const element = document.createElement('div'); - - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('licenseplate'); - component.setValue(value); - - setTimeout(() => { - if (valid) { - expect(!!component.error).toBeFalsy(); - } else { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Invalid Dutch license plate'); - } - - if (value === invalidValues[2]) { - done(); - } - }, 300); - }) - .catch(done); - }); - }; - - testValidity(validValues, true); - testValidity(invalidValues, false); + const element = document.createElement('div'); + + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('licenseplate'); + component.setValue(value); + + await sleep(300); + + if (valid) { + expect(!!component.error).toBeFalsy(); + } else { + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Invalid Dutch license plate'); + } }); }); diff --git a/src/jstests/formio/components/map.spec.js b/src/jstests/formio/components/map.spec.js index 9f537ac07..fd4ba801f 100644 --- a/src/jstests/formio/components/map.spec.js +++ b/src/jstests/formio/components/map.spec.js @@ -23,16 +23,12 @@ const mapForm = { }; describe('Map component', () => { - test('Hidden map component', done => { + test('Hidden map component', async () => { let formJSON = _.cloneDeep(mapForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - done(); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); }); }); diff --git a/src/jstests/formio/components/number.spec.js b/src/jstests/formio/components/number.spec.js index 1d31f3aa4..ccfcc3fab 100644 --- a/src/jstests/formio/components/number.spec.js +++ b/src/jstests/formio/components/number.spec.js @@ -1,5 +1,4 @@ import _ from 'lodash'; -import React from 'react'; import {Formio} from 'react-formio'; import OpenFormsModule from 'formio/module'; @@ -10,26 +9,21 @@ import {numberForm} from './fixtures/number'; Formio.use(OpenFormsModule); describe('Number Component', () => { - test('#2903 - Emptying number component results in null value in data', done => { + test('#2903 - Emptying number component results in null value in data', async () => { let formJSON = _.cloneDeep(numberForm); const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('number'); - component.setValue(13); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('number'); + component.setValue(13); - expect(form._data['number']).toEqual(13); + expect(form._data['number']).toEqual(13); - component.dataValue = null; + component.dataValue = null; - // null, instead of undefined (default Formio behaviour which removes the key from the data) - expect(form._data['number']).toEqual(null); - - done(); - }) - .catch(done); + // null, instead of undefined (default Formio behaviour which removes the key from the data) + expect(form._data['number']).toEqual(null); }); }); diff --git a/src/jstests/formio/components/textfield.spec.js b/src/jstests/formio/components/textfield.spec.js index f16bb78f3..cb977c91e 100644 --- a/src/jstests/formio/components/textfield.spec.js +++ b/src/jstests/formio/components/textfield.spec.js @@ -4,6 +4,7 @@ import {Formio} from 'react-formio'; import {BASE_URL} from 'api-mocks'; import mswServer from 'api-mocks/msw-server'; import OpenFormsModule from 'formio/module'; +import {sleep} from 'utils'; import {addressPrefillForm} from './fixtures/textfield'; import {mockLocationGet} from './textfield.mocks'; @@ -12,28 +13,22 @@ import {mockLocationGet} from './textfield.mocks'; Formio.use(OpenFormsModule); describe('TextField Component', () => { - test('Address prefill city', done => { + test('Address prefill city', async () => { let formJSON = _.cloneDeep(addressPrefillForm); mswServer.use(mockLocationGet({city: 'Amsterdam', streetName: 'Beautiful Street'})); const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentCity = form.getComponent('city'); - componentCity.handleSettingLocationData({postcode: '1111AA', houseNumber: '1'}); - componentCity._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentCity.getValue()).toEqual('Amsterdam'); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentCity = form.getComponent('city'); + componentCity.handleSettingLocationData({postcode: '1111AA', houseNumber: '1'}); + componentCity._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentCity.getValue()).toEqual('Amsterdam'); }); - test('TextField (readonly) with address prefill refreshes city on invalid data', done => { + test('TextField (readonly) with address prefill refreshes city on invalid data', async () => { let formJSON = _.cloneDeep(addressPrefillForm); formJSON.components[2].disabled = true; formJSON.components[3].disabled = true; @@ -41,69 +36,51 @@ describe('TextField Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentCity = form.getComponent('city'); - componentCity.setValue('Amsterdam'); - - componentCity.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); - componentCity._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentCity.getValue()).toEqual(''); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentCity = form.getComponent('city'); + componentCity.setValue('Amsterdam'); + + componentCity.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); + componentCity._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentCity.getValue()).toEqual(''); }); - test('TextField (editable) with address prefill does not modify city if already filled', done => { + test('TextField (editable) with address prefill does not modify city if already filled', async () => { let formJSON = _.cloneDeep(addressPrefillForm); mswServer.use(mockLocationGet({})); const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentCity = form.getComponent('city'); - componentCity.setValue('Amsterdam'); - - componentCity.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); - componentCity.setLocationData; // access the getter so the debounced method is created - componentCity._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentCity.getValue()).toEqual('Amsterdam'); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentCity = form.getComponent('city'); + componentCity.setValue('Amsterdam'); + + componentCity.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); + componentCity.setLocationData; // access the getter so the debounced method is created + componentCity._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentCity.getValue()).toEqual('Amsterdam'); }); - test('Address prefill street', done => { + test('Address prefill street', async () => { let formJSON = _.cloneDeep(addressPrefillForm); mswServer.use(mockLocationGet({city: 'Amsterdam', streetName: 'Beautiful Street'})); const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentStreet = form.getComponent('streetName'); - componentStreet.handleSettingLocationData({postcode: '1111AA', houseNumber: '1'}); - componentStreet._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentStreet.getValue()).toEqual('Beautiful Street'); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentStreet = form.getComponent('streetName'); + componentStreet.handleSettingLocationData({postcode: '1111AA', houseNumber: '1'}); + componentStreet._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentStreet.getValue()).toEqual('Beautiful Street'); }); - test('TextField (readonly) with address prefill refreshes street on invalid data', done => { + test('TextField (readonly) with address prefill refreshes street on invalid data', async () => { let formJSON = _.cloneDeep(addressPrefillForm); formJSON.components[2].disabled = true; formJSON.components[3].disabled = true; @@ -111,44 +88,32 @@ describe('TextField Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentStreet = form.getComponent('streetName'); - componentStreet.setValue('Beautiful Street'); - - componentStreet.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); - componentStreet._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentStreet.getValue()).toEqual(''); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentStreet = form.getComponent('streetName'); + componentStreet.setValue('Beautiful Street'); + + componentStreet.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); + componentStreet._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentStreet.getValue()).toEqual(''); }); - test('TextField (editable) with address prefill (invalid data) does not modify street if already filled', done => { + test('TextField (editable) with address prefill (invalid data) does not modify street if already filled', async () => { let formJSON = _.cloneDeep(addressPrefillForm); mswServer.use(mockLocationGet({})); const element = document.createElement('div'); - Formio.createForm(element, formJSON, {baseUrl: BASE_URL}) - .then(form => { - form.setPristine(false); - const componentStreet = form.getComponent('streetName'); - componentStreet.setValue('Beautiful Street'); - - componentStreet.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); - componentStreet.setLocationData; // access the getter so the debounced method is created - componentStreet._debouncedSetLocationData.flush(); - - setTimeout(() => { - expect(componentStreet.getValue()).toEqual('Beautiful Street'); - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON, {baseUrl: BASE_URL}); + form.setPristine(false); + const componentStreet = form.getComponent('streetName'); + componentStreet.setValue('Beautiful Street'); + + componentStreet.handleSettingLocationData({postcode: '0000AA', houseNumber: '0'}); + componentStreet.setLocationData; // access the getter so the debounced method is created + componentStreet._debouncedSetLocationData.flush(); + await sleep(300); + expect(componentStreet.getValue()).toEqual('Beautiful Street'); }); }); diff --git a/src/jstests/formio/components/time.spec.js b/src/jstests/formio/components/time.spec.js index 384bd2477..5eb2ca6f2 100644 --- a/src/jstests/formio/components/time.spec.js +++ b/src/jstests/formio/components/time.spec.js @@ -134,7 +134,7 @@ describe('Time Component', () => { } ); - test('Time component with both min/max and max > min validation and custom error', done => { + test('Time component with both min/max and max > min validation and custom error', async () => { let formJSON = _.cloneDeep(timeForm); // Note: the backend dynamically updates the configuration so that `translatedErrors` are added to // `errors` in the correct language. @@ -148,24 +148,18 @@ describe('Time Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('time'); - const changed = component.setValue('10:00'); - expect(changed).toBeTruthy(); - - setTimeout(() => { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Custom error! Min time 12:00'); - - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('time'); + const changed = component.setValue('10:00'); + expect(changed).toBeTruthy(); + + await sleep(300); + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Custom error! Min time 12:00'); }); - test('Time component with both min/max and max < min validation and custom error', done => { + test('Time component with both min/max and max < min validation and custom error', async () => { let formJSON = _.cloneDeep(timeForm); // Note: the backend dynamically updates the configuration so that `translatedErrors` are added to // `errors` in the correct language. @@ -179,24 +173,18 @@ describe('Time Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('time'); - const changed = component.setValue('07:00'); - expect(changed).toBeTruthy(); - - setTimeout(() => { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Custom error! Min time: 08:00 Max time: 01:00.'); - - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('time'); + const changed = component.setValue('07:00'); + expect(changed).toBeTruthy(); + + await sleep(300); + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Custom error! Min time: 08:00 Max time: 01:00.'); }); - test('Time component with only min and custom error', done => { + test('Time component with only min and custom error', async () => { let formJSON = _.cloneDeep(timeForm); // Note: the backend dynamically updates the configuration so that `translatedErrors` are added to // `errors` in the correct language. @@ -209,24 +197,18 @@ describe('Time Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('time'); - const changed = component.setValue('10:00'); - expect(changed).toBeTruthy(); - - setTimeout(() => { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Custom error! Min time 13:00'); - - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('time'); + const changed = component.setValue('10:00'); + expect(changed).toBeTruthy(); + + await sleep(300); + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Custom error! Min time 13:00'); }); - test('Time component with only max and custom error', done => { + test('Time component with only max and custom error', async () => { let formJSON = _.cloneDeep(timeForm); // Note: the backend dynamically updates the configuration so that `translatedErrors` are added to // `errors` in the correct language. @@ -239,24 +221,18 @@ describe('Time Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('time'); - const changed = component.setValue('14:00'); - expect(changed).toBeTruthy(); - - setTimeout(() => { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('Custom error! Max time 13:00'); - - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('time'); + const changed = component.setValue('14:00'); + expect(changed).toBeTruthy(); + + await sleep(300); + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('Custom error! Max time 13:00'); }); - test('Time component with empty string error', done => { + test('Time component with empty string error', async () => { let formJSON = _.cloneDeep(timeForm); // Note: the backend dynamically updates the configuration so that `translatedErrors` are added to // `errors` in the correct language. @@ -267,20 +243,14 @@ describe('Time Component', () => { const element = document.createElement('div'); - Formio.createForm(element, formJSON) - .then(form => { - form.setPristine(false); - const component = form.getComponent('time'); - const changed = component.setValue('14:00'); - expect(changed).toBeTruthy(); - - setTimeout(() => { - expect(!!component.error).toBeTruthy(); - expect(component.error.message).toEqual('invalid_time'); - - done(); - }, 300); - }) - .catch(done); + const form = await Formio.createForm(element, formJSON); + form.setPristine(false); + const component = form.getComponent('time'); + const changed = component.setValue('14:00'); + expect(changed).toBeTruthy(); + + await sleep(300); + expect(!!component.error).toBeTruthy(); + expect(component.error.message).toEqual('invalid_time'); }); }); diff --git a/src/sdk.spec.jsx b/src/sdk.spec.jsx index f4a193b43..b3490828c 100644 --- a/src/sdk.spec.jsx +++ b/src/sdk.spec.jsx @@ -1,4 +1,4 @@ -import {act, waitFor, waitForElementToBeRemoved, within} from '@testing-library/react'; +import {act, waitFor, within} from '@testing-library/react'; import {BASE_URL, buildForm, mockAnalyticsToolConfigGet, mockFormGet} from 'api-mocks'; import mswServer from 'api-mocks/msw-server'; @@ -6,8 +6,8 @@ import {mockFormioTranslations, mockLanguageInfoGet} from 'components/LanguageSe import {OpenForm} from './sdk'; -// scrollIntoView is not supported in Jest -let scrollIntoViewMock = jest.fn(); +// scrollIntoView is not supported in jest-dom +let scrollIntoViewMock = vi.fn(); window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; const LANGUAGES = [ @@ -123,7 +123,7 @@ describe('OpenForm', () => { mswServer.use(...apiMocks); const formRoot = document.createElement('div'); const target = document.createElement('div'); - const onLanguageChangeMock = jest.fn(); + const onLanguageChangeMock = vi.fn(); const form = new OpenForm(formRoot, { baseUrl: BASE_URL,