test: convert fake console.log assertions to real expect()

Replace 105+ fake assertions across 8 E2E test files that used
console.log('✓'/'✗') instead of expect(), causing tests to always
pass even when features were broken. Now 87 tests correctly fail,
exposing real application bugs.

Files converted:
- 09-chat-notifications-settings.spec.ts (33 fakes → real)
- 18-empty-states.spec.ts (14 fakes → real)
- 17-modals-dialogs.spec.ts (15 fakes → real)
- 07-social.spec.ts (12 fakes → real)
- 06-search-discover.spec.ts (12 fakes → real)
- 05-playlists.spec.ts (6 fakes → real)
- 08-marketplace.spec.ts (8 fakes → real)
- 10-features.spec.ts (5 fakes → real)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-04-04 13:23:58 +02:00
parent a3f4ac6b70
commit 5b228c729b
8 changed files with 295 additions and 750 deletions

View file

@ -12,51 +12,39 @@ test.describe('PLAYLISTS — CRUD', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i); expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i);
// PlaylistCards use role="article" with aria-label="Playlist: {title}"
const playlistCards = page.locator('[role="article"][aria-label^="Playlist:"]'); const playlistCards = page.locator('[role="article"][aria-label^="Playlist:"]');
const cardCount = await playlistCards.count(); expect(await playlistCards.count()).toBeGreaterThan(0);
console.log(` Playlist cards trouvés: ${cardCount}`);
// Bouton créer une playlist
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }) const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i })
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i })); .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }));
const visible = await createBtn.first().isVisible().catch(() => false); await expect(createBtn.first()).toBeVisible();
console.log(` Bouton créer playlist: ${visible ? '✓' : '✗'}`);
}); });
test('02. Créer une nouvelle playlist @critical', async ({ page }) => { test('02. Créer une nouvelle playlist @critical', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
// Cliquer sur créer — use .or() without .first() to build the union, then take .first()
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }) const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i })
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i })) .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }))
.first(); .first();
if (!(await createBtn.isVisible().catch(() => false))) { await expect(createBtn).toBeVisible();
console.log(' ⚠ Bouton créer non trouvé');
return;
}
await createBtn.click(); await createBtn.click();
// Wait for dialog/form to appear
await page.waitForTimeout(500); await page.waitForTimeout(500);
// Remplir le formulaire — try label first, then placeholder
const nameInput = page.getByLabel(/nom|name|titre|title/i) const nameInput = page.getByLabel(/nom|name|titre|title/i)
.or(page.getByPlaceholder(/nom|name|titre/i)) .or(page.getByPlaceholder(/nom|name|titre/i))
.first(); .first();
if (await nameInput.isVisible().catch(() => false)) { await expect(nameInput).toBeVisible();
const playlistName = `E2E Playlist ${Date.now()}`; const playlistName = `E2E Playlist ${Date.now()}`;
await nameInput.fill(playlistName); await nameInput.fill(playlistName);
// Description si présent
const descInput = page.getByLabel(/description/i).first(); const descInput = page.getByLabel(/description/i).first();
if (await descInput.isVisible().catch(() => false)) { if (await descInput.isVisible().catch(() => false)) {
await descInput.fill('Créée par les tests E2E'); await descInput.fill('Créée par les tests E2E');
} }
// Sauvegarder — try dialog-scoped first, then modal, then last visible matching button
const dialog = page.locator('[role="dialog"], [role="alertdialog"], dialog, [data-state="open"]').first(); const dialog = page.locator('[role="dialog"], [role="alertdialog"], dialog, [data-state="open"]').first();
const dialogVisible = await dialog.isVisible().catch(() => false); const dialogVisible = await dialog.isVisible().catch(() => false);
@ -70,34 +58,27 @@ test.describe('PLAYLISTS — CRUD', () => {
} }
if (!saved) { if (!saved) {
// Fallback: pick the last matching button (typically the submit one, not the page trigger)
const allSaveBtns = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok/i }); const allSaveBtns = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok/i });
const count = await allSaveBtns.count(); const count = await allSaveBtns.count();
if (count > 0) { if (count > 0) {
await allSaveBtns.nth(count - 1).click(); await allSaveBtns.nth(count - 1).click();
saved = true;
} }
} }
await page.waitForTimeout(2_000); await page.waitForTimeout(2_000);
// Vérifier que la playlist est créée — look for a PlaylistCard with the new title
const newCard = page.locator(`[role="article"][aria-label="Playlist: ${playlistName}"]`); const newCard = page.locator(`[role="article"][aria-label="Playlist: ${playlistName}"]`);
const exists = await newCard.isVisible().catch(() => const exists = await newCard.isVisible().catch(() =>
page.getByText(playlistName).isVisible().catch(() => false) page.getByText(playlistName).isVisible().catch(() => false)
); );
console.log(` Playlist créée et visible: ${exists ? '✓' : '✗'}`); expect(exists).toBeTruthy();
}
}); });
test('03. Ouvrir une playlist existante affiche ses tracks', async ({ page }) => { test('03. Ouvrir une playlist existante affiche ses tracks', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
// PlaylistCard wraps a Link with href="/playlists/{id}"
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
@ -113,32 +94,23 @@ test.describe('PLAYLISTS — CRUD', () => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Bouton éditer
const editBtn = page.getByRole('button', { name: /edit|modifier|éditer/i }).first() const editBtn = page.getByRole('button', { name: /edit|modifier|éditer/i }).first()
.or(page.locator('[data-action="edit"]').first()); .or(page.locator('[data-action="edit"]').first());
if (await editBtn.isVisible().catch(() => false)) { await expect(editBtn).toBeVisible();
await editBtn.click(); await editBtn.click();
console.log(' ✓ Mode édition activé');
} else {
console.log(' ⚠ Bouton éditer non trouvé');
}
}); });
test('05. Supprimer une playlist', async ({ page }) => { test('05. Supprimer une playlist', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
@ -146,8 +118,7 @@ test.describe('PLAYLISTS — CRUD', () => {
const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first() const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first()
.or(page.locator('[data-action="delete"]').first()); .or(page.locator('[data-action="delete"]').first());
const visible = await deleteBtn.isVisible().catch(() => false); await expect(deleteBtn).toBeVisible();
console.log(` Bouton supprimer: ${visible ? '✓ visible' : '✗ absent'}`);
}); });
}); });
@ -159,43 +130,33 @@ test.describe('PLAYLISTS — Collaboration', () => {
test('06. Option d\'invitation de collaborateurs', async ({ page }) => { test('06. Option d\'invitation de collaborateurs', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
// PlaylistCard uses role="article" with aria-label="Playlist: {title}" and Link href="/playlists/{id}"
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Chercher option de collaboration / partage
const collabBtn = page.getByRole('button', { name: /collabor|inviter|invite|partager|share/i }).first(); const collabBtn = page.getByRole('button', { name: /collabor|inviter|invite|partager|share/i }).first();
const visible = await collabBtn.isVisible().catch(() => false); await expect(collabBtn).toBeVisible();
console.log(` Bouton collaboration/partage: ${visible ? '✓' : '✗'}`);
}); });
test('07. Export playlist (JSON/CSV/M3U)', async ({ page }) => { test('07. Export playlist (JSON/CSV/M3U)', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Menu d'options
const moreBtn = page.getByRole('button', { name: /more|options|⋯|…|menu/i }).first() const moreBtn = page.getByRole('button', { name: /more|options|⋯|…|menu/i }).first()
.or(page.locator('[class*="more-button"], [class*="kebab"]').first()); .or(page.locator('[class*="more-button"], [class*="kebab"]').first());
if (await moreBtn.isVisible().catch(() => false)) { await expect(moreBtn).toBeVisible();
await moreBtn.click(); await moreBtn.click();
const exportOption = page.getByRole('menuitem', { name: /export|télécharger|download/i }); const exportOption = page.getByRole('menuitem', { name: /export|télécharger|download/i });
const visible = await exportOption.isVisible().catch(() => false); await expect(exportOption).toBeVisible();
console.log(` Option export: ${visible ? '✓' : '✗'}`);
}
}); });
}); });
@ -205,16 +166,12 @@ test.describe('PLAYLISTS — Drag & Drop', () => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { await expect(playlistLink).toBeVisible();
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Vérifier la présence de handles de drag
const dragHandles = page.locator('[class*="drag"], [data-testid="drag-handle"], [class*="grip"]'); const dragHandles = page.locator('[class*="drag"], [data-testid="drag-handle"], [class*="grip"]');
const count = await dragHandles.count(); expect(await dragHandles.count()).toBeGreaterThan(0);
console.log(` Drag handles trouvés: ${count}`);
}); });
}); });

View file

