veza/tests/e2e/37-reset-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

345 lines
14 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { CONFIG, navigateTo, assertNoDebugText } from './helpers';
// =============================================================================
// RESET PASSWORD — Suite de tests exhaustive pour /reset-password
// Couvre les 13 bugs identifiés + fonctionnalités
// =============================================================================
const FORM_SELECTOR = '[data-testid="reset-password-form"]';
const SUBMIT_SELECTOR = '[data-testid="reset-password-submit"]';
const FAKE_TOKEN = 'test-fake-token-e2e-123';
// 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 reset-password with a token
async function goToResetPassword(page: import('@playwright/test').Page, token?: string) {
await clearAuthState(page);
const url = token
? `${CONFIG.baseURL}/reset-password?token=${token}`
: `${CONFIG.baseURL}/reset-password`;
await page.goto(url, { waitUntil: 'networkidle' });
await page.locator('main, [role="main"]').first().waitFor({
state: 'visible',
timeout: 15_000,
});
}
// =============================================================================
// CHARGEMENT & RENDU
// =============================================================================
test.describe('RESET PASSWORD — Chargement & Rendu', () => {
test('01. Avec token : le formulaire s\'affiche', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator('input[type="password"]').first()).toBeVisible();
await expect(page.locator('input[type="password"]').nth(1)).toBeVisible();
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
});
test('02. Sans token : le message d\'erreur s\'affiche', async ({ page }) => {
await goToResetPassword(page);
const alert = page.getByRole('alert');
await expect(alert).toBeVisible({ timeout: 10_000 });
// Form should NOT be visible
await expect(page.locator(FORM_SELECTOR)).not.toBeVisible();
});
test('03. Pas de texte debug visible', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
await assertNoDebugText(page);
});
test('04. Lien "Retour à la connexion" fonctionne', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
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 });
});
});
// =============================================================================
// i18n
// =============================================================================
test.describe('RESET PASSWORD — i18n', () => {
test('05. [BUG #1/#2] Le titre correspond à une traduction connue', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const heading = (await page.locator('h1').textContent()) || '';
const validTitles = ['Reset Password', 'Réinitialiser le mot de passe', 'Restablecer contraseña'];
expect(validTitles.some((t) => heading.includes(t))).toBeTruthy();
});
test('06. [BUG #2] Pas de mélange de langues (labels et indicateur de force)', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const heading = (await page.locator('h1').textContent()) || '';
const isEnglish = /Reset Password/i.test(heading);
// Type a password to trigger the strength indicator
await page.locator('input[type="password"]').first().fill('Ab1!test');
await page.waitForTimeout(300);
const body = (await page.textContent('body')) || '';
if (isEnglish) {
// Labels should be English, not French
expect(body).not.toMatch(/Nouveau mot de passe/);
expect(body).not.toMatch(/Confirmer le mot de passe/);
}
});
test('07. [BUG #10] L\'état "sans token" est traduit', async ({ page }) => {
await goToResetPassword(page);
const heading = (await page.locator('h1').textContent()) || '';
const validTitles = ['Invalid reset link', 'Lien de réinitialisation invalide', 'Enlace de restablecimiento inválido'];
expect(validTitles.some((t) => heading.includes(t))).toBeTruthy();
});
});
// =============================================================================
// ÉTAT SANS TOKEN
// =============================================================================
test.describe('RESET PASSWORD — État sans token', () => {
test('08. Lien vers /forgot-password présent', async ({ page }) => {
await goToResetPassword(page);
const link = page.locator('a[href="/forgot-password"]');
await expect(link).toBeVisible({ timeout: 5_000 });
});
test('09. Lien vers /login présent', async ({ page }) => {
await goToResetPassword(page);
const link = page.locator('a[href="/login"]');
await expect(link.first()).toBeVisible({ timeout: 5_000 });
});
});
// =============================================================================
// VALIDATION
// =============================================================================
test.describe('RESET PASSWORD — Validation', () => {
test('10. [BUG #4/#5] Password vide montre erreur au blur', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
await pw.focus();
await pw.blur();
await page.waitForTimeout(300);
const error = page.getByRole('alert');
await expect(error.first()).toBeVisible({ timeout: 3_000 });
});
test('11. [BUG #4] Password trop court montre erreur', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
await pw.fill('Ab1!');
await pw.blur();
await page.waitForTimeout(300);
const body = (await page.textContent('body')) || '';
expect(body).toMatch(/8 char|8 car|8 caract/i);
});
test('12. Mots de passe différents montre erreur', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
const confirm = page.locator('input[type="password"]').nth(1);
await pw.fill('ValidPass1!xx');
await confirm.fill('DifferentPass1!');
await confirm.blur();
await page.waitForTimeout(300);
const body = (await page.textContent('body')) || '';
expect(body).toMatch(/do not match|correspondent pas|no coinciden/i);
});
test('13. Erreur disparaît quand l\'utilisateur corrige', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
await pw.fill('Ab1!');
await pw.blur();
await page.waitForTimeout(300);
await expect(page.getByRole('alert').first()).toBeVisible({ timeout: 3_000 });
// Fix the password
await pw.fill('ValidPassword1!xx');
await page.waitForTimeout(500);
const validationErrors = page.locator('[role="alert"]').filter({
hasText: /required|requis|short|court|8 char|requerida/i,
});
await expect(validationErrors).not.toBeVisible({ timeout: 3_000 });
});
test('14. Soumission invalide ne fait pas d\'appel API', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const apiCalls: string[] = [];
page.on('request', (req) => {
if (req.url().includes('/auth/password/reset') && req.method() === 'POST') {
apiCalls.push(req.url());
}
});
// Click submit with empty fields
await page.locator(SUBMIT_SELECTOR).click();
await page.waitForTimeout(1_000);
expect(apiCalls.length).toBe(0);
});
});
// =============================================================================
// ACCESSIBILITÉ
// =============================================================================
test.describe('RESET PASSWORD — Accessibilité', () => {
test('15. #main-content existe pour le skip link', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeAttached();
});
test('16. Les inputs password ont des labels associés', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
// Both password inputs should have accessible labels
const inputs = page.locator('input[type="password"]');
const count = await inputs.count();
expect(count).toBe(2);
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const id = await input.getAttribute('id');
expect(id).toBeTruthy();
const label = page.locator(`label[for="${id}"]`);
await expect(label).toBeAttached();
}
});
test('17. Le formulaire a un aria-label', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
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('18. Navigation clavier : Tab traverse les champs correctement', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
// Focus first password
const pw = page.locator('input[type="password"]').first();
await pw.focus();
await expect(pw).toBeFocused();
// Tab to confirm password
await page.keyboard.press('Tab');
const focused1 = await page.evaluate(() => document.activeElement?.getAttribute('type'));
expect(focused1).toBe('password');
});
test('19. [BUG #3] Toggle show/hide password fonctionne', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
await pw.fill('TestPassword1!');
// Find the show password toggle button
const toggleBtn = page.getByRole('button', { name: /show|hide|afficher|masquer/i }).first();
await expect(toggleBtn).toBeVisible({ timeout: 3_000 });
await toggleBtn.click();
// Input type should change to text
const inputType = await pw.getAttribute('type');
expect(inputType).toBe('text');
});
});
// =============================================================================
// SOUMISSION API
// =============================================================================
test.describe('RESET PASSWORD — Soumission API', () => {
test('20. [BUG #9] Token invalide affiche un message d\'erreur lisible', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
const confirm = page.locator('input[type="password"]').nth(1);
await pw.fill('MySecure1!Pass');
await confirm.fill('MySecure1!Pass');
await page.locator(SUBMIT_SELECTOR).click();
// Wait for error to appear
const alert = page.getByRole('alert');
await expect(alert.first()).toBeVisible({ timeout: 10_000 });
const alertText = (await alert.first().textContent()) || '';
// Should NOT show generic "Error authenticating" but a meaningful error
expect(alertText).not.toMatch(/Error authenticating/i);
expect(alertText.length).toBeGreaterThan(0);
});
});
// =============================================================================
// UX
// =============================================================================
test.describe('RESET PASSWORD — UX', () => {
test('21. [BUG #7] Le titre de la page est mis à jour', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const title = await page.title();
expect(title).toMatch(/reset|réinitialis|restablecer/i);
});
test('22. [BUG #8] Le bouton submit a aria-busy pendant le chargement', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
const confirm = page.locator('input[type="password"]').nth(1);
await pw.fill('MySecure1!Pass');
await confirm.fill('MySecure1!Pass');
await page.locator(SUBMIT_SELECTOR).click();
const btn = page.locator(SUBMIT_SELECTOR);
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 goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
const confirm = page.locator('input[type="password"]').nth(1);
await pw.fill('MySecure1!Pass');
await confirm.fill('MySecure1!Pass');
await confirm.press('Enter');
const alert = page.getByRole('alert');
await expect(alert.first()).toBeVisible({ timeout: 10_000 });
});
test('24. Password strength indicator s\'affiche quand on tape', async ({ page }) => {
await goToResetPassword(page, FAKE_TOKEN);
const pw = page.locator('input[type="password"]').first();
await pw.fill('Ab1!test');
await page.waitForTimeout(300);
// Should see strength indicator (Weak/Fair/Good/Strong)
const body = (await page.textContent('body')) || '';
expect(body).toMatch(/Weak|Fair|Good|Strong|Faible|Moyen|Bon|Fort/i);
});
});
// =============================================================================
// RESPONSIVE
// =============================================================================
test.describe('RESET PASSWORD — Responsive', () => {
test('25. Mobile (375px) — formulaire visible sans overflow', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await goToResetPassword(page, FAKE_TOKEN);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
const hasOverflow = await page.evaluate(() =>
document.documentElement.scrollWidth > document.documentElement.clientWidth
);
expect(hasOverflow).toBe(false);
});
test('26. Tablet (768px) — page affichée correctement', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await goToResetPassword(page, FAKE_TOKEN);
await expect(page.locator(FORM_SELECTOR)).toBeVisible({ timeout: 10_000 });
await expect(page.locator(SUBMIT_SELECTOR)).toBeVisible();
});
});