veza/tests/e2e/audit/pixel-perfect/12-tap-targets.spec.ts
senke 0ceb98c322 fix(a11y): fix primary button contrast ratio + tap-target test false positives
- Fix --sumi-text-inverse: #13110f → #f5f0e8 (was dark-on-dark)
  Primary buttons now have ~4.8:1 contrast ratio (WCAG AA pass)
  Affects: Sign In, Register, all primary action buttons

- Tap-target test: skip sr-only elements (intentionally invisible)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:53:51 +01:00

108 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '@chromatic-com/playwright';
import { loginViaAPI, navigateTo } from '../helpers';
import { TEST_USERS, ROUTES, VIEWPORTS } from '../design-tokens';
test.describe('TAP TARGETS — Chaque élément cliquable fait au moins 44×44px', () => {
// Test mobile viewport — c'est là que les tap targets comptent le plus
for (const route of ROUTES.public) {
test(`[PUBLIC] ${route.name} @ mobile — zones cliquables >= 44×44px`, async ({ page }) => {
await page.setViewportSize(VIEWPORTS.mobileSE);
await navigateTo(page, route.path);
const tooSmall = await page.evaluate(() => {
const issues: Array<{ selector: string; text: string; width: number; height: number; fix: string }> = [];
document.querySelectorAll('button, a[href], input, select, [role="button"], [role="tab"], [role="checkbox"], [role="switch"]').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return;
// Skip screen-reader-only elements (intentionally invisible)
if (style.position === 'absolute' && style.overflow === 'hidden' && (rect.width <= 1 || rect.height <= 1)) return;
if (el.classList.contains('sr-only')) return;
// Skip éléments hors viewport
if (rect.bottom < 0 || rect.top > window.innerHeight) return;
if (rect.width < 44 || rect.height < 44) {
const text = el.textContent?.trim().slice(0, 20) || el.getAttribute('aria-label') || '';
const testid = el.getAttribute('data-testid');
const className = (typeof el.className === 'string' ? el.className : '').split(' ')[0] || '';
const selector = testid ? `[data-testid="${testid}"]` : `${el.tagName.toLowerCase()}.${className}`;
const widthFix = rect.width < 44 ? `min-w-[44px]` : '';
const heightFix = rect.height < 44 ? `min-h-[44px]` : '';
issues.push({
selector,
text,
width: Math.round(rect.width),
height: Math.round(rect.height),
fix: `ÉLÉMENT: ${selector} "${text}" | PAGE: ${location.pathname} | MESURÉ: ${Math.round(rect.width)}×${Math.round(rect.height)}px | ATTENDU: >=44×44px (WCAG 2.5.8) | FIX TAILWIND: Ajouter ${[widthFix, heightFix].filter(Boolean).join(' ')} sur ${selector}, ou augmenter padding: p-3`,
});
}
});
return issues;
});
const critical = tooSmall.filter(i => i.width < 30 || i.height < 30);
for (const issue of tooSmall) {
console.log(`[TAP TARGET] ${issue.fix}`);
}
expect(critical.length,
`${critical.length} bouton(s) trop petit(s) (<30px) sur ${route.path}:\n` +
critical.map(i => `${i.fix}`).join('\n')
).toBe(0);
});
}
for (const route of ROUTES.listener.slice(0, 10)) {
test(`[PROTECTED] ${route.name} @ mobile — zones cliquables >= 44×44px`, async ({ page }) => {
await page.setViewportSize(VIEWPORTS.mobileSE);
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
await navigateTo(page, route.path);
const tooSmall = await page.evaluate(() => {
const issues: Array<{ selector: string; text: string; width: number; height: number; fix: string }> = [];
document.querySelectorAll('button, a[href], input, select, [role="button"], [role="tab"], [role="checkbox"], [role="switch"]').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return;
if (rect.bottom < 0 || rect.top > window.innerHeight) return;
if (rect.width < 44 || rect.height < 44) {
const text = el.textContent?.trim().slice(0, 20) || el.getAttribute('aria-label') || '';
const testid = el.getAttribute('data-testid');
const className = (typeof el.className === 'string' ? el.className : '').split(' ')[0] || '';
const selector = testid ? `[data-testid="${testid}"]` : `${el.tagName.toLowerCase()}.${className}`;
issues.push({
selector,
text,
width: Math.round(rect.width),
height: Math.round(rect.height),
fix: `ÉLÉMENT: ${selector} "${text}" | PAGE: ${location.pathname} | MESURÉ: ${Math.round(rect.width)}×${Math.round(rect.height)}px | ATTENDU: >=44×44px | FIX TAILWIND: Ajouter min-w-[44px] min-h-[44px] ou p-3`,
});
}
});
return issues;
});
const critical = tooSmall.filter(i => i.width < 30 || i.height < 30);
for (const issue of tooSmall) {
console.log(`[TAP TARGET] ${issue.fix}`);
}
expect(critical.length,
`${critical.length} bouton(s) trop petit(s) (<30px) sur ${route.path}:\n` +
critical.map(i => `${i.fix}`).join('\n')
).toBe(0);
});
}
});