61f362004e
- Created src/contracts/privacy-settings.js with: - PrivacySettings typedef - PRIVACY_SETTINGS_DEFAULT (both flags false) - PRIVACY_SETTING_KEYS and FEATURE_NAME_MAP constants - createPrivacySettings() factory - isValidPrivacySettings() validator - validateSettingKey(), validateSettingValue(), validateFeatureName() helpers - Created src/core/PlayerPrivacyManager.js with: - Constructor with FoundryAdapter DI validation - getSettings(userId) - retrieves settings from user flags - setSetting(userId, key, value) - async, validates, persists via user.setFlag - isOptedIn(userId, feature) - convenience method for feature checks - getAllSettings() - aggregates all users' settings (GM view) - onChange(callback) - subscription pattern for change events - teardown() - cleanup - Created tests/unit/contracts/privacy-settings.test.js - 44 tests - Created tests/unit/core/PlayerPrivacyManager.test.js - 35 tests - All tests passing, lint clean - Updated sprint-status.yaml: 4-1 from ready-for-dev to in-progress - Updated story file: Task 1 subtasks 1.1-1.8 marked complete Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
380 lines
12 KiB
JavaScript
380 lines
12 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
});
|