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>
320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { CONFIG, loginViaAPI, navigateTo } from './helpers';
|
|
|
|
// ===========================================================================
|
|
// SEARCH PAGE AUDIT
|
|
// ===========================================================================
|
|
|
|
test.describe('Search Page Audit (/search)', () => {
|
|
// -------------------------------------------------------------------------
|
|
// Chargement & Rendu
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Chargement & Rendu', () => {
|
|
test('01. la page /search se charge sans crash', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
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, '/search');
|
|
await expect(page).toHaveTitle(/Search/i);
|
|
});
|
|
|
|
test('03. etat decouverte affiche les cards de navigation', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
await expect(page.getByRole('heading', { name: /new releases/i })).toBeVisible();
|
|
await expect(page.getByRole('heading', { name: /curated mixes/i })).toBeVisible();
|
|
await expect(page.getByRole('heading', { name: /explore artists/i })).toBeVisible();
|
|
});
|
|
|
|
test('04. recherche avec resultats affiche les onglets', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
// Wait for results to load
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByRole('tab', { name: /tracks/i })).toBeVisible();
|
|
await expect(page.getByRole('tab', { name: /artists/i })).toBeVisible();
|
|
await expect(page.getByRole('tab', { name: /playlists/i })).toBeVisible();
|
|
});
|
|
|
|
test('05. recherche sans resultats affiche empty state', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search?q=zzzznonexistent999');
|
|
|
|
await expect(page.getByRole('heading', { name: /no results found/i })).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Fonctionnalites
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Fonctionnalites', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('06. la recherche fonctionne avec debounce', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await input.fill('luna');
|
|
|
|
// Wait for debounce (500ms) + API response
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test('07. le bouton clear efface la recherche', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
// Wait for results
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Click clear
|
|
await page.getByRole('button', { name: /clear search/i }).click();
|
|
|
|
// Input should be empty
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await expect(input).toHaveValue('');
|
|
|
|
// Discovery cards should reappear
|
|
await expect(page.getByRole('heading', { name: /new releases/i })).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test('08. clic sur un resultat artiste navigue vers le profil', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
// Wait for artist results
|
|
await expect(page.getByRole('tab', { name: /artists/i })).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Click first artist link
|
|
const firstArtistLink = page.getByRole('link', { name: /luna/ }).first();
|
|
await firstArtistLink.click();
|
|
|
|
// Should navigate to profile
|
|
await page.waitForURL(/\/u\//, { timeout: 10_000 });
|
|
expect(page.url()).toContain('/u/');
|
|
});
|
|
|
|
test('09. onglets filtrent les resultats', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Click Artists tab
|
|
await page.getByRole('tab', { name: /artists/i }).click();
|
|
await expect(page.getByRole('tabpanel', { name: /artists/i })).toBeVisible();
|
|
|
|
// Click Playlists tab
|
|
await page.getByRole('tab', { name: /playlists/i }).click();
|
|
await expect(page.getByRole('tabpanel', { name: /playlists/i })).toBeVisible();
|
|
});
|
|
|
|
test('10. URL params ?q= pre-remplissent la recherche', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await expect(input).toHaveValue('luna');
|
|
});
|
|
|
|
test('11. historique de recherche apparait', async ({ page }) => {
|
|
// First search to create history
|
|
await navigateTo(page, '/search');
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await input.fill('testhistory');
|
|
await page.waitForTimeout(1000); // wait for debounce + history save
|
|
|
|
// Clear and reload
|
|
await navigateTo(page, '/search');
|
|
|
|
// History should show
|
|
const historyButton = page.getByRole('button', { name: 'testhistory' });
|
|
// History might or might not appear depending on timing
|
|
if (await historyButton.isVisible().catch(() => false)) {
|
|
await historyButton.click();
|
|
await expect(input).toHaveValue('testhistory');
|
|
}
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Securite
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Securite', () => {
|
|
test('12. /search est protege par authentification', async ({ page }) => {
|
|
await page.goto(`${CONFIG.baseURL}/search`);
|
|
await page.waitForURL(/\/(login|search)/, { timeout: CONFIG.timeouts.navigation });
|
|
const url = page.url();
|
|
if (!url.includes('/search')) {
|
|
expect(url).toContain('/login');
|
|
}
|
|
});
|
|
|
|
test('13. pas de token ou email dans l\'URL de recherche', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search?q=luna');
|
|
const url = page.url();
|
|
expect(url).not.toMatch(/token|access_token|email|password/i);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Accessibilite
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Accessibilite', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('14. search input est un combobox avec aria-controls', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await expect(input).toBeVisible();
|
|
const controls = await input.getAttribute('aria-controls');
|
|
expect(controls).toBeTruthy();
|
|
});
|
|
|
|
test('15. discovery cards sont accessibles au clavier', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const card = page.getByRole('link', { name: /new releases/i });
|
|
await expect(card).toBeVisible();
|
|
await card.focus();
|
|
await expect(card).toBeFocused();
|
|
});
|
|
|
|
test('16. HelpText dit "Help:" et pas "Aide:"', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
const helpSpan = page.locator('[aria-label^="Help:"]');
|
|
await expect(helpSpan).toBeVisible();
|
|
|
|
// Should NOT have French prefix
|
|
const aidePrefixed = page.locator('[aria-label^="Aide:"]');
|
|
await expect(aidePrefixed).toHaveCount(0);
|
|
});
|
|
|
|
test('17. resultat cards ont role="link"', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
|
|
const links = page.getByRole('link', { name: /luna/ });
|
|
const count = await links.count();
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// i18n
|
|
// -------------------------------------------------------------------------
|
|
test.describe('i18n', () => {
|
|
test('18. pas de cles i18n brutes', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/\bsearch\.\w+/);
|
|
expect(body).not.toMatch(/\bcommon\.\w+/);
|
|
});
|
|
|
|
test('19. pas de "Trending" ou "Top Artists" (ORIGIN compliance)', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/Trending creators/i);
|
|
// "Top Artists" should be replaced by "Explore Artists"
|
|
expect(body).not.toMatch(/\bTop Artists\b/);
|
|
});
|
|
|
|
test('20. pas de melange FR/EN', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/\bRecherche\b/);
|
|
expect(body).not.toMatch(/\bEffacer\b/);
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Responsive
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Responsive', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('21. mobile 375px: page se charge correctement', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await navigateTo(page, '/search');
|
|
|
|
await expect(page.getByRole('combobox', { name: /search/i })).toBeVisible();
|
|
await expect(page.getByRole('heading', { name: /search/i })).toBeVisible();
|
|
});
|
|
|
|
test('22. tablet 768px: resultats affichent correctement', async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Regression
|
|
// -------------------------------------------------------------------------
|
|
test.describe('Regression', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('23. BUG#1: recherche "luna" ne crash plus', async ({ page }) => {
|
|
await navigateTo(page, '/search?q=luna');
|
|
|
|
// Should NOT show error boundary
|
|
await expect(page.getByText(/Failed to load/i)).not.toBeVisible({ timeout: 5_000 });
|
|
await expect(page.getByText(/Cannot read properties/i)).not.toBeVisible();
|
|
|
|
// Should show results
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test('24. BUG#2: clear search fonctionne correctement', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const input = page.getByRole('combobox', { name: /search/i });
|
|
await input.fill('luna');
|
|
|
|
// Wait for results to appear
|
|
await expect(page.getByRole('tab', { name: /all results/i })).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Click clear
|
|
await page.getByRole('button', { name: /clear search/i }).click();
|
|
|
|
// Input should be empty and discovery should return
|
|
await expect(input).toHaveValue('');
|
|
await expect(page.getByRole('heading', { name: /new releases/i })).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
|
|
test('25. BUG#5: pas de contenu "trending" dans discovery', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
// Should say "Explore Artists" not "Top Artists"
|
|
await expect(page.getByRole('heading', { name: /explore artists/i })).toBeVisible();
|
|
// "Trending creators" text should not appear
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/Trending creators/i);
|
|
});
|
|
|
|
test('26. BUG#9: HelpText prefix is "Help:" not "Aide:"', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
const helpElement = page.locator('[aria-label^="Help:"]');
|
|
await expect(helpElement).toBeVisible();
|
|
});
|
|
});
|
|
});
|