@ -3,7 +3,6 @@ import { loginViaAPI, CONFIG, navigateTo, SELECTORS } from './helpers';
/** /**
* Helper to find the search input on /search page with multiple fallbacks. * Helper to find the search input on /search page with multiple fallbacks.
* Tries combobox, placeholder, role="search" input, and generic text input.
*/ */
async function findSearchInput(page: import('@playwright/test').Page) { async function findSearchInput(page: import('@playwright/test').Page) {
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]') const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
@ -22,38 +21,27 @@ test.describe('SEARCH — Recherche unifiée', () => {
test('01. Le champ de recherche est accessible dans le header @critical', async ({ page }) => { test('01. Le champ de recherche est accessible dans le header @critical', async ({ page }) => {
await navigateTo(page, '/dashboard'); await navigateTo(page, '/dashboard');
// The header search input has data-testid="search-input" type="search" inside role="search"
// It is hidden on mobile viewports (hidden md:block), so check softly
const headerSearch = page.locator('[data-testid="search-input"]') const headerSearch = page.locator('[data-testid="search-input"]')
.or(page.locator(SELECTORS.searchInput)); .or(page.locator(SELECTORS.searchInput));
const headerVisible = await headerSearch.first().isVisible().catch(() => false); const headerVisible = await headerSearch.first().isVisible().catch(() => false);
console.log(` Header search input: ${headerVisible ? '✓' : '✗ (may be hidden on mobile viewport)'}`);
// The search page has its own dedicated search input with multiple possible selectors
await navigateTo(page, '/search'); await navigateTo(page, '/search');
const pageSearch = await findSearchInput(page); const pageSearch = await findSearchInput(page);
const pageSearchVisible = await pageSearch.isVisible().catch(() => false); const pageSearchVisible = await pageSearch.isVisible().catch(() => false);
console.log(` Search page input: ${pageSearchVisible ? '✓' : '✗'}`);
// At least one of the two search inputs should be accessible // At least one of the two search inputs should be accessible
expect(headerVisible || pageSearchVisible).toBeTruthy(); expect(headerVisible || pageSearchVisible).toBeTruthy();
}); });
test('02. Taper une requête affiche des résultats @critical', async ({ page }) => { test('02. Taper une requête affiche des résultats @critical', async ({ page }) => {
// Navigate to /search — the SearchPage has its own input (SearchPageHeader.tsx)
// The useSearchPage hook reads ?q= from URL params and debounces at 500ms
await navigateTo(page, '/search'); await navigateTo(page, '/search');
const searchInput = await findSearchInput(page); const searchInput = await findSearchInput(page);
if (!(await searchInput.isVisible().catch(() => false))) { await expect(searchInput).toBeVisible();
return;
}
await searchInput.fill('test'); await searchInput.fill('test');
// useSearchPage debounces at 500ms, wait for results
await page.waitForTimeout(1_500); await page.waitForTimeout(1_500);
// Results should appear (SearchPageResults with tabs) or empty state (SearchPageEmpty)
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const hasResults = body.length > 500; const hasResults = body.length > 500;
const hasNoResults = /no results|aucun résultat|nothing found/i.test(body); const hasNoResults = /no results|aucun résultat|nothing found/i.test(body);
@ -65,50 +53,36 @@ test.describe('SEARCH — Recherche unifiée', () => {
await navigateTo(page, '/search'); await navigateTo(page, '/search');
const searchInput = await findSearchInput(page); const searchInput = await findSearchInput(page);
if (!(await searchInput.isVisible().catch(() => false))) { await expect(searchInput).toBeVisible();
return;
}
await searchInput.fill('tes'); await searchInput.fill('tes');
// SearchPageHeader debounces suggestions at 300ms
await page.waitForTimeout(1_000); await page.waitForTimeout(1_000);
// Dropdown suggestions use role="listbox" (SearchPageHeader.tsx)
const suggestions = page.locator('[role="listbox"]'); const suggestions = page.locator('[role="listbox"]');
const visible = await suggestions.isVisible().catch(() => false); await expect(suggestions).toBeVisible();
console.log(` Autocomplete: ${visible ? '✓ dropdown visible' : '✗ pas de suggestions'}`);
}); });
test('04. Les résultats de recherche sont catégorisés (tabs: All, Tracks, Artists, Playlists)', async ({ page }) => { test('04. Les résultats de recherche sont catégorisés (tabs: All, Tracks, Artists, Playlists)', async ({ page }) => {
await navigateTo(page, '/search'); await navigateTo(page, '/search');
const searchInput = await findSearchInput(page); const searchInput = await findSearchInput(page);
if (!(await searchInput.isVisible().catch(() => false))) { await expect(searchInput).toBeVisible();
return;
}
await searchInput.fill('music'); await searchInput.fill('music');
// Wait for debounce (500ms) + network
await page.waitForTimeout(2_000); await page.waitForTimeout(2_000);
// SearchPageResults uses Radix Tabs with TabsTrigger elements
// Tab values: "all", "tracks", "artists", "playlists" (SearchPageResults.tsx)
const expectedTabs = ['All Results', 'Tracks', 'Artists', 'Playlists']; const expectedTabs = ['All Results', 'Tracks', 'Artists', 'Playlists'];
for (const tabName of expectedTabs) { for (const tabName of expectedTabs) {
const tab = page.getByRole('tab', { name: new RegExp(tabName, 'i') }); const tab = page.getByRole('tab', { name: new RegExp(tabName, 'i') });
const visible = await tab.isVisible().catch(() => false); await expect(tab).toBeVisible();
if (visible) console.log(` Tab "${tabName}": ✓`);
} }
}); });
test('05. Recherche vide ne crash pas', async ({ page }) => { test('05. Recherche vide ne crash pas', async ({ page }) => {
await navigateTo(page, '/search'); await navigateTo(page, '/search');
// With empty query, useSearchPage shows SearchPageDiscovery (trending tags, etc.)
const searchInput = await findSearchInput(page); const searchInput = await findSearchInput(page);
if (!(await searchInput.isVisible().catch(() => false))) { await expect(searchInput).toBeVisible();
return;
}
await searchInput.fill(''); await searchInput.fill('');
await page.waitForTimeout(1_000); await page.waitForTimeout(1_000);
@ -118,14 +92,10 @@ test.describe('SEARCH — Recherche unifiée', () => {
}); });
test('05b. Recherche via URL params ?q= fonctionne', async ({ page }) => { test('05b. Recherche via URL params ?q= fonctionne', async ({ page }) => {
// useSearchPage reads query from ?q= URL param
await navigateTo(page, '/search?q=test'); await navigateTo(page, '/search?q=test');
// Wait for debounce + search
await page.waitForTimeout(1_500); await page.waitForTimeout(1_500);
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Should show results or empty state, not crash
expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i); expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i);
expect(body.length).toBeGreaterThan(50); expect(body.length).toBeGreaterThan(50);
}); });
@ -139,130 +109,83 @@ test.describe('DISCOVER — Exploration éthique', () => {
test('06. Page /discover affiche les genres @critical', async ({ page }) => { test('06. Page /discover affiche les genres @critical', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// DiscoverPage shows a heading "Découvrir" or "Discover"
const heading = page.getByRole('heading', { name: /découvrir|discover/i }); const heading = page.getByRole('heading', { name: /découvrir|discover/i });
const hasMainHeading = await heading.first().isVisible().catch(() => false); await expect(heading.first()).toBeVisible();
console.log(` Discover heading: ${hasMainHeading ? '✓' : '✗'}`);
// Genre section heading — may be "Par genre", "By genre", or similar
const genreHeading = page.getByRole('heading', { name: /par genre|by genre|genres/i });
const hasGenreSection = await genreHeading.first().isVisible().catch(() => false);
console.log(` Section "Par genre": ${hasGenreSection ? '✓' : '✗'}`);
// Genre cards are buttons with gradient backgrounds in a grid
// Each button contains a span with the genre name — try multiple selectors
const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') }); const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') });
let genreCount = await genreButtons.count(); let genreCount = await genreButtons.count();
// Fallback: look for any genre-like buttons (with gradient bg or genre text)
if (genreCount === 0) { if (genreCount === 0) {
const altGenreButtons = page.locator('button').filter({ hasText: /rock|pop|jazz|hip.?hop|electro|classical|r&b|reggae|metal|folk|blues|soul|country|latin/i }); const altGenreButtons = page.locator('button').filter({ hasText: /rock|pop|jazz|hip.?hop|electro|classical|r&b|reggae|metal|folk|blues|soul|country|latin/i });
genreCount = await altGenreButtons.count(); genreCount = await altGenreButtons.count();
} }
console.log(` Genre cards: ${genreCount}`); expect(genreCount).toBeGreaterThan(0);
// Page loaded without crash — at minimum the page should have content
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i); expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError|Unhandled/i);
// Soft check: page loaded successfully (genres may not be seeded)
const pageLoaded = hasMainHeading || hasGenreSection || genreCount > 0 || body.length > 200;
console.log(` Page loaded: ${pageLoaded ? '✓' : '✗'}`);
if (!hasGenreSection && genreCount === 0) {
console.log(' ⚠ No genre section found — page may not have genre data seeded');
}
// Only assert page didn't crash, don't require genres to exist
expect(body.length).toBeGreaterThan(100);
}); });
test('07. Cliquer sur un genre filtre les résultats', async ({ page }) => { test('07. Cliquer sur un genre filtre les résultats', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// Genre cards are buttons inside the "Par genre" section grid
// They use handleGenreClick which sets ?genre={slug} in URL params
const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') }); const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') });
await expect(genreButtons.first()).toBeVisible();
if (await genreButtons.first().isVisible().catch(() => false)) {
const genreName = await genreButtons.first().locator('.font-heading.font-bold').textContent();
await genreButtons.first().click(); await genreButtons.first().click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// URL should now contain ?genre=
expect(page.url()).toContain('genre='); expect(page.url()).toContain('genre=');
// A "Retour" (back) button should appear
const backBtn = page.getByRole('button', { name: /retour/i }); const backBtn = page.getByRole('button', { name: /retour/i });
const hasBack = await backBtn.isVisible().catch(() => false); await expect(backBtn).toBeVisible();
console.log(` Genre "${genreName}" sélectionné, bouton retour: ${hasBack ? '✓' : '✗'}`);
// Content should be present (tracks grid or empty message)
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(200); expect(body.length).toBeGreaterThan(200);
}
}); });
test('08. Playlists éditoriales affichées sur /discover', async ({ page }) => { test('08. Playlists éditoriales affichées sur /discover', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// DiscoverPage shows editorial playlists section with heading "Playlists éditoriales"
const editorialHeading = page.getByRole('heading', { name: /playlists éditoriales/i }); const editorialHeading = page.getByRole('heading', { name: /playlists éditoriales/i });
const visible = await editorialHeading.isVisible().catch(() => false); await expect(editorialHeading).toBeVisible();
console.log(` Section playlists éditoriales: ${visible ? '✓' : '✗'}`);
if (visible) {
// Editorial playlists use PlaylistCard components with role="article" aria-label="Playlist: ..."
const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]'); const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]');
const count = await editorialCards.count(); expect(await editorialCards.count()).toBeGreaterThanOrEqual(0);
console.log(` Playlists éditoriales trouvées: ${count}`);
}
}); });
test('09. Pas de sections "trending" ou "for you" (design éthique)', async ({ page }) => { test('09. Pas de sections "trending" ou "for you" (design éthique)', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// Verify no algorithmic/trending/recommendation sections exist
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/pour vous|for you|recommended|recommandé|trending/i); expect(body).not.toMatch(/pour vous|for you|recommended|recommandé|trending/i);
console.log(' ✓ Aucune section algorithmique trouvée');
}); });
test('10. Pas de métriques de popularité publiques visibles', async ({ page }) => { test('10. Pas de métriques de popularité publiques visibles', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// Public play/like counters must NOT be visible (ORIGIN_UI_UX_SYSTEM §13)
const publicCounters = page.locator('[class*="play-count"], [class*="like-count"]') const publicCounters = page.locator('[class*="play-count"], [class*="like-count"]')
.filter({ hasText: /\d+\s*(plays?|écoutes?|likes?|vues?)/i }); .filter({ hasText: /\d+\s*(plays?|écoutes?|likes?|vues?)/i });
const count = await publicCounters.count(); expect(await publicCounters.count()).toBe(0);
if (count > 0) {
console.warn(`${count} compteur(s) de popularité publique(s) trouvé(s) — contraire aux principes Veza !`);
} else {
console.log(' ✓ Aucun compteur de popularité public');
}
}); });
test('11. Bouton retour depuis genre revient à la liste des genres', async ({ page }) => { test('11. Bouton retour depuis genre revient à la liste des genres', async ({ page }) => {
await navigateTo(page, '/discover'); await navigateTo(page, '/discover');
// Click a genre to navigate into it
const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') }); const genreButtons = page.locator('button').filter({ has: page.locator('.font-heading.font-bold') });
if (!(await genreButtons.first().isVisible().catch(() => false))) return; await expect(genreButtons.first()).toBeVisible();
await genreButtons.first().click(); await genreButtons.first().click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Click the "Retour" button (goBack clears searchParams)
const backBtn = page.getByRole('button', { name: /retour/i }); const backBtn = page.getByRole('button', { name: /retour/i });
if (await backBtn.isVisible().catch(() => false)) { await expect(backBtn).toBeVisible();
await backBtn.click(); await backBtn.click();
await page.waitForTimeout(500); await page.waitForTimeout(500);
// Should be back on genre list — URL should not contain ?genre=
expect(page.url()).not.toContain('genre='); expect(page.url()).not.toContain('genre=');
// Genre section should be visible again
const genreHeading = page.getByRole('heading', { name: /par genre/i }); const genreHeading = page.getByRole('heading', { name: /par genre/i });
const visible = await genreHeading.isVisible().catch(() => false); await expect(genreHeading).toBeVisible();
console.log(` Retour à la liste genres: ${visible ? '✓' : '✗'}`);
}
}); });
}); });

View file

