import { test, expect } from '@playwright/test'; import { CONFIG, loginViaAPI } from './helpers'; const BASE = CONFIG.baseURL; const VALID_USERNAME = CONFIG.users.admin.username; // admin_veza const LISTENER_USERNAME = CONFIG.users.listener.username; // music_fan test.describe('Profil public utilisateur (/u/:username)', () => { test.describe('Chargement & Rendu', () => { test('la page se charge sans erreur pour un visiteur non connecté', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Should NOT redirect to /login (BUG #1 regression) expect(page.url()).toContain(`/u/${VALID_USERNAME}`); // Profile name should be visible await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); }); test('la page se charge pour un utilisateur connecté', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); expect(page.url()).toContain(`/u/${VALID_USERNAME}`); await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); }); test('le titre de page contient le nom du profil', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #9 regression: title should include username await expect(page).toHaveTitle(new RegExp(VALID_USERNAME)); }); test('le skeleton de chargement est affiché', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'commit' }); // The skeleton should appear with role="status" and aria-label const skeleton = page.locator('[role="status"]'); // It may or may not still be visible depending on load speed // Just verify it was rendered at some point (not a hard requirement) }); }); test.describe('Fonctionnalités', () => { test('affiche les infos publiques (avatar, bio, stats)', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Username heading await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); // @ handle await expect(page.getByText(`@`)).toBeVisible(); // Member since date await expect(page.getByText(/Joined|Membre depuis/)).toBeVisible(); // Stats section - should have tracks, playlists, followers, following await expect(page.getByText(/Tracks|Morceaux|Pistas/)).toBeVisible(); await expect(page.getByText(/Followers|Abonnés|Seguidores/)).toBeVisible(); }); test('les onglets affichent du contenu (Tracks, Playlists, Reposts, Feed)', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #2 regression: tab content should render // Tracks tab (default) should show tabpanel const tracksPanel = page.getByRole('tabpanel'); await expect(tracksPanel).toBeVisible(); // Click Playlists tab await page.getByRole('tab', { name: /Playlists/ }).click(); await expect(page.getByRole('tabpanel')).toBeVisible(); // Click Reposts tab await page.getByRole('tab', { name: /Reposts/ }).click(); await expect(page.getByRole('tabpanel')).toBeVisible(); // Click Feed tab await page.getByRole('tab', { name: /Feed|Fil/ }).click(); await expect(page.getByRole('tabpanel')).toBeVisible(); }); test('page 404 avec username inexistant', async ({ page }) => { await page.goto(`${BASE}/u/nonexistent_user_xyz_test`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #3 regression: should show "User Not Found" not "Something went wrong" await expect(page.getByRole('heading', { level: 2 })).toContainText(/Not Found|introuvable|no encontrado/); // Should NOT have "Try again" button for 404 await expect(page.getByRole('button', { name: /Try again|Réessayer|Reintentar/ })).not.toBeVisible(); // Should have "Return to Base" link await expect(page.getByRole('link', { name: /Return to Base|Retour|Volver/ })).toBeVisible(); }); test('bouton follow visible pour un autre utilisateur connecté', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Follow button should be visible (viewing another user's profile) await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).toBeVisible(); }); test('bouton follow masqué sur son propre profil', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await page.goto(`${BASE}/u/${LISTENER_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Follow button should NOT be visible on own profile await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).not.toBeVisible(); }); test('bouton follow masqué pour visiteur non connecté', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Follow button should NOT be visible when not logged in await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).not.toBeVisible(); }); }); test.describe('Sécurité', () => { test('XSS dans le paramètre username est neutralisé', async ({ page }) => { await page.goto(`${BASE}/u/%3Cscript%3Ealert(1)%3C%2Fscript%3E`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Should show error page, not execute script expect(page.url()).not.toContain('login'); // Should not redirect // No alert dialog should have appeared (React escapes by default) }); test('pas de fuite de token dans l\'URL ou le DOM', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // URL should not contain tokens expect(page.url()).not.toMatch(/token|jwt|bearer/i); // DOM should not expose tokens const html = await page.content(); expect(html).not.toMatch(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/); // JWT pattern }); test('pas de fuite d\'email dans le profil public', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); const html = await page.content(); expect(html).not.toContain('admin@veza'); expect(html).not.toContain('@veza.fr'); expect(html).not.toContain('@veza.music'); }); }); test.describe('Accessibilité', () => { test('le lien Skip to content fonctionne', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #6 regression: #main-content should exist const mainContent = page.locator('#main-content'); await expect(mainContent).toBeAttached(); // Skip link should point to #main-content const skipLink = page.getByRole('link', { name: /Skip to content/ }); await expect(skipLink).toHaveAttribute('href', '#main-content'); }); test('les onglets ont les rôles ARIA corrects', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // tablist should exist await expect(page.getByRole('tablist')).toBeVisible(); // tabs should exist const tabs = page.getByRole('tab'); await expect(tabs).toHaveCount(4); // Active tab should have tabpanel await expect(page.getByRole('tabpanel')).toBeVisible(); }); test('les headings sont structurés correctement (h1, h2)', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // h1 = username/display name await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1); // h2 = About section const h2 = page.getByRole('heading', { level: 2 }); await expect(h2.first()).toBeVisible(); }); }); test.describe('i18n', () => { test('pas de clés i18n brutes affichées', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); const text = await page.textContent('body'); // Should not contain raw i18n keys expect(text).not.toMatch(/profilePublic\./); expect(text).not.toMatch(/common\./); }); test('pas de mélange de langues EN/FR', async ({ page }) => { await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); const text = await page.textContent('body') ?? ''; // BUG #5 regression: should not mix French and English // If page is in English, should not have French-only words if (text.includes('About')) { expect(text).not.toMatch(/\bSuivre\b/); expect(text).not.toMatch(/\bAbonné\b/); expect(text).not.toMatch(/\bDésabonnement\b/); } }); }); test.describe('Responsive', () => { test('mobile 375px - stats ne débordent pas', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #7 regression: all 4 stat labels should be visible await expect(page.getByText(/Following|Abonnements|Siguiendo/)).toBeVisible(); await expect(page.getByText(/Followers|Abonnés|Seguidores/)).toBeVisible(); }); test('tablet 768px - layout correct', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Page should render without crash await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); }); }); test.describe('Réseau & API', () => { test('pas d\'erreur 404 pour /roles', async ({ page }) => { const errors: string[] = []; page.on('response', (response) => { if (response.url().includes('/roles') && response.status() === 404) { errors.push(response.url()); } }); await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // BUG #4 regression: /roles endpoint should not be called expect(errors).toHaveLength(0); }); }); });