/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import Input from '../../src/input'; import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; /* global document */ // NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what // should be in "newChildren" because we don't have yet access to these nodes. We pass new instances, // but this means that DomConverter which is used somewhere internally may return a different instance // (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the // browser will keep the instances of the old elements when modifying the tree when the user is typing // or if it will create new instances itself too. // However, the code handling these mutations doesn't really care what's inside new/old children. It // just needs the mutations common ancestor to understand how big fragment of the tree has changed. describe( 'Bug ckeditor5-typing#100', () => { let domElement, domRoot, editor, model, view, viewDocument, viewRoot; beforeEach( () => { domElement = document.createElement( 'div' ); document.body.appendChild( domElement ); return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEditing ] } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; viewDocument = view.document; viewRoot = viewDocument.getRoot(); domRoot = view.getDomRoot(); // Mock image feature. newEditor.model.schema.register( 'image', { allowWhere: '$text' } ); editor.conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); editor.conversion.for( 'upcast' ).elementToElement( { view: 'img', model: 'image' } ); // Disable MO completely and in a way it won't be reenabled on some Document#render() call. const mutationObserver = view.getObserver( MutationObserver ); mutationObserver.disable(); mutationObserver.enable = () => {}; } ); } ); afterEach( () => { domElement.remove(); return editor.destroy(); } ); // This happens when browser automatically switches parent and child nodes. it( 'should handle mutations switching inner and outer node when adding new text node after', () => { setModelData( model, '' + '<$text italic="true" bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const italic = paragraph.getChild( 0 ); const strong = italic.getChild( 0 ); const text = strong.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'textx'; viewDocument.fire( 'mutations', [ // First mutation - remove all children from link element. { type: 'children', node: italic, oldChildren: [ strong ], newChildren: [] }, // Second mutation - remove link from paragraph and put italic there. { type: 'children', node: paragraph, oldChildren: [ italic ], newChildren: [ new ViewElement( viewDocument, 'i' ) ] }, // Third mutation - italic's new children. { type: 'children', node: strong, oldChildren: [ text ], newChildren: [ new ViewElement( viewDocument, 'a', null, text._clone() ), new ViewText( viewDocument, 'x' ) ] } ] ); expect( getViewData( view ) ).to.equal( '

textx{}

' ); } ); it( 'should handle mutations switching inner and outer node when adding new text node before', () => { setModelData( model, '' + '<$text italic="true" bold="true">' + '[]text' + '' + '' ); expect( getViewData( view ) ).to.equal( '

{}text

' ); const paragraph = viewRoot.getChild( 0 ); const italic = paragraph.getChild( 0 ); const strong = italic.getChild( 0 ); const text = strong.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'xtext'; viewDocument.fire( 'mutations', [ // First mutation - remove all children from link element. { type: 'children', node: italic, oldChildren: [ strong ], newChildren: [] }, // Second mutation - remove link from paragraph and put italic there. { type: 'children', node: paragraph, oldChildren: [ italic ], newChildren: [ new ViewElement( viewDocument, 'i' ) ] }, // Third mutation - italic's new children. { type: 'children', node: strong, oldChildren: [ text ], newChildren: [ new ViewText( viewDocument, 'x' ), new ViewElement( viewDocument, 'a', null, 'text' ) ] } ] ); expect( getViewData( view ) ).to.equal( '

x{}text

' ); } ); it( 'should handle mutations switching inner and outer node - with text before', () => { setModelData( model, '' + 'xxx<$text italic="true" bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

xxxtext{}

' ); const paragraph = viewRoot.getChild( 0 ); const textBefore = paragraph.getChild( 0 ); const link = paragraph.getChild( 1 ); const italic = link.getChild( 0 ); const text = italic.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'xxxtextx'; viewDocument.fire( 'mutations', [ // First mutation - remove all children from link element. { type: 'children', node: link, oldChildren: [ italic ], newChildren: [] }, // Second mutation - remove link from paragraph and put italic there. { type: 'children', node: paragraph, oldChildren: [ textBefore, link ], newChildren: [ new ViewText( viewDocument, 'xxx' ), new ViewElement( viewDocument, 'i' ) ] }, // Third mutation - italic's new children. { type: 'children', node: italic, oldChildren: [ text ], newChildren: [ new ViewElement( viewDocument, 'a', null, 'text' ), new ViewText( viewDocument, 'x' ) ] } ] ); expect( getViewData( view ) ).to.equal( '

xxxtextx{}

' ); } ); // This happens when spell checker is applied on element and changes it to . it( 'should handle mutations replacing node', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'fixed text'; viewDocument.fire( 'mutations', [ // Replace `` with ``. { type: 'children', node: paragraph, oldChildren: [ strong ], newChildren: [ new ViewElement( viewDocument, 'b', null, 'fixed text' ) ] } ] ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '

fixed text

' ); } ); // Spell checker splits text inside attributes to two text nodes. it( 'should handle mutations inside attribute element', () => { setModelData( model, '' + '<$text bold="true">' + 'this is foo text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

this is foo text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); const text = strong.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text'; viewDocument.fire( 'mutations', [ { type: 'children', node: strong, oldChildren: [ text ], newChildren: [ new ViewText( viewDocument, 'this is bar' ), new ViewText( viewDocument, ' text' ) ] } ] ); expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '

