import { test, expect } from '@chromatic-com/playwright'; import { loginViaAPI, CONFIG, navigateTo, navigateToPageWithTracks, assertPlayerVisible, playFirstTrack } from './helpers'; test.describe('PLAYER — Lecteur audio', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('01. Clic sur play lance la lecture d\'un track @critical', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); const trackCard = page.locator('[role="article"]').first(); await trackCard.hover(); await page.waitForTimeout(300); const playBtn = page.getByRole('button', { name: /^Lire /i }).first(); await expect(playBtn).toBeVisible({ timeout: 5_000 }); await playBtn.click(); await assertPlayerVisible(page); }); test('02. Le player affiche titre + artiste du track en cours', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); // Track info must be visible with real content const trackInfo = player.locator('[aria-label="Track info"]'); await expect(trackInfo).toBeVisible({ timeout: 5_000 }); const title = trackInfo.locator('h3'); await expect(title).toBeVisible(); const titleText = await title.textContent(); expect(titleText?.trim().length, 'Track title must not be empty').toBeGreaterThan(0); expect(titleText, 'Track title must not contain debug text').not.toMatch(/undefined|null|NaN/); const artist = trackInfo.locator('p'); await expect(artist).toBeVisible(); const artistText = await artist.textContent(); expect(artistText?.trim().length, 'Artist name must not be empty').toBeGreaterThan(0); expect(artistText, 'Artist name must not contain debug text').not.toMatch(/undefined|null|NaN/); }); test('03. Bouton play/pause toggle fonctionne', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); const playPauseBtn = player.getByTestId('play-button'); await expect(playPauseBtn).toBeVisible({ timeout: 5_000 }); // Toggle should not crash await playPauseBtn.click(); await page.waitForTimeout(500); await playPauseBtn.click(); await page.waitForTimeout(300); // Button must still be interactive after toggling await expect(playPauseBtn).toBeVisible(); await expect(playPauseBtn).toBeEnabled(); }); test('04. La barre de progression est visible et interactive', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); // Play a track to activate the progress bar const trackCard = page.locator('[role="article"]').first(); await trackCard.hover(); await page.waitForTimeout(300); const playBtn = page.getByRole('button', { name: /^Lire /i }).first(); await expect(playBtn).toBeVisible({ timeout: 5_000 }); await playBtn.click(); const player = await assertPlayerVisible(page); // Progress bar must be visible const progressBar = player.locator('[role="slider"][aria-label="Progression"]'); await expect(progressBar).toBeVisible({ timeout: 10_000 }); const box = await progressBar.boundingBox(); expect(box, 'Progress bar must have a bounding box').not.toBeNull(); expect(box!.width, 'Progress bar must have substantial width').toBeGreaterThan(50); // ARIA attributes must be set correctly await expect(progressBar).toHaveAttribute('aria-valuemin', '0'); const valueMax = await progressBar.getAttribute('aria-valuemax'); expect(Number(valueMax), 'aria-valuemax must be >= 0').toBeGreaterThanOrEqual(0); }); test('05. Controle du volume fonctionne', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); // Mute button must exist const muteBtn = player.getByRole('button', { name: /^mute$|^unmute$/i }).first(); await expect(muteBtn).toBeVisible({ timeout: 5_000 }); // Click mute — label must toggle const initialLabel = await muteBtn.getAttribute('aria-label'); await muteBtn.click(); await page.waitForTimeout(300); const newLabel = await player.getByRole('button', { name: /^mute$|^unmute$/i }).first().getAttribute('aria-label'); expect(newLabel, 'Mute button label must change after click').not.toBe(initialLabel); // Click again to restore await player.getByRole('button', { name: /^mute$|^unmute$/i }).first().click(); }); test('06. Boutons next/previous sont presents et actifs', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); const prevBtn = player.getByTestId('prev-button'); const playBtn = player.getByTestId('play-button'); const nextBtn = player.getByTestId('next-button'); await expect(prevBtn).toBeVisible({ timeout: 5_000 }); await expect(playBtn).toBeVisible(); await expect(nextBtn).toBeVisible(); // All transport buttons must be enabled await expect(prevBtn).toBeEnabled(); await expect(playBtn).toBeEnabled(); await expect(nextBtn).toBeEnabled(); }); test('07. Affichage du temps actuel / duree totale', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); await page.waitForTimeout(2_000); // Time display must show at least one timestamp in X:XX format const playbackControls = player.locator('[aria-label="Playback controls"]'); await expect(playbackControls).toBeVisible(); const timeTexts = playbackControls.locator(':text-matches("\\\\d+:\\\\d{2}")'); const count = await timeTexts.count(); expect(count, 'At least one time display must be present').toBeGreaterThanOrEqual(1); const text = await timeTexts.first().textContent(); expect(text, 'Time must match X:XX format').toMatch(/\d+:\d{2}/); }); test('08. Raccourcis clavier — Espace toggle play/pause', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); await page.waitForTimeout(1_000); // Press Space — must not crash await page.keyboard.press('Space'); await page.waitForTimeout(500); // Page must still be functional (no crash, no error) const body = await page.textContent('body') || ''; expect(body).not.toMatch(/error|crash/i); await assertPlayerVisible(page); }); }); test.describe('PLAYER — Queue de lecture', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('09. Ouvrir la queue de lecture', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); const player = await assertPlayerVisible(page); // Queue toggle must exist const queueBtn = player.getByRole('button', { name: /^show queue$|^hide queue$/i }).first(); await expect(queueBtn).toBeVisible({ timeout: 5_000 }); // Must start as "Show queue" const initialLabel = await queueBtn.getAttribute('aria-label'); expect(initialLabel).toMatch(/show queue/i); // Click to open — must change to "Hide queue" await queueBtn.click(); await page.waitForTimeout(500); const updatedLabel = await player.getByRole('button', { name: /^hide queue$/i }).first().getAttribute('aria-label'); expect(updatedLabel).toMatch(/hide queue/i); }); test('10. Menu contextuel track — option ajouter a la queue', async ({ page }) => { const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); const trackCard = page.locator('[role="article"]').first(); await expect(trackCard).toBeVisible(); await trackCard.hover(); await page.waitForTimeout(300); // "More options" button must exist on track cards const moreBtn = trackCard.getByRole('button', { name: /plus d'options/i }).first(); await expect(moreBtn).toBeVisible({ timeout: 5_000 }); await moreBtn.click({ force: true }); // Context menu must appear with queue-related option const menuItem = page.getByRole('menuitem', { name: /queue|file d'attente|ajouter/i }); await expect(menuItem).toBeVisible({ timeout: 3_000 }); }); }); test.describe('PLAYER — Controles avances @critical', () => { test.setTimeout(60_000); test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); const hasTracks = await navigateToPageWithTracks(page); test.skip(!hasTracks, 'No tracks in database — seed required'); await playFirstTrack(page); await assertPlayerVisible(page); }); test('Toggle shuffle — le bouton change d\'etat visuel @critical', async ({ page }) => { const shuffleBtn = page.locator('button').filter({ has: page.locator('[aria-label*="elanger" i]') }).first() .or(page.getByRole('button', { name: /melanger|shuffle/i }).first()); await expect(shuffleBtn).toBeVisible({ timeout: 5_000 }); // Toggle on const initialPressed = await shuffleBtn.getAttribute('aria-pressed'); await shuffleBtn.click(); await page.waitForTimeout(300); const afterClick = await shuffleBtn.getAttribute('aria-pressed'); // Toggle off await shuffleBtn.click(); await page.waitForTimeout(300); const afterSecondClick = await shuffleBtn.getAttribute('aria-pressed'); // Verify toggle behavior if (initialPressed === 'false') { expect(afterClick, 'Shuffle should be on after first click').toBe('true'); expect(afterSecondClick, 'Shuffle should be off after second click').toBe('false'); } }); test('Cycle repeat off -> track -> playlist -> off @critical', async ({ page }) => { let repeatBtn = page.getByRole('button', { name: /repeter|repeat/i }).first(); // If not visible in bar, try expanded player if (!await repeatBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { const trackInfo = page.locator('[aria-label="Track info"]').first(); await expect(trackInfo).toBeVisible(); await trackInfo.click(); await page.waitForTimeout(500); repeatBtn = page.getByRole('button', { name: /repeter|repeat/i }).first(); } await expect(repeatBtn).toBeVisible({ timeout: 5_000 }); // State 1: off const label1 = (await repeatBtn.getAttribute('aria-label') || '').toLowerCase(); expect(label1).toContain('desactiv'); // Click -> track await repeatBtn.click(); await page.waitForTimeout(300); const label2 = (await repeatBtn.getAttribute('aria-label') || '').toLowerCase(); expect(label2).toMatch(/piste|track/); // Click -> playlist await repeatBtn.click(); await page.waitForTimeout(300); const label3 = (await repeatBtn.getAttribute('aria-label') || '').toLowerCase(); expect(label3).toMatch(/playlist/); // Click -> off await repeatBtn.click(); await page.waitForTimeout(300); const label4 = (await repeatBtn.getAttribute('aria-label') || '').toLowerCase(); expect(label4).toContain('desactiv'); }); test('Controle vitesse de lecture — changement visible @critical', async ({ page }) => { // Open expanded player const trackInfo = page.locator('[aria-label="Track info"]').first(); await expect(trackInfo).toBeVisible(); await trackInfo.click(); await page.waitForTimeout(500); const speedBtn = page.locator('[aria-label*="Vitesse de lecture"]').first() .or(page.locator('button:has-text("1x")').first()); await expect(speedBtn).toBeVisible({ timeout: 5_000 }); await expect(speedBtn).toBeEnabled(); await speedBtn.click(); await page.waitForTimeout(300); // Speed option must appear const option15 = page.locator('text="1.5x"').first(); await expect(option15).toBeVisible({ timeout: 2_000 }); await option15.click(); await page.waitForTimeout(300); // Button must now show 1.5x const updatedLabel = await speedBtn.getAttribute('aria-label') || ''; expect(updatedLabel, 'Speed button should show 1.5x after selection').toContain('1.5'); }); test('Clic sur track info ouvre le player en vue etendue @critical', async ({ page }) => { const trackInfo = page.locator('[aria-label="Track info"]').first(); await expect(trackInfo).toBeVisible({ timeout: 5_000 }); await trackInfo.click(); await page.waitForTimeout(500); // Expanded player overlay must appear const expandedPlayer = page.locator('.fixed.inset-0').filter({ hasText: /.+/ }).first() .or(page.locator('[class*="backdrop-blur-3xl"]').first()); await expect(expandedPlayer).toBeVisible({ timeout: 3_000 }); // Must have a close button const closeBtn = expandedPlayer.locator('button').first(); await expect(closeBtn).toBeVisible(); await closeBtn.click(); await page.waitForTimeout(300); }); test('Queue — ouvrir et voir le contenu @critical', async ({ page }) => { const player = await assertPlayerVisible(page); const queueBtn = player.getByTestId('queue-button') .or(player.getByRole('button', { name: /^show queue$/i })); await expect(queueBtn).toBeVisible({ timeout: 5_000 }); await queueBtn.click(); await page.waitForTimeout(500); // Queue panel must be visible with content const queuePanel = page.locator('text=/play queue|file d.attente|your queue/i').first(); await expect(queuePanel).toBeVisible({ timeout: 3_000 }); // Close queue await queueBtn.click(); }); });