mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 07:35:48 +01:00
fix: all typescript errors
This commit is contained in:
parent
b827e4a30f
commit
b677b63eeb
16 changed files with 1077 additions and 67 deletions
|
|
@ -17,7 +17,7 @@
|
|||
"serve": "vite --no-open",
|
||||
"build": "vite build --mode production",
|
||||
"build:test": "vite build --mode test",
|
||||
"lint": "eslint . && tsc --noEmit",
|
||||
"lint": "sh -c 'eslint .; ESLINT_EXIT=$?; tsc --noEmit; TSC_EXIT=$?; if [ $ESLINT_EXIT -ne 0 ] || [ $TSC_EXIT -ne 0 ]; then exit 1; fi'",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:tests": "eslint test/",
|
||||
"lint:types": "tsc --noEmit",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default class ItalicInlineTool implements InlineTool {
|
|||
/**
|
||||
* Elements
|
||||
*/
|
||||
private nodes: {button: HTMLButtonElement} = {
|
||||
private nodes: {button: HTMLButtonElement | null} = {
|
||||
button: null,
|
||||
};
|
||||
|
||||
|
|
@ -58,12 +58,15 @@ export default class ItalicInlineTool implements InlineTool {
|
|||
* Create button for Inline Toolbar
|
||||
*/
|
||||
public render(): HTMLElement {
|
||||
this.nodes.button = document.createElement('button') as HTMLButtonElement;
|
||||
this.nodes.button.type = 'button';
|
||||
this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
this.nodes.button.innerHTML = IconItalic;
|
||||
const button = document.createElement('button');
|
||||
|
||||
return this.nodes.button;
|
||||
button.type = 'button';
|
||||
button.classList.add(this.CSS.button, this.CSS.buttonModifier);
|
||||
button.innerHTML = IconItalic;
|
||||
|
||||
this.nodes.button = button;
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +82,9 @@ export default class ItalicInlineTool implements InlineTool {
|
|||
public checkState(): boolean {
|
||||
const isActive = document.queryCommandState(this.commandName);
|
||||
|
||||
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
|
||||
if (this.nodes.button) {
|
||||
this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);
|
||||
}
|
||||
|
||||
return isActive;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default class I18nAPI extends Module {
|
|||
* @param toolName - tool name
|
||||
* @param isTune - is tool a block tune
|
||||
*/
|
||||
private static getNamespace(toolName, isTune): string {
|
||||
private static getNamespace(toolName: string, isTune: boolean): string {
|
||||
if (isTune) {
|
||||
return `blockTunes.${toolName}`;
|
||||
}
|
||||
|
|
@ -26,10 +26,10 @@ export default class I18nAPI extends Module {
|
|||
*/
|
||||
public get methods(): I18n {
|
||||
return {
|
||||
t: (): string | undefined => {
|
||||
t: (_dictKey?: string): string => {
|
||||
logLabeled('I18n.t() method can be accessed only from Tools', 'warn');
|
||||
|
||||
return undefined;
|
||||
return '';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,15 +24,31 @@ export default class SaverAPI extends Module {
|
|||
*
|
||||
* @returns {OutputData}
|
||||
*/
|
||||
public save(): Promise<OutputData> {
|
||||
public async save(): Promise<OutputData> {
|
||||
const errorText = 'Editor\'s content can not be saved in read-only mode';
|
||||
|
||||
if (this.Editor.ReadOnly.isEnabled) {
|
||||
_.logLabeled(errorText, 'warn');
|
||||
|
||||
return Promise.reject(new Error(errorText));
|
||||
throw new Error(errorText);
|
||||
}
|
||||
|
||||
return this.Editor.Saver.save();
|
||||
const savedData = await this.Editor.Saver.save();
|
||||
|
||||
if (savedData === undefined) {
|
||||
const lastError = this.Editor.Saver.getLastSaveError?.();
|
||||
|
||||
if (lastError instanceof Error) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
const errorMessage = lastError !== undefined
|
||||
? String(lastError)
|
||||
: 'Editor\'s content can not be saved because collecting data failed';
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return savedData;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,9 +98,12 @@ export default class DragNDrop extends Module {
|
|||
this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);
|
||||
} else {
|
||||
const lastBlock = BlockManager.lastBlock;
|
||||
const blockToSet = BlockManager.setCurrentBlockByChildNode(lastBlock.holder) ?? lastBlock;
|
||||
|
||||
this.Editor.Caret.setToBlock(blockToSet, Caret.positions.END);
|
||||
if (lastBlock) {
|
||||
const blockToSet = BlockManager.setCurrentBlockByChildNode(lastBlock.holder) ?? lastBlock;
|
||||
|
||||
this.Editor.Caret.setToBlock(blockToSet, Caret.positions.END);
|
||||
}
|
||||
}
|
||||
|
||||
const { dataTransfer } = dropEvent;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,12 @@ export default class ReadOnly extends Module {
|
|||
*/
|
||||
const savedBlocks = await this.Editor.Saver.save();
|
||||
|
||||
if (savedBlocks === undefined) {
|
||||
this.Editor.ModificationsObserver.enable();
|
||||
|
||||
return this.readOnlyEnabled;
|
||||
}
|
||||
|
||||
await this.Editor.BlockManager.clear();
|
||||
await this.Editor.Renderer.render(savedBlocks.blocks);
|
||||
|
||||
|
|
|
|||
|
|
@ -485,8 +485,8 @@ export default class RectangleSelection extends Module {
|
|||
const centerOfRedactor = widthOfRedactor / 2;
|
||||
const y = this.mouseY - window.pageYOffset;
|
||||
const elementUnderMouse = document.elementFromPoint(centerOfRedactor, y);
|
||||
const lastBlockHolder = this.Editor.BlockManager.lastBlock.holder;
|
||||
const contentElement = lastBlockHolder.querySelector('.' + Block.CSS.content);
|
||||
const lastBlockHolder = this.Editor.BlockManager.lastBlock?.holder;
|
||||
const contentElement = lastBlockHolder?.querySelector('.' + Block.CSS.content);
|
||||
const contentWidth = contentElement ? Number.parseInt(window.getComputedStyle(contentElement).width, 10) : 0;
|
||||
const centerOfBlock = contentWidth / 2;
|
||||
const leftPos = centerOfRedactor - centerOfBlock;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,19 @@
|
|||
* @version 2.0.0
|
||||
*/
|
||||
import Module from '../__module';
|
||||
import type { OutputData, SanitizerConfig } from '../../../types';
|
||||
import type { BlockToolData, OutputData, SanitizerConfig } from '../../../types';
|
||||
import type { SavedData, ValidatedData } from '../../../types/data-formats';
|
||||
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
|
||||
import type Block from '../block';
|
||||
import * as _ from '../utils';
|
||||
import { sanitizeBlocks } from '../utils/sanitizer';
|
||||
|
||||
type SaverValidatedData = ValidatedData & {
|
||||
tunes?: Record<string, BlockTuneData>;
|
||||
};
|
||||
|
||||
type SanitizableBlockData = SaverValidatedData & Pick<SavedData, 'data' | 'tool'>;
|
||||
|
||||
/**
|
||||
* @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data
|
||||
* @typedef {Saver} Saver
|
||||
|
|
@ -19,31 +26,42 @@ import { sanitizeBlocks } from '../utils/sanitizer';
|
|||
* @property {string} json - Editor JSON output
|
||||
*/
|
||||
export default class Saver extends Module {
|
||||
/**
|
||||
* Stores the last error raised during save attempt
|
||||
*/
|
||||
private lastSaveError?: unknown;
|
||||
|
||||
/**
|
||||
* Composes new chain of Promises to fire them alternatelly
|
||||
*
|
||||
* @returns {OutputData}
|
||||
* @returns {OutputData | undefined}
|
||||
*/
|
||||
public async save(): Promise<OutputData> {
|
||||
public async save(): Promise<OutputData | undefined> {
|
||||
const { BlockManager, Tools } = this.Editor;
|
||||
const blocks = BlockManager.blocks;
|
||||
const chainData = [];
|
||||
const chainData: Array<Promise<SaverValidatedData>> = blocks.map((block: Block) => {
|
||||
return this.getSavedData(block);
|
||||
});
|
||||
|
||||
this.lastSaveError = undefined;
|
||||
|
||||
try {
|
||||
blocks.forEach((block: Block) => {
|
||||
chainData.push(this.getSavedData(block));
|
||||
});
|
||||
|
||||
const extractedData = await Promise.all(chainData) as Array<Pick<SavedData, 'data' | 'tool'>>;
|
||||
const sanitizedData = await sanitizeBlocks(
|
||||
const extractedData = await Promise.all(chainData);
|
||||
const sanitizedData = this.sanitizeExtractedData(
|
||||
extractedData,
|
||||
(name) => Tools.blockTools.get(name).sanitizeConfig,
|
||||
(name) => Tools.blockTools.get(name)?.sanitizeConfig,
|
||||
this.config.sanitizer as SanitizerConfig
|
||||
);
|
||||
|
||||
return this.makeOutput(sanitizedData);
|
||||
} catch (e) {
|
||||
_.logLabeled(`Saving failed due to the Error %o`, 'error', e);
|
||||
} catch (error: unknown) {
|
||||
this.lastSaveError = error;
|
||||
|
||||
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
_.logLabeled(`Saving failed due to the Error %o`, 'error', normalizedError);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,9 +71,18 @@ export default class Saver extends Module {
|
|||
* @param {Block} block - Editor's Tool
|
||||
* @returns {ValidatedData} - Tool's validated data
|
||||
*/
|
||||
private async getSavedData(block: Block): Promise<ValidatedData> {
|
||||
private async getSavedData(block: Block): Promise<SaverValidatedData> {
|
||||
const blockData = await block.save();
|
||||
const isValid = blockData && await block.validate(blockData.data);
|
||||
const toolName = block.name;
|
||||
|
||||
if (blockData === undefined) {
|
||||
return {
|
||||
tool: toolName,
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const isValid = await block.validate(blockData.data);
|
||||
|
||||
return {
|
||||
...blockData,
|
||||
|
|
@ -69,8 +96,8 @@ export default class Saver extends Module {
|
|||
* @param {ValidatedData} allExtractedData - data extracted from Blocks
|
||||
* @returns {OutputData}
|
||||
*/
|
||||
private makeOutput(allExtractedData): OutputData {
|
||||
const blocks = [];
|
||||
private makeOutput(allExtractedData: SaverValidatedData[]): OutputData {
|
||||
const blocks: OutputData['blocks'] = [];
|
||||
|
||||
allExtractedData.forEach(({ id, tool, data, tunes, isValid }) => {
|
||||
if (!isValid) {
|
||||
|
|
@ -79,18 +106,29 @@ export default class Saver extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
/** If it was stub Block, get original data */
|
||||
if (tool === this.Editor.Tools.stubTool) {
|
||||
blocks.push(data);
|
||||
if (tool === undefined || data === undefined) {
|
||||
_.log('Block skipped because saved data is missing required fields');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const output = {
|
||||
/** If it was stub Block, get original data */
|
||||
if (tool === this.Editor.Tools.stubTool) {
|
||||
if (this.isStubSavedData(data)) {
|
||||
blocks.push(data);
|
||||
} else {
|
||||
_.log('Stub block data is malformed and was skipped');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isTunesEmpty = tunes === undefined || _.isEmpty(tunes);
|
||||
const output: OutputData['blocks'][number] = {
|
||||
id,
|
||||
type: tool,
|
||||
data,
|
||||
...!_.isEmpty(tunes) && {
|
||||
...!isTunesEmpty && {
|
||||
tunes,
|
||||
},
|
||||
};
|
||||
|
|
@ -104,4 +142,82 @@ export default class Saver extends Module {
|
|||
version: _.getEditorVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes extracted block data in-place
|
||||
*
|
||||
* @param extractedData - collection of saved block data
|
||||
* @param getToolSanitizeConfig - resolver for tool-specific sanitize config
|
||||
* @param globalSanitizer - global sanitizer config specified in editor settings
|
||||
*/
|
||||
private sanitizeExtractedData(
|
||||
extractedData: SaverValidatedData[],
|
||||
getToolSanitizeConfig: (toolName: string) => SanitizerConfig | undefined,
|
||||
globalSanitizer: SanitizerConfig
|
||||
): SaverValidatedData[] {
|
||||
const blocksToSanitize: Array<{ index: number; data: SanitizableBlockData }> = [];
|
||||
|
||||
extractedData.forEach((blockData, index) => {
|
||||
if (this.hasSanitizableData(blockData)) {
|
||||
blocksToSanitize.push({
|
||||
index,
|
||||
data: blockData,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (blocksToSanitize.length === 0) {
|
||||
return extractedData;
|
||||
}
|
||||
|
||||
const sanitizedBlocks = sanitizeBlocks(
|
||||
blocksToSanitize.map(({ data }) => data),
|
||||
getToolSanitizeConfig,
|
||||
globalSanitizer
|
||||
);
|
||||
|
||||
const updatedData = extractedData.map((blockData) => ({ ...blockData }));
|
||||
|
||||
blocksToSanitize.forEach(({ index }, sanitizedIndex) => {
|
||||
const sanitized = sanitizedBlocks[sanitizedIndex];
|
||||
|
||||
updatedData[index] = {
|
||||
...updatedData[index],
|
||||
data: sanitized.data,
|
||||
};
|
||||
});
|
||||
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether block data contains fields required for sanitizing procedure
|
||||
*
|
||||
* @param blockData - data to check
|
||||
*/
|
||||
private hasSanitizableData(blockData: SaverValidatedData): blockData is SanitizableBlockData {
|
||||
return blockData.data !== undefined && typeof blockData.tool === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that stub data matches OutputBlockData format
|
||||
*
|
||||
* @param data - saved stub data that should represent original block payload
|
||||
*/
|
||||
private isStubSavedData(data: BlockToolData): data is OutputData['blocks'][number] {
|
||||
if (!_.isObject(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = data as Record<string, unknown>;
|
||||
|
||||
return typeof candidate.type === 'string' && candidate.data !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error raised during save attempt
|
||||
*/
|
||||
public getLastSaveError(): unknown {
|
||||
return this.lastSaveError;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Tool, ToolConstructable, ToolSettings } from '@/types/tools';
|
||||
import type { SanitizerConfig, API as ApiMethods } from '@/types';
|
||||
import type { SanitizerConfig, API as ApiMethods, ToolConfig } from '@/types';
|
||||
import * as _ from '../utils';
|
||||
import { ToolType } from '@/types/tools/adapters/tool-type';
|
||||
import type { BaseToolAdapter as BaseToolAdapterInterface } from '@/types/tools/adapters/base-tool-adapter';
|
||||
|
|
@ -106,6 +106,11 @@ export enum InternalTuneSettings {
|
|||
|
||||
export type ToolOptions = Omit<ToolSettings, 'class'>;
|
||||
|
||||
type ToolPreparePayload = {
|
||||
toolName: string;
|
||||
config: ToolConfig;
|
||||
};
|
||||
|
||||
interface ConstructorOptions {
|
||||
name: string;
|
||||
constructable: ToolConstructable;
|
||||
|
|
@ -123,7 +128,7 @@ export default abstract class BaseToolAdapter<Type extends ToolType = ToolType,
|
|||
/**
|
||||
* Tool type: Block, Inline or Tune
|
||||
*/
|
||||
public type: Type;
|
||||
public abstract type: Type;
|
||||
|
||||
/**
|
||||
* Tool name specified in EditorJS config
|
||||
|
|
@ -185,8 +190,8 @@ export default abstract class BaseToolAdapter<Type extends ToolType = ToolType,
|
|||
/**
|
||||
* Returns Tool user configuration
|
||||
*/
|
||||
public get settings(): ToolOptions {
|
||||
const config = this.config[UserSettings.Config] || {};
|
||||
public get settings(): ToolConfig {
|
||||
const config = (this.config[UserSettings.Config] ?? {}) as ToolConfig;
|
||||
|
||||
if (this.isDefault && !('placeholder' in config) && this.defaultPlaceholder) {
|
||||
config.placeholder = this.defaultPlaceholder;
|
||||
|
|
@ -208,12 +213,18 @@ export default abstract class BaseToolAdapter<Type extends ToolType = ToolType,
|
|||
* Calls Tool's prepare method
|
||||
*/
|
||||
public prepare(): void | Promise<void> {
|
||||
if (_.isFunction(this.constructable.prepare)) {
|
||||
return this.constructable.prepare({
|
||||
toolName: this.name,
|
||||
config: this.settings,
|
||||
});
|
||||
const prepare = this.constructable.prepare;
|
||||
|
||||
if (!_.isFunction(prepare)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: ToolPreparePayload = {
|
||||
toolName: this.name,
|
||||
config: this.settings,
|
||||
};
|
||||
|
||||
return (prepare as (data: ToolPreparePayload) => void | Promise<void>).call(this.constructable, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
82
test/unit/components/inline-tools/inline-tool-italic.test.ts
Normal file
82
test/unit/components/inline-tools/inline-tool-italic.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { IconItalic } from '@codexteam/icons';
|
||||
|
||||
import ItalicInlineTool from '../../../../src/components/inline-tools/inline-tool-italic';
|
||||
|
||||
type DocumentCommandKey = 'execCommand' | 'queryCommandState';
|
||||
|
||||
const setDocumentCommand = <K extends DocumentCommandKey>(
|
||||
key: K,
|
||||
implementation: Document[K]
|
||||
): void => {
|
||||
Object.defineProperty(document, key, {
|
||||
configurable: true,
|
||||
value: implementation,
|
||||
writable: true,
|
||||
});
|
||||
};
|
||||
|
||||
describe('ItalicInlineTool', () => {
|
||||
let tool: ItalicInlineTool;
|
||||
let execCommandMock: ReturnType<typeof vi.fn<[], boolean>>;
|
||||
let queryCommandStateMock: ReturnType<typeof vi.fn<[], boolean>>;
|
||||
|
||||
beforeEach(() => {
|
||||
execCommandMock = vi.fn(() => true);
|
||||
queryCommandStateMock = vi.fn(() => false);
|
||||
|
||||
setDocumentCommand('execCommand', execCommandMock as Document['execCommand']);
|
||||
setDocumentCommand('queryCommandState', queryCommandStateMock as Document['queryCommandState']);
|
||||
|
||||
tool = new ItalicInlineTool();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('exposes inline metadata and sanitizer config', () => {
|
||||
expect(ItalicInlineTool.isInline).toBe(true);
|
||||
expect(ItalicInlineTool.title).toBe('Italic');
|
||||
expect(ItalicInlineTool.sanitize).toStrictEqual({ i: {} });
|
||||
});
|
||||
|
||||
it('renders an inline toolbar button with italic icon', () => {
|
||||
const element = tool.render();
|
||||
const button = element as HTMLButtonElement;
|
||||
const expectedIcon = document.createElement('div');
|
||||
|
||||
expectedIcon.innerHTML = IconItalic;
|
||||
|
||||
expect(button).toBeInstanceOf(HTMLButtonElement);
|
||||
expect(button.type).toBe('button');
|
||||
expect(button.classList.contains('ce-inline-tool')).toBe(true);
|
||||
expect(button.classList.contains('ce-inline-tool--italic')).toBe(true);
|
||||
expect(button.innerHTML).toBe(expectedIcon.innerHTML);
|
||||
});
|
||||
|
||||
it('executes italic command when surround is called', () => {
|
||||
tool.surround();
|
||||
|
||||
expect(execCommandMock).toHaveBeenCalledWith('italic');
|
||||
});
|
||||
|
||||
it('synchronizes button active state with document command state', () => {
|
||||
const button = tool.render();
|
||||
|
||||
queryCommandStateMock.mockReturnValue(true);
|
||||
|
||||
expect(tool.checkState()).toBe(true);
|
||||
expect(button.classList.contains('ce-inline-tool--active')).toBe(true);
|
||||
|
||||
queryCommandStateMock.mockReturnValue(false);
|
||||
|
||||
expect(tool.checkState()).toBe(false);
|
||||
expect(button.classList.contains('ce-inline-tool--active')).toBe(false);
|
||||
});
|
||||
|
||||
it('exposes CMD+I shortcut', () => {
|
||||
expect(tool.shortcut).toBe('CMD+I');
|
||||
});
|
||||
});
|
||||
|
||||
95
test/unit/components/modules/api/i18n.test.ts
Normal file
95
test/unit/components/modules/api/i18n.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import I18nAPI from '../../../../../src/components/modules/api/i18n';
|
||||
import EventsDispatcher from '../../../../../src/components/utils/events';
|
||||
|
||||
import type { ModuleConfig } from '../../../../../src/types-internal/module-config';
|
||||
import type { EditorConfig } from '../../../../../types';
|
||||
import type { EditorEventMap } from '../../../../../src/components/events';
|
||||
|
||||
type EsModuleKey = '__esModule';
|
||||
type EsModule<T extends object> = T & { [K in EsModuleKey]: true };
|
||||
type WithEsModuleFlag = <T extends object>(moduleMock: T) => EsModule<T>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param moduleMock - The module object to add the ES module flag to
|
||||
*/
|
||||
const withEsModuleFlag: WithEsModuleFlag = vi.hoisted(() => {
|
||||
return (<T extends object>(moduleMock: T): EsModule<T> => {
|
||||
return Object.defineProperty(moduleMock, '__esModule', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: true,
|
||||
}) as EsModule<T>;
|
||||
});
|
||||
});
|
||||
|
||||
const { logLabeledMock, translateMock } = vi.hoisted(() => {
|
||||
return {
|
||||
logLabeledMock: vi.fn(),
|
||||
translateMock: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../../src/components/utils', () =>
|
||||
withEsModuleFlag({
|
||||
logLabeled: logLabeledMock,
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock('../../../../../src/components/i18n', () =>
|
||||
withEsModuleFlag({
|
||||
default: {
|
||||
t: translateMock,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const createI18nApi = (): I18nAPI => {
|
||||
const eventsDispatcher = new EventsDispatcher<EditorEventMap>();
|
||||
const moduleConfig: ModuleConfig = {
|
||||
config: {} as EditorConfig,
|
||||
eventsDispatcher,
|
||||
};
|
||||
|
||||
return new I18nAPI(moduleConfig);
|
||||
};
|
||||
|
||||
describe('I18nAPI', () => {
|
||||
beforeEach(() => {
|
||||
logLabeledMock.mockReset();
|
||||
translateMock.mockReset();
|
||||
});
|
||||
|
||||
it('warns and returns an empty string when calling global t()', () => {
|
||||
const api = createI18nApi();
|
||||
|
||||
const result = api.methods.t('global');
|
||||
|
||||
expect(result).toBe('');
|
||||
expect(logLabeledMock).toHaveBeenCalledWith(
|
||||
'I18n.t() method can be accessed only from Tools',
|
||||
'warn'
|
||||
);
|
||||
});
|
||||
|
||||
it('translates using tools namespace for regular tool', () => {
|
||||
const api = createI18nApi();
|
||||
const methods = api.getMethodsForTool('paragraph', false);
|
||||
|
||||
methods.t('label');
|
||||
|
||||
expect(translateMock).toHaveBeenCalledWith('tools.paragraph', 'label');
|
||||
});
|
||||
|
||||
it('translates using blockTunes namespace for block tune', () => {
|
||||
const api = createI18nApi();
|
||||
const methods = api.getMethodsForTool('settings', true);
|
||||
|
||||
methods.t('title');
|
||||
|
||||
expect(translateMock).toHaveBeenCalledWith('blockTunes.settings', 'title');
|
||||
});
|
||||
});
|
||||
|
||||
135
test/unit/components/modules/api/saver.test.ts
Normal file
135
test/unit/components/modules/api/saver.test.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import SaverAPI from '../../../../../src/components/modules/api/saver';
|
||||
import EventsDispatcher from '../../../../../src/components/utils/events';
|
||||
import * as utils from '../../../../../src/components/utils';
|
||||
|
||||
import type { ModuleConfig } from '../../../../../src/types-internal/module-config';
|
||||
import type { EditorConfig, OutputData } from '../../../../../types';
|
||||
import type { EditorEventMap } from '../../../../../src/components/events';
|
||||
import type { EditorModules } from '../../../../../src/types-internal/editor-modules';
|
||||
|
||||
const READ_ONLY_ERROR_TEXT = 'Editor\'s content can not be saved in read-only mode';
|
||||
const SAVE_FALLBACK_ERROR_TEXT = 'Editor\'s content can not be saved because collecting data failed';
|
||||
|
||||
type SaverSaveMock = ReturnType<typeof vi.fn<[], Promise<OutputData | undefined>>>;
|
||||
type SaverLastErrorMock = ReturnType<typeof vi.fn<[], unknown>>;
|
||||
|
||||
type EditorStub = {
|
||||
ReadOnly: { isEnabled: boolean };
|
||||
Saver: {
|
||||
save: SaverSaveMock;
|
||||
getLastSaveError?: SaverLastErrorMock;
|
||||
};
|
||||
};
|
||||
|
||||
type EditorStubOverrides = {
|
||||
ReadOnly?: Partial<EditorStub['ReadOnly']>;
|
||||
Saver?: Partial<EditorStub['Saver']>;
|
||||
};
|
||||
|
||||
const createSaverApi = (overrides: EditorStubOverrides = {}): { saverApi: SaverAPI; editor: EditorStub } => {
|
||||
const moduleConfig: ModuleConfig = {
|
||||
config: {} as EditorConfig,
|
||||
eventsDispatcher: new EventsDispatcher<EditorEventMap>(),
|
||||
};
|
||||
|
||||
const saverApi = new SaverAPI(moduleConfig);
|
||||
|
||||
const editor: EditorStub = {
|
||||
ReadOnly: {
|
||||
isEnabled: false,
|
||||
},
|
||||
Saver: {
|
||||
save: vi.fn<[], Promise<OutputData | undefined>>().mockResolvedValue({ blocks: [] }),
|
||||
getLastSaveError: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
if (overrides.ReadOnly) {
|
||||
Object.assign(editor.ReadOnly, overrides.ReadOnly);
|
||||
}
|
||||
|
||||
if (overrides.Saver) {
|
||||
Object.assign(editor.Saver, overrides.Saver);
|
||||
}
|
||||
|
||||
saverApi.state = editor as unknown as EditorModules;
|
||||
|
||||
return { saverApi,
|
||||
editor };
|
||||
};
|
||||
|
||||
describe('SaverAPI', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('exposes a save method that proxies to the class method', async () => {
|
||||
const { saverApi } = createSaverApi();
|
||||
const saveSpy = vi.spyOn(saverApi, 'save').mockResolvedValue({ blocks: [] });
|
||||
|
||||
await saverApi.methods.save();
|
||||
|
||||
expect(saveSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('throws and logs when editor is in read-only mode', async () => {
|
||||
const logSpy = vi.spyOn(utils, 'logLabeled').mockImplementation(() => undefined);
|
||||
const { saverApi, editor } = createSaverApi({
|
||||
ReadOnly: { isEnabled: true },
|
||||
});
|
||||
|
||||
await expect(saverApi.save()).rejects.toThrow(READ_ONLY_ERROR_TEXT);
|
||||
expect(editor.Saver.save).not.toHaveBeenCalled();
|
||||
expect(logSpy).toHaveBeenCalledWith(READ_ONLY_ERROR_TEXT, 'warn');
|
||||
});
|
||||
|
||||
it('returns saved data when saver succeeds', async () => {
|
||||
const output: OutputData = {
|
||||
blocks: [
|
||||
{
|
||||
id: 'paragraph-1',
|
||||
type: 'paragraph',
|
||||
data: { text: 'Hello' },
|
||||
},
|
||||
],
|
||||
};
|
||||
const { saverApi, editor } = createSaverApi();
|
||||
|
||||
editor.Saver.save.mockResolvedValueOnce(output);
|
||||
|
||||
await expect(saverApi.save()).resolves.toEqual(output);
|
||||
expect(editor.Saver.save).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rethrows the last saver error when it is an Error instance', async () => {
|
||||
const lastError = new Error('save crashed');
|
||||
const { saverApi, editor } = createSaverApi();
|
||||
|
||||
editor.Saver.save.mockResolvedValueOnce(undefined);
|
||||
editor.Saver.getLastSaveError = vi.fn().mockReturnValue(lastError);
|
||||
|
||||
await expect(saverApi.save()).rejects.toBe(lastError);
|
||||
});
|
||||
|
||||
it('converts non-error last error values to strings', async () => {
|
||||
const { saverApi, editor } = createSaverApi();
|
||||
|
||||
editor.Saver.save.mockResolvedValueOnce(undefined);
|
||||
editor.Saver.getLastSaveError = vi.fn().mockReturnValue(404);
|
||||
|
||||
await expect(saverApi.save()).rejects.toThrow('404');
|
||||
});
|
||||
|
||||
it('throws a fallback error when saver returns undefined without details', async () => {
|
||||
const { saverApi, editor } = createSaverApi();
|
||||
|
||||
editor.Saver.save.mockResolvedValueOnce(undefined);
|
||||
editor.Saver.getLastSaveError = undefined;
|
||||
|
||||
await expect(saverApi.save()).rejects.toThrow(SAVE_FALLBACK_ERROR_TEXT);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
277
test/unit/components/modules/saver.test.ts
Normal file
277
test/unit/components/modules/saver.test.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import Saver from '../../../../src/components/modules/saver';
|
||||
import type Block from '../../../../src/components/block';
|
||||
import type { EditorConfig, SanitizerConfig } from '../../../../types';
|
||||
import type { SavedData } from '../../../../types/data-formats';
|
||||
import * as sanitizer from '../../../../src/components/utils/sanitizer';
|
||||
import * as utils from '../../../../src/components/utils';
|
||||
|
||||
type BlockSaveResult = SavedData & { tunes?: Record<string, unknown> };
|
||||
|
||||
interface BlockMock {
|
||||
block: Block;
|
||||
savedData: BlockSaveResult;
|
||||
saveMock: Mock<[], Promise<BlockSaveResult>>;
|
||||
validateMock: Mock<[BlockSaveResult['data']], Promise<boolean>>;
|
||||
}
|
||||
|
||||
interface BlockMockOptions {
|
||||
id: string;
|
||||
tool: string;
|
||||
data: SavedData['data'];
|
||||
tunes?: Record<string, unknown>;
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
interface CreateSaverOptions {
|
||||
blocks?: Block[];
|
||||
sanitizer?: SanitizerConfig;
|
||||
stubTool?: string;
|
||||
toolSanitizeConfigs?: Record<string, SanitizerConfig>;
|
||||
}
|
||||
|
||||
const createBlockMock = (options: BlockMockOptions): BlockMock => {
|
||||
const savedData: BlockSaveResult = {
|
||||
id: options.id,
|
||||
tool: options.tool,
|
||||
data: options.data,
|
||||
time: 0,
|
||||
...(options.tunes ? { tunes: options.tunes } : {}),
|
||||
};
|
||||
|
||||
const saveMock = vi.fn<[], Promise<BlockSaveResult>>().mockResolvedValue(savedData);
|
||||
const validateMock = vi.fn<[BlockSaveResult['data']], Promise<boolean>>()
|
||||
.mockResolvedValue(options.isValid ?? true);
|
||||
|
||||
const block = {
|
||||
save: saveMock,
|
||||
validate: validateMock,
|
||||
} as unknown as Block;
|
||||
|
||||
return {
|
||||
block,
|
||||
savedData,
|
||||
saveMock,
|
||||
validateMock,
|
||||
};
|
||||
};
|
||||
|
||||
const createSaver = (options: CreateSaverOptions = {}): { saver: Saver } => {
|
||||
const config: EditorConfig = {
|
||||
sanitizer: options.sanitizer ?? ({} as SanitizerConfig),
|
||||
};
|
||||
|
||||
const eventsDispatcher = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
} as unknown as Saver['eventsDispatcher'];
|
||||
|
||||
const saver = new Saver({
|
||||
config,
|
||||
eventsDispatcher,
|
||||
});
|
||||
|
||||
const stubTool = options.stubTool ?? 'stub';
|
||||
const toolConfigs: Record<string, SanitizerConfig> = {
|
||||
...(options.toolSanitizeConfigs ?? {}),
|
||||
};
|
||||
|
||||
if (!toolConfigs[stubTool]) {
|
||||
toolConfigs[stubTool] = {} as SanitizerConfig;
|
||||
}
|
||||
|
||||
const blockTools = new Map<string, { sanitizeConfig?: SanitizerConfig }>(
|
||||
Object.entries(toolConfigs).map(([name, sanitizeConfig]) => [name, { sanitizeConfig } ])
|
||||
);
|
||||
|
||||
const editorState = {
|
||||
BlockManager: {
|
||||
blocks: options.blocks ?? [],
|
||||
},
|
||||
Tools: {
|
||||
blockTools,
|
||||
stubTool,
|
||||
},
|
||||
};
|
||||
|
||||
(saver as unknown as { state: Saver['Editor'] }).state = editorState as unknown as Saver['Editor'];
|
||||
|
||||
return { saver };
|
||||
};
|
||||
|
||||
describe('Saver module', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('saves valid blocks, sanitizes data, and preserves stub data', async () => {
|
||||
vi.useFakeTimers();
|
||||
const frozenDate = new Date('2024-05-01T10:00:00Z');
|
||||
|
||||
vi.setSystemTime(frozenDate);
|
||||
|
||||
const version = 'test-version';
|
||||
|
||||
vi.spyOn(utils, 'getEditorVersion').mockReturnValue(version);
|
||||
|
||||
const paragraphBlock = createBlockMock({
|
||||
id: 'block-1',
|
||||
tool: 'paragraph',
|
||||
data: { text: '<b>Hello</b>' },
|
||||
tunes: {
|
||||
alignment: 'left',
|
||||
},
|
||||
});
|
||||
|
||||
const preservedData = {
|
||||
type: 'legacy-tool',
|
||||
data: { text: 'Keep me intact' },
|
||||
};
|
||||
|
||||
const stubToolName = 'stub-tool';
|
||||
|
||||
const stubBlock = createBlockMock({
|
||||
id: 'block-2',
|
||||
tool: stubToolName,
|
||||
data: preservedData,
|
||||
});
|
||||
|
||||
const sanitizeBlocksSpy = vi.spyOn(sanitizer, 'sanitizeBlocks').mockImplementation((blocks, getConfig, globalSanitizer) => {
|
||||
expect(typeof getConfig).toBe('function');
|
||||
|
||||
if (typeof getConfig !== 'function') {
|
||||
throw new Error('Expected sanitize config resolver to be a function');
|
||||
}
|
||||
|
||||
expect(getConfig('paragraph')).toEqual({ paragraph: { b: true } });
|
||||
expect(globalSanitizer).toEqual({ common: true });
|
||||
|
||||
return blocks.map((block) => {
|
||||
if (block.tool === 'paragraph') {
|
||||
return {
|
||||
...block,
|
||||
data: { text: 'Hello' },
|
||||
};
|
||||
}
|
||||
|
||||
return block;
|
||||
});
|
||||
});
|
||||
|
||||
const { saver } = createSaver({
|
||||
blocks: [paragraphBlock.block, stubBlock.block],
|
||||
sanitizer: { common: true },
|
||||
stubTool: stubToolName,
|
||||
toolSanitizeConfigs: {
|
||||
paragraph: { paragraph: { b: true } },
|
||||
[stubToolName]: {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await saver.save();
|
||||
|
||||
expect(paragraphBlock.saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(paragraphBlock.validateMock).toHaveBeenCalledWith(paragraphBlock.savedData.data);
|
||||
expect(stubBlock.saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(sanitizeBlocksSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [ blocksBeforeSanitize ] = sanitizeBlocksSpy.mock.calls[0];
|
||||
|
||||
expect(blocksBeforeSanitize).toEqual([
|
||||
expect.objectContaining({
|
||||
id: paragraphBlock.savedData.id,
|
||||
tool: 'paragraph',
|
||||
data: paragraphBlock.savedData.data,
|
||||
tunes: paragraphBlock.savedData.tunes,
|
||||
isValid: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: stubBlock.savedData.id,
|
||||
tool: 'stub-tool',
|
||||
data: preservedData,
|
||||
isValid: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result).toEqual({
|
||||
time: frozenDate.valueOf(),
|
||||
version,
|
||||
blocks: [
|
||||
{
|
||||
id: paragraphBlock.savedData.id,
|
||||
type: 'paragraph',
|
||||
data: { text: 'Hello' },
|
||||
tunes: paragraphBlock.savedData.tunes,
|
||||
},
|
||||
preservedData,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('skips invalid blocks and logs the reason', async () => {
|
||||
const logSpy = vi.spyOn(utils, 'log').mockImplementation(() => undefined);
|
||||
|
||||
const invalidBlock = createBlockMock({
|
||||
id: 'invalid',
|
||||
tool: 'quote',
|
||||
data: { text: 'Invalid' },
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
const validBlock = createBlockMock({
|
||||
id: 'valid',
|
||||
tool: 'paragraph',
|
||||
data: { text: 'Valid' },
|
||||
});
|
||||
|
||||
const sanitizeBlocksSpy = vi.spyOn(sanitizer, 'sanitizeBlocks').mockImplementation((blocks) => blocks);
|
||||
|
||||
const { saver } = createSaver({
|
||||
blocks: [invalidBlock.block, validBlock.block],
|
||||
toolSanitizeConfigs: {
|
||||
quote: {},
|
||||
paragraph: {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await saver.save();
|
||||
|
||||
expect(result?.blocks).toEqual([
|
||||
{
|
||||
id: 'valid',
|
||||
type: 'paragraph',
|
||||
data: { text: 'Valid' },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(sanitizeBlocksSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledWith('Block «quote» skipped because saved data is invalid');
|
||||
});
|
||||
|
||||
it('logs a labeled error when saving fails', async () => {
|
||||
const error = new Error('save failed');
|
||||
const logLabeledSpy = vi.spyOn(utils, 'logLabeled').mockImplementation(() => undefined);
|
||||
const sanitizeBlocksSpy = vi.spyOn(sanitizer, 'sanitizeBlocks');
|
||||
|
||||
const failingBlock = {
|
||||
save: vi.fn().mockRejectedValue(error),
|
||||
validate: vi.fn(),
|
||||
} as unknown as Block;
|
||||
|
||||
const { saver } = createSaver({
|
||||
blocks: [ failingBlock ],
|
||||
toolSanitizeConfigs: {
|
||||
paragraph: {},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(saver.save()).resolves.toBeUndefined();
|
||||
expect(logLabeledSpy).toHaveBeenCalledWith('Saving failed due to the Error %o', 'error', error);
|
||||
expect(sanitizeBlocksSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
273
test/unit/tools/base.test.ts
Normal file
273
test/unit/tools/base.test.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Tool, ToolConstructable } from '@/types/tools';
|
||||
import type { API } from '@/types';
|
||||
import type { ToolOptions } from '../../../src/components/tools/base';
|
||||
import BaseToolAdapter, { CommonInternalSettings, UserSettings } from '../../../src/components/tools/base';
|
||||
import { ToolType } from '@/types/tools/adapters/tool-type';
|
||||
|
||||
interface AdapterOptions {
|
||||
name: string;
|
||||
constructable: ToolConstructable;
|
||||
config: ToolOptions;
|
||||
api: API;
|
||||
isDefault: boolean;
|
||||
isInternal: boolean;
|
||||
defaultPlaceholder?: string | false;
|
||||
}
|
||||
|
||||
interface TestToolAdapterOptions extends AdapterOptions {
|
||||
toolType?: ToolType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-friendly adapter that lets specs interact with the base implementation.
|
||||
*/
|
||||
class TestToolAdapter extends BaseToolAdapter<ToolType, Tool> {
|
||||
public type: ToolType;
|
||||
|
||||
/**
|
||||
* Creates a tool adapter instance preloaded with the supplied test options.
|
||||
*
|
||||
* @param options configuration bundle used to build the adapter for a spec
|
||||
*/
|
||||
constructor(options: TestToolAdapterOptions) {
|
||||
super(options);
|
||||
this.type = options.toolType ?? ToolType.Block;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public create(): Tool {
|
||||
return {} as Tool;
|
||||
}
|
||||
}
|
||||
|
||||
const createConstructable = (
|
||||
overrides: Record<string, unknown> = {}
|
||||
): ToolConstructable & Record<string, unknown> => {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class Constructable {}
|
||||
|
||||
Object.assign(Constructable, overrides);
|
||||
|
||||
return Constructable as unknown as ToolConstructable & Record<string, unknown>;
|
||||
};
|
||||
|
||||
const createToolOptions = (overrides: Partial<ToolOptions> = {}): ToolOptions => {
|
||||
return {
|
||||
[UserSettings.Config]: {
|
||||
option1: 'value1',
|
||||
},
|
||||
[UserSettings.Shortcut]: 'CTRL+SHIFT+B',
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const createTool = (
|
||||
overrides: Partial<TestToolAdapterOptions> = {}
|
||||
): {
|
||||
tool: TestToolAdapter;
|
||||
options: TestToolAdapterOptions;
|
||||
} => {
|
||||
const options: TestToolAdapterOptions = {
|
||||
name: overrides.name ?? 'baseTool',
|
||||
constructable: overrides.constructable ?? createConstructable(),
|
||||
config: overrides.config ?? createToolOptions(),
|
||||
api: overrides.api ?? {} as API,
|
||||
isDefault: overrides.isDefault ?? false,
|
||||
isInternal: overrides.isInternal ?? false,
|
||||
defaultPlaceholder: overrides.defaultPlaceholder ?? 'Default placeholder',
|
||||
toolType: overrides.toolType ?? ToolType.Block,
|
||||
};
|
||||
|
||||
return {
|
||||
tool: new TestToolAdapter(options),
|
||||
options,
|
||||
};
|
||||
};
|
||||
|
||||
describe('BaseToolAdapter', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('settings', () => {
|
||||
it('returns user configuration as-is', () => {
|
||||
const customConfig: ToolOptions = {
|
||||
[UserSettings.Config]: {
|
||||
foo: 'bar',
|
||||
},
|
||||
[UserSettings.Shortcut]: 'CMD+ALT+1',
|
||||
};
|
||||
const { tool } = createTool({
|
||||
config: customConfig,
|
||||
});
|
||||
|
||||
expect(tool.settings).toStrictEqual(customConfig[UserSettings.Config]);
|
||||
});
|
||||
|
||||
it('adds default placeholder when default tool lacks placeholder', () => {
|
||||
const { tool } = createTool({
|
||||
isDefault: true,
|
||||
config: {
|
||||
[UserSettings.Shortcut]: 'CMD+ALT+2',
|
||||
} as ToolOptions,
|
||||
});
|
||||
|
||||
expect(tool.settings).toStrictEqual({
|
||||
placeholder: 'Default placeholder',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not override user-defined placeholder', () => {
|
||||
const placeholder = 'Custom placeholder';
|
||||
const { tool } = createTool({
|
||||
isDefault: true,
|
||||
config: {
|
||||
[UserSettings.Config]: {
|
||||
placeholder,
|
||||
},
|
||||
[UserSettings.Shortcut]: 'CMD+ALT+3',
|
||||
} as ToolOptions,
|
||||
});
|
||||
|
||||
expect(tool.settings).toStrictEqual({
|
||||
placeholder,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle hooks', () => {
|
||||
it('delegates prepare to constructable with tool context', async () => {
|
||||
const prepare = vi.fn();
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable({
|
||||
prepare,
|
||||
}),
|
||||
});
|
||||
|
||||
await tool.prepare();
|
||||
|
||||
expect(prepare).toHaveBeenCalledWith({
|
||||
toolName: 'baseTool',
|
||||
config: tool.settings,
|
||||
});
|
||||
});
|
||||
|
||||
it('skips prepare when constructable method is absent', async () => {
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable(),
|
||||
});
|
||||
|
||||
const result = await tool.prepare();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('delegates reset to constructable when available', async () => {
|
||||
const reset = vi.fn();
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable({
|
||||
reset,
|
||||
}),
|
||||
});
|
||||
|
||||
await tool.reset();
|
||||
|
||||
expect(reset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips reset when constructable method is absent', async () => {
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable(),
|
||||
});
|
||||
|
||||
const result = await tool.reset();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortcut', () => {
|
||||
it('prefers user-defined shortcut value', () => {
|
||||
const config: ToolOptions = {
|
||||
[UserSettings.Config]: {},
|
||||
[UserSettings.Shortcut]: 'CMD+ALT+4',
|
||||
};
|
||||
const { tool } = createTool({
|
||||
config,
|
||||
});
|
||||
|
||||
expect(tool.shortcut).toBe(config[UserSettings.Shortcut]);
|
||||
});
|
||||
|
||||
it('falls back to constructable shortcut', () => {
|
||||
const constructableShortcut = 'CTRL+K';
|
||||
const { tool } = createTool({
|
||||
config: {
|
||||
[UserSettings.Config]: {},
|
||||
} as ToolOptions,
|
||||
constructable: createConstructable({
|
||||
[CommonInternalSettings.Shortcut]: constructableShortcut,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(tool.shortcut).toBe(constructableShortcut);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeConfig', () => {
|
||||
it('returns constructable sanitize configuration when provided', () => {
|
||||
const sanitizeConfig = {
|
||||
paragraph: {
|
||||
b: true,
|
||||
},
|
||||
};
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable({
|
||||
[CommonInternalSettings.SanitizeConfig]: sanitizeConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(tool.sanitizeConfig).toBe(sanitizeConfig);
|
||||
});
|
||||
|
||||
it('returns empty object when sanitize configuration is missing', () => {
|
||||
const { tool } = createTool({
|
||||
constructable: createConstructable(),
|
||||
});
|
||||
|
||||
expect(tool.sanitizeConfig).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('type guards', () => {
|
||||
it('identifies tool type correctly', () => {
|
||||
const blockTool = createTool({
|
||||
toolType: ToolType.Block,
|
||||
}).tool;
|
||||
const inlineTool = createTool({
|
||||
toolType: ToolType.Inline,
|
||||
}).tool;
|
||||
const tuneTool = createTool({
|
||||
toolType: ToolType.Tune,
|
||||
}).tool;
|
||||
|
||||
expect(blockTool.isBlock()).toBe(true);
|
||||
expect(blockTool.isInline()).toBe(false);
|
||||
expect(blockTool.isTune()).toBe(false);
|
||||
|
||||
expect(inlineTool.isInline()).toBe(true);
|
||||
expect(inlineTool.isBlock()).toBe(false);
|
||||
expect(inlineTool.isTune()).toBe(false);
|
||||
|
||||
expect(tuneTool.isTune()).toBe(true);
|
||||
expect(tuneTool.isBlock()).toBe(false);
|
||||
expect(tuneTool.isInline()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -23,7 +23,9 @@
|
|||
"@editorjs/editorjs/*": [ "./types/*" ],
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"allowArbitraryExtensions": true
|
||||
"skipLibCheck": true,
|
||||
"allowArbitraryExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
|
@ -31,5 +33,9 @@
|
|||
"**/*.js",
|
||||
"**/*.json",
|
||||
"vite.config*.mjs"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
23
types/block-tunes/block-tune.d.ts
vendored
23
types/block-tunes/block-tune.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import {API, BlockAPI, SanitizerConfig, ToolConfig} from '../index';
|
||||
import {API, BlockAPI, ToolConfig} from '../index';
|
||||
import { BlockTuneData } from './block-tune-data';
|
||||
import { MenuConfig } from '../tools';
|
||||
import { BaseToolConstructable, MenuConfig } from '../tools';
|
||||
|
||||
/**
|
||||
* Describes BLockTune blueprint
|
||||
|
|
@ -33,23 +33,13 @@ export interface BlockTune {
|
|||
/**
|
||||
* Describes BlockTune class constructor function
|
||||
*/
|
||||
export interface BlockTuneConstructable {
|
||||
export interface BlockTuneConstructable extends BaseToolConstructable {
|
||||
|
||||
/**
|
||||
* Flag show Tool is Block Tune
|
||||
*/
|
||||
isTune: boolean;
|
||||
|
||||
/**
|
||||
* Tune's sanitize configuration
|
||||
*/
|
||||
sanitize?: SanitizerConfig;
|
||||
|
||||
/**
|
||||
* Shortcut for Tool
|
||||
*/
|
||||
shortcut?: string;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
|
|
@ -66,10 +56,5 @@ export interface BlockTuneConstructable {
|
|||
* Tune`s prepare method. Can be async
|
||||
* @param data
|
||||
*/
|
||||
prepare?(): Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Tune`s reset method to clean up anything set by prepare. Can be async
|
||||
*/
|
||||
reset?(): void | Promise<void>;
|
||||
prepare?(data: { toolName: string; config: ToolConfig }): Promise<void> | void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue