/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* global document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import UndoEditing from '../src/undoediting'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting'; import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'UndoEditing integration', () => { let editor, model, doc, root, div; beforeEach( () => { div = document.createElement( 'div' ); document.body.appendChild( div ); return ClassicEditor.create( div, { plugins: [ Paragraph, HeadingEditing, Typing, Enter, Clipboard, BoldEditing, UndoEditing, TableEditing ] } ).then( newEditor => { editor = newEditor; model = editor.model; doc = model.document; root = doc.getRoot(); } ); } ); afterEach( () => { div.remove(); return editor.destroy(); } ); function setSelection( pathA, pathB ) { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, pathA ), writer.createPositionFromPath( root, pathB ) ) ); } ); } function input( input ) { model.enqueueChange( 'transparent', () => { setData( model, input ); } ); } function output( output ) { expect( getData( model ) ).to.equal( output ); } function undoDisabled() { expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false; } function redoDisabled() { expect( editor.commands.get( 'redo' ).isEnabled ).to.be.false; } describe( 'adding and removing content', () => { it( 'add and undo', () => { input( 'fo[]obar' ); model.change( writer => { writer.insertText( 'zzz', doc.selection.getFirstPosition() ); } ); output( 'fozzz[]obar' ); editor.execute( 'undo' ); output( 'fo[]obar' ); undoDisabled(); } ); it( 'multiple adding and undo', () => { input( 'fo[]obar' ); model.change( writer => { writer.insertText( 'zzz', doc.selection.getFirstPosition() ); writer.insertText( 'xxx', writer.createPositionFromPath( root, [ 1, 0 ] ) ); } ); output( 'fozzz[]oxxxbar' ); model.change( writer => { setSelection( [ 1, 0 ], [ 1, 0 ] ); writer.insertText( 'yyy', doc.selection.getFirstPosition() ); } ); output( 'fozzzoyyy[]xxxbar' ); editor.execute( 'undo' ); output( 'fozzzo[]xxxbar' ); editor.execute( 'undo' ); output( 'fo[]obar' ); undoDisabled(); } ); it( 'multiple adding mixed with undo', () => { input( 'fo[]obar' ); model.change( writer => { writer.insertText( 'zzz', doc.selection.getFirstPosition() ); } ); output( 'fozzz[]obar' ); model.change( writer => { setSelection( [ 1, 0 ], [ 1, 0 ] ); writer.insertText( 'yyy', doc.selection.getFirstPosition() ); } ); output( 'fozzzoyyy[]bar' ); editor.execute( 'undo' ); output( 'fozzzo[]bar' ); model.change( writer => { setSelection( [ 0, 0 ], [ 0, 0 ] ); writer.insertText( 'xxx', doc.selection.getFirstPosition() ); } ); output( 'xxx[]fozzzobar' ); editor.execute( 'undo' ); output( '[]fozzzobar' ); editor.execute( 'undo' ); output( 'fo[]obar' ); undoDisabled(); } ); it( 'multiple remove and undo', () => { input( '[]foobar' ); model.change( writer => { const start = doc.selection.getFirstPosition(); writer.remove( writer.createRange( start, start.getShiftedBy( 2 ) ) ); } ); output( '[]obar' ); model.change( writer => { setSelection( [ 1, 1 ], [ 1, 1 ] ); const start = doc.selection.getFirstPosition(); writer.remove( writer.createRange( start, start.getShiftedBy( 2 ) ) ); } ); output( 'ob[]' ); editor.execute( 'undo' ); // Here is an edge case that selection could be before or after `ar`. output( 'obar[]' ); editor.execute( 'undo' ); // As above. output( 'fo[]obar' ); undoDisabled(); } ); it( 'add and remove different parts and undo', () => { input( 'fo[]obar' ); model.change( writer => { writer.insertText( 'zzz', doc.selection.getFirstPosition() ); } ); output( 'fozzz[]obar' ); model.change( writer => { setSelection( [ 1, 2 ], [ 1, 2 ] ); const start = writer.createPositionFromPath( root, [ 1, 1 ] ); writer.remove( writer.createRange( start, start.getShiftedBy( 1 ) ) ); } ); output( 'fozzzob[]r' ); editor.execute( 'undo' ); output( 'fozzzoba[]r' ); editor.execute( 'undo' ); output( 'fo[]obar' ); undoDisabled(); } ); it( 'add and remove same part and undo', () => { input( 'fo[]obar' ); model.change( writer => { writer.insertText( 'zzz', doc.selection.getFirstPosition() ); } ); output( 'fozzz[]obar' ); model.change( writer => { const start = writer.createPositionFromPath( root, [ 0, 2 ] ); writer.remove( writer.createRange( start, start.getShiftedBy( 3 ) ) ); } ); output( 'fo[]obar' ); editor.execute( 'undo' ); output( 'fozzz[]obar' ); editor.execute( 'undo' ); output( 'fo[]obar' ); undoDisabled(); } ); it( 'undo remove all content', () => { input( 'foo[]' ); model.change( writer => { writer.remove( writer.createRangeIn( root ) ); } ); output( '[]' ); // All hail our king and savior, autoparagraphing! editor.execute( 'undo' ); output( 'foo[]' ); undoDisabled(); } ); it( 'undo insert first content', () => { input( '' ); output( '[]' ); // All hail our king and savior, autoparagraphing! model.change( writer => { writer.remove( writer.createRangeIn( root ) ); writer.insertElement( 'heading1', doc.selection.getFirstPosition() ); } ); output( '[]' ); editor.execute( 'undo' ); output( '[]' ); undoDisabled(); } ); // #72. it( 'undo paste multiple elements simulation', () => { input( '' ); const p = root.getChild( 0 ); const pos = model.createPositionFromPath( root, [ 0 ] ); model.change( writer => { writer.remove( p ); writer.insertElement( 'heading1', pos ); writer.insertElement( 'heading2', pos.getShiftedBy( 1 ) ); } ); output( '[]' ); editor.execute( 'undo' ); output( '[]' ); undoDisabled(); } ); } ); describe( 'moving', () => { it( 'move same content twice then undo', () => { input( 'f[o]zbar' ); model.change( writer => { writer.move( doc.selection.getFirstRange(), writer.createPositionFromPath( root, [ 1, 0 ] ) ); } ); output( 'fz[o]bar' ); model.change( writer => { writer.move( doc.selection.getFirstRange(), writer.createPositionFromPath( root, [ 0, 2 ] ) ); } ); output( 'fz[o]bar' ); editor.execute( 'undo' ); output( 'fz[o]bar' ); editor.execute( 'undo' ); output( 'f[o]zbar' ); undoDisabled(); } ); it( 'move content and new parent then undo', () => { input( 'f[o]zbar' ); model.change( writer => { writer.move( doc.selection.getFirstRange(), writer.createPositionFromPath( root, [ 1, 0 ] ) ); } ); output( 'fz[o]bar' ); model.change( writer => { setSelection( [ 1 ], [ 2 ] ); writer.move( doc.selection.getFirstRange(), writer.createPositionFromPath( root, [ 0 ] ) ); } ); output( '[obar]fz' ); editor.execute( 'undo' ); output( 'fz[obar]' ); editor.execute( 'undo' ); output( 'f[o]zbar' ); undoDisabled(); } ); } ); describe( 'attributes with other', () => { it( 'attributes then insert inside then undo', () => { input( 'fo[ob]ar' ); model.change( writer => { writer.setAttribute( 'bold', true, doc.selection.getFirstRange() ); } ); output( 'fo[<$text bold="true">ob]ar' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.true; model.change( writer => { setSelection( [ 0, 3 ], [ 0, 3 ] ); writer.insertText( 'zzz', doc.selection.getFirstPosition() ); } ); output( 'fo<$text bold="true">ozzz[]<$text bold="true">bar' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.undefined; editor.execute( 'undo' ); output( 'fo<$text bold="true">o[]bar' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.true; editor.execute( 'undo' ); output( 'fo[ob]ar' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.undefined; undoDisabled(); } ); } ); describe( 'wrapping, unwrapping, merging, splitting', () => { it( 'wrap and undo', () => { model.schema.extend( '$text', { allowIn: '$root' } ); input( 'fo[zb]ar' ); model.change( writer => { writer.wrap( doc.selection.getFirstRange(), 'paragraph' ); } ); output( 'fo[zb]ar' ); editor.execute( 'undo' ); output( 'fo[zb]ar' ); undoDisabled(); } ); it( 'wrap, move and undo', () => { model.schema.extend( '$text', { allowIn: '$root' } ); input( 'fo[zb]ar' ); model.change( writer => { writer.wrap( doc.selection.getFirstRange(), 'paragraph' ); } ); // Would be better if selection was inside P. output( 'fo[zb]ar' ); model.change( writer => { setSelection( [ 2, 0 ], [ 2, 1 ] ); writer.move( doc.selection.getFirstRange(), writer.createPositionFromPath( root, [ 0 ] ) ); } ); output( '[z]fobar' ); editor.execute( 'undo' ); output( 'fo[z]bar' ); editor.execute( 'undo' ); output( 'fo[zb]ar' ); undoDisabled(); } ); it( 'unwrap and undo', () => { input( 'foo[]bar' ); model.change( writer => { writer.unwrap( doc.selection.getFirstPosition().parent ); } ); output( 'foo[]bar' ); editor.execute( 'undo' ); output( 'foo[]bar' ); undoDisabled(); } ); it( 'merge and undo', () => { input( 'foo[]bar' ); model.change( writer => { writer.merge( writer.createPositionFromPath( root, [ 1 ] ) ); } ); output( 'foo[]bar' ); editor.execute( 'undo' ); output( 'foo[]bar' ); undoDisabled(); } ); it( 'split and undo', () => { input( 'foo[]bar' ); model.change( writer => { writer.split( doc.selection.getFirstPosition() ); } ); output( 'foo[]bar' ); editor.execute( 'undo' ); output( 'foo[]bar' ); undoDisabled(); } ); it.skip( 'move, merge and undo', () => { // Add a "div feature". model.schema.register( 'div', { allowWhere: '$block', allowContentOf: '$root' } ); editor.conversion.for( 'downcast' ).elementToElement( { model: 'div', view: 'div' } ); editor.conversion.for( 'upcast' ).elementToElement( { model: 'div', view: 'div' } ); input( '
[]00.01.02.
' + '
10.11.12.
' ); const elements = [ root.getNodeByPath( [ 0, 1 ] ), root.getNodeByPath( [ 1, 0 ] ), root.getNodeByPath( [ 1, 1 ] ) ]; model.change( writer => { for ( let i = elements.length - 1; i > 0; i-- ) { merge( elements[ i ], elements[ i - 1 ], writer ); } } ); output( '
[]00.01.10.11.02.
' + '
12.
' ); editor.execute( 'undo' ); output( '
[]00.01.02.
' + '
10.11.12.
' ); function merge( element, targetElement, writer ) { const positionAfterTarget = writer.createPositionAfter( targetElement ); const positionBeforeElement = writer.createPositionBefore( element ); if ( !positionAfterTarget.isEqual( positionBeforeElement ) ) { writer.move( writer.createRangeOn( element ), positionAfterTarget ); } writer.merge( positionAfterTarget ); } } ); } ); // Restoring selection in those examples may be completely off. describe( 'multiple enters, deletes and typing', () => { it( 'split, split, split', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); split( [ 1, 4 ] ); output( '1234567[]8' ); split( [ 1, 2 ] ); output( '12345[]678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '1234567[]8' ); editor.execute( 'redo' ); output( '12345[]678' ); redoDisabled(); } ); it( 'merge, merge, merge', () => { input( '12345678' ); merge( [ 1 ] ); output( '123[]45678' ); merge( [ 2 ] ); output( '1234567[]8' ); merge( [ 1 ] ); output( '12345[]678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '1234567[]8' ); editor.execute( 'redo' ); output( '12345[]678' ); redoDisabled(); } ); it( 'split, merge, split, merge (same position)', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); merge( [ 1 ] ); output( '123[]45678' ); split( [ 0, 3 ] ); output( '123[]45678' ); merge( [ 1 ] ); output( '123[]45678' ); editor.execute( 'undo' ); output( '123[]45678' ); editor.execute( 'undo' ); output( '123[]45678' ); editor.execute( 'undo' ); output( '123[]45678' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '123[]45678' ); redoDisabled(); } ); it( 'split, split, split, merge, merge, merge', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); split( [ 1, 4 ] ); output( '1234567[]8' ); split( [ 1, 2 ] ); output( '12345[]678' ); merge( [ 1 ] ); output( '123[]45678' ); merge( [ 2 ] ); output( '1234567[]8' ); merge( [ 1 ] ); output( '12345[]678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '1234567[]8' ); editor.execute( 'redo' ); output( '12345[]678' ); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '1234567[]8' ); editor.execute( 'redo' ); output( '12345[]678' ); redoDisabled(); } ); it( 'split, split, merge, split, merge (different order)', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); split( [ 1, 2 ] ); output( '12345[]678' ); merge( [ 1 ] ); output( '123[]45678' ); split( [ 1, 1 ] ); output( '123456[]78' ); merge( [ 1 ] ); output( '12345[]678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '123456[]78' ); editor.execute( 'undo' ); output( '123[]45678' ); editor.execute( 'undo' ); output( '12345[]678' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '12345[]678' ); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '123456[]78' ); editor.execute( 'redo' ); output( '12345[]678' ); redoDisabled(); } ); it( 'split, remove, split, merge, merge', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); remove( [ 1, 4 ] ); remove( [ 1, 3 ] ); output( '12345[]8' ); split( [ 1, 1 ] ); output( '1234[]58' ); merge( [ 1 ] ); output( '123[]458' ); merge( [ 1 ] ); output( '1234[]58' ); editor.execute( 'undo' ); output( '1234[]58' ); editor.execute( 'undo' ); output( '123[]458' ); editor.execute( 'undo' ); output( '1234[]58' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '12345[]8' ); editor.execute( 'redo' ); output( '1234[]58' ); editor.execute( 'redo' ); output( '123[]458' ); editor.execute( 'redo' ); output( '1234[]58' ); redoDisabled(); } ); it( 'split, typing, split, merge, merge', () => { input( '12345678' ); split( [ 0, 3 ] ); output( '123[]45678' ); type( [ 1, 4 ], 'x' ); type( [ 1, 5 ], 'y' ); output( '1234567xy[]8' ); split( [ 1, 2 ] ); output( '12345[]67xy8' ); merge( [ 1 ] ); output( '123[]4567xy8' ); merge( [ 1 ] ); output( '12345[]67xy8' ); editor.execute( 'undo' ); output( '12345[]67xy8' ); editor.execute( 'undo' ); output( '123[]4567xy8' ); editor.execute( 'undo' ); output( '12345[]67xy8' ); editor.execute( 'undo' ); output( '1234567[]8' ); editor.execute( 'undo' ); output( '123[]45678' ); undoDisabled(); editor.execute( 'redo' ); output( '123[]45678' ); editor.execute( 'redo' ); output( '1234567xy[]8' ); editor.execute( 'redo' ); output( '12345[]67xy8' ); editor.execute( 'redo' ); output( '123[]4567xy8' ); editor.execute( 'redo' ); output( '12345[]67xy8' ); redoDisabled(); } ); } ); describe( 'other reported cases', () => { // ckeditor5-engine#t/1051 it( 'rename leaks to other elements on undo #1', () => { input( '[]FooBar' ); model.change( writer => { writer.rename( root.getChild( 0 ), 'paragraph' ); } ); output( '[]FooBar' ); model.change( writer => { writer.split( writer.createPositionAt( root.getChild( 0 ), 1 ) ); } ); output( '[]FooBar' ); model.change( writer => { writer.merge( writer.createPositionAt( root, 2 ) ); } ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); } ); // Similar issue that bases on the same error as above, however here we first merge (above we first split). it( 'rename leaks to other elements on undo #2', () => { input( '[]FooBar' ); model.change( writer => { writer.rename( root.getChild( 0 ), 'heading2' ); } ); output( '[]FooBar' ); model.change( writer => { writer.merge( writer.createPositionAt( root, 1 ) ); } ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); } ); // Reverse issue, this time first operation is merge and then rename. it( 'merge, rename, undo, undo is correct', () => { input( '[]FooBar' ); model.change( writer => { writer.merge( writer.createPositionAt( root, 1 ) ); } ); output( '[]FooBar' ); model.change( writer => { writer.rename( root.getChild( 0 ), 'heading2' ); } ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); editor.execute( 'undo' ); output( '[]FooBar' ); } ); // ckeditor5-engine#t/1053 it( 'wrap, split, undo, undo is correct', () => { // Add a "div feature". model.schema.register( 'div', { allowWhere: '$block', allowContentOf: '$root' } ); editor.conversion.for( 'downcast' ).elementToElement( { model: 'div', view: 'div' } ); editor.conversion.for( 'upcast' ).elementToElement( { model: 'div', view: 'div' } ); input( '[]FooBar' ); model.change( writer => { writer.wrap( writer.createRangeIn( root ), 'div' ); } ); output( '
[]FooBar
' ); model.change( writer => { writer.split( writer.createPositionFromPath( root, [ 0, 0, 1 ] ) ); } ); output( '
[]FooBar
' ); editor.execute( 'undo' ); output( '
[]FooBar
' ); editor.execute( 'undo' ); output( '[]FooBar' ); } ); // ckeditor5-engine#t/1055 it( 'selection attribute setting: split, bold, merge, undo, undo, undo', () => { input( 'Foo[]Bar' ); editor.execute( 'enter' ); output( 'Foo[]Bar' ); editor.execute( 'bold' ); output( 'Foo' + '<$text bold="true">[]' + 'Bar' ); editor.execute( 'forwardDelete' ); output( 'Foo[]Bar' ); editor.execute( 'undo' ); output( 'Foo' + '<$text bold="true">[]' + 'Bar' ); editor.execute( 'undo' ); output( 'Foo[]Bar' ); editor.execute( 'undo' ); output( 'Foo[]Bar' ); } ); // https://github.com/ckeditor/ckeditor5-undo/issues/65#issuecomment-323682195 it( 'undoing split after the element created by split has been removed', () => { input( 'Foo[]bar' ); editor.execute( 'enter' ); model.change( writer => { const range = writer.createRange( writer.createPositionFromPath( root, [ 0, 3 ] ), writer.createPositionFromPath( root, [ 1, 3 ] ) ); writer.setSelection( range ); editor.execute( 'delete' ); } ); output( 'Foo[]' ); editor.execute( 'undo' ); output( 'Foo[bar]' ); editor.execute( 'undo' ); output( 'Foo[]bar' ); } ); } ); describe( 'clipboard', () => { function pasteHtml( editor, html ) { editor.editing.view.document.fire( 'paste', { dataTransfer: createDataTransfer( { 'text/html': html } ), stopPropagation() {}, preventDefault() {} } ); } function createDataTransfer( data ) { return { getData( type ) { return data[ type ]; }, setData() {} }; } // ckeditor5-engine#t/1065 it( 'undo paste into non empty element should not throw and be correct', () => { model.change( () => { input( 'Foo[]' ); } ); model.change( () => { pasteHtml( editor, '

a

b

' ); } ); output( 'Fooab[]' ); model.change( () => { pasteHtml( editor, '

c

d

' ); } ); output( 'Fooabcd[]' ); editor.execute( 'undo' ); output( 'Fooab[]' ); editor.execute( 'undo' ); output( 'Foo[]' ); } ); // ckeditor5#781 it( 'cutting should not create empty undo step', () => { input( 'Fo[oba]r' ); editor.editing.view.document.fire( 'cut', { dataTransfer: createDataTransfer(), preventDefault() {}, method: 'cut' } ); editor.execute( 'undo' ); output( 'Fo[oba]r' ); undoDisabled(); } ); } ); describe( 'other edge cases', () => { it( 'deleteContent between two nodes', () => { input( 'fo[ob]ar' ); editor.model.deleteContent( doc.selection ); output( 'fo[]ar' ); editor.execute( 'undo' ); output( 'fo[ob]ar' ); } ); // Related to ckeditor5-engine#891 and ckeditor5-list#51. it( 'change attribute of removed node then undo and redo', () => { input( '' ); const gy = doc.graveyard; const p = doc.getRoot().getChild( 0 ); model.change( writer => { writer.remove( p ); } ); model.change( writer => { writer.setAttribute( 'bold', true, p ); } ); editor.execute( 'undo' ); editor.execute( 'redo' ); expect( p.root ).to.equal( gy ); expect( p.getAttribute( 'bold' ) ).to.be.true; } ); it( 'undo table cells merge', () => { input( '' + '' + '00' + '[01]' + '[02]' + '03' + '' + '' + '10' + '[11]' + '[12]' + '13' + '' + '' + '20' + '21' + '22' + '23' + '' + '
' ); editor.execute( 'mergeTableCells' ); output( '' + '' + '00' + '' + '[01' + '02' + '11' + '12]' + '' + '03' + '' + '' + '10' + '13' + '' + '' + '20' + '21' + '22' + '23' + '' + '
' ); editor.execute( 'undo' ); output( '' + '' + '00' + '[01]' + '[02]' + '03' + '' + '' + '10' + '[11]' + '[12]' + '13' + '' + '' + '20' + '21' + '22' + '23' + '' + '
' ); } ); } ); it( 'postfixers should not add another undo step when fixing undo changes', () => { input( '[]' ); const paragraph = root.getChild( 0 ); // 1. Step - add text and marker to the paragraph. model.change( writer => { writer.appendText( 'foo', paragraph ); writer.addMarker( 'marker', { range: writer.createRangeIn( paragraph ), usingOperation: true } ); } ); // 2. Step - remove text from paragraph. model.change( writer => { writer.remove( writer.createRangeIn( paragraph ) ); } ); // Register post fixer to remove marker from non-empty paragraph. model.document.registerPostFixer( writer => { const hasMarker = model.markers.has( 'marker' ); const isEmpty = paragraph.childCount === 0; // Remove marker when paragraph is empty. if ( hasMarker && !isEmpty ) { writer.removeMarker( 'marker' ); return true; } return false; } ); editor.execute( 'undo' ); // Check if postfixer changes are executed together with undo and there is no additional undo steps. expect( model.markers.has( 'marker' ) ).to.be.false; expect( editor.commands.get( 'undo' )._stack.length ).to.equal( 1 ); output( 'foo[]' ); } ); function split( path ) { setSelection( path.slice(), path.slice() ); editor.execute( 'enter' ); } function merge( path ) { const selPath = path.slice(); selPath.push( 0 ); setSelection( selPath, selPath.slice() ); editor.execute( 'delete' ); } function type( path, text ) { setSelection( path.slice(), path.slice() ); editor.execute( 'input', { text } ); } function remove( path ) { setSelection( path.slice(), path.slice() ); editor.execute( 'delete' ); } } );