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 <vibe@mistral.ai>
This commit is contained in:
@@ -22,6 +22,9 @@ const _AppBase =
|
|||||||
: class _FallbackApp {
|
: class _FallbackApp {
|
||||||
static DEFAULT_OPTIONS = {};
|
static DEFAULT_OPTIONS = {};
|
||||||
static PARTS = {};
|
static PARTS = {};
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
get rendered() { return this._rendered ?? false; }
|
get rendered() { return this._rendered ?? false; }
|
||||||
set rendered(v) { this._rendered = v; }
|
set rendered(v) { this._rendered = v; }
|
||||||
get element() { return this._element ?? null; }
|
get element() { return this._element ?? null; }
|
||||||
@@ -115,12 +118,16 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
try {
|
try {
|
||||||
const saved = game.user?.getFlag('video-view-manager', 'directorsBoardState');
|
const saved = game.user?.getFlag('video-view-manager', 'directorsBoardState');
|
||||||
if (saved?.open === true && saved.left != null && saved.top != null) {
|
if (saved?.open === true && saved.left != null && saved.top != null) {
|
||||||
this.options.position = {
|
// 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,
|
left: saved.left,
|
||||||
top: saved.top,
|
top: saved.top,
|
||||||
width: saved.width ?? 400,
|
width: saved.width ?? 400,
|
||||||
height: saved.height ?? 300,
|
height: saved.height ?? 300,
|
||||||
};
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ScryingPool] Failed to load directors board position:', err);
|
console.error('[ScryingPool] Failed to load directors board position:', err);
|
||||||
|
|||||||
@@ -74,6 +74,127 @@ describe('DirectorsBoard', () => {
|
|||||||
expect(board._saveDialog).toBeNull();
|
expect(board._saveDialog).toBeNull();
|
||||||
expect(board._loadDialog).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()', () => {
|
describe('init()', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user