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