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