|
|
@@ -3,332 +3,103 @@
|
|
|
* For licensing, see LICENSE.md.
|
|
|
*/
|
|
|
|
|
|
-/* bender-tags: browser-only */
|
|
|
-
|
|
|
'use strict';
|
|
|
|
|
|
-import Editor from '/ckeditor5/editor.js';
|
|
|
-import ModelDocument from '/ckeditor5/engine/model/document.js';
|
|
|
-import Range from '/ckeditor5/engine/model/range.js';
|
|
|
-import Position from '/ckeditor5/engine/model/position.js';
|
|
|
+import ClassicTestEditor from '/tests/ckeditor5/_utils/classictesteditor.js';
|
|
|
import Undo from '/ckeditor5/undo/undo.js';
|
|
|
-import Creator from '/ckeditor5/creator/creator.js';
|
|
|
-
|
|
|
-import { setData, getData } from '/tests/engine/_utils/model.js';
|
|
|
+import UndoEngine from '/ckeditor5/undo/undoengine.js';
|
|
|
+import ButtonController from '/ckeditor5/ui/button/button.js';
|
|
|
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
|
|
|
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
|
|
|
|
|
|
-// import deleteContents from '/ckeditor5/engine/model/composer/deletecontents.js';
|
|
|
+testUtils.createSinonSandbox();
|
|
|
|
|
|
-let element, editor, doc, root;
|
|
|
+describe( 'Undo', () => {
|
|
|
+ let editor;
|
|
|
|
|
|
-beforeEach( () => {
|
|
|
- element = document.createElement( 'div' );
|
|
|
- document.body.appendChild( element );
|
|
|
+ beforeEach( () => {
|
|
|
+ const editorElement = document.createElement( 'div' );
|
|
|
|
|
|
- doc = new ModelDocument();
|
|
|
- root = doc.createRoot( 'root' );
|
|
|
-
|
|
|
- editor = new Editor( element, {
|
|
|
- creator: Creator,
|
|
|
- features: [ Undo ]
|
|
|
+ return ClassicTestEditor.create( editorElement, {
|
|
|
+ features: [ 'undo' ]
|
|
|
+ } )
|
|
|
+ .then( newEditor => {
|
|
|
+ editor = newEditor;
|
|
|
+ } );
|
|
|
} );
|
|
|
|
|
|
- editor.document = doc;
|
|
|
-
|
|
|
- return editor.init();
|
|
|
-} );
|
|
|
-
|
|
|
-function setSelection( pathA, pathB ) {
|
|
|
- doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
|
|
|
-}
|
|
|
-
|
|
|
-function input( input ) {
|
|
|
- setData( doc, input, { rootName: 'root' } );
|
|
|
-}
|
|
|
-
|
|
|
-function output( output ) {
|
|
|
- expect( getData( doc, { rootName: 'root' } ) ).to.equal( output );
|
|
|
-}
|
|
|
-
|
|
|
-function undoDisabled() {
|
|
|
- expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
|
|
|
-}
|
|
|
-
|
|
|
-describe( 'undo integration', () => {
|
|
|
- describe( 'adding and removing content', () => {
|
|
|
- it( 'add and undo', () => {
|
|
|
- input( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
|
|
|
- output( '<p>fozzz<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- it( 'multiple adding and undo', () => {
|
|
|
- input( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch()
|
|
|
- .insert( doc.selection.getFirstPosition(), 'zzz' )
|
|
|
- .insert( new Position( root, [ 1, 0 ] ), 'xxx' );
|
|
|
- output( '<p>fozzz<selection />o</p><p>xxxbar</p>' );
|
|
|
-
|
|
|
- setSelection( [ 1, 0 ], [ 1, 0 ] );
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
|
|
|
- output( '<p>fozzzo</p><p>yyy<selection />xxxbar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fozzzo</p><p><selection />xxxbar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- it( 'multiple adding mixed with undo', () => {
|
|
|
- input( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
|
|
|
- output( '<p>fozzz<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- setSelection( [ 1, 0 ], [ 1, 0 ] );
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
|
|
|
- output( '<p>fozzzo</p><p>yyy<selection />bar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fozzzo</p><p><selection />bar</p>' );
|
|
|
-
|
|
|
- setSelection( [ 0, 0 ], [ 0, 0 ] );
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
|
|
|
- output( '<p>xxx<selection />fozzzo</p><p>bar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p><selection />fozzzo</p><p>bar</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- it( 'multiple remove and undo', () => {
|
|
|
- input( '<p><selection />foo</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
|
|
|
- output( '<p><selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- setSelection( [ 1, 1 ], [ 1, 1 ] );
|
|
|
- doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
|
|
|
- output( '<p>o</p><p>b<selection /></p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- // Here is an edge case that selection could be before or after `ar` but selection always ends up after.
|
|
|
- output( '<p>o</p><p>bar<selection /></p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- // As above.
|
|
|
- output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- it( 'add and remove different parts and undo', () => {
|
|
|
- input( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
|
|
|
- output( '<p>fozzz<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- setSelection( [ 1, 2 ], [ 1, 2 ] );
|
|
|
- doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
|
|
|
- output( '<p>fozzzo</p><p>b<selection />r</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fozzzo</p><p>ba<selection />r</p>' );
|
|
|
-
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
-
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- //it( 'add and remove same part and undo', () => {
|
|
|
- // // This test case fails because some operations are transformed to NoOperations incorrectly.
|
|
|
- // input( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
|
|
|
- // output( '<p>fozzz<selection />o</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
|
|
|
- // output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( '<p>fozzz<selection />o</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( '<p>fo<selection />o</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // undoDisabled();
|
|
|
- //} );
|
|
|
+ it( 'should be loaded', () => {
|
|
|
+ expect( editor.plugins.get( Undo ) ).to.be.instanceOf( Undo );
|
|
|
} );
|
|
|
|
|
|
- describe( 'moving', () => {
|
|
|
- //it( 'move same content twice then undo', () => {
|
|
|
- // // This test case fails because some operations are transformed to NoOperations incorrectly.
|
|
|
- // input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
|
|
|
- // output( '<p>fz</p><p><selection>o</selection>bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
|
|
|
- // output( '<p>fz<selection>o</selection></p><p>bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( '<p>fz</p><p><selection>o</selection>bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
|
|
|
- //
|
|
|
- // undoDisabled();
|
|
|
- //} );
|
|
|
-
|
|
|
- it( 'move content and new parent then undo', () => {
|
|
|
- input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
|
|
|
-
|
|
|
- doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
|
|
|
- output( '<p>fz</p><p><selection>o</selection>bar</p>' );
|
|
|
+ it( 'should load UndoEngine', () => {
|
|
|
+ expect( editor.plugins.get( UndoEngine ) ).to.be.instanceOf( UndoEngine );
|
|
|
+ } );
|
|
|
|
|
|
- setSelection( [ 1 ], [ 2 ] );
|
|
|
- doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
|
|
|
- output( '<selection><p>obar</p></selection><p>fz</p>' );
|
|
|
+ testButton( 'undo' );
|
|
|
+ testButton( 'redo' );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fz</p><selection><p>obar</p></selection>' );
|
|
|
+ it( 'should set CTRL+Z keystroke', () => {
|
|
|
+ const spy = sinon.stub( editor, 'execute' );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
|
|
|
+ const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true } );
|
|
|
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
+ expect( wasHandled ).to.be.true;
|
|
|
+ expect( spy.calledWithExactly( 'undo' ) ).to.be.true;
|
|
|
} );
|
|
|
|
|
|
- describe( 'attributes with other', () => {
|
|
|
- it( 'attributes then insert inside then undo', () => {
|
|
|
- input( '<p>fo<selection>ob</selection>ar</p>' );
|
|
|
+ it( 'should set CTRL+Y keystroke', () => {
|
|
|
+ const spy = sinon.stub( editor, 'execute' );
|
|
|
|
|
|
- doc.batch().setAttr( 'bold', true, doc.selection.getFirstRange() );
|
|
|
- output( '<p>fo<selection><$text bold=true>ob</$text></selection>ar</p>' );
|
|
|
+ const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.y, ctrlKey: true } );
|
|
|
|
|
|
- setSelection( [ 0, 3 ], [ 0, 3 ] );
|
|
|
- doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
|
|
|
- output( '<p>fo<$text bold=true>o</$text>zzz<selection bold=true /><$text bold=true>b</$text>ar</p>' );
|
|
|
+ expect( wasHandled ).to.be.true;
|
|
|
+ expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
|
|
|
+ } );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<$text bold=true>o<selection bold=true />b</$text>ar</p>' );
|
|
|
+ it( 'should set CTRL+SHIFT+Z keystroke', () => {
|
|
|
+ const spy = sinon.stub( editor, 'execute' );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>fo<selection>ob</selection>ar</p>' );
|
|
|
+ const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true, shiftKey: true } );
|
|
|
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
+ expect( wasHandled ).to.be.true;
|
|
|
+ expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
|
|
|
} );
|
|
|
|
|
|
- describe( 'wrapping, unwrapping, merging, splitting', () => {
|
|
|
- it( 'wrap and undo', () => {
|
|
|
- input( 'fo<selection>zb</selection>ar' );
|
|
|
+ function testButton( featureName ) {
|
|
|
+ describe( `${ featureName } button`, () => {
|
|
|
+ let buttonController;
|
|
|
|
|
|
- doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
|
|
|
- output( 'fo<p><selection>zb</selection></p>ar' );
|
|
|
+ beforeEach( () => {
|
|
|
+ buttonController = editor.ui.featureComponents.create( featureName );
|
|
|
+ } );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( 'fo<selection>zb</selection>ar' );
|
|
|
+ it( 'should register feature component', () => {
|
|
|
+ expect( buttonController ).to.be.instanceOf( ButtonController );
|
|
|
+ } );
|
|
|
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
-
|
|
|
- //it( 'wrap, move and undo', () => {
|
|
|
- // input( 'fo<selection>zb</selection>ar' );
|
|
|
- //
|
|
|
- // doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
|
|
|
- // output( 'fo<p><selection>zb</selection></p>ar' );
|
|
|
- //
|
|
|
- // setSelection( [ 2, 0 ], [ 2, 1 ] );
|
|
|
- // doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
|
|
|
- // output( '<selection>z</selection>fo<p>b</p>ar' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( 'fo<p><selection>z</selection>b</p>ar' );
|
|
|
- //
|
|
|
- // // This test case fails here for unknown reason, but "z" letter magically disappears.
|
|
|
- // // AssertionError: expected 'fo<selection>b</selection>ar' to equal 'fo<selection>zb</selection>ar'
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( 'fo<selection>zb</selection>ar' );
|
|
|
- //
|
|
|
- // undoDisabled();
|
|
|
- //} );
|
|
|
+ it( `should execute ${ featureName } command on model execute event`, () => {
|
|
|
+ const executeSpy = testUtils.sinon.stub( editor, 'execute' );
|
|
|
+ const model = buttonController.model;
|
|
|
|
|
|
- it( 'unwrap and undo', () => {
|
|
|
- input( '<p>foo<selection />bar</p>' );
|
|
|
+ model.fire( 'execute' );
|
|
|
|
|
|
- doc.batch().unwrap( doc.selection.getFirstPosition().parent );
|
|
|
- output( 'foo<selection />bar' );
|
|
|
+ sinon.assert.calledOnce( executeSpy );
|
|
|
+ sinon.assert.calledWithExactly( executeSpy, featureName );
|
|
|
+ } );
|
|
|
|
|
|
- editor.execute( 'undo' );
|
|
|
- output( '<p>foo<selection />bar</p>' );
|
|
|
+ it( `should bind model to ${ featureName } command`, () => {
|
|
|
+ const model = buttonController.model;
|
|
|
+ const command = editor.commands.get( featureName );
|
|
|
|
|
|
- undoDisabled();
|
|
|
- } );
|
|
|
+ expect( model.isOn ).to.be.false;
|
|
|
|
|
|
- //it( 'merge and undo', () => {
|
|
|
- // input( '<p>foo</p><p><selection />bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().merge( new Position( root, [ 1 ] ) );
|
|
|
- // // This test fails here because selection is stuck with <p> element and ends up in graveyard.
|
|
|
- // // AssertionError: expected '<p>foobar</p>' to equal '<p>foo<selection />bar</p>'
|
|
|
- // output( '<p>foo<selection />bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // // This test fails because when selection is transformed it is first in empty <p> but when
|
|
|
- // // "bar" is inserted, it gets moved to the right.
|
|
|
- // // AssertionError: expected '<p>foo</p><p>bar<selection /></p>' to equal '<p>foo</p><p><selection />bar</p>'
|
|
|
- // output( '<p>foo</p><p><selection />bar</p>' );
|
|
|
- //
|
|
|
- // undoDisabled();
|
|
|
- //} );
|
|
|
+ const initState = command.isEnabled;
|
|
|
+ expect( model.isEnabled ).to.equal( initState );
|
|
|
|
|
|
- //it( 'split and undo', () => {
|
|
|
- // input( '<p>foo<selection />bar</p>' );
|
|
|
- //
|
|
|
- // doc.batch().split( doc.selection.getFirstPosition() );
|
|
|
- // // This test fails because selection ends up in wrong node after splitting.
|
|
|
- // // AssertionError: expected '<p>foo<selection /></p><p>bar</p>' to equal '<p>foo</p><p><selection />bar</p>'
|
|
|
- // output( '<p>foo</p><p><selection />bar</p>' );
|
|
|
- //
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // // This test fails because selection after transforming ends up after inserted test.
|
|
|
- // // AssertionError: expected '<p>foobar<selection /></p>' to equal '<p>foo<selection />bar</p>'
|
|
|
- // output( '<p>foo<selection />bar</p>' );
|
|
|
- //
|
|
|
- // undoDisabled();
|
|
|
- //} );
|
|
|
- } );
|
|
|
-
|
|
|
- describe( 'other edge cases', () => {
|
|
|
- //it( 'deleteContents between two nodes', () => {
|
|
|
- // input( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
|
|
|
- //
|
|
|
- // deleteContents( doc.batch(), doc.selection, { merge: true } );
|
|
|
- // output( '<p>fo<selection />ar</p>' );
|
|
|
- //
|
|
|
- // // This test case fails because of OT problems.
|
|
|
- // // When the batch is undone, first MergeDelta is reversed to SplitDelta and it is undone.
|
|
|
- // // Then RemoveOperations are reversed to ReinsertOperation.
|
|
|
- // // Unfortunately, ReinsertOperation that inserts "o" points to the same position were split happened.
|
|
|
- // // Then, when ReinsertOperation is transformed by operations of SplitDelta, it ends up in wrong <p>.
|
|
|
- // editor.execute( 'undo' );
|
|
|
- // output( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
|
|
|
- //} );
|
|
|
- } );
|
|
|
+ command.isEnabled = !initState;
|
|
|
+ expect( model.isEnabled ).to.equal( !initState );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ }
|
|
|
} );
|