veza/tests/e2e/discover.spec.ts

261 lines
9.6 KiB
TypeScript
Raw Normal View History

/**
* 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);
});
});
});