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>
332 lines
14 KiB
TypeScript
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();
|
|
});
|
|
});
|