// @ts-nocheck import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PresetLoadDialog } from '../../../../src/ui/gm/PresetLoadDialog.js'; // Test helper: create a mock ScenePresetManager surface function createMockScenePresetManager(overrides = {}) { return { save: vi.fn().mockResolvedValue({ _version: 1, name: 'Test Preset', matrix: {}, createdAt: Date.now(), updatedAt: Date.now() }), list: vi.fn().mockReturnValue([]), get: vi.fn().mockReturnValue(null), load: vi.fn().mockResolvedValue({}), delete: vi.fn().mockResolvedValue({}), rename: vi.fn().mockResolvedValue({}), init: vi.fn(), teardown: vi.fn(), ...overrides, }; } // Test helper: create a mock adapter surface function createMockAdapter(overrides = {}) { return { i18n: { localize: vi.fn((key) => key), ...overrides.i18n, }, notifications: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, ...overrides, }; } // Test helper: create a mock preset function createMockPreset(name = 'Test Preset') { return { _version: 1, name, matrix: { user1: 'active', user2: 'hidden' }, createdAt: Date.now(), updatedAt: Date.now(), }; } // ============================================================================ // PresetLoadDialog Tests // ============================================================================ describe('PresetLoadDialog', () => { let scenePresetManager; let adapter; let dialog; beforeEach(() => { scenePresetManager = createMockScenePresetManager(); adapter = createMockAdapter(); vi.clearAllMocks(); }); afterEach(() => { dialog = null; vi.clearAllMocks(); }); // -------------------------------------------------------------------------- // Constructor Tests // -------------------------------------------------------------------------- describe('constructor()', () => { it('should throw TypeError when scenePresetManager is null', () => { expect(() => new PresetLoadDialog(null, adapter)).toThrow(TypeError); }); it('should throw TypeError when scenePresetManager is not an object', () => { expect(() => new PresetLoadDialog('not an object', adapter)).toThrow(TypeError); }); it('should throw TypeError when adapter is null', () => { expect(() => new PresetLoadDialog(scenePresetManager, null)).toThrow(TypeError); }); it('should throw TypeError when adapter is not an object', () => { expect(() => new PresetLoadDialog(scenePresetManager, 'not an object')).toThrow(TypeError); }); it('should accept valid dependencies and initialize internal state', () => { dialog = new PresetLoadDialog(scenePresetManager, adapter); expect(dialog).toBeDefined(); expect(dialog._scenePresetManager).toBe(scenePresetManager); expect(dialog._adapter).toBe(adapter); expect(dialog._presets).toEqual([]); }); it('should be side-effect-free: no hooks registered in constructor', () => { const originalError = console.error; console.error = vi.fn(); dialog = new PresetLoadDialog(scenePresetManager, adapter); expect(console.error).not.toHaveBeenCalled(); console.error = originalError; }); it('should have DEFAULT_OPTIONS defined', () => { expect(PresetLoadDialog.DEFAULT_OPTIONS).toBeDefined(); expect(PresetLoadDialog.DEFAULT_OPTIONS.id).toBe('scrying-pool-preset-load-dialog'); expect(PresetLoadDialog.DEFAULT_OPTIONS.classes).toEqual(expect.arrayContaining(['scrying-pool', 'preset-load-dialog'])); expect(PresetLoadDialog.DEFAULT_OPTIONS.window.title).toBe('Load Scene Preset'); }); it('should have PARTS defined with template', () => { expect(PresetLoadDialog.PARTS).toBeDefined(); expect(PresetLoadDialog.PARTS.dialog).toBeDefined(); expect(PresetLoadDialog.PARTS.dialog.template).toContain('preset-load-dialog.hbs'); }); }); // -------------------------------------------------------------------------- // _prepareContext() Tests // -------------------------------------------------------------------------- describe('_prepareContext()', () => { beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); }); it('should return an object with presets array', async () => { const context = await dialog._prepareContext(); expect(context).toBeDefined(); expect(typeof context).toBe('object'); expect(context.presets).toBeDefined(); expect(Array.isArray(context.presets)).toBe(true); }); it('should return hasPresets false when no presets exist', async () => { scenePresetManager.list.mockReturnValue([]); adapter.i18n.localize = vi.fn((key) => { if (key === 'scrying-pool.presets.load.emptyMessage') return 'No presets available'; return key; }); const context = await dialog._prepareContext(); expect(context.hasPresets).toBe(false); expect(context.emptyMessage).toBe('No presets available'); }); it('should return hasPresets true when presets exist', async () => { const presets = [createMockPreset('Preset 1'), createMockPreset('Preset 2')]; scenePresetManager.list.mockReturnValue(presets); const context = await dialog._prepareContext(); expect(context.hasPresets).toBe(true); expect(context.presets).toHaveLength(2); }); it('should use i18n for labels', async () => { adapter.i18n.localize = vi.fn((key) => { const translations = { 'scrying-pool.presets.load.loadButton': 'Load', 'scrying-pool.presets.load.cancelButton': 'Cancel', 'scrying-pool.presets.load.title': 'Load Preset', 'scrying-pool.presets.load.emptyMessage': 'No presets', }; return translations[key] || key; }); const context = await dialog._prepareContext(); expect(context.loadLabel).toBe('Load'); expect(context.cancelLabel).toBe('Cancel'); expect(context.title).toBe('Load Preset'); expect(context.emptyMessage).toBe('No presets'); }); it('should store presets in internal _presets array', async () => { const presets = [createMockPreset('Preset 1')]; scenePresetManager.list.mockReturnValue(presets); await dialog._prepareContext(); expect(dialog._presets).toEqual(presets); }); }); // -------------------------------------------------------------------------- // _onRender() Tests // -------------------------------------------------------------------------- describe('_onRender()', () => { let mockElement; beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); mockElement = { querySelector: vi.fn(), querySelectorAll: vi.fn().mockReturnValue([]), addEventListener: vi.fn(), }; dialog.element = mockElement; dialog.rendered = true; }); it('should set up load button handlers for each preset', () => { const loadBtn1 = { addEventListener: vi.fn(), dataset: { action: 'load', presetName: 'Preset 1' } }; const loadBtn2 = { addEventListener: vi.fn(), dataset: { action: 'load', presetName: 'Preset 2' } }; mockElement.querySelectorAll = vi.fn().mockReturnValue([loadBtn1, loadBtn2]); dialog._onRender(mockElement); expect(loadBtn1.addEventListener).toHaveBeenCalledWith('click', expect.any(Function)); expect(loadBtn2.addEventListener).toHaveBeenCalledWith('click', expect.any(Function)); }); it('should set up cancel button handler', () => { const cancelBtn = { addEventListener: vi.fn() }; mockElement.querySelector = vi.fn((selector) => { if (selector === '[data-action="cancel"]') return cancelBtn; return null; }); dialog._onRender(mockElement); expect(cancelBtn.addEventListener).toHaveBeenCalledWith('click', expect.any(Function)); }); it('should set up keyboard handlers', () => { dialog._onRender(mockElement); expect(mockElement.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)); }); }); // -------------------------------------------------------------------------- // _onLoad() Tests // -------------------------------------------------------------------------- describe('_onLoad()', () => { beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); scenePresetManager.load = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); }); it('should throw TypeError when presetName is null', async () => { await expect(dialog._onLoad(null)).rejects.toThrow(TypeError); }); it('should throw TypeError when presetName is empty string', async () => { await expect(dialog._onLoad('')).rejects.toThrow(TypeError); }); it('should throw TypeError when presetName is not a string', async () => { await expect(dialog._onLoad(123)).rejects.toThrow(TypeError); }); it('should call scenePresetManager.load with the preset name', async () => { await dialog._onLoad('My Preset'); expect(scenePresetManager.load).toHaveBeenCalledWith('My Preset'); }); it('should close the dialog on successful load', async () => { await dialog._onLoad('My Preset'); expect(dialog.close).toHaveBeenCalled(); }); it('should show notification on successful load via adapter.notifications', async () => { adapter.i18n.localize = vi.fn((key) => { if (key === 'scrying-pool.presets.notifications.applied') return 'Applied preset: {name}'; return key; }); await dialog._onLoad('My Preset'); expect(adapter.notifications.info).toHaveBeenCalledWith('Applied preset: My Preset'); }); it('should re-throw TypeError from load', async () => { const error = new TypeError('preset "My Preset" not found'); scenePresetManager.load = vi.fn().mockRejectedValue(error); await expect(dialog._onLoad('My Preset')).rejects.toThrow(TypeError); expect(dialog.close).not.toHaveBeenCalled(); }); }); // -------------------------------------------------------------------------- // _onCancel() Tests // -------------------------------------------------------------------------- describe('_onCancel()', () => { beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); dialog.close = vi.fn().mockResolvedValue({}); }); it('should close the dialog', () => { dialog._onCancel(); expect(dialog.close).toHaveBeenCalled(); }); it('should not throw when called multiple times', () => { dialog._onCancel(); dialog._onCancel(); expect(dialog.close).toHaveBeenCalledTimes(2); }); }); // -------------------------------------------------------------------------- // _onKeydown() Tests // -------------------------------------------------------------------------- describe('_onKeydown()', () => { let mockEvent; beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); scenePresetManager.load = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); adapter.i18n.localize = vi.fn((key) => key); mockEvent = { preventDefault: vi.fn(), stopPropagation: vi.fn(), key: '', target: {}, }; }); it('should handle Escape key to cancel', () => { mockEvent.key = 'Escape'; dialog._onKeydown(mockEvent); expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled(); expect(dialog.close).toHaveBeenCalled(); }); it('should handle Enter key on load button', async () => { mockEvent.key = 'Enter'; mockEvent.target = { dataset: { action: 'load', presetName: 'My Preset' } }; await dialog._onKeydown(mockEvent); expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled(); expect(scenePresetManager.load).toHaveBeenCalledWith('My Preset'); }); it('should ignore Enter key on non-load button', async () => { mockEvent.key = 'Enter'; mockEvent.target = { dataset: { action: 'other' } }; await dialog._onKeydown(mockEvent); expect(scenePresetManager.load).not.toHaveBeenCalled(); }); it('should ignore other keys', () => { mockEvent.key = 'A'; dialog._onKeydown(mockEvent); expect(mockEvent.preventDefault).not.toHaveBeenCalled(); expect(dialog.close).not.toHaveBeenCalled(); }); }); // -------------------------------------------------------------------------- // Integration Tests // -------------------------------------------------------------------------- describe('integration', () => { beforeEach(() => { dialog = new PresetLoadDialog(scenePresetManager, adapter); }); it('should have all required methods defined', () => { expect(dialog._prepareContext).toBeDefined(); expect(dialog._onRender).toBeDefined(); expect(dialog._onLoad).toBeDefined(); expect(dialog._onCancel).toBeDefined(); expect(dialog._onKeydown).toBeDefined(); }); it('should use the correct template path', () => { expect(PresetLoadDialog.PARTS.dialog.template).toBe( 'modules/scrying-pool/templates/preset-load-dialog.hbs' ); }); it('should have correct window options', () => { const options = PresetLoadDialog.DEFAULT_OPTIONS; expect(options.id).toBe('scrying-pool-preset-load-dialog'); expect(options.classes).toContain('scrying-pool'); expect(options.classes).toContain('preset-load-dialog'); expect(options.window.title).toBe('Load Scene Preset'); expect(options.window.resizable).toBe(false); expect(options.position.width).toBe(320); }); it('should store references to dependencies', () => { expect(dialog._scenePresetManager).toBe(scenePresetManager); expect(dialog._adapter).toBe(adapter); }); it('should initialize _presets to empty array', () => { expect(dialog._presets).toEqual([]); }); }); });