import { UiFinder } from '@ephox/agar'; import { context, describe, it } from '@ephox/bedrock-client'; import { Fun } from '@ephox/katamari'; import { PlatformDetection } from '@ephox/sand'; import { Attribute, Class, SugarBody } from '@ephox/sugar'; import { TinyAssertions, TinyHooks, TinySelections } from '@ephox/wrap-mcagar'; import { assert } from 'chai'; import Editor from 'tinymce/core/api/Editor'; import EditorManager from 'tinymce/core/api/EditorManager'; import { BeforeSetContentEvent, SaveContentEvent, SetContentEvent } from 'tinymce/core/api/EventTypes'; import PluginManager from 'tinymce/core/api/PluginManager'; import { EditorEvent } from 'tinymce/core/api/util/EventDispatcher'; import URI from 'tinymce/core/api/util/URI'; import { UndoLevel } from 'tinymce/core/undo/UndoManagerTypes'; import * as HtmlUtils from '../module/test/HtmlUtils'; describe('browser.tinymce.core.EditorTest', () => { const browser = PlatformDetection.detect().browser; const hook = TinyHooks.bddSetup({ selector: 'textarea', add_unload_trigger: false, disable_nodechange: true, custom_elements: 'custom1,~custom2', extended_valid_elements: 'custom1,custom2,script[*]', entities: 'raw', indent: false, custom_prop1: 5, custom_prop2: 5, base_url: '/project/tinymce/js/tinymce' }, []); it('TBA: Event: change', () => { const editor = hook.editor(); let level: UndoLevel | undefined; let lastLevel: UndoLevel | undefined; editor.on('change', (e) => { level = e.level; lastLevel = e.lastLevel; }); editor.setContent(''); editor.insertContent('a'); assert.equal(level?.content.toLowerCase(), '

a

', 'Event: change'); assert.equal(lastLevel?.content, editor.undoManager.data[0].content, 'Event: change'); editor.off('change'); }); it('TBA: Event: beforeExecCommand', () => { const editor = hook.editor(); let cmd, ui, value; editor.on('BeforeExecCommand', (e) => { cmd = e.command; ui = e.ui; value = e.value; e.preventDefault(); }); editor.setContent(''); editor.insertContent('a'); assert.equal(editor.getContent(), '', 'BeforeExecCommand'); assert.equal(cmd, 'mceInsertContent', 'BeforeExecCommand'); assert.isFalse(ui, 'BeforeExecCommand'); assert.equal(value, 'a', 'BeforeExecCommand'); editor.off('BeforeExecCommand'); editor.setContent(''); editor.insertContent('a'); assert.equal(editor.getContent(), '

a

', 'BeforeExecCommand'); }); it('TBA: urls - relativeURLs', () => { const editor = hook.editor(); editor.options.set('relative_urls', true); editor.documentBaseURI = new URI('http://www.site.com/dirA/dirB/dirC/'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - relativeURLs'); }); it('TBA: urls - absoluteURLs', () => { const editor = hook.editor(); editor.options.set('relative_urls', false); editor.options.set('remove_script_host', true); editor.documentBaseURI = new URI('http://www.site.com/dirA/dirB/dirC/'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.options.set('relative_urls', false); editor.options.set('remove_script_host', false); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'urls - absoluteURLs'); }); it('TBA: WebKit Serialization range bug', function () { if (!(browser.isChromium() || browser.isSafari())) { this.skip(); } const editor = hook.editor(); // Note that if we create the P with this invalid content directly, Chrome cleans it up differently to other browsers so we don't // wind up testing the serialization functionality we were aiming for and the test fails. const p = editor.dom.create('p', {}, '123
X
456'); editor.dom.replace(p, editor.getBody().firstChild); assert.equal(editor.getContent(), '

123

X

456

', 'WebKit Serialization range bug'); }); it('TBA: editor_methods - getParam', () => { const editor = hook.editor(); assert.isUndefined(editor.getParam('test1'), 'unregistered with no default'); assert.equal(editor.getParam('test2', ''), '', 'unregistered with default'); assert.equal(editor.getParam('test2', 'blah'), 'blah', 'unregistered with different default'); assert.equal(editor.getParam('custom_prop1', 10, 'number'), 5, 'unregistered with correct type'); assert.equal(editor.getParam('custom_prop2', '10', 'string'), '10', 'unregistered with incorrect type'); editor.options.register('test4', { processor: 'string', default: 'default' }); assert.equal(editor.getParam('test4'), 'default', 'registered with no passed default'); assert.equal(editor.getParam('test4', 'override'), 'override', 'registered with passed default'); }); it('TBA: setContent', () => { const editor = hook.editor(); let count: number; const callback = (e: EditorEvent) => { e.content = e.content.replace(/test/, 'X'); count++; }; editor.on('SetContent', callback); editor.on('BeforeSetContent', callback); count = 0; editor.setContent('

test

'); assert.equal(editor.getContent(), '

X

', 'setContent'); assert.equal(count, 2, 'setContent'); editor.off('SetContent', callback); editor.off('BeforeSetContent', callback); count = 0; editor.setContent('

test

'); assert.equal(editor.getContent(), '

test

', 'setContent'); assert.equal(count, 0, 'setContent'); }); it('TBA: setContent with comment bug #4409', () => { const editor = hook.editor(); editor.setContent('
'); editor.options.set('disable_nodechange', false); editor.nodeChanged(); editor.options.set('disable_nodechange', true); assert.equal(editor.getContent(), '

\u00a0

', 'setContent with comment bug #4409'); }); it('TBA: custom elements', () => { const editor = hook.editor(); editor.setContent('c1c1'); assert.equal(editor.getContent(), 'c1

c1

', 'custom elements'); }); it('TBA: Store/restore tabindex', () => { const editor = hook.editor(); editor.setContent('abc'); assert.equal(editor.getContent({ format: 'raw' }).toLowerCase(), '

abc

', 'Store/restore tabindex'); assert.equal(editor.getContent(), '

abc

', 'Store/restore tabindex'); }); it('TBA: show/hide/isHidden and events', () => { const editor = hook.editor(); let lastEvent: EditorEvent<{}> | undefined; editor.on('show hide', (e) => { lastEvent = e; }); assert.isFalse(editor.isHidden(), 'Initial isHidden state'); editor.hide(); assert.isTrue(editor.isHidden(), 'After hide isHidden state'); assert.equal(lastEvent?.type, 'hide', 'show/hide/isHidden and events'); lastEvent = undefined; editor.hide(); assert.isUndefined(lastEvent, 'show/hide/isHidden and events'); editor.show(); assert.isFalse(editor.isHidden(), 'After show isHidden state'); assert.equal((lastEvent as unknown as EditorEvent<{}>).type, 'show', 'show/hide/isHidden and events'); lastEvent = undefined; editor.show(); assert.isUndefined(lastEvent, 'show/hide/isHidden and events'); }); it('TBA: hide save content and hidden state while saving', () => { const editor = hook.editor(); let lastEvent: EditorEvent | undefined; let hiddenStateWhileSaving: boolean | undefined; editor.on('SaveContent', (e) => { lastEvent = e; hiddenStateWhileSaving = editor.isHidden(); }); editor.setContent('xyz'); editor.hide(); const elm: any = document.getElementById(editor.id); assert.isFalse(hiddenStateWhileSaving, 'False isHidden state while saving'); assert.equal(lastEvent?.content, '

xyz

', 'hide save content and hidden state while saving'); assert.equal(elm.value, '

xyz

', 'hide save content and hidden state while saving'); editor.show(); }); it('TBA: insertContent', () => { const editor = hook.editor(); editor.setContent('

ab

'); TinySelections.setCursor(editor, [ 0, 0 ], 1); editor.insertContent('c'); assert.equal(editor.getContent(), '

acb

', 'insertContent'); }); it('TBA: insertContent merge', () => { const editor = hook.editor(); editor.setContent('

a

'); TinySelections.setCursor(editor, [ 0, 0 ], 1); editor.insertContent('b', { merge: true }); assert.equal(editor.getContent(), '

ab

', 'insertContent merge'); }); it('TBA: addCommand', () => { const editor = hook.editor(); const scope = {}; let lastScope: {} | undefined; let lastArgs: IArguments | undefined; const callback = function (this: {}) { // Arrow function cannot be used with 'arguments'. // eslint-disable-next-line lastScope = this; lastArgs = arguments; }; editor.addCommand('CustomCommand1', callback, scope); editor.addCommand('CustomCommand2', callback); editor.execCommand('CustomCommand1', false, 'value'); assert.isFalse(lastArgs?.[0], 'ui'); assert.equal(lastArgs?.[1], 'value', 'value'); assert.strictEqual(lastScope, scope, 'scope'); editor.execCommand('CustomCommand2'); assert.isFalse(lastArgs?.[0], 'ui'); assert.isUndefined(lastArgs?.[1], 'value'); assert.strictEqual(lastScope, editor, 'scope'); }); it('TBA: addQueryStateHandler', () => { const editor = hook.editor(); const scope = {}; let lastScope: {} | undefined; let currentState: boolean; const callback = function (this: {}) { // Arrow function cannot be used with 'this'. // eslint-disable-next-line lastScope = this; return currentState; }; editor.addQueryStateHandler('CustomCommand1', callback, scope); editor.addQueryStateHandler('CustomCommand2', callback); currentState = false; assert.equal(false, editor.queryCommandState('CustomCommand1'), 'addQueryStateHandler'); assert.equal(true, lastScope === scope, 'Scope is not custom scope'); currentState = true; assert.equal(true, editor.queryCommandState('CustomCommand2'), 'addQueryStateHandler'); assert.equal(true, lastScope === editor, 'Scope is not editor'); }); it('TBA: Block script execution', () => { const editor = hook.editor(); editor.setContent('

x

'); assert.equal( HtmlUtils.cleanHtml(editor.getBody().innerHTML), '' + '' + '' + '

x

', 'Block script execution' ); assert.equal( editor.getContent(), '' + '' + '' + '

x

', 'Block script execution' ); }); it('TBA: addQueryValueHandler', () => { const editor = hook.editor(); const scope = {}; let lastScope: {} | undefined; let currentValue: string; const callback = function (this: {}) { // Arrow function cannot be used with 'this'. // eslint-disable-next-line lastScope = this; return currentValue; }; editor.addQueryValueHandler('CustomCommand1', callback, scope); editor.addQueryValueHandler('CustomCommand2', callback); currentValue = 'a'; assert.equal(editor.queryCommandValue('CustomCommand1'), 'a', 'addQueryValueHandler'); assert.strictEqual(lastScope, scope, 'Scope is not custom scope'); currentValue = 'b'; assert.equal(editor.queryCommandValue('CustomCommand2'), 'b', 'addQueryValueHandler'); assert.strictEqual(lastScope, editor, 'Scope is not editor'); }); it('TBA: setDirty/isDirty', () => { const editor = hook.editor(); let lastArgs: EditorEvent<{}> | undefined; editor.on('dirty', (e) => { lastArgs = e; }); editor.setDirty(false); assert.isUndefined(lastArgs, 'setDirty/isDirty'); assert.isFalse(editor.isDirty(), 'setDirty/isDirty'); editor.setDirty(true); assert.equal(lastArgs?.type, 'dirty', 'setDirty/isDirty'); assert.isTrue( editor.isDirty(), 'setDirty/isDirty'); lastArgs = undefined; editor.setDirty(true); assert.isUndefined(lastArgs, 'setDirty/isDirty'); assert.isTrue(editor.isDirty(), 'setDirty/isDirty'); editor.setDirty(false); assert.isUndefined(lastArgs, 'setDirty/isDirty'); assert.isFalse(editor.isDirty(), 'setDirty/isDirty'); }); it('TBA: setMode', () => { const editor = hook.editor(); let clickCount = 0; const isDisabled = (selector: string) => { const elm = UiFinder.findIn(SugarBody.body(), selector); return elm.forall((elm) => Attribute.has(elm, 'disabled') || Class.has(elm, 'tox-tbtn--disabled')); }; editor.on('click', () => { clickCount++; }); editor.dom.dispatch(editor.getBody(), 'click'); assert.equal(clickCount, 1, 'setMode'); editor.mode.set('readonly'); assert.isTrue(isDisabled('.tox-editor-container button:last-of-type'), 'setMode'); editor.dom.dispatch(editor.getBody(), 'click'); assert.equal(clickCount, 1, 'setMode'); editor.mode.set('design'); editor.dom.dispatch(editor.getBody(), 'click'); assert.isFalse(isDisabled('.tox-editor-container button:last-of-type'), 'setMode'); assert.equal(clickCount, 2, 'setMode'); }); it('TBA: translate', () => { const editor = hook.editor(); EditorManager.addI18n('en', { 'input i18n': 'output i18n', 'value:{0}{1}': 'value translation:{0}{1}' }); assert.equal(editor.translate('input i18n'), 'output i18n', 'translate'); assert.equal(editor.translate([ 'value:{0}{1}', 'a', 'b' ]), 'value translation:ab', 'translate'); }); it('TBA: Treat some paragraphs as empty contents', () => { const editor = hook.editor(); editor.setContent('


'); assert.equal(editor.getContent(), '', 'Treat some paragraphs as empty contents'); editor.setContent('

\u00a0

'); assert.equal(editor.getContent(), '', 'Treat some paragraphs as empty contents'); }); it('TBA: kamer word boundaries', () => { const editor = hook.editor(); editor.setContent('

!\u200b!\u200b!

'); assert.equal(editor.getContent(), '

!\u200b!\u200b!

', 'kamer word boundaries'); }); it('TBA: Preserve whitespace pre elements', () => { const editor = hook.editor(); editor.setContent('
 
'); assert.equal(editor.getContent(), '
 
', 'kamer word boundaries'); }); it('TBA: hasFocus', () => { const editor = hook.editor(); editor.focus(); assert.isTrue(editor.hasFocus(), 'hasFocus'); const input = document.createElement('input'); document.body.appendChild(input); input.focus(); assert.isFalse(editor.hasFocus(), 'hasFocus'); editor.focus(); assert.isTrue(editor.hasFocus(), 'hasFocus'); input.parentNode?.removeChild(input); }); it('TINY-6946: Images should be properly cleaned up if they contain invalid trailing data', () => { const editor = hook.editor(); editor.setContent(''); TinyAssertions.assertContent(editor, '

'); }); it('TINY-6946: Images should cut off invalid data, even if the image remains invalid', () => { const editor = hook.editor(); editor.setContent(''); TinyAssertions.assertContent(editor, '

'); }); context('hasPlugin', () => { const checkWithoutManager = (title: string, plugins: string, plugin: string, expected: boolean) => { const editor = hook.editor(); editor.options.set('plugins', plugins.split(/[ ,]/)); assert.equal(editor.hasPlugin(plugin), expected, title); }; const checkWithManager = (title: string, plugins: string, plugin: string, addToManager: boolean, expected: boolean) => { const editor = hook.editor(); if (addToManager) { PluginManager.add('ParticularPlugin', Fun.noop); } editor.options.set('plugins', plugins.split(/[ ,]/)); assert.equal(editor.hasPlugin(plugin, true), expected, title); if (addToManager) { PluginManager.remove('ParticularPlugin'); } }; it('TINY-766: Checking without requiring a plugin to be loaded', () => { checkWithoutManager('Plugin does not exist', 'Plugin Is Not Here', 'ParticularPlugin', false); checkWithoutManager('Plugin does exist with spaces', 'Has ParticularPlugin In List', 'ParticularPlugin', true); checkWithoutManager('Plugin does exist with commas', 'Has,ParticularPlugin,In,List', 'ParticularPlugin', true); checkWithoutManager('Plugin does exist with spaces and commas', 'Has, ParticularPlugin, In, List', 'ParticularPlugin', true); checkWithoutManager('Plugin does not patch to OtherPlugin', 'Has OtherPlugin In List', 'Plugin', false); }); it('TINY-766: Checking while requiring a plugin to be loaded', () => { checkWithManager('Plugin does not exist', 'Plugin Is Not Here', 'ParticularPlugin', true, false); checkWithManager('Plugin does exist with spaces', 'Has ParticularPlugin In List', 'ParticularPlugin', true, true); checkWithManager('Plugin does exist with commas', 'Has,ParticularPlugin,In,List', 'ParticularPlugin', true, true); checkWithManager('Plugin does exist with spaces and commas', 'Has, ParticularPlugin, In, List', 'ParticularPlugin', true, true); checkWithManager('Plugin does not patch to OtherPlugin', 'Has OtherPlugin In List', 'Plugin', true, false); checkWithManager('Plugin which has not loaded does not return true', 'Has ParticularPlugin In List', 'ParticularPlugin', false, false); }); }); });