/** * 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} 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( 'video-view-manager', '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(); }); }); });