veza/tests/e2e/launch-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

457 lines
20 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG, loginViaUI, navigateTo, assertNoDebugText } from './helpers';
// =============================================================================
// AUDIT — Landing page pré-lancement (/launch)
// =============================================================================
const USER = { email: 'user@veza.music', password: 'User123!' };
test.describe('AUDIT — Landing page pré-lancement (/launch)', () => {
// ─── Chargement & Rendu ────────────────────────────────────────────
test.describe('Chargement & Rendu', () => {
test('01. la page se charge sans crash', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-page')).toBeVisible({ timeout: 15_000 });
const body = await page.textContent('body');
expect(body).not.toMatch(/500|Internal Server Error/i);
});
test('02. pas de texte de debug ([object Object], undefined)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
await assertNoDebugText(page);
});
test('03. le titre TALAS est visible dans le hero', async ({ page }) => {
await navigateTo(page, '/launch');
const h1 = page.locator('h1');
await expect(h1).toBeVisible({ timeout: 10_000 });
await expect(h1).toContainText('TALAS');
});
test('04. le formulaire email hero est visible', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-hero-form')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-hero-email')).toBeVisible();
await expect(page.getByTestId('launch-hero-submit')).toBeVisible();
});
test('05. la navigation est visible avec liens', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-nav')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-login-link')).toBeVisible();
});
test('06. le footer est visible avec copyright', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-footer').scrollIntoViewIfNeeded();
await expect(page.getByTestId('launch-footer')).toBeVisible({ timeout: 10_000 });
const footerText = await page.getByTestId('launch-footer').textContent();
expect(footerText).toContain('2026');
});
test('07. les 3 cartes "engagements" sont visibles', async ({ page }) => {
await navigateTo(page, '/launch');
// Scroll to values section to trigger animation
await page.evaluate(() => {
const section = document.querySelector('[data-testid="launch-values"]') ||
document.querySelector('h2');
section?.scrollIntoView({ behavior: 'instant' });
});
await page.waitForTimeout(1_000);
const h3s = page.locator('h3');
await expect(h3s).toHaveCount(3, { timeout: 5_000 });
});
test('08. la section produit (#product) est presente', async ({ page }) => {
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-product')).toBeAttached();
});
});
// ─── Fonctionnalites ───────────────────────────────────────────────
test.describe('Fonctionnalites', () => {
test('09. le formulaire hero envoie POST /newsletter/subscribe', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
const requestPromise = page.waitForRequest(
(req) => req.url().includes('/newsletter/subscribe') && req.method() === 'POST',
{ timeout: 10_000 },
);
await page.getByTestId('launch-hero-email').fill('audit@test.com');
await page.getByTestId('launch-hero-submit').click();
const request = await requestPromise;
const body = JSON.parse(request.postData() ?? '{}');
expect(body.email).toBe('audit@test.com');
});
test('10. apres soumission reussie, le message de succes s affiche', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
// Mock the newsletter API to succeed
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, data: { message: 'Subscribed' } }),
});
});
await page.getByTestId('launch-hero-email').fill('success@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-hero-success')).toBeVisible({ timeout: 5_000 });
});
test('11. le lien CONNEXION navigue vers /login', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-login-link').click();
await page.waitForURL('**/login', { timeout: 10_000 });
expect(page.url()).toContain('/login');
});
test('12. le lien ancre #product scrolle vers la section produit', async ({ page }) => {
await navigateTo(page, '/launch');
const productLink = page.locator('a[href="#product"]');
if (await productLink.isVisible()) {
await productLink.click();
await page.waitForTimeout(500);
const productSection = page.getByTestId('launch-product');
await expect(productSection).toBeInViewport({ timeout: 5_000 });
}
});
test('13. le lien ancre #notify scrolle vers le CTA', async ({ page }) => {
await navigateTo(page, '/launch');
const notifyLink = page.locator('a[href="#notify"]');
// Scroll to make it visible first
await page.evaluate(() => {
const el = document.querySelector('a[href="#notify"]');
el?.scrollIntoView({ behavior: 'instant' });
});
await page.waitForTimeout(500);
if (await notifyLink.isVisible()) {
await notifyLink.click();
await page.waitForTimeout(500);
const notifySection = page.getByTestId('launch-notify');
await expect(notifySection).toBeInViewport({ timeout: 5_000 });
}
});
test('14. validation email : email invalide ne soumet pas', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
let requestFired = false;
page.on('request', (req) => {
if (req.url().includes('/newsletter/subscribe')) requestFired = true;
});
// Type invalid email and try to submit
await page.getByTestId('launch-hero-email').fill('not-an-email');
await page.getByTestId('launch-hero-submit').click();
await page.waitForTimeout(500);
expect(requestFired).toBe(false);
});
test('15. le formulaire CTA (#notify) fonctionne aussi', async ({ page }) => {
await navigateTo(page, '/launch');
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, data: { message: 'Subscribed' } }),
});
});
// Scroll to notify section
await page.evaluate(() => document.querySelector('#notify')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
await page.getByTestId('launch-notify-email').fill('cta@test.com');
await page.getByTestId('launch-notify-submit').click();
await expect(page.getByTestId('launch-notify-success')).toBeVisible({ timeout: 5_000 });
});
test('16. gestion erreur API : message d erreur affiche', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: { message: 'Email already registered' } }),
});
});
await page.getByTestId('launch-hero-email').fill('error@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-error-message')).toBeVisible({ timeout: 5_000 });
});
});
// ─── i18n ──────────────────────────────────────────────────────────
test.describe('i18n', () => {
test('17. pas de clef i18n brute visible (landing.xxx)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const body = await page.textContent('body');
expect(body).not.toMatch(/landing\.\w+\.\w+/);
});
test('18. le document.title est specifique a la page', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const title = await page.title();
expect(title).toMatch(/TALAS/i);
});
test('19. pas de melange de langues dans la nav', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-nav').waitFor({ state: 'visible', timeout: 10_000 });
const navAriaLabel = await page.getByTestId('launch-nav').getAttribute('aria-label');
expect(navAriaLabel).toBeTruthy();
expect(navAriaLabel).not.toMatch(/landing\.\w+/);
});
test('20. les messages d erreur sont dans la langue de la page (pas EN dans page FR)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
await page.route('**/newsletter/subscribe', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({}),
});
});
await page.getByTestId('launch-hero-email').fill('i18n@test.com');
await page.getByTestId('launch-hero-submit').click();
await expect(page.getByTestId('launch-error-message')).toBeVisible({ timeout: 5_000 });
const errorText = await page.getByTestId('launch-error-message').textContent();
// Should NOT contain the old hardcoded English strings
expect(errorText).not.toBe('Subscription failed');
expect(errorText).not.toBe('An error occurred');
});
});
// ─── Accessibilite ─────────────────────────────────────────────────
test.describe('Accessibilite', () => {
test('21. utilise un element main semantique', async ({ page }) => {
await navigateTo(page, '/launch');
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 15_000 });
const tagName = await main.evaluate((el) => el.tagName);
expect(tagName).toBe('MAIN');
});
test('22. main a un id="main-content" (target du skip-to-content)', async ({ page }) => {
await navigateTo(page, '/launch');
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeAttached({ timeout: 10_000 });
});
test('23. la nav principale a un aria-label', async ({ page }) => {
await navigateTo(page, '/launch');
const nav = page.getByTestId('launch-nav');
await expect(nav).toBeVisible({ timeout: 10_000 });
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
});
test('24. les inputs email ont des aria-labels', async ({ page }) => {
await navigateTo(page, '/launch');
const heroEmail = page.getByTestId('launch-hero-email');
await expect(heroEmail).toBeVisible({ timeout: 10_000 });
const ariaLabel = await heroEmail.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
});
test('25. le heading structure est correct (H1 > H2 > H3)', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const headings = await page.evaluate(() =>
Array.from(document.querySelectorAll('h1, h2, h3')).map((h) => h.tagName),
);
// H1 should come first
expect(headings[0]).toBe('H1');
// Should have H1 followed by H2s and H3s
expect(headings.filter((h) => h === 'H1')).toHaveLength(1);
});
test('26. les boutons ont des noms accessibles non-vides', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-hero-form').waitFor({ state: 'visible', timeout: 10_000 });
const buttons = await page.evaluate(() =>
Array.from(document.querySelectorAll('button')).map((b) => ({
text: b.textContent?.trim(),
ariaLabel: b.getAttribute('aria-label'),
})),
);
for (const btn of buttons) {
const hasName = (btn.text?.length ?? 0) > 0 || (btn.ariaLabel?.length ?? 0) > 0;
expect(hasName).toBe(true);
}
});
});
// ─── Responsive ────────────────────────────────────────────────────
test.describe('Responsive', () => {
test('27. pas d overflow horizontal en mobile (375px)', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
test('28. pas d overflow horizontal en tablet (768px)', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
test('29. le formulaire hero est visible en mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await expect(page.getByTestId('launch-hero-form')).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId('launch-hero-email')).toBeVisible();
await expect(page.getByTestId('launch-hero-submit')).toBeVisible();
});
});
// ─── Securite ──────────────────────────────────────────────────────
test.describe('Securite', () => {
test('30. pas de credentials dans localStorage', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const sensitive = await page.evaluate(() => {
const keys = Object.keys(localStorage);
return keys.filter(
(k) =>
k.toLowerCase().includes('password') ||
k.toLowerCase().includes('secret') ||
k.toLowerCase().includes('api_key'),
);
});
expect(sensitive).toHaveLength(0);
});
test('31. liens externes ont target="_blank" et rel="noopener"', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-footer').scrollIntoViewIfNeeded();
const externalLinks = await page.evaluate(() =>
Array.from(document.querySelectorAll('a[href^="http"]')).map((a) => ({
href: a.getAttribute('href'),
target: a.getAttribute('target'),
rel: a.getAttribute('rel'),
})),
);
for (const link of externalLinks) {
expect(link.target).toBe('_blank');
expect(link.rel).toContain('noopener');
}
});
test('32. le token n est pas visible dans l URL', async ({ page }) => {
await navigateTo(page, '/launch');
expect(page.url()).not.toContain('token');
expect(page.url()).not.toContain('api_key');
});
});
// ─── Regression ────────────────────────────────────────────────────
test.describe('Regression', () => {
test('33. BUG-03 FIX: la page utilise un element <main>', async ({ page }) => {
await navigateTo(page, '/launch');
const main = page.locator('main');
await expect(main).toBeVisible({ timeout: 15_000 });
const id = await main.getAttribute('id');
expect(id).toBe('main-content');
});
test('34. BUG-04 FIX: les data-testid sont presents', async ({ page }) => {
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const testIds = await page.evaluate(() =>
Array.from(document.querySelectorAll('[data-testid]')).map((el) =>
el.getAttribute('data-testid'),
),
);
expect(testIds).toContain('launch-page');
expect(testIds).toContain('launch-nav');
expect(testIds).toContain('launch-hero');
expect(testIds).toContain('launch-hero-form');
expect(testIds).toContain('launch-footer');
});
test('35. BUG-05 FIX: le bouton CTA ne dit plus "NOTIFIER MOI"', async ({ page }) => {
await navigateTo(page, '/launch');
// Scroll to notify section
await page.evaluate(() => document.querySelector('#notify')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
const buttonText = await page.getByTestId('launch-notify-submit').textContent();
expect(buttonText?.trim()).not.toBe('NOTIFIER MOI');
});
test('36. BUG-06 FIX: pas de texte duplique dans la description produit', async ({ page }) => {
await navigateTo(page, '/launch');
await page.evaluate(() => document.querySelector('#product')?.scrollIntoView({ behavior: 'instant' }));
await page.waitForTimeout(500);
const productText = await page.getByTestId('launch-product').textContent() ?? '';
// "composants standards" should only appear once
const matches = productText.match(/composants standards|standard components/gi);
expect((matches?.length ?? 0)).toBeLessThanOrEqual(1);
});
test('37. BUG-10 FIX: la nav principale a un aria-label', async ({ page }) => {
await navigateTo(page, '/launch');
const nav = page.getByTestId('launch-nav');
await expect(nav).toBeVisible({ timeout: 10_000 });
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel?.trim().length).toBeGreaterThan(0);
expect(ariaLabel).not.toMatch(/landing\.\w+/);
});
test('38. BUG-15 FIX: pas d overflow en mobile 375px', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/launch');
await page.getByTestId('launch-page').waitFor({ state: 'visible', timeout: 15_000 });
const hasOverflow = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasOverflow).toBe(false);
});
});
});