@ -7,34 +7,26 @@ test.describe('SOCIAL — Follow/Unfollow', () => {
}); });
test('01. Bouton follow visible sur un profil artiste @critical', async ({ page }) => { test('01. Bouton follow visible sur un profil artiste @critical', async ({ page }) => {
// Navigate directly to a known artist profile (seed user top_artist)
await navigateTo(page, `/u/${CONFIG.users.creator.username}`); await navigateTo(page, `/u/${CONFIG.users.creator.username}`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// FollowButton renders "Suivre" (unfollowed) or "Abonne" (followed)
const followBtn = page.getByRole('button', { name: /suivre|abonné|abonnement/i }).first(); const followBtn = page.getByRole('button', { name: /suivre|abonné|abonnement/i }).first();
const visible = await followBtn.isVisible().catch(() => false); await expect(followBtn).toBeVisible();
console.log(` Bouton follow: ${visible ? '✓' : '✗'}`);
}); });
test('02. Follow toggle fonctionne', async ({ page }) => { test('02. Follow toggle fonctionne', async ({ page }) => {
// Navigate directly to a known artist profile
await navigateTo(page, '/u/marcus_beats'); await navigateTo(page, '/u/marcus_beats');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// FollowButton text: "Suivre" (not following) or "Abonné" (following)
const followBtn = page.getByRole('button', { name: /suivre|abonné|abonnement|désabonnement/i }).first(); const followBtn = page.getByRole('button', { name: /suivre|abonné|abonnement|désabonnement/i }).first();
await expect(followBtn).toBeVisible();
if (await followBtn.isVisible().catch(() => false)) {
const initialText = await followBtn.textContent(); const initialText = await followBtn.textContent();
await followBtn.click(); await followBtn.click();
await page.waitForTimeout(1_500); await page.waitForTimeout(1_500);
const newText = await followBtn.textContent(); const newText = await followBtn.textContent();
console.log(` Follow toggle: "${initialText?.trim()}" → "${newText?.trim()}" ${initialText !== newText ? '✓' : '✗'}`); expect(newText?.trim()).not.toBe(initialText?.trim());
} else {
console.log(' ⚠ Bouton follow non visible');
}
}); });
}); });
@ -48,15 +40,10 @@ test.describe('SOCIAL — Profils', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body).toContain(CONFIG.users.listener.username);
// The username should appear on the profile page
const hasUsername = body.includes(CONFIG.users.listener.username);
console.log(` Username affiché: ${hasUsername ? '✓' : '✗'}`);
// Avatar visible (UserProfilePageHeader uses Avatar component)
const avatar = page.locator('[class*="avatar"], img[alt*="avatar"], img[alt*="profil"]').first(); const avatar = page.locator('[class*="avatar"], img[alt*="avatar"], img[alt*="profil"]').first();
const avatarVisible = await avatar.isVisible().catch(() => false); await expect(avatar).toBeVisible();
console.log(` Avatar: ${avatarVisible ? '✓' : '✗'}`);
}); });
test('04. Éditer mon profil (bio, display name)', async ({ page }) => { test('04. Éditer mon profil (bio, display name)', async ({ page }) => {
@ -66,19 +53,14 @@ test.describe('SOCIAL — Profils', () => {
.or(page.locator('textarea[name*="bio"]').first()); .or(page.locator('textarea[name*="bio"]').first());
const nameField = page.getByLabel(/nom.*affichage|display.*name|nom/i).first(); const nameField = page.getByLabel(/nom.*affichage|display.*name|nom/i).first();
const hasBio = await bioField.isVisible().catch(() => false); await expect(bioField).toBeVisible();
const hasName = await nameField.isVisible().catch(() => false); await expect(nameField).toBeVisible();
console.log(` Champ bio: ${hasBio ? '✓' : '✗'}`);
console.log(` Champ display name: ${hasName ? '✓' : '✗'}`);
}); });
test('05. L\'historique d\'écoute est privé (pas visible par d\'autres)', async ({ page }) => { test('05. L\'historique d\'écoute est privé (pas visible par d\'autres)', async ({ page }) => {
// Navigate to another user's public profile at /u/:username
await navigateTo(page, `/u/${CONFIG.users.creator.username}`); await navigateTo(page, `/u/${CONFIG.users.creator.username}`);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Listening history must NOT be visible on someone else's public profile
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/historique.*écoute|listening.*history|recently.*played/i); expect(body).not.toMatch(/historique.*écoute|listening.*history|recently.*played/i);
}); });
@ -88,17 +70,9 @@ test.describe('SOCIAL — Profils', () => {
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).toContain('Tracks');
// UserProfilePageHeader displays stats: Tracks, Playlists, Followers, Following expect(body).toContain('Followers');
const hasTracksLabel = body.includes('Tracks'); expect(body).toContain(CONFIG.users.creator.username);
const hasFollowersLabel = body.includes('Followers');
console.log(` Stats Tracks: ${hasTracksLabel ? '✓' : '✗'}`);
console.log(` Stats Followers: ${hasFollowersLabel ? '✓' : '✗'}`);
// Username should be visible (displayed as @username)
const hasUsername = body.includes(CONFIG.users.creator.username);
console.log(` Username visible: ${hasUsername ? '✓' : '✗'}`);
}); });
}); });
@ -113,25 +87,14 @@ test.describe('SOCIAL — Social Hub', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(100); expect(body.length).toBeGreaterThan(100);
console.log(' Page /social chargée avec succès');
}); });
test('08. Social sidebar tabs (Fresh Tracks, Explore, Communities)', async ({ page }) => { test('08. Social sidebar tabs (Fresh Tracks, Explore, Communities)', async ({ page }) => {
await navigateTo(page, '/social'); await navigateTo(page, '/social');
// SocialViewSidebar has buttons: "Fresh Tracks", "Explore", "Communities" await expect(page.getByRole('button', { name: /fresh tracks/i })).toBeVisible();
const freshTracksBtn = page.getByRole('button', { name: /fresh tracks/i }); await expect(page.getByRole('button', { name: /explore/i })).toBeVisible();
const exploreBtn = page.getByRole('button', { name: /explore/i }); await expect(page.getByRole('button', { name: /communities/i })).toBeVisible();
const communitiesBtn = page.getByRole('button', { name: /communities/i });
const hasFreshTracks = await freshTracksBtn.isVisible().catch(() => false);
const hasExplore = await exploreBtn.isVisible().catch(() => false);
const hasCommunities = await communitiesBtn.isVisible().catch(() => false);
console.log(` Tab Fresh Tracks: ${hasFreshTracks ? '✓' : '✗'}`);
console.log(` Tab Explore: ${hasExplore ? '✓' : '✗'}`);
console.log(` Tab Communities: ${hasCommunities ? '✓' : '✗'}`);
}); });
test('09. Page feed se charge', async ({ page }) => { test('09. Page feed se charge', async ({ page }) => {
@ -140,7 +103,5 @@ test.describe('SOCIAL — Social Hub', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(100); expect(body.length).toBeGreaterThan(100);
console.log(' Page /feed chargée avec succès');
}); });
}); });

View file

@ -13,86 +13,59 @@ test.describe('MARKETPLACE — Navigation', () => {
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(50); expect(body.length).toBeGreaterThan(50);
// MarketplacePage renders heading "Marketplace"
const heading = page.locator('h1').filter({ hasText: /marketplace/i }); const heading = page.locator('h1').filter({ hasText: /marketplace/i });
const hasHeading = await heading.isVisible().catch(() => false); await expect(heading).toBeVisible();
console.log(` Heading Marketplace: ${hasHeading ? '✓' : '✗'}`);
}); });
test('02. Les produits (beats/samples) s\'affichent', async ({ page }) => { test('02. Les produits (beats/samples) s\'affichent', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
// ProductCard wraps in <article aria-label="Product: ...">
const products = page.locator('article[aria-label^="Product:"]'); const products = page.locator('article[aria-label^="Product:"]');
const count = await products.count(); expect(await products.count()).toBeGreaterThan(0);
console.log(` Produits affichés: ${count}`);
}); });
test('03. Filtres marketplace fonctionnent', async ({ page }) => { test('03. Filtres marketplace fonctionnent', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
// Search input in the filters bar
const searchInput = page.getByPlaceholder(/search tracks|search/i).first(); const searchInput = page.getByPlaceholder(/search tracks|search/i).first();
const hasSearch = await searchInput.isVisible().catch(() => false); await expect(searchInput).toBeVisible();
console.log(` Champ recherche: ${hasSearch ? '✓' : '✗'}`);
// Filters button
const filtersBtn = page.getByRole('button', { name: /filters/i }).first(); const filtersBtn = page.getByRole('button', { name: /filters/i }).first();
const hasFilters = await filtersBtn.isVisible().catch(() => false); await expect(filtersBtn).toBeVisible();
console.log(` Bouton Filters: ${hasFilters ? '✓' : '✗'}`);
// Cart button
const cartBtn = page.getByRole('button', { name: /cart/i }).first(); const cartBtn = page.getByRole('button', { name: /cart/i }).first();
const hasCart = await cartBtn.isVisible().catch(() => false); await expect(cartBtn).toBeVisible();
console.log(` Bouton Cart: ${hasCart ? '✓' : '✗'}`);
}); });
test('04. Page détail d\'un produit se charge', async ({ page }) => { test('04. Page détail d\'un produit se charge', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
// ProductCard has "Buy Now" button — check if products exist first
const products = page.locator('article[aria-label^="Product:"]'); const products = page.locator('article[aria-label^="Product:"]');
const count = await products.count(); const count = await products.count();
test.skip(count === 0, 'No products available in marketplace');
if (count === 0) {
console.log(' ⚠ Aucun produit disponible');
return;
}
// Look for a link to product detail page
const productLink = page.locator('a[href*="/marketplace/products/"]').first(); const productLink = page.locator('a[href*="/marketplace/products/"]').first();
if (await productLink.isVisible().catch(() => false)) { await expect(productLink).toBeVisible();
await productLink.click(); await productLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
console.log(' Page détail produit chargée');
} else {
// Products exist but no detail links — the cards may use buy directly
console.log(' ⚠ Pas de liens vers page détail (achat direct sur carte)');
}
}); });
test('05. Bouton Buy Now et Add to Cart présents', async ({ page }) => { test('05. Bouton Buy Now et Add to Cart présents', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
// ProductCard has "Buy Now" and "Add to Cart" buttons const firstProduct = page.locator('article[aria-label^="Product:"]').first();
await expect(firstProduct).toBeVisible();
await firstProduct.hover();
await page.waitForTimeout(500);
const buyBtn = page.getByRole('button', { name: /buy now/i }).first(); const buyBtn = page.getByRole('button', { name: /buy now/i }).first();
const addToCartBtn = page.getByRole('button', { name: /add to cart/i }).first(); const addToCartBtn = page.getByRole('button', { name: /add to cart/i }).first();
// Hover the first product card to reveal the Add to Cart button (it has opacity-0 by default) await expect(buyBtn).toBeVisible();
const firstProduct = page.locator('article[aria-label^="Product:"]').first(); await expect(addToCartBtn).toBeVisible();
if (await firstProduct.isVisible().catch(() => false)) {
await firstProduct.hover();
await page.waitForTimeout(500);
}
const hasBuy = await buyBtn.isVisible().catch(() => false);
const hasAddToCart = await addToCartBtn.isVisible().catch(() => false);
console.log(` Bouton Buy Now: ${hasBuy ? '✓' : '✗'}`);
console.log(` Bouton Add to Cart: ${hasAddToCart ? '✓' : '✗'}`);
}); });
}); });
@ -102,14 +75,11 @@ test.describe('MARKETPLACE — Dashboard vendeur', () => {
}); });
test('06. Dashboard vendeur accessible @critical', async ({ page }) => { test('06. Dashboard vendeur accessible @critical', async ({ page }) => {
// Seller dashboard is at /sell
await navigateTo(page, '/sell'); await navigateTo(page, '/sell');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(100); expect(body.length).toBeGreaterThan(100);
console.log(' Dashboard vendeur chargé à /sell');
}); });
}); });
@ -119,14 +89,11 @@ test.describe('MARKETPLACE — Wishlist', () => {
}); });
test('07. Page wishlist accessible @critical', async ({ page }) => { test('07. Page wishlist accessible @critical', async ({ page }) => {
// Wishlist is at /wishlist
await navigateTo(page, '/wishlist'); await navigateTo(page, '/wishlist');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(50); expect(body.length).toBeGreaterThan(50);
console.log(' Page /wishlist chargée');
}); });
}); });
@ -136,14 +103,11 @@ test.describe('MARKETPLACE — Purchases', () => {
}); });
test('08. Page purchases accessible', async ({ page }) => { test('08. Page purchases accessible', async ({ page }) => {
// Purchases page is at /purchases
await navigateTo(page, '/purchases'); await navigateTo(page, '/purchases');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/); expect(body).not.toMatch(/500|Internal Server Error/);
expect(body.length).toBeGreaterThan(50); expect(body.length).toBeGreaterThan(50);
console.log(' Page /purchases chargée');
}); });
}); });
@ -155,50 +119,31 @@ test.describe('MARKETPLACE — Cart (in-page)', () => {
test('09. Cart s\'ouvre via le bouton Cart sur marketplace', async ({ page }) => { test('09. Cart s\'ouvre via le bouton Cart sur marketplace', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
// MarketplacePage has a Cart button that opens a slide-over Cart component
const cartBtn = page.getByRole('button', { name: /cart/i }).first(); const cartBtn = page.getByRole('button', { name: /cart/i }).first();
if (await cartBtn.isVisible().catch(() => false)) { await expect(cartBtn).toBeVisible();
await cartBtn.click(); await cartBtn.click();
await page.waitForTimeout(500); await page.waitForTimeout(500);
// Cart component should be visible (it's a slide-over panel, not a separate page)
const body = await page.textContent('body') || '';
// Cart panel should show something (empty cart message or items) // Cart panel should show something (empty cart message or items)
console.log(' Cart panel ouvert'); const body = await page.textContent('body') || '';
} else { expect(body).toMatch(/cart|panier|empty|vide|item/i);
console.log(' ⚠ Bouton Cart non visible');
}
}); });
test('10. Ajouter un produit au cart affiche un feedback', async ({ page }) => { test('10. Ajouter un produit au cart affiche un feedback', async ({ page }) => {
await navigateTo(page, '/marketplace'); await navigateTo(page, '/marketplace');
const firstProduct = page.locator('article[aria-label^="Product:"]').first(); const firstProduct = page.locator('article[aria-label^="Product:"]').first();
if (!(await firstProduct.isVisible().catch(() => false))) { test.skip(!(await firstProduct.isVisible().catch(() => false)), 'No products available');
console.log(' ⚠ Aucun produit disponible');
return;
}
// Hover to reveal "Add to Cart" button (hidden by default with opacity-0)
await firstProduct.hover(); await firstProduct.hover();
await page.waitForTimeout(500); await page.waitForTimeout(500);
const addToCartBtn = firstProduct.getByRole('button', { name: /add to cart/i }); const addToCartBtn = firstProduct.getByRole('button', { name: /add to cart/i });
if (await addToCartBtn.isVisible().catch(() => false)) { await expect(addToCartBtn).toBeVisible();
await addToCartBtn.click(); await addToCartBtn.click();
await page.waitForTimeout(1_000); await page.waitForTimeout(1_000);
// Toast feedback: "{title} added to cart"
const toast = page.getByTestId('toast-alert').first(); const toast = page.getByTestId('toast-alert').first();
const hasToast = await toast.isVisible().catch(() => false); await expect(toast).toBeVisible();
console.log(` Toast feedback: ${hasToast ? '✓' : '✗'}`);
// Cart badge should update
const cartBadge = page.locator('button').filter({ hasText: /cart/i }).locator('[class*="badge"], [class*="Badge"]').first();
const hasBadge = await cartBadge.isVisible().catch(() => false);
console.log(` Cart badge mis à jour: ${hasBadge ? '✓' : '✗'}`);
} else {
console.log(' ⚠ Bouton Add to Cart non visible après hover');
}
}); });
}); });

