mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 15:45:47 +01:00
988 lines
30 KiB
TypeScript
988 lines
30 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import InlineToolbar from '../../../../src/components/modules/toolbar/inline';
|
|
import type InlineToolAdapter from '../../../../src/components/tools/inline';
|
|
import SelectionUtils from '../../../../src/components/selection';
|
|
import type { Popover } from '../../../../src/components/utils/popover';
|
|
import Shortcuts from '../../../../src/components/utils/shortcuts';
|
|
import type { InlineTool } from '../../../../types';
|
|
|
|
// Mock dependencies at module level
|
|
const mockPopoverInstance = {
|
|
show: vi.fn(),
|
|
hide: vi.fn(),
|
|
destroy: vi.fn(),
|
|
getElement: vi.fn(() => document.createElement('div')),
|
|
activateItemByName: vi.fn(),
|
|
size: {
|
|
width: 200,
|
|
height: 40,
|
|
},
|
|
};
|
|
|
|
vi.mock('../../../../src/components/utils/popover/popover-inline', () => ({
|
|
PopoverInline: vi.fn().mockImplementation(() => mockPopoverInstance),
|
|
}));
|
|
|
|
vi.mock('../../../../src/components/utils/shortcuts', () => ({
|
|
default: {
|
|
add: vi.fn(),
|
|
remove: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../../src/components/utils', async () => {
|
|
const actual = await vi.importActual('../../../../src/components/utils');
|
|
|
|
return {
|
|
...actual,
|
|
isMobileScreen: vi.fn(() => false),
|
|
beautifyShortcut: vi.fn((shortcut: string) => shortcut),
|
|
capitalize: vi.fn((str: string) => str.charAt(0).toUpperCase() + str.slice(1)),
|
|
isFunction: vi.fn((fn: unknown) => typeof fn === 'function'),
|
|
keyCodes: {
|
|
DOWN: 40,
|
|
UP: 38,
|
|
},
|
|
};
|
|
});
|
|
|
|
// Mock SelectionUtils - we'll use Object.defineProperty in tests to override getters
|
|
// This mock just ensures the module can be imported
|
|
vi.mock('../../../../src/components/selection', async () => {
|
|
const actual = await vi.importActual('../../../../src/components/selection');
|
|
|
|
return actual;
|
|
});
|
|
|
|
vi.mock('../../../../src/components/i18n', () => ({
|
|
default: {
|
|
ui: vi.fn((namespace: string, key: string) => key),
|
|
t: vi.fn((namespace: string, key: string) => key),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../../src/components/dom', () => ({
|
|
default: {
|
|
make: vi.fn((tag: string, className: string | string[]) => {
|
|
const el = document.createElement(tag);
|
|
|
|
if (Array.isArray(className)) {
|
|
el.className = className.join(' ');
|
|
} else {
|
|
el.className = className;
|
|
}
|
|
|
|
return el;
|
|
}),
|
|
append: vi.fn((parent: HTMLElement, child: HTMLElement) => {
|
|
parent.appendChild(child);
|
|
}),
|
|
isElement: vi.fn((node: unknown) => node instanceof HTMLElement),
|
|
},
|
|
}));
|
|
|
|
/**
|
|
* Unit tests for InlineToolbar class
|
|
*
|
|
* Tests internal functionality and edge cases not covered by E2E tests
|
|
*/
|
|
describe('InlineToolbar', () => {
|
|
const TOOLBAR_WIDTH = 200;
|
|
const CONTENT_RECT_RIGHT = 1000;
|
|
const OVERFLOW_ADJUSTMENT = 800;
|
|
|
|
// eslint-disable-next-line no-restricted-syntax -- Test setup variables need to be reassigned in beforeEach
|
|
let inlineToolbar: InlineToolbar;
|
|
// eslint-disable-next-line no-restricted-syntax -- Test setup variables need to be reassigned in beforeEach
|
|
let mockEditor: {
|
|
BlockManager: {
|
|
getBlock: ReturnType<typeof vi.fn>;
|
|
currentBlock: {
|
|
tool: {
|
|
inlineTools: Map<string, InlineToolAdapter>;
|
|
enabledInlineTools: boolean;
|
|
};
|
|
} | null;
|
|
};
|
|
Tools: {
|
|
inlineTools: Map<string, InlineToolAdapter>;
|
|
internal: {
|
|
inlineTools: Map<string, unknown>;
|
|
};
|
|
};
|
|
ReadOnly: {
|
|
isEnabled: boolean;
|
|
};
|
|
UI: {
|
|
nodes: {
|
|
wrapper: HTMLElement;
|
|
redactor: HTMLElement;
|
|
};
|
|
contentRect: {
|
|
right: number;
|
|
};
|
|
CSS: {
|
|
editorRtlFix: string;
|
|
};
|
|
};
|
|
Toolbar: {
|
|
close: ReturnType<typeof vi.fn>;
|
|
};
|
|
};
|
|
|
|
const createMockInlineTool = (name: string, options: {
|
|
title?: string;
|
|
shortcut?: string;
|
|
isReadOnlySupported?: boolean;
|
|
render?: () => HTMLElement | { type: string; [key: string]: unknown };
|
|
checkState?: (selection: Selection) => boolean;
|
|
surround?: (range: Range) => void;
|
|
clear?: () => void;
|
|
} = {}): InlineTool => {
|
|
return {
|
|
render: options.render || (() => document.createElement('button')),
|
|
checkState: options.checkState,
|
|
surround: options.surround,
|
|
clear: options.clear,
|
|
} as InlineTool;
|
|
};
|
|
|
|
const createMockInlineToolAdapter = (name: string, options: {
|
|
title?: string;
|
|
shortcut?: string;
|
|
isReadOnlySupported?: boolean;
|
|
create?: () => InlineTool;
|
|
} = {}): InlineToolAdapter => {
|
|
return {
|
|
name,
|
|
title: options.title || name,
|
|
shortcut: options.shortcut,
|
|
isReadOnlySupported: options.isReadOnlySupported,
|
|
create: options.create || (() => createMockInlineTool(name)),
|
|
} as InlineToolAdapter;
|
|
};
|
|
|
|
const createMockBlock = (): {
|
|
tool: {
|
|
inlineTools: Map<string, InlineToolAdapter>;
|
|
enabledInlineTools: boolean;
|
|
};
|
|
holder: HTMLElement;
|
|
} => {
|
|
const blockElement = document.createElement('div');
|
|
|
|
blockElement.className = 'ce-block';
|
|
blockElement.setAttribute('contenteditable', 'true');
|
|
|
|
const toolAdapter = createMockInlineToolAdapter('bold', {
|
|
title: 'Bold',
|
|
shortcut: 'CMD+B',
|
|
});
|
|
|
|
return {
|
|
tool: {
|
|
inlineTools: new Map([ ['bold', toolAdapter] ]),
|
|
enabledInlineTools: true,
|
|
},
|
|
holder: blockElement,
|
|
};
|
|
};
|
|
|
|
const createMockSelection = (text: string = 'test'): Selection => {
|
|
const range = document.createRange();
|
|
const textNode = document.createTextNode(text);
|
|
|
|
document.body.appendChild(textNode);
|
|
range.setStart(textNode, 0);
|
|
range.setEnd(textNode, text.length);
|
|
|
|
return {
|
|
anchorNode: textNode,
|
|
isCollapsed: false,
|
|
rangeCount: 1,
|
|
getRangeAt: () => range,
|
|
} as unknown as Selection;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
// Reset SelectionUtils spies if they exist
|
|
vi.restoreAllMocks();
|
|
|
|
// Mock requestIdleCallback to execute immediately
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(global as any).requestIdleCallback = vi.fn((callback: () => void) => {
|
|
callback();
|
|
|
|
return 1;
|
|
});
|
|
|
|
// Create mock UI nodes
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.className = 'codex-editor';
|
|
document.body.appendChild(wrapper);
|
|
|
|
const redactor = document.createElement('div');
|
|
|
|
redactor.className = 'codex-editor__redactor';
|
|
wrapper.appendChild(redactor);
|
|
|
|
// Create mock Editor
|
|
mockEditor = {
|
|
BlockManager: {
|
|
getBlock: vi.fn(),
|
|
currentBlock: null,
|
|
},
|
|
Tools: {
|
|
inlineTools: new Map(),
|
|
internal: {
|
|
inlineTools: new Map(),
|
|
},
|
|
},
|
|
ReadOnly: {
|
|
isEnabled: false,
|
|
},
|
|
UI: {
|
|
nodes: {
|
|
wrapper,
|
|
redactor,
|
|
},
|
|
contentRect: {
|
|
right: CONTENT_RECT_RIGHT,
|
|
},
|
|
CSS: {
|
|
editorRtlFix: 'ce-editor--rtl',
|
|
},
|
|
},
|
|
Toolbar: {
|
|
close: vi.fn(),
|
|
},
|
|
};
|
|
|
|
// Create InlineToolbar instance
|
|
inlineToolbar = new InlineToolbar({
|
|
config: {},
|
|
eventsDispatcher: {
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
} as unknown as typeof InlineToolbar.prototype['eventsDispatcher'],
|
|
});
|
|
|
|
// Set Editor modules
|
|
(inlineToolbar as unknown as { Editor: typeof mockEditor }).Editor = mockEditor;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up DOM
|
|
if (mockEditor?.UI?.nodes?.wrapper) {
|
|
const wrapper = mockEditor.UI.nodes.wrapper;
|
|
|
|
if (wrapper.parentNode) {
|
|
wrapper.parentNode.removeChild(wrapper);
|
|
}
|
|
}
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should initialize with opened = false', () => {
|
|
expect(inlineToolbar.opened).toBe(false);
|
|
});
|
|
|
|
it('should register keydown listener for shift+arrow keys', () => {
|
|
// Create new instance to trigger constructor
|
|
const newToolbar = new InlineToolbar({
|
|
config: {},
|
|
eventsDispatcher: {
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
} as unknown as typeof InlineToolbar.prototype['eventsDispatcher'],
|
|
});
|
|
|
|
(newToolbar as unknown as { Editor: typeof mockEditor }).Editor = mockEditor;
|
|
|
|
// The listener is registered via this.listeners.on, not directly
|
|
// So we check that the toolbar was created successfully
|
|
expect(newToolbar).toBeInstanceOf(InlineToolbar);
|
|
});
|
|
|
|
it('should schedule make() and registerInitialShortcuts() via requestIdleCallback', () => {
|
|
const requestIdleCallbackSpy = vi.fn((callback: () => void) => {
|
|
callback();
|
|
|
|
return 1;
|
|
});
|
|
|
|
(global as unknown as { requestIdleCallback: typeof requestIdleCallbackSpy }).requestIdleCallback = requestIdleCallbackSpy;
|
|
|
|
new InlineToolbar({
|
|
config: {},
|
|
eventsDispatcher: {
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
} as unknown as typeof InlineToolbar.prototype['eventsDispatcher'],
|
|
});
|
|
|
|
expect(requestIdleCallbackSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('tryToShow', () => {
|
|
beforeEach(() => {
|
|
// Setup valid selection
|
|
const selection = createMockSelection();
|
|
|
|
vi.spyOn(SelectionUtils, 'get').mockReturnValue(selection);
|
|
vi.spyOn(SelectionUtils, 'text', 'get').mockReturnValue('test');
|
|
vi.spyOn(SelectionUtils, 'rect', 'get').mockReturnValue({
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 20,
|
|
} as DOMRect);
|
|
vi.spyOn(SelectionUtils, 'range', 'get').mockReturnValue(selection.getRangeAt(0));
|
|
|
|
mockEditor.BlockManager.currentBlock = createMockBlock() as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
});
|
|
|
|
it('should close toolbar when needToClose is true', async () => {
|
|
inlineToolbar.opened = true;
|
|
const closeSpy = vi.spyOn(inlineToolbar, 'close');
|
|
|
|
await inlineToolbar.tryToShow(true);
|
|
|
|
expect(closeSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not open toolbar when not allowed to show', async () => {
|
|
// Make selection invalid
|
|
vi.spyOn(SelectionUtils, 'get').mockReturnValue(null);
|
|
vi.spyOn(SelectionUtils, 'text', 'get').mockReturnValue('');
|
|
|
|
await inlineToolbar.tryToShow();
|
|
|
|
expect(inlineToolbar.opened).toBe(false);
|
|
});
|
|
|
|
it('should open toolbar and close main toolbar when allowed', async () => {
|
|
// Mock the open method
|
|
const openSpy = vi.spyOn(inlineToolbar as unknown as { open: () => Promise<void> }, 'open').mockResolvedValue();
|
|
|
|
await inlineToolbar.tryToShow();
|
|
|
|
expect(openSpy).toHaveBeenCalled();
|
|
expect(mockEditor.Toolbar.close).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('close', () => {
|
|
it('should do nothing when toolbar is already closed', () => {
|
|
inlineToolbar.opened = false;
|
|
const hideSpy = vi.spyOn(mockPopoverInstance, 'hide');
|
|
|
|
inlineToolbar.close();
|
|
|
|
expect(hideSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should clear tool instances and reset state when closing', () => {
|
|
inlineToolbar.opened = true;
|
|
const toolInstance = createMockInlineTool('bold', {
|
|
clear: vi.fn(),
|
|
});
|
|
|
|
(inlineToolbar as unknown as { tools: Map<InlineToolAdapter, InlineTool> }).tools = new Map([
|
|
[createMockInlineToolAdapter('bold'), toolInstance],
|
|
]);
|
|
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
inlineToolbar.close();
|
|
|
|
expect(toolInstance.clear).toHaveBeenCalled();
|
|
expect(inlineToolbar.opened).toBe(false);
|
|
expect(mockPopoverInstance.hide).toHaveBeenCalled();
|
|
expect(mockPopoverInstance.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reset wrapper position', () => {
|
|
inlineToolbar.opened = true;
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.style.left = '100px';
|
|
wrapper.style.top = '200px';
|
|
(inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper = wrapper;
|
|
|
|
inlineToolbar.close();
|
|
|
|
expect(wrapper.style.left).toBe('0px');
|
|
expect(wrapper.style.top).toBe('0px');
|
|
});
|
|
});
|
|
|
|
describe('containsNode', () => {
|
|
it('should return false when wrapper is undefined', () => {
|
|
(inlineToolbar as unknown as { nodes: { wrapper: HTMLElement | undefined } }).nodes.wrapper = undefined;
|
|
|
|
const node = document.createElement('div');
|
|
const result = inlineToolbar.containsNode(node);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when node is contained in wrapper', () => {
|
|
const wrapper = document.createElement('div');
|
|
const child = document.createElement('div');
|
|
|
|
wrapper.appendChild(child);
|
|
(inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper = wrapper;
|
|
|
|
const result = inlineToolbar.containsNode(child);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when node is not contained in wrapper', () => {
|
|
const wrapper = document.createElement('div');
|
|
const otherNode = document.createElement('div');
|
|
|
|
(inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper = wrapper;
|
|
|
|
const result = inlineToolbar.containsNode(otherNode);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
it('should destroy popover and remove nodes', () => {
|
|
inlineToolbar.opened = true;
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
inlineToolbar.destroy();
|
|
|
|
expect(mockPopoverInstance.destroy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('allowedToShow', () => {
|
|
it('should return false when selection is null', () => {
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when selection is collapsed', () => {
|
|
const selection = createMockSelection();
|
|
|
|
(selection as unknown as { isCollapsed: boolean }).isCollapsed = true;
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when selected text is empty', () => {
|
|
const selection = createMockSelection('');
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: '',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when target is IMG or INPUT tag', () => {
|
|
const img = document.createElement('img');
|
|
const selection = {
|
|
anchorNode: img,
|
|
isCollapsed: false,
|
|
} as unknown as Selection;
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: 'test',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
mockEditor.BlockManager.getBlock = vi.fn(() => createMockBlock() as unknown as typeof mockEditor.BlockManager.currentBlock) as typeof mockEditor.BlockManager.getBlock;
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when current block is null', () => {
|
|
const selection = createMockSelection();
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: 'test',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
mockEditor.BlockManager.currentBlock = null;
|
|
mockEditor.BlockManager.getBlock = vi.fn(() => null) as typeof mockEditor.BlockManager.getBlock;
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when no inline tools are available for block', () => {
|
|
const selection = createMockSelection();
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: 'test',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const block = createMockBlock();
|
|
|
|
block.tool.inlineTools = new Map();
|
|
mockEditor.BlockManager.currentBlock = block as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
mockEditor.BlockManager.getBlock = vi.fn(() => block as unknown as typeof mockEditor.BlockManager.currentBlock) as typeof mockEditor.BlockManager.getBlock;
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when target is not contenteditable', () => {
|
|
const selection = createMockSelection();
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: 'test',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const block = createMockBlock();
|
|
|
|
block.holder.removeAttribute('contenteditable');
|
|
mockEditor.BlockManager.currentBlock = block as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
mockEditor.BlockManager.getBlock = vi.fn(() => block as unknown as typeof mockEditor.BlockManager.currentBlock) as typeof mockEditor.BlockManager.getBlock;
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when all conditions are met', () => {
|
|
const selection = createMockSelection();
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'text', {
|
|
value: 'test',
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const block = createMockBlock();
|
|
|
|
mockEditor.BlockManager.currentBlock = block as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
mockEditor.BlockManager.getBlock = vi.fn(() => block as unknown as typeof mockEditor.BlockManager.currentBlock) as typeof mockEditor.BlockManager.getBlock;
|
|
|
|
const result = (inlineToolbar as unknown as { allowedToShow: () => boolean }).allowedToShow();
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getTools', () => {
|
|
it('should return empty array when current block is null', () => {
|
|
mockEditor.BlockManager.currentBlock = null;
|
|
|
|
const result = (inlineToolbar as unknown as { getTools: () => InlineToolAdapter[] }).getTools();
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should filter out tools not supported in read-only mode', () => {
|
|
const block = createMockBlock();
|
|
const readOnlyTool = createMockInlineToolAdapter('readonly-tool', {
|
|
isReadOnlySupported: true,
|
|
});
|
|
const normalTool = createMockInlineToolAdapter('normal-tool', {
|
|
isReadOnlySupported: false,
|
|
});
|
|
|
|
block.tool.inlineTools = new Map([
|
|
['readonly-tool', readOnlyTool],
|
|
['normal-tool', normalTool],
|
|
]);
|
|
|
|
mockEditor.BlockManager.currentBlock = block as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
mockEditor.ReadOnly.isEnabled = true;
|
|
|
|
const result = (inlineToolbar as unknown as { getTools: () => InlineToolAdapter[] }).getTools();
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('readonly-tool');
|
|
});
|
|
|
|
it('should return all tools when not in read-only mode', () => {
|
|
const block = createMockBlock();
|
|
const tool1 = createMockInlineToolAdapter('tool1');
|
|
const tool2 = createMockInlineToolAdapter('tool2');
|
|
|
|
block.tool.inlineTools = new Map([
|
|
['tool1', tool1],
|
|
['tool2', tool2],
|
|
]);
|
|
|
|
mockEditor.BlockManager.currentBlock = block as unknown as typeof mockEditor.BlockManager.currentBlock;
|
|
mockEditor.ReadOnly.isEnabled = false;
|
|
|
|
const result = (inlineToolbar as unknown as { getTools: () => InlineToolAdapter[] }).getTools();
|
|
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('toolClicked', () => {
|
|
it('should call surround on tool with range', () => {
|
|
const range = document.createRange();
|
|
const tool = createMockInlineTool('bold', {
|
|
surround: vi.fn(),
|
|
});
|
|
|
|
Object.defineProperty(SelectionUtils, 'range', {
|
|
value: range,
|
|
writable: true,
|
|
});
|
|
|
|
(inlineToolbar as unknown as { toolClicked: (tool: InlineTool) => void }).toolClicked(tool);
|
|
|
|
expect(tool.surround).toHaveBeenCalledWith(range);
|
|
});
|
|
|
|
it('should check tools state after clicking', () => {
|
|
const tool = createMockInlineTool('bold', {
|
|
surround: vi.fn(),
|
|
});
|
|
|
|
const checkToolsStateSpy = vi.spyOn(inlineToolbar as unknown as { checkToolsState: () => void }, 'checkToolsState');
|
|
|
|
Object.defineProperty(SelectionUtils, 'range', {
|
|
value: document.createRange(),
|
|
writable: true,
|
|
});
|
|
|
|
(inlineToolbar as unknown as { toolClicked: (tool: InlineTool) => void }).toolClicked(tool);
|
|
|
|
expect(checkToolsStateSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('activateToolByShortcut', () => {
|
|
beforeEach(() => {
|
|
const selection = createMockSelection();
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it('should try to show toolbar when not opened', async () => {
|
|
inlineToolbar.opened = false;
|
|
const tryToShowSpy = vi.spyOn(inlineToolbar, 'tryToShow').mockResolvedValue();
|
|
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
await (inlineToolbar as unknown as { activateToolByShortcut: (toolName: string) => Promise<void> }).activateToolByShortcut('bold');
|
|
|
|
expect(tryToShowSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should activate item by name when selection is null', async () => {
|
|
inlineToolbar.opened = true;
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
await (inlineToolbar as unknown as { activateToolByShortcut: (toolName: string) => Promise<void> }).activateToolByShortcut('bold');
|
|
|
|
expect(mockPopoverInstance.activateItemByName).toHaveBeenCalledWith('bold');
|
|
});
|
|
|
|
it('should not activate tool when tool is already active', async () => {
|
|
inlineToolbar.opened = true;
|
|
const toolInstance = createMockInlineTool('bold', {
|
|
checkState: vi.fn(() => true),
|
|
});
|
|
const toolAdapter = createMockInlineToolAdapter('bold');
|
|
|
|
(inlineToolbar as unknown as { tools: Map<InlineToolAdapter, InlineTool> }).tools = new Map([
|
|
[toolAdapter, toolInstance],
|
|
]);
|
|
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
await (inlineToolbar as unknown as { activateToolByShortcut: (toolName: string) => Promise<void> }).activateToolByShortcut('bold');
|
|
|
|
expect(mockPopoverInstance.activateItemByName).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should activate tool when tool is not active', async () => {
|
|
inlineToolbar.opened = true;
|
|
const toolInstance = createMockInlineTool('bold', {
|
|
checkState: vi.fn(() => false),
|
|
});
|
|
const toolAdapter = createMockInlineToolAdapter('bold');
|
|
|
|
(inlineToolbar as unknown as { tools: Map<InlineToolAdapter, InlineTool> }).tools = new Map([
|
|
[toolAdapter, toolInstance],
|
|
]);
|
|
|
|
(inlineToolbar as unknown as { popover: Popover | null }).popover = mockPopoverInstance as unknown as Popover;
|
|
|
|
await (inlineToolbar as unknown as { activateToolByShortcut: (toolName: string) => Promise<void> }).activateToolByShortcut('bold');
|
|
|
|
expect(mockPopoverInstance.activateItemByName).toHaveBeenCalledWith('bold');
|
|
});
|
|
});
|
|
|
|
describe('checkToolsState', () => {
|
|
it('should do nothing when selection is null', () => {
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: null,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const toolInstance = createMockInlineTool('bold', {
|
|
checkState: vi.fn(),
|
|
});
|
|
|
|
(inlineToolbar as unknown as { tools: Map<InlineToolAdapter, InlineTool> }).tools = new Map([
|
|
[createMockInlineToolAdapter('bold'), toolInstance],
|
|
]);
|
|
|
|
(inlineToolbar as unknown as { checkToolsState: () => void }).checkToolsState();
|
|
|
|
expect(toolInstance.checkState).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call checkState on all tool instances', () => {
|
|
const selection = createMockSelection();
|
|
const tool1 = createMockInlineTool('bold', {
|
|
checkState: vi.fn(),
|
|
});
|
|
const tool2 = createMockInlineTool('italic', {
|
|
checkState: vi.fn(),
|
|
});
|
|
|
|
Object.defineProperty(SelectionUtils, 'instance', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(SelectionUtils, 'selection', {
|
|
value: selection,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
(inlineToolbar as unknown as { tools: Map<InlineToolAdapter, InlineTool> }).tools = new Map([
|
|
[createMockInlineToolAdapter('bold'), tool1],
|
|
[createMockInlineToolAdapter('italic'), tool2],
|
|
]);
|
|
|
|
(inlineToolbar as unknown as { checkToolsState: () => void }).checkToolsState();
|
|
|
|
expect(tool1.checkState).toHaveBeenCalledWith(selection);
|
|
expect(tool2.checkState).toHaveBeenCalledWith(selection);
|
|
});
|
|
});
|
|
|
|
describe('shortcut registration', () => {
|
|
it('should register shortcuts for tools with shortcuts', () => {
|
|
const toolAdapter = createMockInlineToolAdapter('bold', {
|
|
shortcut: 'CMD+B',
|
|
});
|
|
|
|
mockEditor.Tools.inlineTools.set('bold', toolAdapter);
|
|
|
|
(inlineToolbar as unknown as { registerInitialShortcuts: () => void }).registerInitialShortcuts();
|
|
|
|
expect(Shortcuts.add).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle errors when enabling shortcuts', () => {
|
|
const toolAdapter = createMockInlineToolAdapter('bold', {
|
|
shortcut: 'CMD+B',
|
|
});
|
|
|
|
mockEditor.Tools.inlineTools.set('bold', toolAdapter);
|
|
vi.mocked(Shortcuts.add).mockImplementation(() => {
|
|
throw new Error('Shortcut error');
|
|
});
|
|
|
|
// Should not throw
|
|
expect(() => {
|
|
(inlineToolbar as unknown as { registerInitialShortcuts: () => void }).registerInitialShortcuts();
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('move', () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(SelectionUtils, 'rect', {
|
|
value: {
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 20,
|
|
} as DOMRect,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
wrapper.style.position = 'absolute';
|
|
(inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper = wrapper;
|
|
});
|
|
|
|
it('should position toolbar below selection', () => {
|
|
const wrapper = (inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper;
|
|
|
|
(inlineToolbar as unknown as { move: (popoverWidth: number) => void }).move(TOOLBAR_WIDTH);
|
|
|
|
expect(wrapper.style.left).toBeTruthy();
|
|
expect(wrapper.style.top).toBeTruthy();
|
|
});
|
|
|
|
it('should adjust position when toolbar would overflow right edge', () => {
|
|
const wrapper = (inlineToolbar as unknown as { nodes: { wrapper: HTMLElement } }).nodes.wrapper;
|
|
|
|
Object.defineProperty(SelectionUtils, 'rect', {
|
|
value: {
|
|
x: 900,
|
|
y: 100,
|
|
width: 50,
|
|
height: 20,
|
|
} as DOMRect,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
mockEditor.UI.contentRect.right = CONTENT_RECT_RIGHT;
|
|
|
|
(inlineToolbar as unknown as { move: (popoverWidth: number) => void }).move(TOOLBAR_WIDTH);
|
|
|
|
const left = parseInt(wrapper.style.left, 10);
|
|
|
|
expect(left).toBeLessThanOrEqual(OVERFLOW_ADJUSTMENT); // Should be adjusted to fit
|
|
});
|
|
});
|
|
});
|
|
|