/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Element from '@ckeditor/ckeditor5-engine/src/model/element'; import UndoEngine from '../src/undoengine'; import DeleteCommand from '@ckeditor/ckeditor5-typing/src/deletecommand'; import InputCommand from '@ckeditor/ckeditor5-typing/src/inputcommand'; import EnterCommand from '@ckeditor/ckeditor5-enter/src/entercommand'; import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'UndoEngine integration', () => { let editor, doc, root; beforeEach( () => { return ModelTestEditor.create( { plugins: [ UndoEngine ] } ) .then( newEditor => { editor = newEditor; editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) ); editor.commands.add( 'forwardDelete', new DeleteCommand( editor, 'forward' ) ); editor.commands.add( 'enter', new EnterCommand( editor ) ); editor.commands.add( 'input', new InputCommand( editor, 5 ) ); editor.commands.add( 'bold', new AttributeCommand( editor, 'bold' ) ); doc = editor.document; doc.schema.registerItem( 'p', '$block' ); doc.schema.registerItem( 'h1', '$block' ); doc.schema.registerItem( 'h2', '$block' ); doc.schema.allow( { name: '$inline', attributes: 'bold', inside: '$block' } ); doc.schema.registerItem( 'div', '$block' ); root = doc.getRoot(); } ); } ); function setSelection( pathA, pathB ) { doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] ); } function input( input ) { setData( doc, input ); } function output( output ) { expect( getData( doc ) ).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[]o

bar

' ); doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); output( '

fozzz[]o

bar

' ); editor.execute( 'undo' ); output( '

fo[]o

bar

' ); undoDisabled(); } ); it( 'multiple adding and undo', () => { input( '

fo[]o

bar

' ); doc.batch() .insert( doc.selection.getFirstPosition(), 'zzz' ) .insert( new Position( root, [ 1, 0 ] ), 'xxx' ); output( '

fozzz[]o

xxxbar

' ); setSelection( [ 1, 0 ], [ 1, 0 ] ); doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); output( '

fozzzo

yyy[]xxxbar

' ); editor.execute( 'undo' ); output( '

fozzzo

[]xxxbar

' ); editor.execute( 'undo' ); output( '

fo[]o

bar

' ); undoDisabled(); } ); it( 'multiple adding mixed with undo', () => { input( '

fo[]o

bar

' ); doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); output( '

fozzz[]o

bar

' ); setSelection( [ 1, 0 ], [ 1, 0 ] ); doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); output( '

fozzzo

yyy[]bar

' ); editor.execute( 'undo' ); output( '

fozzzo

[]bar

' ); setSelection( [ 0, 0 ], [ 0, 0 ] ); doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' ); output( '

xxx[]fozzzo

bar

' ); editor.execute( 'undo' ); output( '

[]fozzzo

bar

' ); editor.execute( 'undo' ); output( '

fo[]o

bar

' ); undoDisabled(); } ); it( 'multiple remove and undo', () => { input( '

[]foo

bar

' ); doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); output( '

[]o

bar

' ); setSelection( [ 1, 1 ], [ 1, 1 ] ); doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); output( '

o

b[]

' ); editor.execute( 'undo' ); // Here is an edge case that selection could be before or after `ar`. output( '

o

bar[]

' ); editor.execute( 'undo' ); // As above. output( '

fo[]o

bar

' ); undoDisabled(); } ); it( 'add and remove different parts and undo', () => { input( '

fo[]o

bar

' ); doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); output( '

fozzz[]o

bar

' ); setSelection( [ 1, 2 ], [ 1, 2 ] ); doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ), 1 ) ); output( '

fozzzo

b[]r

' ); editor.execute( 'undo' ); output( '

fozzzo

ba[]r

' ); editor.execute( 'undo' ); output( '

fo[]o

bar

' ); undoDisabled(); } ); it( 'add and remove same part and undo', () => { input( '

fo[]o

bar

' ); doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); output( '

fozzz[]o

bar

' ); doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ), 3 ) ); output( '

fo[]o

bar

' ); editor.execute( 'undo' ); output( '

fozzz[]o

bar

' ); editor.execute( 'undo' ); output( '

fo[]o

bar

' ); undoDisabled(); } ); } ); describe( 'moving', () => { it( 'move same content twice then undo', () => { input( '

f[o]z

bar

' ); doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); output( '

fz

[o]bar

' ); doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) ); output( '

fz[o]

bar

' ); editor.execute( 'undo' ); output( '

fz

[o]bar

' ); editor.execute( 'undo' ); output( '

f[o]z

bar

' ); undoDisabled(); } ); it( 'move content and new parent then undo', () => { input( '

f[o]z

bar

' ); doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); output( '

fz

[o]bar

' ); setSelection( [ 1 ], [ 2 ] ); doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); output( '[

obar

]

fz

' ); editor.execute( 'undo' ); output( '

fz

[

obar

]' ); editor.execute( 'undo' ); output( '

f[o]z

bar

' ); undoDisabled(); } ); } ); describe( 'attributes with other', () => { it( 'attributes then insert inside then undo', () => { input( '

fo[ob]ar

' ); doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true ); output( '

fo[<$text bold="true">ob]ar

' ); setSelection( [ 0, 3 ], [ 0, 3 ] ); doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); output( '

fo<$text bold="true">ozzz<$text bold="true">[]bar

' ); expect( doc.selection.getAttribute( 'bold' ) ).to.true; editor.execute( 'undo' ); output( '

fo<$text bold="true">o[]bar

' ); expect( doc.selection.getAttribute( 'bold' ) ).to.true; editor.execute( 'undo' ); output( '

fo[ob]ar

' ); undoDisabled(); } ); } ); describe( 'wrapping, unwrapping, merging, splitting', () => { it( 'wrap and undo', () => { doc.schema.allow( { name: '$text', inside: '$root' } ); input( 'fo[zb]ar' ); doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); output( 'fo

[zb]

ar' ); editor.execute( 'undo' ); output( 'fo[zb]ar' ); undoDisabled(); } ); it( 'wrap, move and undo', () => { doc.schema.allow( { name: '$text', inside: '$root' } ); input( 'fo[zb]ar' ); doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); // Would be better if selection was inside P. output( 'fo

[zb]

ar' ); setSelection( [ 2, 0 ], [ 2, 1 ] ); doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); output( '[z]fo

b

ar' ); editor.execute( 'undo' ); output( 'fo

[z]b

ar' ); editor.execute( 'undo' ); output( 'fo[zb]ar' ); undoDisabled(); } ); it( 'unwrap and undo', () => { input( '

foo[]bar

' ); doc.batch().unwrap( doc.selection.getFirstPosition().parent ); output( 'foo[]bar' ); editor.execute( 'undo' ); output( '

foo[]bar

' ); undoDisabled(); } ); it( 'merge and undo', () => { input( '

foo

[]bar

' ); doc.batch().merge( new Position( root, [ 1 ] ) ); // Because selection is stuck with

it ends up in graveyard. We have to manually move it to correct node. setSelection( [ 0, 3 ], [ 0, 3 ] ); output( '

foo[]bar

' ); editor.execute( 'undo' ); output( '

foo

bar[]

' ); undoDisabled(); } ); it( 'split and undo', () => { input( '

foo[]bar

' ); doc.batch().split( doc.selection.getFirstPosition() ); // Because selection is stuck with

it ends up in wrong node. We have to manually move it to correct node. setSelection( [ 1, 0 ], [ 1, 0 ] ); output( '

foo

[]bar

' ); editor.execute( 'undo' ); output( '

foobar[]

' ); undoDisabled(); } ); } ); // Restoring selection in those examples may be completely off. describe( 'multiple enters, deletes and typing', () => { 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' ); } it( 'split, split, split', () => { input( '

12345678

' ); split( [ 0, 3 ] ); output( '

123

[]45678

' ); split( [ 1, 4 ] ); output( '

123

4567

[]8

' ); split( [ 1, 2 ] ); output( '

123

45

[]67

8

' ); editor.execute( 'undo' ); output( '

123

4567[]

8

' ); editor.execute( 'undo' ); output( '

123

45678[]

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

123

4567[]

8

' ); editor.execute( 'redo' ); output( '

123

45[]

67

8

' ); redoDisabled(); } ); it( 'merge, merge, merge', () => { input( '

123

45

67

8

' ); merge( [ 1 ] ); output( '

123[]45

67

8

' ); merge( [ 2 ] ); output( '

12345

67[]8

' ); merge( [ 1 ] ); output( '

12345[]678

' ); editor.execute( 'undo' ); output( '

12345

678[]

' ); editor.execute( 'undo' ); output( '

12345

67

8[]

' ); editor.execute( 'undo' ); output( '

123

45[]

67

8

' ); undoDisabled(); editor.execute( 'redo' ); output( '

12345[]

67

8

' ); editor.execute( 'redo' ); output( '

12345

678[]

' ); editor.execute( 'redo' ); output( '

12345678[]

' ); 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( '

12345678[]

' ); editor.execute( 'undo' ); output( '

123

45678[]

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

12345678[]

' ); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

12345678[]

' ); redoDisabled(); } ); it( 'split, split, split, merge, merge, merge', () => { input( '

12345678

' ); split( [ 0, 3 ] ); output( '

123

[]45678

' ); split( [ 1, 4 ] ); output( '

123

4567

[]8

' ); split( [ 1, 2 ] ); output( '

123

45

[]67

8

' ); merge( [ 1 ] ); output( '

123[]45

67

8

' ); merge( [ 2 ] ); output( '

12345

67[]8

' ); merge( [ 1 ] ); output( '

12345[]678

' ); editor.execute( 'undo' ); output( '

12345

678[]

' ); editor.execute( 'undo' ); output( '

12345

67

8[]

' ); editor.execute( 'undo' ); output( '

123

45[]

67

8

' ); editor.execute( 'undo' ); output( '

123

4567[]

8

' ); editor.execute( 'undo' ); output( '

123

45678[]

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

123

4567[]

8

' ); editor.execute( 'redo' ); output( '

123

45[]

67

8

' ); editor.execute( 'redo' ); output( '

12345[]

67

8

' ); editor.execute( 'redo' ); output( '

12345

678[]

' ); editor.execute( 'redo' ); output( '

12345678[]

' ); redoDisabled(); } ); it( 'split, split, merge, split, merge (different order)', () => { input( '

12345678

' ); split( [ 0, 3 ] ); output( '

123

[]45678

' ); split( [ 1, 2 ] ); output( '

123

45

[]678

' ); merge( [ 1 ] ); output( '

123[]45

678

' ); split( [ 1, 1 ] ); output( '

12345

6

[]78

' ); merge( [ 1 ] ); output( '

12345[]6

78

' ); editor.execute( 'undo' ); output( '

12345

6[]

78

' ); editor.execute( 'undo' ); output( '

12345

678[]

' ); editor.execute( 'undo' ); output( '

123

45[]

678

' ); editor.execute( 'undo' ); output( '

123

45678[]

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

123

45[]

678

' ); editor.execute( 'redo' ); output( '

12345[]

678

' ); editor.execute( 'redo' ); output( '

12345

6[]

78

' ); editor.execute( 'redo' ); output( '

123456[]

78

' ); redoDisabled(); } ); it( 'split, remove, split, merge, merge', () => { input( '

12345678

' ); split( [ 0, 3 ] ); output( '

123

[]45678

' ); remove( [ 1, 4 ] ); remove( [ 1, 3 ] ); output( '

123

45[]8

' ); split( [ 1, 1 ] ); output( '

123

4

[]58

' ); merge( [ 1 ] ); output( '

123[]4

58

' ); merge( [ 1 ] ); output( '

1234[]58

' ); editor.execute( 'undo' ); output( '

1234

58[]

' ); editor.execute( 'undo' ); output( '

123

4[]

58

' ); editor.execute( 'undo' ); output( '

123

458[]

' ); editor.execute( 'undo' ); output( '

123

4567[]8

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

123

458[]

' ); editor.execute( 'redo' ); output( '

123

4[]

58

' ); editor.execute( 'redo' ); output( '

1234[]

58

' ); editor.execute( 'redo' ); output( '

123458[]

' ); redoDisabled(); } ); it( 'split, typing, split, merge, merge', () => { input( '

12345678

' ); split( [ 0, 3 ] ); output( '

123

[]45678

' ); type( [ 1, 4 ], 'x' ); type( [ 1, 5 ], 'y' ); output( '

123

4567xy[]8

' ); split( [ 1, 2 ] ); output( '

123

45

[]67xy8

' ); merge( [ 1 ] ); output( '

123[]45

67xy8

' ); merge( [ 1 ] ); output( '

12345[]67xy8

' ); editor.execute( 'undo' ); output( '

12345

67xy8[]

' ); editor.execute( 'undo' ); output( '

123

45[]

67xy8

' ); editor.execute( 'undo' ); output( '

123

4567xy8[]

' ); editor.execute( 'undo' ); output( '

123

4567[]8

' ); editor.execute( 'undo' ); output( '

12345678[]

' ); undoDisabled(); editor.execute( 'redo' ); output( '

123[]

45678

' ); editor.execute( 'redo' ); output( '

123

4567xy8[]

' ); editor.execute( 'redo' ); output( '

123

45[]

67xy8

' ); editor.execute( 'redo' ); output( '

12345[]

67xy8

' ); editor.execute( 'redo' ); output( '

1234567xy8[]

' ); redoDisabled(); } ); } ); describe( 'other reported cases', () => { // ckeditor5-engine#t/1051 it( 'rename leaks to other elements on undo #1', () => { input( '

[]Foo

Bar

' ); doc.batch().rename( root.getChild( 0 ), 'p' ); output( '

[]Foo

Bar

' ); doc.batch().split( Position.createAt( root.getChild( 0 ), 1 ) ); output( '

[]F

oo

Bar

' ); doc.batch().merge( Position.createAt( root, 2 ) ); output( '

[]F

ooBar

' ); editor.execute( 'undo' ); output( '

[]F

oo

Bar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); } ); // 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( '

[]Foo

Bar

' ); doc.batch().rename( root.getChild( 0 ), 'h2' ); output( '

[]Foo

Bar

' ); doc.batch().merge( Position.createAt( root, 1 ) ); output( '

[]FooBar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); } ); // Reverse issue, this time first operation is merge and then rename. it( 'merge, rename, undo, undo is correct', () => { input( '

