|
2 | 2 | * WCAG Compliance Tests |
3 | 3 | * Tests for Web Content Accessibility Guidelines compliance |
4 | 4 | */ |
5 | | -import { BasePage, test, expect } from '@test/e2e/helpers' |
| 5 | +import { BasePage, describe, test, expect } from '@test/e2e/helpers' |
6 | 6 | import { waitForAnimationFrames } from '@test/e2e/helpers/waitHelpers' |
7 | 7 |
|
8 | 8 | // @TODO: The purpose of this file is to have a special test for the "a11y" theme that should meet the higher WCAG 2.2 standards. Those standards require higher contrast ratios and other improvements. |
9 | 9 |
|
10 | | -test.describe('WCAG Compliance', () => { |
11 | | - /** |
12 | | - * target-size rule is disabled in Axe by default!! |
13 | | - * |
14 | | - * Axe can checks if touch targets are at least 24x24 CSS pixels. If a target is smaller |
15 | | - * than 24x24 pixels, it must be at least 24 pixels away from any other touch target. |
16 | | - * A touch target size of at least 44x44 pixels is part of the WCAG 2.1 AAA guidelines, |
17 | | - * which is a more stringent level of compliance, |
18 | | - */ |
19 | | - test.skip('@wip touch targets are at least 44x44 pixels', async ({ page: playwrightPage }) => { |
20 | | - const page = await BasePage.init(playwrightPage) |
21 | | - await page.goto('/') |
22 | | - |
23 | | - const buttons = page.page.locator('button, a') |
24 | | - const count = await buttons.count() |
25 | | - |
26 | | - let validButtonsChecked = 0 |
27 | | - for (let i = 0; i < count && validButtonsChecked < 10; i++) { |
28 | | - const button = buttons.nth(i) |
29 | | - const box = await button.boundingBox() |
30 | | - |
31 | | - if (box && (await button.isVisible()) && box.width > 5 && box.height > 5) { |
32 | | - // 44x44 is WCAG AAA, 24x24 is AA |
33 | | - expect(box.width).toBeGreaterThan(20) |
34 | | - expect(box.height).toBeGreaterThan(20) |
35 | | - validButtonsChecked++ |
36 | | - } |
37 | | - } |
38 | | - |
39 | | - // Ensure we actually checked some buttons |
40 | | - expect(validButtonsChecked).toBeGreaterThan(0) |
41 | | - }) |
42 | | - |
| 10 | +describe('WCAG Compliance', () => { |
43 | 11 | /** |
44 | 12 | * Axe by defaults checks for color contrast of at least 4.5:1 for small text or 3:1 for |
45 | 13 | * large text, even if text is part of an image. Large text has been defined in the |
@@ -67,194 +35,4 @@ test.describe('WCAG Compliance', () => { |
67 | 35 | expect(contrast.color).toBeTruthy() |
68 | 36 | } |
69 | 37 | }) |
70 | | - |
71 | | - /** |
72 | | - * Axe checks for visible focus indicators. The Keyboard Guided Test in axe DevTools Pro |
73 | | - * provides more comprehensive automated testing by simulating a keyboard navigation through |
74 | | - * all focusable elements on the page. It can identify issues such as missing focus indicators, |
75 | | - * missing ARIA roles, and other keyboard-related accessibility problems. Manual testing is |
76 | | - * still necessary to ensure all specific requirements of the accessibility standards, such as |
77 | | - * those in WCAG 2.2, are fully met. This includes checking that the focus indicator meets |
78 | | - * minimum size and contrast criteria relative to adjacent colors, not just the background color. |
79 | | - */ |
80 | | - test.skip('@ready focus indicators are visible', async ({ page: playwrightPage }) => { |
81 | | - const page = await BasePage.init(playwrightPage) |
82 | | - await page.goto('/') |
83 | | - |
84 | | - await page.pressKey('Tab') |
85 | | - await page.pressKey('Tab') |
86 | | - |
87 | | - interface FocusIndicator { |
88 | | - outline: string, |
89 | | - outlineColor: string, |
90 | | - outlineWidth: string, |
91 | | - } |
92 | | - |
93 | | - const focusIndicator = await page.evaluate<FocusIndicator | null>(() => { |
94 | | - const el = document.activeElement |
95 | | - if (!el) return null |
96 | | - |
97 | | - const styles = window.getComputedStyle(el) |
98 | | - return { |
99 | | - outline: styles.outline, |
100 | | - outlineColor: styles.outlineColor, |
101 | | - outlineWidth: styles.outlineWidth, |
102 | | - } |
103 | | - }) |
104 | | - |
105 | | - // Should have visible focus indicator |
106 | | - expect(focusIndicator?.outline !== 'none' || focusIndicator?.outlineWidth !== '0px').toBe(true) |
107 | | - }) |
108 | | - |
109 | | - /** |
110 | | - * Axe checks that the user-scalable="no" parameter is not present in the <meta name="viewport"> |
111 | | - * element and the maximum-scale parameter is not less than 2. |
112 | | - */ |
113 | | - test.skip('@ready page can be zoomed to 200%', async ({ page: playwrightPage }) => { |
114 | | - const page = await BasePage.init(playwrightPage) |
115 | | - |
116 | | - await page.goto('/') |
117 | | - |
118 | | - // Zoom in |
119 | | - await page.evaluate(() => { |
120 | | - document.body.style.zoom = '2' |
121 | | - }) |
122 | | - |
123 | | - await waitForAnimationFrames(page.page, 30) |
124 | | - |
125 | | - // Content should still be accessible |
126 | | - await page.expectMainElement() |
127 | | - |
128 | | - // No horizontal scroll should be needed at 200% zoom (in most cases) |
129 | | - const hasHorizontalScroll = await page.evaluate(() => { |
130 | | - return document.documentElement.scrollWidth > window.innerWidth |
131 | | - }) |
132 | | - |
133 | | - // This may be acceptable in some cases, just checking |
134 | | - expect(typeof hasHorizontalScroll).toBe('boolean') |
135 | | - }) |
136 | | - |
137 | | - /** |
138 | | - * Axe checks that links are distinguishable from text by verifying that they have |
139 | | - * a visual distinction that does not rely solely on color, such as an underline. It |
140 | | - * checks for a color contrast of at least \(3:1\) between the link and the surrounding |
141 | | - * text, and if the contrast is less, it requires a non-color visual distinction like |
142 | | - * an underline. Axe also checks if links have a distinct style on focus and hover. |
143 | | - */ |
144 | | - test.skip('@ready links are distinguishable from text', async ({ page: playwrightPage }) => { |
145 | | - const page = await BasePage.init(playwrightPage) |
146 | | - |
147 | | - await page.goto('/') |
148 | | - |
149 | | - const link = page.page.locator('a[href]').first() |
150 | | - const styles = await link.evaluate((el) => { |
151 | | - const computed = window.getComputedStyle(el) |
152 | | - return { |
153 | | - textDecoration: computed.textDecoration, |
154 | | - fontWeight: computed.fontWeight, |
155 | | - } |
156 | | - }) |
157 | | - |
158 | | - // Should have underline or other non-color indicator |
159 | | - const hasUnderline = styles.textDecoration.includes('underline') |
160 | | - const isBold = parseInt(styles.fontWeight) >= 600 |
161 | | - |
162 | | - // Test passes if there's some non-color distinction |
163 | | - expect(hasUnderline || isBold || typeof styles.textDecoration === 'string').toBe(true) |
164 | | - }) |
165 | | - |
166 | | - /** |
167 | | - * Axe checks that no content flashes more than 3 times per second. It identifies violations |
168 | | - * of both the general flash threshold and the more restrictive "three flashes" rule. |
169 | | - */ |
170 | | - test.skip('@ready no content flashes more than 3 times per second', async ({ page: playwrightPage }) => { |
171 | | - const page = await BasePage.init(playwrightPage) |
172 | | - await page.goto('/') |
173 | | - |
174 | | - // Check for animations |
175 | | - const animations = await page.evaluate(() => { |
176 | | - const elements = document.querySelectorAll('*') |
177 | | - const animated = [] |
178 | | - |
179 | | - elements.forEach((el) => { |
180 | | - const styles = window.getComputedStyle(el) |
181 | | - if (styles.animation !== 'none' || styles.transition !== 'all 0s ease 0s') { |
182 | | - animated.push({ |
183 | | - animation: styles.animation, |
184 | | - transition: styles.transition, |
185 | | - }) |
186 | | - } |
187 | | - }) |
188 | | - |
189 | | - return animated.length |
190 | | - }) |
191 | | - |
192 | | - // Test passes - just checking for animations presence |
193 | | - expect(animations).toBeGreaterThanOrEqual(0) |
194 | | - }) |
195 | | - |
196 | | - /** |
197 | | - * Axe does not check if prefers-reduced-motion is respected; this is considered |
198 | | - * a complex, context-dependent check that requires manual inspection. |
199 | | - */ |
200 | | - test.skip('@ready page is usable without motion', async ({ page: playwrightPage }) => { |
201 | | - const page = await BasePage.init(playwrightPage) |
202 | | - |
203 | | - await page.page.emulateMedia({ reducedMotion: 'reduce' }) |
204 | | - await page.goto('/') |
205 | | - |
206 | | - // Check that animations are disabled/reduced |
207 | | - const hasReducedMotion = await page.evaluate(() => { |
208 | | - return window.matchMedia('(prefers-reduced-motion: reduce)').matches |
209 | | - }) |
210 | | - |
211 | | - expect(hasReducedMotion).toBe(true) |
212 | | - |
213 | | - // Content should still be accessible |
214 | | - await page.expectMainElement() |
215 | | - }) |
216 | | - |
217 | | - /** |
218 | | - * Axe detects if form error indicators are technically accessible to assistive |
219 | | - * technologies like screen readers. A manual check is necessary to confirm that |
220 | | - * correctly entered information remains in the form after a validation error. |
221 | | - * When a page reloads after a server-side error, a manual test is needed to |
222 | | - * ensure that focus is moved to the top of the form or the first field with an |
223 | | - * error. For client-side validation, a manual check confirms that focus shifts |
224 | | - * to the invalid field or an error summary. |
225 | | - */ |
226 | | - test.skip('@wip form errors are clearly identified', async ({ page: playwrightPage }) => { |
227 | | - const page = await BasePage.init(playwrightPage) |
228 | | - |
229 | | - await page.goto('/contact') |
230 | | - |
231 | | - const submitButton = page.page.locator('button[type="submit"]').first() |
232 | | - await submitButton.click() |
233 | | - |
234 | | - const errors = page.page.locator('[data-error], .error, [role="alert"]') |
235 | | - await errors.first().waitFor({ state: 'visible' }) |
236 | | - const count = await errors.count() |
237 | | - |
238 | | - expect(count).toBeGreaterThan(0) |
239 | | - |
240 | | - // Error should be descriptive |
241 | | - const errorText = await errors.first().textContent() |
242 | | - expect(errorText?.trim().length).toBeGreaterThan(5) |
243 | | - }) |
244 | | - |
245 | | - /** |
246 | | - * ? |
247 | | - */ |
248 | | - test.skip('@ready time limits can be extended', async ({ page: playwrightPage }) => { |
249 | | - const page = await BasePage.init(playwrightPage) |
250 | | - |
251 | | - await page.goto('/') |
252 | | - |
253 | | - // Check for timers or session warnings |
254 | | - const timer = page.page.locator('[data-timer], [data-timeout]') |
255 | | - const count = await timer.count() |
256 | | - |
257 | | - // Test passes regardless - just checking for presence |
258 | | - expect(count).toBeGreaterThanOrEqual(0) |
259 | | - }) |
260 | 38 | }) |
0 commit comments