veza/tests/e2e/audit/pixel-perfect/01-element-overlap.spec.ts

112 lines
5 KiB
TypeScript
Raw Normal View History

import { test, expect } from '@chromatic-com/playwright';
import { loginViaAPI, navigateTo, navigateToPageWithTracks, playFirstTrack } from '../helpers';
import { detectOverlaps } from '../helpers/visual-helpers';
import { TEST_USERS, ROUTES, ALL_PROTECTED_ROUTES } from '../design-tokens';
test.describe('CHEVAUCHEMENTS — Aucun élément interactif ne passe par-dessus un autre', () => {
// --- Pages publiques (pas de login) ---
for (const route of ROUTES.public) {
test(`[PUBLIC] ${route.name} (${route.path}) — zéro chevauchement critique`, async ({ page }) => {
await navigateTo(page, route.path);
const overlaps = await detectOverlaps(page);
const critical = overlaps.filter(o => o.severity === 'critical');
for (const o of overlaps) {
console.log(`[${o.severity.toUpperCase()}] "${o.elementA.text}" ↔ "${o.elementB.text}" : ${o.overlapX}px × ${o.overlapY}px`);
console.log(` FIX: ${o.fix}`);
}
expect(critical.length,
`${critical.length} chevauchement(s) critique(s) sur ${route.path}:\n` +
critical.map(o => `• "${o.elementA.text}" (${o.elementA.rect.x},${o.elementA.rect.y} ${o.elementA.rect.width}×${o.elementA.rect.height}) ↔ "${o.elementB.text}" (${o.elementB.rect.x},${o.elementB.rect.y} ${o.elementB.rect.width}×${o.elementB.rect.height}) overlap: ${o.overlapX}×${o.overlapY}px → ${o.fix}`).join('\n')
).toBe(0);
});
}
// --- Pages protégées (listener) ---
for (const route of ROUTES.listener) {
test(`[LISTENER] ${route.name} (${route.path}) — zéro chevauchement critique`, async ({ page }) => {
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
await navigateTo(page, route.path);
const overlaps = await detectOverlaps(page);
const critical = overlaps.filter(o => o.severity === 'critical');
for (const o of overlaps) {
console.log(`[${o.severity.toUpperCase()}] "${o.elementA.text}" ↔ "${o.elementB.text}" : ${o.overlapX}px × ${o.overlapY}px`);
console.log(` FIX: ${o.fix}`);
}
expect(critical.length,
`${critical.length} chevauchement(s) critique(s) sur ${route.path}:\n` +
critical.map(o => `• "${o.elementA.text}" ↔ "${o.elementB.text}" ${o.overlapX}×${o.overlapY}px → ${o.fix}`).join('\n')
).toBe(0);
});
}
// --- Player bar ne recouvre pas le contenu ---
test('Le player bar ne recouvre aucun contenu interactif de la page', async ({ page }) => {
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
const hasTracks = await navigateToPageWithTracks(page);
if (!hasTracks) {
console.log('Pas de tracks disponibles — test player bar non applicable');
return;
}
await playFirstTrack(page);
await page.waitForTimeout(2_000);
const playerIssues = await page.evaluate(() => {
const player = document.querySelector('[data-testid="global-player"]');
if (!player) return ['Player bar non trouvé'];
const playerRect = player.getBoundingClientRect();
const issues: string[] = [];
// Vérifier que le main content a assez de padding en bas
const main = document.querySelector('main, [role="main"]');
if (main) {
const mainRect = main.getBoundingClientRect();
const mainPaddingBottom = parseFloat(getComputedStyle(main).paddingBottom);
if (mainPaddingBottom < playerRect.height) {
issues.push(
`Le main content a padding-bottom=${mainPaddingBottom}px mais le player fait ${Math.round(playerRect.height)}px. ` +
`Les derniers éléments du contenu seront cachés sous le player. ` +
`FIX: Ajouter pb-[${Math.ceil(playerRect.height + 16)}px] au conteneur principal.`
);
}
}
// Vérifier qu'aucun bouton du contenu n'est sous le player
document.querySelectorAll('main button, main a, main input').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
if (getComputedStyle(el).display === 'none') return;
const overlapY = Math.max(0, Math.min(rect.bottom, playerRect.bottom) - Math.max(rect.top, playerRect.top));
const overlapX = Math.max(0, Math.min(rect.right, playerRect.right) - Math.max(rect.left, playerRect.left));
if (overlapX > 0 && overlapY > 10) {
const text = el.textContent?.trim().slice(0, 30) || el.getAttribute('aria-label') || '';
issues.push(
`"${text}" (${el.tagName.toLowerCase()}) est recouvert par le player bar de ${Math.round(overlapY)}px. ` +
`Position: y=${Math.round(rect.top)} vs player.top=${Math.round(playerRect.top)}`
);
}
});
return issues;
});
for (const issue of playerIssues) {
console.log(`[PLAYER OVERLAP] ${issue}`);
}
expect(playerIssues.length,
`Le player bar recouvre du contenu:\n${playerIssues.join('\n')}`
).toBe(0);
});
});