diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml
index c513ed23d..0157d17b6 100644
--- a/.github/workflows/ci-cd.yaml
+++ b/.github/workflows/ci-cd.yaml
@@ -68,6 +68,21 @@ jobs:
working-directory: frontend
run: npm run lint:check
+ - name: Run accessibility tests
+ run: |
+ npm run test -- -t "accessibility"
+
+ - name: Generate accessibility report
+ run: |
+ npm run test:a11y
+ continue-on-error: true # Don't fail the build, but record issues
+
+ - name: Upload accessibility report
+ uses: actions/upload-artifact@v2
+ with:
+ name: accessibility-report
+ path: accessibility-report.json
+
- name: Check for uncommitted changes
run: |
git diff --exit-code || (echo 'Unstaged changes detected. \
@@ -174,6 +189,7 @@ jobs:
- name: Run frontend tests
run: |
docker run --env-file frontend/.env.example ${{ env.DOCKERHUB_USERNAME }}/owasp-nest-test-frontend:latest npm run test
+
build-docker-staging-images:
name: Build Docker Staging Images
diff --git a/frontend/__tests__/src/pages/ChapterDetails.test.tsx b/frontend/__tests__/src/pages/ChapterDetails.test.tsx
index a1fef7521..8577e4be8 100644
--- a/frontend/__tests__/src/pages/ChapterDetails.test.tsx
+++ b/frontend/__tests__/src/pages/ChapterDetails.test.tsx
@@ -1,11 +1,13 @@
import { screen, waitFor } from '@testing-library/react'
-
import { fetchAlgoliaData } from 'api/fetchAlgoliaData'
+import { axe, toHaveNoViolations } from 'jest-axe'
import { ChapterDetailsPage } from 'pages'
import { render } from 'wrappers/testUtil'
import { mockChapterData } from '@tests/data/mockChapterData'
+expect.extend(toHaveNoViolations)
+
jest.mock('api/fetchAlgoliaData', () => ({
fetchAlgoliaData: jest.fn(),
}))
@@ -26,6 +28,18 @@ describe('ChapterDetailsPage Component', () => {
jest.clearAllMocks()
})
+ test('should not have any accessibility violations', async () => {
+ const { container } = render()
+
+ // Wait for content to load
+ await waitFor(() => {
+ expect(screen.queryByText('Loading indicator')).not.toBeInTheDocument()
+ })
+
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
+ })
+
test('renders loading spinner initially', async () => {
render()
const loadingSpinner = screen.getAllByAltText('Loading indicator')
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index 1077e3ce0..607d209a5 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -7,6 +7,7 @@ import typescriptParser from '@typescript-eslint/parser'
import prettierConfig from 'eslint-config-prettier'
import importPlugin from 'eslint-plugin-import'
import jest from 'eslint-plugin-jest'
+import jsxA11y from 'eslint-plugin-jsx-a11y'
import prettier from 'eslint-plugin-prettier'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
@@ -41,6 +42,7 @@ export default [
jest,
prettier,
react,
+ 'jsx-a11y': jsxA11y,
},
settings: {
'import/resolver': {
@@ -59,6 +61,19 @@ export default [
rules: {
...jest.configs.recommended.rules,
...prettierConfig.rules,
+ 'jsx-a11y/alt-text': 'error',
+ 'jsx-a11y/anchor-has-content': 'error',
+ 'jsx-a11y/anchor-is-valid': 'error',
+ 'jsx-a11y/aria-props': 'error',
+ 'jsx-a11y/aria-proptypes': 'error',
+ 'jsx-a11y/aria-unsupported-elements': 'error',
+ 'jsx-a11y/role-has-required-aria-props': 'error',
+ 'jsx-a11y/role-supports-aria-props': 'error',
+ 'jsx-a11y/tabindex-no-positive': 'error',
+ 'jsx-a11y/no-redundant-roles': 'error',
+ 'jsx-a11y/label-has-associated-control': 'error',
+ 'jsx-a11y/no-autofocus': 'warn',
+ 'jsx-a11y/no-noninteractive-tabindex': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts
index 928fcbe07..4de76096a 100644
--- a/frontend/jest.setup.ts
+++ b/frontend/jest.setup.ts
@@ -1,12 +1,20 @@
import '@testing-library/jest-dom'
import { TextEncoder } from 'util'
import dotenv from 'dotenv'
+import { toHaveNoViolations } from 'jest-axe'
import React from 'react'
dotenv.config()
+expect.extend(toHaveNoViolations)
global.React = React
global.TextEncoder = TextEncoder
+global.axe = {
+ run: async () => ({
+ violations: [],
+ }),
+ configure: () => null,
+}
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation((...args) => {
@@ -28,4 +36,21 @@ beforeEach(() => {
})
})
+expect.extend({
+ toBeAccessible: async (received) => {
+ const results = await global.axe.run(received)
+ return {
+ pass: results.violations.length === 0,
+ message: () =>
+ results.violations.length === 0
+ ? 'Expected element to not be accessible'
+ : `Expected element to be accessible but found violations:\n${JSON.stringify(
+ results.violations,
+ null,
+ 2
+ )}`,
+ }
+ },
+})
+
jest.mock('@algolia/autocomplete-theme-classic', () => ({}))
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2c06e7894..22490d47c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -46,12 +46,14 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
+ "@axe-core/react": "^4.10.1",
"@eslint/js": "^9.15.0",
"@swc/core": "^1.10.7",
"@swc/jest": "^0.2.37",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.14",
+ "@types/jest-axe": "^3.5.9",
"@types/mocha": "^10.0.10",
"@types/node": "^22.10.7",
"@types/react": "^19.0.6",
@@ -67,6 +69,7 @@
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.9.0",
+ "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
@@ -74,6 +77,7 @@
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
+ "jest-axe": "^9.0.0",
"jest-environment-jsdom": "^29.7.0",
"open": "^10.1.0",
"postcss": "^8.4.47",
@@ -389,6 +393,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@axe-core/react": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@axe-core/react/-/react-4.10.1.tgz",
+ "integrity": "sha512-GRGx/3Gbce9WQYXgY0iZzqOaHIRXBNGyV+zoUhJzBuDeoTL+XoeY3u/9PzdIVyH0pbNs1n1RChfsmWBy6hzKPg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "axe-core": "~4.10.2",
+ "requestidlecallback": "^0.3.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -1754,12 +1769,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "license": "MIT"
- },
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -3634,6 +3643,27 @@
"pretty-format": "^29.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-axe/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/@types/jest/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
@@ -4355,6 +4385,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -4422,6 +4459,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
@@ -4433,6 +4480,16 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -5083,6 +5140,13 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -5980,6 +6044,70 @@
}
}
},
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/eslint-plugin-prettier": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
@@ -7838,6 +7966,83 @@
}
}
},
+ "node_modules/jest-axe": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/jest-axe/-/jest-axe-9.0.0.tgz",
+ "integrity": "sha512-Xt7O0+wIpW31lv0SO1wQZUTyJE7DEmnDEZeTt9/S9L5WUywxrv8BrgvTuQEqujtfaQOcJ70p4wg7UUgK1E2F5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axe-core": "4.9.1",
+ "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/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.9.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz",
+ "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
@@ -10440,6 +10645,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/requestidlecallback": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz",
+ "integrity": "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -10978,7 +11190,7 @@
"strip-ansi": "^6.0.1"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
}
},
"node_modules/string.prototype.matchall": {
diff --git a/frontend/package.json b/frontend/package.json
index 98fb77fb5..a63538e6a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,9 @@
"preview": "vite preview",
"test": "tsc --noEmit && NODE_OPTIONS=--no-warnings=DEP0040 jest",
"test:coverage": "jest --coverage && chmod +x scripts/open-coverage.sh && scripts/open-coverage.sh",
- "watch": "jest --watch"
+ "watch": "jest --watch",
+ "test:a11y": "jest --config jest.config.ts --testMatch='**/*.test.{ts,tsx}'",
+ "test:a11y:watch": "jest --config jest.config.ts --testMatch='**/*.test.{ts,tsx}' --watch"
},
"dependencies": {
"@algolia/autocomplete-js": "^1.17.9",
@@ -56,12 +58,14 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
+ "@axe-core/react": "^4.10.1",
"@eslint/js": "^9.15.0",
"@swc/core": "^1.10.7",
"@swc/jest": "^0.2.37",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.14",
+ "@types/jest-axe": "^3.5.9",
"@types/mocha": "^10.0.10",
"@types/node": "^22.10.7",
"@types/react": "^19.0.6",
@@ -77,6 +81,7 @@
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.9.0",
+ "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
@@ -84,6 +89,7 @@
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
+ "jest-axe": "^9.0.0",
"jest-environment-jsdom": "^29.7.0",
"open": "^10.1.0",
"postcss": "^8.4.47",
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 12b369f78..85282db43 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,4 +1,7 @@
+import axe from '@axe-core/react'
import { StrictMode } from 'react'
+import React from 'react'
+import ReactDOM from 'react-dom'
import { createRoot } from 'react-dom/client'
import './index.css'
import TagManager from 'react-gtm-module'
@@ -16,6 +19,10 @@ const tagManagerArgs = {
TagManager.initialize(tagManagerArgs)
+if (process.env.NODE_ENV !== 'production') {
+ axe(React, ReactDOM, 1000) // Logs violations to the browser console
+}
+
createRoot(document.getElementById('root')!).render(