Skip to content

Conversation

@raissanjay
Copy link
Collaborator

@raissanjay raissanjay commented Jun 3, 2025

Description

This PR introduces a unit-level accessibility testing framework using jest-axe, adds new a11y tests for the Container component (covering multiple API states), and fixes several accessibility gaps in existing components. Below is a summary of the changes:

1. Accessibility Test Setup

jest-axe-setup.js

Registers the toHaveNoViolations() matcher from jest-axe, so we can assert that rendered DOM snapshots have zero a11y violations:

// jest-axe-setup.js
const { toHaveNoViolations } = require('jest-axe');
expect.extend(toHaveNoViolations);

jest.a11y.config.js

A specialized Jest config that:

  • Extends our existing jest.config.js
  • Disables coverage reporting for accessibility tests
  • Restricts test matching to *.a11y.test.js(x) files only
// jest.a11y.config.js
const baseConfig = require('./jest.config');

module.exports = {
  ...baseConfig,
  collectCoverage: false,
  coverageDirectory: undefined,
  collectCoverageFrom: undefined,
  coverageThreshold: undefined,
  testMatch: ['**/*.a11y.test.js', '**/*.a11y.test.jsx'],
};

Updates to jest.config.js

Added jest-axe-setup.js under setupTestFrameworkScriptFile so that expect(...).toHaveNoViolations() is available globally:

module.exports = {
  // …
  moduleFileExtensions: ['js', 'json', 'jsx'],
- setupFiles: ['<rootDir>/enzyme.config.js'],
+ setupFiles: ['<rootDir>/enzyme.config.js'],
+ setupTestFrameworkScriptFile: '<rootDir>/jest-axe-setup.js',
  testEnvironment: 'jest-environment-jsdom-fifteen',
  // …
};

package.json modifications

Added react-axe, axe-core, and jest-axe to devDependencies.

Introduced a new npm script to run only accessibility tests:

"scripts": {
  "test:unit": "jest",
+ "test:a11y": "jest --config=jest.a11y.config.js",
}

Now, running npm run test:a11y executes only files matching *.a11y.test.js(x) and skips coverage.

2. New Container a11y Tests

File: react/src/js/components/Consonant/Container/Container.a11y.test.jsx

Covers three API-driven states for the Container component, mocking fetch and using waitFor to ensure the DOM is updated before running axe(container):

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { axe } from 'jest-axe';
import Container from './Container';
import config from '../Testing/Mocks/config.json';
import cards from '../Testing/Mocks/cards.json';

// 1) Mock fetch → return non-empty cards array
beforeAll(() => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      status: 200,
      json: () => Promise.resolve({ cards }),
    })
  );
});

afterAll(() => {
  delete global.fetch;
});