View file

@ -15,65 +15,42 @@ test.describe('CHAT — Messagerie', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
// Chat page should show either conversation list (Channels header) or auth prompt expect(body.length).toBeGreaterThan(100);
const hasContent = body.length > 100;
expect(hasContent).toBeTruthy();
console.log(' Chat page loaded at /chat');
}); });
test('02. Sidebar avec liste des conversations (Channels)', async ({ page }) => { test('02. Sidebar avec liste des conversations (Channels)', async ({ page }) => {
await navigateTo(page, '/chat'); await navigateTo(page, '/chat');
// ChatPage renders a sidebar card with heading "Channels"
const channelsHeading = page.getByText('Channels', { exact: true }); const channelsHeading = page.getByText('Channels', { exact: true });
const visible = await channelsHeading.isVisible().catch(() => false); await expect(channelsHeading).toBeVisible();
console.log(` Channels sidebar heading: ${visible ? '✓' : '✗'}`);
// Also check for ChatSidebar component presence
const sidebar = page.locator('[class*="w-80"]'); const sidebar = page.locator('[class*="w-80"]');
const sidebarVisible = await sidebar.first().isVisible().catch(() => false); await expect(sidebar.first()).toBeVisible();
console.log(` Chat sidebar panel: ${sidebarVisible ? '✓' : '✗'}`);
}); });
test('03. Champ de saisie de message visible', async ({ page }) => { test('03. Champ de saisie de message visible', async ({ page }) => {
await navigateTo(page, '/chat'); await navigateTo(page, '/chat');
// ChatInput has aria-label="Type a message" and placeholder containing "Broadcast message"
const msgInput = page.getByLabel('Type a message') const msgInput = page.getByLabel('Type a message')
.or(page.getByPlaceholder(/broadcast message|écrire dans/i)) .or(page.getByPlaceholder(/broadcast message|écrire dans/i))
.or(page.locator('input[type="text"][aria-label="Type a message"]')); .or(page.locator('input[type="text"][aria-label="Type a message"]'));
const visible = await msgInput.first().isVisible().catch(() => false); await expect(msgInput.first()).toBeVisible();
console.log(` Input message: ${visible ? '✓' : '✗'}`);
}); });
test('04. Boutons attach/emoji/send présents', async ({ page }) => { test('04. Boutons attach/emoji/send présents', async ({ page }) => {
await navigateTo(page, '/chat'); await navigateTo(page, '/chat');
// Attach file button await expect(page.getByLabel('Attach file')).toBeVisible();
const attachBtn = page.getByLabel('Attach file'); await expect(page.getByLabel(/add emoji|close emoji/i)).toBeVisible();
const hasAttach = await attachBtn.isVisible().catch(() => false); await expect(page.getByLabel('Send message')).toBeVisible();
console.log(` Bouton attach: ${hasAttach ? '✓' : '✗'}`);
// Emoji button
const emojiBtn = page.getByLabel(/add emoji|close emoji/i);
const hasEmoji = await emojiBtn.isVisible().catch(() => false);
console.log(` Bouton emoji: ${hasEmoji ? '✓' : '✗'}`);
// Send button
const sendBtn = page.getByLabel('Send message');
const hasSend = await sendBtn.isVisible().catch(() => false);
console.log(` Bouton send: ${hasSend ? '✓' : '✗'}`);
}); });
test('05. WebSocket status indicator visible', async ({ page }) => { test('05. WebSocket status indicator visible', async ({ page }) => {
await navigateTo(page, '/chat'); await navigateTo(page, '/chat');
// The ChatPage renders a small dot indicating WS connection status
// green (bg-success) when connected, red (bg-destructive) when disconnected
const statusDot = page.locator('[class*="rounded-full"][class*="bg-success"], [class*="rounded-full"][class*="bg-destructive"]'); const statusDot = page.locator('[class*="rounded-full"][class*="bg-success"], [class*="rounded-full"][class*="bg-destructive"]');
const visible = await statusDot.first().isVisible().catch(() => false); await expect(statusDot.first()).toBeVisible();
console.log(` WS status indicator: ${visible ? '✓' : '✗'}`);
}); });
}); });
@ -89,11 +66,8 @@ test.describe('NOTIFICATIONS — Centre de notifications', () => {
test('06. Bouton notifications (bell) visible dans le header @critical', async ({ page }) => { test('06. Bouton notifications (bell) visible dans le header @critical', async ({ page }) => {
await navigateTo(page, '/dashboard'); await navigateTo(page, '/dashboard');
// NotificationMenuTrigger has aria-label="Notifications" with a Bell icon
const notifBtn = page.getByRole('button', { name: 'Notifications' }); const notifBtn = page.getByRole('button', { name: 'Notifications' });
const visible = await notifBtn.isVisible().catch(() => false); await expect(notifBtn).toBeVisible();
expect(visible).toBeTruthy();
console.log(` Bell notifications button: ${visible ? '✓' : '✗'}`);
}); });
test('07. Page /notifications se charge', async ({ page }) => { test('07. Page /notifications se charge', async ({ page }) => {
@ -102,57 +76,35 @@ test.describe('NOTIFICATIONS — Centre de notifications', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
// NotificationsPageHeader renders an h1 with text "Notifications"
const heading = page.getByRole('heading', { name: /notifications/i }); const heading = page.getByRole('heading', { name: /notifications/i });
const hasHeading = await heading.first().isVisible().catch(() => false); await expect(heading.first()).toBeVisible();
console.log(` Notifications heading: ${hasHeading ? '✓' : '✗'}`);
}); });
test('08. Bouton "Mark All as Read" présent si notifications non lues', async ({ page }) => { test('08. Bouton "Mark All as Read" présent si notifications non lues', async ({ page }) => {
await navigateTo(page, '/notifications'); await navigateTo(page, '/notifications');
// NotificationsPageHeader renders "Mark All as Read" button when hasUnread is true // This button only appears when there are unread notifications — skip if none
const markAllBtn = page.getByRole('button', { name: /mark all as read|marking/i }); const markAllBtn = page.getByRole('button', { name: /mark all as read|marking/i });
const visible = await markAllBtn.isVisible().catch(() => false); const visible = await markAllBtn.isVisible().catch(() => false);
console.log(` Bouton "Mark All as Read": ${visible ? '✓ visible' : '✗ absent (no unread notifications)'}`); test.skip(!visible, 'No unread notifications — Mark All button not expected');
await expect(markAllBtn).toBeVisible();
}); });
test('09. Préférences de notifications accessibles via settings', async ({ page }) => { test('09. Préférences de notifications accessibles via settings', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// SettingsTabs has a tab trigger "Notifications" — try exact and partial match
const notifTab = page.getByRole('tab', { name: /notification/i }); const notifTab = page.getByRole('tab', { name: /notification/i });
const visible = await notifTab.first().isVisible().catch(() => false); await expect(notifTab.first()).toBeVisible();
// Fallback: look for any tab or link containing "notification" await notifTab.first().click();
const altNotifTab = visible ? notifTab.first() : page.locator('[role="tab"]').filter({ hasText: /notif/i }).first();
const altVisible = visible || await altNotifTab.isVisible().catch(() => false);
console.log(` Notifications tab in settings: ${altVisible ? '✓' : '✗'}`);
// Soft assertion — tab may not exist if settings layout differs
if (!altVisible) {
console.log(' ⚠ Notifications tab not found — settings may use a different layout');
// Do not fail — settings tabs may have different names or structure
return;
}
// Click the tab to reveal notification preferences
const tabToClick = visible ? notifTab.first() : altNotifTab;
await tabToClick.click().catch(() => {
console.log(' ⚠ Could not click Notifications tab');
return;
});
await page.waitForTimeout(500); await page.waitForTimeout(500);
// NotificationSettings renders checkboxes for email/push preferences
const emailNotifCheckbox = page.locator('#email_notifications'); const emailNotifCheckbox = page.locator('#email_notifications');
const hasEmailPref = await emailNotifCheckbox.isVisible().catch(() => false); await expect(emailNotifCheckbox).toBeVisible();
console.log(` Email notifications checkbox: ${hasEmailPref ? '✓' : '✗'}`);
const pushNotifCheckbox = page.locator('#push_notifications'); const pushNotifCheckbox = page.locator('#push_notifications');
const hasPushPref = await pushNotifCheckbox.isVisible().catch(() => false); await expect(pushNotifCheckbox).toBeVisible();
console.log(` Push notifications checkbox: ${hasPushPref ? '✓' : '✗'}`);
}); });
}); });
@ -169,15 +121,11 @@ test.describe('SETTINGS — Paramètres', () => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Only fail on actual server errors, not UI elements that contain "error" in their text
expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError/i); expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError/i);
// SettingsPage renders heading "System Config"
const heading = page.getByRole('heading', { name: /system config/i }); const heading = page.getByRole('heading', { name: /system config/i });
const hasHeading = await heading.isVisible().catch(() => false); await expect(heading).toBeVisible();
console.log(` Settings heading: ${hasHeading ? '✓' : '✗'}`);
// SettingsTabs renders tab triggers — names may be in French or English
const tabPatterns: [string, RegExp][] = [ const tabPatterns: [string, RegExp][] = [
['Account', /account|compte/i], ['Account', /account|compte/i],
['Preferences', /pr[ée]f[ée]rences|preferences/i], ['Preferences', /pr[ée]f[ée]rences|preferences/i],
@ -185,173 +133,110 @@ test.describe('SETTINGS — Paramètres', () => {
['Privacy', /confidentialit[ée]|privacy/i], ['Privacy', /confidentialit[ée]|privacy/i],
['Playback', /playback|lecture/i], ['Playback', /playback|lecture/i],
]; ];
for (const [label, pattern] of tabPatterns) { for (const [, pattern] of tabPatterns) {
const tab = page.getByRole('tab', { name: pattern }).first(); const tab = page.getByRole('tab', { name: pattern }).first();
const vis = await tab.isVisible().catch(() => false); await expect(tab).toBeVisible();
console.log(` Tab "${label}": ${vis ? '✓' : '✗'}`);
} }
}); });
test('11. Tab Account — password change form present', async ({ page }) => { test('11. Tab Account — password change form present', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// Account tab is defaultValue, so it should be active by default
// AccountSettingsPasswordCard renders "Change Password" title and fields
const changePasswordTitle = page.getByText('Change Password', { exact: true }); const changePasswordTitle = page.getByText('Change Password', { exact: true });
const visible = await changePasswordTitle.first().isVisible().catch(() => false); await expect(changePasswordTitle.first()).toBeVisible();
console.log(` "Change Password" section: ${visible ? '✓' : '✗'}`);
// Check for password fields by their HTML ids await expect(page.locator('#current-password')).toBeVisible();
const currentPwd = page.locator('#current-password'); await expect(page.locator('#new-password')).toBeVisible();
const newPwd = page.locator('#new-password'); await expect(page.locator('#confirm-password')).toBeVisible();
const confirmPwd = page.locator('#confirm-password');
const hasCurrent = await currentPwd.isVisible().catch(() => false);
const hasNew = await newPwd.isVisible().catch(() => false);
const hasConfirm = await confirmPwd.isVisible().catch(() => false);
console.log(` Current Password field: ${hasCurrent ? '✓' : '✗'}`);
console.log(` New Password field: ${hasNew ? '✓' : '✗'}`);
console.log(` Confirm Password field: ${hasConfirm ? '✓' : '✗'}`);
}); });
test('12. Tab Account — 2FA section present', async ({ page }) => { test('12. Tab Account — 2FA section present', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// TwoFactorSettings renders "Two-Factor Authentication (2FA)" title
const twoFactorTitle = page.getByText('Two-Factor Authentication (2FA)'); const twoFactorTitle = page.getByText('Two-Factor Authentication (2FA)');
const visible = await twoFactorTitle.isVisible().catch(() => false); await expect(twoFactorTitle).toBeVisible();
console.log(` 2FA section: ${visible ? '✓' : '✗'}`);
// Should show either "2FA is enabled" or "2FA is not enabled"
const statusText = page.getByText(/2FA is (enabled|not enabled)/); const statusText = page.getByText(/2FA is (enabled|not enabled)/);
const hasStatus = await statusText.first().isVisible().catch(() => false); await expect(statusText.first()).toBeVisible();
console.log(` 2FA status displayed: ${hasStatus ? '✓' : '✗'}`);
}); });
test('13. Tab Account — data export button (GDPR)', async ({ page }) => { test('13. Tab Account — data export button (GDPR)', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// AccountSettingsExportCard renders "Data Export" title and "Export My Data" button
const exportTitle = page.getByText('Data Export', { exact: true }); const exportTitle = page.getByText('Data Export', { exact: true });
const hasTitleVisible = await exportTitle.first().isVisible().catch(() => false); await expect(exportTitle.first()).toBeVisible();
console.log(` "Data Export" section: ${hasTitleVisible ? '✓' : '✗'}`);
const exportBtn = page.getByRole('button', { name: /export my data/i }); const exportBtn = page.getByRole('button', { name: /export my data/i });
const hasBtn = await exportBtn.isVisible().catch(() => false); await expect(exportBtn).toBeVisible();
console.log(` "Export My Data" button: ${hasBtn ? '✓' : '✗'}`);
}); });
test('14. Tab Account — delete account button with warning', async ({ page }) => { test('14. Tab Account — delete account button with warning', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// AccountSettingsDeleteCard renders "Delete Account" title
const deleteTitle = page.getByText('Delete Account').first(); const deleteTitle = page.getByText('Delete Account').first();
const hasTitle = await deleteTitle.isVisible().catch(() => false); await expect(deleteTitle).toBeVisible();
console.log(` "Delete Account" section: ${hasTitle ? '✓' : '✗'}`);
// Warning text: "This action cannot be undone"
const warningText = page.getByText(/this action cannot be undone/i); const warningText = page.getByText(/this action cannot be undone/i);
const hasWarning = await warningText.first().isVisible().catch(() => false); await expect(warningText.first()).toBeVisible();
console.log(` Warning text present: ${hasWarning ? '✓' : '✗'}`);
// Delete button (we do NOT click it)
const deleteBtn = page.getByRole('button', { name: /delete account/i }); const deleteBtn = page.getByRole('button', { name: /delete account/i });
const hasBtnVisible = await deleteBtn.isVisible().catch(() => false); await expect(deleteBtn).toBeVisible();
console.log(` "Delete Account" button: ${hasBtnVisible ? '✓' : '✗'}`);
}); });
test('15. Tab Preferences — theme radio group', async ({ page }) => { test('15. Tab Preferences — theme radio group', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// Click the Preferences tab — may be "Préférences" or "Preferences"
const prefsTab = page.getByRole('tab', { name: /pr[ée]f[ée]rences|preferences/i }).first(); const prefsTab = page.getByRole('tab', { name: /pr[ée]f[ée]rences|preferences/i }).first();
if (!(await prefsTab.isVisible({ timeout: 3000 }).catch(() => false))) { await expect(prefsTab).toBeVisible();
console.log(' ⚠ Preferences tab not found — skipping'); await prefsTab.click();
return;
}
await prefsTab.click({ timeout: 3000 }).catch(() => {
console.log(' ⚠ Could not click Preferences tab — skipping');
});
await page.waitForTimeout(500); await page.waitForTimeout(500);
// PreferenceSettings has a RadioGroup for theme with items: light, dark, auto await expect(page.locator('#theme-light')).toBeVisible();
const themeLight = page.locator('#theme-light'); await expect(page.locator('#theme-dark')).toBeVisible();
const themeDark = page.locator('#theme-dark'); await expect(page.locator('#theme-auto')).toBeVisible();
const themeAuto = page.locator('#theme-auto');
const hasLight = await themeLight.isVisible().catch(() => false);
const hasDark = await themeDark.isVisible().catch(() => false);
const hasAuto = await themeAuto.isVisible().catch(() => false);
console.log(` Theme light radio: ${hasLight ? '✓' : '✗'}`);
console.log(` Theme dark radio: ${hasDark ? '✓' : '✗'}`);
console.log(` Theme auto radio: ${hasAuto ? '✓' : '✗'}`);
}); });
test('16. Tab Preferences — language selector', async ({ page }) => { test('16. Tab Preferences — language selector', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
const prefsTab = page.getByRole('tab', { name: /pr[ée]f[ée]rences|preferences/i }).first(); const prefsTab = page.getByRole('tab', { name: /pr[ée]f[ée]rences|preferences/i }).first();
if (!(await prefsTab.isVisible({ timeout: 3000 }).catch(() => false))) { await expect(prefsTab).toBeVisible();
console.log(' ⚠ Preferences tab not found — skipping'); await prefsTab.click();
return;
}
await prefsTab.click({ timeout: 3000 }).catch(() => {
console.log(' ⚠ Could not click Preferences tab — skipping');
});
await page.waitForTimeout(500); await page.waitForTimeout(500);
// PreferenceSettings has a Select with name="language"
const langSelect = page.locator('[name="language"]') const langSelect = page.locator('[name="language"]')
.or(page.locator('select[name="language"]')); .or(page.locator('select[name="language"]'));
const visible = await langSelect.first().isVisible().catch(() => false); await expect(langSelect.first()).toBeVisible();
console.log(` Language selector: ${visible ? '✓' : '✗'}`);
}); });
test('17. Tab Privacy — confidentiality settings', async ({ page }) => { test('17. Tab Privacy — confidentiality settings', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// Click the Confidentialite/Privacy tab
const privacyTab = page.getByRole('tab', { name: /confidentialit[ée]|privacy/i }).first(); const privacyTab = page.getByRole('tab', { name: /confidentialit[ée]|privacy/i }).first();
if (!(await privacyTab.isVisible({ timeout: 3000 }).catch(() => false))) { await expect(privacyTab).toBeVisible();
console.log(' ⚠ Privacy tab not found — skipping'); await privacyTab.click();
return;
}
await privacyTab.click({ timeout: 3000 }).catch(() => {
console.log(' ⚠ Could not click Privacy tab — skipping');
});
await page.waitForTimeout(500); await page.waitForTimeout(500);
// PrivacySettings and ProfileVisibilityCard should render
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const hasPrivacyContent = /profil|privacy|visibility|visibilit/i.test(body); expect(body).toMatch(/profil|privacy|visibility|visibilit/i);
console.log(` Privacy content loaded: ${hasPrivacyContent ? '✓' : '✗'}`);
}); });
test('18. Tab Playback — audio quality and crossfade', async ({ page }) => { test('18. Tab Playback — audio quality and crossfade', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// Click the Playback tab
const playbackTab = page.getByRole('tab', { name: /playback|lecture/i }).first(); const playbackTab = page.getByRole('tab', { name: /playback|lecture/i }).first();
if (!(await playbackTab.isVisible({ timeout: 3000 }).catch(() => false))) { await expect(playbackTab).toBeVisible();
console.log(' ⚠ Playback tab not found — skipping'); await playbackTab.click();
return;
}
await playbackTab.click({ timeout: 3000 }).catch(() => {
console.log(' ⚠ Could not click Playback tab — skipping');
});
await page.waitForTimeout(500); await page.waitForTimeout(500);
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const hasPlaybackContent = /quality|crossfade|autoplay|volume/i.test(body); expect(body).toMatch(/quality|crossfade|autoplay|volume/i);
console.log(` Playback settings loaded: ${hasPlaybackContent ? '✓' : '✗'}`);
}); });
test('19. Save Config button visible', async ({ page }) => { test('19. Save Config button visible', async ({ page }) => {
await navigateTo(page, '/settings'); await navigateTo(page, '/settings');
// SettingsPage renders a "Save Config" button
const saveBtn = page.getByRole('button', { name: /save config/i }); const saveBtn = page.getByRole('button', { name: /save config/i });
const visible = await saveBtn.isVisible().catch(() => false); await expect(saveBtn).toBeVisible();
console.log(` "Save Config" button: ${visible ? '✓' : '✗'}`);
}); });
}); });

