Skip to content

Commit c476a7b

Browse files
committed
test(sharesheet): add accessibility standards and screen reader aria compliance coverage
1 parent 41222eb commit c476a7b

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import ShareSheet from './ShareSheet';
4+
import type { DashboardExportData } from '@/types/dashboard';
5+
6+
vi.mock('framer-motion', () => ({
7+
motion: {
8+
div: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
9+
<div {...props}>{children}</div>
10+
),
11+
},
12+
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
13+
}));
14+
15+
vi.mock('react-qr-code', () => ({
16+
default: ({ value }: { value: string }) => <svg data-testid="qr-code" data-value={value} />,
17+
}));
18+
19+
vi.mock('@/hooks/useShareActions', () => ({
20+
useShareActions: () => ({
21+
states: {},
22+
handleTwitter: vi.fn(),
23+
handleLinkedIn: vi.fn(),
24+
handleReddit: vi.fn(),
25+
handleDownloadPNG: vi.fn(),
26+
handleDownloadWEBP: vi.fn(),
27+
handleDownloadSVG: vi.fn(),
28+
handleCopyMarkdown: vi.fn(),
29+
handleDownloadJSON: vi.fn(),
30+
handleNativeShare: vi.fn(),
31+
}),
32+
}));
33+
34+
const mockExportData: DashboardExportData = {
35+
stats: { currentStreak: 5, peakStreak: 10, totalContributions: 100 },
36+
languages: [],
37+
activity: [],
38+
};
39+
40+
const defaultProps = {
41+
username: 'octocat',
42+
isOpen: true,
43+
onClose: vi.fn(),
44+
exportData: mockExportData,
45+
};
46+
47+
describe('ShareSheet - Accessibility Standards & Screen Reader Aria Compliance', () => {
48+
beforeEach(() => {
49+
vi.clearAllMocks();
50+
});
51+
52+
it('Close button has descriptive aria-label for screen reader announcement', () => {
53+
render(<ShareSheet {...defaultProps} />);
54+
55+
const closeButton = screen.getByRole('button', { name: /close share panel/i });
56+
expect(closeButton).toBeTruthy();
57+
expect(closeButton.getAttribute('aria-label')).toBe('Close share panel');
58+
});
59+
60+
it('All interactive buttons are focusable and have accessible names', () => {
61+
render(<ShareSheet {...defaultProps} />);
62+
63+
const buttons = screen.getAllByRole('button');
64+
65+
// Every button must have an accessible name — either via text content or aria-label.
66+
// Icon-only buttons (no text, no aria-label) must at minimum have their decorative
67+
// SVG marked aria-hidden="true" so assistive technology is not given empty noise.
68+
buttons.forEach((button: HTMLElement) => {
69+
const hasAccessibleName: boolean =
70+
(button.textContent?.trim() ?? '').length > 0 ||
71+
button.getAttribute('aria-label') !== null ||
72+
button.getAttribute('aria-labelledby') !== null;
73+
74+
if (!hasAccessibleName) {
75+
// Icon-only button: assert its SVG child is hidden from assistive technology
76+
const hiddenSvg = button.querySelector('svg[aria-hidden="true"]');
77+
expect(
78+
hiddenSvg,
79+
`Icon-only button has no accessible name and its SVG is not aria-hidden: ${button.outerHTML}`
80+
).not.toBeNull();
81+
} else {
82+
expect(hasAccessibleName, `Button has no accessible name: ${button.outerHTML}`).toBe(true);
83+
}
84+
});
85+
});
86+
87+
it('Social share buttons have visible descriptive text labels for screen readers', () => {
88+
render(<ShareSheet {...defaultProps} />);
89+
90+
// Each social button must have visible text content announcing its purpose
91+
expect(screen.getByRole('button', { name: /share on x/i })).toBeTruthy();
92+
expect(screen.getByRole('button', { name: /linkedin/i })).toBeTruthy();
93+
expect(screen.getByRole('button', { name: /reddit/i })).toBeTruthy();
94+
expect(screen.getByRole('button', { name: /system share/i })).toBeTruthy();
95+
});
96+
97+
it('SVG icon elements have aria-hidden to prevent noise in screen reader output', () => {
98+
const { container } = render(<ShareSheet {...defaultProps} />);
99+
100+
// Decorative SVG icons must be hidden from assistive technology
101+
const decorativeSvgs = container.querySelectorAll('svg[aria-hidden="true"]');
102+
expect(decorativeSvgs.length).toBeGreaterThan(0);
103+
});
104+
105+
it('Profile URL input is readonly and accessible with correct semantic markup', () => {
106+
render(<ShareSheet {...defaultProps} />);
107+
108+
// The readonly URL input must be present and programmatically accessible
109+
const input = screen.getByDisplayValue(/commitpulse\.vercel\.app\/dashboard\/octocat/i);
110+
expect(input).toBeTruthy();
111+
expect(input.getAttribute('readonly')).toBeDefined();
112+
expect(input.tagName.toLowerCase()).toBe('input');
113+
});
114+
});

0 commit comments

Comments
 (0)