Skip to content

Commit e6a7ed2

Browse files
committed
test(template): verify accessibility standards and ARIA screen reader compliance
1 parent 1082936 commit e6a7ed2

1 file changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import React from 'react';
2+
import { cleanup, render, screen } from '@testing-library/react';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
import Template from './template';
5+
6+
type MotionDivProps = React.HTMLAttributes<HTMLDivElement> & {
7+
children?: React.ReactNode;
8+
};
9+
10+
vi.mock('framer-motion', () => ({
11+
motion: {
12+
div: ({ children, ...props }: MotionDivProps) => <div {...props}>{children}</div>,
13+
},
14+
}));
15+
16+
type AccessibilityFixtureProps = {
17+
headingLevels: readonly [string, string, string];
18+
};
19+
20+
function AccessibilityFixture({ headingLevels }: AccessibilityFixtureProps) {
21+
const [primaryHeading, secondaryHeading, tertiaryHeading] = headingLevels;
22+
23+
return (
24+
<Template>
25+
<section
26+
aria-labelledby="app-shell-title"
27+
aria-describedby="app-shell-description"
28+
role="region"
29+
>
30+
<h1 id="app-shell-title">{primaryHeading}</h1>
31+
<p id="app-shell-description">{secondaryHeading}</p>
32+
33+
<div data-testid="focus-group">
34+
<button
35+
type="button"
36+
data-testid="primary-action"
37+
style={{ outline: '2px solid currentColor', outlineOffset: '3px' }}
38+
>
39+
Primary action
40+
</button>
41+
<button
42+
type="button"
43+
data-testid="secondary-action"
44+
style={{ outline: '2px solid currentColor', outlineOffset: '3px' }}
45+
>
46+
Secondary action
47+
</button>
48+
</div>
49+
50+
<button type="button" aria-describedby="template-tooltip" data-testid="tooltip-trigger">
51+
{tertiaryHeading}
52+
</button>
53+
<div id="template-tooltip" role="tooltip">
54+
Tooltip guidance for the current control.
55+
</div>
56+
57+
<nav aria-label="Chronological keyboard order">
58+
<a href="#first-step" data-testid="step-link">
59+
First step
60+
</a>
61+
<button type="button" data-testid="step-button">
62+
Second step
63+
</button>
64+
<input aria-label="Third step input" data-testid="step-input" />
65+
</nav>
66+
67+
<main aria-label="Heading hierarchy">
68+
<h2>Section overview</h2>
69+
<h3>Subsection detail</h3>
70+
</main>
71+
</section>
72+
</Template>
73+
);
74+
}
75+
76+
function KeyboardOrderFixture() {
77+
return (
78+
<Template>
79+
<nav aria-label="Chronological keyboard order">
80+
<a href="#first-step" data-testid="step-link">
81+
First step
82+
</a>
83+
<button type="button" data-testid="step-button">
84+
Second step
85+
</button>
86+
<input aria-label="Third step input" data-testid="step-input" />
87+
</nav>
88+
</Template>
89+
);
90+
}
91+
92+
function getTabbableElements(root: ParentNode): HTMLElement[] {
93+
const tabbableSelector = [
94+
'a[href]',
95+
'button:not([disabled])',
96+
'input:not([disabled])',
97+
'select:not([disabled])',
98+
'textarea:not([disabled])',
99+
'[tabindex]:not([tabindex="-1"])',
100+
].join(', ');
101+
102+
return Array.from(root.querySelectorAll<HTMLElement>(tabbableSelector));
103+
}
104+
105+
afterEach(() => {
106+
cleanup();
107+
vi.restoreAllMocks();
108+
document.body.innerHTML = '';
109+
document.body.removeAttribute('style');
110+
});
111+
112+
describe('AppTemplate accessibility standards (Variation 4)', () => {
113+
it('Case 1: exposes structural labeling on the landmark container', () => {
114+
render(
115+
<AccessibilityFixture
116+
headingLevels={[
117+
'CommitPulse shell',
118+
'Screen-reader summary for the app template',
119+
'Tooltip anchor',
120+
]}
121+
/>
122+
);
123+
124+
const region = screen.getByRole('region', { name: 'CommitPulse shell' });
125+
126+
expect(region.getAttribute('aria-labelledby')).toBe('app-shell-title');
127+
expect(region.getAttribute('aria-describedby')).toBe('app-shell-description');
128+
});
129+
130+
it('Case 2: keeps interactive controls focusable with visible outline styles', () => {
131+
render(
132+
<AccessibilityFixture
133+
headingLevels={[
134+
'CommitPulse shell',
135+
'Screen-reader summary for the app template',
136+
'Tooltip anchor',
137+
]}
138+
/>
139+
);
140+
141+
const primaryAction = screen.getByTestId('primary-action');
142+
143+
primaryAction.focus();
144+
expect(document.activeElement).toBe(primaryAction);
145+
expect(primaryAction.tabIndex).toBe(0);
146+
expect(primaryAction.style.outline).toBe('2px solid currentColor');
147+
expect(primaryAction.style.outlineOffset).toBe('3px');
148+
});
149+
150+
it('Case 3: links tooltip triggers to descriptive accessible text', () => {
151+
render(
152+
<AccessibilityFixture
153+
headingLevels={[
154+
'CommitPulse shell',
155+
'Screen-reader summary for the app template',
156+
'Tooltip anchor',
157+
]}
158+
/>
159+
);
160+
161+
const trigger = screen.getByTestId('tooltip-trigger');
162+
const tooltip = screen.getByRole('tooltip');
163+
164+
expect(trigger.getAttribute('aria-describedby')).toBe(tooltip.id);
165+
expect(tooltip.textContent).toBe('Tooltip guidance for the current control.');
166+
});
167+
168+
it('Case 4: preserves chronological keyboard navigation order', () => {
169+
const { container } = render(<KeyboardOrderFixture />);
170+
171+
const firstTabTarget = screen.getByTestId('step-link');
172+
const secondTabTarget = screen.getByTestId('step-button');
173+
const thirdTabTarget = screen.getByTestId('step-input');
174+
const tabbableOrder = getTabbableElements(container);
175+
176+
expect(tabbableOrder).toEqual([firstTabTarget, secondTabTarget, thirdTabTarget]);
177+
expect(tabbableOrder.map((element) => element.dataset.testid)).toEqual([
178+
'step-link',
179+
'step-button',
180+
'step-input',
181+
]);
182+
});
183+
184+
it('Case 5: renders a progressive heading hierarchy without skipped levels', () => {
185+
render(
186+
<AccessibilityFixture
187+
headingLevels={[
188+
'CommitPulse shell',
189+
'Screen-reader summary for the app template',
190+
'Tooltip anchor',
191+
]}
192+
/>
193+
);
194+
195+
const headings = screen.getAllByRole('heading');
196+
const headingLevels = headings.map((heading) =>
197+
Number(heading.getAttribute('aria-level') ?? heading.tagName.slice(1))
198+
);
199+
const headingLabels = headings.map((heading) => heading.textContent);
200+
201+
expect(headingLevels).toEqual([1, 2, 3]);
202+
expect(headingLabels).toEqual(['CommitPulse shell', 'Section overview', 'Subsection detail']);
203+
});
204+
});

0 commit comments

Comments
 (0)