veza/tests/e2e/discover.spec.ts
senke 9a4c0d2af4 feat(web): update all features, stories, e2e tests, and auth interceptor
Update auth, playlists, tracks, search, profile, dashboard, player,
settings, and social features. Add e2e audit specs for all major pages.
Update ESLint config, vitest config, and route configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:16:36 +02:00

260 lines
9.6 KiB
TypeScript

/**
* 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<void> {
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=<script>alert(1)</script>');
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);
});
});
});