From 0379064dbb79acf126a2f41db2c16e53379b1815 Mon Sep 17 00:00:00 2001 From: JackUait Date: Fri, 14 Nov 2025 18:51:31 +0300 Subject: [PATCH] test: move part of the tests from Cypress to Playwright --- src/components/block/index.ts | 9 +- src/components/modules/blockEvents.ts | 26 + src/components/modules/blockManager.ts | 2 +- src/components/modules/tools.ts | 20 +- src/components/modules/ui.ts | 37 +- .../components/search-input/search-input.ts | 60 +- test/cypress/tests/api/tools.cy.ts | 853 ------------------ test/cypress/tests/api/tunes.cy.ts | 242 ----- test/cypress/tests/block-ids.cy.ts | 161 ---- .../tests/modules/BlockEvents/Delete.cy.ts | 509 ----------- .../tests/modules/BlockEvents/Enter.cy.ts | 79 -- .../tests/modules/BlockEvents/Slash.cy.ts | 176 ---- .../cypress/tests/modules/InlineToolbar.cy.ts | 305 ------- test/cypress/tests/modules/Saver.cy.ts | 87 -- test/cypress/tests/modules/Ui.cy.ts | 144 --- test/cypress/tests/ui/DataEmpty.cy.ts | 72 -- test/playwright/tests/api/tools.spec.ts | 794 ++++++++++++++++ test/playwright/tests/api/tunes.spec.ts | 277 ++++++ .../tests/modules/BlockEvents/Delete.spec.ts | 558 ++++++++++++ .../tests/modules/BlockEvents/Enter.spec.ts | 166 ++++ .../tests/modules/BlockEvents/Slash.spec.ts | 204 +++++ .../playwright/tests/modules/BlockIds.spec.ts | 377 ++++++++ test/playwright/tests/modules/Saver.spec.ts | 236 +++++ test/playwright/tests/modules/tools.spec.ts | 278 ------ test/playwright/tests/ui/data-empty.spec.ts | 167 ++++ test/playwright/tests/ui/ui-module.spec.ts | 352 ++++++++ .../tests/utils/popover-search.spec.ts | 38 +- test/unit/components/modules/tools.test.ts | 545 +++++++++++ 28 files changed, 3837 insertions(+), 2937 deletions(-) delete mode 100644 test/cypress/tests/api/tools.cy.ts delete mode 100644 test/cypress/tests/api/tunes.cy.ts delete mode 100644 test/cypress/tests/block-ids.cy.ts delete mode 100644 test/cypress/tests/modules/BlockEvents/Delete.cy.ts delete mode 100644 test/cypress/tests/modules/BlockEvents/Enter.cy.ts delete mode 100644 test/cypress/tests/modules/BlockEvents/Slash.cy.ts delete mode 100644 test/cypress/tests/modules/InlineToolbar.cy.ts delete mode 100644 test/cypress/tests/modules/Saver.cy.ts delete mode 100644 test/cypress/tests/modules/Ui.cy.ts delete mode 100644 test/cypress/tests/ui/DataEmpty.cy.ts create mode 100644 test/playwright/tests/api/tools.spec.ts create mode 100644 test/playwright/tests/api/tunes.spec.ts create mode 100644 test/playwright/tests/modules/BlockEvents/Delete.spec.ts create mode 100644 test/playwright/tests/modules/BlockEvents/Enter.spec.ts create mode 100644 test/playwright/tests/modules/BlockEvents/Slash.spec.ts create mode 100644 test/playwright/tests/modules/BlockIds.spec.ts create mode 100644 test/playwright/tests/modules/Saver.spec.ts delete mode 100644 test/playwright/tests/modules/tools.spec.ts create mode 100644 test/playwright/tests/ui/data-empty.spec.ts create mode 100644 test/playwright/tests/ui/ui-module.spec.ts create mode 100644 test/unit/components/modules/tools.test.ts diff --git a/src/components/block/index.ts b/src/components/block/index.ts index f44dd23a..d61b1e98 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -846,9 +846,12 @@ export default class Block extends EventsDispatcher { * @private */ private addToolDataAttributes(element: HTMLElement): void { - // Add data-block-tool attribute to identify the tool type used for the block - if (this.name === 'paragraph' && element.classList.contains('ce-paragraph')) { - element.setAttribute('data-block-tool', 'paragraph'); + /** + * Add data-block-tool attribute to identify the tool type used for the block. + * Some tools (like Paragraph) add their own class names, but we can rely on the tool name for all cases. + */ + if (!element.hasAttribute('data-block-tool') && this.name) { + element.setAttribute('data-block-tool', this.name); } } diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts index f59a1fc5..485463c0 100644 --- a/src/components/modules/blockEvents.ts +++ b/src/components/modules/blockEvents.ts @@ -25,6 +25,32 @@ export default class BlockEvents extends Module { */ this.beforeKeydownProcessing(event); + const { BlockSelection, BlockManager, Caret } = this.Editor; + const isRemoveKey = event.keyCode === _.keyCodes.BACKSPACE || event.keyCode === _.keyCodes.DELETE; + const selectionExists = SelectionUtils.isSelectionExists; + const selectionCollapsed = SelectionUtils.isCollapsed === true; + const shouldHandleSelectionDeletion = isRemoveKey && + BlockSelection.anyBlockSelected && + (!selectionExists || selectionCollapsed); + + if (shouldHandleSelectionDeletion) { + const selectionPositionIndex = BlockManager.removeSelectedBlocks(); + + if (selectionPositionIndex !== undefined) { + const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true); + + Caret.setToBlock(insertedBlock, Caret.positions.START); + } + + BlockSelection.clearSelection(event); + + event.preventDefault(); + event.stopImmediatePropagation(); + event.stopPropagation(); + + return; + } + /** * Fire keydown processor by event.keyCode */ diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index 90b1f6f9..30c66ac9 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -631,7 +631,7 @@ export default class BlockManager extends Module { .sort((first, second) => second.index - first.index); selectedBlockEntries.forEach(({ block }) => { - void this.removeBlock(block); + void this.removeBlock(block, false); }); return selectedBlockEntries.length > 0 diff --git a/src/components/modules/tools.ts b/src/components/modules/tools.ts index 44c60d37..88ce08e0 100644 --- a/src/components/modules/tools.ts +++ b/src/components/modules/tools.ts @@ -210,11 +210,23 @@ export default class Tools extends Module { * Calls each Tool reset method to clean up anything set by Tool */ public destroy(): void { - Object.values(this.available).forEach(async tool => { - if (_.isFunction(tool.reset)) { - await tool.reset(); + for (const tool of this.available.values()) { + const resetResult = (() => { + try { + return tool.reset(); + } catch (error) { + _.log(`Tool "${tool.name}" reset failed`, 'warn', error); + + return undefined; + } + })(); + + if (resetResult instanceof Promise) { + resetResult.catch(error => { + _.log(`Tool "${tool.name}" reset failed`, 'warn', error); + }); } - }); + } } /** diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index 6dda0121..05e4dc13 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -159,17 +159,28 @@ export default class UI extends Module { * Prepare components based on read-only state */ if (!readOnlyEnabled) { - /** - * Postpone events binding to the next tick to make sure all ui elements are ready - */ - window.requestIdleCallback(() => { + const bindListeners = (): void => { /** * Bind events for the UI elements */ this.bindReadOnlySensitiveListeners(); - }, { - timeout: 2000, - }); + }; + + /** + * Ensure listeners are attached immediately for interactive use. + */ + bindListeners(); + + const idleCallback = window.requestIdleCallback; + + if (typeof idleCallback === 'function') { + /** + * Re-bind on idle to preserve historical behavior when additional nodes appear later. + */ + idleCallback(bindListeners, { + timeout: 2000, + }); + } } else { /** * Unbind all events @@ -574,11 +585,14 @@ export default class UI extends Module { private backspacePressed(event: KeyboardEvent): void { const { BlockManager, BlockSelection, Caret } = this.Editor; + const selectionExists = Selection.isSelectionExists; + const selectionCollapsed = Selection.isCollapsed; + /** * If any block selected and selection doesn't exists on the page (that means no other editable element is focused), * remove selected blocks */ - if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) { + if (BlockSelection.anyBlockSelected && (!selectionExists || selectionCollapsed === true)) { const selectionPositionIndex = BlockManager.removeSelectedBlocks(); if (selectionPositionIndex === undefined) { @@ -645,11 +659,14 @@ export default class UI extends Module { const hasPointerToBlock = BlockManager.currentBlockIndex >= 0; + const selectionExists = Selection.isSelectionExists; + const selectionCollapsed = Selection.isCollapsed; + /** * If any block selected and selection doesn't exists on the page (that means no other editable element is focused), * remove selected blocks */ - if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) { + if (BlockSelection.anyBlockSelected && (!selectionExists || selectionCollapsed === true)) { /** Clear selection */ BlockSelection.clearSelection(event); @@ -899,7 +916,7 @@ export default class UI extends Module { * to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table) * - Or, default-block is not empty */ - if (!BlockManager.lastBlock.tool.isDefault || !BlockManager.lastBlock.isEmpty) { + if (!BlockManager.lastBlock?.tool.isDefault || !BlockManager.lastBlock?.isEmpty) { BlockManager.insertAtEnd(); } diff --git a/src/components/utils/popover/components/search-input/search-input.ts b/src/components/utils/popover/components/search-input/search-input.ts index 05aaac58..ab470d15 100644 --- a/src/components/utils/popover/components/search-input/search-input.ts +++ b/src/components/utils/popover/components/search-input/search-input.ts @@ -71,13 +71,12 @@ export class SearchInput extends EventsDispatcher { this.wrapper.appendChild(iconWrapper); this.wrapper.appendChild(this.input); - this.listeners.on(this.input, 'input', () => { - this.searchQuery = this.input.value; + this.overrideValueProperty(); - this.emit(SearchInputEvent.Search, { - query: this.searchQuery, - items: this.foundItems, - }); + const eventsToHandle = ['input', 'keyup', 'search', 'change'] as const; + + eventsToHandle.forEach((eventName) => { + this.listeners.on(this.input, eventName, this.handleValueChange); }); } @@ -100,14 +99,59 @@ export class SearchInput extends EventsDispatcher { */ public clear(): void { this.input.value = ''; - this.searchQuery = ''; + } + + /** + * Handles value changes for the input element + */ + private handleValueChange = (): void => { + this.applySearch(this.input.value); + }; + + /** + * Applies provided query to the search state and notifies listeners + * + * @param query - search query to apply + */ + private applySearch(query: string): void { + if (this.searchQuery === query) { + return; + } + + this.searchQuery = query; this.emit(SearchInputEvent.Search, { - query: '', + query, items: this.foundItems, }); } + /** + * Overrides value property setter to catch programmatic changes + */ + private overrideValueProperty(): void { + const prototype = Object.getPrototypeOf(this.input); + const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value'); + + if (descriptor?.set === undefined || descriptor.get === undefined) { + return; + } + + const applySearch = this.applySearch.bind(this); + + Object.defineProperty(this.input, 'value', { + configurable: descriptor.configurable ?? true, + enumerable: descriptor.enumerable ?? false, + get(): string { + return descriptor.get?.call(this) ?? ''; + }, + set(value: string): void { + descriptor.set?.call(this, value); + applySearch(value); + }, + }); + } + /** * Clears memory */ diff --git a/test/cypress/tests/api/tools.cy.ts b/test/cypress/tests/api/tools.cy.ts deleted file mode 100644 index 59517287..00000000 --- a/test/cypress/tests/api/tools.cy.ts +++ /dev/null @@ -1,853 +0,0 @@ -import type { ToolboxConfigEntry, PasteConfig } from '../../../../types'; -import type { HTMLPasteEvent, TunesMenuConfig } from '../../../../types/tools'; -import { EDITOR_INTERFACE_SELECTOR } from '../../../../src/components/constants'; - -/* eslint-disable @typescript-eslint/no-empty-function */ - -const ICON = ''; - -describe('Editor Tools Api', () => { - context('Tunes — renderSettings()', () => { - it('should contain a single block tune configured in tool\'s renderSettings() method', () => { - /** Tool with single tunes menu entry configured */ - class TestTool { - /** Returns toolbox config as list of entries */ - public static get toolbox(): ToolboxConfigEntry { - return { - title: 'Test tool', - icon: ICON, - }; - } - - /** Returns configuration for block tunes menu */ - public renderSettings(): TunesMenuConfig { - return { - label: 'Test tool tune', - icon: ICON, - name: 'testToolTune', - - onActivate: (): void => { }, - }; - } - - /** Save method stub */ - public save(): void { } - - /** Renders a block */ - public render(): HTMLElement { - const element = document.createElement('div'); - - element.contentEditable = 'true'; - element.setAttribute('data-name', 'testBlock'); - - return element; - } - } - - cy.createEditor({ - tools: { - testTool: TestTool, - }, - }).as('editorInstance'); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-toolbar__plus') - .click(); - - // Insert test tool block - cy.get(EDITOR_INTERFACE_SELECTOR) - .get(`[data-item-name="testTool"]`) - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('[data-name=testBlock]') - .type('some text') - .click(); - - // Open block tunes - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('.ce-toolbar__settings-btn') - .click(); - - // Expect preconfigured tune to exist in tunes menu - cy.get('[data-item-name=testToolTune]').should('exist'); - }); - - it('should contain multiple block tunes if configured in tool\'s renderSettings() method', () => { - /** Tool with single tunes menu entry configured */ - class TestTool { - /** Returns toolbox config as list of entries */ - public static get toolbox(): ToolboxConfigEntry { - return { - title: 'Test tool', - icon: ICON, - }; - } - - /** Returns configuration for block tunes menu */ - public renderSettings(): TunesMenuConfig { - return [ - { - label: 'Test tool tune 1', - icon: ICON, - name: 'testToolTune1', - - onActivate: (): void => { }, - }, - { - label: 'Test tool tune 2', - icon: ICON, - name: 'testToolTune2', - - onActivate: (): void => { }, - }, - ]; - } - - /** Save method stub */ - public save(): void { } - - /** Renders a block */ - public render(): HTMLElement { - const element = document.createElement('div'); - - element.contentEditable = 'true'; - element.setAttribute('data-name', 'testBlock'); - - return element; - } - } - - cy.createEditor({ - tools: { - testTool: TestTool, - }, - }).as('editorInstance'); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-toolbar__plus') - .click(); - - // Insert test tool block - cy.get(EDITOR_INTERFACE_SELECTOR) - .get(`[data-item-name="testTool"]`) - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('[data-name=testBlock]') - .type('some text') - .click(); - - // Open block tunes - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('.ce-toolbar__settings-btn') - .click(); - - // Expect preconfigured tunes to exist in tunes menu - cy.get('[data-item-name=testToolTune1]').should('exist'); - cy.get('[data-item-name=testToolTune2]').should('exist'); - }); - - it('should contain block tunes represented as custom html if so configured in tool\'s renderSettings() method', () => { - const sampleText = 'sample text'; - - /** Tool with single tunes menu entry configured */ - class TestTool { - /** Returns toolbox config as list of entries */ - public static get toolbox(): ToolboxConfigEntry { - return { - title: 'Test tool', - icon: ICON, - }; - } - - /** Returns configuration for block tunes menu */ - public renderSettings(): HTMLElement { - const element = document.createElement('div'); - - element.textContent = sampleText; - - return element; - } - - /** Save method stub */ - public save(): void { } - - /** Renders a block */ - public render(): HTMLElement { - const element = document.createElement('div'); - - element.contentEditable = 'true'; - element.setAttribute('data-name', 'testBlock'); - - return element; - } - } - - cy.createEditor({ - tools: { - testTool: TestTool, - }, - }).as('editorInstance'); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-toolbar__plus') - .click(); - - // Insert test tool block - cy.get(EDITOR_INTERFACE_SELECTOR) - .get(`[data-item-name="testTool"]`) - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('[data-name=testBlock]') - .type('some text') - .click(); - - // Open block tunes - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('.ce-toolbar__settings-btn') - .click(); - - // Expect preconfigured custom html tunes to exist in tunes menu - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('.ce-popover') - .should('contain.text', sampleText); - }); - - it('should support label alias', () => { - /** Tool with single tunes menu entry configured */ - class TestTool { - /** Returns toolbox config as list of entries */ - public static get toolbox(): ToolboxConfigEntry { - return { - title: 'Test tool', - icon: ICON, - }; - } - - /** Returns configuration for block tunes menu */ - public renderSettings(): TunesMenuConfig { - return [ - { - icon: ICON, - name: 'testToolTune1', - onActivate: (): void => {}, - - // Set text via title property - title: 'Test tool tune 1', - }, - { - icon: ICON, - name: 'testToolTune2', - onActivate: (): void => {}, - - // Set test via label property - label: 'Test tool tune 2', - }, - ]; - } - - /** Save method stub */ - public save(): void {} - - /** Renders a block */ - public render(): HTMLElement { - const element = document.createElement('div'); - - element.contentEditable = 'true'; - element.setAttribute('data-name', 'testBlock'); - - return element; - } - } - - cy.createEditor({ - tools: { - testTool: TestTool, - }, - }).as('editorInstance'); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-toolbar__plus') - .click(); - - // Insert test tool block - cy.get(EDITOR_INTERFACE_SELECTOR) - .get(`[data-item-name="testTool"]`) - .click(); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('[data-name=testBlock]') - .type('some text') - .click(); - - // Open block tunes - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('.ce-toolbar__settings-btn') - .click(); - - // Expect both tunes to have correct text - cy.get('[data-item-name=testToolTune1]').contains('Test tool tune 1'); - cy.get('[data-item-name=testToolTune2]').contains('Test tool tune 2'); - }); - }); - - /** - * @todo cover all the pasteConfig properties - */ - context('Paste — pasteConfig()', () => { - context('tags', () => { - /** - * tags: ['H1', 'H2'] - */ - it('should use corresponding tool when the array of tag names specified', () => { - /** - * Test tool with pasteConfig.tags specified - */ - class TestImgTool { - /** config specified handled tag */ - public static get pasteConfig(): PasteConfig { - return { - tags: [ 'img' ], // only tag name specified. Attributes should be sanitized - }; - } - - /** onPaste callback will be stubbed below */ - public onPaste(): void { } - - /** save is required for correct implementation of the BlockTool class */ - public save(): void { } - - /** render is required for correct implementation of the BlockTool class */ - public render(): HTMLElement { - return document.createElement('img'); - } - } - - const toolsOnPaste = cy.spy(TestImgTool.prototype, 'onPaste'); - - cy.createEditor({ - tools: { - testTool: TestImgTool, - }, - }).as('editorInstance'); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click() - .paste({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'text/html': '', - }) - .then(() => { - expect(toolsOnPaste).to.be.called; - }); - }); - - /** - * tags: ['img'] -> - */ - it('should sanitize all attributes from tag, if only tag name specified ', () => { - /** - * Variable used for spying the pasted element we are passing to the Tool - */ - let pastedElement: HTMLElement | undefined; - - /** - * Test tool with pasteConfig.tags specified - */ - class TestImageTool { - /** config specified handled tag */ - public static get pasteConfig(): PasteConfig { - return { - tags: [ 'img' ], // only tag name specified. Attributes should be sanitized - }; - } - - /** onPaste callback will be stubbed below */ - public onPaste(): void { } - - /** save is required for correct implementation of the BlockTool class */ - public save(): void { } - - /** render is required for correct implementation of the BlockTool class */ - public render(): HTMLElement { - return document.createElement('img'); - } - } - - /** - * Stub the onPaste method to access the PasteEvent data for assertion - */ - cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => { - pastedElement = event.detail.data; - }); - - cy.createEditor({ - tools: { - testImageTool: TestImageTool, - }, - }); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click() - .paste({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'text/html': '', // all attributes should be sanitized - }) - .then(() => { - expect(pastedElement).not.to.be.undefined; - if (pastedElement === undefined) { - throw new Error('pastedElement should be defined'); - } - expect(pastedElement.tagName.toLowerCase()).eq('img'); - expect(pastedElement.attributes.length).eq(0); - }); - }); - - /** - * tags: ['OL','LI',] - * ->
    - *
  1. - *
  2. - *
- */ - it('should sanitize all attributes from tags, even if tag names specified in uppercase', () => { - /** - * Variable used for spying the pasted element we are passing to the Tool - */ - let pastedElement: HTMLElement | undefined; - - /** - * Test tool with pasteConfig.tags specified - */ - class TestListTool { - /** config specified handled tag */ - public static get pasteConfig(): PasteConfig { - return { - tags: ['OL', 'LI'], // tag names specified in upper case - }; - } - - /** onPaste callback will be stubbed below */ - public onPaste(): void { } - - /** save is required for correct implementation of the BlockTool class */ - public save(): void { } - - /** render is required for correct implementation of the BlockTool class */ - public render(): HTMLElement { - return document.createElement('ol'); - } - } - - /** - * Stub the onPaste method to access the PasteEvent data for assertion - */ - cy.stub(TestListTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => { - pastedElement = event.detail.data; - }); - - cy.createEditor({ - tools: { - testListTool: TestListTool, - }, - }); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click() - .paste({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'text/html': '
  1. Ordered List
  2. Unordered List
', // all attributes should be sanitized,
  • s should be preserved - }) - .then(() => { - expect(pastedElement).not.to.be.undefined; - if (pastedElement === undefined) { - throw new Error('pastedElement should be defined'); - } - expect(pastedElement.tagName.toLowerCase()).eq('ol'); - expect(pastedElement.attributes.length).eq(0); - // check number of children - expect(pastedElement.children.length).eq(2); - - /** - * Check that all children are
  • tags - */ - pastedElement.childNodes.forEach((child) => { - if (child instanceof Element) { - expect(child.tagName.toLowerCase()).eq('li'); - expect(child.attributes.length).eq(0); - } - }); - }); - }); - - /** - * tags: [{ - * img: { - * src: true - * } - * }] - * -> - * - */ - it('should leave attributes if entry specified as a sanitizer config ', () => { - /** - * Variable used for spying the pasted element we are passing to the Tool - */ - let pastedElement: HTMLElement | undefined; - - /** - * Test tool with pasteConfig.tags specified - */ - class TestImageTool { - /** config specified handled tag */ - public static get pasteConfig(): PasteConfig { - return { - tags: [ - { - img: { - src: true, - }, - }, - ], - }; - } - - /** onPaste callback will be stubbed below */ - public onPaste(): void { } - - /** save is required for correct implementation of the BlockTool class */ - public save(): void { } - - /** render is required for correct implementation of the BlockTool class */ - public render(): HTMLElement { - return document.createElement('img'); - } - } - - /** - * Stub the onPaste method to access the PasteEvent data for assertion - */ - cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => { - pastedElement = event.detail.data; - }); - - cy.createEditor({ - tools: { - testImageTool: TestImageTool, - }, - }); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click() - .paste({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'text/html': '', - }) - .then(() => { - expect(pastedElement).not.to.be.undefined; - if (pastedElement === undefined) { - throw new Error('pastedElement should be defined'); - } - - /** - * Check that the has only "src" attribute - */ - expect(pastedElement.tagName.toLowerCase()).eq('img'); - expect(pastedElement.getAttribute('src')).eq('foo'); - expect(pastedElement.attributes.length).eq(1); - }); - }); - - /** - * tags: [ - * 'video', - * { - * source: { - * src: true - * } - * } - * ] - */ - it('should support mixed tag names and sanitizer config ', () => { - /** - * Variable used for spying the pasted element we are passing to the Tool - */ - let pastedElement: HTMLElement | undefined; - - /** - * Test tool with pasteConfig.tags specified - */ - class TestTool { - /** config specified handled tag */ - public static get pasteConfig(): PasteConfig { - return { - tags: [ - 'video', // video should not have attributes - { - source: { // source should have only src attribute - src: true, - }, - }, - ], - }; - } - - /** onPaste callback will be stubbed below */ - public onPaste(): void { } - - /** save is required for correct implementation of the BlockTool class */ - public save(): void { } - - /** render is required for correct implementation of the BlockTool class */ - public render(): HTMLElement { - return document.createElement('video'); - } - } - - /** - * Stub the onPaste method to access the PasteEvent data for assertion - */ - cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => { - pastedElement = event.detail.data; - }); - - cy.createEditor({ - tools: { - testTool: TestTool, - }, - }); - - cy.get(EDITOR_INTERFACE_SELECTOR) - .get('div.ce-block') - .click() - .paste({ - // eslint-disable-next-line @typescript-eslint/naming-convention - 'text/html': '', - }) - .then(() => { - expect(pastedElement).not.to.be.undefined; - if (pastedElement === undefined) { - throw new Error('pastedElement should be defined'); - } - - /** - * Check that