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>
223 lines
8.8 KiB
TypeScript
223 lines
8.8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { CONFIG, loginViaAPI } from './helpers';
|
|
|
|
const BASE = CONFIG.baseURL;
|
|
|
|
test.describe('Fil d\'actualité (/feed)', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test.describe('Chargement & Rendu', () => {
|
|
test('la page se charge sans erreur', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
expect(page.url()).toContain('/feed');
|
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
});
|
|
|
|
test('le titre et sous-titre sont affichés', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toContainText(/Feed/);
|
|
await expect(page.getByText(/Latest tracks|Derniers morceaux|Últimas pistas/)).toBeVisible();
|
|
});
|
|
|
|
test('la grille de tracks est visible', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const grid = page.getByRole('grid');
|
|
await expect(grid).toBeVisible();
|
|
|
|
const articles = page.getByRole('article');
|
|
expect(await articles.count()).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
test.describe('Fonctionnalités', () => {
|
|
test('les cartes de tracks ont un bouton play', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Hover over first track card to reveal play button
|
|
const firstArticle = page.getByRole('article').first();
|
|
await firstArticle.hover();
|
|
|
|
// Play button should be visible with proper aria-label
|
|
const playButton = firstArticle.getByRole('button', { name: /Play|Lire|Reproducir/ });
|
|
await expect(playButton).toBeVisible();
|
|
});
|
|
|
|
test('le widget suggestions est visible sur desktop', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 800 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expect(page.getByRole('heading', { name: /Suggested Accounts|Comptes suggérés|Cuentas sugeridas/ })).toBeVisible();
|
|
});
|
|
|
|
test('le widget suggestions a des boutons follow', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 800 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const followButtons = page.getByRole('button', { name: /Follow|Suivre|Seguir/ });
|
|
expect(await followButtons.count()).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('le lien See all mène vers /social', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 800 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const seeAllLink = page.getByRole('link', { name: /See all|Voir tout|Ver todo/ });
|
|
await expect(seeAllLink).toHaveAttribute('href', '/social');
|
|
});
|
|
});
|
|
|
|
test.describe('Sécurité', () => {
|
|
test('la page nécessite une authentification', async ({ page }) => {
|
|
// Clear auth state
|
|
await page.goto(`${BASE}/`, { waitUntil: 'commit' });
|
|
await page.evaluate(() => localStorage.removeItem('auth-storage'));
|
|
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Should redirect to login
|
|
expect(page.url()).toContain('/login');
|
|
});
|
|
|
|
test('pas de fuite de token dans le DOM', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const html = await page.content();
|
|
expect(html).not.toMatch(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/); // JWT pattern
|
|
});
|
|
});
|
|
|
|
test.describe('Accessibilité', () => {
|
|
test('la grille a un aria-label traduit (pas de FR hardcodé)', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// BUG #3 regression: grid aria-label should be translated
|
|
const grid = page.getByRole('grid');
|
|
const gridLabel = await grid.getAttribute('aria-label');
|
|
expect(gridLabel).toBeTruthy();
|
|
// Should not be the old hardcoded French if UI is English
|
|
// (it will be French if user switched to FR, which is correct)
|
|
});
|
|
|
|
test('les boutons play ont un aria-label traduit (pas de mélange FR/EN)', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// BUG #2 regression: play button aria-labels should not mix FR/EN
|
|
const firstArticle = page.getByRole('article').first();
|
|
await firstArticle.hover();
|
|
|
|
const playButton = firstArticle.getByRole('button', { name: /Play|Lire|Reproducir/ });
|
|
const label = await playButton.getAttribute('aria-label');
|
|
expect(label).toBeTruthy();
|
|
// If EN: should start with "Play" not "Lire"
|
|
// If FR: should start with "Lire" not "Play"
|
|
// Should not contain "pour" if English
|
|
if (label?.includes('Play')) {
|
|
expect(label).not.toMatch(/\bpour\b/);
|
|
}
|
|
});
|
|
|
|
test('les éléments interactifs sont focusables par Tab', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Tab should move focus to interactive elements
|
|
await page.keyboard.press('Tab');
|
|
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
expect(focusedElement).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
test.describe('i18n', () => {
|
|
test('pas de clés i18n brutes affichées', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const text = await page.textContent('body');
|
|
expect(text).not.toMatch(/feed\./);
|
|
expect(text).not.toMatch(/tracks\.grid\./);
|
|
});
|
|
|
|
test('pas de mélange FR/EN dans les aria-labels', async ({ page }) => {
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// BUG #2 regression: check that aria-labels are consistent
|
|
const frLabels = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
return buttons
|
|
.map(b => b.getAttribute('aria-label') || '')
|
|
.filter(l => /\bpour\b/.test(l) && /More options/.test(l));
|
|
});
|
|
// Should have no "More options pour" (mixed FR/EN) labels
|
|
expect(frLabels).toHaveLength(0);
|
|
});
|
|
|
|
test('SuggestionsWidget utilise des traductions', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 800 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// BUG #1 regression: "Suggested Accounts" should come from i18n
|
|
const heading = page.getByRole('heading', { name: /Suggested Accounts|Comptes suggérés|Cuentas sugeridas/ });
|
|
await expect(heading).toBeVisible();
|
|
|
|
// "followers" text should be present and translated
|
|
await expect(page.getByText(/followers|abonnés|seguidores/i).first()).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Responsive', () => {
|
|
test('mobile 375px - la page est fonctionnelle', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Feed heading should be visible
|
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
|
|
// Track grid should be visible
|
|
await expect(page.getByRole('grid')).toBeVisible();
|
|
});
|
|
|
|
test('tablet 768px - la page est fonctionnelle', async ({ page }) => {
|
|
await page.setViewportSize({ width: 768, height: 1024 });
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
await expect(page.getByRole('grid')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Réseau & API', () => {
|
|
test('l\'API /feed retourne 200', async ({ page }) => {
|
|
let feedStatus = 0;
|
|
page.on('response', (response) => {
|
|
if (response.url().includes('/api/v1/feed')) {
|
|
feedStatus = response.status();
|
|
}
|
|
});
|
|
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(3000);
|
|
|
|
expect(feedStatus).toBe(200);
|
|
});
|
|
});
|
|
});
|