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:
parent
a3f4ac6b70
commit
5b228c729b
8 changed files with 295 additions and 750 deletions
|
|
@ -12,92 +12,73 @@ 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()}`;
|
|
||||||
await nameInput.fill(playlistName);
|
|
||||||
|
|
||||||
// Description si présent
|
const playlistName = `E2E Playlist ${Date.now()}`;
|
||||||
const descInput = page.getByLabel(/description/i).first();
|
await nameInput.fill(playlistName);
|
||||||
if (await descInput.isVisible().catch(() => false)) {
|
|
||||||
await descInput.fill('Créée par les tests E2E');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sauvegarder — try dialog-scoped first, then modal, then last visible matching button
|
const descInput = page.getByLabel(/description/i).first();
|
||||||
const dialog = page.locator('[role="dialog"], [role="alertdialog"], dialog, [data-state="open"]').first();
|
if (await descInput.isVisible().catch(() => false)) {
|
||||||
const dialogVisible = await dialog.isVisible().catch(() => false);
|
await descInput.fill('Créée par les tests E2E');
|
||||||
|
|
||||||
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 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 }) => {
|
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}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
await genreButtons.first().click();
|
||||||
const genreName = await genreButtons.first().locator('.font-heading.font-bold').textContent();
|
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 backBtn = page.getByRole('button', { name: /retour/i });
|
await expect(backBtn).toBeVisible();
|
||||||
const hasBack = await backBtn.isVisible().catch(() => false);
|
|
||||||
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) {
|
const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]');
|
||||||
// Editorial playlists use PlaylistCard components with role="article" aria-label="Playlist: ..."
|
expect(await editorialCards.count()).toBeGreaterThanOrEqual(0);
|
||||||
const editorialCards = page.locator('[role="article"][aria-label^="Playlist:"]');
|
|
||||||
const count = await editorialCards.count();
|
|
||||||
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 });
|
await expect(genreHeading).toBeVisible();
|
||||||
const visible = await genreHeading.isVisible().catch(() => false);
|
|
||||||
console.log(` Retour à la liste genres: ${visible ? '✓' : '✗'}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)
|
// Cart panel should show something (empty cart message or items)
|
||||||
const body = await page.textContent('body') || '';
|
const body = await page.textContent('body') || '';
|
||||||
// Cart panel should show something (empty cart message or items)
|
expect(body).toMatch(/cart|panier|empty|vide|item/i);
|
||||||
console.log(' Cart panel ouvert');
|
|
||||||
} else {
|
|
||||||
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();
|
await expect(toast).toBeVisible();
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 ? '✓' : '✗'}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 page.waitForTimeout(2_000);
|
|
||||||
|
|
||||||
// Modal should be closed
|
await submitBtn.click();
|
||||||
const dialog = page.locator('[role="dialog"]');
|
await page.waitForTimeout(2_000);
|
||||||
const stillOpen = await dialog.first().isVisible().catch(() => false);
|
|
||||||
console.log(` Modal after submit: ${stillOpen ? '✗ still open' : '✓ closed'}`);
|
|
||||||
|
|
||||||
// Playlist name should appear on the page
|
// Modal should be closed
|
||||||
const created = await page.getByText(playlistName).isVisible().catch(() => false);
|
const dialog = page.locator('[role="dialog"]');
|
||||||
console.log(` Playlist "${playlistName}" visible: ${created ? '✓' : '✗'}`);
|
await expect(dialog.first()).not.toBeVisible();
|
||||||
} else {
|
|
||||||
console.log(' ⚠ Submit button not found in modal');
|
// 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.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);
|
||||||
const urlBefore = page.url();
|
test.skip(!suggestionVisible, 'No clickable suggestion found (data-dependent)');
|
||||||
await suggestionItem.click();
|
|
||||||
await page.waitForTimeout(1_000);
|
|
||||||
|
|
||||||
// URL or page content should have changed
|
const urlBefore = page.url();
|
||||||
const urlAfter = page.url();
|
await suggestionItem.click();
|
||||||
const navigated = urlBefore !== urlAfter;
|
await page.waitForTimeout(1_000);
|
||||||
console.log(` Clicked suggestion — navigated: ${navigated ? '✓' : '✗ (stayed on same page)'}`);
|
|
||||||
} else {
|
// URL or page content should have changed
|
||||||
console.log(' ⚠ No clickable suggestion found');
|
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' })
|
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 page.waitForTimeout(CONFIG.timeouts.animation);
|
|
||||||
|
|
||||||
// Confirmation dialog should be closed
|
await cancelBtn.click();
|
||||||
const dialog = page.locator('[role="alertdialog"]');
|
await page.waitForTimeout(CONFIG.timeouts.animation);
|
||||||
const stillOpen = await dialog.first().isVisible().catch(() => false);
|
|
||||||
console.log(` Dialog after cancel: ${stillOpen ? '✗ still open' : '✓ closed'}`);
|
|
||||||
|
|
||||||
// We should still be on the playlist page (not redirected)
|
// Confirmation dialog should be closed
|
||||||
await assertNotBroken(page);
|
const dialog = page.locator('[role="alertdialog"]');
|
||||||
console.log(' Page still intact after cancel');
|
await expect(dialog.first()).not.toBeVisible();
|
||||||
} else {
|
|
||||||
console.log(' ⚠ Cancel button not found in confirmation dialog');
|
// 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))) {
|
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 page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
const editBtnDetail = page.getByRole('button', { name: /edit|modifier|métadonnées|metadata/i }).first();
|
await trackLink.click();
|
||||||
if (!(await editBtnDetail.isVisible().catch(() => false))) {
|
await page.waitForLoadState('networkidle');
|
||||||
console.log(' ⚠ Edit metadata button not found on track page');
|
|
||||||
return;
|
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 editBtnDetail.click();
|
||||||
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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)' : '✗'}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue