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>
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
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(', ')}`);
|
||||
}
|
||||
Reference in New Issue
Block a user