main
mjs 134 lines 4.33 KB
Raw
1 import { test, expect } from '@playwright/test';
2
3 const NOISE_SUPPRESSION_CSS = `
4 .cost-dashboard__date,
5 [data-cost-date],
6 .run-date,
7 .last-updated { visibility: hidden !important; }
8 .dynamic-counter, .repo-count, [data-live] { visibility: hidden !important; }
9 *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }
10 `;
11
12 const VIEWPORTS = [
13 { width: 320, height: 568 },
14 { width: 360, height: 640 },
15 { width: 390, height: 844 },
16 { width: 414, height: 896 },
17 { width: 768, height: 1024 },
18 ];
19
20 const PAGES = [
21 { key: 'home', label: 'home', path: '/' },
22 { key: 'weekly', label: 'weekly', path: '/weekly/2026/w22/' },
23 { key: 'monthly', label: 'monthly', path: '/monthly/2026/05/' },
24 { key: 'yearly', label: 'yearly', path: '/yearly/2026/' },
25 ];
26
27 async function settle(page) {
28 await page.waitForLoadState('networkidle').catch(() => page.waitForTimeout(1000));
29 await page.addStyleTag({ content: NOISE_SUPPRESSION_CSS });
30 await page.waitForTimeout(300);
31 }
32
33 async function openPage(page, path, viewport) {
34 await page.setViewportSize(viewport);
35 await page.emulateMedia({ colorScheme: 'light' });
36 await page.goto(path);
37 await settle(page);
38 }
39
40 function formatViewport({ width }) {
41 return `${width}px`;
42 }
43
44 function skipNonMatrixProject(testInfo) {
45 test.skip(testInfo.project.name !== 'desktop-light', 'A11y matrix is defined in-spec.');
46 }
47
48 for (const pageConfig of PAGES) {
49 test.describe(pageConfig.label, () => {
50 for (const viewport of VIEWPORTS) {
51 const viewportLabel = formatViewport(viewport);
52
53 test(`${pageConfig.label} — no horizontal overflow at ${viewportLabel}`, async ({ page }, testInfo) => {
54 skipNonMatrixProject(testInfo);
55 await openPage(page, pageConfig.path, viewport);
56
57 const hasNoOverflow = await page.evaluate(() => {
58 const root = document.documentElement;
59 return root.scrollWidth <= root.clientWidth;
60 });
61
62 expect(hasNoOverflow).toBe(true);
63 });
64
65 test(`${pageConfig.label} — tap targets ≥ 44x44 at ${viewportLabel}`, async ({ page }, testInfo) => {
66 skipNonMatrixProject(testInfo);
67 await openPage(page, pageConfig.path, viewport);
68
69 const violations = await page.evaluate(() => {
70 const minSize = 44;
71 const round = value => Math.round(value * 10) / 10;
72
73 return Array.from(document.querySelectorAll('a, button'))
74 .filter(element => !element.hasAttribute('data-small-ok'))
75 .map(element => {
76 const rect = element.getBoundingClientRect();
77 const style = window.getComputedStyle(element);
78
79 if (
80 style.display === 'none' ||
81 style.visibility === 'hidden' ||
82 rect.width <= 0 ||
83 rect.height <= 0
84 ) {
85 return null;
86 }
87
88 if (rect.width >= minSize && rect.height >= minSize) {
89 return null;
90 }
91
92 const label =
93 element.getAttribute('aria-label') ||
94 element.textContent?.replace(/\s+/g, ' ').trim() ||
95 element.getAttribute('href') ||
96 element.tagName.toLowerCase();
97
98 return {
99 tag: element.tagName.toLowerCase(),
100 label,
101 width: round(rect.width),
102 height: round(rect.height),
103 };
104 })
105 .filter(Boolean);
106 });
107
108 expect(violations, `Tap target violations: ${JSON.stringify(violations, null, 2)}`).toEqual([]);
109 });
110
111 if (pageConfig.key === 'home' && viewport.width <= 414) {
112 test(`${pageConfig.label} — main content starts within 600px at ${viewportLabel}`, async ({ page }, testInfo) => {
113 skipNonMatrixProject(testInfo);
114 await openPage(page, pageConfig.path, viewport);
115
116 const mainTop = await page.evaluate(() => {
117 const main =
118 document.querySelector('.main') ||
119 document.querySelector('main') ||
120 document.querySelector('#main-content');
121
122 if (!main) {
123 throw new Error('No main content element found');
124 }
125
126 return main.getBoundingClientRect().top;
127 });
128
129 expect(mainTop).toBeLessThanOrEqual(600);
130 });
131 }
132 }
133 });
134 }