import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import type { EditorConfig } from '../../types'; import type Core from '../../src/components/core'; import type { EditorModules } from '../../src/types-internal/editor-modules'; // Mock VERSION global variable declare global { // eslint-disable-next-line no-var -- TypeScript requires 'var' for global declarations var VERSION: string; } // Define VERSION before importing codex (global as { VERSION?: string }).VERSION = '2.31.0-test'; // Mock dependencies vi.mock('../../src/components/utils/tooltip', () => { const mockDestroyTooltip = vi.fn(); return { destroy: mockDestroyTooltip, mockDestroyTooltip, }; }); vi.mock('../../src/components/utils', async () => { const actual = await vi.importActual('../../src/components/utils'); const mockIsObject = vi.fn((v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)); const mockIsFunction = vi.fn((fn: unknown) => typeof fn === 'function'); return { ...actual, isObject: mockIsObject, isFunction: mockIsFunction, mockIsObject, mockIsFunction, }; }); // Mock Core class - use factory function to avoid hoisting issues vi.mock('../../src/components/core', () => { const createMockModuleInstances = (): Partial => ({ API: { methods: { blocks: { clear: vi.fn(), render: vi.fn(), } as unknown as EditorModules['API']['methods']['blocks'], caret: { focus: vi.fn(), } as unknown as EditorModules['API']['methods']['caret'], events: { on: vi.fn(), off: vi.fn(), emit: vi.fn(), }, saver: { save: vi.fn(), }, } as unknown as EditorModules['API']['methods'], } as unknown as EditorModules['API'], Toolbar: { blockSettings: undefined, inlineToolbar: undefined, } as unknown as EditorModules['Toolbar'], BlockSettings: {} as unknown as EditorModules['BlockSettings'], InlineToolbar: {} as unknown as EditorModules['InlineToolbar'], }); const mockModuleInstances = createMockModuleInstances(); const lastInstanceRef = { value: undefined as Core | undefined }; /** * */ class MockCore { public configuration: Record = {}; public moduleInstances: Partial; public isReady: Promise; /** * */ constructor() { this.moduleInstances = { ...mockModuleInstances, }; this.isReady = Promise.resolve(); // Store the last instance for test access lastInstanceRef.value = this as unknown as Core; } } return { default: MockCore, mockModuleInstances, lastInstance: () => lastInstanceRef.value, }; }); // Mock @babel/register vi.mock('@babel/register', () => ({})); // Mock polyfills vi.mock('../../src/components/polyfills', () => ({})); // Import EditorJS after mocks are set up import EditorJS from '../../src/codex'; describe('EditorJS', () => { // Get mocked instances const mocks = { mockModuleInstances: undefined as Partial | undefined, mockIsObject: undefined as ReturnType | undefined, mockIsFunction: undefined as ReturnType | undefined, mockDestroyTooltip: undefined as ReturnType | undefined, }; beforeEach(async () => { // Import the mocked modules to access the mock instances const coreModule = await import('../../src/components/core') as { default: new (...args: unknown[]) => Core; mockModuleInstances?: Partial; }; const utilsModule = await import('../../src/components/utils') as { mockIsObject?: ReturnType; mockIsFunction?: ReturnType; }; const tooltipModule = await import('../../src/components/utils/tooltip') as { mockDestroyTooltip?: ReturnType; }; mocks.mockModuleInstances = coreModule.mockModuleInstances as Partial; mocks.mockIsObject = utilsModule.mockIsObject as ReturnType; mocks.mockIsFunction = utilsModule.mockIsFunction as ReturnType; mocks.mockDestroyTooltip = tooltipModule.mockDestroyTooltip as ReturnType; vi.clearAllMocks(); mocks.mockModuleInstances!.API = { methods: { blocks: { clear: vi.fn(), render: vi.fn(), } as unknown as EditorModules['API']['methods']['blocks'], caret: { focus: vi.fn(), } as unknown as EditorModules['API']['methods']['caret'], events: { on: vi.fn(), off: vi.fn(), emit: vi.fn(), }, saver: { save: vi.fn(), }, } as unknown as EditorModules['API']['methods'], } as unknown as EditorModules['API']; mocks.mockModuleInstances!.Toolbar = { blockSettings: undefined, inlineToolbar: undefined, } as unknown as EditorModules['Toolbar']; mocks.mockModuleInstances!.BlockSettings = {} as unknown as EditorModules['BlockSettings']; mocks.mockModuleInstances!.InlineToolbar = {} as unknown as EditorModules['InlineToolbar']; }); afterEach(() => { vi.restoreAllMocks(); }); describe('constructor', () => { it('should initialize with no configuration', async () => { const editor = new EditorJS(); expect(editor.isReady).toBeInstanceOf(Promise); expect(editor.destroy).toBeDefined(); expect(typeof editor.destroy).toBe('function'); await editor.isReady; }); it('should initialize with string configuration (holderId)', async () => { const holderId = 'my-editor'; const editor = new EditorJS(holderId); expect(editor.isReady).toBeInstanceOf(Promise); await editor.isReady; }); it('should initialize with EditorConfig object', async () => { const config: EditorConfig = { holder: 'editorjs', placeholder: 'Start typing...', }; const editor = new EditorJS(config); expect(editor.isReady).toBeInstanceOf(Promise); await editor.isReady; }); it('should call onReady callback when provided', async () => { const onReady = vi.fn(); const config: EditorConfig = { holder: 'editorjs', onReady, }; mocks.mockIsObject!.mockReturnValue(true); mocks.mockIsFunction!.mockReturnValue(true); const editor = new EditorJS(config); await editor.isReady; expect(onReady).toHaveBeenCalledTimes(1); }); it('should use default empty onReady function when not provided', async () => { const config: EditorConfig = { holder: 'editorjs', }; mocks.mockIsObject!.mockReturnValue(true); mocks.mockIsFunction!.mockReturnValue(false); const editor = new EditorJS(config); await editor.isReady; // Should not throw expect(editor.isReady).toBeInstanceOf(Promise); }); it('should initialize destroy as no-op before exportAPI', () => { const editor = new EditorJS(); // Before isReady resolves, destroy should be a no-op expect(editor.destroy).toBeDefined(); expect(() => editor.destroy()).not.toThrow(); }); }); describe('isReady promise', () => { it('should resolve when Core is ready', async () => { const editor = new EditorJS(); await expect(editor.isReady).resolves.toBeUndefined(); }); it('should call exportAPI when Core is ready', async () => { const editor = new EditorJS(); const exportAPISpy = vi.spyOn(editor, 'exportAPI'); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); expect(exportAPISpy).toHaveBeenCalled(); if (lastCall) { expect(exportAPISpy).toHaveBeenCalledWith(lastCall); } }); }); describe('exportAPI', () => { it('should export configuration field', async () => { const config: EditorConfig = { holder: 'editorjs', placeholder: 'Test placeholder', }; const editor = new EditorJS(config); await editor.isReady; expect((editor as unknown as Record).configuration).toEqual(config); }); it('should set prototype to API methods', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { default: new (...args: unknown[]) => Core; lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); expect(Object.getPrototypeOf(editor)).toBe(lastCall?.moduleInstances.API?.methods); }); it('should create module aliases', async () => { const editor = new EditorJS(); await editor.isReady; const moduleAliases = (editor as unknown as { module: Record }).module; expect(moduleAliases).toBeDefined(); expect(typeof moduleAliases).toBe('object'); }); it('should create lowercase aliases for uppercase module names', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances) { throw new Error('Core instance not found'); } instances.API = { methods: {}, } as EditorModules['API']; instances.Toolbar = {} as EditorModules['Toolbar']; const moduleAliases = (editor as unknown as { module: Record }).module; // API should become 'api' expect(moduleAliases.api).toBe(instances.API); // Toolbar should become 'toolbar' expect(moduleAliases.toolbar).toBe(instances.Toolbar); }); it('should create camelCase aliases for PascalCase module names', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.BlockSettings = {} as EditorModules['BlockSettings']; instances.InlineToolbar = {} as EditorModules['InlineToolbar']; const moduleAliases = (editor as unknown as { module: Record }).module; expect(moduleAliases.blockSettings).toBe(instances.BlockSettings); expect(moduleAliases.inlineToolbar).toBe(instances.InlineToolbar); }); it('should skip undefined module instances', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.API = { methods: {}, } as EditorModules['API']; instances.Toolbar = undefined as unknown as EditorModules['Toolbar']; const moduleAliases = (editor as unknown as { module: Record }).module; expect(moduleAliases.toolbar).toBeUndefined(); }); it('should attach blockSettings to toolbar module if not already present', async () => { const mockToolbar = { blockSettings: undefined, }; const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.Toolbar = mockToolbar as unknown as EditorModules['Toolbar']; instances.BlockSettings = {} as unknown as EditorModules['BlockSettings']; // Re-export API to apply the changes (editor as unknown as { exportAPI: (core: Core) => void }).exportAPI(lastCall); expect(mockToolbar.blockSettings).toBe(instances.BlockSettings); }); it('should not override existing blockSettings on toolbar module', async () => { const existingBlockSettings = { existing: true }; const mockToolbar = { blockSettings: existingBlockSettings, }; const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.Toolbar = mockToolbar as unknown as EditorModules['Toolbar']; instances.BlockSettings = {} as unknown as EditorModules['BlockSettings']; // Re-export API to apply the changes (editor as unknown as { exportAPI: (core: Core) => void }).exportAPI(lastCall); expect(mockToolbar.blockSettings).toBe(existingBlockSettings); expect(mockToolbar.blockSettings).not.toBe(instances.BlockSettings); }); it('should attach inlineToolbar to toolbar module if not already present', async () => { const mockToolbar = { inlineToolbar: undefined, }; const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.Toolbar = mockToolbar as unknown as EditorModules['Toolbar']; instances.InlineToolbar = {} as unknown as EditorModules['InlineToolbar']; // Re-export API to apply the changes (editor as unknown as { exportAPI: (core: Core) => void }).exportAPI(lastCall); expect(mockToolbar.inlineToolbar).toBe(instances.InlineToolbar); }); it('should not override existing inlineToolbar on toolbar module', async () => { const existingInlineToolbar = { existing: true }; const mockToolbar = { inlineToolbar: existingInlineToolbar, }; const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } instances.Toolbar = mockToolbar as unknown as EditorModules['Toolbar']; instances.InlineToolbar = {} as unknown as EditorModules['InlineToolbar']; // Re-export API to apply the changes (editor as unknown as { exportAPI: (core: Core) => void }).exportAPI(lastCall); expect(mockToolbar.inlineToolbar).toBe(existingInlineToolbar); expect(mockToolbar.inlineToolbar).not.toBe(instances.InlineToolbar); }); it('should create shorthands for blocks methods', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } expect((editor as unknown as { clear: unknown }).clear).toBe(instances.API?.methods.blocks.clear); expect((editor as unknown as { render: unknown }).render).toBe(instances.API?.methods.blocks.render); }); it('should create shorthands for caret methods', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } expect((editor as unknown as { focus: unknown }).focus).toBe(instances.API?.methods.caret.focus); }); it('should create shorthands for events methods', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } expect((editor as unknown as { on: unknown }).on).toBe(instances.API?.methods.events.on); expect((editor as unknown as { off: unknown }).off).toBe(instances.API?.methods.events.off); expect((editor as unknown as { emit: unknown }).emit).toBe(instances.API?.methods.events.emit); }); it('should create shorthands for saver methods', async () => { const editor = new EditorJS(); await editor.isReady; // Get the actual Core instance that was created const coreModule = await import('../../src/components/core') as { lastInstance?: () => Core | undefined; }; const lastCall = coreModule.lastInstance?.(); const instances = lastCall?.moduleInstances; if (!instances || !lastCall) { throw new Error('Core instance not found'); } expect((editor as unknown as { save: unknown }).save).toBe(instances.API?.methods.saver.save); }); it('should delete exportAPI method after export', async () => { const editor = new EditorJS(); await editor.isReady; expect(Object.prototype.hasOwnProperty.call(editor, 'exportAPI')).toBe(false); expect(typeof (editor as unknown as { exportAPI: unknown }).exportAPI).toBe('function'); }); it('should make module property non-enumerable', async () => { const editor = new EditorJS(); await editor.isReady; const descriptor = Object.getOwnPropertyDescriptor(editor, 'module'); expect(descriptor).toBeDefined(); expect(descriptor?.enumerable).toBe(false); expect(descriptor?.configurable).toBe(true); expect(descriptor?.writable).toBe(false); }); }); describe('destroy', () => { it('should call destroy on all module instances that have destroy method', async () => { const mockDestroy1 = vi.fn(); const mockDestroy2 = vi.fn(); const mockModule1 = { destroy: mockDestroy1 }; const mockModule2 = { destroy: mockDestroy2 }; const mockModule3 = { noDestroy: true }; mocks.mockModuleInstances!.Toolbar = mockModule1 as unknown as EditorModules['Toolbar']; mocks.mockModuleInstances!.BlockSettings = mockModule2 as unknown as EditorModules['BlockSettings']; mocks.mockModuleInstances!.InlineToolbar = mockModule3 as unknown as EditorModules['InlineToolbar']; const editor = new EditorJS(); await editor.isReady; editor.destroy(); expect(mockDestroy1).toHaveBeenCalledTimes(1); expect(mockDestroy2).toHaveBeenCalledTimes(1); }); it('should remove all listeners from module instances', async () => { const mockRemoveAll = vi.fn(); const mockModule = { listeners: { removeAll: mockRemoveAll, }, }; mocks.mockModuleInstances!.Toolbar = mockModule as unknown as EditorModules['Toolbar']; const editor = new EditorJS(); await editor.isReady; editor.destroy(); expect(mockRemoveAll).toHaveBeenCalled(); }); it('should call destroyTooltip', async () => { const editor = new EditorJS(); await editor.isReady; editor.destroy(); expect(mocks.mockDestroyTooltip).toHaveBeenCalledTimes(1); }); it('should delete all own properties', async () => { const editor = new EditorJS(); await editor.isReady; // Add some test properties const testValue = 123; (editor as unknown as Record).testProperty = 'test'; (editor as unknown as Record).anotherProperty = testValue; expect((editor as unknown as Record).testProperty).toBe('test'); expect((editor as unknown as Record).anotherProperty).toBe(testValue); editor.destroy(); expect((editor as unknown as Record).testProperty).toBeUndefined(); expect((editor as unknown as Record).anotherProperty).toBeUndefined(); }); it('should set prototype to null', async () => { const editor = new EditorJS(); await editor.isReady; // Before destroy, prototype should be API methods expect(Object.getPrototypeOf(editor)).toBe(mocks.mockModuleInstances!.API?.methods); editor.destroy(); // After destroy, prototype should be null expect(Object.getPrototypeOf(editor)).toBeNull(); }); it('should handle modules without listeners property', async () => { const mockModule = { destroy: vi.fn(), // No listeners property }; mocks.mockModuleInstances!.Toolbar = mockModule as unknown as EditorModules['Toolbar']; const editor = new EditorJS(); await editor.isReady; // Should not throw expect(() => editor.destroy()).not.toThrow(); }); it('should handle modules without destroy method', async () => { const mockModule = { listeners: { removeAll: vi.fn(), }, // No destroy method }; mocks.mockModuleInstances!.Toolbar = mockModule as unknown as EditorModules['Toolbar']; const editor = new EditorJS(); await editor.isReady; // Should not throw expect(() => editor.destroy()).not.toThrow(); expect(mockModule.listeners.removeAll).toHaveBeenCalled(); }); }); describe('static version', () => { it('should expose version as static property', () => { expect(EditorJS.version).toBeDefined(); expect(typeof EditorJS.version).toBe('string'); }); }); });