import { test, expect, type Page, type ConsoleMessage } from '@playwright/test'; import { CONFIG, loginViaAPI, navigateTo } from './helpers'; // Test accounts matching the user's specification const TEST_ACCOUNTS = { user: { email: 'user@veza.music', password: 'User123!' }, creator: { email: 'artist@veza.music', password: 'Artist123!' }, admin: { email: 'admin@veza.music', password: 'Admin123!' }, }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function loginAndGoToDashboard(page: Page, account: keyof typeof TEST_ACCOUNTS) { const { email, password } = TEST_ACCOUNTS[account]; await loginViaAPI(page, email, password); await navigateTo(page, '/dashboard'); // Wait for dashboard content to render await page.locator('main').waitFor({ state: 'visible', timeout: CONFIG.timeouts.navigation }); await page.waitForTimeout(1500); // Allow async data to load } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- test.describe('Dashboard (/dashboard)', () => { // ========================================================================= // Chargement & Rendu // ========================================================================= test.describe('Chargement & Rendu', () => { test('la page se charge sans erreur pour le compte user', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator('h1')).toBeVisible(); await expect(page.locator('h1')).toContainText(/Good (morning|afternoon|evening)/); }); test('la page se charge pour le compte creator', async ({ page }) => { await loginAndGoToDashboard(page, 'creator'); await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator('h1')).toBeVisible(); }); test('la page se charge pour le compte admin', async ({ page }) => { await loginAndGoToDashboard(page, 'admin'); await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator('h1')).toBeVisible(); }); test('tous les elements principaux sont visibles', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // Welcome banner await expect(page.locator('h1')).toBeVisible(); // Quick action links (top row) await expect(page.getByRole('link', { name: 'Upload Track' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Create Playlist' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Discover Music' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Open Chat' })).toBeVisible(); // Stats section await expect(page.getByRole('region', { name: 'Performance statistics' })).toBeVisible(); // Activity and content section await expect(page.getByRole('region', { name: 'Activity and content' })).toBeVisible(); // Quick action buttons (bottom) await expect(page.getByRole('button', { name: 'New Track' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Library' })).toBeVisible(); }); test('pas d\'erreurs critiques dans la console', async ({ page }) => { const errors: string[] = []; page.on('console', (msg: ConsoleMessage) => { if (msg.type() === 'error') { const text = msg.text(); // Ignore known non-critical errors if (text.includes('401') && text.includes('/auth/me')) return; // Pre-login auth check if (text.includes('Download the React DevTools')) return; errors.push(text); } }); await loginAndGoToDashboard(page, 'user'); // Filter for truly critical JS errors (not API 4xx which are expected) const criticalErrors = errors.filter(e => !e.includes('Failed to load resource') && !e.includes('Audio playback error') ); expect(criticalErrors).toHaveLength(0); }); }); // ========================================================================= // Fonctionnalites // ========================================================================= test.describe('Fonctionnalites', () => { test('les stats affichent les bons labels (Tracks Listened, Favorites)', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const statsRegion = page.getByRole('region', { name: 'Performance statistics' }); // BUG #1 regression: labels must match API data await expect(statsRegion.getByText('Tracks Listened')).toBeVisible(); await expect(statsRegion.getByText('Favorites')).toBeVisible(); await expect(statsRegion.getByText('Messages')).toBeVisible(); await expect(statsRegion.getByText('Friends')).toBeVisible(); // Must NOT show old incorrect labels await expect(statsRegion.getByText('Tracks in Library')).not.toBeVisible(); await expect(statsRegion.getByText('Playlists')).not.toBeVisible(); }); test('les quick action links pointent vers les bonnes pages', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // BUG #4 regression: Discover Music -> /discover (not /search) const discoverLink = page.getByRole('link', { name: 'Discover Music' }); await expect(discoverLink).toHaveAttribute('href', '/discover'); // BUG #5 regression: Create Playlist -> /playlists (not /library) const playlistLink = page.getByRole('link', { name: 'Create Playlist' }); await expect(playlistLink).toHaveAttribute('href', '/playlists'); // Other links remain correct await expect(page.getByRole('link', { name: 'Upload Track' })).toHaveAttribute('href', '/library?action=upload'); await expect(page.getByRole('link', { name: 'Open Chat' })).toHaveAttribute('href', '/chat'); }); test('Recent Activity n\'affiche pas de donnees mock hardcodees', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const activitySection = page.getByRole('region', { name: 'Activity and content' }); // BUG #2 regression: must NOT contain hardcoded mock text await expect(activitySection.getByText('Message from @alice')).not.toBeVisible(); }); test('le lien View all de Recent Activity pointe vers /notifications', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // BUG #6 regression: View all in Recent Activity -> /notifications (not /library) const activityCard = page.locator('.ink-card').filter({ hasText: 'Recent Activity' }); const viewAllLink = activityCard.getByRole('link', { name: /View all/ }); await expect(viewAllLink).toHaveAttribute('href', '/notifications'); }); test('les quick action buttons fonctionnent', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // Click "New Track" button await page.getByRole('button', { name: 'New Track' }).click(); await expect(page).toHaveURL(/\/library\?action=upload/); }); test('la sidebar admin est visible pour le compte admin', async ({ page }) => { await loginAndGoToDashboard(page, 'admin'); // Admin should see System section with Admin Panel const sidebar = page.getByRole('complementary', { name: 'Main sidebar' }); await expect(sidebar.getByText('System')).toBeVisible(); await expect(sidebar.getByRole('link', { name: 'Admin Panel' })).toBeVisible(); }); test('la sidebar admin n\'est PAS visible pour le compte user', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const sidebar = page.getByRole('complementary', { name: 'Main sidebar' }); await expect(sidebar.getByText('System')).not.toBeVisible(); }); }); // ========================================================================= // Securite // ========================================================================= test.describe('Securite', () => { test('le dashboard est inaccessible sans authentification', async ({ page }) => { await page.goto(`${CONFIG.baseURL}/dashboard`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2000); // Should redirect to login await expect(page).toHaveURL(/\/login/); }); test('pas de tokens ou emails dans l\'URL', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const url = page.url(); expect(url).not.toContain('token'); expect(url).not.toContain('@'); expect(url).not.toContain('password'); }); }); // ========================================================================= // Accessibilite // ========================================================================= test.describe('Accessibilite', () => { test('les boutons du player ont des aria-labels', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const playerControls = page.getByRole('region', { name: 'Playback controls' }); // BUG #8 regression: all player buttons must have aria-labels const buttons = playerControls.getByRole('button'); const count = await buttons.count(); expect(count).toBeGreaterThanOrEqual(5); for (let i = 0; i < count; i++) { const btn = buttons.nth(i); const label = await btn.getAttribute('aria-label'); expect(label, `Player button ${i} should have aria-label`).toBeTruthy(); } }); test('le bouton theme toggle a un aria-label', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // BUG #9 regression: theme toggle must have accessible name // Find the button between notifications and user avatar in header const header = page.getByRole('banner'); const themeButton = header.getByRole('button', { name: /theme|Change theme/i }); await expect(themeButton).toBeVisible(); }); test('le skip to content link fonctionne', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const skipLink = page.getByRole('link', { name: 'Skip to content' }); await expect(skipLink).toBeAttached(); }); test('les sections ont des labels ARIA', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); await expect(page.getByRole('complementary', { name: 'Main sidebar' })).toBeVisible(); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); await expect(page.getByRole('region', { name: 'Performance statistics' })).toBeVisible(); await expect(page.getByRole('region', { name: 'Activity and content' })).toBeVisible(); await expect(page.getByRole('region', { name: 'Global player' })).toBeVisible(); }); }); // ========================================================================= // i18n // ========================================================================= test.describe('i18n', () => { test('pas de cles i18n brutes visibles', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const bodyText = await page.locator('main').innerText(); // i18n keys look like "namespace.key.subkey" const rawKeyPattern = /\b(dashboard|common|nav)\.\w+\.\w+/; expect(bodyText).not.toMatch(rawKeyPattern); }); test('pas de melange de langues (tout en anglais)', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const bodyText = await page.locator('main').innerText(); // Should not contain French-only words that would indicate mixed i18n expect(bodyText).not.toContain('Tableau de bord'); expect(bodyText).not.toContain('Bibliothèque'); expect(bodyText).not.toContain('Activité récente'); }); }); // ========================================================================= // Regression (un test par bug corrige) // ========================================================================= test.describe('Regression', () => { test('BUG#1: stats labels correspondent aux donnees API', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const stats = page.getByRole('region', { name: 'Performance statistics' }); // "Tracks Listened" not "Tracks in Library", "Favorites" not "Playlists" await expect(stats.getByText('Tracks Listened')).toBeVisible(); await expect(stats.getByText('Favorites')).toBeVisible(); }); test('BUG#2: Recent Activity utilise des donnees reelles', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const activity = page.locator('.ink-card').filter({ hasText: 'Recent Activity' }); // Should not contain hardcoded mock data await expect(activity.getByText('Message from @alice')).not.toBeVisible(); await expect(activity.getByText('New track added')).not.toBeVisible(); await expect(activity.getByText('New favorite added')).not.toBeVisible(); }); test('BUG#4: Discover Music lien vers /discover', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); await expect(page.getByRole('link', { name: 'Discover Music' })).toHaveAttribute('href', '/discover'); }); test('BUG#5: Create Playlist lien vers /playlists', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); await expect(page.getByRole('link', { name: 'Create Playlist' })).toHaveAttribute('href', '/playlists'); }); test('BUG#6: View all Recent Activity lien vers /notifications', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const activityCard = page.locator('.ink-card').filter({ hasText: 'Recent Activity' }); await expect(activityCard.getByRole('link', { name: /View all/ })).toHaveAttribute('href', '/notifications'); }); test('BUG#8: player buttons ont des aria-labels', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // Check specific labels await expect(page.getByRole('button', { name: 'Play' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Previous track' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Next track' })).toBeVisible(); await expect(page.getByRole('button', { name: /shuffle/i })).toBeVisible(); await expect(page.getByRole('button', { name: /repeat/i })).toBeVisible(); }); test('BUG#9: theme toggle a un aria-label', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); const header = page.getByRole('banner'); await expect(header.getByRole('button', { name: /theme/i })).toBeVisible(); }); test('BUG#10: "+N more" annonces est un bouton cliquable', async ({ page }) => { await loginAndGoToDashboard(page, 'user'); // If there are multiple announcements, the "+N more" element should be a button const moreBtn = page.getByRole('button', { name: /\+\d+ more/ }); if (await moreBtn.isVisible()) { await moreBtn.click(); // After clicking, more announcements should be visible (or button disappears) await expect(moreBtn).not.toBeVisible(); } }); }); });