mirror of
https://github.com/codex-team/editor.js
synced 2026-03-18 00:19:53 +01:00
638 lines
22 KiB
TypeScript
638 lines
22 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import BlockSelection from '../../../../src/components/modules/blockSelection';
|
|
import EventsDispatcher from '../../../../src/components/utils/events';
|
|
import type { EditorEventMap } from '../../../../src/components/events';
|
|
import type { EditorModules } from '../../../../src/types-internal/editor-modules';
|
|
import type { EditorConfig } from '../../../../types';
|
|
import SelectionUtils from '../../../../src/components/selection';
|
|
import Shortcuts from '../../../../src/components/utils/shortcuts';
|
|
import * as utils from '../../../../src/components/utils';
|
|
import type { SanitizerConfig } from '../../../../types/configs';
|
|
import type Block from '../../../../src/components/block';
|
|
|
|
type ModuleOverrides = Partial<EditorModules>;
|
|
|
|
type SelectionSpy = {
|
|
save: ReturnType<typeof vi.fn>;
|
|
restore: ReturnType<typeof vi.fn>;
|
|
clearSaved: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
type BlockSelectionSetup = {
|
|
blockSelection: BlockSelection;
|
|
modules: EditorModules;
|
|
blocks: Block[];
|
|
selectionSpy: SelectionSpy;
|
|
redactor: HTMLDivElement;
|
|
};
|
|
|
|
const createBlockStub = (options?: { html?: string; inputs?: HTMLElement[]; initiallySelected?: boolean }): Block => {
|
|
const holder = document.createElement('div');
|
|
|
|
holder.className = 'ce-block';
|
|
holder.innerHTML = options?.html ?? '<p>Sample text</p>';
|
|
|
|
const inputs = options?.inputs ?? [ document.createElement('div') ];
|
|
let isSelected = options?.initiallySelected ?? false;
|
|
|
|
const blockStub = {
|
|
holder,
|
|
inputs,
|
|
save: vi.fn().mockResolvedValue({
|
|
id: 'test',
|
|
tool: 'paragraph',
|
|
data: { text: 'Sample text' },
|
|
tunes: {},
|
|
time: 0,
|
|
}),
|
|
};
|
|
|
|
Object.defineProperty(blockStub, 'selected', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get(): boolean {
|
|
return isSelected;
|
|
},
|
|
set(nextState: boolean) {
|
|
isSelected = nextState;
|
|
holder.classList.toggle('ce-block--selected', nextState);
|
|
},
|
|
});
|
|
|
|
return blockStub as unknown as Block;
|
|
};
|
|
|
|
const createBlockSelection = (overrides: ModuleOverrides = {}): BlockSelectionSetup => {
|
|
const redactor = document.createElement('div');
|
|
const blocks = [
|
|
createBlockStub(),
|
|
createBlockStub(),
|
|
createBlockStub(),
|
|
];
|
|
|
|
const blockManager = {
|
|
blocks,
|
|
currentBlock: blocks[0],
|
|
getBlockByIndex: vi.fn((index: number) => blocks[index]),
|
|
getBlock: vi.fn((element: HTMLElement) => blocks.find((block) => block.holder === element) ?? null),
|
|
removeSelectedBlocks: vi.fn(() => 0),
|
|
insertDefaultBlockAtIndex: vi.fn(),
|
|
};
|
|
|
|
const defaults: ModuleOverrides = {
|
|
BlockManager: blockManager as unknown as EditorModules['BlockManager'],
|
|
Caret: {
|
|
setToBlock: vi.fn(),
|
|
insertContentAtCaretPosition: vi.fn(),
|
|
} as unknown as EditorModules['Caret'],
|
|
RectangleSelection: {
|
|
clearSelection: vi.fn(),
|
|
isRectActivated: vi.fn(() => false),
|
|
} as unknown as EditorModules['RectangleSelection'],
|
|
CrossBlockSelection: {
|
|
clear: vi.fn(),
|
|
} as unknown as EditorModules['CrossBlockSelection'],
|
|
InlineToolbar: {
|
|
close: vi.fn(),
|
|
} as unknown as EditorModules['InlineToolbar'],
|
|
ReadOnly: {
|
|
isEnabled: false,
|
|
} as unknown as EditorModules['ReadOnly'],
|
|
UI: {
|
|
nodes: {
|
|
redactor,
|
|
},
|
|
} as unknown as EditorModules['UI'],
|
|
Paste: {
|
|
MIME_TYPE: 'application/editorjs',
|
|
} as unknown as EditorModules['Paste'],
|
|
};
|
|
|
|
const mergedState = { ...defaults,
|
|
...overrides } as EditorModules;
|
|
const blockSelection = new BlockSelection({
|
|
config: { sanitizer: {} } as EditorConfig,
|
|
eventsDispatcher: new EventsDispatcher<EditorEventMap>(),
|
|
});
|
|
|
|
blockSelection.state = mergedState;
|
|
|
|
const selectionSpy: SelectionSpy = {
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
clearSaved: vi.fn(),
|
|
};
|
|
|
|
(blockSelection as unknown as { selection: SelectionUtils }).selection = selectionSpy as unknown as SelectionUtils;
|
|
|
|
return {
|
|
blockSelection,
|
|
modules: mergedState,
|
|
blocks,
|
|
selectionSpy,
|
|
redactor,
|
|
};
|
|
};
|
|
|
|
describe('BlockSelection', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
describe('allBlocksSelected', () => {
|
|
it('returns true only when every block is selected', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
|
|
for (const currentBlock of blocks) {
|
|
currentBlock.selected = true;
|
|
}
|
|
|
|
expect(blockSelection.allBlocksSelected).toBe(true);
|
|
|
|
blocks[1].selected = false;
|
|
|
|
expect(blockSelection.allBlocksSelected).toBe(false);
|
|
});
|
|
|
|
it('setter selects every block and clears cache', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
|
|
const cacheAware = blockSelection as unknown as { anyBlockSelectedCache: boolean | null };
|
|
|
|
// Warm the cache with false
|
|
expect(blockSelection.anyBlockSelected).toBe(false);
|
|
expect(cacheAware.anyBlockSelectedCache).toBe(false);
|
|
|
|
blockSelection.allBlocksSelected = true;
|
|
|
|
expect(blocks.every((block) => block.selected)).toBe(true);
|
|
expect(cacheAware.anyBlockSelectedCache).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('anyBlockSelected', () => {
|
|
it('reuses cached value until cache is cleared', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
|
|
expect(blockSelection.anyBlockSelected).toBe(false);
|
|
|
|
blocks[0].selected = true;
|
|
|
|
expect(blockSelection.anyBlockSelected).toBe(false);
|
|
|
|
blockSelection.clearCache();
|
|
|
|
expect(blockSelection.anyBlockSelected).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('selectedBlocks', () => {
|
|
it('returns only the blocks marked as selected', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
|
|
blocks[0].selected = true;
|
|
blocks[2].selected = true;
|
|
|
|
expect(blockSelection.selectedBlocks).toEqual([blocks[0], blocks[2]]);
|
|
});
|
|
});
|
|
|
|
describe('selectBlock', () => {
|
|
it('saves selection, clears native selection and selects block', () => {
|
|
const { blockSelection, blocks, modules, selectionSpy } = createBlockSelection();
|
|
const removeAllRanges = vi.fn();
|
|
|
|
vi.spyOn(SelectionUtils, 'get').mockReturnValue({
|
|
removeAllRanges,
|
|
} as unknown as Selection);
|
|
|
|
const inlineToolbarClose = modules.InlineToolbar.close as ReturnType<typeof vi.fn>;
|
|
|
|
blockSelection.selectBlock(blocks[0]);
|
|
|
|
expect(selectionSpy.save).toHaveBeenCalledTimes(1);
|
|
expect(removeAllRanges).toHaveBeenCalledTimes(1);
|
|
expect(blocks[0].selected).toBe(true);
|
|
expect(inlineToolbarClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('unSelectBlockByIndex', () => {
|
|
it('unselects the block by explicit index', () => {
|
|
const { blockSelection, blocks, modules } = createBlockSelection();
|
|
const blockManager = modules.BlockManager as unknown as {
|
|
getBlockByIndex: (index: number) => Block | undefined;
|
|
};
|
|
|
|
blocks[2].selected = true;
|
|
blockSelection.unSelectBlockByIndex(2);
|
|
|
|
expect(blocks[2].selected).toBe(false);
|
|
expect(blockManager.getBlockByIndex).toHaveBeenCalledWith(2);
|
|
});
|
|
|
|
it('falls back to current block when index is not provided', () => {
|
|
const { blockSelection, blocks, modules } = createBlockSelection();
|
|
const blockManager = modules.BlockManager as unknown as { currentBlock: Block | null };
|
|
|
|
blockManager.currentBlock = blocks[1];
|
|
blocks[1].selected = true;
|
|
blockSelection.unSelectBlockByIndex(undefined);
|
|
|
|
expect(blocks[1].selected).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('unselectBlock', () => {
|
|
it('clears selection on provided block and resets cache', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
const cacheAware = blockSelection as unknown as { anyBlockSelectedCache: boolean | null };
|
|
|
|
cacheAware.anyBlockSelectedCache = true;
|
|
blocks[1].selected = true;
|
|
|
|
blockSelection.unselectBlock(blocks[1]);
|
|
|
|
expect(blocks[1].selected).toBe(false);
|
|
expect(cacheAware.anyBlockSelectedCache).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('selectBlockByIndex', () => {
|
|
it('selects block returned by block manager', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
|
|
blocks[2].selected = false;
|
|
|
|
blockSelection.selectBlockByIndex(2);
|
|
|
|
expect(blocks[2].selected).toBe(true);
|
|
});
|
|
|
|
it('ignores unknown block indexes', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
const blockManager = modules.BlockManager as unknown as {
|
|
getBlockByIndex: (index: number) => Block | undefined;
|
|
};
|
|
|
|
(blockManager.getBlockByIndex as ReturnType<typeof vi.fn>).mockReturnValueOnce(undefined);
|
|
|
|
expect(() => blockSelection.selectBlockByIndex(99)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('clearSelection', () => {
|
|
it('clears selected blocks on pointer interaction', () => {
|
|
const { blockSelection, blocks, modules } = createBlockSelection();
|
|
const rectangleSelection = modules.RectangleSelection as unknown as {
|
|
clearSelection: () => void;
|
|
isRectActivated: () => boolean;
|
|
};
|
|
const crossBlockSelection = modules.CrossBlockSelection as unknown as {
|
|
clear: (reason?: Event) => void;
|
|
};
|
|
|
|
blocks[0].selected = true;
|
|
blocks[1].selected = true;
|
|
|
|
const event = new MouseEvent('click');
|
|
|
|
blockSelection.clearSelection(event);
|
|
|
|
expect(blocks[0].selected).toBe(false);
|
|
expect(blocks[1].selected).toBe(false);
|
|
expect(crossBlockSelection.clear).toHaveBeenCalledWith(event);
|
|
expect(rectangleSelection.clearSelection).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('restores previous selection when requested', () => {
|
|
const { blockSelection, selectionSpy } = createBlockSelection();
|
|
const selectionAccessor = blockSelection as unknown as {
|
|
needToSelectAll: boolean;
|
|
readyToBlockSelection: boolean;
|
|
};
|
|
|
|
selectionAccessor.needToSelectAll = true;
|
|
selectionAccessor.readyToBlockSelection = true;
|
|
|
|
blockSelection.clearSelection(undefined, true);
|
|
|
|
expect(selectionSpy.restore).toHaveBeenCalledTimes(1);
|
|
expect(selectionAccessor.needToSelectAll).toBe(false);
|
|
expect(selectionAccessor.readyToBlockSelection).toBe(false);
|
|
});
|
|
|
|
it('replaces selected blocks with printable keys when applicable', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
const replacerHost = blockSelection as unknown as {
|
|
replaceSelectedBlocksWithPrintableKey: (event: KeyboardEvent) => void;
|
|
};
|
|
const replaceSpy = vi.spyOn(replacerHost, 'replaceSelectedBlocksWithPrintableKey');
|
|
|
|
blocks[0].selected = true;
|
|
|
|
vi.spyOn(utils, 'isPrintableKey').mockReturnValue(true);
|
|
vi.spyOn(SelectionUtils, 'isSelectionExists', 'get').mockReturnValue(false);
|
|
|
|
const event = new KeyboardEvent('keydown', { key: 'x' });
|
|
|
|
Object.defineProperty(event, 'keyCode', { value: 88 });
|
|
|
|
blockSelection.clearSelection(event);
|
|
|
|
expect(replaceSpy).toHaveBeenCalledWith(event);
|
|
});
|
|
});
|
|
|
|
describe('copySelectedBlocks', () => {
|
|
it('serializes selected blocks and writes clipboard data', async () => {
|
|
const { blockSelection, blocks, modules } = createBlockSelection();
|
|
const clipboardData = {
|
|
setData: vi.fn(),
|
|
};
|
|
const clipboardEvent = {
|
|
preventDefault: vi.fn(),
|
|
clipboardData,
|
|
} as unknown as ClipboardEvent;
|
|
|
|
const firstBlock = blocks[0];
|
|
const secondBlock = blocks[1];
|
|
|
|
firstBlock.holder.innerHTML = '<p>First</p>';
|
|
secondBlock.holder.innerHTML = '<p>Second</p>';
|
|
firstBlock.selected = true;
|
|
secondBlock.selected = true;
|
|
|
|
await blockSelection.copySelectedBlocks(clipboardEvent);
|
|
|
|
expect(clipboardEvent.preventDefault).toHaveBeenCalledTimes(1);
|
|
expect(clipboardData.setData).toHaveBeenCalledWith('text/plain', 'First\n\nSecond');
|
|
expect(clipboardData.setData).toHaveBeenCalledWith('text/html', '<p>First</p><p>Second</p>');
|
|
expect(clipboardData.setData).toHaveBeenCalledWith(
|
|
(modules.Paste as unknown as { MIME_TYPE: string }).MIME_TYPE,
|
|
expect.stringContaining('"tool":"paragraph"')
|
|
);
|
|
expect(firstBlock.save).toHaveBeenCalledTimes(1);
|
|
expect(secondBlock.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('sanitizerConfig', () => {
|
|
it('provides default tag white-list when no overrides supplied', () => {
|
|
const { blockSelection } = createBlockSelection();
|
|
const config = (blockSelection as unknown as { sanitizerConfig: SanitizerConfig }).sanitizerConfig;
|
|
|
|
expect(config).toMatchObject({
|
|
p: {},
|
|
img: {
|
|
src: true,
|
|
width: true,
|
|
height: true,
|
|
},
|
|
a: { href: true },
|
|
});
|
|
});
|
|
|
|
it('merges provided sanitizer overrides with defaults', () => {
|
|
const { blockSelection } = createBlockSelection();
|
|
const override: SanitizerConfig = {
|
|
a: { rel: 'nofollow' },
|
|
};
|
|
const internals = blockSelection as unknown as { config: EditorConfig };
|
|
|
|
internals.config = {
|
|
...internals.config,
|
|
sanitizer: override,
|
|
};
|
|
|
|
const config = (blockSelection as unknown as { sanitizerConfig: SanitizerConfig }).sanitizerConfig;
|
|
|
|
expect(config.a).toMatchObject({
|
|
href: true,
|
|
rel: 'nofollow',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('toggleReadOnly', () => {
|
|
it('removes native selection and unselects blocks', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
const selectionMock = {
|
|
removeAllRanges: vi.fn(),
|
|
};
|
|
|
|
for (const currentBlock of blocks) {
|
|
currentBlock.selected = true;
|
|
}
|
|
|
|
vi.spyOn(SelectionUtils, 'get').mockReturnValue(selectionMock as unknown as Selection);
|
|
|
|
blockSelection.toggleReadOnly();
|
|
|
|
expect(selectionMock.removeAllRanges).toHaveBeenCalledTimes(1);
|
|
expect(blocks.every((block) => block.selected === false)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('handleCommandA', () => {
|
|
it('selects all blocks on second invocation for single-input tools', () => {
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
const internal = blockSelection as unknown as { needToSelectAll: boolean };
|
|
const selectAllSpy = vi.spyOn(blockSelection as unknown as { selectAllBlocks: () => void }, 'selectAllBlocks');
|
|
const event = {
|
|
target: blocks[0].holder,
|
|
preventDefault: vi.fn(),
|
|
} as unknown as KeyboardEvent;
|
|
|
|
(blockSelection as unknown as { selection: SelectionUtils }).selection = {
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
clearSaved: vi.fn(),
|
|
} as unknown as SelectionUtils;
|
|
|
|
const handler = blockSelection as unknown as { handleCommandA: (keyboardEvent: KeyboardEvent) => void };
|
|
|
|
handler.handleCommandA(event);
|
|
expect(internal.needToSelectAll).toBe(true);
|
|
expect(selectAllSpy).not.toHaveBeenCalled();
|
|
|
|
handler.handleCommandA(event);
|
|
|
|
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
|
expect(selectAllSpy).toHaveBeenCalledTimes(1);
|
|
expect(internal.needToSelectAll).toBe(false);
|
|
});
|
|
|
|
it('selects current block when tool exposes multiple inputs', () => {
|
|
const multiInput = document.createElement('div');
|
|
const { blockSelection, blocks } = createBlockSelection();
|
|
const blockManager = (blockSelection as unknown as { Editor: EditorModules }).Editor.BlockManager;
|
|
const targetBlock = blocks[0];
|
|
|
|
(targetBlock as unknown as { inputs: HTMLElement[] }).inputs = [multiInput, document.createElement('div')];
|
|
const selectBlockSpy = vi.spyOn(blockSelection, 'selectBlock');
|
|
const handler = blockSelection as unknown as { handleCommandA: (keyboardEvent: KeyboardEvent) => void };
|
|
|
|
const event = {
|
|
target: targetBlock.holder,
|
|
preventDefault: vi.fn(),
|
|
} as unknown as KeyboardEvent;
|
|
|
|
(blockManager.getBlock as (element: HTMLElement) => Block | null).call(blockManager, targetBlock.holder);
|
|
|
|
handler.handleCommandA(event);
|
|
expect(selectBlockSpy).not.toHaveBeenCalled();
|
|
|
|
handler.handleCommandA(event);
|
|
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
|
expect(selectBlockSpy).toHaveBeenCalledWith(targetBlock);
|
|
});
|
|
|
|
it('defers to native input selection before enabling block selection', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
const rectangleSelection = modules.RectangleSelection as unknown as { clearSelection: ReturnType<typeof vi.fn> };
|
|
const handler = blockSelection as unknown as {
|
|
handleCommandA: (event: KeyboardEvent) => void;
|
|
readyToBlockSelection: boolean;
|
|
};
|
|
const event = {
|
|
target: document.createElement('input'),
|
|
preventDefault: vi.fn(),
|
|
} as unknown as KeyboardEvent;
|
|
|
|
handler.handleCommandA(event);
|
|
|
|
expect(rectangleSelection.clearSelection).toHaveBeenCalledTimes(1);
|
|
expect(handler.readyToBlockSelection).toBe(true);
|
|
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('replaceSelectedBlocksWithPrintableKey', () => {
|
|
it('inserts new block and types printable character', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
const blockManager = modules.BlockManager as unknown as {
|
|
removeSelectedBlocks: ReturnType<typeof vi.fn>;
|
|
insertDefaultBlockAtIndex: ReturnType<typeof vi.fn>;
|
|
currentBlock: Block | null;
|
|
};
|
|
const caret = modules.Caret as unknown as {
|
|
setToBlock: ReturnType<typeof vi.fn>;
|
|
insertContentAtCaretPosition: ReturnType<typeof vi.fn>;
|
|
};
|
|
const insertedBlock = createBlockStub();
|
|
const delaySpy = vi.spyOn(utils, 'delay').mockImplementation((fn) => {
|
|
return () => fn();
|
|
});
|
|
|
|
blockManager.currentBlock = null;
|
|
blockManager.removeSelectedBlocks = vi.fn().mockReturnValue(1);
|
|
blockManager.insertDefaultBlockAtIndex = vi.fn().mockImplementation(() => {
|
|
blockManager.currentBlock = insertedBlock;
|
|
});
|
|
|
|
const host = blockSelection as unknown as {
|
|
replaceSelectedBlocksWithPrintableKey: (event: KeyboardEvent) => void;
|
|
};
|
|
|
|
host.replaceSelectedBlocksWithPrintableKey({ key: 'x' } as KeyboardEvent);
|
|
|
|
expect(blockManager.removeSelectedBlocks).toHaveBeenCalledTimes(1);
|
|
expect(blockManager.insertDefaultBlockAtIndex).toHaveBeenCalledWith(1, true);
|
|
expect(caret.setToBlock).toHaveBeenCalledWith(insertedBlock);
|
|
expect(caret.insertContentAtCaretPosition).toHaveBeenCalledWith('x');
|
|
expect(delaySpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('inserts empty string for non-printable keys', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
const blockManager = modules.BlockManager as unknown as {
|
|
removeSelectedBlocks: ReturnType<typeof vi.fn>;
|
|
insertDefaultBlockAtIndex: ReturnType<typeof vi.fn>;
|
|
currentBlock: Block | null;
|
|
};
|
|
const caret = modules.Caret as unknown as {
|
|
setToBlock: ReturnType<typeof vi.fn>;
|
|
insertContentAtCaretPosition: ReturnType<typeof vi.fn>;
|
|
};
|
|
const insertedBlock = createBlockStub();
|
|
|
|
vi.spyOn(utils, 'delay').mockImplementation((fn) => {
|
|
return () => fn();
|
|
});
|
|
|
|
blockManager.currentBlock = null;
|
|
blockManager.removeSelectedBlocks = vi.fn().mockReturnValue(2);
|
|
blockManager.insertDefaultBlockAtIndex = vi.fn().mockImplementation(() => {
|
|
blockManager.currentBlock = insertedBlock;
|
|
});
|
|
|
|
const host = blockSelection as unknown as {
|
|
replaceSelectedBlocksWithPrintableKey: (event: KeyboardEvent) => void;
|
|
};
|
|
|
|
host.replaceSelectedBlocksWithPrintableKey({ key: 'Enter' } as KeyboardEvent);
|
|
|
|
expect(blockManager.insertDefaultBlockAtIndex).toHaveBeenCalledWith(2, true);
|
|
expect(caret.setToBlock).toHaveBeenCalledWith(insertedBlock);
|
|
expect(caret.insertContentAtCaretPosition).toHaveBeenCalledWith('');
|
|
});
|
|
});
|
|
|
|
describe('prepare', () => {
|
|
it('creates selection instance and registers shortcut handler', () => {
|
|
const { blockSelection, redactor } = createBlockSelection();
|
|
const shortcutsAdd = vi.spyOn(Shortcuts, 'add').mockImplementation(() => undefined);
|
|
|
|
blockSelection.prepare();
|
|
|
|
expect(shortcutsAdd).toHaveBeenCalledWith(
|
|
expect.objectContaining({ name: 'CMD+A',
|
|
on: redactor })
|
|
);
|
|
expect((blockSelection as unknown as { selection: SelectionUtils }).selection).toBeInstanceOf(SelectionUtils);
|
|
});
|
|
|
|
it('handles read-only mode by selecting all blocks via shortcut handler', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
|
|
Object.defineProperty(modules.ReadOnly, 'isEnabled', {
|
|
value: true,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
const selectAllSpy = vi.spyOn(blockSelection as unknown as { selectAllBlocks: () => void }, 'selectAllBlocks');
|
|
const shortcutsAdd = vi.spyOn(Shortcuts, 'add').mockImplementation(() => undefined);
|
|
|
|
blockSelection.prepare();
|
|
|
|
expect(shortcutsAdd).toHaveBeenCalledTimes(1);
|
|
|
|
const handler = (shortcutsAdd.mock.calls[0]?.[0] as { handler: (event: KeyboardEvent) => void }).handler;
|
|
const event = {
|
|
preventDefault: vi.fn(),
|
|
} as unknown as KeyboardEvent;
|
|
|
|
handler(event);
|
|
|
|
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
|
expect(selectAllSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('destroy', () => {
|
|
it('removes the CMD+A shortcut', () => {
|
|
const { blockSelection, modules } = createBlockSelection();
|
|
const shortcutsRemove = vi.spyOn(Shortcuts, 'remove').mockImplementation(() => undefined);
|
|
|
|
blockSelection.destroy();
|
|
|
|
expect(shortcutsRemove).toHaveBeenCalledWith(modules.UI.nodes.redactor, 'CMD+A');
|
|
});
|
|
});
|
|
});
|