View file

@ -15,15 +15,13 @@ test.describe('ANALYTICS — Créateur', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
console.log(' Analytics page loaded at /analytics');
}); });
test('02. Graphiques/charts s\'affichent', async ({ page }) => { test('02. Graphiques/charts s\'affichent', async ({ page }) => {
await navigateTo(page, '/analytics'); await navigateTo(page, '/analytics');
const charts = page.locator('canvas, svg[class*="chart"], [class*="recharts"], [class*="Chart"]'); const charts = page.locator('canvas, svg[class*="chart"], [class*="recharts"], [class*="Chart"]');
const count = await charts.count(); expect(await charts.count()).toBeGreaterThan(0);
console.log(` Graphiques trouvés: ${count}`);
}); });
test('03. Période sélectionnable (7j, 30j, 90j, etc.)', async ({ page }) => { test('03. Période sélectionnable (7j, 30j, 90j, etc.)', async ({ page }) => {
@ -33,8 +31,7 @@ test.describe('ANALYTICS — Créateur', () => {
.or(page.locator('select[name*="period"]')) .or(page.locator('select[name*="period"]'))
.or(page.locator('[class*="date-range"], [class*="period"]')); .or(page.locator('[class*="date-range"], [class*="period"]'));
const visible = await periodSelector.first().isVisible().catch(() => false); await expect(periodSelector.first()).toBeVisible();
console.log(` Sélecteur de période: ${visible ? '✓' : '✗'}`);
}); });
}); });
@ -52,27 +49,22 @@ test.describe('SUBSCRIPTIONS — Abonnements', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
console.log(' Subscription page loaded at /subscription');
}); });
test('05. Les plans sont affichés', async ({ page }) => { test('05. Les plans sont affichés', async ({ page }) => {
await navigateTo(page, '/subscription'); await navigateTo(page, '/subscription');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).toMatch(/free/i);
const plans = ['free', 'creator', 'premium']; expect(body).toMatch(/creator/i);
for (const plan of plans) { expect(body).toMatch(/premium/i);
const found = new RegExp(plan, 'i').test(body);
console.log(` Plan ${plan}: ${found ? '✓' : '✗'}`);
}
}); });
test('06. Prix affichés correctement', async ({ page }) => { test('06. Prix affichés correctement', async ({ page }) => {
await navigateTo(page, '/subscription'); await navigateTo(page, '/subscription');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const hasPricing = /\$\d+\.\d{2}|\d+[,\.]\d{2}\s*€/i.test(body); expect(body).toMatch(/\$\d+\.\d{2}|\d+[,\.]\d{2}\s*€/i);
console.log(` Prix affichés: ${hasPricing ? '✓' : '✗'}`);
}); });
}); });
@ -89,9 +81,7 @@ test.describe('ADMIN — Dashboard', () => {
await navigateTo(page, '/admin'); await navigateTo(page, '/admin');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Admin pages may show error text in their UI (e.g., "Error loading...") — only fail on server errors
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Admin dashboard loaded at /admin');
}); });
test('08. Modération accessible à /admin/moderation', async ({ page }) => { test('08. Modération accessible à /admin/moderation', async ({ page }) => {
@ -99,7 +89,6 @@ test.describe('ADMIN — Dashboard', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Admin moderation loaded at /admin/moderation');
}); });
test('09. Platform admin à /admin/platform', async ({ page }) => { test('09. Platform admin à /admin/platform', async ({ page }) => {
@ -107,7 +96,6 @@ test.describe('ADMIN — Dashboard', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Admin platform loaded at /admin/platform');
}); });
test('10. Transfers admin à /admin/transfers', async ({ page }) => { test('10. Transfers admin à /admin/transfers', async ({ page }) => {
@ -115,45 +103,33 @@ test.describe('ADMIN — Dashboard', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Admin transfers loaded at /admin/transfers');
}); });
test('11. Roles admin à /admin/roles', async ({ page }) => { test('11. Roles admin à /admin/roles', async ({ page }) => {
await navigateTo(page, '/admin/roles'); await navigateTo(page, '/admin/roles');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Soften assertion: page may show "error" in UI elements (e.g., error state components)
// Only fail on actual server errors (500, Internal Server Error)
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Admin roles loaded at /admin/roles');
}); });
test('12. Admin non accessible pour un user normal', async ({ page }) => { test('12. Admin non accessible pour un user normal', async ({ page }) => {
test.setTimeout(30_000); test.setTimeout(30_000);
// Navigate to login page first, then re-login as a normal listener
await page.goto('/login', { waitUntil: 'domcontentloaded', timeout: 10_000 }); await page.goto('/login', { waitUntil: 'domcontentloaded', timeout: 10_000 });
await page.waitForTimeout(1_000); await page.waitForTimeout(1_000);
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await page.waitForTimeout(3_000); await page.waitForTimeout(3_000);
// If login failed, skip — we cannot test admin access without being logged in
await page.goto('/admin', { timeout: 10_000 }).catch(() => {}); await page.goto('/admin', { timeout: 10_000 }).catch(() => {});
await page.waitForLoadState('domcontentloaded').catch(() => {}); await page.waitForLoadState('domcontentloaded').catch(() => {});
await page.waitForTimeout(2_000); await page.waitForTimeout(2_000);
// Should be redirected away, get a 403/unauthorized, or show an error/access denied page
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const currentUrl = page.url(); const currentUrl = page.url();
const isRedirected = !currentUrl.includes('/admin'); const isRedirected = !currentUrl.includes('/admin');
const isBlockedByMessage = /403|forbidden|accès.*refusé|unauthorized|not authorized|access denied/i.test(body); const isBlockedByMessage = /403|forbidden|accès.*refusé|unauthorized|not authorized|access denied/i.test(body);
const isBlocked = isRedirected || isBlockedByMessage; expect(isRedirected || isBlockedByMessage).toBeTruthy();
// Soft assertion: even if not explicitly blocked, the page loaded without admin content
if (!isBlocked) {
console.log(' Warning: Admin page did not explicitly block normal user — may need manual verification');
}
console.log(` Admin blocked for normal user (redirected: ${isRedirected}, blocked message: ${isBlockedByMessage})`);
}); });
}); });
@ -168,7 +144,6 @@ test.describe('LIVE — Streaming', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Live page loaded at /live');
}); });
test('14. Page /live/go-live accessible pour créateur', async ({ page }) => { test('14. Page /live/go-live accessible pour créateur', async ({ page }) => {
@ -176,11 +151,8 @@ test.describe('LIVE — Streaming', () => {
await navigateTo(page, '/live/go-live'); await navigateTo(page, '/live/go-live');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Only fail on actual server errors, not UI "error" text
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
// Look for RTMP or stream key related content expect(body).toMatch(/rtmp|stream.*key|clé|go.*live|broadcast/i);
const hasStreamConfig = /rtmp|stream.*key|clé|go.*live|broadcast/i.test(body);
console.log(` Go Live page content: ${hasStreamConfig ? '✓ stream config found' : '✗ no stream config text'}`);
}); });
}); });
@ -194,9 +166,7 @@ test.describe('CLOUD — Stockage', () => {
await navigateTo(page, '/cloud'); await navigateTo(page, '/cloud');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Only fail on actual server errors, not UI "error" text
expect(body).not.toMatch(/500|Internal Server Error/i); expect(body).not.toMatch(/500|Internal Server Error/i);
console.log(' Cloud page loaded at /cloud');
}); });
test('16. Zone d\'upload de fichiers', async ({ page }) => { test('16. Zone d\'upload de fichiers', async ({ page }) => {
@ -206,8 +176,7 @@ test.describe('CLOUD — Stockage', () => {
const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter|add/i }) const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter|add/i })
.or(page.locator('input[type="file"]')); .or(page.locator('input[type="file"]'));
const visible = await uploadBtn.first().isVisible().catch(() => false); await expect(uploadBtn.first()).toBeVisible();
console.log(` Upload zone/button: ${visible ? '✓' : '✗'}`);
}); });
}); });
@ -222,7 +191,6 @@ test.describe('EDUCATION — Cours', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
console.log(' Education page loaded at /education');
}); });
}); });
@ -237,7 +205,6 @@ test.describe('GEAR — Équipement', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
console.log(' Gear page loaded at /gear');
}); });
}); });
@ -251,9 +218,7 @@ test.describe('DEVELOPER — API publique', () => {
await navigateTo(page, '/developer'); await navigateTo(page, '/developer');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
// Only fail on actual server errors, not UI elements that contain "error" in their text
expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError/i); expect(body).not.toMatch(/500|Internal Server Error|crash|TypeError/i);
console.log(' Developer page loaded at /developer');
}); });
test('20. Page /webhooks accessible', async ({ page }) => { test('20. Page /webhooks accessible', async ({ page }) => {
@ -262,6 +227,5 @@ test.describe('DEVELOPER — API publique', () => {
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|error|crash/i); expect(body).not.toMatch(/500|error|crash/i);
console.log(' Webhooks page loaded at /webhooks');
}); });
}); });

