diff --git a/src/ui/gm/DirectorsBoard.js b/src/ui/gm/DirectorsBoard.js index 4f05fce..a2deaa6 100644 --- a/src/ui/gm/DirectorsBoard.js +++ b/src/ui/gm/DirectorsBoard.js @@ -22,6 +22,9 @@ const _AppBase = : class _FallbackApp { static DEFAULT_OPTIONS = {}; static PARTS = {}; + constructor(options = {}) { + this.options = options; + } get rendered() { return this._rendered ?? false; } set rendered(v) { this._rendered = v; } get element() { return this._element ?? null; } @@ -115,12 +118,16 @@ export class DirectorsBoard extends _AppBase { try { const saved = game.user?.getFlag('video-view-manager', 'directorsBoardState'); if (saved?.open === true && saved.left != null && saved.top != null) { - this.options.position = { - left: saved.left, - top: saved.top, - width: saved.width ?? 400, - height: saved.height ?? 300, - }; + // Ensure options.position exists and is mutable + if (this.options?.position) { + // Use Object.assign to avoid TypeError when options is frozen (Foundry ApplicationV2) + Object.assign(this.options.position, { + left: saved.left, + top: saved.top, + width: saved.width ?? 400, + height: saved.height ?? 300, + }); + } } } catch (err) { console.error('[ScryingPool] Failed to load directors board position:', err); diff --git a/tests/unit/ui/gm/DirectorsBoard.test.js b/tests/unit/ui/gm/DirectorsBoard.test.js index 0fb0690..c2a3441 100644 --- a/tests/unit/ui/gm/DirectorsBoard.test.js +++ b/tests/unit/ui/gm/DirectorsBoard.test.js @@ -74,6 +74,127 @@ describe('DirectorsBoard', () => { expect(board._saveDialog).toBeNull(); expect(board._loadDialog).toBeNull(); }); + + it('has DEFAULT_OPTIONS with position', () => { + expect(DirectorsBoard.DEFAULT_OPTIONS.position).toEqual({ + width: 400, + height: 300, + }); + }); + }); + + describe('_loadPosition()', () => { + // Helper to create a board with options for testing _loadPosition + function createBoardWithOptions(options = {}) { + return new DirectorsBoard( + stateStore, + controller, + adapter, + scenePresetManager, + undefined, + options + ); + } + + it('loads position from user flag when saved state has open=true', () => { + const savedState = { + open: true, + left: 100, + top: 200, + width: 500, + height: 600, + }; + game.user.getFlag.mockReturnValue(savedState); + + const boardWithOptions = createBoardWithOptions({ position: {} }); + boardWithOptions._loadPosition(); + + expect(game.user.getFlag).toHaveBeenCalledWith( + 'video-view-manager', + 'directorsBoardState' + ); + // Position should be merged into options.position (not replaced) + expect(boardWithOptions.options.position).toEqual({ + left: 100, + top: 200, + width: 500, + height: 600, + }); + }); + + it('uses defaults when saved state has no width/height', () => { + const savedState = { + open: true, + left: 100, + top: 200, + }; + game.user.getFlag.mockReturnValue(savedState); + + const boardWithOptions = createBoardWithOptions({ position: {} }); + boardWithOptions._loadPosition(); + + expect(boardWithOptions.options.position).toEqual({ + left: 100, + top: 200, + width: 400, + height: 300, + }); + }); + + it('does not modify position when saved state is null', () => { + game.user.getFlag.mockReturnValue(null); + + const boardWithOptions = createBoardWithOptions({ position: { width: 400, height: 300 } }); + const originalPosition = boardWithOptions.options.position; + boardWithOptions._loadPosition(); + + expect(boardWithOptions.options.position).toBe(originalPosition); + }); + + it('does not modify position when open is false', () => { + game.user.getFlag.mockReturnValue({ open: false, left: 100, top: 200 }); + + const boardWithOptions = createBoardWithOptions({ position: { width: 400, height: 300 } }); + const originalPosition = boardWithOptions.options.position; + boardWithOptions._loadPosition(); + + expect(boardWithOptions.options.position).toBe(originalPosition); + }); + + it('does not modify position when left is null', () => { + game.user.getFlag.mockReturnValue({ open: true, left: null, top: 200 }); + + const boardWithOptions = createBoardWithOptions({ position: { width: 400, height: 300 } }); + const originalPosition = boardWithOptions.options.position; + boardWithOptions._loadPosition(); + + expect(boardWithOptions.options.position).toBe(originalPosition); + }); + + it('does not throw when options is undefined', () => { + game.user.getFlag.mockReturnValue({ open: true, left: 100, top: 200 }); + + // Board created without options (fallback class doesn't set options) + expect(() => board._loadPosition()).not.toThrow(); + }); + + it('handles errors gracefully and logs to console', () => { + game.user.getFlag.mockImplementation(() => { + throw new Error('Flag read error'); + }); + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const boardWithOptions = createBoardWithOptions({ position: { width: 400, height: 300 } }); + const originalPosition = boardWithOptions.options.position; + + boardWithOptions._loadPosition(); + + expect(consoleSpy).toHaveBeenCalledWith( + '[ScryingPool] Failed to load directors board position:', + expect.any(Error) + ); + expect(boardWithOptions.options.position).toBe(originalPosition); + }); }); describe('init()', () => {