veza/tests/e2e/queue-audit.spec.ts

384 lines
16 KiB
TypeScript
Raw Normal View History

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