View file

@ -25,11 +25,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
test('01. Cliquer sur l\'avatar ouvre le menu utilisateur', async ({ page }) => { test('01. Cliquer sur l\'avatar ouvre le menu utilisateur', async ({ page }) => {
// The user menu trigger has data-testid="user-menu" in Header.tsx // The user menu trigger has data-testid="user-menu" in Header.tsx
const userMenuTrigger = page.getByTestId('user-menu'); const userMenuTrigger = page.getByTestId('user-menu');
await expect(userMenuTrigger).toBeVisible();
if (!(await userMenuTrigger.isVisible().catch(() => false))) {
console.log(' User menu trigger not found — skipping');
return;
}
await userMenuTrigger.click(); await userMenuTrigger.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -46,48 +42,36 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const menuOpened = profileVisible || settingsVisible || signOutVisible; const menuOpened = profileVisible || settingsVisible || signOutVisible;
expect(menuOpened).toBeTruthy(); expect(menuOpened).toBeTruthy();
console.log(` User menu dropdown: ${menuOpened ? 'open' : 'not detected'} (profile: ${profileVisible}, settings: ${settingsVisible}, signOut: ${signOutVisible})`);
}); });
test('02. Escape ferme le menu utilisateur', async ({ page }) => { test('02. Escape ferme le menu utilisateur', async ({ page }) => {
const userMenuTrigger = page.getByTestId('user-menu'); const userMenuTrigger = page.getByTestId('user-menu');
if (!(await userMenuTrigger.isVisible().catch(() => false))) return; await expect(userMenuTrigger).toBeVisible();
await userMenuTrigger.click(); await userMenuTrigger.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
// Verify menu is open — the dropdown contains a link to /profile // Verify menu is open — the dropdown contains a link to /profile
const profileLink = page.locator('a[href="/profile"]'); const profileLink = page.locator('a[href="/profile"]');
const wasOpen = await profileLink.isVisible().catch(() => false); await expect(profileLink).toBeVisible();
// Press Escape — Header.tsx FocusTrap has onEscape handler // Press Escape — Header.tsx FocusTrap has onEscape handler
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
// Menu should be closed // Menu should be closed
const stillOpen = await profileLink.isVisible().catch(() => false); await expect(profileLink).not.toBeVisible();
if (wasOpen) {
expect(stillOpen).toBeFalsy();
console.log(' Escape closed user menu');
} else {
console.log(' Menu was not open to begin with');
}
}); });
test('03. Cliquer en dehors ferme le menu', async ({ page }) => { test('03. Cliquer en dehors ferme le menu', async ({ page }) => {
const userMenuTrigger = page.getByTestId('user-menu'); const userMenuTrigger = page.getByTestId('user-menu');
if (!(await userMenuTrigger.isVisible().catch(() => false))) return; await expect(userMenuTrigger).toBeVisible();
await userMenuTrigger.click(); await userMenuTrigger.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const profileLink = page.locator('a[href="/profile"]'); const profileLink = page.locator('a[href="/profile"]');
const wasOpen = await profileLink.isVisible().catch(() => false); await expect(profileLink).toBeVisible();
if (!wasOpen) {
console.log(' Menu was not open to begin with — skipping');
return;
}
// The user menu uses FocusTrap — clicking outside may not work via click(). // The user menu uses FocusTrap — clicking outside may not work via click().
// Use Escape as a reliable close mechanism, or click on a distant area. // Use Escape as a reliable close mechanism, or click on a distant area.
@ -95,9 +79,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const stillOpen = await profileLink.isVisible().catch(() => false); await expect(profileLink).not.toBeVisible();
expect(stillOpen).toBeFalsy();
console.log(' Click outside / Escape closed user menu');
}); });
}); });
@ -114,10 +96,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first() const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first()
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first()); .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first());
if (!(await createBtn.isVisible().catch(() => false))) { await expect(createBtn).toBeVisible();
console.log(' ⚠ Create playlist button not found');
return;
}
await createBtn.click(); await createBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -126,22 +105,22 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const dialog = page.locator('[role="dialog"]') const dialog = page.locator('[role="dialog"]')
.or(page.locator('[role="alertdialog"]')) .or(page.locator('[role="alertdialog"]'))
.or(page.locator('[data-state="open"]')); .or(page.locator('[data-state="open"]'));
const visible = await dialog.first().isVisible().catch(() => false);
// Also check for a name/title input inside the modal // Also check for a name/title input inside the modal
const nameInput = page.getByLabel(/nom|name|titre|title/i).first() const nameInput = page.getByLabel(/nom|name|titre|title/i).first()
.or(page.getByPlaceholder(/nom|name|titre/i).first()); .or(page.getByPlaceholder(/nom|name|titre/i).first());
const dialogVisible = await dialog.first().isVisible().catch(() => false);
const hasInput = await nameInput.isVisible().catch(() => false); const hasInput = await nameInput.isVisible().catch(() => false);
expect(visible || hasInput).toBeTruthy(); expect(dialogVisible || hasInput).toBeTruthy();
console.log(` Create playlist modal: ${visible ? '✓ dialog visible' : '✗ dialog not found'}, input: ${hasInput ? '✓' : '✗'}`);
}); });
test('05. Escape ferme la modale de création', async ({ page }) => { test('05. Escape ferme la modale de création', async ({ page }) => {
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first() const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first()
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first()); .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first());
if (!(await createBtn.isVisible().catch(() => false))) return; await expect(createBtn).toBeVisible();
await createBtn.click(); await createBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -149,25 +128,19 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const dialog = page.locator('[role="dialog"]') const dialog = page.locator('[role="dialog"]')
.or(page.locator('[role="alertdialog"]')); .or(page.locator('[role="alertdialog"]'));
const wasOpen = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).toBeVisible();
if (!wasOpen) {
console.log(' ⚠ Dialog did not open, skipping Escape test');
return;
}
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const stillOpen = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).not.toBeVisible();
expect(stillOpen).toBeFalsy();
console.log(' Escape closed playlist creation modal');
}); });
test('06. Soumettre un titre valide crée la playlist et ferme la modale', async ({ page }) => { test('06. Soumettre un titre valide crée la playlist et ferme la modale', async ({ page }) => {
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first() const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first()
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first()); .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first());
if (!(await createBtn.isVisible().catch(() => false))) return; await expect(createBtn).toBeVisible();
await createBtn.click(); await createBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -175,31 +148,24 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const nameInput = page.getByLabel(/nom|name|titre|title/i).first() const nameInput = page.getByLabel(/nom|name|titre|title/i).first()
.or(page.getByPlaceholder(/nom|name|titre/i).first()); .or(page.getByPlaceholder(/nom|name|titre/i).first());
if (!(await nameInput.isVisible().catch(() => false))) { await expect(nameInput).toBeVisible();
console.log(' ⚠ Name input not found in modal');
return;
}
const playlistName = `E2E Modal Test ${testId()}`; const playlistName = `E2E Modal Test ${testId()}`;
await nameInput.fill(playlistName); await nameInput.fill(playlistName);
// Submit — look for create/save/ok button inside the dialog // Submit — look for create/save/ok button inside the dialog
const submitBtn = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok|submit/i }); const submitBtn = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok|submit/i });
if (await submitBtn.isVisible().catch(() => false)) { await expect(submitBtn).toBeVisible();
await submitBtn.click(); await submitBtn.click();
await page.waitForTimeout(2_000); await page.waitForTimeout(2_000);
// Modal should be closed // Modal should be closed
const dialog = page.locator('[role="dialog"]'); const dialog = page.locator('[role="dialog"]');
const stillOpen = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).not.toBeVisible();
console.log(` Modal after submit: ${stillOpen ? '✗ still open' : '✓ closed'}`);
// Playlist name should appear on the page // Playlist name should appear on the page
const created = await page.getByText(playlistName).isVisible().catch(() => false); await expect(page.getByText(playlistName)).toBeVisible();
console.log(` Playlist "${playlistName}" visible: ${created ? '✓' : '✗'}`);
} else {
console.log(' ⚠ Submit button not found in modal');
}
}); });
}); });
@ -217,10 +183,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
.or(page.getByPlaceholder(/search for tracks/i)) .or(page.getByPlaceholder(/search for tracks/i))
.or(page.locator(SELECTORS.searchInput)); .or(page.locator(SELECTORS.searchInput));
if (!(await searchInput.first().isVisible().catch(() => false))) { await expect(searchInput.first()).toBeVisible();
console.log(' ⚠ Search input not found');
return;
}
await searchInput.first().fill('tes'); await searchInput.first().fill('tes');
// Wait for debounce (300-500ms) + network // Wait for debounce (300-500ms) + network
@ -229,8 +192,10 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
// Suggestions dropdown uses role="listbox" (SearchPageHeader.tsx) // Suggestions dropdown uses role="listbox" (SearchPageHeader.tsx)
const suggestions = page.locator('[role="listbox"]') const suggestions = page.locator('[role="listbox"]')
.or(page.locator('[data-radix-popper-content-wrapper]')); .or(page.locator('[data-radix-popper-content-wrapper]'));
// Suggestions depend on data — may not appear if nothing matches
const visible = await suggestions.first().isVisible().catch(() => false); const visible = await suggestions.first().isVisible().catch(() => false);
console.log(` Search suggestions dropdown: ${visible ? '✓ visible' : '✗ not visible (may have no suggestions)'}`); test.skip(!visible, 'No search suggestions appeared (may have no matching data)');
await expect(suggestions.first()).toBeVisible();
}); });
test('08. Escape ferme le dropdown de recherche', async ({ page }) => { test('08. Escape ferme le dropdown de recherche', async ({ page }) => {
@ -238,7 +203,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
.or(page.getByPlaceholder(/search for tracks/i)) .or(page.getByPlaceholder(/search for tracks/i))
.or(page.locator(SELECTORS.searchInput)); .or(page.locator(SELECTORS.searchInput));
if (!(await searchInput.first().isVisible().catch(() => false))) return; await expect(searchInput.first()).toBeVisible();
await searchInput.first().fill('tes'); await searchInput.first().fill('tes');
await page.waitForTimeout(1_500); await page.waitForTimeout(1_500);
@ -246,17 +211,12 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const suggestions = page.locator('[role="listbox"]') const suggestions = page.locator('[role="listbox"]')
.or(page.locator('[data-radix-popper-content-wrapper]')); .or(page.locator('[data-radix-popper-content-wrapper]'));
const wasOpen = await suggestions.first().isVisible().catch(() => false); const wasOpen = await suggestions.first().isVisible().catch(() => false);
test.skip(!wasOpen, 'No suggestions appeared to close');
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
if (wasOpen) { await expect(suggestions.first()).not.toBeVisible();
const stillOpen = await suggestions.first().isVisible().catch(() => false);
expect(stillOpen).toBeFalsy();
console.log(' Escape closed search suggestions');
} else {
console.log(' ⚠ No suggestions were open to close');
}
}); });
test('09. Cliquer une suggestion navigue vers le resultat', async ({ page }) => { test('09. Cliquer une suggestion navigue vers le resultat', async ({ page }) => {
@ -264,7 +224,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
.or(page.getByPlaceholder(/search for tracks/i)) .or(page.getByPlaceholder(/search for tracks/i))
.or(page.locator(SELECTORS.searchInput)); .or(page.locator(SELECTORS.searchInput));
if (!(await searchInput.first().isVisible().catch(() => false))) return; await expect(searchInput.first()).toBeVisible();
await searchInput.first().fill('music'); await searchInput.first().fill('music');
await page.waitForTimeout(1_500); await page.waitForTimeout(1_500);
@ -274,18 +234,16 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
.or(page.locator('[role="listbox"] li').first()) .or(page.locator('[role="listbox"] li').first())
.or(page.locator('[role="listbox"] a').first()); .or(page.locator('[role="listbox"] a').first());
if (await suggestionItem.isVisible().catch(() => false)) { const suggestionVisible = await suggestionItem.isVisible().catch(() => false);
test.skip(!suggestionVisible, 'No clickable suggestion found (data-dependent)');
const urlBefore = page.url(); const urlBefore = page.url();
await suggestionItem.click(); await suggestionItem.click();
await page.waitForTimeout(1_000); await page.waitForTimeout(1_000);
// URL or page content should have changed // URL or page content should have changed
const urlAfter = page.url(); const urlAfter = page.url();
const navigated = urlBefore !== urlAfter; expect(urlBefore !== urlAfter).toBeTruthy();
console.log(` Clicked suggestion — navigated: ${navigated ? '✓' : '✗ (stayed on same page)'}`);
} else {
console.log(' ⚠ No clickable suggestion found');
}
}); });
}); });
@ -302,10 +260,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const notifBtn = page.getByRole('button', { name: 'Notifications' }) const notifBtn = page.getByRole('button', { name: 'Notifications' })
.or(page.locator('[aria-label="Notifications"]')); .or(page.locator('[aria-label="Notifications"]'));
if (!(await notifBtn.first().isVisible().catch(() => false))) { await expect(notifBtn.first()).toBeVisible();
console.log(' ⚠ Notification bell button not found');
return;
}
await notifBtn.first().click(); await notifBtn.first().click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -314,15 +269,14 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const dropdown = page.locator('[role="menu"]') const dropdown = page.locator('[role="menu"]')
.or(page.locator('[data-radix-popper-content-wrapper]')) .or(page.locator('[data-radix-popper-content-wrapper]'))
.or(page.locator('[role="dialog"]')); .or(page.locator('[role="dialog"]'));
const visible = await dropdown.first().isVisible().catch(() => false); await expect(dropdown.first()).toBeVisible();
console.log(` Notification dropdown: ${visible ? '✓ open' : '✗ not visible'}`);
}); });
test('11. Escape ferme le dropdown', async ({ page }) => { test('11. Escape ferme le dropdown', async ({ page }) => {
const notifBtn = page.getByRole('button', { name: 'Notifications' }) const notifBtn = page.getByRole('button', { name: 'Notifications' })
.or(page.locator('[aria-label="Notifications"]')); .or(page.locator('[aria-label="Notifications"]'));
if (!(await notifBtn.first().isVisible().catch(() => false))) return; await expect(notifBtn.first()).toBeVisible();
await notifBtn.first().click(); await notifBtn.first().click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -330,18 +284,13 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const dropdown = page.locator('[role="menu"]') const dropdown = page.locator('[role="menu"]')
.or(page.locator('[data-radix-popper-content-wrapper]')) .or(page.locator('[data-radix-popper-content-wrapper]'))
.or(page.locator('[role="dialog"]')); .or(page.locator('[role="dialog"]'));
const wasOpen = await dropdown.first().isVisible().catch(() => false);
await expect(dropdown.first()).toBeVisible();
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
if (wasOpen) { await expect(dropdown.first()).not.toBeVisible();
const stillOpen = await dropdown.first().isVisible().catch(() => false);
expect(stillOpen).toBeFalsy();
console.log(' Escape closed notification dropdown');
} else {
console.log(' ⚠ Dropdown was not open');
}
}); });
}); });
@ -358,26 +307,21 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first() const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
.or(page.getByRole('link', { name: /upload|importer/i }).first()); .or(page.getByRole('link', { name: /upload|importer/i }).first());
if (!(await uploadBtn.isVisible().catch(() => false))) { await expect(uploadBtn).toBeVisible();
console.log(' ⚠ Upload button not found on /library');
return;
}
await uploadBtn.click(); await uploadBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const dialog = page.locator('[role="dialog"]') const dialog = page.locator('[role="dialog"]')
.or(page.locator('[role="alertdialog"]')); .or(page.locator('[role="alertdialog"]'));
const visible = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).toBeVisible();
expect(visible).toBeTruthy();
console.log(` Upload modal: ${visible ? '✓ open' : '✗ not open'}`);
}); });
test('13. La modale a une zone de drag-drop ou un input file', async ({ page }) => { test('13. La modale a une zone de drag-drop ou un input file', async ({ page }) => {
const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first() const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
.or(page.getByRole('link', { name: /upload|importer/i }).first()); .or(page.getByRole('link', { name: /upload|importer/i }).first());
if (!(await uploadBtn.isVisible().catch(() => false))) return; await expect(uploadBtn).toBeVisible();
await uploadBtn.click(); await uploadBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -391,33 +335,26 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const hasDropZone = await dropZone.first().isVisible().catch(() => false); const hasDropZone = await dropZone.first().isVisible().catch(() => false);
expect(hasFileInput || hasDropZone).toBeTruthy(); expect(hasFileInput || hasDropZone).toBeTruthy();
console.log(` File input: ${hasFileInput ? '✓' : '✗'}, Drop zone: ${hasDropZone ? '✓' : '✗'}`);
}); });
test('14. Escape ferme la modale d\'upload', async ({ page }) => { test('14. Escape ferme la modale d\'upload', async ({ page }) => {
const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first() const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
.or(page.getByRole('link', { name: /upload|importer/i }).first()); .or(page.getByRole('link', { name: /upload|importer/i }).first());
if (!(await uploadBtn.isVisible().catch(() => false))) return; await expect(uploadBtn).toBeVisible();
await uploadBtn.click(); await uploadBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const dialog = page.locator('[role="dialog"]') const dialog = page.locator('[role="dialog"]')
.or(page.locator('[role="alertdialog"]')); .or(page.locator('[role="alertdialog"]'));
const wasOpen = await dialog.first().isVisible().catch(() => false);
if (!wasOpen) { await expect(dialog.first()).toBeVisible();
console.log(' ⚠ Upload modal did not open');
return;
}
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
const stillOpen = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).not.toBeVisible();
expect(stillOpen).toBeFalsy();
console.log(' Escape closed upload modal');
}); });
}); });
@ -434,10 +371,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
// Open an existing playlist // Open an existing playlist
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { test.skip(!(await playlistLink.isVisible().catch(() => false)), 'No existing playlist to test delete confirmation');
console.log(' ⚠ No existing playlist to test delete confirmation');
return;
}
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
@ -446,10 +380,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first() const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first()
.or(page.locator('[data-action="delete"]').first()); .or(page.locator('[data-action="delete"]').first());
if (!(await deleteBtn.isVisible().catch(() => false))) { test.skip(!(await deleteBtn.isVisible().catch(() => false)), 'Delete button not found on playlist page');
console.log(' ⚠ Delete button not found on playlist page');
return;
}
await deleteBtn.click(); await deleteBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
@ -457,54 +388,46 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
// A confirmation dialog should appear // A confirmation dialog should appear
const confirmDialog = page.locator('[role="alertdialog"]') const confirmDialog = page.locator('[role="alertdialog"]')
.or(page.locator('[role="dialog"]')); .or(page.locator('[role="dialog"]'));
const visible = await confirmDialog.first().isVisible().catch(() => false);
// Look for confirmation text // Look for confirmation text
const confirmText = page.getByText(/confirmer|confirm|supprimer|are you sure|etes-vous/i); const confirmText = page.getByText(/confirmer|confirm|supprimer|are you sure|etes-vous/i);
const dialogVisible = await confirmDialog.first().isVisible().catch(() => false);
const hasText = await confirmText.first().isVisible().catch(() => false); const hasText = await confirmText.first().isVisible().catch(() => false);
expect(visible || hasText).toBeTruthy(); expect(dialogVisible || hasText).toBeTruthy();
console.log(` Confirmation dialog: ${visible ? '✓ dialog' : '✗'}, text: ${hasText ? '✓' : '✗'}`);
}); });
test('16. Annuler la confirmation ne supprime pas', async ({ page }) => { test('16. Annuler la confirmation ne supprime pas', async ({ page }) => {
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const playlistLink = page.locator('a[href*="/playlists/"]').first(); const playlistLink = page.locator('a[href*="/playlists/"]').first();
if (!(await playlistLink.isVisible().catch(() => false))) { test.skip(!(await playlistLink.isVisible().catch(() => false)), 'No existing playlist to test cancel confirmation');
console.log(' ⚠ No existing playlist to test cancel confirmation');
return;
}
const playlistText = await playlistLink.textContent() || '';
await playlistLink.click(); await playlistLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first() const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first()
.or(page.locator('[data-action="delete"]').first()); .or(page.locator('[data-action="delete"]').first());
if (!(await deleteBtn.isVisible().catch(() => false))) return; test.skip(!(await deleteBtn.isVisible().catch(() => false)), 'Delete button not found on playlist page');
await deleteBtn.click(); await deleteBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
// Click Cancel/Annuler button // Click Cancel/Annuler button
const cancelBtn = page.getByRole('button', { name: /annuler|cancel|non|no/i }); const cancelBtn = page.getByRole('button', { name: /annuler|cancel|non|no/i });
if (await cancelBtn.isVisible().catch(() => false)) { await expect(cancelBtn).toBeVisible();
await cancelBtn.click(); await cancelBtn.click();
await page.waitForTimeout(CONFIG.timeouts.animation); await page.waitForTimeout(CONFIG.timeouts.animation);
// Confirmation dialog should be closed // Confirmation dialog should be closed
const dialog = page.locator('[role="alertdialog"]'); const dialog = page.locator('[role="alertdialog"]');
const stillOpen = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).not.toBeVisible();
console.log(` Dialog after cancel: ${stillOpen ? '✗ still open' : '✓ closed'}`);
// We should still be on the playlist page (not redirected) // We should still be on the playlist page (not redirected)
await assertNotBroken(page); await assertNotBroken(page);
console.log(' Page still intact after cancel');
} else {
console.log(' ⚠ Cancel button not found in confirmation dialog');
}
}); });
}); });
@ -528,20 +451,15 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
if (!(await editBtn.isVisible().catch(() => false))) { if (!(await editBtn.isVisible().catch(() => false))) {
// Try track detail page — navigate to a track // Try track detail page — navigate to a track
const trackLink = page.locator('a[href*="/tracks/"]').first(); const trackLink = page.locator('a[href*="/tracks/"]').first();
if (await trackLink.isVisible().catch(() => false)) { test.skip(!(await trackLink.isVisible().catch(() => false)), 'No tracks or edit button found');
await trackLink.click(); await trackLink.click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
const editBtnDetail = page.getByRole('button', { name: /edit|modifier|métadonnées|metadata/i }).first(); const editBtnDetail = page.getByRole('button', { name: /edit|modifier|métadonnées|metadata/i }).first();
if (!(await editBtnDetail.isVisible().catch(() => false))) { test.skip(!(await editBtnDetail.isVisible().catch(() => false)), 'Edit metadata button not found on track page');
console.log(' ⚠ Edit metadata button not found on track page');
return;
}
await editBtnDetail.click(); await editBtnDetail.click();
} else {
console.log(' ⚠ No tracks or edit button found');
return;
}
} else { } else {
await editBtn.click(); await editBtn.click();
} }
@ -550,8 +468,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const dialog = page.locator('[role="dialog"]') const dialog = page.locator('[role="dialog"]')
.or(page.locator('[role="alertdialog"]')); .or(page.locator('[role="alertdialog"]'));
const visible = await dialog.first().isVisible().catch(() => false); await expect(dialog.first()).toBeVisible();
console.log(` Edit metadata modal: ${visible ? '✓ open' : '✗ not open'}`);
}); });
test('18. La modale contient les champs BPM, key, genres, tags', async ({ page }) => { test('18. La modale contient les champs BPM, key, genres, tags', async ({ page }) => {
@ -583,10 +500,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
} }
} }
if (!modalOpened) { test.skip(!modalOpened, 'Could not open metadata edit modal (no tracks or edit button found)');
console.log(' ⚠ Could not open metadata edit modal');
return;
}
// Check for metadata fields // Check for metadata fields
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
@ -600,10 +514,10 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => {
const hasTags = /tag/i.test(body) const hasTags = /tag/i.test(body)
|| await page.getByLabel(/tag/i).first().isVisible().catch(() => false); || await page.getByLabel(/tag/i).first().isVisible().catch(() => false);
console.log(` BPM field: ${hasBPM ? '✓' : '✗'}`); expect(hasBPM).toBeTruthy();
console.log(` Key field: ${hasKey ? '✓' : '✗'}`); expect(hasKey).toBeTruthy();
console.log(` Genres field: ${hasGenres ? '✓' : '✗'}`); expect(hasGenres).toBeTruthy();
console.log(` Tags field: ${hasTags ? '✓' : '✗'}`); expect(hasTags).toBeTruthy();
}); });
}); });
}); });

