20d13fc678
- 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>
277 lines
10 KiB
JavaScript
277 lines
10 KiB
JavaScript
/**
|
|
* Epic 1: Core Camera Visibility Control - Tests E2E
|
|
*
|
|
* FR-1: GM toggles Participant visibility via right-click context menu
|
|
* FR-2: All Visibility Matrix changes are broadcast to all connected clients
|
|
* FR-3: Visibility Matrix state persists across page refreshes
|
|
* FR-4: AV Tile visual indicator distinguishes all Participant States
|
|
* FR-5: All eight Participant States render without layout disruption
|
|
* FR-6: GM always sees all activated Participant feeds
|
|
* FR-7: WebRTC track disabling (CSS fallback for v14)
|
|
* FR-8: Portrait Fallback displayed when Participant has no camera
|
|
* FR-22: Persistent feed status badge on own AV Tile
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import {
|
|
waitForFoundryReady,
|
|
waitForVVMModule,
|
|
hideParticipant,
|
|
showParticipant,
|
|
verifyAVTileState,
|
|
clickFoundryButton,
|
|
} from '../utils/foundry-helpers';
|
|
|
|
// Données de test - à adapter selon votre environnement
|
|
const TEST_PLAYERS = [
|
|
{ name: 'Player1', id: 'player1' },
|
|
{ name: 'Player2', id: 'player2' },
|
|
];
|
|
|
|
// Configuration du test
|
|
test.describe('Epic 1: Core Camera Visibility Control', () => {
|
|
// Timeout plus long pour Foundry
|
|
test.setTimeout(120000);
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Naviguer vers FoundryVTT
|
|
await page.goto('https://localhost:31000', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
|
|
// Attendre que Foundry soit prêt
|
|
await waitForFoundryReady(page);
|
|
|
|
// Attendre que le module soit chargé
|
|
await waitForVVMModule(page);
|
|
|
|
// Vérifier qu'on est en mode GM
|
|
const isGM = await page.evaluate(() => game.user?.isGM || false);
|
|
if (!isGM) {
|
|
test.skip(true, 'Test requires GM user');
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// FR-1: GM toggles Participant visibility via right-click context menu
|
|
// ============================================================================
|
|
test.describe('FR-1: Hide/Show Participant via Context Menu', () => {
|
|
test('GM can hide a participant from the table via right-click', async ({ page }) => {
|
|
// Attendre qu'une tuile AV soit présente
|
|
await page.waitForSelector('.av-tile', { state: 'visible', timeout: 10000 });
|
|
|
|
const participantName = 'Player1';
|
|
|
|
// Cacher le participant
|
|
await hideParticipant(page, participantName);
|
|
|
|
// Vérifier que le badge montre "Hidden from table"
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
await expect(tile.locator('.sp-visibility-badge')).toContainText('Hidden from table', {
|
|
timeout: 5000,
|
|
});
|
|
});
|
|
|
|
test('GM can show a hidden participant via right-click', async ({ page }) => {
|
|
const participantName = 'Player1';
|
|
|
|
// D'abord cacher
|
|
await hideParticipant(page, participantName);
|
|
|
|
// Puis montrer
|
|
await showParticipant(page, participantName);
|
|
|
|
// Vérifier que le badge montre "Live"
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
await expect(tile.locator('.sp-visibility-badge')).toContainText('Live', {
|
|
timeout: 5000,
|
|
});
|
|
});
|
|
|
|
test('Visibility change is immediate (no layout shift)', async ({ page }) => {
|
|
const participantName = 'Player1';
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
|
|
// Capturer la position avant
|
|
const beforeBox = await tile.boundingBox();
|
|
|
|
// Changer la visibilité
|
|
await hideParticipant(page, participantName);
|
|
|
|
// Capturer la position après
|
|
const afterBox = await tile.boundingBox();
|
|
|
|
// Vérifier qu'il n'y a pas de shift de layout
|
|
expect(afterBox.x).toBeCloseTo(beforeBox.x, 1);
|
|
expect(afterBox.y).toBeCloseTo(beforeBox.y, 1);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// FR-4: AV Tile visual indicator distinguishes all Participant States
|
|
// ============================================================================
|
|
test.describe('FR-4: AV Tile Visual State Indicators', () => {
|
|
test('Hidden state shows grey overlay + lock icon + tooltip', async ({ page }) => {
|
|
const participantName = 'Player1';
|
|
|
|
// Cacher le participant
|
|
await hideParticipant(page, participantName);
|
|
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
|
|
// Vérifier l'overlay gris
|
|
const overlay = tile.locator('.sp-visibility-overlay');
|
|
await expect(overlay).toBeVisible();
|
|
|
|
// Vérifier l'icône de lock
|
|
const lockIcon = tile.locator('.fa-lock');
|
|
await expect(lockIcon).toBeVisible();
|
|
});
|
|
|
|
test('Self-muted state shows camera-off icon', async ({ page }) => {
|
|
// Simuler l'état self-muted via le module
|
|
// Note: Cela nécessite une implémentation spécifique ou un mock
|
|
// Pour les tests E2E, on peut utiliser l'API du module directement
|
|
await page.evaluate((userId) => {
|
|
if (game.modules.get('video-view-manager')) {
|
|
// Appeler l'API interne si disponible
|
|
const controller = game.modules.get('video-view-manager').api?.controller;
|
|
if (controller) {
|
|
controller.action('test', userId, 'self-muted');
|
|
}
|
|
}
|
|
}, 'player1');
|
|
|
|
const tile = page.locator(`.av-tile:has-text("Player1")`);
|
|
const badge = tile.locator('.sp-visibility-badge');
|
|
await expect(badge).toContainText('Camera paused');
|
|
});
|
|
|
|
test('Active state shows no overlay or indicator', async ({ page }) => {
|
|
const participantName = 'Player1';
|
|
|
|
// S'assurer que le participant est visible
|
|
await showParticipant(page, participantName);
|
|
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
|
|
// Vérifier qu'il n'y a pas d'overlay
|
|
const overlay = tile.locator('.sp-visibility-overlay');
|
|
await expect(overlay).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// FR-6: GM always sees all activated Participant feeds
|
|
// ============================================================================
|
|
test.describe('FR-6: GM Visibility Guarantee', () => {
|
|
test('GM can see all participant tiles regardless of visibility state', async ({ page }) => {
|
|
// Cacher plusieurs participants
|
|
for (const player of TEST_PLAYERS) {
|
|
await hideParticipant(page, player.name);
|
|
}
|
|
|
|
// Vérifier que toutes les tuiles sont toujours visibles pour le GM
|
|
for (const player of TEST_PLAYERS) {
|
|
const tile = page.locator(`.av-tile:has-text("${player.name}")`);
|
|
await expect(tile).toBeVisible({ timeout: 5000 });
|
|
|
|
// Vérifier qu'elles ont l'overlay "hidden" mais sont visibles
|
|
const overlay = tile.locator('.sp-visibility-overlay');
|
|
await expect(overlay).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('Hidden tiles have reduced opacity for GM', async ({ page }) => {
|
|
const participantName = 'Player1';
|
|
await hideParticipant(page, participantName);
|
|
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
|
|
// Vérifier l'opacité réduite via CSS
|
|
const opacity = await tile.evaluate(el => {
|
|
return window.getComputedStyle(el).opacity;
|
|
});
|
|
|
|
// L'opacité devrait être réduite (par exemple 0.5)
|
|
expect(parseFloat(opacity)).toBeLessThan(1);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// FR-8: Portrait Fallback displayed when Participant has no camera
|
|
// ============================================================================
|
|
test.describe('FR-8: Portrait Fallback', () => {
|
|
test('Never-connected participants show portrait fallback', async ({ page }) => {
|
|
// Un participant qui n'a jamais connecté son caméra
|
|
// devrait montrer le fallback
|
|
const tile = page.locator('.av-tile[data-state="never-connected"]');
|
|
|
|
if (await tile.count() > 0) {
|
|
const img = tile.locator('img.sp-portrait-fallback');
|
|
await expect(img).toBeVisible({ timeout: 5000 });
|
|
} else {
|
|
test.skip(true, 'No never-connected participants in test');
|
|
}
|
|
});
|
|
|
|
test('Cam-lost participants show portrait fallback', async ({ page }) => {
|
|
// Simuler la perte de caméra
|
|
await page.evaluate((userId) => {
|
|
if (game.modules.get('video-view-manager')) {
|
|
const controller = game.modules.get('video-view-manager').api?.controller;
|
|
if (controller) {
|
|
controller.action('test', userId, 'cam-lost');
|
|
}
|
|
}
|
|
}, 'player1');
|
|
|
|
const tile = page.locator(`.av-tile:has-text("Player1")`);
|
|
const img = tile.locator('img.sp-portrait-fallback');
|
|
await expect(img).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// FR-22: Persistent feed status badge on own AV Tile
|
|
// ============================================================================
|
|
test.describe('FR-22: Player Self Status Badge', () => {
|
|
test('Own AV Tile shows Live badge when visible', async ({ page }) => {
|
|
const tile = page.locator('.av-tile.self');
|
|
const badge = tile.locator('.sp-visibility-badge');
|
|
await expect(badge).toContainText('Live');
|
|
});
|
|
|
|
test('Own AV Tile shows Hidden by GM when hidden', async ({ page }) => {
|
|
// Se cacher soi-même (si c'est un joueur) ou simuler
|
|
// Pour les tests GM, nous pouvons vérifier sur un autre utilisateur
|
|
// qui a été caché
|
|
const participantName = 'Player1';
|
|
await hideParticipant(page, participantName);
|
|
|
|
const tile = page.locator(`.av-tile:has-text("${participantName}")`);
|
|
const badge = tile.locator('.sp-visibility-badge');
|
|
await expect(badge).toContainText('Hidden by GM');
|
|
});
|
|
|
|
test('First encounter shows explanatory tooltip', async ({ page }) => {
|
|
// Effacer le flag de premier badge
|
|
await page.evaluate(() => {
|
|
game.user?.unsetFlag('video-view-manager', 'firstBadgeEncounter');
|
|
});
|
|
|
|
// Recharger la page pour déclencher le first encounter
|
|
await page.reload();
|
|
await waitForFoundryReady(page);
|
|
await waitForVVMModule(page);
|
|
|
|
// Vérifier que le FirstEncounterPanel apparaît
|
|
await page.waitForSelector('.sp-first-encounter-panel', {
|
|
state: 'visible',
|
|
timeout: 10000,
|
|
});
|
|
});
|
|
});
|
|
});
|