diff --git a/CHANGELOG.md b/CHANGELOG.md index 044f9f6dd..09fbefb17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * Fix field collision in the record generation. Fixes [UILD-838]. * Update language used for importing works/instances. Refs [UILD-839]. * Fix multiple settings per profile. Refs [UILD-780]. +* Add accessibility tests. Refs [UILD-841]. [UILD-744]:https://folio-org.atlassian.net/browse/UILD-744 [UILD-816]:https://folio-org.atlassian.net/browse/UILD-816 @@ -23,6 +24,7 @@ [UILD-838]:https://folio-org.atlassian.net/browse/UILD-838 [UILD-839]:https://folio-org.atlassian.net/browse/UILD-839 [UILD-780]:https://folio-org.atlassian.net/browse/UILD-780 +[UILD-841]:https://folio-org.atlassian.net/browse/UILD-841 ## 2.0.4 (2026-06-03) * Fix default profile type persistence across edit form and profile settings. Fixes [UILD-820]. diff --git a/package-lock.json b/package-lock.json index 5750244b0..d1c605f58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@testing-library/user-event": "^14.6.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/jest": "^30.0.0", + "@types/jest-axe": "^3.5.9", "@types/lodash": "^4.17.14", "@types/node": "^26.1.1", "@types/react": "^19.2.2", @@ -57,6 +58,7 @@ "husky": "^9.1.7", "identity-obj-proxy": "^3.0.0", "jest": "^30.3.0", + "jest-axe": "^10.0.0", "jest-environment-jsdom": "^30.3.0", "jest-fetch-mock": "^3.0.3", "lint-staged": "^16.4.0", @@ -837,10 +839,33 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -2419,6 +2444,17 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -3161,6 +3197,17 @@ "pretty-format": "^30.0.0" } }, + "node_modules/@types/jest-axe": { + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@types/jest-axe/-/jest-axe-3.5.9.tgz", + "integrity": "sha512-z98CzR0yVDalCEuhGXXO4/zN4HHuSebAukXDjTLJyjEAgoUf1H1i+sr7SUB/mz8CRS/03/XChsx0dcLjHkndoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jest": "*", + "axe-core": "^3.5.5" + } + }, "node_modules/@types/jest/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -4239,6 +4286,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.6.tgz", + "integrity": "sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/babel-jest": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", @@ -5137,6 +5194,16 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7492,6 +7559,119 @@ } } }, + "node_modules/jest-axe": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jest-axe/-/jest-axe-10.0.0.tgz", + "integrity": "sha512-9QR0M7//o5UVRnEUUm68IsGapHrcKGakYy9dKWWMX79LmeUKguDI6DREyljC5I13j78OUmtKLF5My6ccffLFBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "axe-core": "4.10.2", + "chalk": "4.1.2", + "jest-matcher-utils": "29.2.2", + "lodash.merge": "4.6.2" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/jest-axe/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-axe/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-axe/node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-axe/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/jest-matcher-utils": { + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-axe/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-changed-files": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", @@ -7862,6 +8042,16 @@ "promise-polyfill": "^8.1.3" } }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-haste-map": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", diff --git a/package.json b/package.json index 173cb4e55..316fff4d5 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@testing-library/user-event": "^14.6.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/jest": "^30.0.0", + "@types/jest-axe": "^3.5.9", "@types/lodash": "^4.17.14", "@types/node": "^26.1.1", "@types/react": "^19.2.2", @@ -82,6 +83,7 @@ "husky": "^9.1.7", "identity-obj-proxy": "^3.0.0", "jest": "^30.3.0", + "jest-axe": "^10.0.0", "jest-environment-jsdom": "^30.3.0", "jest-fetch-mock": "^3.0.3", "lint-staged": "^16.4.0", diff --git a/src/components/Dropzone/Drop.tsx b/src/components/Dropzone/Drop.tsx index eb5c898cb..37b5ebc9a 100644 --- a/src/components/Dropzone/Drop.tsx +++ b/src/components/Dropzone/Drop.tsx @@ -19,13 +19,13 @@ export const Drop: FC = ({ getRootProps, getInputProps, isDragActive }) = {...getRootProps()} data-testid="dropzone" > - + {isDragActive ? (
) : ( -
+
}], + ['label as ReactElement', { label: Custom Label }], + ])('has no accessibility violations when %s', async (_description, props) => { + setInitialGlobalState([ + { + store: useUIStore, + state: { + isSearchPaneCollapsed: false, + setIsSearchPaneCollapsed: mockSetIsSearchPaneCollapsed, + }, + }, + ]); + + const { container } = renderWithIntl(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when pane is collapsed', async () => { + setInitialGlobalState([ + { + store: useUIStore, + state: { + isSearchPaneCollapsed: true, + setIsSearchPaneCollapsed: mockSetIsSearchPaneCollapsed, + }, + }, + ]); + + const { container } = renderWithIntl(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControlPane/SearchControlPane.test.tsx b/src/features/search/ui/components/SearchControlPane/SearchControlPane.test.tsx index ec50aac56..a07f70d7f 100644 --- a/src/features/search/ui/components/SearchControlPane/SearchControlPane.test.tsx +++ b/src/features/search/ui/components/SearchControlPane/SearchControlPane.test.tsx @@ -1,6 +1,9 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; +import { ComponentProps } from 'react'; + import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore, useUIStore } from '@/store'; @@ -379,4 +382,86 @@ describe('SearchControlPane', () => { const formattedMessage = screen.getByTestId('formatted-message-ld.recordsFound'); expect(formattedMessage).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['with label', { componentProps: { label: 'Test Label' } }], + ['with ReactElement label', { componentProps: { label:
Custom Label
} }], + ['with children', { componentProps: { label: 'Test', children:
Child Content
} }], + [ + 'with sublabel shown', + { + contextOverride: { + activeUIConfig: { + ui: { titleId: undefined, subtitleId: undefined }, + features: { isVisibleSubLabel: false }, + }, + results: { totalRecords: 25 }, + }, + componentProps: { + label: 'Test', + showSubLabel: true, + renderSubLabel: (count: number) => Total: {count}, + }, + }, + ], + ['without label', { componentProps: {} }], + [ + 'with context titleId', + { + contextOverride: { + activeUIConfig: { + ui: { titleId: 'ld.resources', subtitleId: undefined }, + features: { isVisibleSubLabel: false }, + }, + }, + componentProps: {}, + }, + ], + [ + 'with subtitle from context', + { + contextOverride: { + activeUIConfig: { + ui: { titleId: 'ld.resources', subtitleId: 'ld.recordsFound' }, + features: { isVisibleSubLabel: true }, + }, + results: { totalRecords: 42 }, + }, + componentProps: {}, + }, + ], + ] as [ + string, + { contextOverride?: Record; componentProps: ComponentProps }, + ][])('has no accessibility violations when %s', async (_description, { contextOverride, componentProps }) => { + if (contextOverride) { + mockUseSearchContext.mockReturnValue(contextOverride); + } + + setInitialGlobalState([ + { + store: useSearchStore, + state: { + pageMetadata: { + totalElements: 25, + }, + }, + }, + { + store: useUIStore, + state: { + isSearchPaneCollapsed: false, + setIsSearchPaneCollapsed: mockSetIsSearchPaneCollapsed, + }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/InputsWrapper.test.tsx b/src/features/search/ui/components/SearchControls/InputsWrapper.test.tsx index c1d109359..a826bf01b 100644 --- a/src/features/search/ui/components/SearchControls/InputsWrapper.test.tsx +++ b/src/features/search/ui/components/SearchControls/InputsWrapper.test.tsx @@ -2,6 +2,9 @@ import { render, screen } from '@testing-library/react'; import { InputsWrapper } from './InputsWrapper'; +// No direct accessibility test. Children are tested for this, and the +// wrapper itself does not include any elements affecting accessibility. + jest.mock('../../providers/SearchProvider', () => ({ useSearchContext: () => ({ mode: 'auto', diff --git a/src/features/search/ui/components/SearchControls/MetaControls.test.tsx b/src/features/search/ui/components/SearchControls/MetaControls.test.tsx index 7478f406f..85be5150a 100644 --- a/src/features/search/ui/components/SearchControls/MetaControls.test.tsx +++ b/src/features/search/ui/components/SearchControls/MetaControls.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useUIStore } from '@/store'; @@ -201,4 +202,45 @@ describe('MetaControls', () => { const wrapper = container.querySelector('.meta-controls'); expect(wrapper?.tagName).toBe('DIV'); }); + + describe('accessibility', () => { + test.each([ + ['advanced search closed', { isAdvancedSearchOpen: false, setIsAdvancedSearchOpen: mockSetIsAdvancedSearchOpen }], + ['advanced search open', { isAdvancedSearchOpen: true, setIsAdvancedSearchOpen: mockSetIsAdvancedSearchOpen }], + ])('has no accessibility violations when %s', async (_description, state) => { + setInitialGlobalState([ + { + store: useUIStore, + state, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when opens advanced search modal button is clicked', async () => { + setInitialGlobalState([ + { + store: useUIStore, + state: { + isAdvancedSearchOpen: false, + setIsAdvancedSearchOpen: mockSetIsAdvancedSearchOpen, + }, + }, + ]); + + const { container } = render(); + + const advancedButton = screen.getByText('ld.advanced'); + fireEvent.click(advancedButton); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/QueryInput.test.tsx b/src/features/search/ui/components/SearchControls/QueryInput.test.tsx index 0067c6ce2..fffa819bb 100644 --- a/src/features/search/ui/components/SearchControls/QueryInput.test.tsx +++ b/src/features/search/ui/components/SearchControls/QueryInput.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore } from '@/store'; @@ -165,4 +166,24 @@ describe('QueryInput', () => { const input = screen.getByTestId('id-search-input'); expect(input).toHaveAttribute('aria-label'); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { + query: 'test query', + setQuery: jest.fn(), + }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/ResetButton.test.tsx b/src/features/search/ui/components/SearchControls/ResetButton.test.tsx index bc47fa742..231a76211 100644 --- a/src/features/search/ui/components/SearchControls/ResetButton.test.tsx +++ b/src/features/search/ui/components/SearchControls/ResetButton.test.tsx @@ -3,6 +3,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore } from '@/store'; @@ -224,4 +225,32 @@ describe('ResetButton', () => { const button = screen.getByTestId('id-search-reset-button'); expect(button).toHaveAttribute('aria-label'); }); + + describe('accessibility', () => { + test.each([ + ['query exists', 'test query', ['/']], + ['query is empty', '', ['/']], + ['advanced search is active', '', ['/?query=title+adj+value']], + ['query empty and no advanced search active', '', ['/?query=some+query&searchBy=title']], + ])('has no accessibility violations when %s', async (_description, query, initialEntries) => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { + query, + }, + }, + ]); + + const { container } = render( + + + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/RootControls.test.tsx b/src/features/search/ui/components/SearchControls/RootControls.test.tsx index 733a45c55..ebfb3a4cb 100644 --- a/src/features/search/ui/components/SearchControls/RootControls.test.tsx +++ b/src/features/search/ui/components/SearchControls/RootControls.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useUIStore } from '@/store'; @@ -257,4 +258,32 @@ describe('RootControls', () => { expect(container.querySelector('.search-pane-content')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['not collapsed', false, {}], + ['collapsed', true, {}], + [ + 'showFilters with filtersComponent', + false, + { showFilters: true, filtersComponent:
Filters
}, + ], + ])('has no accessibility violations when %s', async (_description, isSearchPaneCollapsed, props) => { + setInitialGlobalState([ + { + store: useUIStore, + state: { + isSearchPaneCollapsed, + setIsSearchPaneCollapsed: mockSetIsSearchPaneCollapsed, + }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/SearchBySelect.test.tsx b/src/features/search/ui/components/SearchControls/SearchBySelect.test.tsx index f04521321..1c244bb6f 100644 --- a/src/features/search/ui/components/SearchControls/SearchBySelect.test.tsx +++ b/src/features/search/ui/components/SearchControls/SearchBySelect.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore } from '@/store'; @@ -131,4 +132,27 @@ describe('SearchBySelect', () => { const select = screen.getByTestId('id-search-select'); expect(select).toHaveAttribute('id', 'id-search-select'); }); + + describe('accessibility', () => { + test.each([ + ['searchBy is "keyword"', 'keyword'], + ['searchBy is "title"', 'title'], + ])('has no accessibility violations when %s', async (_description, searchBy) => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { + searchBy, + setSearchBy: jest.fn(), + }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/Segment.test.tsx b/src/features/search/ui/components/SearchControls/Segment.test.tsx index 8c716e509..f04ebe228 100644 --- a/src/features/search/ui/components/SearchControls/Segment.test.tsx +++ b/src/features/search/ui/components/SearchControls/Segment.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchParam } from '@/features/search/core'; @@ -243,4 +244,40 @@ describe('Segment', () => { expect(mockOnSegmentChange).not.toHaveBeenCalled(); }); }); + + describe('accessibility', () => { + test.each([ + ['renders button with derived label from path', '', {}, { path: 'resources' }], + ['renders button with custom labelId', '', {}, { path: 'resources', labelId: 'custom.label' }], + [ + 'renders custom children instead of label', + '', + {}, + { path: 'resources', children: Custom Content }, + ], + ['derives label from composite path', '', {}, { path: 'authorities:search' }], + ['active segment is highlighted', 'resources', { [SearchParam.SEGMENT]: 'resources' }, { path: 'resources' }], + ])( + 'has no accessibility violations when %s', + async (_description, currentSegment, navigationStateOverrides, props) => { + mockCurrentSegment = currentSegment; + setInitialGlobalState([ + { + store: useSearchStore, + state: { navigationState: navigationStateOverrides, setNavigationState }, + }, + ]); + + const { container } = render( +
+ +
, + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }, + ); + }); }); diff --git a/src/features/search/ui/components/SearchControls/SegmentContent.test.tsx b/src/features/search/ui/components/SearchControls/SegmentContent.test.tsx index 7dfa546ba..6094d47cc 100644 --- a/src/features/search/ui/components/SearchControls/SegmentContent.test.tsx +++ b/src/features/search/ui/components/SearchControls/SegmentContent.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchParam } from '@/features/search/core'; @@ -193,4 +194,38 @@ describe('SegmentContent', () => { expect(container.firstChild).toBeNull(); }); }); + + describe('accessibility', () => { + test.each([ + ['segment matches exactly', 'resources', { [SearchParam.SEGMENT]: 'resources' }, { segment: 'resources' }], + [ + 'prefix matches', + 'authorities:search', + { [SearchParam.SEGMENT]: 'authorities:search' }, + { segment: 'authorities', matchPrefix: true }, + ], + ['segment does not match', 'authorities', { [SearchParam.SEGMENT]: 'authorities' }, { segment: 'resources' }], + ])( + 'has no accessibility violations when %s', + async (_description, currentSegment, navigationStateOverrides, props) => { + mockCurrentSegment = currentSegment; + setInitialGlobalState([ + { + store: useSearchStore, + state: { navigationState: navigationStateOverrides, setNavigationState }, + }, + ]); + + const { container } = render( + +
Content
+
, + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }, + ); + }); }); diff --git a/src/features/search/ui/components/SearchControls/SegmentGroup.test.tsx b/src/features/search/ui/components/SearchControls/SegmentGroup.test.tsx index 7fcb36c51..c27de5e85 100644 --- a/src/features/search/ui/components/SearchControls/SegmentGroup.test.tsx +++ b/src/features/search/ui/components/SearchControls/SegmentGroup.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchParam } from '@/features/search/core'; @@ -149,4 +150,44 @@ describe('SegmentGroup', () => { expect(group).toBeInTheDocument(); expect(group?.classList).toContain('search-segments'); }); + + describe('accessibility', () => { + test.each([ + ['no parentPath specified', '', {}, {}], + [ + 'current segment matches parent path', + 'authorities', + { [SearchParam.SEGMENT]: 'authorities' }, + { parentPath: 'authorities' }, + ], + [ + 'current segment does not match parent path', + 'resources', + { [SearchParam.SEGMENT]: 'resources' }, + { parentPath: 'authorities' }, + ], + ['custom className provided', '', {}, { className: 'custom-class' }], + ])( + 'has no accessibility violations when %s', + async (_description, currentSegment, navigationStateOverrides, props) => { + mockCurrentSegment = currentSegment; + setInitialGlobalState([ + { + store: useSearchStore, + state: { navigationState: navigationStateOverrides, setNavigationState }, + }, + ]); + + const { container } = render( + + + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }, + ); + }); }); diff --git a/src/features/search/ui/components/SearchControls/SourceSelector.test.tsx b/src/features/search/ui/components/SearchControls/SourceSelector.test.tsx index 40642ea2e..2c8f51ba1 100644 --- a/src/features/search/ui/components/SearchControls/SourceSelector.test.tsx +++ b/src/features/search/ui/components/SearchControls/SourceSelector.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchParam } from '@/features/search/core'; @@ -254,4 +255,41 @@ describe('SourceSelector', () => { expect(() => fireEvent.click(externalRadio)).not.toThrow(); }); }); + + describe('accessibility', () => { + test.each([ + ['default props', {}, { options: mockOptions, defaultValue: 'local' }], + [ + 'custom accordion props', + {}, + { + options: mockOptions, + defaultValue: 'local', + accordionId: 'custom-id', + accordionTitleId: 'custom.title', + groupId: 'custom-group', + }, + ], + ['no defaultValue provided', {}, { options: mockOptions }], + [ + 'navigationState source overrides defaultValue', + { [SearchParam.SOURCE]: 'external' }, + { options: mockOptions, defaultValue: 'local' }, + ], + ['empty options array', {}, { options: [], defaultValue: 'local' }], + ])('has no accessibility violations when %s', async (_description, navigationState, props) => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { navigationState }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchControls/SubmitButton.test.tsx b/src/features/search/ui/components/SearchControls/SubmitButton.test.tsx index d86a90fd2..552775c46 100644 --- a/src/features/search/ui/components/SearchControls/SubmitButton.test.tsx +++ b/src/features/search/ui/components/SearchControls/SubmitButton.test.tsx @@ -1,6 +1,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore } from '@/store'; @@ -119,4 +120,26 @@ describe('SubmitButton', () => { const button = screen.getByTestId('id-search-button'); expect(button).toHaveClass('search-button', 'primary-search'); }); + + describe('accessibility', () => { + test.each([ + ['query exists', 'test query'], + ['query is empty', ''], + ])('has no accessibility violations when %s', async (_description, query) => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { + query, + }, + }, + ]); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchEmptyPlaceholder/SearchEmptyPlaceholder.test.tsx b/src/features/search/ui/components/SearchEmptyPlaceholder/SearchEmptyPlaceholder.test.tsx index 741835b59..63d41424c 100644 --- a/src/features/search/ui/components/SearchEmptyPlaceholder/SearchEmptyPlaceholder.test.tsx +++ b/src/features/search/ui/components/SearchEmptyPlaceholder/SearchEmptyPlaceholder.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchEmptyPlaceholder } from './SearchEmptyPlaceholder'; @@ -38,4 +39,19 @@ describe('SearchEmptyPlaceholder', () => { const placeholderDiv = container.querySelector('.empty-placeholder'); expect(placeholderDiv).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['no props', {}], + ['custom labelId', { labelId: 'ld.enterSearchCriteria' }], + ['custom className', { className: 'custom-class' }], + ['labelId and className', { labelId: 'ld.noResults', className: 'custom-empty' }], + ])('has no accessibility violations when %s', async (_description, props) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchPagination/SearchPagination.test.tsx b/src/features/search/ui/components/SearchPagination/SearchPagination.test.tsx index 7d2d9756f..1fd30dd68 100644 --- a/src/features/search/ui/components/SearchPagination/SearchPagination.test.tsx +++ b/src/features/search/ui/components/SearchPagination/SearchPagination.test.tsx @@ -1,6 +1,7 @@ import { BrowserRouter } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as UseCommittedSearchParams from '../../hooks/useCommittedSearchParams'; import * as SearchProvider from '../../providers/SearchProvider'; @@ -152,4 +153,28 @@ describe('SearchPagination', () => { expect(screen.getByTestId('pagination')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['data exists', {}, {}, {}], + [ + 'offset and data variant', + { results: { items: [], totalRecords: 50, pageMetadata: { totalElements: 50, totalPages: 5 } } }, + { offset: 20 }, + {}, + ], + ['custom showCount prop', {}, {}, { showCount: false }], + ['custom isLooped prop', {}, {}, { isLooped: true }], + ['pageMetadata is null', { results: { items: [], totalRecords: 0, pageMetadata: null } }, {}, {}], + ])('has no accessibility violations when %s', async (_description, contextOverrides, paramsOverrides, props) => { + mockUseSearchContext(contextOverrides); + mockUseCommittedSearchParams(paramsOverrides); + + const { container } = renderWithRouter(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/SearchResults/SearchResultsRoot.test.tsx b/src/features/search/ui/components/SearchResults/SearchResultsRoot.test.tsx index 2d7d8c0ee..425c6af9f 100644 --- a/src/features/search/ui/components/SearchResults/SearchResultsRoot.test.tsx +++ b/src/features/search/ui/components/SearchResults/SearchResultsRoot.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { SearchResultsRoot } from './SearchResultsRoot'; @@ -64,4 +65,25 @@ describe('SearchResultsRoot', () => { expect(wrapper).toHaveClass('class-one'); expect(wrapper).toHaveClass('class-two'); }); + + describe('accessibility', () => { + test.each([ + ['single child', {},
Content
], + ['custom className', { className: 'custom-class' },
Content
], + [ + 'multiple children', + {}, + <> +
First Child
+
Second Child
+ , + ], + ])('has no accessibility violations when %s', async (_description, props, children) => { + const { container } = render({children}); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/authorities/AuthoritiesPageResultList.test.tsx b/src/features/search/ui/components/results/authorities/AuthoritiesPageResultList.test.tsx index 3aaf15e44..de07b240f 100644 --- a/src/features/search/ui/components/results/authorities/AuthoritiesPageResultList.test.tsx +++ b/src/features/search/ui/components/results/authorities/AuthoritiesPageResultList.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as useAuthoritiesPageTableFormatterModule from '@/features/search/ui/hooks/useAuthoritiesPageTableFormatter'; @@ -76,4 +77,14 @@ describe('AuthoritiesPageResultList', () => { expect(mockNavigateWithState).toHaveBeenCalledWith('/linked-data/resources/auth-99/edit'); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/authorities/AuthoritiesResultList.test.tsx b/src/features/search/ui/components/results/authorities/AuthoritiesResultList.test.tsx index 68693557e..b7a8c2000 100644 --- a/src/features/search/ui/components/results/authorities/AuthoritiesResultList.test.tsx +++ b/src/features/search/ui/components/results/authorities/AuthoritiesResultList.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as useFormattedResultsHook from '../../../hooks/useFormattedResults'; import * as useTableFormatterHook from '../../../hooks/useTableFormatter'; @@ -200,4 +201,34 @@ describe('AuthoritiesResultList', () => { }), ); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + mockUseFormattedResults.mockReturnValue(mockData); + mockUseTableFormatter.mockReturnValue({ + formattedData: mockFormattedData, + listHeader: mockListHeader, + }); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when useFormattedResults returns undefined', async () => { + mockUseFormattedResults.mockReturnValue(undefined); + mockUseTableFormatter.mockReturnValue({ + formattedData: mockFormattedData, + listHeader: mockListHeader, + }); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/hubs/HubsLookupResultList.test.tsx b/src/features/search/ui/components/results/hubs/HubsLookupResultList.test.tsx index 4327e68be..3dc00811e 100644 --- a/src/features/search/ui/components/results/hubs/HubsLookupResultList.test.tsx +++ b/src/features/search/ui/components/results/hubs/HubsLookupResultList.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as useFormattedResultsHook from '@/features/search/ui/hooks/useFormattedResults'; import * as useTableFormatterHook from '@/features/search/ui/hooks/useTableFormatter'; @@ -130,4 +131,20 @@ describe('HubsLookupResultList', () => { }), ); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + mockUseFormattedResults.mockReturnValue(mockData); + mockUseTableFormatter.mockReturnValue({ + formattedData: mockFormattedData, + listHeader: mockListHeader, + }); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/hubs/HubsResultList.test.tsx b/src/features/search/ui/components/results/hubs/HubsResultList.test.tsx index 4d145be60..b2ec98984 100644 --- a/src/features/search/ui/components/results/hubs/HubsResultList.test.tsx +++ b/src/features/search/ui/components/results/hubs/HubsResultList.test.tsx @@ -2,6 +2,7 @@ import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Row } from '@/components/Table'; @@ -43,7 +44,7 @@ describe('HubsResultList', () => { }; const renderComponent = () => { - render( + return render( @@ -129,4 +130,16 @@ describe('HubsResultList', () => { expect(screen.getByTestId('table-flex')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + jest.spyOn(useHubsTableFormatterModule, 'useHubsTableFormatter').mockReturnValue(mockFormatterReturn); + + const { container } = renderComponent(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/resources/ResourcesResultList.test.tsx b/src/features/search/ui/components/results/resources/ResourcesResultList.test.tsx index 9f1ffed5a..3f7921f82 100644 --- a/src/features/search/ui/components/results/resources/ResourcesResultList.test.tsx +++ b/src/features/search/ui/components/results/resources/ResourcesResultList.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as useFormattedResultsHook from '../../../hooks/useFormattedResults'; import { ResourcesResultList } from './ResourcesResultList'; @@ -90,4 +91,25 @@ describe('ResourcesResultList', () => { expect(screen.getByTestId('result-entry-123')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + [ + 'entries exist', + [ + { id: '1', title: 'Test Work 1', author: 'Author 1' } as WorkAsSearchResultDTO, + { id: '2', title: 'Test Work 2', author: 'Author 2' } as WorkAsSearchResultDTO, + ], + ], + ['no data', undefined], + ])('has no accessibility violations when %s', async (_description, data) => { + mockUseFormattedResults.mockReturnValue(data); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/components/results/resources/SearchResultEntry.test.tsx b/src/features/search/ui/components/results/resources/SearchResultEntry.test.tsx index 84d37ebbc..79e4dc804 100644 --- a/src/features/search/ui/components/results/resources/SearchResultEntry.test.tsx +++ b/src/features/search/ui/components/results/resources/SearchResultEntry.test.tsx @@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { useSearchStore, useUIStore } from '@/store'; @@ -205,4 +206,38 @@ describe('SearchResultEntry', () => { expect(getByText('ld.noInstancesAvailable')).toBeInTheDocument(); }); }); + + describe('accessibility', () => { + beforeEach(() => { + setInitialGlobalState([ + { + store: useSearchStore, + state: { + data: [], + query: '', + searchBy: 'keyword', + selectedInstances: [], + setSelectedInstances: jest.fn(), + }, + }, + { + store: useUIStore, + state: { + isSearchPaneCollapsed: false, + }, + }, + ]); + }); + + test.each([ + ['with instances', mockProps], + ['without instances', { ...mockProps, instances: [] }], + ])('has no accessibility violations when %s', async (_description, props) => { + const { container } = renderWithProviders(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/AssignFormatter.test.tsx b/src/features/search/ui/formatters/authorities/AssignFormatter.test.tsx index 57549ffb6..96fcce550 100644 --- a/src/features/search/ui/formatters/authorities/AssignFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/AssignFormatter.test.tsx @@ -1,5 +1,6 @@ import { fireEvent } from '@testing-library/dom'; import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { AuthRefType } from '@/common/constants/search.constants'; @@ -63,4 +64,34 @@ describe('AssignFormatter', () => { expect(queryByTestId('assign-button-2')).toBeNull(); }); + + describe('accessibility', () => { + const authorizedRow = { + authorized: { label: AuthRefType.Authorized }, + __meta: { id: '1' }, + title: { label: 'Title 1' }, + subclass: { label: 'Subclass 1' }, + }; + const unauthorizedRow = { + authorized: { label: AuthRefType.AuthRef }, + __meta: { id: '2' }, + title: { label: 'Title 2' }, + subclass: { label: 'Subclass 2' }, + }; + + test.each([ + ['renders disabled Button', authorizedRow, true], + ['does not render Button when not authorized', unauthorizedRow, false], + ])('has no accessibility violations when %s', async (_description, testRow, checkFailedIdResult) => { + mockCheckFailedId.mockReturnValue(checkFailedIdResult); + + const { container } = render( + AssignFormatter({ row: testRow, onAssign: mockOnAssign, checkFailedId: mockCheckFailedId }), + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/AuthorityActionFormatter.test.tsx b/src/features/search/ui/formatters/authorities/AuthorityActionFormatter.test.tsx index 584972c5b..2462be5a1 100644 --- a/src/features/search/ui/formatters/authorities/AuthorityActionFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/AuthorityActionFormatter.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { AuthorityActionFormatter } from './AuthorityActionFormatter'; @@ -84,4 +85,22 @@ describe('AuthorityActionFormatter', () => { expect(() => fireEvent.click(screen.getByTestId('authority-import-marc-safe'))).not.toThrow(); }); + + describe('accessibility', () => { + test.each([ + ['LD authority with edit button', { row: makeRow('ld-1', true), onEdit: mockOnEdit, onImport: mockOnImport }], + ['LD authority without import button', { row: makeRow('ld-3', true), onEdit: mockOnEdit }], + [ + 'MARC authority with import/edit button', + { row: makeRow('marc-1', false), onEdit: mockOnEdit, onImport: mockOnImport }, + ], + ['MARC authority without edit button', { row: makeRow('marc-3', false), onImport: mockOnImport }], + ])('has no accessibility violations when %s', async (_description, props) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/AuthorityLabelFormatter.test.tsx b/src/features/search/ui/formatters/authorities/AuthorityLabelFormatter.test.tsx index 493814c7c..0a731627f 100644 --- a/src/features/search/ui/formatters/authorities/AuthorityLabelFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/AuthorityLabelFormatter.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { AuthorityLabelFormatter } from './AuthorityLabelFormatter'; @@ -35,4 +36,18 @@ describe('AuthorityLabelFormatter', () => { const button = screen.getByRole('button'); expect(() => fireEvent.click(button)).not.toThrow(); }); + + describe('accessibility', () => { + test.each([ + ['title is absent', makeRow('')], + ['title is present', makeRow('Shakespeare, William', 'auth-42')], + ['row id is set', makeRow('Some Title', 'auth-99')], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/AuthoritySourceFormatter.test.tsx b/src/features/search/ui/formatters/authorities/AuthoritySourceFormatter.test.tsx index 8056209f8..d14c9fecf 100644 --- a/src/features/search/ui/formatters/authorities/AuthoritySourceFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/AuthoritySourceFormatter.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { AuthoritySourceFormatter } from './AuthoritySourceFormatter'; @@ -52,4 +53,37 @@ describe('AuthoritySourceFormatter', () => { expect(container.querySelector('span')).toHaveTextContent('MeSH'); }); + + describe('accessibility', () => { + test.each([ + ['label is empty', makeRow()], + [ + 'LD authority', + makeRow({ + __meta: { id: 'ld-1', key: 'k', isAnchor: false, isLD: true }, + source: { label: 'ld.source.linkedData' }, + }), + ], + [ + 'MARC authority with text label', + makeRow({ + __meta: { id: 'marc-1', key: 'k', isAnchor: false, isLD: false }, + source: { label: 'LC Name Authority File' }, + }), + ], + [ + 'MARC authority with plain span', + makeRow({ + __meta: { id: 'marc-2', key: 'k', isAnchor: false, isLD: false }, + source: { label: 'MeSH' }, + }), + ], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/AuthorizedFormatter.test.tsx b/src/features/search/ui/formatters/authorities/AuthorizedFormatter.test.tsx index a0b384476..b527923f7 100644 --- a/src/features/search/ui/formatters/authorities/AuthorizedFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/AuthorizedFormatter.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { AuthRefType } from '@/common/constants/search.constants'; @@ -32,4 +33,17 @@ describe('AuthorizedFormatter', () => { expect(labelElement.tagName.toLowerCase()).toBe('span'); }); + + describe('accessibility', () => { + test.each([ + ['authorized', { ...defaultRow, authorized: { label: AuthRefType.Authorized } }], + ['not authorized', { ...defaultRow, authorized: { label: 'Not Authorized' } }], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/authorities/TitleFormatter.test.tsx b/src/features/search/ui/formatters/authorities/TitleFormatter.test.tsx index 86a58bbbe..edc03535b 100644 --- a/src/features/search/ui/formatters/authorities/TitleFormatter.test.tsx +++ b/src/features/search/ui/formatters/authorities/TitleFormatter.test.tsx @@ -1,5 +1,6 @@ import { fireEvent } from '@testing-library/dom'; import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { TitleFormatter } from './TitleFormatter'; @@ -49,4 +50,26 @@ describe('TitleFormatter', () => { expect(onTitleClick).toHaveBeenCalledWith(row.__meta.id, row.title.label, row.subclass.label); }); + + describe('accessibility', () => { + test.each([ + [ + 'isMissingMatchQuery is true', + { + ...defaultRow, + __meta: { ...defaultRow.__meta, isAnchor: true }, + subclass: { label: '' }, + authorized: { label: '' }, + authoritySource: { label: '' }, + }, + ], + ['isMissingMatchQuery is false', { ...defaultRow, __meta: { ...defaultRow.__meta, isAnchor: false } }], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/BaseNotesFormatter.test.tsx b/src/features/search/ui/formatters/hubs/BaseNotesFormatter.test.tsx index 354a122d3..7ddf88632 100644 --- a/src/features/search/ui/formatters/hubs/BaseNotesFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/BaseNotesFormatter.test.tsx @@ -1,6 +1,7 @@ import { IntlProvider } from 'react-intl'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { BaseNotesFormatter } from './BaseNotesFormatter'; @@ -105,4 +106,23 @@ describe('BaseNotesFormatter', () => { expect(screen.getByText('-')).toBeInTheDocument(); }); }); + + describe('accessibility', () => { + test.each([ + ['auth label exists', mockRowWithAuthLabel, 'auth'], + ['auth label is undefined', mockRowWithoutAuthLabel, 'auth'], + ['auth label is null', mockRowWithNullLabels, 'auth'], + ['auth object is empty', mockRowWithEmptyObject, 'auth'], + ['rda label exists', mockRowWithRdaLabel, 'rda'], + ['rda label is undefined', mockRowWithoutRdaLabel, 'rda'], + ['rda label is null', mockRowWithNullLabels, 'rda'], + ['rda object is empty', mockRowWithEmptyObject, 'rda'], + ] as const)('has no accessibility violations when %s', async (_description, row, fieldKey) => { + const { container } = renderWithIntl(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/HubActionFormatter.test.tsx b/src/features/search/ui/formatters/hubs/HubActionFormatter.test.tsx index b8587edaf..c44fdae0c 100644 --- a/src/features/search/ui/formatters/hubs/HubActionFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/HubActionFormatter.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { HubActionFormatter } from './HubActionFormatter'; @@ -202,4 +203,39 @@ describe('HubActionFormatter', () => { // Should not throw error expect(importButton).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + [ + 'isLocal is true', + { + __meta: { id: 'token_1', key: 'key_1', isAnchor: false, isLocal: true }, + hub: { label: 'Local Hub', uri: 'http://example.com/hub_1', className: 'hub-title' }, + source: { label: 'ld.source.libraryOfCongress.local', className: 'hub-source' }, + }, + ], + [ + 'isLocal is false', + { + __meta: { id: 'token_2', key: 'key_2', isAnchor: false, isLocal: false }, + hub: { label: 'Remote Hub', uri: 'http://example.com/hub_2', className: 'hub-title' }, + source: { label: 'ld.source.libraryOfCongress', className: 'hub-source' }, + }, + ], + [ + 'isLocal is undefined', + { + __meta: { id: 'token_3', key: 'key_3', isAnchor: false }, + hub: { label: 'Hub Without isLocal', uri: 'http://example.com/hub_3', className: 'hub-title' }, + source: { label: 'ld.source.libraryOfCongress', className: 'hub-source' }, + }, + ], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/HubAssignFormatter.test.tsx b/src/features/search/ui/formatters/hubs/HubAssignFormatter.test.tsx index f1e6f74c3..5f7ea3588 100644 --- a/src/features/search/ui/formatters/hubs/HubAssignFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/HubAssignFormatter.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { HubAssignFormatter } from './HubAssignFormatter'; @@ -170,4 +171,17 @@ describe('HubAssignFormatter', () => { true, ); }); + + describe('accessibility', () => { + test.each([ + ['external hub', defaultRow], + ['local hub', { ...defaultRow, __meta: { ...defaultRow.__meta, isLocal: true } }], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/HubLinkFormatter.test.tsx b/src/features/search/ui/formatters/hubs/HubLinkFormatter.test.tsx index db413dd15..caa8d2ce5 100644 --- a/src/features/search/ui/formatters/hubs/HubLinkFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/HubLinkFormatter.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { HubLinkFormatter } from './HubLinkFormatter'; @@ -130,4 +131,22 @@ describe('HubLinkFormatter', () => { expect(span?.tagName.toLowerCase()).toBe('span'); expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['title and URI are present', defaultRow], + ['title is missing', { ...defaultRow, hub: { label: '', uri: 'test_hub_url' } }], + ['URI is missing', { ...defaultRow, hub: { label: 'Test Hub Label', uri: '' } }], + ['title and URI are missing', { ...defaultRow, hub: { label: '', uri: '' } }], + ['hub data is undefined', { ...defaultRow, hub: { label: undefined, uri: undefined } }], + ['title only, URI is undefined', { ...defaultRow, hub: { label: 'Title Only', uri: undefined } }], + ['URI only, title is undefined', { ...defaultRow, hub: { label: undefined, uri: 'test_hub_url_2' } }], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/HubSourceFormatter.test.tsx b/src/features/search/ui/formatters/hubs/HubSourceFormatter.test.tsx index a6b570a20..55bef2d29 100644 --- a/src/features/search/ui/formatters/hubs/HubSourceFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/HubSourceFormatter.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { HubSourceFormatter } from './HubSourceFormatter'; @@ -104,4 +105,39 @@ describe('HubSourceFormatter', () => { expect(screen.getByTestId('hub-source-unique-test-id')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + [ + 'label is a string', + { + __meta: { id: 'token_1', key: 'key_1', isAnchor: false }, + hub: { label: 'Test Hub', uri: 'http://example.com/hub_1', className: 'hub-title' }, + source: { label: 'ld.source.libraryOfCongress', className: 'hub-source' }, + }, + ], + [ + 'label is a local source translation key', + { + __meta: { id: 'token_2', key: 'key_2', isAnchor: false }, + hub: { label: 'Local Hub', uri: 'http://example.com/hub_2', className: 'hub-title' }, + source: { label: 'ld.source.libraryOfCongress.local', className: 'hub-source' }, + }, + ], + [ + 'label is not a string', + { + __meta: { id: 'token_3', key: 'key_3', isAnchor: false }, + hub: { label: 'Test Hub', uri: 'http://example.com/hub_3', className: 'hub-title' }, + source: { label: Custom Label, className: 'hub-source' }, + }, + ], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/hubs/RDANotesFormatter.test.tsx b/src/features/search/ui/formatters/hubs/RDANotesFormatter.test.tsx index 817c9d1be..582d69f0e 100644 --- a/src/features/search/ui/formatters/hubs/RDANotesFormatter.test.tsx +++ b/src/features/search/ui/formatters/hubs/RDANotesFormatter.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { RDANotesFormatter } from './RDANotesFormatter'; @@ -74,4 +75,25 @@ describe('RDANotesFormatter', () => { expect(baseFormatter).toHaveAttribute('data-field-key', 'rda'); expect(baseFormatter).toHaveTextContent(JSON.stringify(emptyRow)); }); + + describe('accessibility', () => { + test.each([ + ['default row data', defaultRow], + [ + 'custom row data', + { + __meta: { id: 'custom_id_456' }, + rda: { notes: ['Custom note'], additionalData: 'test' }, + hub: { label: 'Custom Hub', uri: 'test_custom_url' }, + }, + ], + ['empty row data', { __meta: { id: 'empty_id' } }], + ])('has no accessibility violations when %s', async (_description, row) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/instances/InstanceEditCtlFormatter.test.tsx b/src/features/search/ui/formatters/instances/InstanceEditCtlFormatter.test.tsx index d3252973d..f78fee156 100644 --- a/src/features/search/ui/formatters/instances/InstanceEditCtlFormatter.test.tsx +++ b/src/features/search/ui/formatters/instances/InstanceEditCtlFormatter.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { generateEditResourceUrl } from '@/common/helpers/navigation.helper'; import type { Row } from '@/components/Table'; @@ -106,4 +107,16 @@ describe('InstanceEditCtlFormatter', () => { expect(screen.getByRole('button')).toHaveAttribute('data-type', 'primary'); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = render( + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/instances/InstanceSelectCtlFormatter.test.tsx b/src/features/search/ui/formatters/instances/InstanceSelectCtlFormatter.test.tsx index c5a4ffd6f..aea35076a 100644 --- a/src/features/search/ui/formatters/instances/InstanceSelectCtlFormatter.test.tsx +++ b/src/features/search/ui/formatters/instances/InstanceSelectCtlFormatter.test.tsx @@ -1,17 +1,12 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import type { Row } from '@/components/Table'; import { InstanceSelectCtlFormatter } from './InstanceSelectCtlFormatter'; describe('InstanceSelectCtlFormatter', () => { - const formatMessage = jest.fn((descriptor, values) => { - if (descriptor.id === 'ld.aria.table.selectRow') { - return `Select row for ${values?.title}`; - } - return descriptor.id; - }); - + const formatMessage = jest.fn(); const onToggleSelect = jest.fn(); const mockRow: Row = { @@ -21,6 +16,12 @@ describe('InstanceSelectCtlFormatter', () => { beforeEach(() => { jest.clearAllMocks(); + formatMessage.mockImplementation((descriptor, values) => { + if (descriptor.id === 'ld.aria.table.selectRow') { + return `Select row for ${values?.title}`; + } + return descriptor.id; + }); }); test('renders checkbox input', () => { @@ -125,4 +126,24 @@ describe('InstanceSelectCtlFormatter', () => { expect(container.firstChild).toHaveClass('row-select-container'); }); + + describe('accessibility', () => { + test.each([ + ['instance is not selected', []], + ['instance is selected', ['456', '123', '789']], + ])('has no accessibility violations when %s', async (_description, selectedInstances) => { + const { container } = render( + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/features/search/ui/formatters/instances/InstanceTitleFormatter.test.tsx b/src/features/search/ui/formatters/instances/InstanceTitleFormatter.test.tsx index 3f45a1451..a7fee5a96 100644 --- a/src/features/search/ui/formatters/instances/InstanceTitleFormatter.test.tsx +++ b/src/features/search/ui/formatters/instances/InstanceTitleFormatter.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { FullDisplayType } from '@/common/constants/uiElements.constants'; import type { Row } from '@/components/Table'; @@ -160,4 +161,32 @@ describe('InstanceTitleFormatter', () => { expect(screen.getByTestId('preview-button__123')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['default state', { selectedInstances: [], activePreviewIds: [] }], + [ + 'instance is selected and shows comparison index', + { selectedInstances: ['999', '123', '456'], activePreviewIds: ['999', '123'] }, + ], + ['instance is not selected', { selectedInstances: ['999', '456'], activePreviewIds: ['999'] }], + [ + 'fullDisplayComponentType is Comparison', + { + selectedInstances: ['123'], + activePreviewIds: ['123'], + fullDisplayComponentType: FullDisplayType.Comparison, + }, + ], + ['single preview not in comparison mode', { selectedInstances: ['123'], activePreviewIds: ['123'] }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render( + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Accordion.test.tsx b/src/test/__tests__/components/Accordion.test.tsx index ab47a36e9..ea625f34d 100644 --- a/src/test/__tests__/components/Accordion.test.tsx +++ b/src/test/__tests__/components/Accordion.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Accordion } from '@/components/Accordion'; @@ -12,4 +13,24 @@ describe('Accordion', () => { expect(queryByTestId('accordion-contents')).toHaveProperty('hidden'); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when closed', async () => { + const { container, getByTestId } = render(); + + fireEvent.click(getByTestId('accordion-toggle-button')); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/App.test.tsx b/src/test/__tests__/components/App.test.tsx index 24b737ca1..005893c63 100644 --- a/src/test/__tests__/components/App.test.tsx +++ b/src/test/__tests__/components/App.test.tsx @@ -1,17 +1,28 @@ import '@/test/__mocks__/common/helpers/pageScrolling.helper.mock'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { App } from '@/App'; jest.mock('@/common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: false })); describe('App', () => { + let container: HTMLElement; + beforeEach(() => { - render(); + ({ container } = render()); }); test('renders Loading (default) component', () => { expect(screen.getByTestId('loading')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/DatePicker.test.tsx b/src/test/__tests__/components/DatePicker.test.tsx index a6c74d1f5..2631bda85 100644 --- a/src/test/__tests__/components/DatePicker.test.tsx +++ b/src/test/__tests__/components/DatePicker.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { DatePicker } from '@/components/DatePicker/DatePicker'; @@ -40,4 +41,20 @@ describe('DatePicker Component', () => { expect(input).toHaveAttribute('name', 'datePicker'); }); + + describe('accessibility', () => { + const baseProps = { id: 'test-date', value: '2024-11-11', onChange: () => {} }; + + // in practice, a default placeholder is always supplied, satisfying accessibility + test.each([ + ['default', { placeholder: 'YYYY-MM-DD' }], + ['with placeholder and name', { placeholder: 'Select a date', name: 'datePicker' }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/DateRange.test.tsx b/src/test/__tests__/components/DateRange.test.tsx index 1918fadf4..44403a5ac 100644 --- a/src/test/__tests__/components/DateRange.test.tsx +++ b/src/test/__tests__/components/DateRange.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { DateRange } from '@/components/DateRange'; @@ -62,4 +63,17 @@ describe('DateRange Component', () => { expect(mockSubmit).not.toHaveBeenCalled(); }); + + describe('accessibility', () => { + test.each([ + ['with facet', { facet: 'testFacet' }], + ['without facet', {}], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Dropdown.test.tsx b/src/test/__tests__/components/Dropdown.test.tsx index 3116b8c47..3f08f2df1 100644 --- a/src/test/__tests__/components/Dropdown.test.tsx +++ b/src/test/__tests__/components/Dropdown.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { DropdownItemType } from '@/common/constants/uiElements.constants'; import { Dropdown } from '@/components/Dropdown'; @@ -87,4 +88,69 @@ describe('Dropdown', () => { expect(getByText('item.label')).toBeInTheDocument(); expect(getByText('Custom Component')).toBeInTheDocument(); }); + + describe('accessibility', () => { + const itemsWithContent: DropdownItems = [ + { + id: 'group_1', + labelId: 'groupLabel', + data: [ + { + id: 'testItem_1', + type: DropdownItemType.basic, + labelId: 'item.label', + action: jest.fn(), + }, + { + id: 'testItem_2', + type: DropdownItemType.customComponent, + renderComponent: (key: string | number) =>
Custom Component
, + }, + ], + }, + ]; + + test.each([ + ['empty items', items], + ['basic and custom items', itemsWithContent], + ])('has no accessibility violations when %s', async (_description, dropdownItems) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when toggling', async () => { + const dropdownItems: DropdownItems = [ + { + id: 'group_1', + labelId: 'groupLabel', + data: [ + { + id: 'testItem_1', + type: DropdownItemType.basic, + labelId: 'item.label', + action: jest.fn(), + }, + { + id: 'testItem_2', + type: DropdownItemType.customComponent, + renderComponent: (key: string | number) =>
Custom Component
, + }, + ], + }, + ]; + + const { container, getByRole } = render(); + + const button = getByRole('button'); + + fireEvent.click(button); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/DropdownField.test.tsx b/src/test/__tests__/components/DropdownField.test.tsx index 36eb9d77e..e77133ee8 100644 --- a/src/test/__tests__/components/DropdownField.test.tsx +++ b/src/test/__tests__/components/DropdownField.test.tsx @@ -1,6 +1,7 @@ import '@/test/__mocks__/lib/react-select.mock'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { DropdownField } from '@/components/DropdownField'; @@ -14,7 +15,7 @@ describe('Dropdown Field', () => { const { getByTestId } = screen; function renderComponent() { - render(); + return render(); } test('triggers onChange', () => { @@ -29,4 +30,19 @@ describe('Dropdown Field', () => { expect(onChangeFn).toHaveBeenCalledWith(options[0], uuid, true); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = render( +
+
label
+ +
, + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Dropzone.test.tsx b/src/test/__tests__/components/Dropzone.test.tsx index 43d161a08..06638b10b 100644 --- a/src/test/__tests__/components/Dropzone.test.tsx +++ b/src/test/__tests__/components/Dropzone.test.tsx @@ -1,6 +1,8 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; +// import { axe } from 'jest-axe'; + import { Dropzone } from '@/components/Dropzone'; describe('Dropzone', () => { @@ -8,12 +10,16 @@ describe('Dropzone', () => { const acceptableFile = new File(['{}'], 'resources.json', { type: 'application/json' }); const rejectableFile = new File([''], 'not-json.txt', { type: 'text/plain' }); + // let container: HTMLElement; + beforeEach(() => { let files: File[] = []; + //let rerender: ReturnType['rerender']; const setFiles = (f: File[]) => { files = f; rerender(); }; + // ({ rerender, container } = render()); const { rerender } = render(); }); @@ -54,4 +60,14 @@ describe('Dropzone', () => { fireEvent.click(screen.getByTestId('dropzone-file-remove')); expect(screen.queryByTestId('dropzone-file')).not.toBeInTheDocument(); }); + + /* UILD-844: DropzoneFile remove button requires an aria-label + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); + */ }); diff --git a/src/test/__tests__/components/ErrorBoundary.test.tsx b/src/test/__tests__/components/ErrorBoundary.test.tsx index a2051261e..b9ebf71a2 100644 --- a/src/test/__tests__/components/ErrorBoundary.test.tsx +++ b/src/test/__tests__/components/ErrorBoundary.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { ErrorBoundary } from '@/components/ErrorBoundary'; @@ -18,4 +19,23 @@ describe('Error Boundary', () => { expect(screen.getByTestId('errorBoundary')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + console.error = jest.fn(); + const ThrowError = () => { + throw new Error('Test error boundary'); + }; + + const { container } = render( + + + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Input.test.tsx b/src/test/__tests__/components/Input.test.tsx index a75a9bfab..7666b52d8 100644 --- a/src/test/__tests__/components/Input.test.tsx +++ b/src/test/__tests__/components/Input.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Input } from '@/components/Input'; @@ -9,9 +10,10 @@ describe('Input', () => { const onChange = jest.fn(); const onPressEnter = jest.fn(); let inputElement: HTMLElement; + let container: HTMLElement; beforeEach(() => { - render( + ({ container } = render( { onChange={onChange} onPressEnter={onPressEnter} />, - ); + )); inputElement = screen.getByTestId(testId); }); @@ -43,4 +45,12 @@ describe('Input', () => { expect(onPressEnter).toHaveBeenCalledTimes(2); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/LiteralField.test.tsx b/src/test/__tests__/components/LiteralField.test.tsx index 00d8ff1ac..2bed10dc3 100644 --- a/src/test/__tests__/components/LiteralField.test.tsx +++ b/src/test/__tests__/components/LiteralField.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { v4 as uuidv4 } from 'uuid'; import { LiteralField } from '@/components/LiteralField'; @@ -10,7 +11,7 @@ const { getByTestId } = screen; describe('Literal Field', () => { function renderComponent() { - render(); + return render(); } test('triggers handleOnChange', () => { @@ -25,4 +26,20 @@ describe('Literal Field', () => { expect(onChangeFn).toHaveBeenCalledWith(uuid, [{ label: event.target.value }]); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + // In practice, a valid htmlId is passed to this component. + const { container } = render( +
+
Label
+ +
, + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Loading.test.tsx b/src/test/__tests__/components/Loading.test.tsx index ee4da26f5..95af0dd78 100644 --- a/src/test/__tests__/components/Loading.test.tsx +++ b/src/test/__tests__/components/Loading.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Loading } from '@/components/Loading'; @@ -9,4 +10,14 @@ describe('Loading', () => { expect(container.querySelector('.loader-overlay')).toBeInTheDocument(); expect(container.querySelector('.loader')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/MarcTooltip.test.tsx b/src/test/__tests__/components/MarcTooltip.test.tsx index 926d9e6b2..61479523b 100644 --- a/src/test/__tests__/components/MarcTooltip.test.tsx +++ b/src/test/__tests__/components/MarcTooltip.test.tsx @@ -1,6 +1,7 @@ import { IntlProvider } from 'react-intl'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { MarcMapping, MarcTooltip, MarcTooltipProps } from '@/components/MarcTooltip'; @@ -51,4 +52,21 @@ describe('MarcTooltip', () => { expect(button.parentElement).toHaveClass('custom-class'); expect(button).toHaveAttribute('data-testid', expect.stringContaining('custom-id')); }); + + describe('accessibility', () => { + const mapping: MarcMapping = { Title: '245 $a', Author: '100 $a' }; + + test.each([ + ['mapping undefined', { mapping: undefined }], + ['mapping empty', { mapping: {} }], + ['mapping with entries', { mapping }], + ['custom className and htmlId', { mapping, className: 'custom-class', htmlId: 'custom-id' }], + ])('has no accessibility violations when %s', async (_description, props) => { + const { container } = renderMarcTooltip(props); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Modal.test.tsx b/src/test/__tests__/components/Modal.test.tsx index f0d27f216..eca7766c3 100644 --- a/src/test/__tests__/components/Modal.test.tsx +++ b/src/test/__tests__/components/Modal.test.tsx @@ -1,6 +1,7 @@ import { createModalContainer } from '@/test/__mocks__/common/misc/createModalContainer.mock'; import { act, fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Modal } from '@/components/Modal'; @@ -52,6 +53,22 @@ describe('Modal', () => { expect(screen.queryByTestId('modal-cancel-button')).not.toBeInTheDocument(); }); + describe('accessibility', () => { + test.each([ + ['renders null', { isOpen: false }], + ['renders Modal component', {}], + ['renders Modal with custom test id', { 'data-testid': 'custom-modal-id' }], + ['renders submit button when not hidden', { submitButtonHidden: false, submitButtonLabel: 'Submit' }], + ['renders Modal component without a cancel button', { cancelButtonHidden: true }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); + describe('event handlers', () => { beforeEach(() => { render(); diff --git a/src/test/__tests__/components/Pagination.test.tsx b/src/test/__tests__/components/Pagination.test.tsx index 1f1fe50ed..6fcf7590f 100644 --- a/src/test/__tests__/components/Pagination.test.tsx +++ b/src/test/__tests__/components/Pagination.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Pagination } from '@/components/Pagination'; @@ -15,6 +16,19 @@ describe('Pagination', () => { expect(getByTestId('pagination')).toBeInTheDocument(); }); + describe('accessibility', () => { + test.each([ + ['first page', { currentPage: 0, totalPages: 2 }], + ['last page', { currentPage: 1, totalPages: 2 }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); + describe('counts', () => { type PaginationProps = { currentPage: number; diff --git a/src/test/__tests__/components/SimpleLookupField.test.tsx b/src/test/__tests__/components/SimpleLookupField.test.tsx index 4dc6b8f30..c82fcf744 100644 --- a/src/test/__tests__/components/SimpleLookupField.test.tsx +++ b/src/test/__tests__/components/SimpleLookupField.test.tsx @@ -1,6 +1,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { axe } from 'jest-axe'; import { SimpleLookupField } from '@/components/SimpleLookupField'; @@ -28,7 +29,15 @@ describe('Simple lookup field', () => { return render( - {}} value={initialValue} isMulti={isMulti} /> +
label
+ {}} + value={initialValue} + isMulti={isMulti} + htmlId="label" + />
, ); }; @@ -80,4 +89,17 @@ describe('Simple lookup field', () => { expect(await findByText('value-3')).toBeInTheDocument(); }); }); + + describe('accessibility', () => { + test.each([ + ['multi select', true], + ['single select', false], + ])('has no accessibility violations when %s', async (_description, isMulti) => { + const { container } = renderScreen(isMulti); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Table/HeaderCell.test.tsx b/src/test/__tests__/components/Table/HeaderCell.test.tsx index 15801b292..431f17021 100644 --- a/src/test/__tests__/components/Table/HeaderCell.test.tsx +++ b/src/test/__tests__/components/Table/HeaderCell.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { HeaderCell } from '@/components/Table/HeaderCell'; @@ -101,4 +102,21 @@ describe('HeaderCell', () => { expect(() => expectClickBehavior()).not.toThrow(); }); }); + + describe('accessibility', () => { + test.each([ + ['default', {}], + ['td element', { elementType: 'td' as const }], + ['JSX label', { label: JSX Label }], + ['undefined label', { label: undefined }], + ['clickable', { onHeaderCellClick: () => {} }], + ['custom className', { className: 'custom-class' }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = renderHeaderCell(overrides); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Table/Table.test.tsx b/src/test/__tests__/components/Table/Table.test.tsx index 4e3e4e1ea..7dc328e96 100644 --- a/src/test/__tests__/components/Table/Table.test.tsx +++ b/src/test/__tests__/components/Table/Table.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Table } from '@/components/Table'; @@ -30,9 +31,13 @@ describe('Table', () => { const onHeaderCellClick = jest.fn(); const onRowClick = jest.fn(); - beforeEach(() => - render(), - ); + let container: HTMLElement; + + beforeEach(() => { + ({ container } = render( +
, + )); + }); it('renders Table', () => { expect(screen.getByTestId('table')).toBeInTheDocument(); @@ -61,4 +66,12 @@ describe('Table', () => { it('renders empty cell if no corresponding entry exists in data arg', () => { expect(screen.getByTestId(mockHeader.nonExistent.label)).toBeEmptyDOMElement(); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/TableFlex.test.tsx b/src/test/__tests__/components/TableFlex.test.tsx index c3a180b28..82ff01782 100644 --- a/src/test/__tests__/components/TableFlex.test.tsx +++ b/src/test/__tests__/components/TableFlex.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { TableFlex } from '@/components/Table/TableFlex'; @@ -16,8 +17,12 @@ describe('TableFlex Component', () => { const onRowClick = jest.fn(); const onHeaderCellClick = jest.fn(); + let container: HTMLElement; + beforeEach(() => { - render(); + ({ container } = render( + , + )); }); test('renders TableFlex component', () => { @@ -56,4 +61,12 @@ describe('TableFlex Component', () => { expect(onRowClick).toHaveBeenCalledWith(data[0]); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/components/Tooltip.test.tsx b/src/test/__tests__/components/Tooltip.test.tsx index d9b303c9b..0396cea97 100644 --- a/src/test/__tests__/components/Tooltip.test.tsx +++ b/src/test/__tests__/components/Tooltip.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Tooltip } from '@/components/Tooltip'; @@ -126,4 +127,46 @@ describe('Tooltip', () => { fireEvent.click(button); expect(screen.getByTestId('tooltip-trigger__content')).toHaveClass('custom-content'); }); + + describe('accessibility', () => { + test.each([ + ['default', {}], + ['with custom class names', { className: 'custom-trigger', contentClassName: 'custom-content' }], + ])('has no accessibility violations when %s', async (_description, overrides) => { + const { container } = render( + , + ); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations when clicking to show', async () => { + const { container } = render( + , + ); + + const button = screen.getByRole('button', { name: ariaLabelOpen }); + + fireEvent.click(button); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/views/ExternalResourcePreview.test.tsx b/src/test/__tests__/views/ExternalResourcePreview.test.tsx index ad74abe59..f7bd3e957 100644 --- a/src/test/__tests__/views/ExternalResourcePreview.test.tsx +++ b/src/test/__tests__/views/ExternalResourcePreview.test.tsx @@ -1,6 +1,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { ExternalResourcePreview } from '@/views'; @@ -42,4 +43,19 @@ describe('ExternalResourcePreview', () => { expect(screen.getByTestId('preview-fields')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['loading', { data: null, isLoading: true }], + ['loaded', { data: { schema: new Map(), userValues: {}, initKey: 'key' }, isLoading: false }], + ])('has no accessibility violations when %s', async (_description, mockReturn) => { + mockUseResourcePreviewQuery.mockReturnValue(mockReturn); + + const { container } = renderComponent(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/views/Root.test.tsx b/src/test/__tests__/views/Root.test.tsx index 7b337ae7c..31c90ccc5 100644 --- a/src/test/__tests__/views/Root.test.tsx +++ b/src/test/__tests__/views/Root.test.tsx @@ -8,6 +8,7 @@ import { setInitialGlobalState } from '@/test/__mocks__/store'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Root } from '@/views'; @@ -26,7 +27,7 @@ describe('Root', () => { }, ]); - render( + return render( } /> @@ -49,4 +50,17 @@ describe('Root', () => { expect(getByTestId('loading-component')).toBeInTheDocument(); }); + + describe('accessibility', () => { + test.each([ + ['default', false], + ['loading', true], + ])('has no accessibility violations when %s', async (_description, isLoading) => { + const { container } = renderRootComponent(isLoading); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/__tests__/views/Search.test.tsx b/src/test/__tests__/views/Search.test.tsx index c1e178419..d0eee527e 100644 --- a/src/test/__tests__/views/Search.test.tsx +++ b/src/test/__tests__/views/Search.test.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { TYPE_URIS } from '@/common/constants/bibframe.constants'; import * as BuildConstants from '@/common/constants/build.constants'; @@ -52,9 +53,11 @@ const renderWithProviders = (component: React.ReactNode) => { }; describe('Search', () => { + let container: HTMLElement; + beforeEach(() => { queryClient.clear(); - renderWithProviders(); + ({ container } = renderWithProviders()); }); test('renders Search component', () => { @@ -124,4 +127,12 @@ describe('Search', () => { expect(navigateToManageProfileSettings).toHaveBeenCalled(); }); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/test/setupTests.ts b/src/test/setupTests.ts index fc7d7dca9..bb52f648b 100644 --- a/src/test/setupTests.ts +++ b/src/test/setupTests.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import 'jest-axe/extend-expect'; import { enableFetchMocks } from 'jest-fetch-mock'; enableFetchMocks(); diff --git a/src/views/Edit/Edit.test.tsx b/src/views/Edit/Edit.test.tsx index 3f2d99f4f..da96df770 100644 --- a/src/views/Edit/Edit.test.tsx +++ b/src/views/Edit/Edit.test.tsx @@ -11,6 +11,7 @@ import { setInitialGlobalState, setUpdatedGlobalState } from '@/test/__mocks__/s import * as Router from 'react-router-dom'; import { act, render, screen, waitFor } from '@testing-library/react'; +import { axe } from 'jest-axe'; import * as BibframeConstants from '@/common/constants/bibframe.constants'; import { Edit } from '@/views'; @@ -127,4 +128,16 @@ describe('Edit', () => { expect(applyUpdatedSettingsToResource).toHaveBeenCalled(); }); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + jest.spyOn(Router, 'useParams').mockReturnValue({ resourceId: 'testResourceId' }); + + const { container } = await renderComponent(monograph as unknown as ProfileEntry); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/views/ExternalResource/components/PreviewExternalResourceControls/PreviewExternalResourceControls.test.tsx b/src/views/ExternalResource/components/PreviewExternalResourceControls/PreviewExternalResourceControls.test.tsx index 96a1c16e8..8d91bba35 100644 --- a/src/views/ExternalResource/components/PreviewExternalResourceControls/PreviewExternalResourceControls.test.tsx +++ b/src/views/ExternalResource/components/PreviewExternalResourceControls/PreviewExternalResourceControls.test.tsx @@ -2,6 +2,7 @@ import * as ReactRouterDom from 'react-router-dom'; import { fireEvent, screen, waitFor } from '@testing-library/dom'; import { act, render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { PreviewExternalResourceControls } from '@/views/ExternalResource/components/PreviewExternalResourceControls'; @@ -58,4 +59,14 @@ describe('PreviewExternalResourceControls', () => { expect(navigate).toHaveBeenCalled(); }); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const { container } = renderComponent(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/views/ExternalResource/components/PreviewExternalResourcePane/PreviewExternalResourcePane.test.tsx b/src/views/ExternalResource/components/PreviewExternalResourcePane/PreviewExternalResourcePane.test.tsx index 0cce3a023..fb9bdcf06 100644 --- a/src/views/ExternalResource/components/PreviewExternalResourcePane/PreviewExternalResourcePane.test.tsx +++ b/src/views/ExternalResource/components/PreviewExternalResourcePane/PreviewExternalResourcePane.test.tsx @@ -1,5 +1,6 @@ import { fireEvent, screen } from '@testing-library/dom'; import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { PreviewExternalResourcePane } from '@/views/ExternalResource/components/PreviewExternalResourcePane'; @@ -51,4 +52,19 @@ describe('PreviewExternalResourcePane', () => { expect(navigate).toHaveBeenCalled(); }); + + describe('accessibility', () => { + test.each([ + ['query data is available', { data: { record: mockRecord } }], + ['query has no data', { data: null }], + ])('has no accessibility violations when %s', async (_description, queryResult) => { + mockUseResourcePreviewQuery.mockReturnValue(queryResult); + + const { container } = render(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/views/ManageProfileSettings/ManageProfileSettings.test.tsx b/src/views/ManageProfileSettings/ManageProfileSettings.test.tsx index 42bf5db45..ade2ff4ce 100644 --- a/src/views/ManageProfileSettings/ManageProfileSettings.test.tsx +++ b/src/views/ManageProfileSettings/ManageProfileSettings.test.tsx @@ -3,8 +3,15 @@ import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; - -import { fetchPreferredProfiles, fetchProfile, fetchProfileSettings, fetchProfiles } from '@/common/api/profiles.api'; +import { axe } from 'jest-axe'; + +import { + fetchAllSettingsForProfile, + fetchPreferredProfiles, + fetchProfile, + fetchProfileSettings, + fetchProfiles, +} from '@/common/api/profiles.api'; import { BFLITE_URIS } from '@/common/constants/bibframeMapping.constants'; import { AdvancedFieldType } from '@/common/constants/uiControls.constants'; @@ -15,6 +22,7 @@ jest.mock('@/common/api/profiles.api', () => ({ fetchPreferredProfiles: jest.fn(), fetchProfile: jest.fn(), fetchProfileSettings: jest.fn(), + fetchAllSettingsForProfile: jest.fn(), })); const renderComponent = () => { @@ -84,13 +92,24 @@ describe('ManageProfileSettings', () => { active: false, children: [], }; + const mockAllSettingsForProfile = [ + { + id: 1, + profileId: 'one-profile', + name: 'one', + }, + ]; + + let container: HTMLElement; - beforeEach(() => { + beforeEach(async () => { (fetchProfiles as jest.Mock).mockResolvedValue(mockProfiles); (fetchPreferredProfiles as jest.Mock).mockResolvedValue(mockPreferredProfiles); (fetchProfile as jest.Mock).mockResolvedValue(mockProfile); (fetchProfileSettings as jest.Mock).mockResolvedValue(mockProfileSettings); - renderComponent(); + (fetchAllSettingsForProfile as jest.Mock).mockResolvedValue(mockAllSettingsForProfile); + ({ container } = renderComponent()); + await screen.findByTestId('manage-profile-settings'); }); afterEach(() => { @@ -387,4 +406,32 @@ describe('ManageProfileSettings', () => { }); }); }); + + describe('accessibility', () => { + test('basic render has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + /* UILD-847: ProfileSettings button requires aria-label + test('settings render has no accessibility violations', async () => { + fireEvent.click(screen.getAllByTestId('resource-profile-item')[0]); + + waitFor(() => { + expect(screen.getByTestId('profile-settings')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('profile-settings-select-create')); + + waitFor(() => { + expect(screen.getByTestId('component-test:childC')).toBeInTheDocument(); + }); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + */ + }); }); diff --git a/src/views/Root/components/CommonStatus.test.tsx b/src/views/Root/components/CommonStatus.test.tsx index 832f9dc3a..87018e2fa 100644 --- a/src/views/Root/components/CommonStatus.test.tsx +++ b/src/views/Root/components/CommonStatus.test.tsx @@ -4,6 +4,8 @@ import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +// import { axe } from 'jest-axe'; + import { StatusType } from '@/common/constants/status.constants'; import { useStatusStore } from '@/store'; @@ -120,4 +122,26 @@ describe('CommonStatus', () => { expect(getStatusMessagesCount(container)).toBe(1); expect(screen.getByText('ld.rdUpdateSuccess')).toBeInTheDocument(); }); + + /* // UILD-845: Message close button requires aria-label + describe('accessibility', () => { + test.each([ + ['no status messages', []], + [ + 'status messages present', + [ + { id: '01', type: StatusType.success, message: 'test message 1' }, + { id: '02', type: StatusType.error, message: 'test message 2' }, + { id: '03', type: StatusType.warning, message: 'test message 3' }, + ], + ], + ])('has no accessibility violations when %s', async (_description, statusMessages) => { + const { container } = renderComponent(statusMessages); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); + */ }); diff --git a/src/views/Root/components/Footer.test.tsx b/src/views/Root/components/Footer.test.tsx index b5b226245..4107eb4bc 100644 --- a/src/views/Root/components/Footer.test.tsx +++ b/src/views/Root/components/Footer.test.tsx @@ -1,6 +1,7 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Footer } from './Footer'; @@ -106,4 +107,34 @@ describe('Footer', () => { expect(screen.getByTestId('record-controls')).toBeInTheDocument(); }); }); + + describe('accessibility', () => { + test.each([ + ['external resource route with no data', () => setActiveRoute(2)], + [ + 'external resource route with data', + () => { + setActiveRoute(2); + mockUseResourcePreviewQuery.mockReturnValue({ data: { record: {} } }); + }, + ], + ['hub import route with no data', () => setActiveRoute(3)], + [ + 'hub import route with data', + () => { + setActiveRoute(3); + mockUseHubQuery.mockReturnValue({ data: { resource: {} } }); + }, + ], + ['edit route', () => setActiveRoute(1)], + ])('has no accessibility violations when %s', async (_description, setup) => { + setup(); + + const { container } = renderFooter(); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/views/Root/components/Nav.test.tsx b/src/views/Root/components/Nav.test.tsx index 736b52063..2a4b357b4 100644 --- a/src/views/Root/components/Nav.test.tsx +++ b/src/views/Root/components/Nav.test.tsx @@ -4,6 +4,9 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { Nav } from './Nav'; +// No direct accessibility test. Largely composed of conditional +// children that are tested separately. + const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ diff --git a/src/views/Search/components/FullDisplay/FullDisplay.test.tsx b/src/views/Search/components/FullDisplay/FullDisplay.test.tsx index d7e8fa93a..a37a401f0 100644 --- a/src/views/Search/components/FullDisplay/FullDisplay.test.tsx +++ b/src/views/Search/components/FullDisplay/FullDisplay.test.tsx @@ -2,10 +2,10 @@ import '@/test/__mocks__/common/helpers/pageScrolling.helper.mock'; import '@/test/__mocks__/features/edit/hooks/useEditPage.mock'; import { setInitialGlobalState } from '@/test/__mocks__/store'; -import { Fragment, ReactNode } from 'react'; import { RouterProvider, createMemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { axe } from 'jest-axe'; import { Edit } from '@/views'; import { FullDisplay } from '@/views/Search/components/FullDisplay'; @@ -23,22 +23,9 @@ const mockUseResourcePreviewQuery = useResourcePreviewQuery as jest.Mock; jest.mock('@/common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: false })); -jest.mock('react-intl', () => ({ - FormattedMessage: ({ id, values }: never) => { - return ( -
- {Object.entries(values ?? {})?.map(([k, v]) => ( - {v as ReactNode} - ))} -
- ); - }, - useIntl: () => ({ - formatMessage: ({ id }: { id: string }) => id, - }), -})); - describe('FullDisplay', () => { + let container: HTMLElement; + beforeEach(() => { mockUseResourcePreviewQuery.mockImplementation((id: string) => ({ data: @@ -77,7 +64,7 @@ describe('FullDisplay', () => { }, ]; - return render(); + ({ container } = render()); }); const { getByTestId, getAllByTestId } = screen; @@ -99,4 +86,12 @@ describe('FullDisplay', () => { expect(screen.getByTestId('edit-page')).toBeInTheDocument(); }); }); + + describe('accessibility', () => { + test('has no accessibility violations', async () => { + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); }); diff --git a/src/views/Search/components/ModalImport/ModalImport.test.tsx b/src/views/Search/components/ModalImport/ModalImport.test.tsx index 79236d757..b2867f0cd 100644 --- a/src/views/Search/components/ModalImport/ModalImport.test.tsx +++ b/src/views/Search/components/ModalImport/ModalImport.test.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; +import { axe } from 'jest-axe'; import * as importApi from '@/common/api/import.api'; import { ImportFilterTypes, ImportModes } from '@/common/constants/import.constants'; @@ -57,7 +58,7 @@ describe('ModalImport', () => { }, }, ]); - render( + return render( , @@ -359,4 +360,28 @@ describe('ModalImport', () => { expect(spy).toHaveBeenCalled(); }); }); + + describe('accessibility', () => { + test.each([ + ['default import window', undefined], + ['hub import', ImportFilterTypes.Hub], + ['instance import', ImportFilterTypes.Instance], + ])('has no accessibility violations when %s', async (_description, filterType) => { + const { container } = renderComponent(filterType); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + test('has no accessibility violations in URL import mode', async () => { + const { container } = renderComponent(); + + fireEvent.change(screen.getByTestId('modal-import-mode-selector'), { target: { value: ImportModes.JsonUrl } }); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + }); });