Files
scrying-pool/tests/unit/core/ScenePresetManager.test.js
uberwald 5dc9b3b8d4
CI / ci (push) Failing after 7s
Module cleanup and tests
2026-05-24 23:13:45 +02:00

945 lines
32 KiB
JavaScript

/**
* ScenePresetManager unit tests.
*
* Import rule: tests may import from src/ but test files themselves are not subject to
* the src/ import boundary rules (they're in tests/).
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ScenePresetManager } from '../../../src/core/ScenePresetManager.js';
import { createScenePreset, MAX_PRESETS_PER_WORLD } from '../../../src/contracts/scene-preset.js';
import { SOCKET_EVENTS } from '../../../src/contracts/socket-message.js';
// Test helper: create a mock FoundryAdapter surface
/**
* Creates a mock adapter with minimal required surfaces for ScenePresetManager testing.
* @param {Partial<import('../../../src/foundry/FoundryAdapter.js').FoundryAdapter>} overrides
* @returns {import('../../../src/foundry/FoundryAdapter.js').FoundryAdapter}
*/
function createMockAdapter(overrides = {}) {
return {
scenes: {
current: vi.fn(() => ({ id: 'scene1', getFlag: vi.fn(), setFlag: vi.fn().mockResolvedValue({}) })),
...overrides.scenes,
},
users: {
isGM: vi.fn(() => true),
current: vi.fn(() => ({ id: 'gm1', name: 'Test GM' })),
all: vi.fn(() => [{ id: 'user1' }, { id: 'user2' }]),
...overrides.users,
},
settings: {
get: vi.fn((key) => {
// Default: auto-apply enabled
if (key === 'autoApplyEnabled') return true;
return null;
}),
set: vi.fn().mockResolvedValue({}),
...overrides.settings,
},
hooks: {
on: vi.fn(() => 42),
off: vi.fn(),
callAll: vi.fn(),
once: vi.fn(),
...overrides.hooks,
},
socket: {
emit: vi.fn(),
on: vi.fn(),
off: vi.fn(),
...overrides.socket,
},
notifications: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
...overrides.notifications,
},
i18n: {
localize: vi.fn((key) => key),
...overrides.i18n,
},
...overrides,
};
}
// Test helper: create a mock StateStore surface
function createMockStateStore(overrides = {}) {
return {
getState: vi.fn(() => 'active'),
getMatrix: vi.fn(() => ({ _version: 1, matrix: { user1: 'active', user2: 'active' } })),
setMatrix: vi.fn().mockResolvedValue({}),
init: vi.fn(),
...overrides,
};
}
// Test helper: create a mock SocketHandler surface
function createMockSocketHandler(overrides = {}) {
return {
emit: vi.fn(),
registerPendingOp: vi.fn(),
confirmPendingOp: vi.fn(),
setReady: vi.fn(),
destroy: vi.fn(),
...overrides,
};
}
// Test helper: create a valid ScenePreset
function createTestPreset(name = 'Test Preset', matrix = { user1: 'active', user2: 'hidden' }) {
return createScenePreset(name, matrix);
}
// Test helper: create a scene flag presets object
function createSceneFlagPresets(presets = {}) {
return { _version: 1, presets };
}
// ============================================================================
// ScenePresetManager Tests
// ============================================================================
describe('ScenePresetManager', () => {
let adapter;
let stateStore;
let socketHandler;
let manager;
beforeEach(() => {
vi.clearAllMocks();
adapter = createMockAdapter();
stateStore = createMockStateStore();
socketHandler = createMockSocketHandler();
});
afterEach(() => {
vi.restoreAllMocks();
});
// --------------------------------------------------------------------------
// Constructor Tests
// --------------------------------------------------------------------------
describe('constructor()', () => {
it('should throw TypeError when adapter is null', () => {
expect(() => new ScenePresetManager(null, stateStore, socketHandler)).toThrow(TypeError);
});
it('should throw TypeError when adapter is not an object', () => {
expect(() => new ScenePresetManager('invalid', stateStore, socketHandler)).toThrow(TypeError);
});
it('should throw TypeError when stateStore is null', () => {
expect(() => new ScenePresetManager(adapter, null, socketHandler)).toThrow(TypeError);
});
it('should throw TypeError when stateStore is not an object', () => {
expect(() => new ScenePresetManager(adapter, 'invalid', socketHandler)).toThrow(TypeError);
});
it('should throw TypeError when socketHandler is null', () => {
expect(() => new ScenePresetManager(adapter, stateStore, null)).toThrow(TypeError);
});
it('should throw TypeError when socketHandler is not an object', () => {
expect(() => new ScenePresetManager(adapter, stateStore, 'invalid')).toThrow(TypeError);
});
it('should accept valid dependencies and initialize internal state', () => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
expect(manager._adapter).toBe(adapter);
expect(manager._stateStore).toBe(stateStore);
expect(manager._socketHandler).toBe(socketHandler);
expect(manager._presetsCache).toBeInstanceOf(Map);
});
it('should be side-effect-free: no hooks registered in constructor', () => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
expect(adapter.hooks.on).not.toHaveBeenCalled();
expect(adapter.socket.on).not.toHaveBeenCalled();
expect(socketHandler.emit).not.toHaveBeenCalled();
});
});
// --------------------------------------------------------------------------
// init() Tests
// --------------------------------------------------------------------------
describe('init()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
});
it('should load presets from current scene on init', () => {
const mockScene = { id: 'scene1', getFlag: vi.fn().mockReturnValue(null) };
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
expect(adapter.scenes.current).toHaveBeenCalled();
});
it('should handle missing current scene gracefully', () => {
adapter.scenes.current.mockReturnValue(null);
expect(() => manager.init()).not.toThrow();
});
it('should be idempotent: calling init() multiple times should not cause issues', () => {
const mockScene = { id: 'scene1', getFlag: vi.fn().mockReturnValue(null) };
adapter.scenes.current.mockReturnValue(mockScene);
expect(() => {
manager.init();
manager.init();
}).not.toThrow();
});
});
// --------------------------------------------------------------------------
// teardown() Tests
// --------------------------------------------------------------------------
describe('teardown()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
// Pre-populate cache
manager._presetsCache.set('preset1', createTestPreset('preset1'));
});
it('should clear the presets cache', () => {
expect(manager._presetsCache.size).toBe(1);
manager.teardown();
expect(manager._presetsCache.size).toBe(0);
});
it('should be idempotent: calling teardown() multiple times should not cause issues', () => {
expect(() => {
manager.teardown();
manager.teardown();
}).not.toThrow();
});
it('should not throw when called before init()', () => {
const freshManager = new ScenePresetManager(adapter, stateStore, socketHandler);
expect(() => freshManager.teardown()).not.toThrow();
});
});
// --------------------------------------------------------------------------
// save() Tests
// --------------------------------------------------------------------------
describe('save()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
});
it('should throw TypeError when name is null', async () => {
await expect(manager.save(null)).rejects.toThrow(TypeError);
});
it('should throw TypeError when name is empty string', async () => {
await expect(manager.save('')).rejects.toThrow(TypeError);
});
it('should throw TypeError when name is not a string', async () => {
await expect(manager.save(123)).rejects.toThrow(TypeError);
});
it('should throw TypeError when max presets (50) is reached', async () => {
// Pre-populate with 50 presets
const presets = {};
for (let i = 0; i < MAX_PRESETS_PER_WORLD; i++) {
presets[`preset${i}`] = createTestPreset(`preset${i}`);
}
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
const freshManager = new ScenePresetManager(adapter, stateStore, socketHandler);
freshManager.init();
await expect(freshManager.save('new-preset')).rejects.toThrow(TypeError);
});
it('should save preset with current visibility matrix', async () => {
const matrix = { user1: 'active', user2: 'hidden' };
stateStore.getMatrix.mockReturnValue({ _version: 1, matrix });
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
const preset = await manager.save('Combat View');
expect(preset.name).toBe('Combat View');
expect(preset.matrix).toEqual(matrix);
expect(mockScene.setFlag).toHaveBeenCalled();
});
it('should return the created preset', async () => {
const matrix = { user1: 'active', user2: 'hidden' };
stateStore.getMatrix.mockReturnValue({ _version: 1, matrix });
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
const preset = await manager.save('Test Preset');
expect(preset).toBeDefined();
expect(preset.name).toBe('Test Preset');
expect(preset._version).toBe(1);
expect(preset.createdAt).toBeDefined();
expect(preset.updatedAt).toBeDefined();
});
it('should use adapter.i18n.localize for notification messages', async () => {
const matrix = { user1: 'active' };
stateStore.getMatrix.mockReturnValue({ _version: 1, matrix });
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
adapter.i18n.localize.mockReturnValue('Preset saved');
await manager.save('Test Preset');
expect(adapter.i18n.localize).toHaveBeenCalled();
});
});
// --------------------------------------------------------------------------
// Duplicate Name Handling Tests
// --------------------------------------------------------------------------
describe('save() duplicate name handling', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
// Start with empty presets to allow first save to succeed
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets({})),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
});
it('should detect duplicate preset name', async () => {
// First save should succeed
stateStore.getMatrix.mockReturnValue({ _version: 1, matrix: { user1: 'active' } });
await manager.save('Existing');
// Second save with same name should fail
await expect(manager.save('Existing')).rejects.toThrow(TypeError);
});
});
// --------------------------------------------------------------------------
// load() Tests
// --------------------------------------------------------------------------
describe('load()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
const presets = {
'Test Preset': createTestPreset('Test Preset', { user1: 'active', user2: 'hidden' }),
};
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
});
it('should throw TypeError when name is null', async () => {
await expect(manager.load(null)).rejects.toThrow(TypeError);
});
it('should throw TypeError when name is empty string', async () => {
await expect(manager.load('')).rejects.toThrow(TypeError);
});
it('should throw TypeError when preset not found', async () => {
await expect(manager.load('NonExistent')).rejects.toThrow(TypeError);
});
it('should load preset and apply its matrix via StateStore.setMatrix()', async () => {
await manager.load('Test Preset');
expect(stateStore.setMatrix).toHaveBeenCalledWith({
_version: 1,
matrix: { user1: 'active', user2: 'hidden' },
});
});
it('should emit socket message for preset applied', async () => {
await manager.load('Test Preset');
expect(socketHandler.emit).toHaveBeenCalledWith(
SOCKET_EVENTS.PRESET_APPLIED,
expect.objectContaining({
presetName: 'Test Preset',
})
);
});
it('should emit notification via adapter.notifications', async () => {
adapter.i18n.localize.mockReturnValue('GM applied preset: Test Preset');
await manager.load('Test Preset');
expect(adapter.notifications.info).toHaveBeenCalledWith('GM applied preset: Test Preset');
});
it('should return without error on successful load', async () => {
await expect(manager.load('Test Preset')).resolves.not.toThrow();
});
});
// --------------------------------------------------------------------------
// list() Tests
// --------------------------------------------------------------------------
describe('list()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
});
it('should return empty array when no scene is active', () => {
adapter.scenes.current.mockReturnValue(null);
manager.init();
const presets = manager.list();
expect(presets).toEqual([]);
});
it('should return all presets for current scene', () => {
const presets = {
'Preset 1': createTestPreset('Preset 1'),
'Preset 2': createTestPreset('Preset 2'),
};
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
const result = manager.list();
expect(result).toHaveLength(2);
expect(result[0].name).toBe('Preset 1');
expect(result[1].name).toBe('Preset 2');
});
it('should return empty array when no presets exist', () => {
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
const presets = manager.list();
expect(presets).toEqual([]);
});
it('should validate and filter invalid presets', () => {
const presets = {
'Valid': createTestPreset('Valid'),
'Invalid': { invalid: true },
};
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
const result = manager.list();
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Valid');
});
});
// --------------------------------------------------------------------------
// delete() Tests
// --------------------------------------------------------------------------
describe('delete()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
});
it('should throw TypeError when name is null', async () => {
await expect(manager.delete(null)).rejects.toThrow(TypeError);
});
it('should throw TypeError when name is empty string', async () => {
await expect(manager.delete('')).rejects.toThrow(TypeError);
});
it('should delete preset from scene flag', async () => {
const presets = {
'To Delete': createTestPreset('To Delete'),
'To Keep': createTestPreset('To Keep'),
};
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
await manager.delete('To Delete');
const setFlagCall = mockScene.setFlag.mock.calls[0];
expect(setFlagCall[2].presets).toHaveProperty('To Keep');
expect(setFlagCall[2].presets).not.toHaveProperty('To Delete');
});
it('should return without error on successful delete', async () => {
const presets = { 'To Delete': createTestPreset('To Delete') };
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
await expect(manager.delete('To Delete')).resolves.not.toThrow();
});
});
// --------------------------------------------------------------------------
// rename() Tests
// --------------------------------------------------------------------------
describe('rename()', () => {
beforeEach(() => {
manager = new ScenePresetManager(adapter, stateStore, socketHandler);
});
it('should throw TypeError when oldName is null', async () => {
await expect(manager.rename(null, 'New Name')).rejects.toThrow(TypeError);
});
it('should throw TypeError when newName is null', async () => {
await expect(manager.rename('Old Name', null)).rejects.toThrow(TypeError);
});
it('should throw TypeError when oldName not found', async () => {
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets({})),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
await expect(manager.rename('NonExistent', 'New Name')).rejects.toThrow(TypeError);
});
it('should throw TypeError when newName conflicts with existing preset', async () => {
const presets = {
'Existing': createTestPreset('Existing'),
'Old Name': createTestPreset('Old Name'),
};
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
await expect(manager.rename('Old Name', 'Existing')).rejects.toThrow(TypeError);
});
it('should rename preset and update timestamps', async () => {
const presets = { 'Old Name': createTestPreset('Old Name') };
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
await manager.rename('Old Name', 'New Name');
const setFlagCall = mockScene.setFlag.mock.calls[0];
expect(setFlagCall[2].presets['New Name']).toBeDefined();
expect(setFlagCall[2].presets['Old Name']).toBeUndefined();
});
it('should return the renamed preset', async () => {
const presets = { 'Old Name': createTestPreset('Old Name') };
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(createSceneFlagPresets(presets)),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
manager.init();
const preset = await manager.rename('Old Name', 'New Name');
expect(preset.name).toBe('New Name');
});
});
// --------------------------------------------------------------------------
// Story 3.2: Auto-Apply Tests
// NOTE: These tests are written BEFORE implementation (TDD Red Phase)
// They will FAIL until the Story 3.2 implementation is complete.
// --------------------------------------------------------------------------
describe('constructor() with visibilityManager [Story 3.2]', () => {
let visibilityManager;
beforeEach(() => {
visibilityManager = {
applyMatrix: vi.fn().mockResolvedValue({}),
getMatrix: vi.fn(() => ({ _version: 1, matrix: {} })),
};
});
it('STORY32-TDD: should accept visibilityManager as 4th parameter', () => {
// This test drives the constructor extension for Story 3.2
// Currently fails because constructor only accepts 3 parameters
expect(() => {
// @ts-expect-error - Adding 4th parameter for Story 3.2
new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
}).not.toThrow();
});
it('STORY32-TDD: should store visibilityManager reference', () => {
// This test drives the internal field storage
// @ts-expect-error - Adding 4th parameter for Story 3.2
const manager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
expect(manager._visibilityManager).toBe(visibilityManager);
});
it('STORY32-TDD: should still validate first 3 parameters', () => {
// Ensure backward compatibility with validation
expect(() => {
// @ts-expect-error - Adding 4th parameter for Story 3.2
new ScenePresetManager(null, stateStore, socketHandler, visibilityManager);
}).toThrow(TypeError);
});
});
describe('onSceneActivate() [Story 3.2]', () => {
let mockScene;
let visibilityManager;
beforeEach(() => {
visibilityManager = {
applyMatrix: vi.fn().mockResolvedValue({}),
getMatrix: vi.fn(() => ({ _version: 1, matrix: {} })),
};
mockScene = {
id: 'scene1',
getFlag: vi.fn(),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
adapter.settings.get.mockReturnValue(true); // Global auto-apply enabled
// @ts-expect-error - Adding 4th parameter for Story 3.2
manager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
manager.init();
});
it('STORY32-TDD: should do nothing when auto-apply is globally disabled', async () => {
adapter.settings.get.mockReturnValue(false);
await manager.onSceneActivate(mockScene);
expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
});
it('STORY32-TDD: should do nothing when scene has no auto-apply config', async () => {
mockScene.getFlag.mockReturnValue(null);
await manager.onSceneActivate(mockScene);
expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
});
it('STORY32-TDD: should do nothing when auto-apply is disabled for this scene', async () => {
mockScene.getFlag.mockReturnValue({
_version: 1,
presets: {},
autoApply: { enabled: false, presetName: null, preDelay: 0 }
});
await manager.onSceneActivate(mockScene);
expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
});
it('STORY32-TDD: should apply preset after pre-delay when auto-apply enabled', async () => {
vi.useFakeTimers();
const preset = createTestPreset('Combat', { user1: 'hidden', user2: 'active' });
manager._presetsCache.set('Combat', preset);
mockScene.getFlag.mockReturnValue({
_version: 1,
presets: { Combat: preset },
autoApply: { enabled: true, presetName: 'Combat', preDelay: 1000 }
});
const promise = manager.onSceneActivate(mockScene);
// Fast-forward past pre-delay
vi.advanceTimersByTime(1000);
await promise;
expect(visibilityManager.applyMatrix).toHaveBeenCalledWith(preset.matrix);
expect(socketHandler.emit).toHaveBeenCalledWith(
SOCKET_EVENTS.PRESET_APPLIED,
expect.objectContaining({ presetName: 'Combat', autoApplied: true })
);
vi.useRealTimers();
});
it('STORY32-TDD: should clear pre-delay timer on new scene activation', async () => {
vi.useFakeTimers();
const preset = createTestPreset('Combat', { user1: 'hidden' });
manager._presetsCache.set('Combat', preset);
const mockScene2 = {
id: 'scene2',
getFlag: vi.fn().mockReturnValue({
_version: 1,
presets: {},
autoApply: { enabled: true, presetName: 'Combat', preDelay: 5000 }
}),
setFlag: vi.fn().mockResolvedValue({}),
};
// Start first scene activation
const promise1 = manager.onSceneActivate(mockScene);
// Activate second scene before first timer fires
vi.advanceTimersByTime(1000);
const promise2 = manager.onSceneActivate(mockScene2);
// First scene's timer should be cleared
vi.advanceTimersByTime(4000);
// Second scene should be applied
vi.advanceTimersByTime(1000);
await Promise.all([promise1, promise2]);
expect(visibilityManager.applyMatrix).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
});
describe('applyPreset() with auto-apply [Story 3.2]', () => {
let visibilityManager;
beforeEach(() => {
visibilityManager = {
applyMatrix: vi.fn().mockResolvedValue({}),
};
// @ts-expect-error - Adding 4th parameter for Story 3.2
manager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
const preset = createTestPreset('Test Preset', { user1: 'active', user2: 'hidden' });
manager._presetsCache.set('Test Preset', preset);
});
it('STORY32-TDD: should apply preset matrix via visibilityManager', async () => {
await manager.applyPreset('Test Preset', { autoApplied: true });
expect(visibilityManager.applyMatrix).toHaveBeenCalledWith({ user1: 'active', user2: 'hidden' });
});
it('STORY32-TDD: should emit socket message with autoApplied flag', async () => {
await manager.applyPreset('Test Preset', { autoApplied: true });
expect(socketHandler.emit).toHaveBeenCalledWith(
SOCKET_EVENTS.PRESET_APPLIED,
expect.objectContaining({ presetName: 'Test Preset', autoApplied: true })
);
});
it('STORY32-TDD: should throw when preset not found', async () => {
await expect(manager.applyPreset('NonExistent', { autoApplied: true }))
.rejects.toThrow(TypeError);
});
});
describe('configureAutoApply() [Story 3.2]', () => {
beforeEach(() => {
// @ts-expect-error - Adding 4th parameter for Story 3.2
manager = new ScenePresetManager(adapter, stateStore, socketHandler, {});
});
it('STORY32-TDD: should update scene flag with auto-apply config', async () => {
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
await manager.configureAutoApply(mockScene, {
enabled: true,
presetName: 'Combat',
preDelay: 1000
});
expect(mockScene.setFlag).toHaveBeenCalledWith(
'scrying-pool',
'presets',
expect.objectContaining({
_version: 1,
presets: {},
autoApply: { enabled: true, presetName: 'Combat', preDelay: 1000 }
})
);
});
it('STORY32-TDD: should validate config parameters', async () => {
const mockScene = {
id: 'scene1',
getFlag: vi.fn().mockReturnValue(null),
setFlag: vi.fn().mockResolvedValue({}),
};
adapter.scenes.current.mockReturnValue(mockScene);
// Invalid preDelay (negative)
await expect(manager.configureAutoApply(mockScene, {
enabled: true,
presetName: 'Combat',
preDelay: -100
})).rejects.toThrow(TypeError);
// Invalid preDelay (over max)
await expect(manager.configureAutoApply(mockScene, {
enabled: true,
presetName: 'Combat',
preDelay: 6000
})).rejects.toThrow(TypeError);
});
});
describe('_getAutoApplyConfig() [Story 3.2]', () => {
beforeEach(() => {
// @ts-expect-error - Adding 4th parameter for Story 3.2
manager = new ScenePresetManager(adapter, stateStore, socketHandler, {});
});
it('STORY32-TDD: should return default config when autoApply field missing', () => {
// _getAutoApplyConfig takes flagData, not scene
const flagData = {
_version: 1,
presets: {}
// autoApply field missing
};
const config = manager._getAutoApplyConfig(flagData);
expect(config).toEqual({ enabled: false, presetName: null, preDelay: 0 });
});
it('STORY32-TDD: should return stored config when present', () => {
// _getAutoApplyConfig takes flagData, not scene
const flagData = {
_version: 1,
presets: {},
autoApply: { enabled: true, presetName: 'Combat', preDelay: 500 }
};
const config = manager._getAutoApplyConfig(flagData);
expect(config).toEqual({ enabled: true, presetName: 'Combat', preDelay: 500 });
});
it('STORY32-TDD: should return defaults for invalid flagData', () => {
const config = manager._getAutoApplyConfig(null);
expect(config).toEqual({ enabled: false, presetName: null, preDelay: 0 });
const config2 = manager._getAutoApplyConfig({});
expect(config2).toEqual({ enabled: false, presetName: null, preDelay: 0 });
});
});
describe('_applyWithDelay() [Story 3.2]', () => {
let visibilityManager;
beforeEach(() => {
visibilityManager = {
applyMatrix: vi.fn().mockResolvedValue({}),
};
// @ts-expect-error - Adding 4th parameter for Story 3.2
manager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager);
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('STORY32-TDD: should apply preset after specified delay', async () => {
const mockScene = { id: 'scene1' };
const preset = createTestPreset('Test', { user1: 'hidden' });
manager._presetsCache.set('Test', preset);
// @ts-expect-error - _applyWithDelay is private, accessing for testing
manager._applyWithDelay(mockScene, 'Test', 500);
// Before delay, not applied
expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
// After delay, applied
vi.advanceTimersByTime(500);
// Need to await any async operations
await vi.waitFor(() => {
expect(visibilityManager.applyMatrix).toHaveBeenCalled();
});
});
it('STORY32-TDD: should be cancellable via clear method', async () => {
const mockScene = { id: 'scene1' };
const preset = createTestPreset('Test', { user1: 'hidden' });
manager._presetsCache.set('Test', preset);
// @ts-expect-error - _applyWithDelay is private, accessing for testing
const timerId = manager._applyWithDelay(mockScene, 'Test', 500);
// Clear before delay
clearTimeout(timerId);
vi.advanceTimersByTime(1000);
expect(visibilityManager.applyMatrix).not.toHaveBeenCalled();
});
});
});