veza/tests/e2e/dashboard-audit.spec.ts

344 lines
15 KiB
TypeScript
Raw Normal View History

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