import { test, expect } from '@chromatic-com/playwright'; import { loginViaAPI, CONFIG, navigateTo, playFirstTrack } from './helpers'; /** * VISUAL BUGS — Tests ciblés pour prévenir les bugs visuels * Touch targets, images cassées, overflow, contraste */ test.describe('VISUAL — Touch targets mobile @visual @a11y @mobile', () => { test.use({ viewport: { width: 375, height: 667 } }); test('Player controls — touch targets ≥ 44x44px', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await navigateTo(page, '/discover'); await playFirstTrack(page); await page.waitForTimeout(2000); const playerBar = page.getByTestId('player-bar').or(page.getByTestId('global-player')); if (await playerBar.isVisible({ timeout: 5000 }).catch(() => false)) { const buttons = await playerBar.locator('button').all(); const tooSmall: string[] = []; for (const btn of buttons) { if (await btn.isVisible().catch(() => false)) { const box = await btn.boundingBox(); if (box && (box.width < 32 || box.height < 32)) { const label = await btn.getAttribute('aria-label') || await btn.textContent() || 'unknown'; tooSmall.push(`${label}: ${box.width}x${box.height}`); } } } if (tooSmall.length > 0) { console.warn(`⚠ Small touch targets: ${tooSmall.join(', ')}`); } } }); test('Sidebar — pas de débordement horizontal sur mobile', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await navigateTo(page, '/dashboard'); const hasOverflow = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasOverflow).toBeFalsy(); }); test('Login form — centré et pas de débordement', async ({ page }) => { await page.goto('/login'); await page.waitForLoadState('networkidle').catch(() => {}); const hasOverflow = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasOverflow).toBeFalsy(); }); }); test.describe('VISUAL — Images cassées @visual', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('Discover — aucune image cassée', async ({ page }) => { await navigateTo(page, '/discover'); await page.waitForTimeout(3000); const brokenImages = await page.evaluate(() => { const imgs = document.querySelectorAll('img'); return Array.from(imgs) .filter(img => img.src && !img.complete && img.naturalWidth === 0) .map(img => ({ src: img.src, alt: img.alt })); }); if (brokenImages.length > 0) { console.warn(`⚠ Broken images on /discover: ${JSON.stringify(brokenImages)}`); } }); test('Library — aucune image cassée', async ({ page }) => { await navigateTo(page, '/library'); await page.waitForTimeout(3000); const brokenImages = await page.evaluate(() => { const imgs = document.querySelectorAll('img'); return Array.from(imgs) .filter(img => img.src && !img.complete && img.naturalWidth === 0) .map(img => ({ src: img.src, alt: img.alt })); }); if (brokenImages.length > 0) { console.warn(`⚠ Broken images on /library: ${JSON.stringify(brokenImages)}`); } }); test('Marketplace — aucune image cassée', async ({ page }) => { await navigateTo(page, '/marketplace'); await page.waitForTimeout(3000); const brokenImages = await page.evaluate(() => { const imgs = document.querySelectorAll('img'); return Array.from(imgs) .filter(img => img.src && !img.complete && img.naturalWidth === 0) .map(img => ({ src: img.src, alt: img.alt })); }); if (brokenImages.length > 0) { console.warn(`⚠ Broken images on /marketplace: ${JSON.stringify(brokenImages)}`); } }); }); test.describe('VISUAL — Layout responsive @visual', () => { test.setTimeout(60_000); const viewports = [ { width: 375, height: 667, name: 'iPhone SE' }, { width: 390, height: 844, name: 'iPhone 14' }, { width: 768, height: 1024, name: 'iPad' }, { width: 1280, height: 720, name: 'Desktop' }, ]; for (const vp of viewports) { test(`Pas de débordement horizontal sur ${vp.name} (${vp.width}x${vp.height}) @mobile`, async ({ page }) => { await page.setViewportSize({ width: vp.width, height: vp.height }); await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); const pages = ['/dashboard', '/discover', '/library', '/playlists']; for (const path of pages) { await navigateTo(page, path); const hasOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth, ); expect(hasOverflow, `Overflow on ${path} at ${vp.width}x${vp.height}`).toBeFalsy(); } }); } }); test.describe('VISUAL — Contraste et accessibilité @visual @a11y', () => { test('Messages d\'erreur sur login ont un contraste suffisant', async ({ page }) => { await page.goto('/login'); await page.waitForLoadState('networkidle').catch(() => {}); // Submit empty form to trigger validation const submitBtn = page.getByRole('button', { name: /sign in|connexion|se connecter/i }).first(); if (await submitBtn.isVisible({ timeout: 5000 }).catch(() => false)) { await submitBtn.click(); await page.waitForTimeout(500); // Check error messages exist and are visible const errorElements = await page.locator('[class*="destructive"], [role="alert"], [class*="error"]').all(); for (const el of errorElements) { if (await el.isVisible().catch(() => false)) { const color = await el.evaluate(e => getComputedStyle(e).color); const opacity = await el.evaluate(e => getComputedStyle(e).opacity); // Ensure text is not invisible expect(parseFloat(opacity)).toBeGreaterThan(0.5); console.log(`Error element: color=${color}, opacity=${opacity}`); } } } }); test('Focus ring visible sur les éléments interactifs', async ({ page }) => { await page.goto('/login'); await page.waitForLoadState('networkidle').catch(() => {}); // Tab through elements await page.keyboard.press('Tab'); await page.waitForTimeout(200); const focusedElement = page.locator(':focus'); if (await focusedElement.isVisible().catch(() => false)) { const outline = await focusedElement.evaluate(e => { const styles = getComputedStyle(e); return { outline: styles.outline, boxShadow: styles.boxShadow, border: styles.borderColor, }; }); // Should have some visible focus indicator const hasFocusIndicator = outline.outline !== 'none' || outline.boxShadow !== 'none' || outline.border !== ''; if (!hasFocusIndicator) { console.warn('⚠ No visible focus indicator on first tab target'); } } }); });