import { test, expect } from '@chromatic-com/playwright'; import { CONFIG, loginViaUI, navigateTo, assertNoDebugText } from './helpers'; // ============================================================================= // AUDIT — Page Verification email (/verify-email) // ============================================================================= const USER = { email: 'user@veza.music', password: 'User123!' }; const FAKE_TOKEN = 'fake-audit-token-12345'; test.describe('AUDIT — Verification email (/verify-email)', () => { // ─── Chargement & Rendu ──────────────────────────────────────────── test.describe('Chargement & Rendu', () => { test('01. la page se charge sans crash (sans token)', async ({ page }) => { await navigateTo(page, '/verify-email'); await expect(page.locator('main')).toBeVisible({ timeout: 10_000 }); const body = await page.textContent('body'); expect(body).not.toMatch(/500|Internal Server Error/i); }); test('02. la page se charge sans crash (avec token)', async ({ page }) => { await navigateTo(page, `/verify-email?token=${FAKE_TOKEN}`); await expect(page.locator('main')).toBeVisible({ timeout: 10_000 }); const body = await page.textContent('body'); expect(body).not.toMatch(/500|Internal Server Error/i); }); test('03. pas de texte de debug ([object Object], undefined)', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.locator('main').waitFor({ state: 'visible', timeout: 10_000 }); await assertNoDebugText(page); }); test('04. le logo VEZA est visible', async ({ page }) => { await navigateTo(page, '/verify-email'); await expect(page.getByText('VEZA')).toBeVisible({ timeout: 5_000 }); }); test('05. le titre h1 est present et non-vide', async ({ page }) => { await navigateTo(page, '/verify-email'); const heading = page.locator('h1'); await expect(heading).toBeVisible({ timeout: 5_000 }); const text = await heading.textContent(); expect(text?.trim().length).toBeGreaterThan(0); }); test('06. le lien footer vers /login est present', async ({ page }) => { await navigateTo(page, '/verify-email'); const link = page.locator('nav a[href="/login"]'); await expect(link).toBeVisible({ timeout: 5_000 }); }); }); // ─── Etats de la page ────────────────────────────────────────────── test.describe('Etats de la page', () => { test('07. sans token -> etat erreur avec message et bouton resend', async ({ page }) => { await navigateTo(page, '/verify-email'); await expect(page.getByTestId('verify-email-error')).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId('verify-email-resend')).toBeVisible(); // Retry button should NOT be visible (no token) await expect(page.getByTestId('verify-email-retry')).not.toBeVisible(); }); test('08. avec token invalide -> erreur API, retry + resend visibles', async ({ page }) => { await navigateTo(page, `/verify-email?token=${FAKE_TOKEN}`); // Wait for the API call to complete and show error state await expect(page.getByTestId('verify-email-error')).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId('verify-email-retry')).toBeVisible(); await expect(page.getByTestId('verify-email-resend')).toBeVisible(); }); test('09. le message d erreur est visible dans le container', async ({ page }) => { await navigateTo(page, '/verify-email'); await expect(page.getByTestId('verify-email-message')).toBeVisible({ timeout: 10_000 }); const text = await page.getByTestId('verify-email-message').textContent(); expect(text?.trim().length).toBeGreaterThan(0); }); }); // ─── i18n ────────────────────────────────────────────────────────── test.describe('i18n', () => { test('10. pas de clef i18n brute visible (auth.xxx, common.xxx)', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const body = await page.textContent('body'); expect(body).not.toMatch(/auth\.\w+\.\w+/); expect(body).not.toMatch(/common\.\w+\.\w+/); }); test('11. le main aria-label est non-vide et pas une clef i18n', async ({ page }) => { await navigateTo(page, '/verify-email'); const main = page.locator('main'); await expect(main).toBeVisible({ timeout: 10_000 }); const ariaLabel = await main.getAttribute('aria-label'); expect(ariaLabel).toBeTruthy(); expect(ariaLabel).not.toMatch(/auth\.\w+/); }); test('12. le document.title est specifique a la page (pas generique)', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const title = await page.title(); // Should contain a verification-related keyword, not just the generic app title expect(title).toMatch(/verif|email|vérif|correo/i); }); test('13. pas de melange de langues entre aria-label et contenu visible', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const mainAriaLabel = await page.locator('main').getAttribute('aria-label'); const h1Text = await page.locator('h1').textContent(); // Both should be in the same language. // If aria-label contains "Authentication" (EN), h1 should not be in French // If aria-label contains "authentification" (FR), h1 should be in French if (mainAriaLabel?.match(/authentication/i)) { // EN context: h1 should not contain accented FR characters in an EN-only word expect(h1Text).not.toMatch(/Vérification/); } if (mainAriaLabel?.match(/authentification/i)) { // FR context: h1 should be in French expect(h1Text).toMatch(/Vérification/); } }); }); // ─── Fonctionnalites ─────────────────────────────────────────────── test.describe('Fonctionnalites', () => { test('14. le lien footer navigue vers /login', async ({ page }) => { await navigateTo(page, '/verify-email'); const link = page.locator('nav a[href="/login"]'); await expect(link).toBeVisible({ timeout: 10_000 }); await link.click(); await page.waitForURL('**/login', { timeout: 10_000 }); expect(page.url()).toContain('/login'); }); test('15. resend sans pendingVerificationEmail montre message d erreur', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Ensure localStorage is clear await page.evaluate(() => localStorage.removeItem('pendingVerificationEmail')); await page.getByTestId('verify-email-resend').click(); // Should show "email not found" message await expect(page.getByTestId('verify-email-message')).toContainText( /not found|non trouvé|no encontrado/i, { timeout: 5_000 }, ); }); test('16. resend avec localStorage set envoie requete API', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Set pendingVerificationEmail await page.evaluate(() => localStorage.setItem('pendingVerificationEmail', 'test@example.com'), ); // Listen for the resend API call const resendPromise = page.waitForRequest( (req) => req.url().includes('/auth/resend-verification') && req.method() === 'POST', { timeout: 10_000 }, ); await page.getByTestId('verify-email-resend').click(); const request = await resendPromise; expect(request.method()).toBe('POST'); }); test('17. cooldown s active apres resend', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); await page.evaluate(() => localStorage.setItem('pendingVerificationEmail', 'test@example.com'), ); await page.getByTestId('verify-email-resend').click(); // Wait for cooldown to appear await expect(page.getByTestId('verify-email-resend')).toBeDisabled({ timeout: 5_000 }); }); test('18. le bouton retry envoie une requete verify-email', async ({ page }) => { await navigateTo(page, `/verify-email?token=${FAKE_TOKEN}`); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const verifyPromise = page.waitForRequest( (req) => req.url().includes('/auth/verify-email') && req.method() === 'POST', { timeout: 10_000 }, ); await page.getByTestId('verify-email-retry').click(); const request = await verifyPromise; expect(request.method()).toBe('POST'); }); test('19. apres resend reussi, un container succes s affiche', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); await page.evaluate(() => localStorage.setItem('pendingVerificationEmail', 'test@example.com'), ); // Mock the resend API to succeed await page.route('**/auth/resend-verification', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: { message: 'Email sent' } }), }); }); await page.getByTestId('verify-email-resend').click(); // Should show the success container (not the red error one) await expect(page.getByTestId('verify-email-resend-success')).toBeVisible({ timeout: 5_000 }); }); }); // ─── Accessibilite ───────────────────────────────────────────────── test.describe('Accessibilite', () => { test('20. utilise un element main semantique', async ({ page }) => { await navigateTo(page, '/verify-email'); const main = page.locator('main'); await expect(main).toBeVisible({ timeout: 10_000 }); const tagName = await main.evaluate((el) => el.tagName); expect(tagName).toBe('MAIN'); }); test('21. main a un aria-label non-vide', async ({ page }) => { await navigateTo(page, '/verify-email'); const main = page.locator('main'); await expect(main).toBeVisible({ timeout: 10_000 }); const ariaLabel = await main.getAttribute('aria-label'); expect(ariaLabel?.trim().length).toBeGreaterThan(0); }); test('22. le container erreur a role=alert et aria-live=assertive', async ({ page }) => { await navigateTo(page, '/verify-email'); const alert = page.locator('[role="alert"]'); await expect(alert).toBeVisible({ timeout: 10_000 }); const ariaLive = await alert.getAttribute('aria-live'); expect(ariaLive).toBe('assertive'); }); test('23. les boutons ont des noms accessibles', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const resendButton = page.getByTestId('verify-email-resend'); await expect(resendButton).toBeVisible(); const ariaLabel = await resendButton.getAttribute('aria-label'); expect(ariaLabel?.trim().length).toBeGreaterThan(0); }); test('24. tab order est logique', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Get all focusable elements in tab order const focusableOrder = await page.evaluate(() => { const focusable = Array.from( document.querySelectorAll( 'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])', ), ); return focusable.map((el) => ({ tag: el.tagName, text: el.textContent?.trim()?.substring(0, 40), testId: el.getAttribute('data-testid'), })); }); // Should have at least: skip link, resend button, footer link expect(focusableOrder.length).toBeGreaterThanOrEqual(3); // First focusable should be skip-to-content link expect(focusableOrder[0]?.tag).toBe('A'); }); }); // ─── Responsive ──────────────────────────────────────────────────── test.describe('Responsive', () => { test('25. layout correct en mobile (375px)', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Check no horizontal overflow const hasOverflow = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasOverflow).toBe(false); // Button should still be visible and usable await expect(page.getByTestId('verify-email-resend')).toBeVisible(); }); test('26. layout correct en tablet (768px)', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const hasOverflow = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasOverflow).toBe(false); await expect(page.getByTestId('verify-email-resend')).toBeVisible(); }); test('27. layout correct en desktop (1280px)', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 720 }); await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const hasOverflow = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasOverflow).toBe(false); await expect(page.getByTestId('verify-email-resend')).toBeVisible(); }); }); // ─── Securite ────────────────────────────────────────────────────── test.describe('Securite', () => { test('28. utilisateur authentifie est redirige vers /dashboard', async ({ page }) => { await loginViaUI(page, USER.email, USER.password); // Confirm we're logged in await page.waitForURL('**/dashboard', { timeout: 15_000 }); // Now try to access /verify-email await page.goto(`${CONFIG.baseURL}/verify-email`, { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle').catch(() => {}); // Should be redirected away from /verify-email (PublicRoute guard) await page.waitForURL((url) => !url.pathname.includes('/verify-email'), { timeout: 10_000, }); expect(page.url()).not.toContain('/verify-email'); }); test('29. pas de credentials dans localStorage apres chargement', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const sensitiveData = await page.evaluate(() => { const keys = Object.keys(localStorage); const sensitive = keys.filter( (k) => k.toLowerCase().includes('password') || k.toLowerCase().includes('secret') || k.toLowerCase().includes('api_key'), ); return sensitive; }); expect(sensitiveData).toHaveLength(0); }); test('30. le token n est pas affiche dans le contenu visible', async ({ page }) => { const token = 'super-secret-token-xyz'; await navigateTo(page, `/verify-email?token=${token}`); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const body = await page.textContent('body'); expect(body).not.toContain(token); }); }); // ─── Regression ──────────────────────────────────────────────────── test.describe('Regression', () => { test('31. BUG-01 FIX: les chaines sont internationalisees (pas de hardcode FR dans page EN)', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // The page should use i18n keys — verify the title matches the current locale const h1Text = await page.locator('h1').textContent(); const mainAriaLabel = await page.locator('main').getAttribute('aria-label'); // Both should be in the same language direction (both EN or both FR) // They should not be in different languages const h1IsFR = /Vérification/.test(h1Text ?? ''); const ariaIsEN = /Authentication/.test(mainAriaLabel ?? ''); const ariaIsFR = /authentification/i.test(mainAriaLabel ?? ''); // If both are FR or both are EN, we're good. No mixed languages. if (ariaIsEN) { expect(h1IsFR).toBe(false); } if (ariaIsFR) { expect(h1IsFR).toBe(true); } }); test('32. BUG-02 FIX: resend fonctionne avec pendingVerificationEmail en localStorage', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Simulate registration having stored the email await page.evaluate(() => localStorage.setItem('pendingVerificationEmail', 'test@example.com'), ); const requestPromise = page.waitForRequest( (req) => req.url().includes('/auth/resend-verification'), { timeout: 10_000 }, ); await page.getByTestId('verify-email-resend').click(); const request = await requestPromise; const body = JSON.parse(request.postData() ?? '{}'); expect(body.email).toBe('test@example.com'); }); test('33. BUG-04 FIX: apres resend reussi, le container est style succes (pas erreur)', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); await page.evaluate(() => localStorage.setItem('pendingVerificationEmail', 'test@example.com'), ); // Mock the resend API await page.route('**/auth/resend-verification', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, data: { message: 'Email sent' } }), }); }); await page.getByTestId('verify-email-resend').click(); // Verify the success container appears await expect(page.getByTestId('verify-email-resend-success')).toBeVisible({ timeout: 5_000 }); // The destructive/error alert should NOT be visible anymore const errorAlert = page.locator('[role="alert"][aria-live="assertive"]'); await expect(errorAlert).not.toBeVisible(); }); test('34. BUG-05 FIX: les data-testid sont presents', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const testIds = await page.evaluate(() => Array.from(document.querySelectorAll('[data-testid]')).map((el) => el.getAttribute('data-testid'), ), ); expect(testIds).toContain('verify-email-error'); expect(testIds).toContain('verify-email-resend'); expect(testIds).toContain('verify-email-message'); }); test('35. BUG-03 FIX: l API verify-email n est appelee qu une seule fois', async ({ page }) => { let callCount = 0; page.on('request', (req) => { if (req.url().includes('/auth/verify-email') && req.method() === 'POST') { callCount++; } }); await navigateTo(page, `/verify-email?token=${FAKE_TOKEN}`); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); // Wait a bit to ensure no extra calls happen await page.waitForTimeout(1_000); expect(callCount).toBe(1); }); test('36. BUG-08 FIX: le document.title est specifique a la page', async ({ page }) => { await navigateTo(page, '/verify-email'); await page.getByTestId('verify-email-error').waitFor({ state: 'visible', timeout: 10_000 }); const title = await page.title(); expect(title).toContain('Veza'); expect(title).toMatch(/verif|email|vérif|correo/i); }); }); });