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

477 lines
21 KiB
TypeScript

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