2025-12-22 21:00:50 +00:00
import { test , expect , type Page } from '@playwright/test' ;
import {
TEST_CONFIG ,
loginAsUser ,
forceSubmitForm ,
openModal ,
closeModal ,
fillField ,
safeClick ,
navigateViaHref ,
setupErrorCapture ,
waitForToast ,
waitForListLoaded ,
} from './utils/test-helpers' ;
/ * *
* Playlists E2E Test Suite
*
* Teste le cycle de vie complet des playlists :
* - Création d ' une playlist
* - Lecture de la liste des playlists
* - Modification d ' une playlist
* - Ajout de tracks à une playlist
* - Suppression de tracks d ' une playlist
* - Suppression d ' une playlist
* /
test . describe ( 'Playlists CRUD' , ( ) = > {
let consoleErrors : string [ ] = [ ] ;
let networkErrors : Array < { url : string ; status : number ; method : string } > = [ ] ;
test . beforeEach ( async ( { page } ) = > {
const errorCapture = setupErrorCapture ( page ) ;
consoleErrors = errorCapture . consoleErrors ;
networkErrors = errorCapture . networkErrors ;
// 1. Login avant chaque test (nous laisse sur /dashboard si déjà connecté)
await loginAsUser ( page ) ;
// 2. CORRECTION : Forcer la navigation vers la page des playlists
console . log ( '🧭 [NAVIGATION] Going to playlists page...' ) ;
// 🔴 FIX: Utiliser l'URL complète pour éviter "Cannot navigate to invalid URL"
// S'assurer que TEST_CONFIG.FRONTEND_URL est défini
const baseUrl = TEST_CONFIG . FRONTEND_URL || 'http://localhost:3000' ;
const playlistsUrl = ` ${ baseUrl } /playlists ` ;
console . log ( ` 🧭 [NAVIGATION] Navigating to: ${ playlistsUrl } ` ) ;
await page . goto ( playlistsUrl , { waitUntil : 'networkidle' } ) ;
await page . waitForLoadState ( 'networkidle' ) ;
// 🔴 FIX: Attendre que la page soit complètement chargée et hydratée
// Attendre le titre de la page ou la fin du loading
try {
await Promise . race ( [
page . locator ( 'h1:has-text("Playlist"), h1:has-text("Playlists"), h2:has-text("Playlist")' ) . first ( ) . waitFor ( { state : 'visible' , timeout : 10000 } ) ,
page . locator ( '[data-testid="playlists-page"], [data-testid="playlist-list"]' ) . first ( ) . waitFor ( { state : 'visible' , timeout : 10000 } ) ,
// Attendre qu'un élément de contenu soit visible (pas juste le skeleton)
page . locator ( 'main, [role="main"]' ) . first ( ) . waitFor ( { state : 'visible' , timeout : 10000 } ) ,
] ) ;
console . log ( '✅ [PLAYLISTS] Page fully loaded' ) ;
} catch {
console . warn ( '⚠️ [PLAYLISTS] Page load check timeout, continuing...' ) ;
}
// Attendre que les requêtes API soient terminées (si applicable)
try {
await page . waitForResponse (
( response ) = > response . url ( ) . includes ( '/playlists' ) && response . status ( ) < 500 ,
{ timeout : 10000 }
) . catch ( ( ) = > {
// Si pas de requête API, ce n'est pas grave
} ) ;
} catch {
// Ignorer si pas de requête API
}
} ) ;
/ * *
* TEST 1 : Créer une nouvelle playlist
* /
test ( 'should create a new playlist successfully' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Create new playlist' ) ;
// Naviguer directement vers la page des playlists (pas de lien dans sidebar)
// Utiliser l'URL complète et domcontentloaded pour éviter les timeouts
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } /playlists ` , { waitUntil : 'domcontentloaded' } ) ;
// Attendre un peu pour que React Router mette à jour l'URL
await page . waitForTimeout ( 500 ) ;
// Vérifier l'URL mais ne pas timeout si elle ne change pas immédiatement
await page . waitForURL ( /\/playlists/ , { timeout : 15000 } ) . catch ( ( ) = > {
// Si l'URL n'a pas changé, vérifier qu'on est au moins sur la bonne page
const currentUrl = page . url ( ) ;
if ( ! currentUrl . includes ( '/playlists' ) ) {
throw new Error ( ` Navigation to /playlists failed. Current URL: ${ currentUrl } ` ) ;
}
} ) ;
// Ouvrir la modal de création
// Le bouton a maintenant data-testid="create-playlist-btn" et aria-label="Créer une nouvelle playlist"
await openModal ( page , /create|créer|nouvelle/i ) ;
// Remplir le formulaire
const playlistName = ` Test Playlist ${ Date . now ( ) } ` ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , playlistName ) ;
// Description (optionnelle)
const descriptionField = page . locator ( 'textarea[name="description"], textarea#description' ) . first ( ) ;
const isDescriptionVisible = await descriptionField . isVisible ( ) . catch ( ( ) = > false ) ;
if ( isDescriptionVisible ) {
await descriptionField . fill ( 'Playlist de test créée par E2E automation' ) ;
}
// Soumettre le formulaire
await forceSubmitForm ( page , 'form' ) ;
// Attendre le succès
await waitForToast ( page , 'success' , 10000 ) ;
// Attendre que la modal se ferme
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger la page pour forcer le rafraîchissement de la liste
// La liste peut ne pas se rafraîchir automatiquement après création
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Utiliser directement getByText au lieu de waitForListLoaded
// Plus fiable car il cherche directement le texte, indépendamment de la structure UI
await expect ( page . getByText ( playlistName ) ) . toBeVisible ( { timeout : 15000 } ) ;
console . log ( '✅ [PLAYLISTS] Playlist created successfully' ) ;
} ) ;
/ * *
* TEST 2 : Lire la liste des playlists
* /
test ( 'should display list of playlists' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Display playlists list' ) ;
// Naviguer directement vers la page des playlists (pas de lien dans sidebar)
// Utiliser l'URL complète et domcontentloaded pour éviter les timeouts
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } /playlists ` , { waitUntil : 'domcontentloaded' } ) ;
// Attendre un peu pour que React Router mette à jour l'URL
await page . waitForTimeout ( 500 ) ;
// Vérifier l'URL mais ne pas timeout si elle ne change pas immédiatement
await page . waitForURL ( /\/playlists/ , { timeout : 15000 } ) . catch ( ( ) = > {
// Si l'URL n'a pas changé, vérifier qu'on est au moins sur la bonne page
const currentUrl = page . url ( ) ;
if ( ! currentUrl . includes ( '/playlists' ) ) {
throw new Error ( ` Navigation to /playlists failed. Current URL: ${ currentUrl } ` ) ;
}
} ) ;
// Attendre que la liste soit chargée (peut être vide, donc minRows=0)
await waitForListLoaded ( page , 0 ) ;
// Vérifier que la page affiche le titre "Playlists" ou équivalent
const pageTitle = page . locator ( 'h1:has-text("Playlists"), h1:has-text("Mes playlists")' ) ;
await expect ( pageTitle ) . toBeVisible ( { timeout : 10000 } ) ;
// Vérifier que soit la liste est visible, soit l'état vide est affiché
const listOrEmpty = page . locator ( '[role="list"], [role="table"], text=/aucune|no.*found|empty|vide/i' ) . first ( ) ;
const isVisible = await listOrEmpty . isVisible ( { timeout : 5000 } ) . catch ( ( ) = > false ) ;
if ( ! isVisible ) {
// Si ni liste ni état vide, vérifier au moins que le conteneur de la page est visible
const container = page . locator ( '.playlist-container, [data-testid="playlists-page"]' ) . first ( ) ;
await expect ( container ) . toBeVisible ( { timeout : 5000 } ) ;
}
console . log ( '✅ [PLAYLISTS] Playlists page loaded successfully' ) ;
} ) ;
/ * *
* TEST 3 : Modifier une playlist existante
* /
test ( 'should update playlist name and description' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Update playlist' ) ;
// Créer d'abord une playlist
await navigateViaHref ( page , '/playlists' , /\/playlists/ ) ;
await openModal ( page , /create|créer|nouvelle/i ) ;
const originalName = ` Original Playlist ${ Date . now ( ) } ` ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , originalName ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
// Attendre que la modal se ferme
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger la page pour forcer le rafraîchissement de la liste
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Utiliser directement getByText au lieu de waitForListLoaded
// Plus fiable car il cherche directement le texte, indépendamment de la structure UI
// 🔴 FIX: Cibler le lien de la card spécifiquement
// getByText peut cibler un élément non cliquable si le CSS est complexe
const playlistCard = page . locator ( 'a[href*="/playlists/"]' ) . filter ( { hasText : originalName } ) . first ( ) ;
// 🔴 FIX: Naviguer manuellement vers la page de détails pour éviter les problèmes de clic/overlay
const href = await playlistCard . getAttribute ( 'href' ) ;
if ( ! href ) throw new Error ( 'Playlist card has no href' ) ;
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } ${ href } ` , { waitUntil : 'networkidle' } ) ;
// Attendre que la page de détails se charge (redondant mais sûr)
await page . waitForURL ( /\/playlists\/[^/]+/ , { timeout : 10000 } ) ;
// Sur la page de détails, chercher le bouton d'édition
// Sur la page de détails, chercher le bouton d'édition
// Note: Le texte est "Modifier" en français, pas "Éditer"
const editButton = page . locator ( 'button:has-text("Edit"), button:has-text("Éditer"), button:has-text("Modifier"), button[aria-label*="edit" i], button[aria-label*="modifier" i]' ) . first ( ) ;
const moreButton = page . locator ( 'button:has-text("More"), button:has-text("Actions"), button[aria-label*="more" i], button[aria-label*="actions" i]' ) . first ( ) ;
// Attendre que les actions soient chargées
await page . waitForSelector ( '[role="group"][aria-label="Actions de la playlist"]' , { timeout : 10000 } ) . catch ( ( ) = > console . warn ( '⚠️ Actions group not found' ) ) ;
const isEditVisible = await editButton . isVisible ( ) . catch ( ( ) = > false ) ;
const isMoreVisible = await moreButton . isVisible ( ) . catch ( ( ) = > false ) ;
if ( isEditVisible ) {
console . log ( '🔍 Clicking edit button via dispatchEvent' ) ;
// Utiliser dispatchEvent pour contourner l'overlay de la sidebar qui intercepte le click
await editButton . dispatchEvent ( 'click' ) ;
} else if ( isMoreVisible ) {
await moreButton . click ( ) ;
await page . waitForTimeout ( 500 ) ;
await page . locator ( '[role="menuitem"]:has-text("Edit"), [role="menuitem"]:has-text("Éditer")' ) . first ( ) . click ( ) ;
} else {
// Si pas de bouton d'édition visible, on est peut-être déjà sur la page de détails
// Chercher un formulaire d'édition ou un bouton pour ouvrir l'édition
console . warn ( '⚠️ [PLAYLISTS] Edit button not found, playlist may not be editable or UI changed' ) ;
}
// Attendre que la modal d'édition s'ouvre
await page . waitForSelector ( '[role="dialog"]' , { timeout : 5000 } ) ;
// Modifier le nom
const updatedName = ` Updated Playlist ${ Date . now ( ) } ` ;
// 🔴 FIX: Ajouter l'ID spécifique utilisé dans PlaylistActions (edit-title)
const nameField = page . locator ( 'input[name="name"], input[name="title"], input#title, input#edit-title' ) . first ( ) ;
await nameField . clear ( ) ;
await nameField . fill ( updatedName ) ;
// Soumettre en cliquant sur "Enregistrer" (pas de balise form dans le dialog)
// await forceSubmitForm(page, 'form'); // Ne marche pas car pas de form
const saveButton = page . locator ( '[role="dialog"] button' ) . filter ( { hasText : /enregistrer/i } ) . first ( ) ;
await saveButton . click ( { force : true } ) ;
await waitForToast ( page , 'success' , 10000 ) ;
// Retourner à la liste des playlists pour vérifier
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } /playlists ` , { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Utiliser getByText pour une recherche directe et fiable
// 🔴 FIX: Cibler le lien de la card pour la vérification
const updatedPlaylist = page . locator ( 'a[href*="/playlists/"]' ) . filter ( { hasText : updatedName } ) . first ( ) ;
await expect ( updatedPlaylist ) . toBeVisible ( { timeout : 15000 } ) ;
console . log ( '✅ [PLAYLISTS] Playlist updated successfully' ) ;
} ) ;
/ * *
* TEST 4 : Ajouter une track à une playlist
* /
test ( 'should add track to playlist' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Add track to playlist' ) ;
// Créer une playlist
await navigateViaHref ( page , '/playlists' , /\/playlists/ ) ;
await openModal ( page , /create|créer|nouvelle/i ) ;
const playlistName = ` Add Track Playlist ${ Date . now ( ) } ` ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , playlistName ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
// Attendre que la modal se ferme
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger pour s'assurer que la playlist est créée avant de naviguer
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 1000 ) ;
// Naviguer vers la bibliothèque pour trouver une track
await navigateViaHref ( page , '/library' , /\/library/ ) ;
// Attendre que la page soit chargée
await page . waitForLoadState ( 'domcontentloaded' ) ;
await page . waitForTimeout ( 1000 ) ;
// 🔴 FIX: La bibliothèque peut utiliser une table OU une grille de cards
// Attendre qu'au moins un élément de track soit visible (plus flexible)
try {
await waitForListLoaded ( page , 1 ) ;
} catch {
// Si waitForListLoaded échoue, essayer de trouver directement une track
const trackElement = page . locator ( 'tr, [role="row"], [role="listitem"], .track-card, [data-testid*="track"], [role="grid"] > *' ) . first ( ) ;
await expect ( trackElement ) . toBeVisible ( { timeout : 10000 } ) ;
}
// 🔴 FIX: Trouver la première track avec un sélecteur générique (table OU grid)
// Essayer d'abord table row, puis grid item, puis n'importe quel élément contenant du texte de track
let firstTrack = page . locator ( 'tr, [role="row"]' ) . filter ( { has : page.locator ( 'td, [role="cell"]' ) } ) . first ( ) ;
if ( ! ( await firstTrack . isVisible ( { timeout : 2000 } ) . catch ( ( ) = > false ) ) ) {
// Si pas de table, essayer grid ou card
firstTrack = page . locator ( '[role="grid"] > *, [role="listitem"], .track-card, [data-testid*="track"]' ) . first ( ) ;
}
await expect ( firstTrack ) . toBeVisible ( { timeout : 10000 } ) ;
// Ouvrir le menu "Add to Playlist"
const addToPlaylistButton = firstTrack . locator ( 'button:has-text("Add to playlist"), button:has-text("Ajouter à"), button[aria-label*="playlist" i]' ) . first ( ) ;
const moreButton = firstTrack . locator ( 'button:has-text("More"), button:has-text("Actions")' ) . first ( ) ;
const isAddVisible = await addToPlaylistButton . isVisible ( ) . catch ( ( ) = > false ) ;
const isMoreVisible = await moreButton . isVisible ( ) . catch ( ( ) = > false ) ;
if ( isAddVisible ) {
await addToPlaylistButton . click ( ) ;
} else if ( isMoreVisible ) {
await moreButton . click ( ) ;
await page . waitForTimeout ( 500 ) ;
await page . locator ( '[role="menuitem"]:has-text("Add to playlist"), [role="menuitem"]:has-text("Ajouter")' ) . first ( ) . click ( ) ;
} else {
console . warn ( '⚠️ [PLAYLISTS] Add to playlist button not found, skipping test' ) ;
test . skip ( ) ;
return ;
}
// Sélectionner la playlist dans le menu/modal
await page . waitForTimeout ( 500 ) ;
const playlistOption = page . locator ( ` text= ${ playlistName } , [role="menuitem"]:has-text(" ${ playlistName } ") ` ) . first ( ) ;
const isPlaylistOptionVisible = await playlistOption . isVisible ( { timeout : 5000 } ) . catch ( ( ) = > false ) ;
if ( isPlaylistOptionVisible ) {
await playlistOption . click ( ) ;
await waitForToast ( page , 'success' , 10000 ) ;
console . log ( '✅ [PLAYLISTS] Track added to playlist successfully' ) ;
} else {
console . warn ( '⚠️ [PLAYLISTS] Playlist option not found in menu' ) ;
}
} ) ;
/ * *
* TEST 5 : Supprimer une playlist
* /
test ( 'should delete playlist successfully' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Delete playlist' ) ;
// Créer une playlist à supprimer
await navigateViaHref ( page , '/playlists' , /\/playlists/ ) ;
await openModal ( page , /create|créer|nouvelle/i ) ;
const playlistName = ` Delete Playlist ${ Date . now ( ) } ` ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , playlistName ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
// Attendre que la modal se ferme
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger la page pour forcer le rafraîchissement de la liste
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Utiliser directement getByText au lieu de waitForListLoaded
// Plus fiable car il cherche directement le texte, indépendamment de la structure UI
// 🔴 FIX: Cibler le lien de la card spécifiquement
const playlistCard = page . locator ( 'a[href*="/playlists/"]' ) . filter ( { hasText : playlistName } ) . first ( ) ;
await expect ( playlistCard ) . toBeVisible ( { timeout : 15000 } ) ;
// 🔴 FIX: Naviguer manuellement vers la page de détails
const href = await playlistCard . getAttribute ( 'href' ) ;
if ( ! href ) throw new Error ( 'Playlist card has no href' ) ;
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } ${ href } ` , { waitUntil : 'networkidle' } ) ;
await page . waitForURL ( /\/playlists\/[^/]+/ , { timeout : 10000 } ) ;
// Sur la page de détails, chercher le bouton de suppression
const deleteButton = page . locator ( 'button:has-text("Delete"), button:has-text("Supprimer"), button[aria-label*="delete" i], button[aria-label*="supprimer" i]' ) . first ( ) ;
const moreButton = page . locator ( 'button:has-text("More"), button:has-text("Actions"), button[aria-label*="more" i], button[aria-label*="actions" i]' ) . first ( ) ;
// Attendre que les actions soient chargées
await page . waitForSelector ( '[role="group"][aria-label="Actions de la playlist"]' , { timeout : 10000 } ) . catch ( ( ) = > console . warn ( '⚠️ Actions group not found' ) ) ;
const isDeleteVisible = await deleteButton . isVisible ( ) . catch ( ( ) = > false ) ;
const isMoreVisible = await moreButton . isVisible ( ) . catch ( ( ) = > false ) ;
if ( isDeleteVisible ) {
await deleteButton . click ( { force : true } ) ;
} else if ( isMoreVisible ) {
await moreButton . click ( ) ;
await page . waitForTimeout ( 500 ) ;
await page . locator ( '[role="menuitem"]:has-text("Delete"), [role="menuitem"]:has-text("Supprimer")' ) . first ( ) . click ( ) ;
} else {
// Fallback: icône de corbeille
const trashButton = page . locator ( 'button svg.lucide-trash, button svg.fa-trash' ) . first ( ) ;
if ( await trashButton . isVisible ( { timeout : 2000 } ) . catch ( ( ) = > false ) ) {
await trashButton . click ( ) ;
} else {
console . warn ( '⚠️ [PLAYLISTS] Delete button not found, playlist may not be deletable or UI changed' ) ;
}
}
// Confirmer la suppression si modal de confirmation
await page . waitForTimeout ( 500 ) ;
// 🔴 FIX: Cibler le bouton DANS le dialog
const confirmButton = page . locator ( '[role="dialog"] button:has-text("Confirm"), [role="dialog"] button:has-text("Oui"), [role="dialog"] button:has-text("Supprimer")' ) . first ( ) ;
const isConfirmVisible = await confirmButton . isVisible ( ) . catch ( ( ) = > false ) ;
if ( isConfirmVisible ) {
await confirmButton . click ( { force : true } ) ;
// 🔴 FIX: Attendre la confirmation de suppression avant de continuer
// Sinon la navigation manuelle suivante peut annuler la requête
await waitForToast ( page , 'success' , 10000 ) ;
}
// Attendre que la navigation automatique se fasse (le composant redirige vers /playlists)
await page . waitForURL ( /\/playlists$/ , { timeout : 10000 } ) . catch ( ( ) = > {
// Fallback si la redirection auto ne marche pas ou est lente
console . log ( '⚠️ [PLAYLISTS] Auto-redirect failed/slow, manual navigation' ) ;
return page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } /playlists ` , { waitUntil : 'networkidle' } ) ;
} ) ;
// Attendre le rechargement de la liste
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Vérifier que la playlist supprimée n'apparaît plus dans la liste
// Utiliser getByText qui est plus fiable pour vérifier l'absence
// 🔴 FIX: Vérifier que la playlist supprimée n'apparaît plus dans la liste
const deletedPlaylistCard = page . locator ( 'a[href*="/playlists/"]' ) . filter ( { hasText : playlistName } ) . first ( ) ;
await expect ( deletedPlaylistCard ) . not . toBeVisible ( { timeout : 15000 } ) ;
// Vérifier persistence (reload pour s'assurer que la suppression est persistée)
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 1000 ) ;
// Ne pas utiliser waitForListLoaded ici car on ne sait pas combien de playlists restent
// Vérifier directement que la playlist supprimée n'est plus visible
const deletedPlaylist = page . getByText ( playlistName ) ;
await expect ( deletedPlaylist ) . not . toBeVisible ( { timeout : 10000 } ) ;
console . log ( '✅ [PLAYLISTS] Playlist deleted successfully' ) ;
} ) ;
/ * *
* TEST 6 : Playlist vide ( sans tracks )
* /
test ( 'should display empty state for new playlist' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Empty playlist state' ) ;
// Créer une playlist
await navigateViaHref ( page , '/playlists' , /\/playlists/ ) ;
await openModal ( page , /create|créer|nouvelle/i ) ;
const playlistName = ` Empty Playlist ${ Date . now ( ) } ` ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , playlistName ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
// Attendre que la modal se ferme
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger la page pour forcer le rafraîchissement de la liste
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Utiliser directement getByText au lieu de waitForListLoaded
// Plus fiable car il cherche directement le texte, indépendamment de la structure UI
// 🔴 FIX: Naviguer manuellement vers la page de détails pour éviter les problèmes de clic/overlay
// Comme fait dans les autres tests (update/delete)
const playlistLink = page . locator ( 'a[href*="/playlists/"]' ) . filter ( { hasText : playlistName } ) . first ( ) ;
const href = await playlistLink . getAttribute ( 'href' ) ;
if ( ! href ) throw new Error ( 'Playlist card has no href' ) ;
await page . goto ( ` ${ TEST_CONFIG . FRONTEND_URL } ${ href } ` , { waitUntil : 'networkidle' } ) ;
// Attendre que la page de détails se charge
await page . waitForURL ( /\/playlists\/[^/]+/ , { timeout : 10000 } ) ;
// Vérifier l'état vide
const emptyState = page . locator ( 'text=/empty|vide|aucune track|no tracks/i' ) . first ( ) ;
const isEmptyStateVisible = await emptyState . isVisible ( { timeout : 5000 } ) . catch ( ( ) = > false ) ;
if ( isEmptyStateVisible ) {
console . log ( '✅ [PLAYLISTS] Empty state displayed correctly' ) ;
} else {
console . log ( 'ℹ ️ [PLAYLISTS] Empty state not explicitly shown (may be implicit)' ) ;
}
} ) ;
/ * *
* TEST 7 : Recherche de playlists
* /
test ( 'should search playlists by name' , async ( { page } ) = > {
console . log ( '🧪 [PLAYLISTS] Running: Search playlists' ) ;
// Créer plusieurs playlists
await navigateViaHref ( page , '/playlists' , /\/playlists/ ) ;
const searchTerm = ` SearchTest ${ Date . now ( ) } ` ;
// Créer playlist 1
await openModal ( page , /create|créer|nouvelle/i ) ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , ` ${ searchTerm } Alpha ` ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// Créer playlist 2
await openModal ( page , /create|créer|nouvelle/i ) ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , ` ${ searchTerm } Beta ` ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// Créer playlist 3 (différente)
const differentName = ` Different ${ Date . now ( ) } ` ;
await openModal ( page , /create|créer|nouvelle/i ) ;
await fillField ( page , 'input[name="name"], input[name="title"], input#title' , differentName ) ;
await forceSubmitForm ( page , 'form' ) ;
await waitForToast ( page , 'success' , 10000 ) ;
await page . waitForSelector ( '[role="dialog"]' , { state : 'hidden' , timeout : 5000 } ) . catch ( ( ) = > { } ) ;
// 🔴 FIX: Recharger la page pour forcer le rafraîchissement de la liste
await page . reload ( { waitUntil : 'networkidle' } ) ;
await page . waitForTimeout ( 2000 ) ;
// 🔴 FIX: Vérifier directement que les playlists créées sont visibles
// Au lieu de compter les éléments, on vérifie directement les textes
await expect ( page . getByText ( ` ${ searchTerm } Alpha ` ) ) . toBeVisible ( { timeout : 10000 } ) ;
await expect ( page . getByText ( ` ${ searchTerm } Beta ` ) ) . toBeVisible ( { timeout : 10000 } ) ;
await expect ( page . getByText ( differentName ) ) . toBeVisible ( { timeout : 10000 } ) ;
// Chercher un champ de recherche
// Chercher un champ de recherche
// 🔴 FIX: Cibler spécifiquement la recherche de playlist (éviter la recherche globale)
const searchInput = page . locator ( '[data-testid="playlist-search"]' ) . first ( ) ;
2025-12-28 15:07:02 +00:00
const isSearchVisible = await searchInput . isVisible ( { timeout : 2000 } ) . catch ( ( ) = > false ) ;
2025-12-22 21:00:50 +00:00
// Fallback: ancien sélecteur si data-testid pas encore déployé (ou autre input)
if ( ! isSearchVisible ) {
const fallbackInput = page . locator ( 'input[placeholder*="Search" i], input[placeholder*="Recherche" i], input[type="search"]' ) . filter ( { hasNot : page.locator ( '[aria-label="Global search"]' ) } ) . first ( ) ;
if ( await fallbackInput . isVisible ( ) . catch ( ( ) = > false ) ) {
// Mais attention, si c'est la recherche globale, ça ne marchera pas
console . warn ( '⚠️ Using fallback search selector, might be global search' ) ;
// On continue quand même pour voir
}
}
if ( isSearchVisible ) {
// Effectuer la recherche
await searchInput . fill ( searchTerm ) ;
await page . waitForTimeout ( 1000 ) ; // Attendre le debounce
// 🔴 FIX: Utiliser getByText pour une recherche directe et fiable
const alphaPlaylist = page . getByText ( ` ${ searchTerm } Alpha ` ) ;
const betaPlaylist = page . getByText ( ` ${ searchTerm } Beta ` ) ;
// differentName est défini dans le scope ci-dessus
const differentPlaylist = page . getByText ( differentName ) ;
await expect ( alphaPlaylist ) . toBeVisible ( { timeout : 5000 } ) ;
await expect ( betaPlaylist ) . toBeVisible ( { timeout : 5000 } ) ;
await expect ( differentPlaylist ) . not . toBeVisible ( ) ;
console . log ( '✅ [PLAYLISTS] Search functionality works correctly' ) ;
} else {
console . log ( 'ℹ ️ [PLAYLISTS] Search functionality not implemented yet' ) ;
}
} ) ;
/ * *
* FINAL VERIFICATIONS
* /
2026-01-16 14:16:53 +00:00
test . afterEach ( async ( { } , testInfo ) = > {
2025-12-22 21:00:50 +00:00
console . log ( '\n📊 [PLAYLISTS] === Final Verifications ===' ) ;
if ( consoleErrors . length > 0 ) {
console . log ( ` 🔴 [PLAYLISTS] Console errors ( ${ consoleErrors . length } ): ` ) ;
consoleErrors . forEach ( ( error ) = > {
console . log ( ` - ${ error } ` ) ;
} ) ;
} else {
console . log ( '✅ [PLAYLISTS] No console errors' ) ;
}
if ( networkErrors . length > 0 ) {
console . log ( ` 🔴 [PLAYLISTS] Network errors ( ${ networkErrors . length } ): ` ) ;
networkErrors . forEach ( ( error ) = > {
console . log ( ` - ${ error . method } ${ error . url } : ${ error . status } ` ) ;
} ) ;
} else {
console . log ( '✅ [PLAYLISTS] No network errors' ) ;
}
} ) ;
} ) ;