fix: lint issues in tests

This commit is contained in:
JackUait 2025-11-07 02:23:21 +03:00
commit 4edf334b41
41 changed files with 373 additions and 145 deletions

View file

@ -22,7 +22,6 @@ export class SimpleHeader implements BaseTool {
/**
* Return Tool's view
*
* @returns {HTMLHeadingElement}
* @public
*/
@ -44,7 +43,6 @@ export class SimpleHeader implements BaseTool {
/**
* Extract Tool's data from the view
*
* @param toolsContent - Text tools rendered view
*/
public save(toolsContent: HTMLHeadingElement): BlockToolData {

View file

@ -32,7 +32,7 @@ export default class ToolMock implements BlockTool {
public render(): HTMLElement {
const contenteditable = document.createElement('div');
if (this.data && this.data.text) {
if (this.data.text) {
contenteditable.innerHTML = this.data.text;
}

28
test/cypress/simple-image.d.ts vendored Normal file
View file

@ -0,0 +1,28 @@
/**
* Declaration for external JS module @editorjs/simple-image
*/
declare module '@editorjs/simple-image' {
interface SimpleImageConfig {
data?: Record<string, unknown>;
readOnly?: boolean;
api?: unknown;
block?: unknown;
}
interface SimpleImageInstance {
render(): HTMLElement;
save(block: HTMLElement): Record<string, unknown>;
}
interface SimpleImageTool {
toolbox?: unknown;
pasteConfig?: unknown;
conversionConfig?: unknown;
isReadOnlySupported?: boolean;
new (config: SimpleImageConfig): SimpleImageInstance;
}
const Image: SimpleImageTool;
export default Image;
}

8
test/cypress/support/chai-subset.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
/**
* Type declaration for chai-subset module
*/
declare module 'chai-subset' {
const chaiSubset: (chai: any, utils: any) => void;
export default chaiSubset;
}

View file

@ -13,7 +13,6 @@ import Chainable = Cypress.Chainable;
/**
* Create a wrapper and initialize the new instance of editor.js
* Then return the instance
*
* @param editorConfig - config to pass to the editor
* @returns EditorJS - created instance
*/
@ -43,7 +42,6 @@ Cypress.Commands.add('createEditor', (editorConfig: EditorConfig = {}): Chainabl
*
* Usage
* cy.get('div').paste({'text/plain': 'Text', 'text/html': '<b>Text</b>'})
*
* @param data - map with MIME type as a key and data as value
*/
Cypress.Commands.add('paste', {
@ -54,7 +52,7 @@ Cypress.Commands.add('paste', {
cancelable: true,
}), {
clipboardData: {
getData: (type): string => data[type],
getData: (type: string): string => data[type],
types: Object.keys(data),
},
});
@ -70,7 +68,9 @@ Cypress.Commands.add('paste', {
* Usage:
* cy.get('div').copy().then(data => {})
*/
Cypress.Commands.add('copy', { prevSubject: true }, (subject) => {
Cypress.Commands.add('copy', {
prevSubject: ['element'],
}, (subject) => {
const clipboardData: {[type: string]: any} = {};
const copyEvent = Object.assign(new Event('copy', {
@ -86,7 +86,7 @@ Cypress.Commands.add('copy', { prevSubject: true }, (subject) => {
subject[0].dispatchEvent(copyEvent);
return cy.wrap(clipboardData);
return cy.wrap<Record<string, string>>(clipboardData);
});
/**
@ -95,7 +95,7 @@ Cypress.Commands.add('copy', { prevSubject: true }, (subject) => {
* Usage:
* cy.get('div').cut().then(data => {})
*/
Cypress.Commands.add('cut', { prevSubject: true }, (subject) => {
Cypress.Commands.add('cut', { prevSubject: ['element'] }, (subject) => {
const clipboardData: {[type: string]: any} = {};
const copyEvent = Object.assign(new Event('cut', {
@ -111,12 +111,11 @@ Cypress.Commands.add('cut', { prevSubject: true }, (subject) => {
subject[0].dispatchEvent(copyEvent);
return cy.wrap(clipboardData);
return cy.wrap<Record<string, string>>(clipboardData);
});
/**
* Calls EditorJS API render method
*
* @param data data to render
*/
Cypress.Commands.add('render', { prevSubject: true }, (subject: EditorJS, data: OutputData) => {
@ -133,9 +132,8 @@ Cypress.Commands.add('render', { prevSubject: true }, (subject: EditorJS, data:
*
* Usage
* cy.get('[data-cy=editorjs]')
* .find('.ce-paragraph')
* .selectText('block te')
*
* .find('.ce-paragraph')
* .selectText('block te')
* @param text - text to select
*/
Cypress.Commands.add('selectText', {
@ -162,9 +160,8 @@ Cypress.Commands.add('selectText', {
*
* Usage
* cy.get('[data-cy=editorjs]')
* .find('.ce-paragraph')
* .selectTextByOffset([0, 5])
*
* .find('.ce-paragraph')
* .selectTextByOffset([0, 5])
* @param offset - offset to select
*/
Cypress.Commands.add('selectTextByOffset', {
@ -190,9 +187,8 @@ Cypress.Commands.add('selectTextByOffset', {
*
* Usage
* cy.get('[data-cy=editorjs]')
* .find('.ce-paragraph')
* .getLineWrapPositions()
*
* .find('.ce-paragraph')
* .getLineWrapPositions()
* @returns number[] - array of line wrap positions
*/
Cypress.Commands.add('getLineWrapPositions', {
@ -249,7 +245,6 @@ Cypress.Commands.add('keydown', {
* so real-world and Cypress behaviour were different.
*
* To make it work we need to trigger Cypress event with "eventConstructor: 'KeyboardEvent'",
*
* @see https://github.com/cypress-io/cypress/issues/5650
* @see https://github.com/cypress-io/cypress/pull/8305/files
*/
@ -264,15 +259,17 @@ Cypress.Commands.add('keydown', {
/**
* Extract content of pseudo element
*
* @example cy.get('element').getPseudoElementContent('::before').should('eq', 'my-test-string')
*/
Cypress.Commands.add('getPseudoElementContent', {
prevSubject: true,
}, (subject, pseudoElement: 'string') => {
prevSubject: ['element'],
}, (subject, pseudoElement: string) => {
const win = subject[0].ownerDocument.defaultView;
if (!win) {
throw new Error('defaultView is null');
}
const computedStyle = win.getComputedStyle(subject[0], pseudoElement);
const content = computedStyle.getPropertyValue('content');
return content.replace(/['"]/g, ''); // Remove quotes around the content
return cy.wrap(content.replace(/['"]/g, '')); // Remove quotes around the content
});

View file

@ -6,20 +6,20 @@ import '@cypress/code-coverage/support';
// available to them because the supportFile is bundled and served
// prior to any spec files loading
declare const chai: Chai.ChaiStatic;
import type PartialBlockMutationEvent from '../fixtures/types/PartialBlockMutationEvent';
/**
* Chai plugin for checking if passed onChange method is called with an array of passed events
*
* @param _chai - Chai instance
*/
const beCalledWithBatchedEvents = (_chai): void => {
const beCalledWithBatchedEvents = (_chai: typeof chai): void => {
/**
* Check if passed onChange method is called with an array of passed events
*
* @param expectedEvents - batched events to check
*/
function assertToBeCalledWithBatchedEvents(expectedEvents: PartialBlockMutationEvent[]): void {
function assertToBeCalledWithBatchedEvents(this: Chai.AssertionStatic, expectedEvents: PartialBlockMutationEvent[]): void {
/**
* EditorJS API is passed as the first parameter of the onChange callback
*/
@ -29,7 +29,8 @@ const beCalledWithBatchedEvents = (_chai): void => {
this.assert(
$onChange.calledOnce,
'expected #{this} to be called once',
'expected #{this} to not be called once'
'expected #{this} to not be called once',
true
);
this.assert(

View file

@ -31,7 +31,7 @@ declare global {
* @usage
* cy.get('div').copy().then(data => {})
*/
copy(): Chainable<Subject>;
copy(): Chainable<Record<string, string>>;
/**
* Cut command to dispatch cut event on subject
@ -39,7 +39,7 @@ declare global {
* @usage
* cy.get('div').cut().then(data => {})
*/
cut(): Chainable<Subject>;
cut(): Chainable<Record<string, string>>;
/**
* Calls EditorJS API render method

View file

@ -5,7 +5,6 @@ import type EditorJS from '../../../../types/index';
/**
* Creates Editor instance with list of Paragraph blocks of passed texts
*
* @param textBlocks - list of texts for Paragraph blocks
* @param editorConfig - config to pass to the editor
*/

View file

@ -2,7 +2,6 @@ import { nanoid } from 'nanoid';
/**
* Creates a paragraph mock
*
* @param text - text for the paragraph
* @returns paragraph mock
*/

View file

@ -9,10 +9,17 @@ export const NESTED_EDITOR_ID = 'nested-editor';
export default class NestedEditor implements BlockTool {
private data: { text: string };
/**
*
* @param value - The constructor options for the block tool
*/
constructor(value: BlockToolConstructorOptions) {
this.data = value.data;
}
/**
*
*/
public render(): HTMLDivElement {
const editorEl = Object.assign(document.createElement('div'), {
id: NESTED_EDITOR_ID,
@ -25,6 +32,9 @@ export default class NestedEditor implements BlockTool {
return editorEl;
}
/**
*
*/
public save(): string {
return this.data.text;
}

View file

@ -1,4 +1,5 @@
import type EditorJS from '../../../../types';
import type { API, BlockMutationEvent, OutputData } from '../../../../types';
import { BlockChangedMutationType } from '../../../../types/events/block/BlockChanged';
/**
@ -25,12 +26,11 @@ describe('BlockAPI', () => {
/**
* Creates Editor instance
*
* @param [data] - data to render
*/
function createEditor(data = undefined): void {
function createEditor(data?: OutputData): void {
const config = {
onChange: (api, event): void => {
onChange: (_api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
data,
@ -55,6 +55,10 @@ describe('BlockAPI', () => {
.then(async (editor) => {
const block = editor.blocks.getById(firstBlock.id);
if (!block) {
throw new Error(`Block with id ${firstBlock.id} not found`);
}
block.dispatchChange();
cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({

View file

@ -1,5 +1,6 @@
import type EditorJS from '../../../../types/index';
import type { ConversionConfig, ToolboxConfig, ToolConfig } from '../../../../types';
import type { ConversionConfig, ToolboxConfig, ToolConfig, API, BlockAPI } from '../../../../types';
import type { BlockTuneData } from '../../../../types/block-tunes/block-tune-data';
import ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock';
import { nanoid } from 'nanoid';
@ -36,7 +37,7 @@ describe('api.blocks', () => {
const block = editor.blocks.getById(firstBlock.id);
expect(block).not.to.be.undefined;
expect(block.id).to.be.eq(firstBlock.id);
expect(block?.id).to.be.eq(firstBlock.id);
});
});
@ -108,18 +109,17 @@ describe('api.blocks', () => {
* Example Tune Class
*/
class ExampleTune {
protected data: object;
protected data: BlockTuneData | null | undefined;
/**
*
* @param data
* @param config - Block Tune config
*/
constructor({ data }) {
constructor({ data }: { api: API; config?: ToolConfig; block: BlockAPI; data: BlockTuneData }) {
this.data = data;
}
/**
* Tell editor.js that this Tool is a Block Tune
*
* @returns {boolean}
*/
public static get isTune(): boolean {
@ -128,10 +128,9 @@ describe('api.blocks', () => {
/**
* Create Tunes controls wrapper that will be appended to the Block Tunes panel
*
* @returns {Element}
* @returns {HTMLElement}
*/
public render(): Element {
public render(): HTMLElement {
return document.createElement('div');
}
@ -144,11 +143,10 @@ describe('api.blocks', () => {
/**
* Returns Tune state
*
* @returns {string}
* @returns {BlockTuneData}
*/
public save(): object | string {
return this.data || '';
public save(): BlockTuneData {
return this.data ?? '';
}
}
@ -178,7 +176,12 @@ describe('api.blocks', () => {
// Check if it is updated
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
await editor.blocks.update(editor.blocks.getBlockByIndex(0).id, null, {
const block = editor.blocks.getBlockByIndex(0);
if (!block) {
throw new Error('Block at index 0 not found');
}
await editor.blocks.update(block.id, undefined, {
exampleTune: 'test',
});
const data = await editor.save();
@ -481,6 +484,8 @@ describe('api.blocks', () => {
export: (data) => data,
/**
* Passed config should be returned
* @param _content - The content string (unused in this implementation)
* @param config - The tool configuration object
*/
import: (_content, config) => {
return { text: JSON.stringify(config) };

View file

@ -40,6 +40,10 @@ describe('Caret API', () => {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection) {
throw new Error('Selection not found');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -65,6 +69,10 @@ describe('Caret API', () => {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection) {
throw new Error('Selection not found');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -83,6 +91,10 @@ describe('Caret API', () => {
cy.get<EditorJS>('@editorInstance')
.then(async (editor) => {
const block = editor.blocks.getById(paragraphDataMock.id);
if (!block) {
throw new Error('Block not found');
}
const returnedValue = editor.caret.setToBlock(block);
/**
@ -91,6 +103,10 @@ describe('Caret API', () => {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection) {
throw new Error('Selection not found');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')

View file

@ -28,7 +28,7 @@ describe('api.toolbar', () => {
});
afterEach(function () {
if (this.editorInstance) {
if (this.editorInstance != null) {
this.editorInstance.destroy();
}
});

View file

@ -90,12 +90,12 @@ describe('Editor Tools Api', () => {
cy.get('[data-cy=editorjs]')
.get('.ce-popover-item[data-item-name=testTool]')
.first()
.should('contain.text', TestTool.toolbox[0].title);
.should('contain.text', (TestTool.toolbox as ToolboxConfigEntry[])[0].title);
cy.get('[data-cy=editorjs]')
.get('.ce-popover-item[data-item-name=testTool]')
.last()
.should('contain.text', TestTool.toolbox[1].title);
.should('contain.text', (TestTool.toolbox as ToolboxConfigEntry[])[1].title);
});
it('should insert block with overridden data on entry click in case toolbox entry provides data overrides', () => {
@ -114,10 +114,9 @@ describe('Editor Tools Api', () => {
/**
* Tool constructor
*
* @param data - previously saved data
*/
constructor({ data }) {
constructor({ data }: { data: { testProp: string } }) {
this._data = data;
}
@ -147,7 +146,6 @@ describe('Editor Tools Api', () => {
/**
* Extracts Tool's data from the view
*
* @param el - tool view
*/
public save(el: HTMLElement): BlockToolData {

View file

@ -4,7 +4,7 @@ import type { ToolboxConfig } from '../../../types';
describe('Editor i18n', () => {
context('Toolbox', () => {
it('should translate tool title in a toolbox', function () {
if (this && this.editorInstance) {
if (this != null && this.editorInstance != null) {
this.editorInstance.destroy();
}
const toolNamesDictionary = {
@ -36,7 +36,7 @@ describe('Editor i18n', () => {
});
it('should translate titles of toolbox entries', function () {
if (this && this.editorInstance) {
if (this != null && this.editorInstance != null) {
this.editorInstance.destroy();
}
const toolNamesDictionary = {
@ -96,7 +96,7 @@ describe('Editor i18n', () => {
});
it('should use capitalized tool name as translation key if toolbox title is missing', function () {
if (this && this.editorInstance) {
if (this != null && this.editorInstance != null) {
this.editorInstance.destroy();
}

View file

@ -13,7 +13,7 @@ describe('Editor basic initialization', () => {
});
afterEach(function () {
if (this.editorInstance) {
if (this.editorInstance != null) {
this.editorInstance.destroy();
}
});
@ -30,8 +30,8 @@ describe('Editor basic initialization', () => {
describe('Configuration', () => {
describe('readOnly', () => {
beforeEach(() => {
if (this && this.editorInstance) {
beforeEach(function () {
if (this.editorInstance != null) {
this.editorInstance.destroy();
}
});

View file

@ -23,6 +23,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -52,6 +55,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -82,6 +88,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -112,6 +121,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -143,6 +155,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -174,6 +189,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -266,6 +284,9 @@ describe('Arrow Left', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')

View file

@ -25,6 +25,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -57,6 +60,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -89,6 +95,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -121,6 +130,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -154,6 +166,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -187,6 +202,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -279,6 +297,9 @@ describe('Arrow Right', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')

View file

@ -239,6 +239,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@firstInput').should(($div) => {
@ -330,13 +333,16 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')
.should(($block) => {
expect($block[0].contains(range.startContainer)).to.be.true;
expect(range.startOffset).to.be.eq($block[0].textContent.length);
expect(range.startOffset).to.be.eq($block[0].textContent?.length ?? 0);
});
});
@ -395,6 +401,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -461,6 +470,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -526,6 +538,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -607,6 +622,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@firstBlock').should(($div) => {
@ -690,6 +708,9 @@ describe('Backspace keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@firstBlock').should(($div) => {
@ -700,7 +721,7 @@ describe('Backspace keydown', function () {
describe('at the start of the first Block', function () {
it('should do nothing if Block is not empty', function () {
createEditorWithTextBlocks(['The only block. Not empty']);
createEditorWithTextBlocks([ 'The only block. Not empty' ]);
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')

View file

@ -124,7 +124,6 @@ describe('Delete keydown', function () {
* - Firefox merge blocks and with whitespace - "1 2"
*
* So, we have to check both variants.
*
* @todo remove this check after fixing the Firefox merge behaviour
*/
.should(($block) => {
@ -232,6 +231,9 @@ describe('Delete keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@secondInput').should(($div) => {
@ -323,6 +325,9 @@ describe('Delete keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -387,6 +392,9 @@ describe('Delete keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('[data-cy=editorjs]')
@ -466,6 +474,9 @@ describe('Delete keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@secondBlock').should(($div) => {

View file

@ -63,6 +63,9 @@ describe('Enter keydown', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('Selection is null or has no ranges');
}
const range = selection.getRangeAt(0);
cy.get('@lastBlock').should(($block) => {

View file

@ -81,7 +81,13 @@ describe('Tab keydown', function () {
.last()
.then(($secondBlock) => {
const editorWindow = $secondBlock.get(0).ownerDocument.defaultView;
if (!editorWindow) {
throw new Error('Window is not available');
}
const selection = editorWindow.getSelection();
if (!selection) {
throw new Error('Selection is not available');
}
const range = selection.getRangeAt(0);
@ -127,7 +133,13 @@ describe('Tab keydown', function () {
.last()
.then(($secondInput) => {
const editorWindow = $secondInput.get(0).ownerDocument.defaultView;
if (!editorWindow) {
throw new Error('Window is not available');
}
const selection = editorWindow.getSelection();
if (!selection) {
throw new Error('Selection is not available');
}
const range = selection.getRangeAt(0);
@ -240,7 +252,13 @@ describe('Shift+Tab keydown', function () {
.first()
.then(($firstBlock) => {
const editorWindow = $firstBlock.get(0).ownerDocument.defaultView;
if (!editorWindow) {
throw new Error('Window is not available');
}
const selection = editorWindow.getSelection();
if (!selection) {
throw new Error('Selection is not available');
}
const range = selection.getRangeAt(0);
@ -289,7 +307,13 @@ describe('Shift+Tab keydown', function () {
.first()
.then(($firstInput) => {
const editorWindow = $firstInput.get(0).ownerDocument.defaultView;
if (!editorWindow) {
throw new Error('Window is not available');
}
const selection = editorWindow.getSelection();
if (!selection) {
throw new Error('Selection is not available');
}
const range = selection.getRangeAt(0);

View file

@ -1,6 +1,6 @@
import Header from '@editorjs/header';
import NestedEditor, { NESTED_EDITOR_ID } from '../../support/utils/nestedEditorInstance';
import type { MenuConfig } from '@/types/tools';
import type { MenuConfig, ToolConstructable } from '@/types/tools';
describe('Inline Toolbar', () => {
it('should appear aligned with left coord of selection rect', () => {
@ -25,12 +25,21 @@ describe('Inline Toolbar', () => {
.should('be.visible')
.then(($toolbar) => {
const editorWindow = $toolbar.get(0).ownerDocument.defaultView;
if (!editorWindow) {
throw new Error('Unable to access window from toolbar element');
}
const selection = editorWindow.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('No selection available');
}
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
expect($toolbar.offset().left).to.be.closeTo(rect.left, 1);
expect($toolbar.offset()?.left).to.be.closeTo(rect.left, 1);
});
});
@ -70,10 +79,17 @@ describe('Inline Toolbar', () => {
.then(($blockWrapper) => {
const blockWrapperRect = $blockWrapper.get(0).getBoundingClientRect();
const toolbarOffset = $toolbar.offset();
const toolbarWidth = $toolbar.width();
if (!toolbarOffset || toolbarWidth === undefined) {
throw new Error('Unable to get toolbar offset or width');
}
/**
* Toolbar should be aligned with right side of text column
*/
expect($toolbar.offset().left + $toolbar.width()).to.closeTo(blockWrapperRect.right, 10);
expect(toolbarOffset.left + toolbarWidth).to.closeTo(blockWrapperRect.right, 10);
});
});
});
@ -82,7 +98,7 @@ describe('Inline Toolbar', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as ToolConstructable,
inlineToolbar: ['bold', 'testTool'],
},
testTool: {
@ -90,6 +106,10 @@ describe('Inline Toolbar', () => {
public static isInline = true;
public static isReadOnlySupported = true;
// eslint-disable-next-line jsdoc/require-jsdoc
public constructor() {
// Constructor required for InlineToolConstructable
}
// eslint-disable-next-line jsdoc/require-jsdoc
public render(): MenuConfig {
return {
title: 'Test Tool',
@ -98,7 +118,7 @@ describe('Inline Toolbar', () => {
onActivate: () => {},
};
}
},
} as unknown as ToolConstructable,
},
},
readOnly: true,
@ -154,7 +174,13 @@ describe('Inline Toolbar', () => {
doc.body.appendChild(form);
/* Move editor to form */
form.appendChild(doc.getElementById('editorjs'));
const editorElement = doc.getElementById('editorjs');
if (!editorElement) {
throw new Error('Editor element not found');
}
form.appendChild(editorElement);
cy.get('[data-cy=editorjs]')
.find('.ce-paragraph')
@ -172,7 +198,7 @@ describe('Inline Toolbar', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as ToolConstructable,
},
},
data: {
@ -207,9 +233,13 @@ describe('Inline Toolbar', () => {
.then((window) => {
const selection = window.getSelection();
expect(selection.rangeCount).to.be.equal(1);
expect(selection?.rangeCount).to.be.equal(1);
const range = selection.getRangeAt(0);
const range = selection?.getRangeAt(0);
if (!range) {
throw new Error('No range available');
}
cy.get('[data-cy=editorjs]')
.find('.ce-header')

View file

@ -1,5 +1,6 @@
import ToolMock from '../../fixtures/tools/ToolMock';
import ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock';
import type EditorJS from '../../../../types/index';
import type { BlockToolConstructorOptions } from '../../../../types';
describe('Renderer module', function () {
it('should not cause onChange firing during initial rendering', function () {
@ -90,7 +91,7 @@ describe('Renderer module', function () {
/**
* @param options - tool options
*/
constructor(options) {
constructor(options: BlockToolConstructorOptions<MockToolData>) {
super(options);
throw new Error('Tool error');
}

View file

@ -11,7 +11,6 @@ describe('Tools module', () => {
/**
* Construct Tools module for testing purposes
*
* @param config - Editor config
*/
function constructModule(config: EditorConfig = defaultConfig): Tools {
@ -238,53 +237,53 @@ describe('Tools module', () => {
it('Block Tools should contain default tunes if no settings is specified', () => {
const tool = module.blockTools.get('blockToolWithoutSettings');
expect(tool.tunes.has('delete')).to.be.true;
expect(tool.tunes.has('moveUp')).to.be.true;
expect(tool.tunes.has('moveDown')).to.be.true;
expect(tool?.tunes.has('delete')).to.be.true;
expect(tool?.tunes.has('moveUp')).to.be.true;
expect(tool?.tunes.has('moveDown')).to.be.true;
});
it('Block Tools should contain default tunes', () => {
const tool = module.blockTools.get('blockTool');
expect(tool.tunes.has('delete')).to.be.true;
expect(tool.tunes.has('moveUp')).to.be.true;
expect(tool.tunes.has('moveDown')).to.be.true;
expect(tool?.tunes.has('delete')).to.be.true;
expect(tool?.tunes.has('moveUp')).to.be.true;
expect(tool?.tunes.has('moveDown')).to.be.true;
});
it('Block Tools should contain tunes in correct order', () => {
let tool = module.blockTools.get('blockTool');
expect(tool.tunes.has('blockTune')).to.be.true;
expect(tool.tunes.has('blockTune2')).to.be.true;
expect(Array.from(tool.tunes.keys())).to.be.deep.eq(['blockTune2', 'blockTune', 'moveUp', 'delete', 'moveDown']);
expect(tool?.tunes.has('blockTune')).to.be.true;
expect(tool?.tunes.has('blockTune2')).to.be.true;
expect(Array.from(tool?.tunes.keys() ?? [])).to.be.deep.eq(['blockTune2', 'blockTune', 'moveUp', 'delete', 'moveDown']);
tool = module.blockTools.get('withSuccessfulPrepare');
expect(tool.tunes.has('blockTune')).to.be.false;
expect(tool.tunes.has('blockTune2')).to.be.true;
expect(tool?.tunes.has('blockTune')).to.be.false;
expect(tool?.tunes.has('blockTune2')).to.be.true;
tool = module.blockTools.get('withoutPrepare');
expect(tool.tunes.has('blockTune')).to.be.false;
expect(tool.tunes.has('blockTune2')).to.be.false;
expect(tool?.tunes.has('blockTune')).to.be.false;
expect(tool?.tunes.has('blockTune2')).to.be.false;
});
it('Block Tools should contain inline tools in correct order', () => {
let tool = module.blockTools.get('blockTool');
expect(tool.inlineTools.has('inlineTool')).to.be.true;
expect(tool.inlineTools.has('inlineTool2')).to.be.true;
expect(Array.from(tool.inlineTools.keys())).to.be.deep.eq(['inlineTool2', 'inlineTool']);
expect(tool?.inlineTools.has('inlineTool')).to.be.true;
expect(tool?.inlineTools.has('inlineTool2')).to.be.true;
expect(Array.from(tool?.inlineTools.keys() ?? [])).to.be.deep.eq(['inlineTool2', 'inlineTool']);
tool = module.blockTools.get('withSuccessfulPrepare');
expect(tool.inlineTools.has('inlineTool')).to.be.false;
expect(tool.inlineTools.has('inlineTool2')).to.be.true;
expect(tool?.inlineTools.has('inlineTool')).to.be.false;
expect(tool?.inlineTools.has('inlineTool2')).to.be.true;
tool = module.blockTools.get('withoutPrepare');
expect(tool.inlineTools.has('inlineTool')).to.be.false;
expect(tool.inlineTools.has('inlineTool2')).to.be.false;
expect(tool?.inlineTools.has('inlineTool')).to.be.false;
expect(tool?.inlineTools.has('inlineTool2')).to.be.false;
});
});

View file

@ -7,6 +7,7 @@ import { BlockChangedMutationType } from '../../../types/events/block/BlockChang
import { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
import type EditorJS from '../../../types/index';
import type { API, OutputData, BlockMutationEvent } from '../../../types/index';
import { modificationsObserverBatchTimeout } from '../../../src/components/constants';
/**
@ -22,21 +23,20 @@ const EditorJSApiMock = Cypress.sinon.match.any;
describe('onChange callback', () => {
/**
* Creates Editor instance
*
* @param blocks - list of blocks to prefill the editor
*/
function createEditor(blocks = null): void {
function createEditor(blocks?: OutputData['blocks']): void {
const config = {
tools: {
header: Header,
code: Code,
},
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
data: blocks ? {
blocks,
} : null,
} : undefined,
};
cy.spy(config, 'onChange').as('onChange');
@ -46,23 +46,22 @@ describe('onChange callback', () => {
/**
* Creates Editor instance with save inside the onChange event.
*
* @param blocks - list of blocks to prefill the editor
*/
function createEditorWithSave(blocks = null): void {
function createEditorWithSave(blocks?: OutputData['blocks']): void {
const config = {
tools: {
header: Header,
code: Code,
delimiter: Delimiter,
},
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
api.saver.save();
},
data: blocks ? {
blocks,
} : null,
} : undefined,
};
cy.spy(config, 'onChange').as('onChange');
@ -515,7 +514,7 @@ describe('onChange callback', () => {
tools: {
testTool: ToolWithMutationFreeAttribute,
},
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
data: {
@ -582,7 +581,7 @@ describe('onChange callback', () => {
tools: {
testTool: ToolWithMutationFreeAttribute,
},
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
data: {
@ -652,7 +651,7 @@ describe('onChange callback', () => {
tools: {
testTool: ToolWithMutationFreeAttribute,
},
onChange: function (api, event) {
onChange: function (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void {
console.log('something changed!!!!!!!!', event);
},
data: {
@ -767,7 +766,7 @@ describe('onChange callback', () => {
block,
],
},
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
};
@ -852,7 +851,7 @@ describe('onChange callback', () => {
it('should not be called when editor is initialized with readOnly mode', () => {
const config = {
readOnly: true,
onChange: (api, event): void => {
onChange: (api: API, event: BlockMutationEvent | BlockMutationEvent[]): void => {
console.log('something changed', event);
},
data: {

View file

@ -4,7 +4,6 @@ import type EditorJS from '../../../types';
describe('ReadOnly API spec', () => {
/**
* Creates the new editor instance
*
* @param config - Editor Config
*/
function createEditor(config?: EditorConfig): void {

View file

@ -6,7 +6,7 @@ describe('Blocks selection', () => {
});
afterEach(function () {
if (this.editorInstance) {
if (this.editorInstance != null) {
this.editorInstance.destroy();
}
});

View file

@ -37,8 +37,8 @@ describe('BlockTool', () => {
public static isReadOnlySupported = true;
public static reset;
public static prepare;
public static reset?: () => void | Promise<void>;
public static prepare?: (data: {toolName: string, config: ToolSettings}) => void | Promise<void>;
public static shortcut = 'CTRL+N';
@ -49,7 +49,7 @@ describe('BlockTool', () => {
public config: ToolSettings;
// eslint-disable-next-line jsdoc/require-jsdoc
constructor({ data, block, readOnly, api, config }) {
constructor({ data, block, readOnly, api, config }: { data: BlockToolData; block: object; readOnly: boolean; api: object; config: ToolSettings }) {
this.data = data;
this.block = block;
this.readonly = readOnly;
@ -157,8 +157,8 @@ describe('BlockTool', () => {
// tslint:disable-next-line:forin
for (const key in expected) {
expected[key] = {
...expected[key],
(expected as Record<string, Record<string, boolean>>)[key] = {
...expected[key as keyof typeof expected],
b: true,
};
}
@ -493,7 +493,7 @@ describe('BlockTool', () => {
const expected = userDefinedToolboxConfig.map((item, i) => {
const toolToolboxEntry = toolboxEntries[i];
if (toolToolboxEntry) {
if (toolToolboxEntry !== undefined) {
return {
...toolToolboxEntry,
...item,

View file

@ -12,8 +12,8 @@ describe('BlockTune', () => {
const options = {
name: 'blockTune',
constructable: class {
public static reset;
public static prepare;
public static reset?: () => void | Promise<void>;
public static prepare?: (data: {toolName: string, config: ToolSettings}) => void | Promise<void>;
public api: object;
public config: ToolSettings;
@ -21,7 +21,7 @@ describe('BlockTune', () => {
public block: object;
// eslint-disable-next-line jsdoc/require-jsdoc
constructor({ api, config, block, data }) {
constructor({ api, config, block, data }: { api: object; config: ToolSettings; block: object; data: BlockTuneData }) {
this.api = api;
this.config = config;
this.block = block;

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* tslint:disable:max-classes-per-file */
import type { ToolSettings } from '@/types';
import type { ToolSettings, ToolConfig } from '@/types';
import { ToolType } from '@/types/tools/adapters/tool-type';
import InlineToolAdapter from '../../../../src/components/tools/inline';
@ -17,8 +17,8 @@ describe('InlineTool', () => {
public static title = 'Title';
public static reset;
public static prepare;
public static reset?: () => void | Promise<void>;
public static prepare?: (data: {toolName: string, config: ToolConfig}) => void | Promise<void>;
public static shortcut = 'CTRL+N';
public static isReadOnlySupported = true;
@ -31,7 +31,7 @@ describe('InlineTool', () => {
* @param options.api - EditorAPI
* @param options.config - tool config
*/
constructor({ api, config }) {
constructor({ api, config }: { api: object; config: ToolSettings }) {
this.api = api;
this.config = config;
}

View file

@ -43,7 +43,7 @@ const FakeBlockTune = {
* Unit tests for ToolsCollection class
*/
describe('ToolsCollection', (): void => {
let collection;
let collection: ToolsCollection;
/**
* Mock for Tools in collection

View file

@ -6,12 +6,14 @@ import InlineToolAdapter from '../../../../src/components/tools/inline';
import BlockToolAdapter from '../../../../src/components/tools/block';
import BlockTuneAdapter from '../../../../src/components/tools/tune';
import Paragraph from '@editorjs/paragraph';
import type { BlockToolConstructable } from '../../../../types';
describe('ToolsFactory', (): void => {
let factory;
let factory: ToolsFactory;
const paragraphClass = Paragraph as unknown as BlockToolConstructable;
const config = {
paragraph: {
class: Paragraph,
class: paragraphClass,
},
link: {
class: LinkInlineTool,

View file

@ -33,9 +33,13 @@ describe('BlockTunes', function () {
.then((window) => {
const selection = window.getSelection();
expect(selection.rangeCount).to.be.equal(1);
expect(selection?.rangeCount).to.be.equal(1);
const range = selection.getRangeAt(0);
const range = selection?.getRangeAt(0);
if (!range) {
throw new Error('Range is undefined');
}
cy.get('[data-cy=editorjs]')
.find('[data-cy="block-tunes"] .cdx-search-field')
@ -369,7 +373,11 @@ describe('BlockTunes', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const range = selection?.getRangeAt(0);
if (!range) {
throw new Error('Range is undefined');
}
cy.get('[data-cy=editorjs]')
.find('.ce-header')

View file

@ -1,5 +1,5 @@
import Header from '@editorjs/header';
import type { InlineTool, MenuConfig } from '../../../../types/tools';
import type { InlineTool, InlineToolConstructorOptions, MenuConfig, ToolConstructable } from '../../../../types/tools';
import { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks';
describe('Inline Toolbar', () => {
@ -8,7 +8,7 @@ describe('Inline Toolbar', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as ToolConstructable,
},
},
data: {
@ -46,14 +46,18 @@ describe('Inline Toolbar', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as ToolConstructable,
inlineToolbar: ['bold', 'testTool', 'link'],
},
testTool: {
class: class {
class: class TestTool {
public static isInline = true;
// eslint-disable-next-line jsdoc/require-jsdoc
public constructor(_config: InlineToolConstructorOptions) {
// Constructor required by InlineToolConstructable
}
// eslint-disable-next-line jsdoc/require-jsdoc
public render(): MenuConfig {
return {
icon: 'n',
@ -71,7 +75,7 @@ describe('Inline Toolbar', () => {
},
};
}
},
} as unknown as ToolConstructable,
},
},
data: {
@ -115,14 +119,18 @@ describe('Inline Toolbar', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as ToolConstructable,
inlineToolbar: ['bold', 'testTool'],
},
testTool: {
class: class {
class: class TestTool {
public static isInline = true;
// eslint-disable-next-line jsdoc/require-jsdoc
public constructor(_config: InlineToolConstructorOptions) {
// Constructor required by InlineToolConstructable
}
// eslint-disable-next-line jsdoc/require-jsdoc
public render(): MenuConfig {
return {
icon: 'n',
@ -140,7 +148,7 @@ describe('Inline Toolbar', () => {
},
};
}
},
} as unknown as ToolConstructable,
},
},
data: {

View file

@ -61,7 +61,11 @@ describe('Toolbox', function () {
cy.window()
.then((window) => {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const range = selection?.getRangeAt(0);
if (!range) {
throw new Error('Selection range is not available');
}
cy.get('[data-cy=editorjs]')
.find(`.ce-block[data-id=${blocks[0].id}]`)

View file

@ -1,6 +1,7 @@
import { PopoverDesktop as Popover, PopoverItemType } from '../../../../src/components/utils/popover';
import type { PopoverItemParams } from '@/types/utils/popover';
import type { MenuConfig } from '../../../../types/tools';
import type { BlockToolConstructable } from '../../../../types/tools';
import Header from '@editorjs/header';
/* eslint-disable @typescript-eslint/no-empty-function */
@ -975,7 +976,8 @@ describe('Popover', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as BlockToolConstructable,
config: {},
},
},
data: {
@ -1020,7 +1022,8 @@ describe('Popover', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as BlockToolConstructable,
config: {},
},
},
data: {
@ -1080,7 +1083,8 @@ describe('Popover', () => {
cy.createEditor({
tools: {
header: {
class: Header,
class: Header as unknown as BlockToolConstructable,
config: {},
},
},
data: {

View file

@ -70,7 +70,7 @@ export interface Blocks {
/**
* Returns the index of Block by id;
*/
getBlockIndex(blockId: string): number;
getBlockIndex(blockId: string): number | undefined;
/**
* Get Block API object by html element

View file

@ -45,6 +45,11 @@ export interface BlockTuneConstructable {
*/
sanitize?: SanitizerConfig;
/**
* Shortcut for Tool
*/
shortcut?: string;
/**
* @constructor
*

View file

@ -41,6 +41,11 @@ export interface BaseToolConstructable {
*/
sanitize?: SanitizerConfig;
/**
* Shortcut for Tool
*/
shortcut?: string;
/**
* Title of Inline Tool.
* @deprecated use {@link MenuConfig} item title instead