veza/tests/e2e/dashboard-audit.spec.ts
senke 9a4c0d2af4 feat(web): update all features, stories, e2e tests, and auth interceptor
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>
2026-03-31 19:16:36 +02:00

343 lines
15 KiB
TypeScript

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();
}
});
});
});