veza/tests/e2e/39-feed.spec.ts
senke 9a4c0d2af4 feat(web): update all features, stories, e2e tests, and auth interceptor
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>
2026-03-31 19:16:36 +02:00

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