undoengine-integration.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
  6. import Range from '@ckeditor/ckeditor5-engine/src/model/range';
  7. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  8. import Element from '@ckeditor/ckeditor5-engine/src/model/element';
  9. import UndoEngine from '../src/undoengine';
  10. import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  11. describe( 'UndoEngine integration', () => {
  12. let editor, doc, root;
  13. beforeEach( () => {
  14. return ModelTestEditor.create( {
  15. plugins: [ UndoEngine ]
  16. } )
  17. .then( newEditor => {
  18. editor = newEditor;
  19. doc = editor.document;
  20. doc.schema.registerItem( 'p', '$block' );
  21. root = doc.getRoot();
  22. } );
  23. } );
  24. function setSelection( pathA, pathB ) {
  25. doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
  26. }
  27. function input( input ) {
  28. setData( doc, input );
  29. }
  30. function output( output ) {
  31. expect( getData( doc ) ).to.equal( output );
  32. }
  33. function undoDisabled() {
  34. expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
  35. }
  36. describe( 'UndoEngine integration', () => {
  37. describe( 'adding and removing content', () => {
  38. it( 'add and undo', () => {
  39. input( '<p>fo[]o</p><p>bar</p>' );
  40. doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
  41. output( '<p>fozzz[]o</p><p>bar</p>' );
  42. editor.execute( 'undo' );
  43. output( '<p>fo[]o</p><p>bar</p>' );
  44. undoDisabled();
  45. } );
  46. it( 'multiple adding and undo', () => {
  47. input( '<p>fo[]o</p><p>bar</p>' );
  48. doc.batch()
  49. .insert( doc.selection.getFirstPosition(), 'zzz' )
  50. .insert( new Position( root, [ 1, 0 ] ), 'xxx' );
  51. output( '<p>fozzz[]o</p><p>xxxbar</p>' );
  52. setSelection( [ 1, 0 ], [ 1, 0 ] );
  53. doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
  54. output( '<p>fozzzo</p><p>yyy[]xxxbar</p>' );
  55. editor.execute( 'undo' );
  56. output( '<p>fozzzo</p><p>[]xxxbar</p>' );
  57. editor.execute( 'undo' );
  58. output( '<p>fo[]o</p><p>bar</p>' );
  59. undoDisabled();
  60. } );
  61. it( 'multiple adding mixed with undo', () => {
  62. input( '<p>fo[]o</p><p>bar</p>' );
  63. doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
  64. output( '<p>fozzz[]o</p><p>bar</p>' );
  65. setSelection( [ 1, 0 ], [ 1, 0 ] );
  66. doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
  67. output( '<p>fozzzo</p><p>yyy[]bar</p>' );
  68. editor.execute( 'undo' );
  69. output( '<p>fozzzo</p><p>[]bar</p>' );
  70. setSelection( [ 0, 0 ], [ 0, 0 ] );
  71. doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
  72. output( '<p>xxx[]fozzzo</p><p>bar</p>' );
  73. editor.execute( 'undo' );
  74. output( '<p>[]fozzzo</p><p>bar</p>' );
  75. editor.execute( 'undo' );
  76. output( '<p>fo[]o</p><p>bar</p>' );
  77. undoDisabled();
  78. } );
  79. it( 'multiple remove and undo', () => {
  80. input( '<p>[]foo</p><p>bar</p>' );
  81. doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
  82. output( '<p>[]o</p><p>bar</p>' );
  83. setSelection( [ 1, 1 ], [ 1, 1 ] );
  84. doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
  85. output( '<p>o</p><p>b[]</p>' );
  86. editor.execute( 'undo' );
  87. // Here is an edge case that selection could be before or after `ar`.
  88. output( '<p>o</p><p>b[]ar</p>' );
  89. editor.execute( 'undo' );
  90. // As above.
  91. output( '<p>[]foo</p><p>bar</p>' );
  92. undoDisabled();
  93. } );
  94. it( 'add and remove different parts and undo', () => {
  95. input( '<p>fo[]o</p><p>bar</p>' );
  96. doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
  97. output( '<p>fozzz[]o</p><p>bar</p>' );
  98. setSelection( [ 1, 2 ], [ 1, 2 ] );
  99. doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
  100. output( '<p>fozzzo</p><p>b[]r</p>' );
  101. editor.execute( 'undo' );
  102. output( '<p>fozzzo</p><p>ba[]r</p>' );
  103. editor.execute( 'undo' );
  104. output( '<p>fo[]o</p><p>bar</p>' );
  105. undoDisabled();
  106. } );
  107. it( 'add and remove same part and undo', () => {
  108. input( '<p>fo[]o</p><p>bar</p>' );
  109. doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
  110. output( '<p>fozzz[]o</p><p>bar</p>' );
  111. doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
  112. output( '<p>fo[]o</p><p>bar</p>' );
  113. editor.execute( 'undo' );
  114. output( '<p>fozzz[]o</p><p>bar</p>' );
  115. editor.execute( 'undo' );
  116. output( '<p>fo[]o</p><p>bar</p>' );
  117. undoDisabled();
  118. } );
  119. } );
  120. describe( 'moving', () => {
  121. it( 'move same content twice then undo', () => {
  122. input( '<p>f[o]z</p><p>bar</p>' );
  123. doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
  124. output( '<p>fz</p><p>[o]bar</p>' );
  125. doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
  126. output( '<p>fz[o]</p><p>bar</p>' );
  127. editor.execute( 'undo' );
  128. output( '<p>fz</p><p>[o]bar</p>' );
  129. editor.execute( 'undo' );
  130. output( '<p>f[o]z</p><p>bar</p>' );
  131. undoDisabled();
  132. } );
  133. it( 'move content and new parent then undo', () => {
  134. input( '<p>f[o]z</p><p>bar</p>' );
  135. doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
  136. output( '<p>fz</p><p>[o]bar</p>' );
  137. setSelection( [ 1 ], [ 2 ] );
  138. doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
  139. output( '[<p>obar</p>]<p>fz</p>' );
  140. editor.execute( 'undo' );
  141. output( '<p>fz</p>[<p>obar</p>]' );
  142. editor.execute( 'undo' );
  143. output( '<p>f[o]z</p><p>bar</p>' );
  144. undoDisabled();
  145. } );
  146. } );
  147. describe( 'attributes with other', () => {
  148. it( 'attributes then insert inside then undo', () => {
  149. input( '<p>fo[ob]ar</p>' );
  150. doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true );
  151. output( '<p>fo[<$text bold="true">ob</$text>]ar</p>' );
  152. setSelection( [ 0, 3 ], [ 0, 3 ] );
  153. doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
  154. output( '<p>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</p>' );
  155. expect( doc.selection.getAttribute( 'bold' ) ).to.true;
  156. editor.execute( 'undo' );
  157. output( '<p>fo<$text bold="true">o[]b</$text>ar</p>' );
  158. expect( doc.selection.getAttribute( 'bold' ) ).to.true;
  159. editor.execute( 'undo' );
  160. output( '<p>fo[ob]ar</p>' );
  161. undoDisabled();
  162. } );
  163. } );
  164. describe( 'wrapping, unwrapping, merging, splitting', () => {
  165. it( 'wrap and undo', () => {
  166. doc.schema.allow( { name: '$text', inside: '$root' } );
  167. input( 'fo[zb]ar' );
  168. doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
  169. output( 'fo<p>[zb]</p>ar' );
  170. editor.execute( 'undo' );
  171. output( 'fo[zb]ar' );
  172. undoDisabled();
  173. } );
  174. it( 'wrap, move and undo', () => {
  175. doc.schema.allow( { name: '$text', inside: '$root' } );
  176. input( 'fo[zb]ar' );
  177. doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
  178. // Would be better if selection was inside P.
  179. output( 'fo<p>[zb]</p>ar' );
  180. setSelection( [ 2, 0 ], [ 2, 1 ] );
  181. doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
  182. output( '[z]fo<p>b</p>ar' );
  183. editor.execute( 'undo' );
  184. output( 'fo<p>[z]b</p>ar' );
  185. editor.execute( 'undo' );
  186. output( 'fo[zb]ar' );
  187. undoDisabled();
  188. } );
  189. it( 'unwrap and undo', () => {
  190. input( '<p>foo[]bar</p>' );
  191. doc.batch().unwrap( doc.selection.getFirstPosition().parent );
  192. output( 'foo[]bar' );
  193. editor.execute( 'undo' );
  194. output( '<p>foo[]bar</p>' );
  195. undoDisabled();
  196. } );
  197. it( 'merge and undo', () => {
  198. input( '<p>foo</p><p>[]bar</p>' );
  199. doc.batch().merge( new Position( root, [ 1 ] ) );
  200. // Because selection is stuck with <p> it ends up in graveyard. We have to manually move it to correct node.
  201. setSelection( [ 0, 3 ], [ 0, 3 ] );
  202. output( '<p>foo[]bar</p>' );
  203. editor.execute( 'undo' );
  204. output( '<p>foo</p><p>[]bar</p>' );
  205. undoDisabled();
  206. } );
  207. it( 'split and undo', () => {
  208. input( '<p>foo[]bar</p>' );
  209. doc.batch().split( doc.selection.getFirstPosition() );
  210. // Because selection is stuck with <p> it ends up in wrong node. We have to manually move it to correct node.
  211. setSelection( [ 1, 0 ], [ 1, 0 ] );
  212. output( '<p>foo</p><p>[]bar</p>' );
  213. editor.execute( 'undo' );
  214. output( '<p>foo[]bar</p>' );
  215. undoDisabled();
  216. } );
  217. } );
  218. describe( 'other edge cases', () => {
  219. it( 'deleteContent between two nodes', () => {
  220. input( '<p>fo[o</p><p>b]ar</p>' );
  221. editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } );
  222. output( '<p>fo[]ar</p>' );
  223. editor.execute( 'undo' );
  224. output( '<p>fo[o</p><p>b]ar</p>' );
  225. } );
  226. // Related to ckeditor5-engine#891 and ckeditor5-list#51.
  227. it( 'change attribute of removed node then undo and redo', () => {
  228. const gy = doc.graveyard;
  229. const batch = doc.batch();
  230. const p = new Element( 'p' );
  231. root.appendChildren( p );
  232. batch.remove( p );
  233. batch.setAttribute( p, 'bold', true );
  234. editor.execute( 'undo' );
  235. editor.execute( 'redo' );
  236. expect( p.root ).to.equal( gy );
  237. expect( p.getAttribute( 'bold' ) ).to.be.true;
  238. } );
  239. } );
  240. } );
  241. } );