veza/tests/e2e/36-forgot-password.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

332 lines
14 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG, navigateTo, assertNoDebugText } from './helpers';
// =============================================================================
// FORGOT PASSWORD — Suite de tests exhaustive pour /forgot-password
// Couvre les 12 bugs identifiés + fonctionnalités
// =============================================================================
const FORM_SELECTOR = '[data-testid="forgot-password-form"]';
const SUBMIT_SELECTOR = '[data-testid="forgot-password-submit"]';
// Helper: clear auth state so PublicRoute doesn't redirect to /dashboard
async function clearAuthState(page: import('@playwright/test').Page) {
const context = page.context();
await context.clearCookies();
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
}
// Helper: navigate to forgot-password with clean session
async function goToForgotPassword(page: import('@playwright/test').Page) {
await clearAuthState(page);
await page.goto(`${CONFIG.baseURL}/forgot-password`, { waitUntil: 'networkidle' });
// Wait for the form to be visible (app may show splash screen first)
await page.locator(`${FORM_SELECTOR}, [role="status"]`).first().waitFor({
state: 'visible',
timeout: 15_000,
});
}
// =============================================================================
// CHARGEMENT & RENDU
// =============================================================================
test.describe('FORGOT PASSWORD — Chargement & Rendu', () => {
test('01. La page /forgot-password se charge avec le formulaire visible', async ({ page }) => {
await goToForgotPassword(page);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator('input[type="email"]')).toBeVisible();
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
});
test('02. Pas de texte debug visible', async ({ page }) => {
await goToForgotPassword(page);
await assertNoDebugText(page);
});
test('03. Lien footer vers /login fonctionne', async ({ page }) => {
await goToForgotPassword(page);
const loginLink = page.locator('a[href="/login"]');
await expect(loginLink.first()).toBeVisible({ timeout: 5_000 });
await loginLink.first().click();
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
});
test('04. Le logo VEZA est visible', async ({ page }) => {
await goToForgotPassword(page);
await expect(page.getByText('VEZA')).toBeVisible({ timeout: 5_000 });
});
test('05. Le titre H1 et le sous-titre sont visibles et non vides', async ({ page }) => {
await goToForgotPassword(page);
const heading = page.locator('h1');
await expect(heading).toBeVisible({ timeout: 5_000 });
const text = await heading.textContent();
expect(text).toBeTruthy();
expect(text!.length).toBeGreaterThan(0);
});
});
// =============================================================================
// i18n
// =============================================================================
test.describe('FORGOT PASSWORD — i18n', () => {
test('06. [BUG #1/#2] Le titre correspond à une traduction connue (pas de texte hardcodé)', async ({ page }) => {
await goToForgotPassword(page);
const headingText = (await page.locator('h1').textContent()) || '';
// The title must be one of the known i18n translations
const validTitles = ['Forgot Password', 'Mot de passe oublié', 'Contraseña olvidada'];
expect(validTitles.some((t) => headingText.includes(t))).toBeTruthy();
});
test('07. [BUG #2] La langue est cohérente avec la page /login', async ({ page }) => {
// Visit login to determine the current language
await clearAuthState(page);
await page.goto(`${CONFIG.baseURL}/login`, { waitUntil: 'networkidle' });
await page.locator('h1').waitFor({ state: 'visible', timeout: 10_000 });
const loginHeading = (await page.locator('h1').textContent()) || '';
// Visit forgot-password
await goToForgotPassword(page);
const fpHeading = (await page.locator('h1').textContent()) || '';
// Both should be in the same language
const loginIsEnglish = /Login|Sign in/i.test(loginHeading);
const loginIsFrench = /Connexion/i.test(loginHeading);
const fpIsEnglish = /Forgot Password/i.test(fpHeading);
const fpIsFrench = /Mot de passe oublié/i.test(fpHeading);
if (loginIsEnglish) expect(fpIsEnglish).toBeTruthy();
if (loginIsFrench) expect(fpIsFrench).toBeTruthy();
});
test('08. [BUG #4] html lang correspond à la langue du contenu', async ({ page }) => {
await goToForgotPassword(page);
const htmlLang = (await page.locator('html').getAttribute('lang')) || 'en';
const heading = (await page.locator('h1').textContent()) || '';
if (htmlLang === 'en') {
expect(heading).toMatch(/Forgot Password/i);
} else if (htmlLang === 'fr') {
expect(heading).toMatch(/Mot de passe oublié/i);
} else if (htmlLang === 'es') {
expect(heading).toMatch(/Contraseña olvidada/i);
}
});
});
// =============================================================================
// VALIDATION
// =============================================================================
test.describe('FORGOT PASSWORD — Validation', () => {
test('09. [BUG #7] Email vide montre une erreur au blur', async ({ page }) => {
await goToForgotPassword(page);
const emailInput = page.locator('input[type="email"]');
await emailInput.focus();
await emailInput.blur();
const error = page.getByRole('alert');
await expect(error.first()).toBeVisible({ timeout: 3_000 });
});
test('10. [BUG #7] Email invalide montre une erreur de format', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill('not-an-email');
await page.locator('input[type="email"]').blur();
const error = page.getByRole('alert');
await expect(error.first()).toBeVisible({ timeout: 3_000 });
});
test('11. L\'erreur disparaît quand l\'utilisateur corrige sa saisie', async ({ page }) => {
await goToForgotPassword(page);
const emailInput = page.locator('input[type="email"]');
await emailInput.fill('bad');
await emailInput.blur();
await expect(page.getByRole('alert').first()).toBeVisible({ timeout: 3_000 });
// Fix the input
await emailInput.fill('valid@example.com');
await page.waitForTimeout(500);
// Validation error should be gone (the API error alert may remain)
const validationErrors = page.locator('[role="alert"]').filter({
hasText: /required|requis|invalid|invalide|requerido|inválido/i,
});
await expect(validationErrors).not.toBeVisible({ timeout: 3_000 });
});
test('12. Soumission vide ne déclenche pas d\'appel API', async ({ page }) => {
await goToForgotPassword(page);
const apiCalls: string[] = [];
page.on('request', (req) => {
if (req.url().includes('/password/reset') && req.method() === 'POST') {
apiCalls.push(req.url());
}
});
// Click submit with empty email
await page.locator(SUBMIT_SELECTOR).click();
await page.waitForTimeout(1_000);
expect(apiCalls.length).toBe(0);
});
});
// =============================================================================
// ACCESSIBILITÉ
// =============================================================================
test.describe('FORGOT PASSWORD — Accessibilité', () => {
test('13. [BUG #3] Le lien "Skip to content" a une cible #main-content existante', async ({ page }) => {
await goToForgotPassword(page);
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeAttached();
});
test('14. L\'input email a un label associé', async ({ page }) => {
await goToForgotPassword(page);
await expect(page.getByRole('textbox', { name: /email/i })).toBeVisible();
});
test('15. Le formulaire a un aria-label', async ({ page }) => {
await goToForgotPassword(page);
const form = page.locator(FORM_SELECTOR);
await expect(form).toBeVisible({ timeout: 10_000 });
const ariaLabel = await form.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
expect(ariaLabel!.length).toBeGreaterThan(0);
});
test('16. Navigation clavier : Tab traverse email → submit → lien retour', async ({ page }) => {
await goToForgotPassword(page);
const emailInput = page.locator('input[type="email"]');
await emailInput.focus();
await expect(emailInput).toBeFocused();
// Tab to submit button
await page.keyboard.press('Tab');
const focusedTag = await page.evaluate(() => document.activeElement?.tagName.toLowerCase());
expect(focusedTag).toBe('button');
});
});
// =============================================================================
// ÉTAT SUCCÈS
// =============================================================================
test.describe('FORGOT PASSWORD — État succès', () => {
test('17. [BUG #5] Le message de succès est ambigu (pas d\'email affiché)', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill('test-noexist@example.com');
await page.locator(SUBMIT_SELECTOR).click();
// Wait for success or error state
const statusOrAlert = page.getByRole('alert').or(page.getByRole('status'));
await expect(statusOrAlert.first()).toBeVisible({ timeout: 10_000 });
const body = (await page.textContent('body')) || '';
// Should NOT contain the submitted email address
expect(body).not.toContain('test-noexist@example.com');
// Should contain ambiguous "if an account exists" type wording
expect(body).toMatch(/if.*account.*exist|si.*compte.*existe|si.*cuenta.*existe/i);
});
test('18. [BUG #6] Pas de double lien "Retour à la connexion" dans l\'état succès', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill(CONFIG.users.listener.email);
await page.locator(SUBMIT_SELECTOR).click();
await page.getByRole('status').or(page.getByRole('alert')).first().waitFor({
state: 'visible',
timeout: 10_000,
});
// Count back-to-login links — should be exactly 1
const backLinks = page.locator('a[href="/login"]');
const count = await backLinks.count();
expect(count).toBe(1);
});
test('19. [BUG #9] Le bouton "Renvoyer" ramène au formulaire', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill(CONFIG.users.listener.email);
await page.locator(SUBMIT_SELECTOR).click();
await page.getByRole('status').or(page.getByRole('alert')).first().waitFor({
state: 'visible',
timeout: 10_000,
});
// Click resend button
const resendBtn = page.getByRole('button', { name: /resend|renvoyer|reenviar/i });
await expect(resendBtn).toBeVisible({ timeout: 3_000 });
await resendBtn.click();
// Form should reappear
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 5_000 });
});
test('20. [BUG #12] Le titre H1 change entre le formulaire et l\'état succès', async ({ page }) => {
await goToForgotPassword(page);
const titleBefore = await page.locator('h1').textContent();
await page.locator('input[type="email"]').fill(CONFIG.users.listener.email);
await page.locator(SUBMIT_SELECTOR).click();
await page.getByRole('status').or(page.getByRole('alert')).first().waitFor({
state: 'visible',
timeout: 10_000,
});
const titleAfter = await page.locator('h1').textContent();
expect(titleBefore).not.toBe(titleAfter);
});
});
// =============================================================================
// UX
// =============================================================================
test.describe('FORGOT PASSWORD — UX', () => {
test('21. [BUG #8] Le titre de la page (document.title) est mis à jour', async ({ page }) => {
await goToForgotPassword(page);
const title = await page.title();
// Should contain forgot/oublié/olvidada and not be the generic default
expect(title).toMatch(/forgot|oublié|olvidada/i);
});
test('22. [BUG #11] Le bouton submit a aria-busy pendant le chargement', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill(CONFIG.users.listener.email);
// Click and immediately check aria-busy
await page.locator(SUBMIT_SELECTOR).click();
// The button should become busy/disabled during loading
const btn = page.locator(SUBMIT_SELECTOR);
// Check either aria-busy or disabled state was applied
const ariaBusy = await btn.getAttribute('aria-busy').catch(() => null);
const ariaDisabled = await btn.getAttribute('aria-disabled').catch(() => null);
expect(ariaBusy === 'true' || ariaDisabled === 'true').toBeTruthy();
});
test('23. Soumission par touche Entrée fonctionne', async ({ page }) => {
await goToForgotPassword(page);
await page.locator('input[type="email"]').fill('enter-test@example.com');
await page.locator('input[type="email"]').press('Enter');
// Should reach success or error state (not stay idle)
const statusOrAlert = page.getByRole('alert').or(page.getByRole('status'));
await expect(statusOrAlert.first()).toBeVisible({ timeout: 10_000 });
});
});
// =============================================================================
// RESPONSIVE
// =============================================================================
test.describe('FORGOT PASSWORD — Responsive', () => {
test('24. Mobile (375px) — formulaire visible sans overflow', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await goToForgotPassword(page);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
// Check no horizontal overflow
const hasOverflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasOverflow).toBe(false);
});
test('25. Tablet (768px) — page affichée correctement', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await goToForgotPassword(page);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
});
});