From 5b421d6d495491ecc4d6ba1e6485af6f716f587c Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Sun, 24 May 2026 13:50:46 +0200 Subject: [PATCH] Fix DirectorsBoard position loading error - Fixed TypeError: Cannot assign to read only property 'position' of object - Changed _loadPosition() to use Object.assign() instead of direct assignment - Added null check for this.options?.position to handle both Foundry and test environments - Updated fallback _AppBase class to store options in constructor for test compatibility - Added comprehensive tests for _loadPosition() method The error occurred because in FoundryVTT v14, ApplicationV2 freezes the options object, making direct assignment to this.options.position impossible. Using Object.assign() merges the properties instead, which works with both frozen and unfrozen objects. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- src/ui/gm/DirectorsBoard.js | 19 ++-- tests/unit/ui/gm/DirectorsBoard.test.js | 121 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 6 deletions(-) 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()', () => {