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