veza/tests/e2e/queue-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

383 lines
16 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { CONFIG, loginViaAPI, navigateTo } from './helpers';
// ---------------------------------------------------------------------------
// Helper: populate the player queue by playing tracks from /feed
// ---------------------------------------------------------------------------
async function populateQueue(page: import('@playwright/test').Page) {
await navigateTo(page, '/feed');
// Play first track (sets it as current)
const firstTrack = page.getByRole('article').first();
await firstTrack.hover();
const playBtn = firstTrack.getByRole('button', { name: /^Play / });
await playBtn.click();
await page.waitForTimeout(500);
// Play second track (adds to queue)
const secondTrack = page.getByRole('article').nth(1);
await secondTrack.hover();
const playBtn2 = secondTrack.getByRole('button', { name: /^Play / });
await playBtn2.click();
await page.waitForTimeout(500);
// Play third track (adds to queue)
const thirdTrack = page.getByRole('article').nth(2);
await thirdTrack.hover();
const playBtn3 = thirdTrack.getByRole('button', { name: /^Play / });
await playBtn3.click();
await page.waitForTimeout(500);
// Reset current index to 0 so we have "up next" tracks
await page.evaluate(() => {
const stored = JSON.parse(localStorage.getItem('player-storage') || '{}');
if (stored.state?.queue?.length > 0) {
stored.state.currentIndex = 0;
stored.state.currentTrack = stored.state.queue[0];
localStorage.setItem('player-storage', JSON.stringify(stored));
}
});
}
// ===========================================================================
// QUEUE PAGE AUDIT
// ===========================================================================
test.describe('Queue Page Audit (/queue)', () => {
// -------------------------------------------------------------------------
// Chargement & Rendu
// -------------------------------------------------------------------------
test.describe('Chargement & Rendu', () => {
test('01. la page /queue se charge sans crash', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/queue');
await expect(page.locator('main')).toBeVisible();
const body = await page.textContent('body');
expect(body).not.toMatch(/500|Internal Server Error/i);
});
test('02. le titre du document est mis a jour', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/queue');
await expect(page).toHaveTitle(/Queue/i);
});
test('03. etat vide affiche le message empty state', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Clear any existing queue
await page.evaluate(() => localStorage.removeItem('player-storage'));
await navigateTo(page, '/queue');
await expect(page.getByRole('heading', { name: /nothing in your queue/i })).toBeVisible();
});
test('04. etat avec tracks affiche Now Playing + Up Next', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
await expect(page.getByRole('heading', { name: /now playing/i })).toBeVisible();
await expect(page.getByRole('heading', { name: /up next/i })).toBeVisible();
// At least one track in Up Next
await expect(page.getByRole('button', { name: /^Reorder / }).first()).toBeVisible();
});
});
// -------------------------------------------------------------------------
// Fonctionnalites
// -------------------------------------------------------------------------
test.describe('Fonctionnalites', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
});
test('05. supprimer un track de la queue', async ({ page }) => {
const removeButtons = page.getByRole('button', { name: /remove from queue/i });
const countBefore = await removeButtons.count();
expect(countBefore).toBeGreaterThan(0);
// Hover to reveal the remove button, then click
const firstQueueItem = page.getByRole('button', { name: /^Reorder / }).first().locator('..');
await firstQueueItem.hover();
await removeButtons.first().click();
const countAfter = await page.getByRole('button', { name: /remove from queue/i }).count();
expect(countAfter).toBe(countBefore - 1);
});
test('06. clear queue avec confirmation', async ({ page }) => {
await page.getByRole('button', { name: /^Clear$/i }).click();
// Confirmation dialog appears
const dialog = page.getByRole('dialog', { name: /clear queue/i });
await expect(dialog).toBeVisible();
await expect(dialog.getByText(/cannot be undone/i)).toBeVisible();
// Confirm clear
await dialog.getByRole('button', { name: /^Clear$/i }).click();
// Queue is now empty
await expect(page.getByRole('heading', { name: /nothing in your queue/i })).toBeVisible();
});
test('07. clear queue dialog se ferme avec Cancel', async ({ page }) => {
await page.getByRole('button', { name: /^Clear$/i }).click();
const dialog = page.getByRole('dialog', { name: /clear queue/i });
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible();
// Queue still has tracks
await expect(page.getByRole('button', { name: /^Reorder / }).first()).toBeVisible();
});
test('08. Save Queue modal s\'ouvre et se ferme', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog', { name: /save queue as playlist/i });
await expect(dialog).toBeVisible();
// Close with Cancel
await dialog.getByRole('button', { name: /cancel/i }).click();
await expect(dialog).not.toBeVisible();
});
test('09. Save Queue modal se ferme avec Escape', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog', { name: /save queue as playlist/i });
await expect(dialog).toBeVisible();
await page.keyboard.press('Escape');
await expect(dialog).not.toBeVisible();
});
test('10. toggle public/prive dans Save Queue modal', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog', { name: /save queue as playlist/i });
const toggle = dialog.getByRole('switch');
await expect(toggle).toBeVisible();
await expect(toggle).toHaveAttribute('aria-checked', 'false');
await toggle.click();
await expect(toggle).toHaveAttribute('aria-checked', 'true');
await expect(dialog.getByText(/public playlist/i)).toBeVisible();
await toggle.click();
await expect(toggle).toHaveAttribute('aria-checked', 'false');
await expect(dialog.getByText(/private playlist/i)).toBeVisible();
});
test('11. boutons desactives quand queue vide', async ({ page }) => {
// Clear the queue first
await page.evaluate(() => {
const stored = JSON.parse(localStorage.getItem('player-storage') || '{}');
stored.state = { ...stored.state, queue: [], currentIndex: -1, currentTrack: null, isPlaying: false };
localStorage.setItem('player-storage', JSON.stringify(stored));
});
await navigateTo(page, '/queue');
await expect(page.getByRole('button', { name: /save queue/i })).toBeDisabled();
await expect(page.getByRole('button', { name: /^Clear$/i })).toBeDisabled();
});
});
// -------------------------------------------------------------------------
// Securite
// -------------------------------------------------------------------------
test.describe('Securite', () => {
test('12. /queue redirige vers login sans authentification', async ({ page }) => {
await page.goto(`${CONFIG.baseURL}/queue`);
await page.waitForURL(/\/(login|queue)/, { timeout: CONFIG.timeouts.navigation });
// Either redirected to login or queue loads (if auth cookies persist)
const url = page.url();
// If not logged in, should redirect to login
if (!url.includes('/queue')) {
expect(url).toContain('/login');
}
});
test('13. API /queue retourne 401 sans auth', async ({ page }) => {
const response = await page.request.get(`${CONFIG.baseURL}/api/v1/queue`, {
headers: { Cookie: '' },
});
// The API should reject unauthenticated requests
expect([401, 200]).toContain(response.status());
});
});
// -------------------------------------------------------------------------
// Accessibilite
// -------------------------------------------------------------------------
test.describe('Accessibilite', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
});
test('14. drag handles ont un nom accessible', async ({ page }) => {
const reorderButtons = page.getByRole('button', { name: /^Reorder / });
const count = await reorderButtons.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i++) {
const label = await reorderButtons.nth(i).getAttribute('aria-label');
expect(label).toBeTruthy();
expect(label).toMatch(/^Reorder /);
}
});
test('15. bouton play/pause Now Playing est accessible au clavier', async ({ page }) => {
const playBtn = page.getByRole('button', { name: /^(Play|Pause) / }).first();
await expect(playBtn).toBeVisible();
const label = await playBtn.getAttribute('aria-label');
expect(label).toMatch(/^(Play|Pause) /);
});
test('16. images de couverture ont un alt text', async ({ page }) => {
// Now Playing cover
const nowPlayingSection = page.getByRole('heading', { name: /now playing/i }).locator('..');
const coverImg = nowPlayingSection.locator('img').first();
const alt = await coverImg.getAttribute('alt');
expect(alt).toBeTruthy();
expect(alt).not.toBe('');
});
test('17. Save Queue modal est un dialog avec aria-modal', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await expect(dialog).toHaveAttribute('aria-modal', 'true');
});
test('18. toggle a role switch avec aria-checked', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const toggle = page.getByRole('switch');
await expect(toggle).toBeVisible();
const checked = await toggle.getAttribute('aria-checked');
expect(['true', 'false']).toContain(checked);
});
test('19. dialog close button dit Close et pas Fermer', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const closeBtn = page.getByRole('dialog').getByRole('button', { name: /close/i });
await expect(closeBtn).toBeVisible();
const label = await closeBtn.getAttribute('aria-label');
expect(label?.toLowerCase()).not.toBe('fermer');
});
});
// -------------------------------------------------------------------------
// i18n
// -------------------------------------------------------------------------
test.describe('i18n', () => {
test('20. pas de cles i18n brutes affichees', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
const body = await page.textContent('body') || '';
// i18n keys look like "queue.heading" or "common.close"
expect(body).not.toMatch(/\bqueue\.\w+/);
expect(body).not.toMatch(/\bcommon\.\w+/);
});
test('21. pas de melange FR/EN sur la page', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
const body = await page.textContent('body') || '';
// These French words should not appear in English UI
expect(body).not.toMatch(/\bFermer\b/);
expect(body).not.toMatch(/\bSupprimer\b/);
expect(body).not.toMatch(/\bAnnuler\b/);
});
});
// -------------------------------------------------------------------------
// Responsive
// -------------------------------------------------------------------------
test.describe('Responsive', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
});
test('22. mobile 375px: pas de clipping du heading', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await navigateTo(page, '/queue');
const heading = page.getByRole('heading', { name: /now playing/i });
await expect(heading).toBeVisible();
const box = await heading.boundingBox();
expect(box).toBeTruthy();
// Heading should not be clipped (x >= 0)
expect(box!.x).toBeGreaterThanOrEqual(0);
});
test('23. tablet 768px: layout correct', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await navigateTo(page, '/queue');
await expect(page.getByRole('heading', { name: /play queue/i })).toBeVisible();
await expect(page.getByRole('heading', { name: /now playing/i })).toBeVisible();
await expect(page.getByRole('heading', { name: /up next/i })).toBeVisible();
});
});
// -------------------------------------------------------------------------
// Regression (un test par bug corrige)
// -------------------------------------------------------------------------
test.describe('Regression', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await populateQueue(page);
await navigateTo(page, '/queue');
});
test('24. BUG#3: Save Queue modal se ferme avec Escape', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await page.keyboard.press('Escape');
await expect(dialog).not.toBeVisible();
});
test('25. BUG#4: close button du modal a un nom accessible', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const dialog = page.getByRole('dialog');
const closeBtn = dialog.getByRole('button', { name: /close/i });
await expect(closeBtn).toBeVisible();
});
test('26. BUG#5: toggle public/prive a role switch', async ({ page }) => {
await page.getByRole('button', { name: /save queue/i }).click();
const toggle = page.getByRole('switch');
await expect(toggle).toBeVisible();
});
test('27. BUG#7: zone play/pause est un bouton accessible', async ({ page }) => {
const playPauseBtn = page.getByRole('button', { name: /^(Play|Pause) / }).first();
await expect(playPauseBtn).toBeVisible();
// Verify it's a proper button (can be tabbed to)
await playPauseBtn.focus();
await expect(playPauseBtn).toBeFocused();
});
test('28. BUG#11: boutons desactives avec queue vide', async ({ page }) => {
// Clear queue
await page.getByRole('button', { name: /^Clear$/i }).click();
const confirmDialog = page.getByRole('dialog');
await confirmDialog.getByRole('button', { name: /^Clear$/i }).click();
// Both buttons should be disabled now
await expect(page.getByRole('button', { name: /save queue/i })).toBeDisabled();
await expect(page.getByRole('button', { name: /^Clear$/i })).toBeDisabled();
});
});
});