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>
343 lines
15 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|