475 lines
16 KiB
JavaScript
475 lines
16 KiB
JavaScript
// @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();
|
|
});
|
|
});
|
|
});
|