/** * E2E Tests — Page Découverte musicale (/discover) * * Tests exhaustifs : chargement, fonctionnalités, sécurité, a11y, i18n, responsive, régression. */ import { test, expect } from '@playwright/test'; import { CONFIG, loginViaAPI, navigateTo } from './helpers'; // --------------------------------------------------------------------------- // HELPERS // --------------------------------------------------------------------------- async function goToDiscover(page: import('@playwright/test').Page): Promise { await loginViaAPI(page, 'user@veza.music', 'User123!'); await navigateTo(page, '/discover'); } // --------------------------------------------------------------------------- // TESTS // --------------------------------------------------------------------------- test.describe('Découverte musicale (/discover)', () => { test.describe('Chargement & Rendu', () => { test('la page se charge sans erreur', async ({ page }) => { const errors: string[] = []; page.on('pageerror', (err) => errors.push(err.message)); await goToDiscover(page); // Page loaded, not redirected expect(page.url()).toContain('/discover'); // Title visible await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); // No critical JS errors (filter out audio playback errors) const criticalErrors = errors.filter( (e) => !e.includes('audio') && !e.includes('playback') && !e.includes('download'), ); expect(criticalErrors).toHaveLength(0); }); test('le titre "Discover" et le sous-titre sont visibles', async ({ page }) => { await goToDiscover(page); await expect(page.getByRole('heading', { name: /discover/i, level: 1 })).toBeVisible(); // Subtitle const body = await page.textContent('body'); expect(body).toMatch(/explore by genre|explorez par genre|explora por/i); }); test('les genre cards sont affichées', async ({ page }) => { await goToDiscover(page); // At least some genre buttons should be visible const genreButtons = page.locator('button[aria-label*="Browse"]'); const count = await genreButtons.count(); expect(count).toBeGreaterThan(5); }); test('la section Editorial Playlists est présente', async ({ page }) => { await goToDiscover(page); await expect(page.getByRole('heading', { name: /editorial playlist/i })).toBeVisible(); }); }); test.describe('Fonctionnalités', () => { test('cliquer sur un genre navigue vers ?genre=slug', async ({ page }) => { await goToDiscover(page); // Click a genre button const genreBtn = page.locator('button[aria-label*="Browse"]').first(); await genreBtn.click(); await page.waitForTimeout(1_000); // URL should have ?genre= parameter expect(page.url()).toMatch(/\?genre=/); // Title should change to the genre name const h1 = page.getByRole('heading', { level: 1 }); await expect(h1).toBeVisible(); const title = await h1.textContent(); expect(title).not.toBe('Discover'); }); test('le bouton Back ramène à la liste des genres', async ({ page }) => { await goToDiscover(page); // Click genre await page.locator('button[aria-label*="Browse"]').first().click(); await page.waitForTimeout(1_000); expect(page.url()).toMatch(/\?genre=/); // Click Back await page.getByRole('button', { name: /back|retour|volver/i }).click(); await page.waitForTimeout(1_000); // Should be back on /discover without query params expect(page.url()).toBe(`${CONFIG.baseURL}/discover`); await expect(page.getByRole('heading', { name: /discover/i, level: 1 })).toBeVisible(); }); test('la section genres a un empty state quand no tracks', async ({ page }) => { await goToDiscover(page); // Click a genre await page.locator('button[aria-label*="Browse"]').first().click(); await page.waitForTimeout(2_000); // Should show empty message or tracks const body = await page.textContent('body'); expect(body).toMatch(/no tracks|aucune piste|no hay pistas|track/i); }); test('editorial playlists empty state affiché quand vide', async ({ page }) => { await goToDiscover(page); // The empty state message should be visible when no editorial playlists const section = page.getByRole('region', { name: /editorial/i }); const sectionText = await section.textContent(); // Either has playlist cards or the empty message expect(sectionText).toMatch( /no editorial|aucune playlist|no hay playlist|playlist/i, ); }); }); test.describe('Sécurité', () => { test('pas de fuite de tokens sensibles dans le DOM', async ({ page }) => { await goToDiscover(page); const html = await page.content(); expect(html).not.toMatch(/access_token/); expect(html).not.toMatch(/refresh_token/); }); test('XSS via query param genre ne s\'exécute pas', async ({ page }) => { let alertTriggered = false; page.on('dialog', (dialog) => { alertTriggered = true; dialog.dismiss(); }); await loginViaAPI(page, 'user@veza.music', 'User123!'); await navigateTo(page, '/discover?genre='); expect(alertTriggered).toBe(false); // Page should not crash expect(page.url()).toContain('/discover'); }); }); test.describe('Accessibilité', () => { test('les genre buttons ont des aria-labels descriptifs', async ({ page }) => { await goToDiscover(page); const genreButtons = page.locator('button[aria-label*="Browse"]'); const count = await genreButtons.count(); expect(count).toBeGreaterThan(0); // Check first button has descriptive label const label = await genreButtons.first().getAttribute('aria-label'); expect(label).toMatch(/browse .+ tracks/i); }); test('les sections ont des rôles region avec aria-label', async ({ page }) => { await goToDiscover(page); await expect(page.getByRole('region', { name: /by genre|par genre/i })).toBeVisible(); await expect(page.getByRole('region', { name: /editorial/i })).toBeVisible(); }); test('focus order logique via Tab', async ({ page }) => { await goToDiscover(page); // Tab should hit interactive elements await page.keyboard.press('Tab'); const firstFocused = page.locator(':focus'); const tag = await firstFocused.evaluate((el) => el.tagName.toLowerCase()); // Should be a link or button (skip to content or sidebar) expect(['a', 'button']).toContain(tag); }); }); test.describe('i18n', () => { test('pas de clés i18n brutes affichées', async ({ page }) => { await goToDiscover(page); const bodyText = await page.textContent('body') ?? ''; expect(bodyText).not.toMatch(/discover\.title/); expect(bodyText).not.toMatch(/discover\.subtitle/); expect(bodyText).not.toMatch(/discover\.byGenre/); expect(bodyText).not.toMatch(/discover\.editorialPlaylists/); }); test('pas de mélange FR/EN', async ({ page }) => { await goToDiscover(page); const bodyText = await page.textContent('body') ?? ''; // In English mode, should not have hardcoded French if (bodyText.includes('Discover')) { expect(bodyText).not.toContain('Explorez par genre'); expect(bodyText).not.toContain('Par genre'); expect(bodyText).not.toContain('Playlists éditoriales'); } }); }); test.describe('Responsive', () => { test('tablet 768px — la page se charge correctement', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await goToDiscover(page); await expect(page.getByRole('heading', { name: /discover/i, level: 1 })).toBeVisible(); const genreButtons = page.locator('button[aria-label*="Browse"]'); expect(await genreButtons.count()).toBeGreaterThan(0); }); test('desktop 1280px — layout complet avec grille 5 colonnes', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 800 }); await goToDiscover(page); await expect(page.getByRole('heading', { name: /discover/i, level: 1 })).toBeVisible(); const genreButtons = page.locator('button[aria-label*="Browse"]'); expect(await genreButtons.count()).toBeGreaterThan(0); }); }); test.describe('Régression', () => { test('[BUG#1] i18n — textes traduits, pas de hardcoded English', async ({ page }) => { await goToDiscover(page); // Title uses i18n key await expect(page.getByRole('heading', { name: /discover/i, level: 1 })).toBeVisible(); // Subtitle uses i18n key const body = await page.textContent('body'); expect(body).toMatch(/explore by genre|explorez par genre|explora por/i); }); test('[BUG#2] editorial playlists — pas de heading orphelin quand vide', async ({ page }) => { await goToDiscover(page); const section = page.getByRole('region', { name: /editorial/i }); await expect(section).toBeVisible(); const text = await section.textContent(); // Must have either playlist content or empty state text (not just heading) expect(text!.length).toBeGreaterThan(25); }); test('[BUG#3] genre buttons — ont des aria-labels descriptifs', async ({ page }) => { await goToDiscover(page); const firstBtn = page.locator('button[aria-label*="Browse"]').first(); await expect(firstBtn).toBeVisible(); const label = await firstBtn.getAttribute('aria-label'); expect(label).toMatch(/browse .+ tracks/i); }); }); });