import { test, expect } from '@chromatic-com/playwright'; import { loginViaAPI, CONFIG, navigateTo, assertPageLoads, assertNoDebugText, assertNotBroken } from './helpers'; test.describe('NAVIGATION — Pages publiques (sans auth)', () => { test('01. Page d\'accueil / redirige vers /dashboard ou /login', async ({ page }) => { const errors = await assertPageLoads(page, '/'); expect(errors.length).toBeLessThan(3); // Root / should redirect to /dashboard (if auth) or /login (if not) await expect(page).toHaveURL(/dashboard|login/); await assertNoDebugText(page); }); test('02. Page /login se charge', async ({ page }) => { await assertPageLoads(page, '/login'); }); test('03. Page /register se charge', async ({ page }) => { await assertPageLoads(page, '/register'); }); test('04. Page /discover redirige vers /login si non authentifié', async ({ page }) => { test.setTimeout(60_000); await page.goto('/discover', { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle').catch(() => {}); // /discover is a protected route, should redirect to login // The app may take time to check auth and redirect await expect(page).toHaveURL(/login/, { timeout: 20_000 }); }); test('05. Page 404 pour route inexistante', async ({ page }) => { await navigateTo(page, '/this-page-does-not-exist-12345'); const body = await page.textContent('body'); // Should display a proper 404, not a crash expect(body).toMatch(/404|not found|page.*introuvable|n'existe pas/i); }); }); test.describe('NAVIGATION — Pages authentifiées', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); const authenticatedPages = [ { path: '/dashboard', name: 'Dashboard' }, { path: '/library', name: 'Bibliothèque' }, { path: '/playlists', name: 'Playlists' }, { path: '/notifications', name: 'Notifications' }, { path: '/chat', name: 'Chat' }, { path: '/settings', name: 'Paramètres' }, { path: '/profile', name: 'Profil' }, { path: '/feed', name: 'Feed' }, { path: '/discover', name: 'Découverte' }, { path: '/search', name: 'Recherche' }, ]; for (const { path, name } of authenticatedPages) { test(`06. Page ${name} (${path}) se charge @critical`, async ({ page }) => { await navigateTo(page, path); // No crash const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error|unexpected error|something went wrong/i); // Page has content (not just an infinite spinner) expect(body.length).toBeGreaterThan(100); await assertNoDebugText(page); }); } }); test.describe('NAVIGATION — Layout principal', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('07. La sidebar est visible @critical', async ({ page }) => { // Check login succeeded await navigateTo(page, '/dashboard'); const sidebar = page.getByTestId('app-sidebar'); await expect(sidebar).toBeVisible({ timeout: 10_000 }); }); test('08. Le header est visible et le logo est dans la sidebar', async ({ page }) => { await navigateTo(page, '/dashboard'); // Header has data-testid="app-header" const header = page.locator('[data-testid="app-header"], header').first(); await expect(header).toBeVisible({ timeout: 5_000 }); // Logo "veza" is an h2 in the sidebar — it may be visually hidden when collapsed but still attached const sidebar = page.getByTestId('app-sidebar'); await expect(sidebar).toBeVisible({ timeout: 5_000 }); // The h2 "veza" may be collapsed (opacity-0 max-w-0) but still in DOM await expect(sidebar.locator('h2')).toBeAttached(); }); test('09. Les liens de navigation principaux sont présents et cliquables', async ({ page }) => { await navigateTo(page, '/dashboard'); const navLinks = [ /dashboard/i, /discover/i, /library/i, ]; const sidebar = page.getByTestId('app-sidebar'); for (const linkText of navLinks) { const link = sidebar.getByRole('link', { name: linkText }) .or(sidebar.getByRole('button', { name: linkText })) .first(); await expect(link).toBeVisible({ timeout: 5_000 }); } }); test('10. Le player bar est présent dans le DOM', async ({ page }) => { await navigateTo(page, '/dashboard'); const playerBar = page.getByTestId('global-player'); // The player bar container should exist in the DOM even if not visible (nothing playing) await expect(playerBar).toBeAttached({ timeout: 5_000 }); }); test('10b. Le search est dans le header avec role="search"', async ({ page }) => { await navigateTo(page, '/dashboard'); // Header search: data-testid="search-input" type="search" inside role="search" container const searchInput = page.locator('[data-testid="search-input"]') .or(page.locator('[role="search"] input')) .or(page.locator('input[type="search"]')); // Check it exists in DOM even if hidden on small viewports (hidden md:block) await expect(searchInput.first()).toBeAttached({ timeout: 5_000 }); }); }); test.describe('NAVIGATION — Responsive mobile @mobile', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('11. La page d\'accueil est utilisable sur mobile', async ({ page }) => { await navigateTo(page, '/dashboard'); // No horizontal scroll (sign of broken layout) const hasHorizontalScroll = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth; }); expect(hasHorizontalScroll).toBeFalsy(); }); test('12. Le menu hamburger fonctionne sur mobile', async ({ page }) => { await navigateTo(page, '/dashboard'); const menuButton = page.getByRole('button', { name: /menu/i }) .or(page.locator('[class*="hamburger"]')) .or(page.locator('[class*="menu-toggle"]')) .or(page.getByTestId('mobile-menu')); if (await menuButton.isVisible().catch(() => false)) { await menuButton.click(); // Menu should open const sidebar = page.getByTestId('app-sidebar'); await expect(sidebar).toBeVisible({ timeout: 3_000 }); } }); }); test.describe('NAVIGATION — Internationalisation (i18n)', () => { test('13. Changement de langue FR -> EN', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await navigateTo(page, '/settings'); // Find the language selector const langSelector = page.getByLabel(/langue|language/i) .or(page.locator('select[name*="lang"]')) .or(page.getByTestId('language-selector')); const langVisible = await langSelector.isVisible().catch(() => false); if (!langVisible) { test.skip(true, 'Language selector not found in /settings'); return; } await langSelector.selectOption({ label: /english/i }); await page.waitForTimeout(1_000); // Verify English text appears const body = await page.textContent('body') || ''; expect(body).toMatch(/settings|profile|account|logout/i); }); test('14. Pas de clés i18n brutes visibles (ex: "auth.login.title")', async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); const pagesToCheck = ['/dashboard', '/discover', '/settings', '/library']; for (const path of pagesToCheck) { await navigateTo(page, path); const body = await page.textContent('body') || ''; // Pattern: "word.word.word" that looks like an untranslated i18n key const i18nKeyPattern = /\b[a-z]+\.[a-z]+\.[a-z]+\b/g; const matches = body.match(i18nKeyPattern) || []; // Filter false positives (URLs, etc.) const suspiciousKeys = matches.filter(m => !m.includes('http') && !m.includes('www') && !m.includes('com') && !m.includes('min') && !m.includes('max') && m.length < 50 ); expect(suspiciousKeys.length).toBeLessThanOrEqual(5); } }); });