diff --git a/.eslintrc b/.eslintrc index c08354567fc..519066c9cad 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,7 @@ "plugin:react-hooks/recommended", "plugin:react/jsx-runtime" ], - "plugins": ["@18f/eslint-plugin-identity"], + "plugins": ["@18f/eslint-plugin-identity", "testing-library"], "env": { "browser": true, "commonjs": true @@ -20,10 +20,6 @@ { "selector": "AssignmentExpression[left.property.name='href'][right.type=/(Template)?Literal/]", "message": "Do not assign window.location.href to a string or string template to avoid losing i18n parameters" - }, - { - "selector": "ExpressionStatement[expression.callee.object.name='userEvent']", - "message": "Await the promised result of a userEvent interaction" } ] }, @@ -44,11 +40,17 @@ "devDependencies": true, "packageDir": "." } - ] + ], + "testing-library/await-async-events": "error", + "testing-library/await-async-queries": "error", + "testing-library/await-async-utils": "error", + "testing-library/no-await-sync-events": "error", + "testing-library/no-await-sync-queries": "error", + "testing-library/no-debugging-utils": "error" } }, { - // Turn off react linting rules for most packages/files + // Turn off react linting rules for most packages/files "files": [ "spec/**", "app/javascript/packs/**", @@ -63,8 +65,8 @@ "app/javascript/packages/validated-field/**", "app/javascript/packages/verify-flow/**", // In progress: enabling these rules for all files in packages/document-capture - "app/javascript/packages/document-capture/context/**", - "app/javascript/packages/document-capture/higher-order/**", + "app/javascript/packages/document-capture/context/**", + "app/javascript/packages/document-capture/higher-order/**", "app/javascript/packages/document-capture/hooks/**", // Comment out a file to enable react lint rules for that file only "app/javascript/packages/document-capture/components/acuant-camera.tsx", @@ -113,6 +115,6 @@ "react-hooks/exhaustive-deps": "off", "react-hooks/rules-of-hooks": "off" } - }, + } ] -} \ No newline at end of file +} diff --git a/app/javascript/packages/address-search/components/address-search.spec.tsx b/app/javascript/packages/address-search/components/address-search.spec.tsx index 2ea8c677245..5b053153b69 100644 --- a/app/javascript/packages/address-search/components/address-search.spec.tsx +++ b/app/javascript/packages/address-search/components/address-search.spec.tsx @@ -9,7 +9,7 @@ describe('AddressSearch', () => { const locationsURL = 'https://localhost:3000/locations/endpoint'; context('Page Heading and PO Search About Message', () => { - it('both render when handleLocationSelect is not null', async () => { + it('both render when handleLocationSelect is not null', () => { const handleLocationsFound = sandbox.stub(); const onSelect = sinon.stub(); const { queryByText, queryByRole } = render( @@ -25,8 +25,8 @@ describe('AddressSearch', () => { , ); - const heading = await queryByText('in_person_proofing.headings.po_search.location'); - const aboutMessage = await queryByText( + const heading = queryByText('in_person_proofing.headings.po_search.location'); + const aboutMessage = queryByText( 'in_person_proofing.body.location.po_search.po_search_about', ); @@ -37,7 +37,7 @@ describe('AddressSearch', () => { ).to.exist(); }); - it('both do not render when handleLocationSelect is null', async () => { + it('both do not render when handleLocationSelect is null', () => { const handleLocationsFound = sandbox.stub(); const onSelect = sinon.stub(); const { queryByText } = render( @@ -53,8 +53,8 @@ describe('AddressSearch', () => { , ); - const heading = await queryByText('in_person_proofing.headings.po_search.location'); - const aboutMessage = await queryByText( + const heading = queryByText('in_person_proofing.headings.po_search.location'); + const aboutMessage = queryByText( 'in_person_proofing.body.location.po_search.po_search_about', ); expect(heading).to.be.empty; diff --git a/app/javascript/packages/address-search/components/full-address-search.spec.tsx b/app/javascript/packages/address-search/components/full-address-search.spec.tsx index 91c3cbcd2d4..94be94460d6 100644 --- a/app/javascript/packages/address-search/components/full-address-search.spec.tsx +++ b/app/javascript/packages/address-search/components/full-address-search.spec.tsx @@ -14,7 +14,7 @@ describe('FullAddressSearch', () => { const usStatesTerritories = [['Delware', 'DE']]; context('Page Heading and PO Search About Message', () => { - it('both render when handleLocationSelect is not null', async () => { + it('both render when handleLocationSelect is not null', () => { const handleLocationsFound = sandbox.stub(); const onSelect = sinon.stub(); const { queryByText, queryByRole } = render( @@ -30,8 +30,8 @@ describe('FullAddressSearch', () => { , ); - const heading = await queryByText('in_person_proofing.headings.po_search.location'); - const aboutMessage = await queryByText( + const heading = queryByText('in_person_proofing.headings.po_search.location'); + const aboutMessage = queryByText( 'in_person_proofing.body.location.po_search.po_search_about', ); @@ -42,7 +42,7 @@ describe('FullAddressSearch', () => { ).to.exist(); }); - it('both do not render when handleLocationSelect is null', async () => { + it('both do not render when handleLocationSelect is null', () => { const handleLocationsFound = sandbox.stub(); const { queryByText } = render( new Map() }}> @@ -57,8 +57,8 @@ describe('FullAddressSearch', () => { , ); - const heading = await queryByText('in_person_proofing.headings.po_search.location'); - const aboutMessage = await queryByText( + const heading = queryByText('in_person_proofing.headings.po_search.location'); + const aboutMessage = queryByText( 'in_person_proofing.body.location.po_search.po_search_about', ); expect(heading).to.be.empty; @@ -67,7 +67,7 @@ describe('FullAddressSearch', () => { }); context('Address Search Label Text', () => { - it('does not render when handleLocationSelect is not null', async () => { + it('does not render when handleLocationSelect is not null', () => { const handleLocationsFound = sandbox.stub(); const onSelect = sinon.stub(); const { queryByText } = render( @@ -83,13 +83,11 @@ describe('FullAddressSearch', () => { , ); - const searchLabel = await queryByText( - 'in_person_proofing.headings.po_search.address_search_label', - ); + const searchLabel = queryByText('in_person_proofing.headings.po_search.address_search_label'); expect(searchLabel).to.be.empty; }); - it('renders when handleLocationSelect is null', async () => { + it('renders when handleLocationSelect is null', () => { const handleLocationsFound = sandbox.stub(); const { queryByText } = render( new Map() }}> @@ -104,7 +102,7 @@ describe('FullAddressSearch', () => { , ); - const searchLabel = await queryByText( + const searchLabel = queryByText( 'in_person_proofing.body.location.po_search.address_search_label', ); expect(searchLabel).to.exist(); diff --git a/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx index 1771ad0fa3f..e82f66c621f 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-full-address-entry-post-office-search-step.spec.tsx @@ -44,9 +44,11 @@ const DEFAULT_PROPS = { describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { const usStatesTerritories: [string, string][] = [['Delware', 'DE']]; const locationsURL = 'https://localhost:3000/locations/endpoint'; + const inPersonURL = '#in_person'; const wrapper: ComponentType = ({ children }) => ( { beforeEach(() => { server.resetHandlers(); // todo: should we return USPS_RESPONSE here? - server.use(rest.post(locationsURL, (_req, res, ctx) => res(ctx.json([{ name: 'Baltimore' }])))); + server.use( + rest.post(locationsURL, (_req, res, ctx) => res(ctx.json([{ name: 'Baltimore' }]))), + rest.put(locationsURL, (_req, res, ctx) => res(ctx.json({ success: true }))), + ); }); it('renders the step', () => { @@ -326,8 +331,11 @@ describe('InPersonLocationFullAddressEntryPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.zipcode_label'), ); - await userEvent.click(findAllByText('in_person_proofing.body.location.location_button')[0]); + await userEvent.click( + (await findAllByText('in_person_proofing.body.location.location_button'))[0], + ); - expect(await queryByText('simple_form.required.text')).to.be.null(); + expect(queryByText('simple_form.required.text')).to.be.null(); + expect(window.location.hash).to.equal(inPersonURL); }); }); diff --git a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx index 8b4472f3e1e..d5f62a77f4f 100644 --- a/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx +++ b/app/javascript/packages/document-capture/components/in-person-location-post-office-search-step.spec.tsx @@ -60,9 +60,11 @@ describe('InPersonLocationPostOfficeSearchStep', () => { const usStatesTerritories: [string, string][] = [['Delware', 'DE']]; const locationsURL = 'https://localhost:3000/locations/endpoint'; const addressSearchURL = 'https://localhost:3000/addresses/endpoint'; + const inPersonURL = '#in_person'; const wrapper: ComponentType = ({ children }) => ( { await userEvent.click( await findByText('in_person_proofing.body.location.po_search.search_button'), ); - const moreResults = await queryAllByText('in_person_proofing.body.location.location_button'); + const moreResults = queryAllByText('in_person_proofing.body.location.location_button'); expect(moreResults).to.be.empty(); }); @@ -366,6 +368,7 @@ describe('InPersonLocationPostOfficeSearchStep', () => { res(ctx.json(DEFAULT_RESPONSE), ctx.status(200)), ), rest.post(locationsURL, (_req, res, ctx) => res(ctx.json([{ name: 'Baltimore' }]))), + rest.put(locationsURL, (_req, res, ctx) => res(ctx.json({ success: true }))), ); }); @@ -387,9 +390,12 @@ describe('InPersonLocationPostOfficeSearchStep', () => { await findByLabelText('in_person_proofing.body.location.po_search.address_search_label'), ); - await userEvent.click(findAllByText('in_person_proofing.body.location.location_button')[0]); + await userEvent.click( + (await findAllByText('in_person_proofing.body.location.location_button'))[0], + ); - expect(await queryByText('in_person_proofing.body.location.inline_error')).to.be.null(); + expect(queryByText('in_person_proofing.body.location.inline_error')).to.be.null(); + expect(window.location.hash).to.equal(inPersonURL); }); }); }); diff --git a/app/javascript/packages/form-steps/form-steps.spec.tsx b/app/javascript/packages/form-steps/form-steps.spec.tsx index 1f2c4c00a78..6877d2c6d2e 100644 --- a/app/javascript/packages/form-steps/form-steps.spec.tsx +++ b/app/javascript/packages/form-steps/form-steps.spec.tsx @@ -405,7 +405,7 @@ describe('FormSteps', () => { await userEvent.click(getByText(t('forms.buttons.continue'))); - await expect(findByText('Second Title')).to.be.fulfilled(); + await findByText('Second Title'); await expect(checkFormHasExpectedErrors()).to.be.rejected(); window.history.back(); diff --git a/package.json b/package.json index 1277c05089e..c5c38767672 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^6.2.0", "jsdom": "^22.1.0", "mocha": "^10.0.0", "mq-polyfill": "^1.1.8", diff --git a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx index 0a140ae24be..6ab8b14f2d7 100644 --- a/spec/javascript/packages/document-capture/components/document-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/document-capture-spec.jsx @@ -137,9 +137,10 @@ describe('document-capture/components/document-capture', () => { submitButton = getByText('forms.buttons.submit.default'); expect(isFormValid(submitButton.closest('form'))).to.be.true(); + // eslint-disable-next-line require-await await new Promise((resolve) => { onSubmit.callsFake(resolve); - // eslint-disable-next-line no-restricted-syntax + // eslint-disable-next-line testing-library/await-async-events userEvent.click(submitButton); }); diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx index e7eac8637d1..dd5a2467c9d 100644 --- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx @@ -409,7 +409,7 @@ describe('document-capture/components/review-issues-step', () => { }); it('skip renders initially with warning page when failed image is submitted again', () => { - const { findByRole, getByRole, getByText } = render( + const { queryByRole, getByRole, getByText } = render( { , ); - expect(findByRole('button', { name: 'idv.failure.button.warning' })).not.to.exist; - expect(getByRole('heading', { name: 'doc_auth.headings.review_issues' })).to.be.ok; - expect(getByText('duplicate image')).to.be.ok; + expect(queryByRole('button', { name: 'idv.failure.button.warning' })).to.not.exist(); + expect(getByRole('heading', { name: 'doc_auth.headings.review_issues' })).to.be.ok(); + expect(getByText('duplicate image')).to.be.ok(); }); context('ial2 strict', () => { diff --git a/yarn.lock b/yarn.lock index a468f2ad812..6725ce7331a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1572,10 +1572,10 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== -"@types/semver@^7.5.0": - version "7.5.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" - integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== +"@types/semver@^7.3.12", "@types/semver@^7.5.0": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== "@types/serve-index@^1.9.1": version "1.9.1" @@ -1685,6 +1685,14 @@ "@typescript-eslint/visitor-keys" "6.7.5" debug "^4.3.4" +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + "@typescript-eslint/scope-manager@6.7.5": version "6.7.5" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz#1cf33b991043886cd67f4f3600b8e122fc14e711" @@ -1703,11 +1711,29 @@ debug "^4.3.4" ts-api-utils "^1.0.1" +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + "@typescript-eslint/types@6.7.5": version "6.7.5" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.5.tgz#4571320fb9cf669de9a95d9849f922c3af809790" integrity sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ== +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/typescript-estree@6.7.5": version "6.7.5" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz#4578de1a26e9f24950f029a4f00d1bfe41f15a39" @@ -1734,6 +1760,28 @@ "@typescript-eslint/typescript-estree" "6.7.5" semver "^7.5.4" +"@typescript-eslint/utils@^5.58.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + "@typescript-eslint/visitor-keys@6.7.5": version "6.7.5" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz#84c68d6ceb5b12d5246b918b84f2b79affd6c2f1" @@ -3260,7 +3308,14 @@ eslint-plugin-react@^7.31.8: semver "^6.3.0" string.prototype.matchall "^4.0.7" -eslint-scope@5.1.1: +eslint-plugin-testing-library@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz#af3340b783c881eb19ec5ac6b3a4bfe8ab4a1f74" + integrity sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw== + dependencies: + "@typescript-eslint/utils" "^5.58.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -6019,7 +6074,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.5.4: +semver@^7.3.4, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -6661,11 +6716,23 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"