diff --git a/tests/e2e/05-playlists.spec.ts b/tests/e2e/05-playlists.spec.ts index 2cdbefb49..c3f316eb2 100644 --- a/tests/e2e/05-playlists.spec.ts +++ b/tests/e2e/05-playlists.spec.ts @@ -12,92 +12,73 @@ test.describe('PLAYLISTS — CRUD', () => { const body = await page.textContent('body') || ''; 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 cardCount = await playlistCards.count(); - console.log(` Playlist cards trouvés: ${cardCount}`); + expect(await playlistCards.count()).toBeGreaterThan(0); - // Bouton créer une playlist const createBtn = page.getByRole('button', { 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); - console.log(` Bouton créer playlist: ${visible ? '✓' : '✗'}`); + await expect(createBtn.first()).toBeVisible(); }); test('02. Créer une nouvelle playlist @critical', async ({ page }) => { 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 }) .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i })) .first(); - if (!(await createBtn.isVisible().catch(() => false))) { - console.log(' ⚠ Bouton créer non trouvé'); - return; - } + await expect(createBtn).toBeVisible(); await createBtn.click(); - - // Wait for dialog/form to appear await page.waitForTimeout(500); - // Remplir le formulaire — try label first, then placeholder const nameInput = page.getByLabel(/nom|name|titre|title/i) .or(page.getByPlaceholder(/nom|name|titre/i)) .first(); - if (await nameInput.isVisible().catch(() => false)) { - const playlistName = `E2E Playlist ${Date.now()}`; - await nameInput.fill(playlistName); + await expect(nameInput).toBeVisible(); - // Description si présent - const descInput = page.getByLabel(/description/i).first(); - if (await descInput.isVisible().catch(() => false)) { - await descInput.fill('Créée par les tests E2E'); - } + const playlistName = `E2E Playlist ${Date.now()}`; + await nameInput.fill(playlistName); - // 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 dialogVisible = await dialog.isVisible().catch(() => false); - - let saved = false; - if (dialogVisible) { - const dialogSaveBtn = dialog.getByRole('button', { name: /créer|create|sauvegarder|save|ok/i }).first(); - if (await dialogSaveBtn.isVisible().catch(() => false)) { - await dialogSaveBtn.click(); - saved = true; - } - } - - 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 count = await allSaveBtns.count(); - if (count > 0) { - await allSaveBtns.nth(count - 1).click(); - saved = true; - } - } - - 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 exists = await newCard.isVisible().catch(() => - page.getByText(playlistName).isVisible().catch(() => false) - ); - console.log(` Playlist créée et visible: ${exists ? '✓' : '✗'}`); + const descInput = page.getByLabel(/description/i).first(); + if (await descInput.isVisible().catch(() => false)) { + await descInput.fill('Créée par les tests E2E'); } + + const dialog = page.locator('[role="dialog"], [role="alertdialog"], dialog, [data-state="open"]').first(); + const dialogVisible = await dialog.isVisible().catch(() => false); + + let saved = false; + if (dialogVisible) { + const dialogSaveBtn = dialog.getByRole('button', { name: /créer|create|sauvegarder|save|ok/i }).first(); + if (await dialogSaveBtn.isVisible().catch(() => false)) { + await dialogSaveBtn.click(); + saved = true; + } + } + + if (!saved) { + const allSaveBtns = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok/i }); + const count = await allSaveBtns.count(); + if (count > 0) { + await allSaveBtns.nth(count - 1).click(); + } + } + + await page.waitForTimeout(2_000); + + const newCard = page.locator(`[role="article"][aria-label="Playlist: ${playlistName}"]`); + const exists = await newCard.isVisible().catch(() => + page.getByText(playlistName).isVisible().catch(() => false) + ); + expect(exists).toBeTruthy(); }); test('03. Ouvrir une playlist existante affiche ses tracks', async ({ page }) => { await navigateTo(page, '/playlists'); - // PlaylistCard wraps a Link with href="/playlists/{id}" const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); await page.waitForLoadState('networkidle'); @@ -113,32 +94,23 @@ test.describe('PLAYLISTS — CRUD', () => { await navigateTo(page, '/playlists'); const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); await page.waitForLoadState('networkidle'); - // Bouton éditer const editBtn = page.getByRole('button', { name: /edit|modifier|éditer/i }).first() .or(page.locator('[data-action="edit"]').first()); - if (await editBtn.isVisible().catch(() => false)) { - await editBtn.click(); - console.log(' ✓ Mode édition activé'); - } else { - console.log(' ⚠ Bouton éditer non trouvé'); - } + await expect(editBtn).toBeVisible(); + await editBtn.click(); }); test('05. Supprimer une playlist', async ({ page }) => { await navigateTo(page, '/playlists'); const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); await page.waitForLoadState('networkidle'); @@ -146,8 +118,7 @@ test.describe('PLAYLISTS — CRUD', () => { const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).first() .or(page.locator('[data-action="delete"]').first()); - const visible = await deleteBtn.isVisible().catch(() => false); - console.log(` Bouton supprimer: ${visible ? '✓ visible' : '✗ absent'}`); + await expect(deleteBtn).toBeVisible(); }); }); @@ -159,43 +130,33 @@ test.describe('PLAYLISTS — Collaboration', () => { test('06. Option d\'invitation de collaborateurs', async ({ page }) => { 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(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); await page.waitForLoadState('networkidle'); - // Chercher option de collaboration / partage const collabBtn = page.getByRole('button', { name: /collabor|inviter|invite|partager|share/i }).first(); - const visible = await collabBtn.isVisible().catch(() => false); - console.log(` Bouton collaboration/partage: ${visible ? '✓' : '✗'}`); + await expect(collabBtn).toBeVisible(); }); test('07. Export playlist (JSON/CSV/M3U)', async ({ page }) => { await navigateTo(page, '/playlists'); const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); await page.waitForLoadState('networkidle'); - // Menu d'options const moreBtn = page.getByRole('button', { name: /more|options|⋯|…|menu/i }).first() .or(page.locator('[class*="more-button"], [class*="kebab"]').first()); - if (await moreBtn.isVisible().catch(() => false)) { - await moreBtn.click(); + await expect(moreBtn).toBeVisible(); + await moreBtn.click(); - const exportOption = page.getByRole('menuitem', { name: /export|télécharger|download/i }); - const visible = await exportOption.isVisible().catch(() => false); - console.log(` Option export: ${visible ? '✓' : '✗'}`); - } + const exportOption = page.getByRole('menuitem', { name: /export|télécharger|download/i }); + await expect(exportOption).toBeVisible(); }); }); @@ -205,16 +166,12 @@ test.describe('PLAYLISTS — Drag & Drop', () => { await navigateTo(page, '/playlists'); const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - return; - } + await expect(playlistLink).toBeVisible(); await playlistLink.click(); 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 count = await dragHandles.count(); - console.log(` Drag handles trouvés: ${count}`); + expect(await dragHandles.count()).toBeGreaterThan(0); }); }); diff --git a/tests/e2e/06-search-discover.spec.ts b/tests/e2e/06-search-discover.spec.ts index 89f8e65d1..1e9750fe0 100644 --- a/tests/e2e/06-search-discover.spec.ts +++ b/tests/e2e/06-search-discover.spec.ts @@ -3,7 +3,6 @@ import { loginViaAPI, CONFIG, navigateTo, SELECTORS } from './helpers'; /** * 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) { 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 }) => { 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"]') .or(page.locator(SELECTORS.searchInput)); 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'); const pageSearch = await findSearchInput(page); const pageSearchVisible = await pageSearch.isVisible().catch(() => false); - console.log(` Search page input: ${pageSearchVisible ? '✓' : '✗'}`); // At least one of the two search inputs should be accessible expect(headerVisible || pageSearchVisible).toBeTruthy(); }); 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'); const searchInput = await findSearchInput(page); - if (!(await searchInput.isVisible().catch(() => false))) { - return; - } + await expect(searchInput).toBeVisible(); await searchInput.fill('test'); - // useSearchPage debounces at 500ms, wait for results await page.waitForTimeout(1_500); - // Results should appear (SearchPageResults with tabs) or empty state (SearchPageEmpty) const body = await page.textContent('body') || ''; const hasResults = body.length > 500; 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'); const searchInput = await findSearchInput(page); - if (!(await searchInput.isVisible().catch(() => false))) { - return; - } + await expect(searchInput).toBeVisible(); await searchInput.fill('tes'); - // SearchPageHeader debounces suggestions at 300ms await page.waitForTimeout(1_000); - // Dropdown suggestions use role="listbox" (SearchPageHeader.tsx) const suggestions = page.locator('[role="listbox"]'); - const visible = await suggestions.isVisible().catch(() => false); - console.log(` Autocomplete: ${visible ? '✓ dropdown visible' : '✗ pas de suggestions'}`); + await expect(suggestions).toBeVisible(); }); test('04. Les résultats de recherche sont catégorisés (tabs: All, Tracks, Artists, Playlists)', async ({ page }) => { await navigateTo(page, '/search'); const searchInput = await findSearchInput(page); - if (!(await searchInput.isVisible().catch(() => false))) { - return; - } + await expect(searchInput).toBeVisible(); await searchInput.fill('music'); - // Wait for debounce (500ms) + network 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']; for (const tabName of expectedTabs) { const tab = page.getByRole('tab', { name: new RegExp(tabName, 'i') }); - const visible = await tab.isVisible().catch(() => false); - if (visible) console.log(` Tab "${tabName}": ✓`); + await expect(tab).toBeVisible(); } }); test('05. Recherche vide ne crash pas', async ({ page }) => { await navigateTo(page, '/search'); - // With empty query, useSearchPage shows SearchPageDiscovery (trending tags, etc.) const searchInput = await findSearchInput(page); - if (!(await searchInput.isVisible().catch(() => false))) { - return; - } + await expect(searchInput).toBeVisible(); await searchInput.fill(''); 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 }) => { - // useSearchPage reads query from ?q= URL param await navigateTo(page, '/search?q=test'); - - // Wait for debounce + search await page.waitForTimeout(1_500); 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.length).toBeGreaterThan(50); }); @@ -139,130 +109,83 @@ test.describe('DISCOVER — Exploration éthique', () => { test('06. Page /discover affiche les genres @critical', async ({ page }) => { await navigateTo(page, '/discover'); - // DiscoverPage shows a heading "Découvrir" or "Discover" const heading = page.getByRole('heading', { name: /découvrir|discover/i }); - const hasMainHeading = await heading.first().isVisible().catch(() => false); - console.log(` Discover heading: ${hasMainHeading ? '✓' : '✗'}`); + await expect(heading.first()).toBeVisible(); - // 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') }); let genreCount = await genreButtons.count(); - // Fallback: look for any genre-like buttons (with gradient bg or genre text) 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 }); 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') || ''; 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 }) => { 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') }); + 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 page.waitForLoadState('networkidle'); + await genreButtons.first().click(); + 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 hasBack = await backBtn.isVisible().catch(() => false); - console.log(` Genre "${genreName}" sélectionné, bouton retour: ${hasBack ? '✓' : '✗'}`); + const backBtn = page.getByRole('button', { name: /retour/i }); + await expect(backBtn).toBeVisible(); - // Content should be present (tracks grid or empty message) - const body = await page.textContent('body') || ''; - expect(body.length).toBeGreaterThan(200); - } + const body = await page.textContent('body') || ''; + expect(body.length).toBeGreaterThan(200); }); test('08. Playlists éditoriales affichées sur /discover', async ({ page }) => { await navigateTo(page, '/discover'); - // DiscoverPage shows editorial playlists section with heading "Playlists éditoriales" const editorialHeading = page.getByRole('heading', { name: /playlists éditoriales/i }); - const visible = await editorialHeading.isVisible().catch(() => false); - console.log(` Section playlists éditoriales: ${visible ? '✓' : '✗'}`); + await expect(editorialHeading).toBeVisible(); - if (visible) { - // Editorial playlists use PlaylistCard components with role="article" aria-label="Playlist: ..." - const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]'); - const count = await editorialCards.count(); - console.log(` Playlists éditoriales trouvées: ${count}`); - } + const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]'); + expect(await editorialCards.count()).toBeGreaterThanOrEqual(0); }); test('09. Pas de sections "trending" ou "for you" (design éthique)', async ({ page }) => { await navigateTo(page, '/discover'); - // Verify no algorithmic/trending/recommendation sections exist const body = await page.textContent('body') || ''; 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 }) => { 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"]') .filter({ hasText: /\d+\s*(plays?|écoutes?|likes?|vues?)/i }); - const count = await publicCounters.count(); - 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'); - } + expect(await publicCounters.count()).toBe(0); }); test('11. Bouton retour depuis genre revient à la liste des genres', async ({ page }) => { await navigateTo(page, '/discover'); - // Click a genre to navigate into it 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 page.waitForLoadState('networkidle'); - // Click the "Retour" button (goBack clears searchParams) const backBtn = page.getByRole('button', { name: /retour/i }); - if (await backBtn.isVisible().catch(() => false)) { - await backBtn.click(); - await page.waitForTimeout(500); + await expect(backBtn).toBeVisible(); + await backBtn.click(); + 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 visible = await genreHeading.isVisible().catch(() => false); - console.log(` Retour à la liste genres: ${visible ? '✓' : '✗'}`); - } + const genreHeading = page.getByRole('heading', { name: /par genre/i }); + await expect(genreHeading).toBeVisible(); }); }); diff --git a/tests/e2e/07-social.spec.ts b/tests/e2e/07-social.spec.ts index 45851a730..c5a357cc2 100644 --- a/tests/e2e/07-social.spec.ts +++ b/tests/e2e/07-social.spec.ts @@ -7,34 +7,26 @@ test.describe('SOCIAL — Follow/Unfollow', () => { }); 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 page.waitForLoadState('networkidle'); - // FollowButton renders "Suivre" (unfollowed) or "Abonne" (followed) const followBtn = page.getByRole('button', { name: /suivre|abonné|abonnement/i }).first(); - const visible = await followBtn.isVisible().catch(() => false); - console.log(` Bouton follow: ${visible ? '✓' : '✗'}`); + await expect(followBtn).toBeVisible(); }); test('02. Follow toggle fonctionne', async ({ page }) => { - // Navigate directly to a known artist profile await navigateTo(page, '/u/marcus_beats'); 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(); + await expect(followBtn).toBeVisible(); - if (await followBtn.isVisible().catch(() => false)) { - const initialText = await followBtn.textContent(); - await followBtn.click(); - await page.waitForTimeout(1_500); - const newText = await followBtn.textContent(); + const initialText = await followBtn.textContent(); + await followBtn.click(); + await page.waitForTimeout(1_500); + const newText = await followBtn.textContent(); - console.log(` Follow toggle: "${initialText?.trim()}" → "${newText?.trim()}" ${initialText !== newText ? '✓' : '✗'}`); - } else { - console.log(' ⚠ Bouton follow non visible'); - } + expect(newText?.trim()).not.toBe(initialText?.trim()); }); }); @@ -48,15 +40,10 @@ test.describe('SOCIAL — Profils', () => { const body = await page.textContent('body') || ''; 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 avatarVisible = await avatar.isVisible().catch(() => false); - console.log(` Avatar: ${avatarVisible ? '✓' : '✗'}`); + await expect(avatar).toBeVisible(); }); 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()); const nameField = page.getByLabel(/nom.*affichage|display.*name|nom/i).first(); - const hasBio = await bioField.isVisible().catch(() => false); - const hasName = await nameField.isVisible().catch(() => false); - - console.log(` Champ bio: ${hasBio ? '✓' : '✗'}`); - console.log(` Champ display name: ${hasName ? '✓' : '✗'}`); + await expect(bioField).toBeVisible(); + await expect(nameField).toBeVisible(); }); 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 page.waitForLoadState('networkidle'); - // Listening history must NOT be visible on someone else's public profile const body = await page.textContent('body') || ''; expect(body).not.toMatch(/historique.*écoute|listening.*history|recently.*played/i); }); @@ -88,17 +70,9 @@ test.describe('SOCIAL — Profils', () => { await page.waitForLoadState('networkidle'); const body = await page.textContent('body') || ''; - - // UserProfilePageHeader displays stats: Tracks, Playlists, Followers, Following - const hasTracksLabel = body.includes('Tracks'); - 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 ? '✓' : '✗'}`); + expect(body).toContain('Tracks'); + expect(body).toContain('Followers'); + expect(body).toContain(CONFIG.users.creator.username); }); }); @@ -113,25 +87,14 @@ test.describe('SOCIAL — Social Hub', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error/); 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 }) => { await navigateTo(page, '/social'); - // SocialViewSidebar has buttons: "Fresh Tracks", "Explore", "Communities" - const freshTracksBtn = page.getByRole('button', { name: /fresh tracks/i }); - const exploreBtn = page.getByRole('button', { name: /explore/i }); - 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 ? '✓' : '✗'}`); + await expect(page.getByRole('button', { name: /fresh tracks/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /explore/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /communities/i })).toBeVisible(); }); test('09. Page feed se charge', async ({ page }) => { @@ -140,7 +103,5 @@ test.describe('SOCIAL — Social Hub', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error/); expect(body.length).toBeGreaterThan(100); - - console.log(' Page /feed chargée avec succès'); }); }); diff --git a/tests/e2e/08-marketplace.spec.ts b/tests/e2e/08-marketplace.spec.ts index 22732fab6..c478e8922 100644 --- a/tests/e2e/08-marketplace.spec.ts +++ b/tests/e2e/08-marketplace.spec.ts @@ -13,86 +13,59 @@ test.describe('MARKETPLACE — Navigation', () => { expect(body).not.toMatch(/500|Internal Server Error/); expect(body.length).toBeGreaterThan(50); - // MarketplacePage renders heading "Marketplace" const heading = page.locator('h1').filter({ hasText: /marketplace/i }); - const hasHeading = await heading.isVisible().catch(() => false); - console.log(` Heading Marketplace: ${hasHeading ? '✓' : '✗'}`); + await expect(heading).toBeVisible(); }); test('02. Les produits (beats/samples) s\'affichent', async ({ page }) => { await navigateTo(page, '/marketplace'); - // ProductCard wraps in
const products = page.locator('article[aria-label^="Product:"]'); - const count = await products.count(); - console.log(` Produits affichés: ${count}`); + expect(await products.count()).toBeGreaterThan(0); }); test('03. Filtres marketplace fonctionnent', async ({ page }) => { await navigateTo(page, '/marketplace'); - // Search input in the filters bar const searchInput = page.getByPlaceholder(/search tracks|search/i).first(); - const hasSearch = await searchInput.isVisible().catch(() => false); - console.log(` Champ recherche: ${hasSearch ? '✓' : '✗'}`); + await expect(searchInput).toBeVisible(); - // Filters button const filtersBtn = page.getByRole('button', { name: /filters/i }).first(); - const hasFilters = await filtersBtn.isVisible().catch(() => false); - console.log(` Bouton Filters: ${hasFilters ? '✓' : '✗'}`); + await expect(filtersBtn).toBeVisible(); - // Cart button const cartBtn = page.getByRole('button', { name: /cart/i }).first(); - const hasCart = await cartBtn.isVisible().catch(() => false); - console.log(` Bouton Cart: ${hasCart ? '✓' : '✗'}`); + await expect(cartBtn).toBeVisible(); }); test('04. Page détail d\'un produit se charge', async ({ page }) => { await navigateTo(page, '/marketplace'); - // ProductCard has "Buy Now" button — check if products exist first const products = page.locator('article[aria-label^="Product:"]'); 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(); - if (await productLink.isVisible().catch(() => false)) { - await productLink.click(); - await page.waitForLoadState('networkidle'); + await expect(productLink).toBeVisible(); + await productLink.click(); + await page.waitForLoadState('networkidle'); - const body = await page.textContent('body') || ''; - 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)'); - } + const body = await page.textContent('body') || ''; + expect(body).not.toMatch(/500|Internal Server Error/); }); test('05. Bouton Buy Now et Add to Cart présents', async ({ page }) => { 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 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) - const firstProduct = page.locator('article[aria-label^="Product:"]').first(); - 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 ? '✓' : '✗'}`); + await expect(buyBtn).toBeVisible(); + await expect(addToCartBtn).toBeVisible(); }); }); @@ -102,14 +75,11 @@ test.describe('MARKETPLACE — Dashboard vendeur', () => { }); test('06. Dashboard vendeur accessible @critical', async ({ page }) => { - // Seller dashboard is at /sell await navigateTo(page, '/sell'); const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error/); 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 }) => { - // Wishlist is at /wishlist await navigateTo(page, '/wishlist'); const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error/); 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 }) => { - // Purchases page is at /purchases await navigateTo(page, '/purchases'); const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|Internal Server Error/); 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 }) => { 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(); - if (await cartBtn.isVisible().catch(() => false)) { - await cartBtn.click(); - await page.waitForTimeout(500); + await expect(cartBtn).toBeVisible(); + await cartBtn.click(); + 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) - console.log(' Cart panel ouvert'); - } else { - console.log(' ⚠ Bouton Cart non visible'); - } + // Cart panel should show something (empty cart message or items) + const body = await page.textContent('body') || ''; + expect(body).toMatch(/cart|panier|empty|vide|item/i); }); test('10. Ajouter un produit au cart affiche un feedback', async ({ page }) => { await navigateTo(page, '/marketplace'); const firstProduct = page.locator('article[aria-label^="Product:"]').first(); - if (!(await firstProduct.isVisible().catch(() => false))) { - console.log(' ⚠ Aucun produit disponible'); - return; - } + test.skip(!(await firstProduct.isVisible().catch(() => false)), 'No products available'); - // Hover to reveal "Add to Cart" button (hidden by default with opacity-0) await firstProduct.hover(); await page.waitForTimeout(500); const addToCartBtn = firstProduct.getByRole('button', { name: /add to cart/i }); - if (await addToCartBtn.isVisible().catch(() => false)) { - await addToCartBtn.click(); - await page.waitForTimeout(1_000); + await expect(addToCartBtn).toBeVisible(); + await addToCartBtn.click(); + await page.waitForTimeout(1_000); - // Toast feedback: "{title} added to cart" - const toast = page.getByTestId('toast-alert').first(); - const hasToast = await toast.isVisible().catch(() => false); - 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'); - } + const toast = page.getByTestId('toast-alert').first(); + await expect(toast).toBeVisible(); }); }); diff --git a/tests/e2e/09-chat-notifications-settings.spec.ts b/tests/e2e/09-chat-notifications-settings.spec.ts index e89f207dc..860eb3c4a 100644 --- a/tests/e2e/09-chat-notifications-settings.spec.ts +++ b/tests/e2e/09-chat-notifications-settings.spec.ts @@ -15,65 +15,42 @@ test.describe('CHAT — Messagerie', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|error|crash/i); - // Chat page should show either conversation list (Channels header) or auth prompt - const hasContent = body.length > 100; - expect(hasContent).toBeTruthy(); - console.log(' Chat page loaded at /chat'); + expect(body.length).toBeGreaterThan(100); }); test('02. Sidebar avec liste des conversations (Channels)', async ({ page }) => { await navigateTo(page, '/chat'); - // ChatPage renders a sidebar card with heading "Channels" const channelsHeading = page.getByText('Channels', { exact: true }); - const visible = await channelsHeading.isVisible().catch(() => false); - console.log(` Channels sidebar heading: ${visible ? '✓' : '✗'}`); + await expect(channelsHeading).toBeVisible(); - // Also check for ChatSidebar component presence const sidebar = page.locator('[class*="w-80"]'); - const sidebarVisible = await sidebar.first().isVisible().catch(() => false); - console.log(` Chat sidebar panel: ${sidebarVisible ? '✓' : '✗'}`); + await expect(sidebar.first()).toBeVisible(); }); test('03. Champ de saisie de message visible', async ({ page }) => { await navigateTo(page, '/chat'); - // ChatInput has aria-label="Type a message" and placeholder containing "Broadcast message" const msgInput = page.getByLabel('Type a message') .or(page.getByPlaceholder(/broadcast message|écrire dans/i)) .or(page.locator('input[type="text"][aria-label="Type a message"]')); - const visible = await msgInput.first().isVisible().catch(() => false); - console.log(` Input message: ${visible ? '✓' : '✗'}`); + await expect(msgInput.first()).toBeVisible(); }); test('04. Boutons attach/emoji/send présents', async ({ page }) => { await navigateTo(page, '/chat'); - // Attach file button - const attachBtn = page.getByLabel('Attach file'); - const hasAttach = await attachBtn.isVisible().catch(() => false); - 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 ? '✓' : '✗'}`); + await expect(page.getByLabel('Attach file')).toBeVisible(); + await expect(page.getByLabel(/add emoji|close emoji/i)).toBeVisible(); + await expect(page.getByLabel('Send message')).toBeVisible(); }); test('05. WebSocket status indicator visible', async ({ page }) => { 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 visible = await statusDot.first().isVisible().catch(() => false); - console.log(` WS status indicator: ${visible ? '✓' : '✗'}`); + await expect(statusDot.first()).toBeVisible(); }); }); @@ -89,11 +66,8 @@ test.describe('NOTIFICATIONS — Centre de notifications', () => { test('06. Bouton notifications (bell) visible dans le header @critical', async ({ page }) => { await navigateTo(page, '/dashboard'); - // NotificationMenuTrigger has aria-label="Notifications" with a Bell icon const notifBtn = page.getByRole('button', { name: 'Notifications' }); - const visible = await notifBtn.isVisible().catch(() => false); - expect(visible).toBeTruthy(); - console.log(` Bell notifications button: ${visible ? '✓' : '✗'}`); + await expect(notifBtn).toBeVisible(); }); test('07. Page /notifications se charge', async ({ page }) => { @@ -102,57 +76,35 @@ test.describe('NOTIFICATIONS — Centre de notifications', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|error|crash/i); - // NotificationsPageHeader renders an h1 with text "Notifications" const heading = page.getByRole('heading', { name: /notifications/i }); - const hasHeading = await heading.first().isVisible().catch(() => false); - console.log(` Notifications heading: ${hasHeading ? '✓' : '✗'}`); + await expect(heading.first()).toBeVisible(); }); test('08. Bouton "Mark All as Read" présent si notifications non lues', async ({ page }) => { 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 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 }) => { await navigateTo(page, '/settings'); - // SettingsTabs has a tab trigger "Notifications" — try exact and partial match 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" - 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 notifTab.first().click(); await page.waitForTimeout(500); - // NotificationSettings renders checkboxes for email/push preferences const emailNotifCheckbox = page.locator('#email_notifications'); - const hasEmailPref = await emailNotifCheckbox.isVisible().catch(() => false); - console.log(` Email notifications checkbox: ${hasEmailPref ? '✓' : '✗'}`); + await expect(emailNotifCheckbox).toBeVisible(); const pushNotifCheckbox = page.locator('#push_notifications'); - const hasPushPref = await pushNotifCheckbox.isVisible().catch(() => false); - console.log(` Push notifications checkbox: ${hasPushPref ? '✓' : '✗'}`); + await expect(pushNotifCheckbox).toBeVisible(); }); }); @@ -169,15 +121,11 @@ test.describe('SETTINGS — Paramètres', () => { await navigateTo(page, '/settings'); 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); - // SettingsPage renders heading "System Config" const heading = page.getByRole('heading', { name: /system config/i }); - const hasHeading = await heading.isVisible().catch(() => false); - console.log(` Settings heading: ${hasHeading ? '✓' : '✗'}`); + await expect(heading).toBeVisible(); - // SettingsTabs renders tab triggers — names may be in French or English const tabPatterns: [string, RegExp][] = [ ['Account', /account|compte/i], ['Preferences', /pr[ée]f[ée]rences|preferences/i], @@ -185,173 +133,110 @@ test.describe('SETTINGS — Paramètres', () => { ['Privacy', /confidentialit[ée]|privacy/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 vis = await tab.isVisible().catch(() => false); - console.log(` Tab "${label}": ${vis ? '✓' : '✗'}`); + await expect(tab).toBeVisible(); } }); test('11. Tab Account — password change form present', async ({ page }) => { 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 visible = await changePasswordTitle.first().isVisible().catch(() => false); - console.log(` "Change Password" section: ${visible ? '✓' : '✗'}`); + await expect(changePasswordTitle.first()).toBeVisible(); - // Check for password fields by their HTML ids - const currentPwd = page.locator('#current-password'); - const newPwd = page.locator('#new-password'); - 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 ? '✓' : '✗'}`); + await expect(page.locator('#current-password')).toBeVisible(); + await expect(page.locator('#new-password')).toBeVisible(); + await expect(page.locator('#confirm-password')).toBeVisible(); }); test('12. Tab Account — 2FA section present', async ({ page }) => { await navigateTo(page, '/settings'); - // TwoFactorSettings renders "Two-Factor Authentication (2FA)" title const twoFactorTitle = page.getByText('Two-Factor Authentication (2FA)'); - const visible = await twoFactorTitle.isVisible().catch(() => false); - console.log(` 2FA section: ${visible ? '✓' : '✗'}`); + await expect(twoFactorTitle).toBeVisible(); - // Should show either "2FA is enabled" or "2FA is not enabled" const statusText = page.getByText(/2FA is (enabled|not enabled)/); - const hasStatus = await statusText.first().isVisible().catch(() => false); - console.log(` 2FA status displayed: ${hasStatus ? '✓' : '✗'}`); + await expect(statusText.first()).toBeVisible(); }); test('13. Tab Account — data export button (GDPR)', async ({ page }) => { await navigateTo(page, '/settings'); - // AccountSettingsExportCard renders "Data Export" title and "Export My Data" button const exportTitle = page.getByText('Data Export', { exact: true }); - const hasTitleVisible = await exportTitle.first().isVisible().catch(() => false); - console.log(` "Data Export" section: ${hasTitleVisible ? '✓' : '✗'}`); + await expect(exportTitle.first()).toBeVisible(); const exportBtn = page.getByRole('button', { name: /export my data/i }); - const hasBtn = await exportBtn.isVisible().catch(() => false); - console.log(` "Export My Data" button: ${hasBtn ? '✓' : '✗'}`); + await expect(exportBtn).toBeVisible(); }); test('14. Tab Account — delete account button with warning', async ({ page }) => { await navigateTo(page, '/settings'); - // AccountSettingsDeleteCard renders "Delete Account" title const deleteTitle = page.getByText('Delete Account').first(); - const hasTitle = await deleteTitle.isVisible().catch(() => false); - console.log(` "Delete Account" section: ${hasTitle ? '✓' : '✗'}`); + await expect(deleteTitle).toBeVisible(); - // Warning text: "This action cannot be undone" const warningText = page.getByText(/this action cannot be undone/i); - const hasWarning = await warningText.first().isVisible().catch(() => false); - console.log(` Warning text present: ${hasWarning ? '✓' : '✗'}`); + await expect(warningText.first()).toBeVisible(); - // Delete button (we do NOT click it) const deleteBtn = page.getByRole('button', { name: /delete account/i }); - const hasBtnVisible = await deleteBtn.isVisible().catch(() => false); - console.log(` "Delete Account" button: ${hasBtnVisible ? '✓' : '✗'}`); + await expect(deleteBtn).toBeVisible(); }); test('15. Tab Preferences — theme radio group', async ({ page }) => { 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(); - if (!(await prefsTab.isVisible({ timeout: 3000 }).catch(() => false))) { - console.log(' ⚠ Preferences tab not found — skipping'); - return; - } - await prefsTab.click({ timeout: 3000 }).catch(() => { - console.log(' ⚠ Could not click Preferences tab — skipping'); - }); + await expect(prefsTab).toBeVisible(); + await prefsTab.click(); await page.waitForTimeout(500); - // PreferenceSettings has a RadioGroup for theme with items: light, dark, auto - const themeLight = page.locator('#theme-light'); - const themeDark = page.locator('#theme-dark'); - 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 ? '✓' : '✗'}`); + await expect(page.locator('#theme-light')).toBeVisible(); + await expect(page.locator('#theme-dark')).toBeVisible(); + await expect(page.locator('#theme-auto')).toBeVisible(); }); test('16. Tab Preferences — language selector', async ({ page }) => { await navigateTo(page, '/settings'); const prefsTab = page.getByRole('tab', { name: /pr[ée]f[ée]rences|preferences/i }).first(); - if (!(await prefsTab.isVisible({ timeout: 3000 }).catch(() => false))) { - console.log(' ⚠ Preferences tab not found — skipping'); - return; - } - await prefsTab.click({ timeout: 3000 }).catch(() => { - console.log(' ⚠ Could not click Preferences tab — skipping'); - }); + await expect(prefsTab).toBeVisible(); + await prefsTab.click(); await page.waitForTimeout(500); - // PreferenceSettings has a Select with name="language" const langSelect = page.locator('[name="language"]') .or(page.locator('select[name="language"]')); - const visible = await langSelect.first().isVisible().catch(() => false); - console.log(` Language selector: ${visible ? '✓' : '✗'}`); + await expect(langSelect.first()).toBeVisible(); }); test('17. Tab Privacy — confidentiality settings', async ({ page }) => { await navigateTo(page, '/settings'); - // Click the Confidentialite/Privacy tab const privacyTab = page.getByRole('tab', { name: /confidentialit[ée]|privacy/i }).first(); - if (!(await privacyTab.isVisible({ timeout: 3000 }).catch(() => false))) { - console.log(' ⚠ Privacy tab not found — skipping'); - return; - } - await privacyTab.click({ timeout: 3000 }).catch(() => { - console.log(' ⚠ Could not click Privacy tab — skipping'); - }); + await expect(privacyTab).toBeVisible(); + await privacyTab.click(); await page.waitForTimeout(500); - // PrivacySettings and ProfileVisibilityCard should render const body = await page.textContent('body') || ''; - const hasPrivacyContent = /profil|privacy|visibility|visibilit/i.test(body); - console.log(` Privacy content loaded: ${hasPrivacyContent ? '✓' : '✗'}`); + expect(body).toMatch(/profil|privacy|visibility|visibilit/i); }); test('18. Tab Playback — audio quality and crossfade', async ({ page }) => { await navigateTo(page, '/settings'); - // Click the Playback tab const playbackTab = page.getByRole('tab', { name: /playback|lecture/i }).first(); - if (!(await playbackTab.isVisible({ timeout: 3000 }).catch(() => false))) { - console.log(' ⚠ Playback tab not found — skipping'); - return; - } - await playbackTab.click({ timeout: 3000 }).catch(() => { - console.log(' ⚠ Could not click Playback tab — skipping'); - }); + await expect(playbackTab).toBeVisible(); + await playbackTab.click(); await page.waitForTimeout(500); const body = await page.textContent('body') || ''; - const hasPlaybackContent = /quality|crossfade|autoplay|volume/i.test(body); - console.log(` Playback settings loaded: ${hasPlaybackContent ? '✓' : '✗'}`); + expect(body).toMatch(/quality|crossfade|autoplay|volume/i); }); test('19. Save Config button visible', async ({ page }) => { await navigateTo(page, '/settings'); - // SettingsPage renders a "Save Config" button const saveBtn = page.getByRole('button', { name: /save config/i }); - const visible = await saveBtn.isVisible().catch(() => false); - console.log(` "Save Config" button: ${visible ? '✓' : '✗'}`); + await expect(saveBtn).toBeVisible(); }); }); diff --git a/tests/e2e/10-features.spec.ts b/tests/e2e/10-features.spec.ts index fbc1ccf1a..dc9c727f1 100644 --- a/tests/e2e/10-features.spec.ts +++ b/tests/e2e/10-features.spec.ts @@ -15,15 +15,13 @@ test.describe('ANALYTICS — Créateur', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|error|crash/i); - console.log(' Analytics page loaded at /analytics'); }); test('02. Graphiques/charts s\'affichent', async ({ page }) => { await navigateTo(page, '/analytics'); const charts = page.locator('canvas, svg[class*="chart"], [class*="recharts"], [class*="Chart"]'); - const count = await charts.count(); - console.log(` Graphiques trouvés: ${count}`); + expect(await charts.count()).toBeGreaterThan(0); }); 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('[class*="date-range"], [class*="period"]')); - const visible = await periodSelector.first().isVisible().catch(() => false); - console.log(` Sélecteur de période: ${visible ? '✓' : '✗'}`); + await expect(periodSelector.first()).toBeVisible(); }); }); @@ -52,27 +49,22 @@ test.describe('SUBSCRIPTIONS — Abonnements', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|error|crash/i); - console.log(' Subscription page loaded at /subscription'); }); test('05. Les plans sont affichés', async ({ page }) => { await navigateTo(page, '/subscription'); const body = await page.textContent('body') || ''; - - const plans = ['free', 'creator', 'premium']; - for (const plan of plans) { - const found = new RegExp(plan, 'i').test(body); - console.log(` Plan ${plan}: ${found ? '✓' : '✗'}`); - } + expect(body).toMatch(/free/i); + expect(body).toMatch(/creator/i); + expect(body).toMatch(/premium/i); }); test('06. Prix affichés correctement', async ({ page }) => { await navigateTo(page, '/subscription'); const body = await page.textContent('body') || ''; - const hasPricing = /\$\d+\.\d{2}|\d+[,\.]\d{2}\s*€/i.test(body); - console.log(` Prix affichés: ${hasPricing ? '✓' : '✗'}`); + expect(body).toMatch(/\$\d+\.\d{2}|\d+[,\.]\d{2}\s*€/i); }); }); @@ -89,9 +81,7 @@ test.describe('ADMIN — Dashboard', () => { await navigateTo(page, '/admin'); 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); - console.log(' Admin dashboard loaded at /admin'); }); test('08. Modération accessible à /admin/moderation', async ({ page }) => { @@ -99,7 +89,6 @@ test.describe('ADMIN — Dashboard', () => { const body = await page.textContent('body') || ''; 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 }) => { @@ -107,7 +96,6 @@ test.describe('ADMIN — Dashboard', () => { const body = await page.textContent('body') || ''; 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 }) => { @@ -115,45 +103,33 @@ test.describe('ADMIN — Dashboard', () => { const body = await page.textContent('body') || ''; 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 }) => { await navigateTo(page, '/admin/roles'); 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); - console.log(' Admin roles loaded at /admin/roles'); }); test('12. Admin non accessible pour un user normal', async ({ page }) => { 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.waitForTimeout(1_000); await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); 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.waitForLoadState('domcontentloaded').catch(() => {}); 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 currentUrl = page.url(); const isRedirected = !currentUrl.includes('/admin'); const isBlockedByMessage = /403|forbidden|accès.*refusé|unauthorized|not authorized|access denied/i.test(body); - const isBlocked = isRedirected || isBlockedByMessage; - // 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})`); + expect(isRedirected || isBlockedByMessage).toBeTruthy(); }); }); @@ -168,7 +144,6 @@ test.describe('LIVE — Streaming', () => { const body = await page.textContent('body') || ''; 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 }) => { @@ -176,11 +151,8 @@ test.describe('LIVE — Streaming', () => { await navigateTo(page, '/live/go-live'); 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); - // Look for RTMP or stream key related content - 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'}`); + expect(body).toMatch(/rtmp|stream.*key|clé|go.*live|broadcast/i); }); }); @@ -194,9 +166,7 @@ test.describe('CLOUD — Stockage', () => { await navigateTo(page, '/cloud'); 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); - console.log(' Cloud page loaded at /cloud'); }); 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 }) .or(page.locator('input[type="file"]')); - const visible = await uploadBtn.first().isVisible().catch(() => false); - console.log(` Upload zone/button: ${visible ? '✓' : '✗'}`); + await expect(uploadBtn.first()).toBeVisible(); }); }); @@ -222,7 +191,6 @@ test.describe('EDUCATION — Cours', () => { const body = await page.textContent('body') || ''; 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') || ''; 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'); 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); - console.log(' Developer page loaded at /developer'); }); test('20. Page /webhooks accessible', async ({ page }) => { @@ -262,6 +227,5 @@ test.describe('DEVELOPER — API publique', () => { const body = await page.textContent('body') || ''; expect(body).not.toMatch(/500|error|crash/i); - console.log(' Webhooks page loaded at /webhooks'); }); }); diff --git a/tests/e2e/17-modals-dialogs.spec.ts b/tests/e2e/17-modals-dialogs.spec.ts index a79520f19..ba155b497 100644 --- a/tests/e2e/17-modals-dialogs.spec.ts +++ b/tests/e2e/17-modals-dialogs.spec.ts @@ -25,11 +25,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { test('01. Cliquer sur l\'avatar ouvre le menu utilisateur', async ({ page }) => { // The user menu trigger has data-testid="user-menu" in Header.tsx const userMenuTrigger = page.getByTestId('user-menu'); - - if (!(await userMenuTrigger.isVisible().catch(() => false))) { - console.log(' User menu trigger not found — skipping'); - return; - } + await expect(userMenuTrigger).toBeVisible(); await userMenuTrigger.click(); await page.waitForTimeout(CONFIG.timeouts.animation); @@ -46,48 +42,36 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const menuOpened = profileVisible || settingsVisible || signOutVisible; 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 }) => { const userMenuTrigger = page.getByTestId('user-menu'); - if (!(await userMenuTrigger.isVisible().catch(() => false))) return; + await expect(userMenuTrigger).toBeVisible(); await userMenuTrigger.click(); await page.waitForTimeout(CONFIG.timeouts.animation); // Verify menu is open — the dropdown contains a link to /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 await page.keyboard.press('Escape'); await page.waitForTimeout(CONFIG.timeouts.animation); // Menu should be closed - const stillOpen = await profileLink.isVisible().catch(() => false); - if (wasOpen) { - expect(stillOpen).toBeFalsy(); - console.log(' Escape closed user menu'); - } else { - console.log(' Menu was not open to begin with'); - } + await expect(profileLink).not.toBeVisible(); }); test('03. Cliquer en dehors ferme le menu', async ({ page }) => { const userMenuTrigger = page.getByTestId('user-menu'); - if (!(await userMenuTrigger.isVisible().catch(() => false))) return; + await expect(userMenuTrigger).toBeVisible(); await userMenuTrigger.click(); await page.waitForTimeout(CONFIG.timeouts.animation); const profileLink = page.locator('a[href="/profile"]'); - const wasOpen = await profileLink.isVisible().catch(() => false); - - if (!wasOpen) { - console.log(' Menu was not open to begin with — skipping'); - return; - } + await expect(profileLink).toBeVisible(); // 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. @@ -95,9 +79,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { await page.keyboard.press('Escape'); await page.waitForTimeout(CONFIG.timeouts.animation); - const stillOpen = await profileLink.isVisible().catch(() => false); - expect(stillOpen).toBeFalsy(); - console.log(' Click outside / Escape closed user menu'); + await expect(profileLink).not.toBeVisible(); }); }); @@ -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() .or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first()); - if (!(await createBtn.isVisible().catch(() => false))) { - console.log(' ⚠ Create playlist button not found'); - return; - } + await expect(createBtn).toBeVisible(); await createBtn.click(); await page.waitForTimeout(CONFIG.timeouts.animation); @@ -126,22 +105,22 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const dialog = page.locator('[role="dialog"]') .or(page.locator('[role="alertdialog"]')) .or(page.locator('[data-state="open"]')); - const visible = await dialog.first().isVisible().catch(() => false); // Also check for a name/title input inside the modal const nameInput = page.getByLabel(/nom|name|titre|title/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); - expect(visible || hasInput).toBeTruthy(); - console.log(` Create playlist modal: ${visible ? '✓ dialog visible' : '✗ dialog not found'}, input: ${hasInput ? '✓' : '✗'}`); + expect(dialogVisible || hasInput).toBeTruthy(); }); test('05. Escape ferme la modale de création', async ({ page }) => { 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()); - if (!(await createBtn.isVisible().catch(() => false))) return; + await expect(createBtn).toBeVisible(); await createBtn.click(); await page.waitForTimeout(CONFIG.timeouts.animation); @@ -149,25 +128,19 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const dialog = page.locator('[role="dialog"]') .or(page.locator('[role="alertdialog"]')); - const wasOpen = await dialog.first().isVisible().catch(() => false); - if (!wasOpen) { - console.log(' ⚠ Dialog did not open, skipping Escape test'); - return; - } + await expect(dialog.first()).toBeVisible(); await page.keyboard.press('Escape'); await page.waitForTimeout(CONFIG.timeouts.animation); - const stillOpen = await dialog.first().isVisible().catch(() => false); - expect(stillOpen).toBeFalsy(); - console.log(' Escape closed playlist creation modal'); + await expect(dialog.first()).not.toBeVisible(); }); 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() .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 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() .or(page.getByPlaceholder(/nom|name|titre/i).first()); - if (!(await nameInput.isVisible().catch(() => false))) { - console.log(' ⚠ Name input not found in modal'); - return; - } + await expect(nameInput).toBeVisible(); const playlistName = `E2E Modal Test ${testId()}`; await nameInput.fill(playlistName); // Submit — look for create/save/ok button inside the dialog const submitBtn = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok|submit/i }); - if (await submitBtn.isVisible().catch(() => false)) { - await submitBtn.click(); - await page.waitForTimeout(2_000); + await expect(submitBtn).toBeVisible(); - // Modal should be closed - const dialog = page.locator('[role="dialog"]'); - const stillOpen = await dialog.first().isVisible().catch(() => false); - console.log(` Modal after submit: ${stillOpen ? '✗ still open' : '✓ closed'}`); + await submitBtn.click(); + await page.waitForTimeout(2_000); - // Playlist name should appear on the page - const created = await page.getByText(playlistName).isVisible().catch(() => false); - console.log(` Playlist "${playlistName}" visible: ${created ? '✓' : '✗'}`); - } else { - console.log(' ⚠ Submit button not found in modal'); - } + // Modal should be closed + const dialog = page.locator('[role="dialog"]'); + await expect(dialog.first()).not.toBeVisible(); + + // Playlist name should appear on the page + await expect(page.getByText(playlistName)).toBeVisible(); }); }); @@ -217,10 +183,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { .or(page.getByPlaceholder(/search for tracks/i)) .or(page.locator(SELECTORS.searchInput)); - if (!(await searchInput.first().isVisible().catch(() => false))) { - console.log(' ⚠ Search input not found'); - return; - } + await expect(searchInput.first()).toBeVisible(); await searchInput.first().fill('tes'); // 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) const suggestions = page.locator('[role="listbox"]') .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); - 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 }) => { @@ -238,7 +203,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { .or(page.getByPlaceholder(/search for tracks/i)) .or(page.locator(SELECTORS.searchInput)); - if (!(await searchInput.first().isVisible().catch(() => false))) return; + await expect(searchInput.first()).toBeVisible(); await searchInput.first().fill('tes'); await page.waitForTimeout(1_500); @@ -246,17 +211,12 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const suggestions = page.locator('[role="listbox"]') .or(page.locator('[data-radix-popper-content-wrapper]')); const wasOpen = await suggestions.first().isVisible().catch(() => false); + test.skip(!wasOpen, 'No suggestions appeared to close'); await page.keyboard.press('Escape'); await page.waitForTimeout(CONFIG.timeouts.animation); - if (wasOpen) { - 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'); - } + await expect(suggestions.first()).not.toBeVisible(); }); 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.locator(SELECTORS.searchInput)); - if (!(await searchInput.first().isVisible().catch(() => false))) return; + await expect(searchInput.first()).toBeVisible(); await searchInput.first().fill('music'); 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"] a').first()); - if (await suggestionItem.isVisible().catch(() => false)) { - const urlBefore = page.url(); - await suggestionItem.click(); - await page.waitForTimeout(1_000); + const suggestionVisible = await suggestionItem.isVisible().catch(() => false); + test.skip(!suggestionVisible, 'No clickable suggestion found (data-dependent)'); - // URL or page content should have changed - const urlAfter = page.url(); - const navigated = urlBefore !== urlAfter; - console.log(` Clicked suggestion — navigated: ${navigated ? '✓' : '✗ (stayed on same page)'}`); - } else { - console.log(' ⚠ No clickable suggestion found'); - } + const urlBefore = page.url(); + await suggestionItem.click(); + await page.waitForTimeout(1_000); + + // URL or page content should have changed + const urlAfter = page.url(); + expect(urlBefore !== urlAfter).toBeTruthy(); }); }); @@ -302,10 +260,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const notifBtn = page.getByRole('button', { name: 'Notifications' }) .or(page.locator('[aria-label="Notifications"]')); - if (!(await notifBtn.first().isVisible().catch(() => false))) { - console.log(' ⚠ Notification bell button not found'); - return; - } + await expect(notifBtn.first()).toBeVisible(); await notifBtn.first().click(); await page.waitForTimeout(CONFIG.timeouts.animation); @@ -314,15 +269,14 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const dropdown = page.locator('[role="menu"]') .or(page.locator('[data-radix-popper-content-wrapper]')) .or(page.locator('[role="dialog"]')); - const visible = await dropdown.first().isVisible().catch(() => false); - console.log(` Notification dropdown: ${visible ? '✓ open' : '✗ not visible'}`); + await expect(dropdown.first()).toBeVisible(); }); test('11. Escape ferme le dropdown', async ({ page }) => { const notifBtn = page.getByRole('button', { name: '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 page.waitForTimeout(CONFIG.timeouts.animation); @@ -330,18 +284,13 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const dropdown = page.locator('[role="menu"]') .or(page.locator('[data-radix-popper-content-wrapper]')) .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.waitForTimeout(CONFIG.timeouts.animation); - if (wasOpen) { - const stillOpen = await dropdown.first().isVisible().catch(() => false); - expect(stillOpen).toBeFalsy(); - console.log(' Escape closed notification dropdown'); - } else { - console.log(' ⚠ Dropdown was not open'); - } + await expect(dropdown.first()).not.toBeVisible(); }); }); @@ -358,26 +307,21 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/i }).first() .or(page.getByRole('link', { name: /upload|importer/i }).first()); - if (!(await uploadBtn.isVisible().catch(() => false))) { - console.log(' ⚠ Upload button not found on /library'); - return; - } + await expect(uploadBtn).toBeVisible(); await uploadBtn.click(); await page.waitForTimeout(CONFIG.timeouts.animation); const dialog = page.locator('[role="dialog"]') .or(page.locator('[role="alertdialog"]')); - const visible = await dialog.first().isVisible().catch(() => false); - expect(visible).toBeTruthy(); - console.log(` Upload modal: ${visible ? '✓ open' : '✗ not open'}`); + await expect(dialog.first()).toBeVisible(); }); 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() .or(page.getByRole('link', { name: /upload|importer/i }).first()); - if (!(await uploadBtn.isVisible().catch(() => false))) return; + await expect(uploadBtn).toBeVisible(); await uploadBtn.click(); 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); expect(hasFileInput || hasDropZone).toBeTruthy(); - console.log(` File input: ${hasFileInput ? '✓' : '✗'}, Drop zone: ${hasDropZone ? '✓' : '✗'}`); }); test('14. Escape ferme la modale d\'upload', async ({ page }) => { const uploadBtn = page.getByRole('button', { name: /upload|importer|ajouter/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 page.waitForTimeout(CONFIG.timeouts.animation); const dialog = page.locator('[role="dialog"]') .or(page.locator('[role="alertdialog"]')); - const wasOpen = await dialog.first().isVisible().catch(() => false); - if (!wasOpen) { - console.log(' ⚠ Upload modal did not open'); - return; - } + await expect(dialog.first()).toBeVisible(); await page.keyboard.press('Escape'); await page.waitForTimeout(CONFIG.timeouts.animation); - const stillOpen = await dialog.first().isVisible().catch(() => false); - expect(stillOpen).toBeFalsy(); - console.log(' Escape closed upload modal'); + await expect(dialog.first()).not.toBeVisible(); }); }); @@ -434,10 +371,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { // Open an existing playlist const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - console.log(' ⚠ No existing playlist to test delete confirmation'); - return; - } + test.skip(!(await playlistLink.isVisible().catch(() => false)), 'No existing playlist to test delete confirmation'); await playlistLink.click(); 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() .or(page.locator('[data-action="delete"]').first()); - if (!(await deleteBtn.isVisible().catch(() => false))) { - console.log(' ⚠ Delete button not found on playlist page'); - return; - } + test.skip(!(await deleteBtn.isVisible().catch(() => false)), 'Delete button not found on playlist page'); await deleteBtn.click(); await page.waitForTimeout(CONFIG.timeouts.animation); @@ -457,54 +388,46 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { // A confirmation dialog should appear const confirmDialog = page.locator('[role="alertdialog"]') .or(page.locator('[role="dialog"]')); - const visible = await confirmDialog.first().isVisible().catch(() => false); // Look for confirmation text 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); - expect(visible || hasText).toBeTruthy(); - console.log(` Confirmation dialog: ${visible ? '✓ dialog' : '✗'}, text: ${hasText ? '✓' : '✗'}`); + expect(dialogVisible || hasText).toBeTruthy(); }); test('16. Annuler la confirmation ne supprime pas', async ({ page }) => { await navigateTo(page, '/playlists'); const playlistLink = page.locator('a[href*="/playlists/"]').first(); - if (!(await playlistLink.isVisible().catch(() => false))) { - console.log(' ⚠ No existing playlist to test cancel confirmation'); - return; - } + test.skip(!(await playlistLink.isVisible().catch(() => false)), 'No existing playlist to test cancel confirmation'); - const playlistText = await playlistLink.textContent() || ''; await playlistLink.click(); await page.waitForLoadState('networkidle'); const deleteBtn = page.getByRole('button', { name: /supprimer|delete|remove/i }).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 page.waitForTimeout(CONFIG.timeouts.animation); // Click Cancel/Annuler button const cancelBtn = page.getByRole('button', { name: /annuler|cancel|non|no/i }); - if (await cancelBtn.isVisible().catch(() => false)) { - await cancelBtn.click(); - await page.waitForTimeout(CONFIG.timeouts.animation); + await expect(cancelBtn).toBeVisible(); - // Confirmation dialog should be closed - const dialog = page.locator('[role="alertdialog"]'); - const stillOpen = await dialog.first().isVisible().catch(() => false); - console.log(` Dialog after cancel: ${stillOpen ? '✗ still open' : '✓ closed'}`); + await cancelBtn.click(); + await page.waitForTimeout(CONFIG.timeouts.animation); - // We should still be on the playlist page (not redirected) - await assertNotBroken(page); - console.log(' Page still intact after cancel'); - } else { - console.log(' ⚠ Cancel button not found in confirmation dialog'); - } + // Confirmation dialog should be closed + const dialog = page.locator('[role="alertdialog"]'); + await expect(dialog.first()).not.toBeVisible(); + + // We should still be on the playlist page (not redirected) + await assertNotBroken(page); }); }); @@ -528,20 +451,15 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { if (!(await editBtn.isVisible().catch(() => false))) { // Try track detail page — navigate to a track const trackLink = page.locator('a[href*="/tracks/"]').first(); - if (await trackLink.isVisible().catch(() => false)) { - await trackLink.click(); - await page.waitForLoadState('networkidle'); + test.skip(!(await trackLink.isVisible().catch(() => false)), 'No tracks or edit button found'); - const editBtnDetail = page.getByRole('button', { name: /edit|modifier|métadonnées|metadata/i }).first(); - if (!(await editBtnDetail.isVisible().catch(() => false))) { - console.log(' ⚠ Edit metadata button not found on track page'); - return; - } - await editBtnDetail.click(); - } else { - console.log(' ⚠ No tracks or edit button found'); - return; - } + await trackLink.click(); + await page.waitForLoadState('networkidle'); + + const editBtnDetail = page.getByRole('button', { name: /edit|modifier|métadonnées|metadata/i }).first(); + test.skip(!(await editBtnDetail.isVisible().catch(() => false)), 'Edit metadata button not found on track page'); + + await editBtnDetail.click(); } else { await editBtn.click(); } @@ -550,8 +468,7 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const dialog = page.locator('[role="dialog"]') .or(page.locator('[role="alertdialog"]')); - const visible = await dialog.first().isVisible().catch(() => false); - console.log(` Edit metadata modal: ${visible ? '✓ open' : '✗ not open'}`); + await expect(dialog.first()).toBeVisible(); }); 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) { - console.log(' ⚠ Could not open metadata edit modal'); - return; - } + test.skip(!modalOpened, 'Could not open metadata edit modal (no tracks or edit button found)'); // Check for metadata fields const body = await page.textContent('body') || ''; @@ -600,10 +514,10 @@ test.describe('MODALS — Ouverture et fermeture @feature-modals', () => { const hasTags = /tag/i.test(body) || await page.getByLabel(/tag/i).first().isVisible().catch(() => false); - console.log(` BPM field: ${hasBPM ? '✓' : '✗'}`); - console.log(` Key field: ${hasKey ? '✓' : '✗'}`); - console.log(` Genres field: ${hasGenres ? '✓' : '✗'}`); - console.log(` Tags field: ${hasTags ? '✓' : '✗'}`); + expect(hasBPM).toBeTruthy(); + expect(hasKey).toBeTruthy(); + expect(hasGenres).toBeTruthy(); + expect(hasTags).toBeTruthy(); }); }); }); diff --git a/tests/e2e/18-empty-states.spec.ts b/tests/e2e/18-empty-states.spec.ts index 2f1f48b36..e7570ef54 100644 --- a/tests/e2e/18-empty-states.spec.ts +++ b/tests/e2e/18-empty-states.spec.ts @@ -32,12 +32,7 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states' }, }); - if (response.ok()) { - 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`); - } + expect(response.ok()).toBeTruthy(); }); /** @@ -53,17 +48,14 @@ test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states' return true; } catch { // Fallback: use listener account (may not have truly empty states) - console.log(' ⚠ Falling back to listener account'); try { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); // Check that fallback login also succeeded if (page.url().includes('/login')) { - console.log(' ⚠ Fallback login also failed — still on /login'); return false; } return true; } catch { - console.log(' ⚠ Fallback login threw an error'); 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 }) => { const loggedIn = await loginAsFreshUser(page); - if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/library'); 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, }); - console.log(` /library empty state: ${hasEmptyState ? '✓' : '✗'}`); - console.log(` /library CTA button: ${hasCta ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); + expect(hasCta).toBeTruthy(); // Page should not be blank 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 }) => { const loggedIn = await loginAsFreshUser(page); - if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/playlists'); 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, }); - console.log(` /playlists empty state: ${hasEmptyState ? '✓' : '✗'}`); - console.log(` /playlists CTA button: ${hasCta ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); + expect(hasCta).toBeTruthy(); const body = await page.textContent('body') || ''; expect(body.length).toBeGreaterThan(50); }); test('03. Notifications vides — message approprie', async ({ page }) => { - await loginAsFreshUser(page); + const loggedIn = await loginAsFreshUser(page); + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/notifications'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/notification|aucune|no notification/i], }); - console.log(` /notifications empty state: ${hasEmptyState ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); await assertNotBroken(page); }); test('04. Feed vide — message + suggestion', async ({ page }) => { - await loginAsFreshUser(page); + const loggedIn = await loginAsFreshUser(page); + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/feed'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/feed|follow|suivre|discover|découvr/i], }); - console.log(` /feed empty state: ${hasEmptyState ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); // Page should load without crash await assertNotBroken(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'); // 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 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); }); test('06. Queue vide — message', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); - if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/queue'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/queue|file d'attente|no tracks|aucun/i], }); - console.log(` /queue empty state: ${hasEmptyState ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); await assertNotBroken(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'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/chat|conversation|message|channel/i], }); - console.log(` /chat empty state: ${hasEmptyState ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); await assertNotBroken(page); }); test('08. Wishlist vide — message + CTA browse', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); - if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/wishlist'); 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, }); - console.log(` /wishlist empty state: ${hasEmptyState ? '✓' : '✗'}`); - console.log(` /wishlist CTA button: ${hasCta ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); + expect(hasCta).toBeTruthy(); await assertNotBroken(page); }); test('09. Purchases vides — message', async ({ page }) => { - await loginAsFreshUser(page); + const loggedIn = await loginAsFreshUser(page); + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/purchases'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/purchase|achat|order|commande|no .* yet/i], }); - console.log(` /purchases empty state: ${hasEmptyState ? '✓' : '✗'}`); + expect(hasEmptyState).toBeTruthy(); 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 // Try fresh user first, fallback to existing creator const loggedIn = await loginAsFreshUser(page); - if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } + expect(loggedIn).toBeTruthy(); await navigateTo(page, '/analytics'); 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 await assertNotBroken(page); - console.log(` /analytics empty state text: ${hasEmptyAnalytics ? '✓' : '✗'}`); - console.log(` /analytics chart area: ${hasChartArea ? '✓ (zero-state chart)' : '✗'}`); + expect(hasEmptyAnalytics || hasChartArea).toBeTruthy(); }); });