// @ts-nocheck import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PresetSaveDialog } from '../../../../src/ui/gm/PresetSaveDialog.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().mockResolvedValue([]), get: vi.fn().mockResolvedValue(null), 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, }; } // ============================================================================ // PresetSaveDialog Tests // ============================================================================ describe('PresetSaveDialog', () => { 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 PresetSaveDialog(null, adapter)).toThrow(TypeError); }); it('should throw TypeError when scenePresetManager is not an object', () => { expect(() => new PresetSaveDialog('not an object', adapter)).toThrow(TypeError); }); it('should throw TypeError when adapter is null', () => { expect(() => new PresetSaveDialog(scenePresetManager, null)).toThrow(TypeError); }); it('should throw TypeError when adapter is not an object', () => { expect(() => new PresetSaveDialog(scenePresetManager, 'not an object')).toThrow(TypeError); }); it('should accept valid dependencies and initialize internal state', () => { dialog = new PresetSaveDialog(scenePresetManager, adapter); expect(dialog).toBeDefined(); expect(dialog._scenePresetManager).toBe(scenePresetManager); expect(dialog._adapter).toBe(adapter); }); it('should be side-effect-free: no hooks registered in constructor', () => { const originalError = console.error; console.error = vi.fn(); dialog = new PresetSaveDialog(scenePresetManager, adapter); expect(console.error).not.toHaveBeenCalled(); console.error = originalError; }); it('should have DEFAULT_OPTIONS defined', () => { expect(PresetSaveDialog.DEFAULT_OPTIONS).toBeDefined(); expect(PresetSaveDialog.DEFAULT_OPTIONS.id).toBe('scrying-pool-preset-save-dialog'); expect(PresetSaveDialog.DEFAULT_OPTIONS.classes).toEqual(expect.arrayContaining(['scrying-pool', 'preset-save-dialog'])); expect(PresetSaveDialog.DEFAULT_OPTIONS.window.title).toBe('Save Scene Preset'); }); it('should have PARTS defined with template', () => { expect(PresetSaveDialog.PARTS).toBeDefined(); expect(PresetSaveDialog.PARTS.dialog).toBeDefined(); expect(PresetSaveDialog.PARTS.dialog.template).toContain('preset-save-dialog.hbs'); }); }); // -------------------------------------------------------------------------- // _prepareContext() Tests // -------------------------------------------------------------------------- describe('_prepareContext()', () => { beforeEach(() => { dialog = new PresetSaveDialog(scenePresetManager, adapter); }); it('should return an object with defaultName property', async () => { const context = await dialog._prepareContext(); expect(context).toBeDefined(); expect(typeof context).toBe('object'); expect(context.defaultName).toBeDefined(); }); it('should return empty string as defaultName when no presets exist', async () => { adapter.i18n.localize = vi.fn((key) => { if (key === 'video-view-manager.presets.save.namePlaceholder') return 'Enter preset name'; return key; }); const context = await dialog._prepareContext(); expect(context.defaultName).toBe(''); }); it('should use i18n for labels', async () => { adapter.i18n.localize = vi.fn((key) => `Localized: ${key}`); const context = await dialog._prepareContext(); expect(adapter.i18n.localize).toHaveBeenCalled(); expect(context).toBeDefined(); }); it('should return all i18n labels', async () => { adapter.i18n.localize = vi.fn((key) => { const translations = { 'video-view-manager.presets.save.saveButton': 'Save', 'video-view-manager.presets.save.cancelButton': 'Cancel', 'video-view-manager.presets.save.title': 'Save Preset', 'video-view-manager.presets.save.nameLabel': 'Preset Name', 'video-view-manager.presets.save.namePlaceholder': 'Enter preset name', }; return translations[key] || key; }); const context = await dialog._prepareContext(); expect(context.saveLabel).toBe('Save'); expect(context.cancelLabel).toBe('Cancel'); expect(context.title).toBe('Save Preset'); expect(context.nameLabel).toBe('Preset Name'); expect(context.namePlaceholder).toBe('Enter preset name'); }); }); // -------------------------------------------------------------------------- // _onRender() Tests // -------------------------------------------------------------------------- describe('_onRender()', () => { let mockForm; beforeEach(() => { dialog = new PresetSaveDialog(scenePresetManager, adapter); mockForm = { querySelector: vi.fn((selector) => { if (selector === 'form') return mockForm; if (selector === '[name="presetName"]') return { focus: vi.fn(), value: '' }; if (selector === '[data-action="cancel"]') return { addEventListener: vi.fn() }; return null; }), addEventListener: vi.fn(), focus: vi.fn(), }; dialog.element = mockForm; dialog.rendered = true; }); it('should cache the name input element', () => { dialog._onRender(mockForm); expect(dialog._nameInput).toBeDefined(); expect(mockForm.querySelector).toHaveBeenCalledWith('[name="presetName"]'); }); it('should focus the name input field when it exists', () => { const nameInput = { focus: vi.fn() }; mockForm.querySelector = vi.fn((selector) => { if (selector === '[name="presetName"]') return nameInput; if (selector === 'form') return mockForm; if (selector === '[data-action="cancel"]') return { addEventListener: vi.fn() }; return null; }); dialog._onRender(mockForm); expect(nameInput.focus).toHaveBeenCalled(); }); it('should set up form submit handler', () => { dialog._onRender(mockForm); expect(mockForm.addEventListener).toHaveBeenCalledWith('submit', expect.any(Function)); }); it('should set up cancel button handler', () => { const cancelBtn = { addEventListener: vi.fn() }; mockForm.querySelector = vi.fn((selector) => { if (selector === 'form') return mockForm; if (selector === '[name="presetName"]') return { focus: vi.fn(), value: '' }; if (selector === '[data-action="cancel"]') return cancelBtn; return null; }); dialog._onRender(mockForm); expect(cancelBtn.addEventListener).toHaveBeenCalledWith('click', expect.any(Function)); }); it('should set up keyboard handlers', () => { dialog._onRender(mockForm); expect(mockForm.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function)); }); }); // -------------------------------------------------------------------------- // _onSubmit() Tests // -------------------------------------------------------------------------- describe('_onSubmit()', () => { let mockEvent; beforeEach(() => { dialog = new PresetSaveDialog(scenePresetManager, adapter); mockEvent = { preventDefault: vi.fn(), stopPropagation: vi.fn(), target: { querySelector: vi.fn((selector) => { if (selector === '[name="presetName"]') return { value: 'My Preset' }; return null; }), }, }; }); it('should throw TypeError when event is null', async () => { await expect(dialog._onSubmit(null)).rejects.toThrow(TypeError); }); it('should prevent default and stop propagation', async () => { scenePresetManager.save = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); await dialog._onSubmit(mockEvent); expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled(); }); it('should throw TypeError when preset name input is not found', async () => { mockEvent.target.querySelector = vi.fn(() => null); await expect(dialog._onSubmit(mockEvent)).rejects.toThrow(TypeError); }); it('should throw TypeError when preset name is empty', async () => { mockEvent.target.querySelector = vi.fn((selector) => { if (selector === '[name="presetName"]') return { value: '' }; return null; }); await expect(dialog._onSubmit(mockEvent)).rejects.toThrow(TypeError); }); it('should throw TypeError when preset name is only whitespace', async () => { mockEvent.target.querySelector = vi.fn((selector) => { if (selector === '[name="presetName"]') return { value: ' ' }; return null; }); await expect(dialog._onSubmit(mockEvent)).rejects.toThrow(TypeError); }); it('should call scenePresetManager.save with the trimmed preset name', async () => { scenePresetManager.save = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); await dialog._onSubmit(mockEvent); expect(scenePresetManager.save).toHaveBeenCalledWith('My Preset'); }); it('should close the dialog on successful save', async () => { scenePresetManager.save = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); await dialog._onSubmit(mockEvent); expect(dialog.close).toHaveBeenCalled(); }); it('should show notification on successful save via adapter.notifications', async () => { scenePresetManager.save = vi.fn().mockResolvedValue({ name: 'My Preset' }); dialog.close = vi.fn().mockResolvedValue({}); adapter.i18n.localize = vi.fn((key) => { if (key === 'video-view-manager.presets.notifications.saved') return 'Preset {name} saved!'; return key; }); await dialog._onSubmit(mockEvent); expect(adapter.notifications.info).toHaveBeenCalledWith('Preset My Preset saved!'); }); it('should re-throw TypeError from save', async () => { const error = new TypeError('a preset with name "My Preset" already exists'); scenePresetManager.save = vi.fn().mockRejectedValue(error); dialog.close = vi.fn().mockResolvedValue({}); await expect(dialog._onSubmit(mockEvent)).rejects.toThrow(TypeError); expect(dialog.close).not.toHaveBeenCalled(); }); it('should re-throw max presets error from save', async () => { const error = new TypeError('maximum of 50 presets reached'); scenePresetManager.save = vi.fn().mockRejectedValue(error); dialog.close = vi.fn().mockResolvedValue({}); await expect(dialog._onSubmit(mockEvent)).rejects.toThrow(TypeError); expect(dialog.close).not.toHaveBeenCalled(); }); }); // -------------------------------------------------------------------------- // _onCancel() Tests // -------------------------------------------------------------------------- describe('_onCancel()', () => { beforeEach(() => { dialog = new PresetSaveDialog(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 PresetSaveDialog(scenePresetManager, adapter); scenePresetManager.save = vi.fn().mockResolvedValue({}); dialog.close = vi.fn().mockResolvedValue({}); adapter.i18n.localize = vi.fn((key) => key); mockEvent = { preventDefault: vi.fn(), stopPropagation: vi.fn(), key: '', target: { tagName: 'INPUT', form: { querySelector: vi.fn() } }, }; }); it('should handle Enter key on input field', async () => { mockEvent.key = 'Enter'; mockEvent.target.form.querySelector = vi.fn((selector) => { if (selector === '[name="presetName"]') return { value: 'Test' }; return null; }); await dialog._onKeydown(mockEvent); expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled(); expect(scenePresetManager.save).toHaveBeenCalledWith('Test'); }); 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 ignore other keys', () => { mockEvent.key = 'A'; dialog._onKeydown(mockEvent); expect(mockEvent.preventDefault).not.toHaveBeenCalled(); expect(scenePresetManager.save).not.toHaveBeenCalled(); expect(dialog.close).not.toHaveBeenCalled(); }); }); // -------------------------------------------------------------------------- // Integration Tests // -------------------------------------------------------------------------- describe('integration', () => { beforeEach(() => { dialog = new PresetSaveDialog(scenePresetManager, adapter); }); it('should have all required methods defined', () => { expect(dialog._prepareContext).toBeDefined(); expect(dialog._onRender).toBeDefined(); expect(dialog._onSubmit).toBeDefined(); expect(dialog._onCancel).toBeDefined(); expect(dialog._onKeydown).toBeDefined(); }); it('should use the correct template path', () => { expect(PresetSaveDialog.PARTS.dialog.template).toBe( 'modules/video-view-manager/templates/preset-save-dialog.hbs' ); }); it('should have correct window options', () => { const options = PresetSaveDialog.DEFAULT_OPTIONS; expect(options.id).toBe('scrying-pool-preset-save-dialog'); expect(options.classes).toContain('scrying-pool'); expect(options.classes).toContain('preset-save-dialog'); expect(options.window.title).toBe('Save 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 _nameInput to null', () => { expect(dialog._nameInput).toBeNull(); }); }); });