- Update E2E test credentials to match actual seed users (user@veza.music, artist@veza.music, admin@veza.music, mod@veza.music) - Fix hardcoded "Suggested Accounts" in SuggestionsWidget with i18n key - Replace hardcoded amelie_dubois references with CONFIG.users.creator - Refactor auth, player, upload E2E tests for reliability - Add tmt test plans and scripts for CI integration - Simplify CI workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
270 lines
11 KiB
TypeScript
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_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);
|
|
});
|
|
});
|
|
});
|