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>
477 lines
21 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|