Refine auth, player, tracks, playlists, search, workflows, edge cases, forms, responsive, network errors, error boundary, performance, visual regression, cross-browser, profile, smoke, storybook, chat, and session tests. Add audit test suite (accessibility, ethical, functional, design tokens). Update test helpers and visual snapshots. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
105 lines
4.9 KiB
TypeScript
105 lines
4.9 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
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 é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);
|
||
});
|
||
}
|
||
});
|