Files
uberwald 20d13fc678 Story 4.2: Fix lint errors and code review findings
- Remove unused StripOverlayLayer import and stripOverlayLayer variable from module.js
- Add comprehensive JSDoc annotations to FoundryAdapter.js methods (settings, socket, users, scenes, notifications, hooks)
- Add /* global Dialog */ comment to PlayerPrivacyPanel.js for ESLint
- Remove unused _force parameter from GMPlayerPrivacySelector.js render() method
- Fix PlayerPrivacyPanelMenu.js: add constructor() to fallback class and call super()

All 862 unit tests passing. All Story 4.2 acceptance criteria met.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-24 01:25:30 +02:00

468 lines
15 KiB
JavaScript

/**
* 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<string>} 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<string>} 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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Report: ${data.title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 { color: #333; border-bottom: 2px solid #4a90d9; padding-bottom: 10px; }
h2 { color: #555; margin-top: 30px; }
.passed { color: #2e7d32; font-weight: bold; }
.failed { color: #c62828; font-weight: bold; }
.skipped { color: #757575; font-style: italic; }
.section { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.summary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; }
pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 5px; overflow-x: auto; }
.metadata { color: #666; font-size: 14px; margin-top: 20px; }
</style>
</head>
<body>
<h1>📊 Test Report: ${data.title}</h1>
<div class="summary">
<h2>Summary</h2>
<p>Total: ${data.total} | Passed: <span class="passed">${data.passed}</span> | Failed: <span class="failed">${data.failed}</span> | Skipped: <span class="skipped">${data.skipped}</span></p>
</div>
${data.sections?.map(section => `
<div class="section">
<h2>📋 ${section.title}</h2>
${section.items?.map(item => `
<p>${item.status === 'passed' ? '✅' : item.status === 'failed' ? '❌' : '⏭️'} ${item.name}</p>
`).join('')}
</div>
`).join('')}
${data.error ? `
<div class="section">
<h2>❌ Error Details</h2>
<pre>${escapeHtml(data.error.stack || data.error.message || String(data.error))}</pre>
</div>
` : ''}
${data.logs ? `
<div class="section">
<h2>📝 Test Logs</h2>
<pre>${escapeHtml(data.logs)}</pre>
</div>
` : ''}
<div class="metadata">
Generated: ${new Date().toISOString()}
</div>
</body>
</html>
`;
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
};
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<boolean>} 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<import('@playwright/test').Locator>}
*/
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(', ')}`);
}