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>
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
import { loginViaAPI, navigateTo } from '../helpers';
|
||
import { checkOverflow } from '../helpers/visual-helpers';
|
||
import { TEST_USERS, ROUTES, VIEWPORTS } from '../design-tokens';
|
||
|
||
test.describe('RESPONSIVE — Chaque page × chaque viewport, zéro overflow', () => {
|
||
const viewportsToTest = [
|
||
VIEWPORTS.mobileSE,
|
||
VIEWPORTS.tablet,
|
||
VIEWPORTS.laptop,
|
||
VIEWPORTS.desktop,
|
||
] as const;
|
||
|
||
const viewportNames = ['mobileSE (375×667)', 'tablet (768×1024)', 'laptop (1280×720)', 'desktop (1440×900)'] as const;
|
||
|
||
// --- Pages publiques ---
|
||
for (const route of ROUTES.public) {
|
||
for (let v = 0; v < viewportsToTest.length; v++) {
|
||
const viewport = viewportsToTest[v];
|
||
const vpName = viewportNames[v];
|
||
|
||
test(`[PUBLIC] ${route.name} @ ${vpName} — pas de débordement horizontal`, async ({ page }) => {
|
||
await page.setViewportSize(viewport);
|
||
await navigateTo(page, route.path);
|
||
|
||
const overflows = await checkOverflow(page);
|
||
|
||
for (const o of overflows) {
|
||
console.log(`[OVERFLOW] ${route.path} @ ${vpName}: ${o.selector} dépasse de ${o.overflowX}px`);
|
||
console.log(` FIX: ${o.fix}`);
|
||
}
|
||
|
||
expect(overflows.length,
|
||
`${overflows.length} débordement(s) sur ${route.path} @ ${vpName}:\n` +
|
||
overflows.map(o => `• ${o.selector}: +${o.overflowX}px → ${o.fix}`).join('\n')
|
||
).toBe(0);
|
||
});
|
||
}
|
||
}
|
||
|
||
// --- Pages protégées (sélection) ---
|
||
const protectedPages = [
|
||
ROUTES.listener[0], // Dashboard
|
||
ROUTES.listener[1], // Feed
|
||
ROUTES.listener[2], // Discover
|
||
ROUTES.listener[3], // Library
|
||
ROUTES.listener[6], // Profile
|
||
ROUTES.listener[7], // Settings
|
||
ROUTES.listener[10], // Playlists
|
||
ROUTES.listener[13], // Marketplace
|
||
];
|
||
|
||
for (const route of protectedPages) {
|
||
for (let v = 0; v < viewportsToTest.length; v++) {
|
||
const viewport = viewportsToTest[v];
|
||
const vpName = viewportNames[v];
|
||
|
||
test(`[PROTECTED] ${route.name} @ ${vpName} — pas de débordement horizontal`, async ({ page }) => {
|
||
await page.setViewportSize(viewport);
|
||
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
||
await navigateTo(page, route.path);
|
||
|
||
const overflows = await checkOverflow(page);
|
||
|
||
for (const o of overflows) {
|
||
console.log(`[OVERFLOW] ${route.path} @ ${vpName}: ${o.selector} dépasse de ${o.overflowX}px`);
|
||
console.log(` FIX: ${o.fix}`);
|
||
}
|
||
|
||
expect(overflows.length,
|
||
`${overflows.length} débordement(s) sur ${route.path} @ ${vpName}:\n` +
|
||
overflows.map(o => `• ${o.selector}: +${o.overflowX}px → ${o.fix}`).join('\n')
|
||
).toBe(0);
|
||
});
|
||
}
|
||
}
|
||
|
||
// --- Vérifications mobiles spécifiques ---
|
||
test('Mobile — le sidebar est caché par défaut', async ({ page }) => {
|
||
await page.setViewportSize(VIEWPORTS.mobileSE);
|
||
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
||
await navigateTo(page, '/dashboard');
|
||
|
||
const sidebar = page.locator('[data-testid="app-sidebar"]');
|
||
// Sur mobile (<1024px), le sidebar devrait être caché ou collapsé
|
||
const sidebarVisible = await sidebar.isVisible({ timeout: 3_000 }).catch(() => false);
|
||
if (sidebarVisible) {
|
||
const box = await sidebar.boundingBox();
|
||
if (box && box.width > 100) {
|
||
console.log(`[MOBILE] Le sidebar est visible et prend ${box.width}px sur mobile — devrait être caché`);
|
||
expect(box.width, `Le sidebar est trop large sur mobile (${box.width}px). FIX: Cacher avec lg:block.`).toBeLessThan(100);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('Mobile — le contenu principal utilise toute la largeur', async ({ page }) => {
|
||
await page.setViewportSize(VIEWPORTS.mobileSE);
|
||
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
||
await navigateTo(page, '/dashboard');
|
||
|
||
const mainWidth = await page.evaluate(() => {
|
||
const main = document.querySelector('main, [role="main"]');
|
||
if (!main) return null;
|
||
const rect = main.getBoundingClientRect();
|
||
return { width: Math.round(rect.width), viewportWidth: window.innerWidth };
|
||
});
|
||
|
||
if (mainWidth) {
|
||
const usagePercent = (mainWidth.width / mainWidth.viewportWidth) * 100;
|
||
expect(usagePercent,
|
||
`Le contenu principal n'utilise que ${usagePercent.toFixed(0)}% de la largeur mobile (${mainWidth.width}px / ${mainWidth.viewportWidth}px). FIX: Retirer les margin-left/right sur mobile.`
|
||
).toBeGreaterThan(80);
|
||
}
|
||
});
|
||
|
||
test('Mobile — le player bar est visible et accessible', async ({ page }) => {
|
||
await page.setViewportSize(VIEWPORTS.mobileSE);
|
||
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
||
await navigateTo(page, '/dashboard');
|
||
|
||
// Le player bar apparaît quand un track est en lecture
|
||
// Vérifier qu'il ne déborde pas sur mobile
|
||
const playerBar = page.locator('[data-testid="global-player"]');
|
||
if (await playerBar.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
||
const box = await playerBar.boundingBox();
|
||
if (box) {
|
||
expect(box.width, `Player bar dépasse du viewport mobile: ${box.width}px > ${VIEWPORTS.mobileSE.width}px`).toBeLessThanOrEqual(VIEWPORTS.mobileSE.width + 2);
|
||
}
|
||
}
|
||
});
|
||
});
|