View file

@ -32,12 +32,7 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
}, },
}); });
if (response.ok()) { expect(response.ok()).toBeTruthy();
console.log(` Fresh user registered: ${freshUserEmail}`);
} else {
// If registration fails (e.g. endpoint shape differs), fall back to listener account
console.log(` ⚠ Fresh user registration failed (${response.status()}), tests will adapt`);
}
}); });
/** /**
@ -53,17 +48,14 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
return true; return true;
} catch { } catch {
// Fallback: use listener account (may not have truly empty states) // Fallback: use listener account (may not have truly empty states)
console.log(' ⚠ Falling back to listener account');
try { try {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Check that fallback login also succeeded // Check that fallback login also succeeded
if (page.url().includes('/login')) { if (page.url().includes('/login')) {
console.log(' ⚠ Fallback login also failed — still on /login');
return false; return false;
} }
return true; return true;
} catch { } catch {
console.log(' ⚠ Fallback login threw an error');
return false; return false;
} }
} }
@ -137,7 +129,7 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
test('01. Bibliotheque vide — message + CTA upload @critical', async ({ page }) => { test('01. Bibliotheque vide — message + CTA upload @critical', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } expect(loggedIn).toBeTruthy();
await navigateTo(page, '/library'); await navigateTo(page, '/library');
const { hasEmptyState, hasCta } = await assertEmptyState(page, { const { hasEmptyState, hasCta } = await assertEmptyState(page, {
@ -145,8 +137,8 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
ctaPattern: /upload|importer|ajouter|add/i, ctaPattern: /upload|importer|ajouter|add/i,
}); });
console.log(` /library empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
console.log(` /library CTA button: ${hasCta ? '✓' : '✗'}`); expect(hasCta).toBeTruthy();
// Page should not be blank // Page should not be blank
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
@ -155,7 +147,7 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
test('02. Playlists vides — message + CTA creer', async ({ page }) => { test('02. Playlists vides — message + CTA creer', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } expect(loggedIn).toBeTruthy();
await navigateTo(page, '/playlists'); await navigateTo(page, '/playlists');
const { hasEmptyState, hasCta } = await assertEmptyState(page, { const { hasEmptyState, hasCta } = await assertEmptyState(page, {
@ -163,42 +155,45 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
ctaPattern: /créer|create|nouvelle|new/i, ctaPattern: /créer|create|nouvelle|new/i,
}); });
console.log(` /playlists empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
console.log(` /playlists CTA button: ${hasCta ? '✓' : '✗'}`); expect(hasCta).toBeTruthy();
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50); expect(body.length).toBeGreaterThan(50);
}); });
test('03. Notifications vides — message approprie', async ({ page }) => { test('03. Notifications vides — message approprie', async ({ page }) => {
await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/notifications'); await navigateTo(page, '/notifications');
const { hasEmptyState } = await assertEmptyState(page, { const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/notification|aucune|no notification/i], expectedTextPatterns: [/notification|aucune|no notification/i],
}); });
console.log(` /notifications empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('04. Feed vide — message + suggestion', async ({ page }) => { test('04. Feed vide — message + suggestion', async ({ page }) => {
await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/feed'); await navigateTo(page, '/feed');
const { hasEmptyState } = await assertEmptyState(page, { const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/feed|follow|suivre|discover|découvr/i], expectedTextPatterns: [/feed|follow|suivre|discover|découvr/i],
}); });
console.log(` /feed empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
// Page should load without crash // Page should load without crash
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('05. Recherche sans resultat — message "aucun resultat"', async ({ page }) => { test('05. Recherche sans resultat — message "aucun resultat"', async ({ page }) => {
await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/search'); await navigateTo(page, '/search');
// Type a query that will return no results // Type a query that will return no results
@ -219,38 +214,39 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
const hasNoResults = /no results|aucun résultat|nothing found|no .* found/i.test(body); const hasNoResults = /no results|aucun résultat|nothing found|no .* found/i.test(body);
console.log(` Search no-results message: ${hasNoResults ? '✓' : '✗'}`); expect(hasNoResults).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('06. Queue vide — message', async ({ page }) => { test('06. Queue vide — message', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } expect(loggedIn).toBeTruthy();
await navigateTo(page, '/queue'); await navigateTo(page, '/queue');
const { hasEmptyState } = await assertEmptyState(page, { const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/queue|file d'attente|no tracks|aucun/i], expectedTextPatterns: [/queue|file d'attente|no tracks|aucun/i],
}); });
console.log(` /queue empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('07. Chat sans conversation — message + CTA', async ({ page }) => { test('07. Chat sans conversation — message + CTA', async ({ page }) => {
await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/chat'); await navigateTo(page, '/chat');
const { hasEmptyState } = await assertEmptyState(page, { const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/chat|conversation|message|channel/i], expectedTextPatterns: [/chat|conversation|message|channel/i],
}); });
console.log(` /chat empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('08. Wishlist vide — message + CTA browse', async ({ page }) => { test('08. Wishlist vide — message + CTA browse', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } expect(loggedIn).toBeTruthy();
await navigateTo(page, '/wishlist'); await navigateTo(page, '/wishlist');
const { hasEmptyState, hasCta } = await assertEmptyState(page, { const { hasEmptyState, hasCta } = await assertEmptyState(page, {
@ -258,20 +254,21 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
ctaPattern: /browse|parcourir|discover|explorer|marketplace/i, ctaPattern: /browse|parcourir|discover|explorer|marketplace/i,
}); });
console.log(` /wishlist empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
console.log(` /wishlist CTA button: ${hasCta ? '✓' : '✗'}`); expect(hasCta).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
test('09. Purchases vides — message', async ({ page }) => { test('09. Purchases vides — message', async ({ page }) => {
await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/purchases'); await navigateTo(page, '/purchases');
const { hasEmptyState } = await assertEmptyState(page, { const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/purchase|achat|order|commande|no .* yet/i], expectedTextPatterns: [/purchase|achat|order|commande|no .* yet/i],
}); });
console.log(` /purchases empty state: ${hasEmptyState ? '✓' : '✗'}`); expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page); await assertNotBroken(page);
}); });
@ -279,7 +276,7 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
// Use creator account for analytics, but a fresh creator would have no data // Use creator account for analytics, but a fresh creator would have no data
// Try fresh user first, fallback to existing creator // Try fresh user first, fallback to existing creator
const loggedIn = await loginAsFreshUser(page); const loggedIn = await loginAsFreshUser(page);
if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } expect(loggedIn).toBeTruthy();
await navigateTo(page, '/analytics'); await navigateTo(page, '/analytics');
const body = await page.textContent('body') || ''; const body = await page.textContent('body') || '';
@ -292,7 +289,6 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states'
// At minimum, the page should not crash // At minimum, the page should not crash
await assertNotBroken(page); await assertNotBroken(page);
console.log(` /analytics empty state text: ${hasEmptyAnalytics ? '✓' : '✗'}`); expect(hasEmptyAnalytics || hasChartArea).toBeTruthy();
console.log(` /analytics chart area: ${hasChartArea ? '✓ (zero-state chart)' : '✗'}`);
}); });
}); });