/** * 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('scrying-pool')) { // Appeler l'API interne si disponible const controller = game.modules.get('scrying-pool').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('scrying-pool')) { const controller = game.modules.get('scrying-pool').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('scrying-pool', '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, }); }); }); });