veza/tests/e2e/tracks-detail-audit.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

363 lines
11 KiB
TypeScript

/**
* E2E tests — Track Detail Page (/tracks/:id)
* Covers: loading, features, security, a11y, i18n, responsive, regression
*/
import { test, expect } from '@playwright/test';
import { loginViaAPI, CONFIG, navigateTo } from './helpers';
// Discover a valid track ID dynamically via API
async function getValidTrackId(page: import('@playwright/test').Page): Promise<string | null> {
const result = await page.evaluate(() =>
fetch('/api/v1/tracks?page=1&limit=1')
.then((r) => r.json())
.then((d) => d.data?.tracks?.[0]?.id ?? null),
);
return result as string | null;
}
test.describe('Track Detail (/tracks/:id)', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test.describe('Chargement & Rendu', () => {
test('page loads without crash @critical', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 15_000 });
// Should show track title as h1
const heading = page.locator('h1');
await expect(heading).toBeVisible({ timeout: 10_000 });
});
test('no JS errors on valid track', async ({ page }) => {
const jsErrors: string[] = [];
page.on('pageerror', (error) => {
jsErrors.push(error.message);
});
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
await page.waitForLoadState('networkidle').catch(() => {});
const criticalErrors = jsErrors.filter(
(e) => !e.includes('React DevTools'),
);
expect(criticalErrors).toHaveLength(0);
});
test('all key visual elements present', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Back button
const backBtn = page.getByRole('button', { name: /back/i });
await expect(backBtn).toBeVisible({ timeout: 10_000 });
// Play button
const playBtn = page.getByRole('button', { name: /^play$/i }).first();
await expect(playBtn).toBeVisible();
// Tabs
const discussionTab = page.getByRole('tab', { name: /discussion/i });
await expect(discussionTab).toBeVisible();
});
});
test.describe('Fonctionnalites', () => {
test('metadata grid shows track info', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Duration label should be present
const duration = page.getByText('Duration');
await expect(duration).toBeVisible({ timeout: 10_000 });
// Genre should be visible
const genre = page.getByText('Genre');
await expect(genre).toBeVisible();
});
test('tabs switch content', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Discussion tab selected by default
const discussionTab = page.getByRole('tab', { name: /discussion/i });
await expect(discussionTab).toHaveAttribute('data-state', 'active', { timeout: 10_000 });
// Click Lyrics tab
const lyricsTab = page.getByRole('tab', { name: /lyrics/i });
await lyricsTab.click();
await expect(lyricsTab).toHaveAttribute('data-state', 'active');
});
test('action buttons are present', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Like button
const likeBtn = page.getByRole('button', { name: /add to favorites/i });
await expect(likeBtn).toBeVisible({ timeout: 10_000 });
// Repost button
const repostBtn = page.getByRole('button', { name: /repost to your profile/i });
await expect(repostBtn).toBeVisible();
// Add to Queue
const queueBtn = page.getByRole('button', { name: /add to queue/i });
await expect(queueBtn).toBeVisible();
// Share
const shareBtn = page.getByRole('button', { name: /share/i }).first();
await expect(shareBtn).toBeVisible();
});
test('comment section shows placeholder', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
const commentInput = page.getByPlaceholder(/write a comment/i);
await expect(commentInput).toBeVisible({ timeout: 10_000 });
});
});
test.describe('Securite', () => {
test('invalid UUID shows error state', async ({ page }) => {
await navigateTo(page, '/tracks/00000000-0000-0000-0000-000000000000');
await page.waitForLoadState('networkidle').catch(() => {});
// Should show error/not-found state
const errorAlert = page.getByRole('alert');
await expect(errorAlert).toBeVisible({ timeout: 10_000 });
});
test('XSS injection in ID is safe', async ({ page }) => {
const jsErrors: string[] = [];
page.on('pageerror', (error) => {
jsErrors.push(error.message);
});
await navigateTo(page, '/tracks/%3Cscript%3Ealert(1)%3C%2Fscript%3E');
await page.waitForLoadState('networkidle').catch(() => {});
const xssErrors = jsErrors.filter((e) => e.includes('alert'));
expect(xssErrors).toHaveLength(0);
});
test('no sensitive data in URL', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
const url = page.url();
expect(url).not.toMatch(/token=/i);
expect(url).not.toMatch(/@.*\./);
});
});
test.describe('Accessibilite', () => {
test('action buttons have descriptive aria-labels', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Like button has meaningful aria-label
const likeBtn = page.getByRole('button', { name: /favorites/i }).first();
await expect(likeBtn).toBeVisible({ timeout: 10_000 });
const ariaLabel = await likeBtn.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
expect(ariaLabel).not.toBe('Button');
// Repost button has meaningful aria-label
const repostBtn = page.getByRole('button', { name: /repost/i });
const repostLabel = await repostBtn.getAttribute('aria-label');
expect(repostLabel).toBeTruthy();
});
test('tabs have proper ARIA roles', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
const tablist = page.getByRole('tablist');
await expect(tablist).toBeVisible({ timeout: 10_000 });
const tabs = page.getByRole('tab');
const count = await tabs.count();
expect(count).toBeGreaterThanOrEqual(4);
});
});
test.describe('i18n', () => {
test('no raw i18n keys displayed', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
await page.waitForLoadState('networkidle').catch(() => {});
const bodyText = (await page.textContent('main')) || '';
expect(bodyText).not.toMatch(/tracks\.detail\./);
expect(bodyText).not.toMatch(/tracks\.commentSection\./);
expect(bodyText).not.toMatch(/tracks\.repost\./);
expect(bodyText).not.toMatch(/tracks\.likeAction\./);
expect(bodyText).not.toMatch(/tracks\.shareDialog\./);
expect(bodyText).not.toMatch(/tracks\.lyricsSection\./);
expect(bodyText).not.toMatch(/tracks\.stemsSection\./);
expect(bodyText).not.toMatch(/tracks\.stats\./);
});
test('no FR/EN mix on page', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
await page.waitForLoadState('networkidle').catch(() => {});
const mainText = (await page.textContent('main')) || '';
// These FR strings should NOT appear when locale is EN
expect(mainText).not.toContain('Reposter sur votre profil');
expect(mainText).not.toContain('Commentaires');
expect(mainText).not.toContain('Écrire un commentaire');
expect(mainText).not.toContain('Aucun commentaire');
});
});
test.describe('Responsive', () => {
test('mobile (375px) no overflow', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 10_000 });
const overflowX = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(overflowX).toBe(false);
});
});
test.describe('Regression', () => {
test('BUG #1: page title includes track name', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
await page.waitForLoadState('networkidle').catch(() => {});
await page.waitForTimeout(1_000);
const title = await page.title();
expect(title).toContain('Veza');
expect(title).not.toBe('Veza');
expect(title).toMatch(/.+ — Veza/);
});
test('BUG #2: repost button uses EN not FR', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Repost button should have EN aria-label, not FR
const repostBtn = page.locator('button[aria-label]').filter({
has: page.locator('svg'),
});
const allLabels = await repostBtn.evaluateAll((btns) =>
btns.map((b) => b.getAttribute('aria-label')).filter(Boolean),
);
const frLabels = allLabels.filter(
(l) => l && (l.includes('Reposter') || l.includes('Retirer le repost')),
);
expect(frLabels).toHaveLength(0);
});
test('BUG #5: comment section uses EN not FR', async ({ page }) => {
const trackId = await getValidTrackId(page);
if (!trackId) {
test.skip();
return;
}
await navigateTo(page, `/tracks/${trackId}`);
// Comments heading should be EN
const heading = page.getByRole('heading', { name: /comments/i });
await expect(heading).toBeVisible({ timeout: 10_000 });
// Placeholder should be EN
const input = page.getByPlaceholder(/write a comment/i);
await expect(input).toBeVisible();
});
});
});