this is bar text

' ); } ); it( 'should do nothing if elements mutated', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'text'; viewDocument.fire( 'mutations', [ { type: 'children', node: paragraph, oldChildren: [ strong ], newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'text' ) ), new ViewElement( viewDocument, 'img' ) ] } ] ); expect( getViewData( view ) ).to.equal( '

text{}

' ); } ); it( 'should do nothing if text is not changed', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'text'; viewDocument.fire( 'mutations', [ { type: 'children', node: paragraph, oldChildren: [ strong ], newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'text' ) ) ] } ] ); expect( getViewData( view ) ).to.equal( '

text{}

' ); } ); it( 'should do nothing on empty mutations', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'text'; viewDocument.fire( 'mutations', [] ); expect( getViewData( view ) ).to.equal( '

text{}

' ); } ); it( 'should do nothing if mutations does not have common ancestor', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'text'; viewDocument.fire( 'mutations', [ { type: 'children', node: paragraph, oldChildren: [ strong ], newChildren: [ strong ] }, { type: 'children', node: new ViewContainerElement( 'div' ), oldChildren: [], newChildren: [ new ViewText( viewDocument, 'foo' ), new ViewText( viewDocument, 'bar' ) ] } ] ); expect( getViewData( view ) ).to.equal( '

text{}

' ); } ); it( 'should handle view selection if one is returned from mutations', () => { setModelData( model, '' + '<$text bold="true">' + 'text[]' + '' + '' ); expect( getViewData( view ) ).to.equal( '

text{}

' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); const viewSelection = view.createSelection( paragraph, 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'textx'; viewDocument.fire( 'mutations', [ // Replace `` with ``. { type: 'children', node: paragraph, oldChildren: [ strong ], newChildren: [ new ViewElement( viewDocument, 'b', null, new ViewText( viewDocument, 'textx' ) ) ] } ], viewSelection ); expect( getModelData( model ) ).to.equal( '<$text bold="true">[]textx' ); expect( getViewData( view ) ).to.equal( '

{}textx

' ); } ); // #117. it( 'should handle mixed mutations', () => { setModelData( model, '[]<$text bold="true">Foo bar aple' ); const paragraph = viewRoot.getChild( 0 ); const strong = paragraph.getChild( 0 ); const viewSelection = view.createSelection( paragraph, 0 ); // Simulate mutations and DOM change. domRoot.childNodes[ 0 ].innerHTML = 'Foo bar apple'; viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Foo bar aple', newText: 'Foo bar ', node: viewRoot.getChild( 0 ).getChild( 0 ) }, { type: 'children', oldChildren: [ strong ], newChildren: [ strong, new ViewElement( viewDocument, 'b', null, new ViewText( viewDocument, 'apple' ) ) ], node: paragraph } ], viewSelection ); expect( getModelData( model ) ).to.equal( '<$text bold="true">[]Foo bar apple' ); expect( getViewData( view ) ).to.equal( '

{}Foo bar apple

' ); } ); } );