mergeoperation.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Model from '../../../src/model/model';
  6. import MergeOperation from '../../../src/model/operation/mergeoperation';
  7. import SplitOperation from '../../../src/model/operation/splitoperation';
  8. import Position from '../../../src/model/position';
  9. import Element from '../../../src/model/element';
  10. import Text from '../../../src/model/text';
  11. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  12. describe( 'MergeOperation', () => {
  13. let model, doc, root, gy, gyPos;
  14. beforeEach( () => {
  15. model = new Model();
  16. doc = model.document;
  17. root = doc.createRoot();
  18. gy = doc.graveyard;
  19. gyPos = new Position( gy, [ 0 ] );
  20. } );
  21. it( 'should have proper type', () => {
  22. const merge = new MergeOperation( new Position( root, [ 1, 0 ] ), 2, new Position( root, [ 0, 1 ] ), gyPos, 1 );
  23. expect( merge.type ).to.equal( 'merge' );
  24. } );
  25. it( 'should have proper deletionPosition', () => {
  26. const merge = new MergeOperation( new Position( root, [ 1, 0 ] ), 2, new Position( root, [ 0, 1 ] ), gyPos, 1 );
  27. expect( merge.deletionPosition.path ).to.deep.equal( [ 1 ] );
  28. } );
  29. it( 'should have proper movedRange', () => {
  30. const merge = new MergeOperation( new Position( root, [ 1, 0 ] ), 2, new Position( root, [ 0, 1 ] ), gyPos, 1 );
  31. expect( merge.movedRange.start.path ).to.deep.equal( [ 1, 0 ] );
  32. expect( merge.movedRange.end.path ).to.deep.equal( [ 1, Number.POSITIVE_INFINITY ] );
  33. } );
  34. it( 'should merge two nodes together', () => {
  35. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  36. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  37. root._insertChild( 0, [ p1, p2 ] );
  38. model.applyOperation(
  39. new MergeOperation(
  40. new Position( root, [ 1, 0 ] ),
  41. 3,
  42. new Position( root, [ 0, 3 ] ),
  43. gyPos,
  44. doc.version
  45. )
  46. );
  47. expect( doc.version ).to.equal( 1 );
  48. expect( root.maxOffset ).to.equal( 1 );
  49. expect( root.getChild( 0 ).name ).to.equal( 'p1' );
  50. expect( p1.maxOffset ).to.equal( 6 );
  51. expect( p1.getChild( 0 ).data ).to.equal( 'Foobar' );
  52. } );
  53. it( 'should create a proper SplitOperation as a reverse', () => {
  54. const sourcePosition = new Position( root, [ 1, 0 ] );
  55. const targetPosition = new Position( root, [ 0, 3 ] );
  56. const operation = new MergeOperation( sourcePosition, 2, targetPosition, gyPos, doc.version );
  57. const reverse = operation.getReversed();
  58. expect( reverse ).to.be.an.instanceof( SplitOperation );
  59. expect( reverse.baseVersion ).to.equal( 1 );
  60. expect( reverse.howMany ).to.equal( 2 );
  61. expect( reverse.position.isEqual( targetPosition ) ).to.be.true;
  62. expect( reverse.graveyardPosition.isEqual( gyPos ) ).to.be.true;
  63. } );
  64. it( 'should undo merge by applying reverse operation', () => {
  65. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  66. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  67. root._insertChild( 0, [ p1, p2 ] );
  68. const operation = new MergeOperation(
  69. new Position( root, [ 1, 0 ] ),
  70. 3,
  71. new Position( root, [ 0, 3 ] ),
  72. gyPos,
  73. doc.version
  74. );
  75. model.applyOperation( operation );
  76. model.applyOperation( operation.getReversed() );
  77. expect( doc.version ).to.equal( 2 );
  78. expect( root.maxOffset ).to.equal( 2 );
  79. expect( p1.maxOffset ).to.equal( 3 );
  80. expect( p1.getChild( 0 ).data ).to.equal( 'Foo' );
  81. expect( p2.maxOffset ).to.equal( 3 );
  82. expect( p2.getChild( 0 ).data ).to.equal( 'bar' );
  83. } );
  84. describe( '_validate()', () => {
  85. it( 'should throw an error if source position is invalid', () => {
  86. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  87. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  88. root._insertChild( 0, [ p1, p2 ] );
  89. const operation = new MergeOperation(
  90. new Position( root, [ 0, 3 ] ),
  91. 3,
  92. new Position( root, [ 2, 0 ] ),
  93. gyPos,
  94. doc.version
  95. );
  96. expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-target-position-invalid/ );
  97. } );
  98. it( 'should throw an error if source position is in root', () => {
  99. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  100. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  101. root._insertChild( 0, [ p1, p2 ] );
  102. const operation = new MergeOperation(
  103. new Position( root, [ 0 ] ),
  104. 3,
  105. new Position( root, [ 0, 3 ] ),
  106. gyPos,
  107. doc.version
  108. );
  109. expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-source-position-invalid/ );
  110. } );
  111. it( 'should throw an error if target position is invalid', () => {
  112. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  113. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  114. root._insertChild( 0, [ p1, p2 ] );
  115. const operation = new MergeOperation(
  116. new Position( root, [ 2, 3 ] ),
  117. 3,
  118. new Position( root, [ 1, 0 ] ),
  119. gyPos,
  120. doc.version
  121. );
  122. expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-source-position-invalid/ );
  123. } );
  124. it( 'should throw an error if number of nodes to move is invalid', () => {
  125. const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
  126. const p2 = new Element( 'p2', null, new Text( 'bar' ) );
  127. root._insertChild( 0, [ p1, p2 ] );
  128. const operation = new MergeOperation(
  129. new Position( root, [ 0, 3 ] ),
  130. 5,
  131. new Position( root, [ 1, 0 ] ),
  132. gyPos,
  133. doc.version
  134. );
  135. expect( () => operation._validate() ).to.throw( CKEditorError, /merge-operation-how-many-invalid/ );
  136. } );
  137. } );
  138. it( 'should create MergeOperation with the same parameters when cloned', () => {
  139. const sourcePosition = new Position( root, [ 1, 0 ] );
  140. const targetPosition = new Position( root, [ 0, 3 ] );
  141. const howMany = 4;
  142. const baseVersion = doc.version;
  143. const op = new MergeOperation( sourcePosition, howMany, targetPosition, gyPos, baseVersion );
  144. const clone = op.clone();
  145. // New instance rather than a pointer to the old instance.
  146. expect( clone ).not.to.be.equal( op );
  147. expect( clone ).to.be.instanceof( MergeOperation );
  148. expect( clone.sourcePosition.isEqual( sourcePosition ) ).to.be.true;
  149. expect( clone.targetPosition.isEqual( targetPosition ) ).to.be.true;
  150. expect( clone.howMany ).to.equal( howMany );
  151. expect( clone.graveyardPosition.isEqual( gyPos ) ).to.be.true;
  152. expect( clone.baseVersion ).to.equal( baseVersion );
  153. } );
  154. describe( 'toJSON', () => {
  155. it( 'should create proper json object', () => {
  156. const sourcePosition = new Position( root, [ 1, 0 ] );
  157. const targetPosition = new Position( root, [ 0, 3 ] );
  158. const op = new MergeOperation( sourcePosition, 1, targetPosition, gyPos, doc.version );
  159. const serialized = op.toJSON();
  160. expect( serialized ).to.deep.equal( {
  161. __className: 'engine.model.operation.MergeOperation',
  162. baseVersion: 0,
  163. howMany: 1,
  164. sourcePosition: op.sourcePosition.toJSON(),
  165. targetPosition: op.targetPosition.toJSON(),
  166. graveyardPosition: gyPos.toJSON()
  167. } );
  168. } );
  169. } );
  170. describe( 'fromJSON', () => {
  171. it( 'should create proper MergeOperation from json object', () => {
  172. const sourcePosition = new Position( root, [ 1, 0 ] );
  173. const targetPosition = new Position( root, [ 0, 3 ] );
  174. const op = new MergeOperation( sourcePosition, 1, targetPosition, gyPos, doc.version );
  175. const serialized = op.toJSON();
  176. const deserialized = MergeOperation.fromJSON( serialized, doc );
  177. expect( deserialized ).to.deep.equal( op );
  178. } );
  179. } );
  180. } );