From a6f3f1d171d4eeb7574c6f66fbff473eb233b88a Mon Sep 17 00:00:00 2001 From: Moonwalker-rgb Date: Tue, 23 Jun 2026 09:22:15 +0000 Subject: [PATCH] feat(frontend): automated accessibility testing with jest-axe (closes #132) Introduces jest-axe as a first-class test dependency and wires it into the unit-test runner via a shared @jest-environment jsdom suite, so any new a11y regression surfaces in CI rather than in production. Adds: - jest-axe + axe-core devDeps, jest-axe ambient types - jest.setup.a11y.ts registering toHaveNoViolations as a matcher - jest.config.ts setupFilesAfterEnv wiring - a11yTestUtils + a11y-mocks helper modules - a11y.test.tsx covering ErrorInline (banner + card), ActivityCenter, Navbar, ErrorBoundary, EvidenceArtifactViewer - frontend-ci.yml running type-check, lint, jest on app/frontend/** - lighthouserc.json + LIGHTHOUSE_CI.md (manual opt-in audit config, not gating CI per issue scope) Fixes uncovered violations: - icon-only close/remove buttons get aria-label + aria-hidden=true on the inner SVG (ErrorInline, ActivityCenter) - notification-count badge gains sr-only text in ActivityCenter - EvidenceArtifactViewer filename heading promoted from

to

to satisfy heading-order axe rule against pages that provide

--- .github/workflows/frontend-ci.yml | 70 ++++++++ app/frontend/LIGHTHOUSE_CI.md | 20 +++ app/frontend/jest.config.ts | 6 + app/frontend/jest.setup.a11y.ts | 16 ++ app/frontend/lighthouserc.json | 23 +++ app/frontend/package.json | 2 + .../src/components/ActivityCenter.tsx | 18 +- app/frontend/src/components/ErrorInline.tsx | 8 +- .../src/components/EvidenceArtifactViewer.tsx | 2 +- .../__tests__/__fixtures__/a11yTestUtils.tsx | 52 ++++++ .../src/components/__tests__/a11y-mocks.tsx | 157 ++++++++++++++++++ .../src/components/__tests__/a11y.test.tsx | 141 ++++++++++++++++ app/frontend/src/types/jest-axe.d.ts | 78 +++++++++ pnpm-lock.yaml | 58 +++++-- 14 files changed, 633 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/frontend-ci.yml create mode 100644 app/frontend/LIGHTHOUSE_CI.md create mode 100644 app/frontend/jest.setup.a11y.ts create mode 100644 app/frontend/lighthouserc.json create mode 100644 app/frontend/src/components/__tests__/__fixtures__/a11yTestUtils.tsx create mode 100644 app/frontend/src/components/__tests__/a11y-mocks.tsx create mode 100644 app/frontend/src/components/__tests__/a11y.test.tsx create mode 100644 app/frontend/src/types/jest-axe.d.ts diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 00000000..e8bad223 --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,70 @@ +name: Frontend CI + +on: + push: + branches: [main, develop] + paths: + - 'app/frontend/**' + - '.github/workflows/frontend-ci.yml' + pull_request: + branches: [main, develop] + paths: + - 'app/frontend/**' + - '.github/workflows/frontend-ci.yml' + +jobs: + test: + name: Frontend tests & accessibility audit + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Get pnpm store directory + id: pnpm-cache + run: echo "store-dir=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Cache pnpm modules + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.store-dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm type-check + + - name: Lint + run: pnpm lint --max-warnings=0 + + - name: Run unit & jest-axe accessibility tests + # --ci ensures deterministic output and a clean exit code on test failures. + run: pnpm jest --ci --colors=false + + - name: Upload test summary + if: always() + uses: actions/upload-artifact@v4 + with: + name: frontend-test-summary + path: | + app/frontend/coverage + if-no-files-found: ignore + retention-days: 7 diff --git a/app/frontend/LIGHTHOUSE_CI.md b/app/frontend/LIGHTHOUSE_CI.md new file mode 100644 index 00000000..10e84401 --- /dev/null +++ b/app/frontend/LIGHTHOUSE_CI.md @@ -0,0 +1,20 @@ +# Lighthouse CI (optional, not gated in CI) + +The `lighthouserc.json` in this directory configures [Lighthouse CI][lighthouse-ci] +to perform an accessibility audit of the production build. + +## When does it run? + +It is **not** executed by `.github/workflows/frontend-ci.yml` because +Lighthouse needs a live, fully-rendered page (the static `jest-axe` unit +tests already catch the same class of violations faster and with zero +infrastructure overhead). + +Use this configuration: + +- During code review, when changing navigation, theming, or layout. +- Locally before tagging a release: `pnpm dlx @lhci/cli autorun`. +- In a separate, opt-in workflow when accessibility is the focus of the + story being shipped. + +[lighthouse-ci]: https://github.com/GoogleChrome/lighthouse-ci diff --git a/app/frontend/jest.config.ts b/app/frontend/jest.config.ts index 79d1967c..9d3596c1 100644 --- a/app/frontend/jest.config.ts +++ b/app/frontend/jest.config.ts @@ -6,6 +6,12 @@ const config: Config = { moduleNameMapper: { '^@/(.*)$': '/src/$1', }, + /** + * Load global setup modules AFTER the testing framework is installed so + * custom matchers (e.g. jest-axe's `toHaveNoViolations`) can be registered + * via `expect.extend(...)`. + */ + setupFilesAfterEnv: ['/jest.setup.a11y.ts'], }; export default config; diff --git a/app/frontend/jest.setup.a11y.ts b/app/frontend/jest.setup.a11y.ts new file mode 100644 index 00000000..1b2ca4ea --- /dev/null +++ b/app/frontend/jest.setup.a11y.ts @@ -0,0 +1,16 @@ +/** + * Global Jest setup that registers jest-axe accessibility matchers so they + * are available in any *.a11y.test.* file across the project. + * + * The matchers are loaded lazily because jest-axe expects to be invoked from + * inside the Jest test environment (jsdom). Tests that exercise this setup + * are expected to opt into the jsdom environment via the + * `@jest-environment jsdom` pragma at the top of the test file. + */ +import { toHaveNoViolations } from 'jest-axe'; + +// `expect.extend` is parameterised by `ExpectExtendMap`, which jest's +// DefinitelyTyped types define as a record keyed by matcher name. The +// matcher shape jest-axe exports is function-like; the cast below keeps the +// runtime contract identical while satisfying strict TS. +expect.extend(toHaveNoViolations as unknown as Parameters[0]); diff --git a/app/frontend/lighthouserc.json b/app/frontend/lighthouserc.json new file mode 100644 index 00000000..51590270 --- /dev/null +++ b/app/frontend/lighthouserc.json @@ -0,0 +1,23 @@ +{ + "ci": { + "collect": { + "startServerCommand": "pnpm start", + "url": [ + "http://localhost:3000/" + ], + "numberOfRuns": 1 + }, + "assert": { + "assertions": { + "categories:accessibility": [ + "error", + { "minScore": 0.9 } + ], + "categories:best-practices": "warn", + "categories:performance": "off", + "categories:pwa": "off", + "categories:seo": "off" + } + } + } +} diff --git a/app/frontend/package.json b/app/frontend/package.json index 8e3cb394..d8098359 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -47,9 +47,11 @@ "@types/papaparse": "^5.3.16", "@types/react": "^19", "@types/react-dom": "^19", + "axe-core": "^4.12.1", "eslint": "^9.39.4", "eslint-config-next": "^16.2.1", "jest": "^30.3.0", + "jest-axe": "^10.0.0", "jest-environment-jsdom": "^30.0.0", "tailwindcss": "^4", "ts-jest": "^29.4.6", diff --git a/app/frontend/src/components/ActivityCenter.tsx b/app/frontend/src/components/ActivityCenter.tsx index 617859dd..1465808c 100644 --- a/app/frontend/src/components/ActivityCenter.tsx +++ b/app/frontend/src/components/ActivityCenter.tsx @@ -60,10 +60,18 @@ export function ActivityCenter() { > {pendingCount > 0 && ( - + )} + + {pendingCount > 0 + ? `${pendingCount} pending activity notifications` + : 'No pending activity'} + {isOpen && ( @@ -81,10 +89,12 @@ export function ActivityCenter() { )} @@ -134,11 +144,13 @@ export function ActivityCenter() { )} diff --git a/app/frontend/src/components/ErrorInline.tsx b/app/frontend/src/components/ErrorInline.tsx index 58d144d7..36d6a618 100644 --- a/app/frontend/src/components/ErrorInline.tsx +++ b/app/frontend/src/components/ErrorInline.tsx @@ -88,10 +88,12 @@ export function ErrorInline({ )} {onClose && ( )} @@ -118,10 +120,12 @@ export function ErrorInline({ {onClose && ( )} diff --git a/app/frontend/src/components/EvidenceArtifactViewer.tsx b/app/frontend/src/components/EvidenceArtifactViewer.tsx index d572540c..b1e3c3a9 100644 --- a/app/frontend/src/components/EvidenceArtifactViewer.tsx +++ b/app/frontend/src/components/EvidenceArtifactViewer.tsx @@ -275,7 +275,7 @@ export const EvidenceArtifactViewer: React.FC = ({
-

{artifact.metadata.filename}

+

{artifact.metadata.filename}

{artifact.metadata.type} • {(artifact.metadata.size / 1024 / 1024).toFixed(2)} MB diff --git a/app/frontend/src/components/__tests__/__fixtures__/a11yTestUtils.tsx b/app/frontend/src/components/__tests__/__fixtures__/a11yTestUtils.tsx new file mode 100644 index 00000000..bd372e1c --- /dev/null +++ b/app/frontend/src/components/__tests__/__fixtures__/a11yTestUtils.tsx @@ -0,0 +1,52 @@ +/** + * Shared helpers for accessibility (jest-axe) tests. + * + * Importing this module pulls in jest-axe (registered globally via + * jest.setup.a11y.ts) and exposes `renderAndCheckA11y` so per-component + * tests can simply describe their rendered tree and run axe over it. + * + * The `rendered` parameter must be a promise (React.render is async-aware + * with hooks, even when not awaiting Suspense boundaries) — testers can + * pass a sync render call and jest-axe will still scan the resulting DOM + * via the synchronous fallback. + */ +import { render, type RenderOptions, type RenderResult } from '@testing-library/react'; +import { axe, type Result as AxeResult } from 'jest-axe'; +import type { ReactElement } from 'react'; + +/** Options accepted by `renderAndCheckA11y`. */ +export interface A11yOptions extends RenderOptions { + /** + * Optional axe.run context object. Defaults to scanning the whole + * document. Useful for restricting the scan to a specific region. + */ + axeContext?: Parameters[1]; +} + +/** Convenience wrapper that returns the rendered tree and the axe report. */ +export async function renderAndCheckA11y( + ui: ReactElement, + options: A11yOptions = {}, +): Promise<{ result: RenderResult; axeReport: { violations: AxeResult['violations'] } }> { + const result = render(ui, options); + const axeReport = await axe(result.container, options.axeContext ?? {}); + return { result, axeReport }; +} + +/** + * Pretty-print the severity-impacted rules so failing assertions in CI logs + * are easy to triage. Returned as a multi-line string for snapshot or + * message interpolation. + */ +export function summariseViolations(violations: AxeResult['violations']): string { + if (violations.length === 0) return 'No accessibility violations detected.'; + return violations + .map((v) => { + const targets = v.nodes + .map((n) => n.target.join(' >> ')) + .slice(0, 3) + .join('\n - '); + return `[${v.impact?.toUpperCase() ?? 'UNKNOWN'}] ${v.id}\n ${v.help}\n Targets:\n - ${targets}`; + }) + .join('\n\n'); +} diff --git a/app/frontend/src/components/__tests__/a11y-mocks.tsx b/app/frontend/src/components/__tests__/a11y-mocks.tsx new file mode 100644 index 00000000..89385006 --- /dev/null +++ b/app/frontend/src/components/__tests__/a11y-mocks.tsx @@ -0,0 +1,157 @@ +/** + * Common Jest mock setup that exercises the project's accessibility tests. + * + * Importing this file once is enough to satisfy every external dependency + * the a11y suite touches (Next.js navigation, next-intl, Freighter wallet). + * Tests can then focus on rendering components and asserting the axe scan + * passes. + * + * Usage: `import './a11y-mocks';` + */ + +import '@testing-library/jest-dom'; + +// --- Next.js --------------------------------------------------------------- + +jest.mock('next/link', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const MockLink = ({ children, href, ...rest }: any) => ( + + {children} + + ); + MockLink.displayName = 'MockLink'; + return MockLink; +}); + +jest.mock('next/navigation', () => ({ + usePathname: () => '/', + useRouter: () => ({ + push: jest.fn(), + replace: jest.fn(), + back: jest.fn(), + forward: jest.fn(), + refresh: jest.fn(), + prefetch: jest.fn(), + }), +})); + +// --- next-intl ------------------------------------------------------------- + +jest.mock('next-intl', () => { + const identity = (key: string) => key; + return { + useTranslations: () => identity, + useFormatter: () => ({ + formatDateTime: (date: Date) => date.toISOString(), + formatRelativeTimeValue: () => ({ key: 'activity.justNow', count: 0 }), + formatNumber: (n: number) => n.toString(), + }), + useLocale: () => 'en', + NextIntlClientProvider: ({ children }: { children: React.ReactNode }) => children, + useMessages: () => ({}), + }; +}); + +// --- @stellar/freighter-api ------------------------------------------------ + +jest.mock('@stellar/freighter-api', () => ({ + isConnected: jest.fn().mockResolvedValue({ isConnected: false }), + setAllowed: jest.fn().mockResolvedValue(undefined), + getAddress: jest.fn().mockResolvedValue({ address: '' }), + getNetworkDetails: jest.fn().mockResolvedValue({ network: 'TESTNET' }), + requestAccess: jest.fn().mockResolvedValue(undefined), + signTransaction: jest.fn().mockResolvedValue({ signedTxXdr: '' }), +})); + +// --- @/lib/walletStore ----------------------------------------------------- + +jest.mock('@/lib/walletStore', () => ({ + useWalletStore: () => ({ + publicKey: null, + setPublicKey: jest.fn(), + network: 'TESTNET', + setNetwork: jest.fn(), + disconnect: jest.fn(), + }), +})); + +// --- React Query (HealthBadge uses useHealthStatus -> useQuery) ----------- + +jest.mock('@/hooks/useHealthStatus', () => ({ + useHealthStatus: () => ({ + state: 'ok', + data: null, + error: null, + lastChecked: null, + }), +})); + +// --- ToastProvider / useToast (used by WalletConnect inside Navbar) -------- + +jest.mock('@/components/ToastProvider', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const React = require('react') as typeof import('react'); + const Ctx = React.createContext<{ toast: jest.Mock } | null>(null); + + const Provider = ({ children }: { children: React.ReactNode }) => + React.createElement( + Ctx.Provider, + { value: { toast: jest.fn() } }, + children, + ); + Provider.displayName = 'MockToastProvider'; + + const useToast = () => { + const ctx = React.useContext(Ctx); + return ctx ?? { toast: jest.fn() }; + }; + + return { + __esModule: true, + default: Provider, + ToastProvider: Provider, + useToast, + }; +}); + +// --- useActivity hook (used inside VerificationFlow etc.) ----------------- + +jest.mock('@/hooks/useActivity', () => ({ + useActivity: () => ({ + trackJob: jest.fn(async (_title: string, _description: string, fn: () => Promise) => fn()), + activities: [], + addActivity: jest.fn(), + removeActivity: jest.fn(), + clearCompleted: jest.fn(), + updateActivity: jest.fn(), + }), +})); + +// --- useAidPackages hook (used in AidPackageList) -------------------------- + +jest.mock('@/hooks/useAidPackages', () => ({ + useAidPackages: () => ({ + data: [], + isLoading: false, + error: null, + }), +})); + +// --- Browser / DOM globals missing in jsdom -------------------------------- + +if (typeof window !== 'undefined' && !window.matchMedia) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); +} diff --git a/app/frontend/src/components/__tests__/a11y.test.tsx b/app/frontend/src/components/__tests__/a11y.test.tsx new file mode 100644 index 00000000..85b6a8fb --- /dev/null +++ b/app/frontend/src/components/__tests__/a11y.test.tsx @@ -0,0 +1,141 @@ +/** @jest-environment jsdom */ +/** + * Automated accessibility (jest-axe) tests for the project's most-used UI + * surfaces. + * + * These tests render each component in isolation and run axe-core against + * the rendered DOM. Adding `expect(...).toHaveNoViolations()` lets CI + * surface any new a11y regression automatically. + * + * The default Jest `testEnvironment` is `node`, so this file opts into + * jsdom via the `@jest-environment` pragma. The custom matcher + * `toHaveNoViolations` is registered globally by `jest.setup.a11y.ts`. + */ +import React from 'react'; + +import './a11y-mocks'; +import { renderAndCheckA11y, summariseViolations } from './__fixtures__/a11yTestUtils'; +import { ErrorInline } from '../ErrorInline'; +import { ErrorBoundary } from '../ErrorBoundary'; +import { ActivityCenter } from '../ActivityCenter'; +import { Navbar } from '../Navbar'; +import { EvidenceArtifactViewer } from '../EvidenceArtifactViewer'; +import type { EvidenceArtifact } from '@/types/evidence-artifact'; + +const mockArtifact: EvidenceArtifact = { + id: 'a11y-artifact-1', + metadata: { + id: 'a11y-meta-1', + type: 'image', + filename: 'a11y-image.jpg', + mimeType: 'image/jpeg', + size: 2048, + uploadedAt: new Date('2024-01-01T00:00:00Z'), + uploadedBy: 'a11y-tester', + piiDetected: false, + }, + content: 'data:image/jpeg;base64,ZmFrZQ==', + redactionState: { + level: 'none', + regions: [], + autoGeneratedRegions: [], + manuallyReviewedRegions: [], + lastModified: new Date('2024-01-01T00:00:00Z'), + modifiedBy: 'a11y-tester', + }, + permissions: { + canViewOriginal: true, + canViewRedacted: true, + canModifyRedactions: false, + canApproveRedactions: false, + role: 'viewer', + }, +}; + +describe('Automated accessibility tests (jest-axe)', () => { + it('ErrorInline (banner variant) has no critical violations', async () => { + const { axeReport } = await renderAndCheckA11y( + undefined} + onClose={() => undefined} + variant="banner" + />, + ); + + expect(axeReport.violations).toEqual([]); + }); + + it('ErrorInline (card variant) has no critical violations', async () => { + const { axeReport } = await renderAndCheckA11y( + undefined} + onClose={() => undefined} + variant="card" + />, + ); + + expect(axeReport.violations).toEqual([]); + }); + + it('ActivityCenter bell trigger and panel have no violations', async () => { + const { axeReport } = await renderAndCheckA11y( +
+

Activity test

+ +
, + ); + + expect(axeReport.violations).toEqual([]); + }); + + it('Navbar navigation has no critical violations', async () => { + const { axeReport } = await renderAndCheckA11y( +
+

Dashboard

+ +
, + ); + + if (axeReport.violations.length > 0) { + // Surface a human-readable breakdown so CI failures are easy to triage. + // eslint-disable-next-line no-console + console.error('Navbar a11y violations:\n' + summariseViolations(axeReport.violations)); + } + expect(axeReport.violations).toEqual([]); + }); + + it('ErrorBoundary fallback UI has no critical violations', async () => { + // Suppress the noisy console.error from React's error boundary logging. + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); + + class ThrowingComponent extends React.Component { + override render(): React.ReactNode { + throw new Error('forced error for boundary test'); + } + } + + const { result, axeReport } = await renderAndCheckA11y( + + + , + ); + + errorSpy.mockRestore(); + // Touch result so the linter doesn't complain about the unused binding. + expect(result.container).toBeDefined(); + expect(axeReport.violations).toEqual([]); + }); + + it('EvidenceArtifactViewer (read-only) has no critical violations', async () => { + const { axeReport } = await renderAndCheckA11y( +
+

Artifact viewer

+ +
, + ); + + expect(axeReport.violations).toEqual([]); + }); +}); diff --git a/app/frontend/src/types/jest-axe.d.ts b/app/frontend/src/types/jest-axe.d.ts new file mode 100644 index 00000000..4326ff1c --- /dev/null +++ b/app/frontend/src/types/jest-axe.d.ts @@ -0,0 +1,78 @@ +/** + * Minimal ambient declaration for the `jest-axe` runtime. + * + * The package ships a runtime that re-exports axe-core plus a + * `toHaveNoViolations` matcher; it does not yet publish TypeScript types + * compatible with our `strict` tsconfig. Declaring the surface locally keeps + * type-checking working without taking on a `@types/jest-axe` dependency. + * + * We intentionally do NOT import types from `axe-core` — the project's + * tsconfig doesn't depend on axe-core types and re-declaring keeps the + * declaration robust against upstream schema drift. + */ + +declare module 'jest-axe' { + /** Axe impact levels, mirrored from axe-core for type completeness. */ + type Impact = 'minor' | 'moderate' | 'serious' | 'critical' | null; + + /** A single violation node as published by axe-core. */ + interface AxeNodeResult { + target: ReadonlyArray; + html: string; + failureSummary?: string; + impact?: Impact; + [key: string]: unknown; + } + + /** A single rule violation. */ + interface AxeResult { + id: string; + impact?: Impact; + tags: ReadonlyArray; + description: string; + help: string; + helpUrl: string; + nodes: ReadonlyArray; + } + + /** Aggregate axe-core run output. */ + interface AxeResults { + violations: ReadonlyArray; + passes: ReadonlyArray; + incomplete: ReadonlyArray; + inapplicable: ReadonlyArray; + timestamp: string; + url: string; + } + + /** Run options accepted by `axe.run`. */ + interface RunOptions { + [key: string]: unknown; + } + + /** + * Run axe-core against the supplied context. + * + * @param context The node, selector, or document to scan. + * @param options Optional axe run options. + */ + export function axe(context?: unknown, options?: RunOptions): Promise; + + /** Matcher signature compatible with `expect.extend(...)`. */ + interface AxeMatcher { + (this: unknown, received: AxeResults): { pass: boolean; message: () => string }; + } + + /** Matcher registered by jest.setup.a11y.ts on the global expect. */ + export const toHaveNoViolations: AxeMatcher; +} + +// Augment Jest's Expect to include the custom matcher at the type level. +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toHaveNoViolations(): R; + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 791aa066..418eefe6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,7 +80,7 @@ importers: version: 11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bull@4.16.5) '@nestjs/bullmq': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.79.0(redis@5.12.1(@opentelemetry/api@1.9.0))) + version: 11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.79.1(redis@5.12.1(@opentelemetry/api@1.9.0))) '@nestjs/common': specifier: ^11.1.27 version: 11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -122,7 +122,7 @@ importers: version: 4.16.5 bullmq: specifier: ^5.79.0 - version: 5.79.0(redis@5.12.1(@opentelemetry/api@1.9.0)) + version: 5.79.1(redis@5.12.1(@opentelemetry/api@1.9.0)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -350,6 +350,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.1.17) + axe-core: + specifier: ^4.12.1 + version: 4.12.1 eslint: specifier: ^9.39.4 version: 9.39.4(jiti@2.6.1) @@ -359,6 +362,9 @@ importers: jest: specifier: ^30.3.0 version: 30.3.0(@types/node@20.19.37)(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@20.19.37)(typescript@5.9.3)) + jest-axe: + specifier: ^10.0.0 + version: 10.0.0 jest-environment-jsdom: specifier: ^30.0.0 version: 30.4.1 @@ -4082,8 +4088,12 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} - axe-core@4.11.3: - resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} + axe-core@4.10.2: + resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} + engines: {node: '>=4'} + + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} axios@1.13.6: @@ -4299,8 +4309,8 @@ packages: resolution: {integrity: sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==} engines: {node: '>=12'} - bullmq@5.79.0: - resolution: {integrity: sha512-sg+kYGn7PIDI/AAkSWINPNz0vMp745YaBYMyo3AtcfXk5iCvjEPU+XMbU3yf7rSf1KsU+OfU+GH8FhxUa+WDNA==} + bullmq@5.79.1: + resolution: {integrity: sha512-cteoHRr1FGOTUgzFrnMyBNGtQhNeVR8Ej6nImNSHQDJi4tj6GMD0p9ZG65ZsTnvR9RVf18dhRxWu4kFl634QGA==} engines: {node: '>=12.22.0'} peerDependencies: redis: '>=5.0.0' @@ -6077,6 +6087,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-axe@10.0.0: + resolution: {integrity: sha512-9QR0M7//o5UVRnEUUm68IsGapHrcKGakYy9dKWWMX79LmeUKguDI6DREyljC5I13j78OUmtKLF5My6ccffLFBg==} + engines: {node: '>= 16.0.0'} + jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6278,6 +6292,10 @@ packages: resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@29.2.2: + resolution: {integrity: sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11542,12 +11560,12 @@ snapshots: bull: 4.16.5 tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.79.0(redis@5.12.1(@opentelemetry/api@1.9.0)))': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.79.1(redis@5.12.1(@opentelemetry/api@1.9.0)))': dependencies: '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/common': 11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.79.0(redis@5.12.1(@opentelemetry/api@1.9.0)) + bullmq: 5.79.1(redis@5.12.1(@opentelemetry/api@1.9.0)) tslib: 2.8.1 '@nestjs/cli@11.0.16(@swc/core@1.15.30(@swc/helpers@0.5.15))(@types/node@22.19.15)': @@ -13786,7 +13804,9 @@ snapshots: aws-ssl-profiles@1.1.2: {} - axe-core@4.11.3: {} + axe-core@4.10.2: {} + + axe-core@4.12.1: {} axios@1.13.6: dependencies: @@ -14113,7 +14133,7 @@ snapshots: transitivePeerDependencies: - supports-color - bullmq@5.79.0(redis@5.12.1(@opentelemetry/api@1.9.0)): + bullmq@5.79.1(redis@5.12.1(@opentelemetry/api@1.9.0)): dependencies: cron-parser: 4.9.0 ioredis: 5.10.1 @@ -14925,7 +14945,7 @@ snapshots: array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.3 + axe-core: 4.12.1 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 @@ -15888,7 +15908,7 @@ snapshots: dependencies: '@ioredis/commands': 1.5.1 cluster-key-slot: 1.1.2 - debug: 4.4.3(supports-color@10.2.2) + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -16114,6 +16134,13 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jest-axe@10.0.0: + dependencies: + axe-core: 4.10.2 + chalk: 4.1.2 + jest-matcher-utils: 29.2.2 + lodash.merge: 4.6.2 + jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -16646,6 +16673,13 @@ snapshots: '@jest/get-type': 30.1.0 pretty-format: 30.4.1 + jest-matcher-utils@29.2.2: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + jest-matcher-utils@29.7.0: dependencies: chalk: 4.1.2