veza/tests/e2e/38-user-profile.spec.ts

271 lines
11 KiB
TypeScript
Raw Normal View History

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