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>
457 lines
20 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|