describe('Container accessibility', () => {
  it('renders without a11y violations when API returns cards', async () => {
    const { container } = render(<Container config={config} />);
    // Wait until the grid is in the DOM
    await waitFor(() => screen.getByTestId('consonant-CardsGrid'));
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('renders accessible no-results view when API returns empty array', async () => {
    // Override fetch to return no cards
    global.fetch.mockImplementationOnce(() =>
      Promise.resolve({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ cards: [] }),
      })
    );
    const { container } = render(<Container config={config} />);
    await waitFor(() => screen.getByTestId('consonant-NoResultsView'));
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  it('renders accessible error view when API fails', async () => {
    // Override fetch to simulate a network error
    global.fetch.mockImplementationOnce(() =>
      Promise.resolve({
        ok: false,
        status: 404,
        statusText: 'Not Found',
        json: () => Promise.resolve({}),
      })
    );
    const { container } = render(<Container config={config} />);
    await waitFor(() => screen.getByTestId('consonant-NoResultsView'));
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Why this matters:

  • By mocking fetch and waiting for the relevant elements (CardsGrid or NoResultsView) to appear, we guarantee that axe inspects the final DOM (including dynamic content).
  • This pattern ensures we catch missing ARIA labels, focus issues, or color-contrast problems even when the component's contents change based on API responses.

3. Component Accessibility Fixes

Card.jsx heading level logic

Ensures headingLevel is always a valid number (defaults to 2 if invalid), preventing invalid aria-level values:

- const headingLevel = getConfig('collection.i18n', 'cardTitleAccessibilityLevel');
+ let headingLevel = getConfig('collection.i18n', 'cardTitleAccessibilityLevel');
+
+ // Support alternate key "titleHeadingLevel" if supplied
+ if (!headingLevel) {
+   const titleLevel = getConfig('collection.i18n', 'titleHeadingLevel');
+   if (typeof titleLevel === 'string') {
+     const parsed = parseInt(titleLevel.replace(/[^0-9]/g, ''), 10);
+     headingLevel = Number.isNaN(parsed) ? undefined : parsed;
+   }
+ }
+
+ // Default to level 2 if still undefined or invalid
+ if (!headingLevel || typeof headingLevel !== 'number') {
+   headingLevel = 2;
+ }

const additionalParams = getConfig('collection', 'additionalRequestParams');
// …
const lastModified = getConfig('collection', 'i18n.lastModified');

PanelFooter.jsx (mobile filters)

Added an aria-label on the "Done" button so screen readers announce it correctly:

- <button
-   onClick={onMobileFiltersToggleClick}
-   ref={footerBtnRef}
-   onKeyDown={handleMobileFooterButtonTab}>
-   {buttonText}
- </button>
+ <button
+   onClick={onMobileFiltersToggleClick}
+   ref={footerBtnRef}
+   onKeyDown={handleMobileFooterButtonTab}
+   aria-label={buttonText || 'Done'}>
+   {buttonText}
+ </button>

Title.jsx (mobile filter header)

Added an explicit aria-label="Back" on the back-button element so assistive technologies know its purpose:

- <div
-   onClick={onClick}
-   className="consonant-LeftFilters-mobBack"
-   onKeyDown={onKeyDown}
-   ref={mobBackRef} />
+ <div
+   onClick={onClick}
+   className="consonant-LeftFilters-mobBack"
+   onKeyDown={onKeyDown}
+   ref={mobBackRef}
+   aria-label="Back" />

4. Why These Changes Matter

Unit-level a11y coverage with jest-axe

  • Running axe(container) in JSDOM gives immediate feedback on missing alt text, improper ARIA roles, focus-order issues, and color-contrast failures (to the extent JSDOM can evaluate them).
  • By mocking API calls and using waitFor, we test dynamic states—for example, opening a filters panel or handling empty/error responses—without launching a full browser.

Component-level fixes

  • Ensuring every interactive element or landmark has an accessible name (aria-label, valid aria-level, etc.) reduces the chance of keyboard/navigation or screen-reader issues.
  • Defaulting to a valid headingLevel prevents rendering invalid aria-level attributes.

CI integration

  • The new npm run test:a11y command runs only a11y tests (matching *.a11y.test.js(x)), so our CI can block merges if any accessibility violations occur.
  • Unit tests now automatically catch regressions—no need for a separate Pa11y or Puppeteer job on every PR.

5. Next Steps

Extend coverage as new components are built

Follow the same pattern (render parent, fire events, wait for dynamic DOM changes, await axe(container)).

Consider full-browser audits later

For screenshots, color-contrast based on compiled CSS, and real focus-navigation tests, we can add an AxePuppeteer or Pa11y job against our staging environment in a separate PR.

Summary

This PR:

  • ✅ Sets up Jest-axe for unit-level a11y tests
  • ✅ Adds three new accessibility tests for Container (normal, empty, error states)
  • ✅ Fixes missing aria-label and invalid heading levels in key components
  • ✅ Introduces test:a11y for CI to verify no regressions in accessibility

Please review and approve so we can catch a11y issues automatically on every merge.

@github-actions
Copy link

github-actions bot commented Jun 3, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-03T23:42:30.145Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 3, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-03T23:46:23.231Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T03:08:19.633Z
PR: #287

@raissanjay raissanjay changed the title test(mwpw-1234): some descr test(mwpw-1234): add jest‐axe accessibility testing, container unit tests, and component a11y fixes Jun 4, 2025
@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T04:42:23.797Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T04:44:06.321Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T04:45:22.787Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T05:32:11.091Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T05:51:00.021Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T06:04:52.422Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T07:06:48.527Z
PR: #287

@github-actions
Copy link

github-actions bot commented Jun 4, 2025

Core Web Vitals Metrics

Metric Value
LCP N/A s
FID N/A ms
CLS N/A

Recorded at: 2025-06-04T07:20:38.712Z
PR: #287

@sanrai sanrai requested review from cmiqueo, gmirijan, jedjedjedM, sanrai, sheridansunier and shkhan91 and removed request for sanrai June 4, 2025 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants