/** * Epic 3 Scene Presets - Integration Tests * Tests the deployed video-view-manager module on localhost:31000 * Run with: npx playwright test _bmad-output/tests/integration/epic-3-scene-presets.spec.js * * Note: Chrome DevTools must be running on port 9222 with FoundryVTT * Browser WSEndpoint: ws://localhost:9222/devtools/browser/1aeaf428-412f-4e20-9f2d-c13533d031ae */ import { chromium, expect, test } from '@playwright/test'; // Configuration const CHROME_DEVTOOLS_URL = 'ws://localhost:9222/devtools/browser/1aeaf428-412f-4e20-9f2d-c13533d031ae'; const FOUNDRY_URL = 'https://localhost:31000/game'; // ============================================================================ // Helper Functions // ============================================================================ async function waitForModule(page) { await page.waitForFunction(() => { const module = game?.modules?.get('video-view-manager'); return module?.active === true; }, { timeout: 30000 }); } async function waitForDirectorsBoard(page) { await page.waitForFunction(() => { const board = game?.modules?.get('video-view-manager')?._directorsBoard; return board?.rendered === true; }, { timeout: 10000 }); } async function clickByDataAction(page, action) { await page.evaluate((action) => { const element = document.querySelector(`[data-action="${action}"]`); if (element) element.click(); }, action); } async function getPresetCount(page) { return await page.evaluate(() => { const scene = game.scenes.current; const presets = scene?.getFlag('video-view-manager', 'presets') || {}; return Object.keys(presets.presets || {}).length; }); } async function verifyNotification(page, text) { await page.waitForFunction((text) => { const notifications = ui.notifications?.notifications || []; return notifications.some(n => n.message?.includes(text)); }, { text, timeout: 5000 }); } async function openDirectorsBoard(page) { await page.keyboard.down('Control'); await page.keyboard.press('Shift+V'); await page.keyboard.up('Control'); await waitForDirectorsBoard(page); } // ============================================================================ // Test Suite: ScenePresetManager Core Functionality // ============================================================================ test.describe('Epic 3 - ScenePresetManager Core', () => { let browser; let page; test.beforeAll(async () => { browser = await chromium.connect({ wsEndpoint: CHROME_DEVTOOLS_URL }); const pages = await browser.pages(); page = pages.find(p => p.url().includes('localhost:31000/game')) || pages[0]; if (!page.url().includes('localhost:31000')) { await page.goto(FOUNDRY_URL); } await waitForModule(page); }); test.afterAll(async () => { await browser.close(); }); test('Module is active and ScenePresetManager exists', async () => { const isActive = await page.evaluate(() => { const module = game.modules.get('video-view-manager'); return module?.active && module?.scenePresetManager; }); expect(isActive).toBe(true); }); test('Save and load preset via DirectorsBoard', async () => { await openDirectorsBoard(page); const initialCount = await getPresetCount(page); await clickByDataAction(page, 'save-preset'); await page.waitForSelector('.dialog [name="presetName"]', { timeout: 5000 }); await page.fill('.dialog [name="presetName"]', 'Epic 3 Test Preset'); await page.click('.dialog button:has-text("Save")'); await page.waitForTimeout(500); const newCount = await getPresetCount(page); expect(newCount).toBe(initialCount + 1); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog', { timeout: 5000 }); await page.click('.dialog [data-preset-name="Epic 3 Test Preset"]'); await page.click('.dialog button:has-text("Load")'); await verifyNotification(page, 'applied preset: Epic 3 Test Preset'); }); test('Duplicate preset name shows confirmation dialog', async () => { await openDirectorsBoard(page); await clickByDataAction(page, 'save-preset'); await page.waitForSelector('.dialog [name="presetName"]', { timeout: 5000 }); await page.fill('.dialog [name="presetName"]', 'Epic 3 Test Preset'); await page.waitForSelector('.dialog:has-text("already exists")', { timeout: 3000 }); }); }); // ============================================================================ // Test Suite: ConfirmationBar & Auto-Apply // ============================================================================ test.describe('Epic 3 - ConfirmationBar & Auto-Apply', () => { let browser; let page; test.beforeAll(async () => { browser = await chromium.connect({ wsEndpoint: CHROME_DEVTOOLS_URL }); const pages = await browser.pages(); page = pages.find(p => p.url().includes('localhost:31000/game')) || pages[0]; if (!page.url().includes('localhost:31000')) { await page.goto(FOUNDRY_URL); } await waitForModule(page); }); test.afterAll(async () => { await browser.close(); }); test('ConfirmationBar appears after preset load', async () => { await openDirectorsBoard(page); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog', { timeout: 5000 }); await page.click('.dialog [data-preset-name="Epic 3 Test Preset"]'); await page.click('.dialog button:has-text("Load")'); await page.waitForSelector('.sp-confirmation-bar', { timeout: 5000 }); const barText = await page.$eval('.sp-confirmation-bar', el => el.textContent); expect(barText).toContain('Preset applied'); }); test('ConfirmationBar undo reverts matrix', async () => { const initialMatrix = await page.evaluate(() => { const controller = game.modules.get('video-view-manager')?._scryingPoolController; return JSON.stringify(controller?.visibilityMatrix); }); await openDirectorsBoard(page); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog', { timeout: 5000 }); await page.click('.dialog [data-preset-name="Epic 3 Test Preset"]'); await page.click('.dialog button:has-text("Load")'); await page.waitForSelector('.sp-confirmation-bar', { timeout: 5000 }); await page.click('.sp-confirmation-bar [data-action="undo"]'); await page.waitForTimeout(500); const revertedMatrix = await page.evaluate(() => { const controller = game.modules.get('video-view-manager')?._scryingPoolController; return JSON.stringify(controller?.visibilityMatrix); }); expect(revertedMatrix).toBe(initialMatrix); }); test('ConfirmationBar auto-dismisses after 8 seconds', async () => { await openDirectorsBoard(page); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog', { timeout: 5000 }); await page.click('.dialog [data-preset-name="Epic 3 Test Preset"]'); await page.click('.dialog button:has-text("Load")'); await page.waitForSelector('.sp-confirmation-bar', { timeout: 5000 }); // Note: This test may be flaky depending on timing // The bar should auto-dismiss after 8 seconds await page.waitForTimeout(8500); const isHidden = await page.evaluate(() => { const bar = document.querySelector('.sp-confirmation-bar'); return !bar || bar.offsetParent === null; }); expect(isHidden).toBe(true); }); }); // ============================================================================ // Test Suite: Import/Export // ============================================================================ test.describe('Epic 3 - Preset Import/Export', () => { let browser; let page; test.beforeAll(async () => { browser = await chromium.connect({ wsEndpoint: CHROME_DEVTOOLS_URL }); const pages = await browser.pages(); page = pages.find(p => p.url().includes('localhost:31000/game')) || pages[0]; if (!page.url().includes('localhost:31000')) { await page.goto(FOUNDRY_URL); } await waitForModule(page); }); test.afterAll(async () => { await browser.close(); }); test('Export presets downloads JSON file', async () => { await openDirectorsBoard(page); await clickByDataAction(page, 'export-presets'); const download = await page.waitForEvent('download'); const path = await download.path(); const { readFileSync } = await import('fs'); const contents = readFileSync(path, 'utf8'); const data = JSON.parse(contents); expect(data._version).toBe(1); expect(data.presets).toBeDefined(); expect(Object.keys(data.presets).length).toBeGreaterThan(0); }); test('Import with merge adds new presets', async () => { // First export await openDirectorsBoard(page); await clickByDataAction(page, 'export-presets'); const download = await page.waitForEvent('download'); const exportPath = await download.path(); // Delete all presets await page.evaluate(() => { const scene = game.scenes.current; scene.unsetFlag('video-view-manager', 'presets'); }); // Import with merge await openDirectorsBoard(page); await clickByDataAction(page, 'import-presets'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(exportPath); await page.click('text="Merge"'); await page.click('button:has-text("Import")'); await page.waitForTimeout(1000); const finalCount = await getPresetCount(page); expect(finalCount).toBeGreaterThan(0); await verifyNotification(page, 'Imported'); }); test('Import with invalid JSON shows error', async () => { await openDirectorsBoard(page); await clickByDataAction(page, 'import-presets'); const { writeFileSync } = await import('fs'); const invalidJsonPath = '/tmp/invalid-presets.json'; writeFileSync(invalidJsonPath, '{invalid json}'); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(invalidJsonPath); await verifyNotification(page, 'Invalid JSON'); }); }); // ============================================================================ // Test Suite: Edge Cases // ============================================================================ test.describe('Epic 3 - Edge Cases', () => { let browser; let page; test.beforeAll(async () => { browser = await chromium.connect({ wsEndpoint: CHROME_DEVTOOLS_URL }); const pages = await browser.pages(); page = pages.find(p => p.url().includes('localhost:31000/game')) || pages[0]; if (!page.url().includes('localhost:31000')) { await page.goto(FOUNDRY_URL); } await waitForModule(page); }); test.afterAll(async () => { await browser.close(); }); test('Load preset with no presets shows empty message', async () => { await page.evaluate(() => { const scene = game.scenes.current; scene.unsetFlag('video-view-manager', 'presets'); }); await openDirectorsBoard(page); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog:has-text("No presets saved yet")', { timeout: 5000 }); }); test('Auto-apply respects per-scene disable', async () => { const hasSetting = await page.evaluate(() => { const scene = game.scenes.current; const autoApply = scene.getFlag('video-view-manager', 'autoApply') || {}; return autoApply.enabled !== undefined; }); expect(hasSetting).toBe(true); }); test('Socket events fire correctly', async () => { await page.evaluate(() => { window.testEvents = []; Hooks.off('scrying-pool.preset.apply'); Hooks.off('scrying-pool.preset.applied'); }); await page.evaluate(() => { Hooks.on('scrying-pool.preset.apply', () => window.testEvents.push('apply')); Hooks.on('scrying-pool.preset.applied', () => window.testEvents.push('applied')); }); await openDirectorsBoard(page); await clickByDataAction(page, 'load-preset'); await page.waitForSelector('.dialog', { timeout: 5000 }); await page.click('.dialog [data-preset-name="Epic 3 Test Preset"]'); await page.click('.dialog button:has-text("Load")'); await page.waitForTimeout(1000); const events = await page.evaluate(() => window.testEvents); expect(events).toContain('apply'); expect(events).toContain('applied'); }); });