[]Foo

Bar

' ); doc.batch().merge( Position.createAt( root, 1 ) ); output( '

[]FooBar

' ); doc.batch().rename( root.getChild( 0 ), 'h2' ); output( '

[]FooBar

' ); editor.execute( 'undo' ); output( '

[]FooBar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); } ); // ckeditor5-engine#t/1053 it( 'wrap, split, undo, undo is correct', () => { input( '

[]Foo

Bar

' ); doc.batch().wrap( Range.createIn( root ), 'div' ); output( '

[]Foo

Bar

' ); doc.batch().split( new Position( root, [ 0, 0, 1 ] ) ); output( '

[]F

oo

Bar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); editor.execute( 'undo' ); output( '

[]Foo

Bar

' ); } ); // 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

' ); } ); } ); describe( 'other edge cases', () => { it( 'deleteContent between two nodes', () => { input( '

fo[o

b]ar

' ); editor.data.deleteContent( doc.selection, doc.batch() ); output( '

fo[]ar

' ); editor.execute( 'undo' ); output( '

fo[o

b]ar

' ); } ); // Related to ckeditor5-engine#891 and ckeditor5-list#51. it( 'change attribute of removed node then undo and redo', () => { const gy = doc.graveyard; const batch = doc.batch(); const p = new Element( 'p' ); root.appendChildren( p ); batch.remove( p ); batch.setAttribute( p, 'bold', true ); editor.execute( 'undo' ); editor.execute( 'redo' ); expect( p.root ).to.equal( gy ); expect( p.getAttribute( 'bold' ) ).to.be.true; } ); // Related to ckeditor5-engine#891. it( 'change attribute of removed node then undo and redo', () => { const gy = doc.graveyard; const batch = doc.batch(); const p1 = new Element( 'p' ); const p2 = new Element( 'p' ); const p3 = new Element( 'p' ); root.appendChildren( [ p1, p2 ] ); batch.remove( p1 ).remove( p2 ).insert( new Position( root, [ 0 ] ), p3 ); editor.execute( 'undo' ); editor.execute( 'redo' ); expect( p1.root ).to.equal( gy ); expect( p2.root ).to.equal( gy ); expect( p3.root ).to.equal( root ); } ); } ); } );