/** * Test Utilities pour les tests E2E * * Fournit des fonctions utilitaires pour : * - Créer des fixtures de test * - Nettoyer l'état * - Générer des données de test * - Aider au debugging */ import { expect } from '@playwright/test'; /** * Crée un utilisateur de test dans FoundryVTT * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} username - Nom de l'utilisateur * @param {string} role - Rôle (Player, Game Master, etc.) * @param {string} password - Mot de passe * @returns {Promise} L'ID de l'utilisateur créé */ export async function createTestUser(page, username, role = 'Player', password = 'test123') { // Naviguer vers la gestion des utilisateurs await page.goto('https://localhost:31000/setup/users', { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('#users-list', { timeout: 10000 }); // Vérifier si l'utilisateur existe déjà const userExists = await page.locator(`#users-list [data-username="${username}"]`).count(); if (userExists > 0) { console.log(`⚠️ User "${username}" already exists`); return username; } // Cliquer sur Add User await page.locator('button:has-text("Add User")').click(); await page.waitForSelector('#add-user-dialog', { timeout: 5000 }); // Remplir le formulaire await page.locator('#add-user-dialog input[name="username"]').fill(username); await page.locator('#add-user-dialog input[name="password"]').fill(password); await page.locator('#add-user-dialog input[name="passwordConfirm"]').fill(password); await page.locator('#add-user-dialog select[name="role"]').selectOption(role); // Sauvegarder await page.locator('#add-user-dialog button:has-text("Add")').click(); // Attendre la confirmation await page.waitForSelector(`#users-list [data-username="${username}"]`, { timeout: 10000, }); console.log(`✅ Created user: ${username} (${role})`); return username; } /** * Crée une scène de test dans FoundryVTT * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} name - Nom de la scène * @returns {Promise} L'ID de la scène créée */ export async function createTestScene(page, name) { await page.goto('https://localhost:31000/setup/scenes', { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('#scenes-list', { timeout: 10000 }); // Vérifier si la scène existe const sceneExists = await page.locator(`#scenes-list [data-scene-name="${name}"]`).count(); if (sceneExists > 0) { console.log(`⚠️ Scene "${name}" already exists`); return name; } // Cliquer sur Create Scene await page.locator('button:has-text("Create Scene")').click(); await page.waitForSelector('#create-scene-dialog', { timeout: 5000 }); // Remplir le nom await page.locator('#create-scene-dialog input[name="name"]').fill(name); // Sauvegarder await page.locator('#create-scene-dialog button:has-text("Create")').click(); // Attendre la confirmation await page.waitForSelector(`#scenes-list [data-scene-name="${name}"]`, { timeout: 10000, }); console.log(`✅ Created scene: ${name}`); return name; } /** * Active une scène * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} name - Nom de la scène */ export async function activateScene(page, name) { await page.goto('https://localhost:31000/setup/scenes', { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('#scenes-list', { timeout: 10000 }); const sceneItem = page.locator(`#scenes-list [data-scene-name="${name}"]`); await sceneItem.hover(); // Cliquer sur Activate await page.locator('.scene-actions:has-text("Activate")').click(); // Attendre que la scène soit active await page.waitForFunction((sceneName) => { const scene = game.scenes?.getName(sceneName); return scene?.active === true; }, { timeout: 10000 }, name); console.log(`✅ Activated scene: ${name}`); } /** * Supprime un utilisateur de test * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} username - Nom de l'utilisateur */ export async function deleteTestUser(page, username) { await page.goto('https://localhost:31000/setup/users', { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('#users-list', { timeout: 10000 }); const userItem = page.locator(`#users-list [data-username="${username}"]`); if (await userItem.count() > 0) { await userItem.hover(); await page.locator('.user-actions:has-text("Delete")').click(); // Confirmer await page.waitForSelector('.delete-confirmation-dialog', { timeout: 5000 }); await page.locator('.delete-confirmation-dialog button:has-text("Delete")').click(); // Attendre la suppression await page.waitForSelector(`#users-list [data-username="${username}"]`, { state: 'detached', timeout: 10000, }); console.log(`✅ Deleted user: ${username}`); } else { console.log(`⚠️ User "${username}" not found`); } } /** * Supprime une scène de test * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} name - Nom de la scène */ export async function deleteTestScene(page, name) { await page.goto('https://localhost:31000/setup/scenes', { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('#scenes-list', { timeout: 10000 }); const sceneItem = page.locator(`#scenes-list [data-scene-name="${name}"]`); if (await sceneItem.count() > 0) { await sceneItem.hover(); await page.locator('.scene-actions:has-text("Delete")').click(); // Confirmer await page.waitForSelector('.delete-confirmation-dialog', { timeout: 5000 }); await page.locator('.delete-confirmation-dialog button:has-text("Delete")').click(); // Attendre la suppression await page.waitForSelector(`#scenes-list [data-scene-name="${name}"]`, { state: 'detached', timeout: 10000, }); console.log(`✅ Deleted scene: ${name}`); } else { console.log(`⚠️ Scene "${name}" not found`); } } /** * Capture un screenshot pour le debugging * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} name - Nom du screenshot */ export async function debugScreenshot(page, name) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `_bmad-output/e2e-screenshots/${name}-${timestamp}.png`; await page.screenshot({ path: filename, fullPage: true }); console.log(`📸 Screenshot saved: ${filename}`); } /** * Capture une vidéo de la page * @param {import('@playwright/test').Page} page - La page Playwright * @param {string} name - Nom de la vidéo */ export async function debugVideo(page, name) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `_bmad-output/e2e-videos/${name}-${timestamp}.webm`; await page.video()?.saveAs(filename); console.log(`🎥 Video saved: ${filename}`); } /** * Passe en mode debugging interactif * @param {import('@playwright/test').Page} page - La page Playwright */ export async function debugPause(page) { console.log('🛑 Test paused for debugging. Press any key to continue...'); await page.pause(); } /** * Affiche les informations de l'élément dans la console * @param {import('@playwright/test').Page} page - La page Playwright * @param {import('@playwright/test').Locator} locator - Le locator */ export async function debugElement(page, locator) { const tagName = await locator.evaluate(el => el.tagName); const textContent = await locator.evaluate(el => el.textContent?.trim() || ''); const className = await locator.evaluate(el => el.className); const id = await locator.evaluate(el => el.id); const boundingBox = await locator.boundingBox(); console.log('🔍 Element Debug Info:'); console.log(` Tag: ${tagName}`); console.log(` ID: ${id}`); console.log(` Classes: ${className}`); console.log(` Text: "${textContent}"`); console.log(` Bounding Box: ${JSON.stringify(boundingBox)}`); } /** * Affiche les propriétés CSS d'un élément * @param {import('@playwright/test').Page} page - La page Playwright * @param {import('@playwright/test').Locator} locator - Le locator */ export async function debugStyles(page, locator) { const styles = await locator.evaluate(el => { const computedStyle = getComputedStyle(el); const importantProps = [ 'display', 'position', 'visibility', 'opacity', 'width', 'height', 'top', 'left', 'right', 'bottom', 'color', 'background-color', 'font-size', 'z-index', 'pointer-events' ]; const result = {}; for (const prop of importantProps) { result[prop] = computedStyle.getPropertyValue(prop); } return result; }); console.log('🎨 CSS Properties:'); for (const [prop, value] of Object.entries(styles)) { console.log(` ${prop}: ${value}`); } } /** * Vérifie si un élément est visible avec plus de détails * @param {import('@playwright/test').Page} page - La page Playwright * @param {import('@playwright/test').Locator} locator - Le locator */ export async function debugVisibility(page, locator) { const isVisible = await locator.isVisible(); const isInViewport = await locator.isInViewport(); const boundingBox = await locator.boundingBox(); const opacity = await locator.evaluate(el => { let current = el; let opacity = 1; while (current && current !== document.body) { const currentOpacity = parseFloat(getComputedStyle(current).opacity); opacity *= currentOpacity; current = current.parentElement; } return opacity; }); console.log('👀 Visibility Debug:'); console.log(` Is Visible: ${isVisible}`); console.log(` In Viewport: ${isInViewport}`); console.log(` Bounding Box: ${JSON.stringify(boundingBox)}`); console.log(` Opacity (accumulated): ${opacity}`); } /** * Génère un rapport HTML pour un test * @param {object} data - Données du rapport * @param {string} filename - Nom du fichier */ export async function generateTestReport(data, filename) { const fs = require('fs'); const path = require('path'); const reportPath = `_bmad-output/e2e-reports/${filename}`; // Assurer que le dossier existe const dir = path.dirname(reportPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const html = ` Test Report: ${data.title}

📊 Test Report: ${data.title}

Summary

Total: ${data.total} | Passed: ${data.passed} | Failed: ${data.failed} | Skipped: ${data.skipped}

${data.sections?.map(section => `

📋 ${section.title}

${section.items?.map(item => `

${item.status === 'passed' ? '✅' : item.status === 'failed' ? '❌' : '⏭️'} ${item.name}

`).join('')}
`).join('')} ${data.error ? `

❌ Error Details

${escapeHtml(data.error.stack || data.error.message || String(data.error))}
` : ''} ${data.logs ? `

📝 Test Logs

${escapeHtml(data.logs)}
` : ''} `; fs.writeFileSync(reportPath, html); console.log(`📄 Report generated: ${reportPath}`); } /** * Échappe les caractères HTML * @param {string} text - Texte à échapper * @returns {string} Texte échappé */ function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, m => map[m]); } /** * Mesure le temps d'exécution d'une fonction * @param {Function} fn - Fonction à mesurer * @returns {Promise<{result: any, duration: number}>} */ export async function measureTime(fn) { const start = performance.now(); const result = await fn(); const end = performance.now(); return { result, duration: end - start }; } /** * Attend qu'une condition soit vraie * @param {Function} condition - Fonction qui retourne un booléen * @param {number} timeout - Timeout en ms * @param {number} interval - Intervalle de vérification en ms * @returns {Promise} Vrai si la condition est devenue vraie */ export async function waitForCondition(condition, timeout = 10000, interval = 500) { const start = Date.now(); while (Date.now() - start < timeout) { try { if (await condition()) { return true; } } catch (e) { // Condition a lancé une erreur, continuer à essayer } await new Promise(resolve => setTimeout(resolve, interval)); } return false; } /** * Essayez plusieurs sélecteurs jusqu'à ce qu'un fonctionne * @param {import('@playwright/test').Page} page - La page * @param {string[]} selectors - Tableau de sélecteurs * @param {object} options - Options pour waitForSelector * @returns {Promise} */ export async function waitForAnySelector(page, selectors, options = {}) { for (const selector of selectors) { try { await page.waitForSelector(selector, { ...options, timeout: options.timeout || 500 }); return page.locator(selector); } catch (e) { // Essayez le prochain } } // Aucun sélecteur n'a fonctionné throw new Error(`None of the selectors matched: ${selectors.join(', ')}`); }