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
', 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); }); }); });