veza/tests/e2e/38-user-profile.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

270 lines
11 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { CONFIG, loginViaAPI } from './helpers';
const BASE = CONFIG.baseURL;
const VALID_USERNAME = CONFIG.users.admin.username; // admin_veza
const LISTENER_USERNAME = CONFIG.users.listener.username; // music_lover
test.describe('Profil public utilisateur (/u/:username)', () => {
test.describe('Chargement & Rendu', () => {
test('la page se charge sans erreur pour un visiteur non connecté', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Should NOT redirect to /login (BUG #1 regression)
expect(page.url()).toContain(`/u/${VALID_USERNAME}`);
// Profile name should be visible
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
test('la page se charge pour un utilisateur connecté', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
expect(page.url()).toContain(`/u/${VALID_USERNAME}`);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
test('le titre de page contient le nom du profil', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #9 regression: title should include username
await expect(page).toHaveTitle(new RegExp(VALID_USERNAME));
});
test('le skeleton de chargement est affiché', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'commit' });
// The skeleton should appear with role="status" and aria-label
const skeleton = page.locator('[role="status"]');
// It may or may not still be visible depending on load speed
// Just verify it was rendered at some point (not a hard requirement)
});
});
test.describe('Fonctionnalités', () => {
test('affiche les infos publiques (avatar, bio, stats)', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Username heading
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
// @ handle
await expect(page.getByText(`@`)).toBeVisible();
// Member since date
await expect(page.getByText(/Joined|Membre depuis/)).toBeVisible();
// Stats section - should have tracks, playlists, followers, following
await expect(page.getByText(/Tracks|Morceaux|Pistas/)).toBeVisible();
await expect(page.getByText(/Followers|Abonnés|Seguidores/)).toBeVisible();
});
test('les onglets affichent du contenu (Tracks, Playlists, Reposts, Feed)', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #2 regression: tab content should render
// Tracks tab (default) should show tabpanel
const tracksPanel = page.getByRole('tabpanel');
await expect(tracksPanel).toBeVisible();
// Click Playlists tab
await page.getByRole('tab', { name: /Playlists/ }).click();
await expect(page.getByRole('tabpanel')).toBeVisible();
// Click Reposts tab
await page.getByRole('tab', { name: /Reposts/ }).click();
await expect(page.getByRole('tabpanel')).toBeVisible();
// Click Feed tab
await page.getByRole('tab', { name: /Feed|Fil/ }).click();
await expect(page.getByRole('tabpanel')).toBeVisible();
});
test('page 404 avec username inexistant', async ({ page }) => {
await page.goto(`${BASE}/u/nonexistent_user_xyz_test`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #3 regression: should show "User Not Found" not "Something went wrong"
await expect(page.getByRole('heading', { level: 2 })).toContainText(/Not Found|introuvable|no encontrado/);
// Should NOT have "Try again" button for 404
await expect(page.getByRole('button', { name: /Try again|Réessayer|Reintentar/ })).not.toBeVisible();
// Should have "Return to Base" link
await expect(page.getByRole('link', { name: /Return to Base|Retour|Volver/ })).toBeVisible();
});
test('bouton follow visible pour un autre utilisateur connecté', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Follow button should be visible (viewing another user's profile)
await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).toBeVisible();
});
test('bouton follow masqué sur son propre profil', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.goto(`${BASE}/u/${LISTENER_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Follow button should NOT be visible on own profile
await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).not.toBeVisible();
});
test('bouton follow masqué pour visiteur non connecté', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Follow button should NOT be visible when not logged in
await expect(page.getByRole('button', { name: /Follow|Suivre|Seguir/ })).not.toBeVisible();
});
});
test.describe('Sécurité', () => {
test('XSS dans le paramètre username est neutralisé', async ({ page }) => {
await page.goto(`${BASE}/u/%3Cscript%3Ealert(1)%3C%2Fscript%3E`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Should show error page, not execute script
expect(page.url()).not.toContain('login'); // Should not redirect
// No alert dialog should have appeared (React escapes by default)
});
test('pas de fuite de token dans l\'URL ou le DOM', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// URL should not contain tokens
expect(page.url()).not.toMatch(/token|jwt|bearer/i);
// DOM should not expose tokens
const html = await page.content();
expect(html).not.toMatch(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/); // JWT pattern
});
test('pas de fuite d\'email dans le profil public', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
const html = await page.content();
expect(html).not.toContain('admin@veza');
expect(html).not.toContain('@veza.fr');
expect(html).not.toContain('@veza.music');
});
});
test.describe('Accessibilité', () => {
test('le lien Skip to content fonctionne', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #6 regression: #main-content should exist
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeAttached();
// Skip link should point to #main-content
const skipLink = page.getByRole('link', { name: /Skip to content/ });
await expect(skipLink).toHaveAttribute('href', '#main-content');
});
test('les onglets ont les rôles ARIA corrects', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// tablist should exist
await expect(page.getByRole('tablist')).toBeVisible();
// tabs should exist
const tabs = page.getByRole('tab');
await expect(tabs).toHaveCount(4);
// Active tab should have tabpanel
await expect(page.getByRole('tabpanel')).toBeVisible();
});
test('les headings sont structurés correctement (h1, h2)', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// h1 = username/display name
await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1);
// h2 = About section
const h2 = page.getByRole('heading', { level: 2 });
await expect(h2.first()).toBeVisible();
});
});
test.describe('i18n', () => {
test('pas de clés i18n brutes affichées', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
const text = await page.textContent('body');
// Should not contain raw i18n keys
expect(text).not.toMatch(/profilePublic\./);
expect(text).not.toMatch(/common\./);
});
test('pas de mélange de langues EN/FR', async ({ page }) => {
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
const text = await page.textContent('body') ?? '';
// BUG #5 regression: should not mix French and English
// If page is in English, should not have French-only words
if (text.includes('About')) {
expect(text).not.toMatch(/\bSuivre\b/);
expect(text).not.toMatch(/\bAbonné\b/);
expect(text).not.toMatch(/\bDésabonnement\b/);
}
});
});
test.describe('Responsive', () => {
test('mobile 375px - stats ne débordent pas', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #7 regression: all 4 stat labels should be visible
await expect(page.getByText(/Following|Abonnements|Siguiendo/)).toBeVisible();
await expect(page.getByText(/Followers|Abonnés|Seguidores/)).toBeVisible();
});
test('tablet 768px - layout correct', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Page should render without crash
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test.describe('Réseau & API', () => {
test('pas d\'erreur 404 pour /roles', async ({ page }) => {
const errors: string[] = [];
page.on('response', (response) => {
if (response.url().includes('/roles') && response.status() === 404) {
errors.push(response.url());
}
});
await page.goto(`${BASE}/u/${VALID_USERNAME}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// BUG #4 regression: /roles endpoint should not be called
expect(errors).toHaveLength(0);
});
});
});