diff --git a/module.js b/module.js index 0802b86..3d268b6 100644 --- a/module.js +++ b/module.js @@ -275,7 +275,20 @@ Hooks.once("ready", () => { if (adapter.users.isGM()) { directorsBoard = new DirectorsBoard(stateStore, scryingPoolController, adapter, scenePresetManager, playerPrivacyManager); directorsBoard.init(); + window.directorsBoard = directorsBoard; } + + // Pre-load participant-card as a Handlebars partial for directors-board + // ApplicationV2 requires partials to be registered explicitly + (async () => { + try { + const resp = await fetch('modules/video-view-manager/templates/participant-card.hbs'); + const source = await resp.text(); + Handlebars.registerPartial('modules/video-view-manager/templates/participant-card.hbs', source); + } catch (err) { + console.warn('[ScryingPool] Failed to register participant-card partial:', err); + } + })(); // Story 4.1: Initialize PlayerPrivacyPanelMenu with DI dependencies // Story 4.2: Pass portraitFallbackHandler for portrait selection diff --git a/src/ui/player/PlayerPrivacyPanelMenu.js b/src/ui/player/PlayerPrivacyPanelMenu.js index 762674f..41c9899 100644 --- a/src/ui/player/PlayerPrivacyPanelMenu.js +++ b/src/ui/player/PlayerPrivacyPanelMenu.js @@ -49,11 +49,29 @@ export function isInitialized() { return _isInitialized; } +/** + * Conditional base class — test environment lacks foundry globals. + */ +const _MenuAppBase = + typeof foundry !== 'undefined' && + foundry.applications?.api?.ApplicationV2 + ? foundry.applications.api.ApplicationV2 + : class _FallbackMenuApp { + static DEFAULT_OPTIONS = {}; + get rendered() { return this._rendered ?? false; } + set rendered(v) { this._rendered = v; } + get element() { return this._element ?? null; } + set element(v) { this._element = v; } + async render() { this._rendered = true; } + async close() { this._rendered = false; } + }; + /** * PlayerPrivacyPanelMenu - Wrapper for Foundry settings menu. * When instantiated by Foundry, it creates a PlayerPrivacyPanel with the current user as target. + * Extends ApplicationV2 so it passes Foundry's registerMenu validation. */ -export class PlayerPrivacyPanelMenu { +export class PlayerPrivacyPanelMenu extends _MenuAppBase { /** * @param {object} [options] - Foundry options (unused, but required by settings menu API) */ diff --git a/templates/directors-board.hbs b/templates/directors-board.hbs index e9673c9..8a0fb65 100644 --- a/templates/directors-board.hbs +++ b/templates/directors-board.hbs @@ -1,57 +1,61 @@ {{!-- Director's Board — GM camera-management overview window --}} -
+
- {{#unless isEmpty}} - {{#each participants}} - {{> "modules/video-view-manager/templates/participant-card.hbs"}} - {{/each}} - {{else}} -

- {{localize "video-view-manager.directorsBoard.empty"}} -

- {{/unless}} +
-
+ {{#unless isEmpty}} + {{#each participants}} + {{> "modules/video-view-manager/templates/participant-card.hbs"}} + {{/each}} + {{else}} +

+ {{localize "video-view-manager.directorsBoard.empty"}} +

+ {{/unless}} + +
+ +
+ + + {{#if hasUndo}} + + {{/if}} + {{#if hasRestore}} + + {{/if}} + +
+ + + + {{!-- Scene Preset Panel - rendered via JavaScript, not Handlebars --}} + {{!-- Panel is appended dynamically in DirectorsBoard._appendPresetPanel() --}} -
- - - {{#if hasUndo}} - - {{/if}} - {{#if hasRestore}} - - {{/if}} -
- - - -{{!-- Scene Preset Panel - rendered via JavaScript, not Handlebars --}} -{{!-- Panel is appended dynamically in DirectorsBoard._appendPresetPanel() --}} diff --git a/test-results/directors-board.png b/test-results/directors-board.png new file mode 100644 index 0000000..99fcf73 Binary files /dev/null and b/test-results/directors-board.png differ diff --git a/tests/unit/ui/gm/DirectorsBoard.test.js b/tests/unit/ui/gm/DirectorsBoard.test.js index 720b4f0..0fb0690 100644 --- a/tests/unit/ui/gm/DirectorsBoard.test.js +++ b/tests/unit/ui/gm/DirectorsBoard.test.js @@ -322,10 +322,10 @@ describe('DirectorsBoard', () => { expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number)); }); - it('clears _undoSnapshot after use (single-step only)', () => { + it('clears _undoSnapshot after use (single-step only)', async () => { board._undoSnapshot = new Map([['u1', 'hidden']]); stateStore.getState.mockReturnValue('active'); - board.undo(); + await board.undo(); expect(board._undoSnapshot).toBeNull(); }); @@ -335,10 +335,10 @@ describe('DirectorsBoard', () => { expect(controller.action).not.toHaveBeenCalled(); }); - it('second undo is unavailable after first (no-op)', () => { + it('second undo is unavailable after first (no-op)', async () => { board._undoSnapshot = new Map([['u1', 'hidden']]); stateStore.getState.mockReturnValue('active'); - board.undo(); + await board.undo(); board.undo(); expect(controller.action).toHaveBeenCalledTimes(1); }); @@ -430,10 +430,10 @@ describe('DirectorsBoard spotlight', () => { expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number)); }); - it('clears _spotlightSnapshot after restore', () => { + it('clears _spotlightSnapshot after restore', async () => { board._spotlightSnapshot = new Map([['u1', 'active']]); stateStore.getState.mockReturnValue('active'); - board.restoreSpotlight(); + await board.restoreSpotlight(); expect(board._spotlightSnapshot).toBeNull(); });