- Update E2E test credentials to match actual seed users (user@veza.music, artist@veza.music, admin@veza.music, mod@veza.music) - Fix hardcoded "Suggested Accounts" in SuggestionsWidget with i18n key - Replace hardcoded amelie_dubois references with CONFIG.users.creator - Refactor auth, player, upload E2E tests for reliability - Add tmt test plans and scripts for CI integration - Simplify CI workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
361 lines
14 KiB
TypeScript
361 lines
14 KiB
TypeScript
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();
|
|
});
|
|
});
|