945 lines
32 KiB
JavaScript
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(
|
|
'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();
|
|
});
|
|
});
|
|
});
|