moveoperation.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: document */
  6. /* bender-include: ../../_tools/tools.js */
  7. /* global describe, before, beforeEach, it, expect */
  8. 'use strict';
  9. const modules = bender.amd.require(
  10. 'document/document',
  11. 'document/operation/moveoperation',
  12. 'document/operation/insertoperation',
  13. 'document/operation/changeoperation',
  14. 'document/operation/nooperation',
  15. 'document/attribute',
  16. 'document/position',
  17. 'document/range',
  18. 'document/element',
  19. 'document/node',
  20. 'document/nodelist',
  21. 'ckeditorerror'
  22. );
  23. describe( 'MoveOperation', () => {
  24. let Document, MoveOperation, InsertOperation, ChangeOperation, NoOperation,
  25. Attribute, Position, Range, Element, Node, NodeList, CKEditorError;
  26. before( () => {
  27. Document = modules[ 'document/document' ];
  28. MoveOperation = modules[ 'document/operation/moveoperation' ];
  29. InsertOperation = modules[ 'document/operation/insertoperation' ];
  30. ChangeOperation = modules[ 'document/operation/changeoperation' ];
  31. NoOperation = modules[ 'document/operation/nooperation' ];
  32. Attribute = modules[ 'document/attribute' ];
  33. Position = modules[ 'document/position' ];
  34. Range = modules[ 'document/range' ];
  35. Element = modules[ 'document/element' ];
  36. Node = modules[ 'document/node' ];
  37. NodeList = modules[ 'document/nodelist' ];
  38. CKEditorError = modules.ckeditorerror;
  39. } );
  40. let doc, root;
  41. beforeEach( () => {
  42. doc = new Document();
  43. root = doc.createRoot( 'root' );
  44. } );
  45. it( 'should move from one node to another', () => {
  46. let p1 = new Element( 'p1', [], new Element( 'x' ) );
  47. let p2 = new Element( 'p2' );
  48. root.insertChildren( 0, [ p1, p2 ] );
  49. doc.applyOperation(
  50. new MoveOperation(
  51. new Position( [ 0, 0 ], root ),
  52. new Position( [ 1, 0 ], root ),
  53. 1,
  54. doc.version
  55. )
  56. );
  57. expect( doc.version ).to.equal( 1 );
  58. expect( root.getChildCount() ).to.equal( 2 );
  59. expect( root.getChild( 0 ).name ).to.equal( 'p1' );
  60. expect( root.getChild( 1 ).name ).to.equal( 'p2' );
  61. expect( p1.getChildCount() ).to.equal( 0 );
  62. expect( p2.getChildCount() ).to.equal( 1 );
  63. expect( p2.getChild( 0 ).name ).to.equal( 'x' );
  64. } );
  65. it( 'should move position of children in one node backward', () => {
  66. root.insertChildren( 0, 'xbarx' );
  67. doc.applyOperation(
  68. new MoveOperation(
  69. new Position( [ 2 ], root ),
  70. new Position( [ 1 ], root ),
  71. 2,
  72. doc.version
  73. )
  74. );
  75. expect( doc.version ).to.equal( 1 );
  76. expect( root.getChildCount() ).to.equal( 5 );
  77. expect( root.getChild( 0 ).character ).to.equal( 'x' );
  78. expect( root.getChild( 1 ).character ).to.equal( 'a' );
  79. expect( root.getChild( 2 ).character ).to.equal( 'r' );
  80. expect( root.getChild( 3 ).character ).to.equal( 'b' );
  81. expect( root.getChild( 4 ).character ).to.equal( 'x' );
  82. } );
  83. it( 'should move position of children in one node forward', () => {
  84. root.insertChildren( 0, 'xbarx' );
  85. doc.applyOperation(
  86. new MoveOperation(
  87. new Position( [ 1 ], root ),
  88. new Position( [ 4 ], root ),
  89. 2,
  90. doc.version
  91. )
  92. );
  93. expect( doc.version ).to.equal( 1 );
  94. expect( root.getChildCount() ).to.equal( 5 );
  95. expect( root.getChild( 0 ).character ).to.equal( 'x' );
  96. expect( root.getChild( 1 ).character ).to.equal( 'r' );
  97. expect( root.getChild( 2 ).character ).to.equal( 'b' );
  98. expect( root.getChild( 3 ).character ).to.equal( 'a' );
  99. expect( root.getChild( 4 ).character ).to.equal( 'x' );
  100. } );
  101. it( 'should create a move operation as a reverse', () => {
  102. let nodeList = new NodeList( 'bar' );
  103. let sourcePosition = new Position( [ 0 ], root );
  104. let targetPosition = new Position( [ 4 ], root );
  105. let operation = new MoveOperation( sourcePosition, targetPosition, nodeList.length, doc.version );
  106. let reverse = operation.getReversed();
  107. expect( reverse ).to.be.an.instanceof( MoveOperation );
  108. expect( reverse.baseVersion ).to.equal( 1 );
  109. expect( reverse.howMany ).to.equal( nodeList.length );
  110. expect( reverse.sourcePosition.isEqual( targetPosition ) ).to.be.true;
  111. expect( reverse.targetPosition.isEqual( sourcePosition ) ).to.be.true;
  112. } );
  113. it( 'should undo move node by applying reverse operation', () => {
  114. let p1 = new Element( 'p1', [], new Element( 'x' ) );
  115. let p2 = new Element( 'p2' );
  116. root.insertChildren( 0, [ p1, p2 ] );
  117. let operation = new MoveOperation(
  118. new Position( [ 0, 0 ], root ),
  119. new Position( [ 1, 0 ], root ),
  120. 1,
  121. doc.version
  122. );
  123. doc.applyOperation( operation );
  124. expect( doc.version ).to.equal( 1 );
  125. expect( root.getChildCount() ).to.equal( 2 );
  126. expect( p1.getChildCount() ).to.equal( 0 );
  127. expect( p2.getChildCount() ).to.equal( 1 );
  128. expect( p2.getChild( 0 ).name ).to.equal( 'x' );
  129. doc.applyOperation( operation.getReversed() );
  130. expect( doc.version ).to.equal( 2 );
  131. expect( root.getChildCount() ).to.equal( 2 );
  132. expect( p1.getChildCount() ).to.equal( 1 );
  133. expect( p1.getChild( 0 ).name ).to.equal( 'x' );
  134. expect( p2.getChildCount() ).to.equal( 0 );
  135. } );
  136. it( 'should throw an error if number of nodes to move exceeds the number of existing nodes in given element', () => {
  137. root.insertChildren( 0, 'xbarx' );
  138. let operation = new MoveOperation(
  139. new Position( [ 3 ], root ),
  140. new Position( [ 1 ], root ),
  141. 3,
  142. doc.version
  143. );
  144. expect( () => doc.applyOperation( operation ) ).to.throw( CKEditorError, /operation-move-nodes-do-not-exist/ );
  145. } );
  146. it( 'should throw an error if target or source parent-element specified by position does not exist', () => {
  147. let p = new Element( 'p' );
  148. p.insertChildren( 0, 'foo' );
  149. root.insertChildren( 0, [ 'ab', p ] );
  150. let operation = new MoveOperation(
  151. new Position( [ 2, 0 ], root ),
  152. new Position( [ 1 ], root ),
  153. 3,
  154. doc.version
  155. );
  156. root.removeChildren( 2, 1 );
  157. expect( () => doc.applyOperation( operation ) ).to.throw( CKEditorError, /operation-move-position-invalid/ );
  158. } );
  159. it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', () => {
  160. root.insertChildren( 0, 'xbarx' );
  161. let operation = new MoveOperation(
  162. new Position( [ 1 ], root ),
  163. new Position( [ 2 ], root ),
  164. 3,
  165. doc.version
  166. );
  167. expect( () => doc.applyOperation( operation ) ).to.throw( CKEditorError, /operation-move-range-into-itself/ );
  168. } );
  169. it( 'should throw an error if operation tries to move a range into a sub-tree of a node that is in that range', () => {
  170. let p = new Element( 'p', [], [ new Element( 'p' ) ] );
  171. root.insertChildren( 0, [ 'ab', p, 'xy' ] );
  172. let operation = new MoveOperation(
  173. new Position( [ 1 ], root ),
  174. new Position( [ 2, 0, 0 ], root ),
  175. 3,
  176. doc.version
  177. );
  178. expect( () => doc.applyOperation( operation ) ).to.throw( CKEditorError, /operation-move-node-into-itself/ );
  179. } );
  180. it( 'should not throw an error if operation move a range into a sibling', () => {
  181. let p = new Element( 'p' );
  182. root.insertChildren( 0, [ 'ab', p, 'xy' ] );
  183. let operation = new MoveOperation(
  184. new Position( [ 1 ], root ),
  185. new Position( [ 2, 0 ], root ),
  186. 1,
  187. doc.version
  188. );
  189. expect(
  190. () => {
  191. doc.applyOperation( operation );
  192. }
  193. ).not.to.throw();
  194. expect( root.getChildCount() ).to.equal( 4 );
  195. expect( p.getChildCount() ).to.equal( 1 );
  196. expect( p.getChild( 0 ).character ).to.equal( 'b' );
  197. } );
  198. it( 'should create operation with the same parameters when cloned', () => {
  199. let sourcePosition = new Position( [ 0 ], root );
  200. let targetPosition = new Position( [ 1 ], root );
  201. let howMany = 4;
  202. let baseVersion = doc.version;
  203. let op = new MoveOperation( sourcePosition, targetPosition, howMany, baseVersion );
  204. let clone = op.clone();
  205. // New instance rather than a pointer to the old instance.
  206. expect( clone ).not.to.be.equal( op );
  207. expect( clone ).to.be.instanceof( MoveOperation );
  208. expect( clone.sourcePosition.isEqual( sourcePosition ) ).to.be.true;
  209. expect( clone.targetPosition.isEqual( targetPosition ) ).to.be.true;
  210. expect( clone.howMany ).to.equal( howMany );
  211. expect( clone.baseVersion ).to.equal( baseVersion );
  212. } );
  213. describe( 'getTransformedBy', () => {
  214. let nodeA, nodeB, op, baseVersion, sourcePosition, targetPosition, rangeEnd, howMany, expected;
  215. let expectOperation;
  216. beforeEach( () => {
  217. nodeA = new Node();
  218. nodeB = new Node();
  219. baseVersion = doc.version;
  220. sourcePosition = new Position( [ 2, 2, 4 ], root );
  221. targetPosition = new Position( [ 3, 3, 3 ], root );
  222. howMany = 2;
  223. rangeEnd = sourcePosition.clone();
  224. rangeEnd.offset += howMany;
  225. op = new MoveOperation( sourcePosition, targetPosition, howMany, baseVersion );
  226. expected = {
  227. type: MoveOperation,
  228. sourcePosition: sourcePosition.clone(),
  229. targetPosition: targetPosition.clone(),
  230. howMany: howMany,
  231. baseVersion: baseVersion + 1
  232. };
  233. expectOperation = bender.tools.operations.expectOperation( Position, Range );
  234. } );
  235. describe( 'InsertOperation', () => {
  236. // insert in different spots than move op
  237. it( 'should not change if origin and destination are different than insert address', () => {
  238. let transformBy = new InsertOperation(
  239. new Position( [ 1, 3, 2 ], root ),
  240. [ nodeA, nodeB ],
  241. baseVersion
  242. );
  243. let transOp = op.getTransformedBy( transformBy );
  244. expectOperation( transOp[ 0 ], expected );
  245. } );
  246. // insert inside moved node
  247. it( 'should not change if insert was inside moved sub-tree', () => {
  248. let transformBy = new InsertOperation(
  249. new Position( [ 2, 2, 3, 1 ], root ),
  250. [ nodeA, nodeB ],
  251. baseVersion
  252. );
  253. let transOp = op.getTransformedBy( transformBy );
  254. expectOperation( transOp[ 0 ], expected );
  255. } );
  256. // insert before to moved node
  257. it( 'should increment origin offset if insert was in the same parent but before moved node', () => {
  258. let transformBy = new InsertOperation(
  259. new Position( [ 2, 2, 0 ], root ),
  260. [ nodeA, nodeB ],
  261. baseVersion
  262. );
  263. let transOp = op.getTransformedBy( transformBy );
  264. expected.sourcePosition.offset += 2;
  265. expectOperation( transOp[ 0 ], expected );
  266. } );
  267. // insert after to moved node
  268. it( 'should not increment offset if insert was in the same parent but after moved node', () => {
  269. let transformBy = new InsertOperation(
  270. new Position( [ 2, 2, 6 ], root ),
  271. [ nodeA, nodeB ],
  272. baseVersion
  273. );
  274. let transOp = op.getTransformedBy( transformBy );
  275. expectOperation( transOp[ 0 ], expected );
  276. } );
  277. // insert next to a path to moved node
  278. it( 'should update origin path if insert was before a node from that path', () => {
  279. let transformBy = new InsertOperation(
  280. new Position( [ 2, 0 ], root ),
  281. [ nodeA, nodeB ],
  282. baseVersion
  283. );
  284. let transOp = op.getTransformedBy( transformBy );
  285. expected.sourcePosition.path[ 1 ] += 2;
  286. expectOperation( transOp[ 0 ], expected );
  287. } );
  288. it( 'should not update origin path if insert was after a node from that path', () => {
  289. let transformBy = new InsertOperation(
  290. new Position( [ 2, 3 ], root ),
  291. [ nodeA, nodeB ],
  292. baseVersion
  293. );
  294. let transOp = op.getTransformedBy( transformBy );
  295. expectOperation( transOp[ 0 ], expected );
  296. } );
  297. // insert before to moved node
  298. it( 'should increment destination offset if insert was in the same parent but before moved node', () => {
  299. let transformBy = new InsertOperation(
  300. new Position( [ 3, 3, 2 ], root ),
  301. [ nodeA, nodeB ],
  302. baseVersion
  303. );
  304. let transOp = op.getTransformedBy( transformBy );
  305. expected.targetPosition.offset += 2;
  306. expectOperation( transOp[ 0 ], expected );
  307. } );
  308. // insert after to moved node
  309. it( 'should not increment destination offset if insert was in the same parent but after moved node', () => {
  310. let transformBy = new InsertOperation(
  311. new Position( [ 3, 3, 4 ], root ),
  312. [ nodeA, nodeB ],
  313. baseVersion
  314. );
  315. let transOp = op.getTransformedBy( transformBy );
  316. expectOperation( transOp[ 0 ], expected );
  317. } );
  318. // insert next to a path to moved node
  319. it( 'should update destination path if insert was before a node from that path', () => {
  320. let transformBy = new InsertOperation(
  321. new Position( [ 3, 0 ], root ),
  322. [ nodeA, nodeB ],
  323. baseVersion
  324. );
  325. let transOp = op.getTransformedBy( transformBy );
  326. expected.targetPosition.path[ 1 ] += 2;
  327. expectOperation( transOp[ 0 ], expected );
  328. } );
  329. it( 'should not update destination path if insert was after a node from that path', () => {
  330. let transformBy = new InsertOperation(
  331. new Position( [ 3, 6 ], root ),
  332. [ nodeA, nodeB ],
  333. baseVersion
  334. );
  335. let transOp = op.getTransformedBy( transformBy );
  336. expectOperation( transOp[ 0 ], expected );
  337. } );
  338. it( 'should increment destination offset if insert is on the same position and move operation is weaker', () => {
  339. let transformBy = new InsertOperation(
  340. new Position( [ 3, 3, 3 ], root ),
  341. [ nodeA, nodeB ],
  342. baseVersion
  343. );
  344. let transOp = op.getTransformedBy( transformBy );
  345. expected.targetPosition.offset += 2;
  346. expectOperation( transOp[ 0 ], expected );
  347. } );
  348. it( 'should not increment destination offset if insert is on the same position and move operation is stronger', () => {
  349. let transformBy = new InsertOperation(
  350. new Position( [ 3, 3, 3 ], root ),
  351. [ nodeA, nodeB ],
  352. baseVersion
  353. );
  354. let transOp = op.getTransformedBy( transformBy, true );
  355. expectOperation( transOp[ 0 ], expected );
  356. } );
  357. it( 'should update destination path if insert is at the same offset and move operation is weaker', () => {
  358. let transformBy = new InsertOperation(
  359. new Position( [ 3, 3 ], root ),
  360. [ nodeA, nodeB ],
  361. baseVersion
  362. );
  363. let transOp = op.getTransformedBy( transformBy );
  364. expected.targetPosition.path[ 1 ] += 2;
  365. expectOperation( transOp[ 0 ], expected );
  366. } );
  367. it( 'should be split into two operations if insert was inside the moved range', () => {
  368. let transformBy = new InsertOperation(
  369. new Position( [ 2, 2, 5 ], root ),
  370. [ nodeA, nodeB ],
  371. baseVersion
  372. );
  373. let transOp = op.getTransformedBy( transformBy );
  374. expect( transOp ).to.be.instanceof( Array );
  375. expect( transOp.length ).to.equal( 2 );
  376. expected.sourcePosition.path = [ 2, 2, 7 ];
  377. expected.howMany = 1;
  378. expectOperation( transOp[ 0 ], expected );
  379. expected.sourcePosition.path = [ 2, 2, 4 ];
  380. expected.howMany = 1;
  381. expected.baseVersion++;
  382. expectOperation( transOp[ 1 ], expected );
  383. } );
  384. } );
  385. describe( 'ChangeOperation', () => {
  386. it( 'should not get updated', () => {
  387. let transformBy = new ChangeOperation(
  388. new Range( sourcePosition, rangeEnd ),
  389. new Attribute( 'abc', true ),
  390. new Attribute( 'abc', false ),
  391. baseVersion
  392. );
  393. let transOp = op.getTransformedBy( transformBy );
  394. expectOperation( transOp[ 0 ], expected );
  395. } );
  396. } );
  397. describe( 'MoveOperation', () => {
  398. it( 'should not change if both operations are happening in different parts of tree', () => {
  399. let transformBy = new MoveOperation(
  400. new Position( [ 1, 2 ], root ),
  401. new Position( [ 4, 1, 0 ], root ),
  402. 3,
  403. baseVersion
  404. );
  405. let transOp = op.getTransformedBy( transformBy );
  406. expectOperation( transOp[ 0 ], expected );
  407. } );
  408. describe( 'when incoming move origin node does not and is not contained by on-site move origin node', () => {
  409. it( 'should increment origin offset if affected by on-site move-to', () => {
  410. let transformBy = new MoveOperation(
  411. new Position( [ 4, 1, 0 ], root ),
  412. new Position( [ 2, 2, 0 ], root ),
  413. 2,
  414. baseVersion
  415. );
  416. let transOp = op.getTransformedBy( transformBy );
  417. expected.sourcePosition.offset += 2;
  418. expectOperation( transOp[ 0 ], expected );
  419. } );
  420. it( 'should decrement origin offset if affected by on-site move-out', () => {
  421. let transformBy = new MoveOperation(
  422. new Position( [ 2, 2, 0 ], root ),
  423. new Position( [ 4, 1, 0 ], root ),
  424. 2,
  425. baseVersion
  426. );
  427. let transOp = op.getTransformedBy( transformBy );
  428. expected.sourcePosition.offset -= 2;
  429. expectOperation( transOp[ 0 ], expected );
  430. } );
  431. it( 'should not decrement origin offset if on-site moved range is after incoming moved range', () => {
  432. let transformBy = new MoveOperation(
  433. new Position( [ 2, 2, 9 ], root ),
  434. new Position( [ 4, 1, 0 ], root ),
  435. 2,
  436. baseVersion
  437. );
  438. let transOp = op.getTransformedBy( transformBy, true );
  439. expectOperation( transOp[ 0 ], expected );
  440. } );
  441. it( 'should update origin path if affected by on-site move-to', () => {
  442. let transformBy = new MoveOperation(
  443. new Position( [ 4, 1, 0 ], root ),
  444. new Position( [ 2, 1 ], root ),
  445. 2,
  446. baseVersion
  447. );
  448. let transOp = op.getTransformedBy( transformBy );
  449. expected.sourcePosition.path[ 1 ] += 2;
  450. expectOperation( transOp[ 0 ], expected );
  451. } );
  452. it( 'should not update origin path if on-site moved range is after a node from that path', () => {
  453. let transformBy = new MoveOperation(
  454. new Position( [ 2, 3 ], root ),
  455. new Position( [ 4, 1, 0 ], root ),
  456. 2,
  457. baseVersion
  458. );
  459. let transOp = op.getTransformedBy( transformBy );
  460. expectOperation( transOp[ 0 ], expected );
  461. } );
  462. it( 'should update destination path if affected by on-site move-to', () => {
  463. let transformBy = new MoveOperation(
  464. new Position( [ 4, 1, 0 ], root ),
  465. new Position( [ 3, 1 ], root ),
  466. 2,
  467. baseVersion
  468. );
  469. let transOp = op.getTransformedBy( transformBy );
  470. expected.targetPosition.path[ 1 ] += 2;
  471. expectOperation( transOp[ 0 ], expected );
  472. } );
  473. it( 'should update origin path if affected by on-site move-out', () => {
  474. let transformBy = new MoveOperation(
  475. new Position( [ 2, 0 ], root ),
  476. new Position( [ 4, 1, 0 ], root ),
  477. 2,
  478. baseVersion
  479. );
  480. let transOp = op.getTransformedBy( transformBy );
  481. expected.sourcePosition.path[ 1 ] -= 2;
  482. expectOperation( transOp[ 0 ], expected );
  483. } );
  484. it( 'should update origin path if affected by on-site move-out', () => {
  485. let transformBy = new MoveOperation(
  486. new Position( [ 3, 0 ], root ),
  487. new Position( [ 4, 1, 0 ], root ),
  488. 2,
  489. baseVersion
  490. );
  491. let transOp = op.getTransformedBy( transformBy );
  492. expected.targetPosition.path[ 1 ] -= 2;
  493. expectOperation( transOp[ 0 ], expected );
  494. } );
  495. it( 'should decrement destination offset if affected by on-site move-out', () => {
  496. let transformBy = new MoveOperation(
  497. new Position( [ 3, 3, 0 ], root ),
  498. new Position( [ 4, 1, 0 ], root ),
  499. 2,
  500. baseVersion
  501. );
  502. let transOp = op.getTransformedBy( transformBy );
  503. expected.targetPosition.offset -= 2;
  504. expectOperation( transOp[ 0 ], expected );
  505. } );
  506. it( 'should increment destination offset affected by on-site move-to', () => {
  507. let transformBy = new MoveOperation(
  508. new Position( [ 4, 1, 0 ], root ),
  509. new Position( [ 3, 3, 0 ], root ),
  510. 2,
  511. baseVersion
  512. );
  513. let transOp = op.getTransformedBy( transformBy );
  514. expected.targetPosition.offset += 2;
  515. expectOperation( transOp[ 0 ], expected );
  516. } );
  517. it( 'should get split into two operations if on-site move-to was inside incoming move range', () => {
  518. let transformBy = new MoveOperation(
  519. new Position( [ 4, 1, 0 ], root ),
  520. new Position( [ 2, 2, 5 ], root ),
  521. 2,
  522. baseVersion
  523. );
  524. let transOp = op.getTransformedBy( transformBy );
  525. expect( transOp ).to.be.instanceof( Array );
  526. expect( transOp.length ).to.equal( 2 );
  527. expected.howMany = 1;
  528. expected.sourcePosition.offset = 7;
  529. expectOperation( transOp[ 0 ], expected );
  530. expected.sourcePosition.offset = 4;
  531. expected.baseVersion++;
  532. expectOperation( transOp[ 1 ], expected );
  533. } );
  534. } );
  535. describe( 'when incoming move origin node sub-tree contains on-site move origin', () => {
  536. it( 'should increment origin offset if affected by on-site move-to', () => {
  537. let transformBy = new MoveOperation(
  538. new Position( [ 2, 2, 5, 1 ], root ),
  539. new Position( [ 2, 2, 0 ], root ),
  540. 2,
  541. baseVersion
  542. );
  543. let transOp = op.getTransformedBy( transformBy );
  544. expected.sourcePosition.offset += 2;
  545. expectOperation( transOp[ 0 ], expected );
  546. } );
  547. it( 'should update origin path if affected by on-site move-to', () => {
  548. let transformBy = new MoveOperation(
  549. new Position( [ 2, 2, 5, 1 ], root ),
  550. new Position( [ 2, 1 ], root ),
  551. 2,
  552. baseVersion
  553. );
  554. let transOp = op.getTransformedBy( transformBy );
  555. expected.sourcePosition.path[ 1 ] += 2;
  556. expectOperation( transOp[ 0 ], expected );
  557. } );
  558. it( 'should update destination path if affected by on-site move-to', () => {
  559. let transformBy = new MoveOperation(
  560. new Position( [ 2, 2, 5, 1 ], root ),
  561. new Position( [ 3, 1 ], root ),
  562. 2,
  563. baseVersion
  564. );
  565. let transOp = op.getTransformedBy( transformBy );
  566. expected.targetPosition.path[ 1 ] += 2;
  567. expectOperation( transOp[ 0 ], expected );
  568. } );
  569. it( 'should increment destination offset affected by on-site move-to', () => {
  570. let transformBy = new MoveOperation(
  571. new Position( [ 2, 2, 5, 1 ], root ),
  572. new Position( [ 3, 3, 0 ], root ),
  573. 2,
  574. baseVersion
  575. );
  576. let transOp = op.getTransformedBy( transformBy );
  577. expected.targetPosition.offset += 2;
  578. expectOperation( transOp[ 0 ], expected );
  579. } );
  580. it( 'should get split into two operations if on-site move-to was inside incoming move range', () => {
  581. let transformBy = new MoveOperation(
  582. new Position( [ 2, 2, 5, 1 ], root ),
  583. new Position( [ 2, 2, 5 ], root ),
  584. 2,
  585. baseVersion
  586. );
  587. let transOp = op.getTransformedBy( transformBy );
  588. expect( transOp ).to.be.instanceof( Array );
  589. expect( transOp.length ).to.equal( 2 );
  590. expected.howMany = 1;
  591. expected.sourcePosition.offset = 7;
  592. expectOperation( transOp[ 0 ], expected );
  593. expected.sourcePosition.offset = 4;
  594. expected.baseVersion++;
  595. expectOperation( transOp[ 1 ], expected );
  596. } );
  597. } );
  598. it( 'should not change if on-site move is from non-affecting position to inside of moved sub-tree', () => {
  599. let transformBy = new MoveOperation(
  600. new Position( [ 4, 1, 0 ], root ),
  601. new Position( [ 2, 2, 5, 1 ], root ),
  602. 2,
  603. baseVersion
  604. );
  605. let transOp = op.getTransformedBy( transformBy );
  606. expectOperation( transOp[ 0 ], expected );
  607. } );
  608. it( 'should update origin address if on-site move origin node sub-tree includes incoming move origin node', () => {
  609. let transformBy = new MoveOperation(
  610. new Position( [ 2, 1 ], root ),
  611. new Position( [ 4, 2 ], root ),
  612. 3,
  613. baseVersion
  614. );
  615. let transOp = op.getTransformedBy( transformBy );
  616. expected.sourcePosition.path = [ 4, 3, 4 ];
  617. expectOperation( transOp[ 0 ], expected );
  618. } );
  619. it( 'should update destination address if incoming move destination is inside of on-site moved sub-tree', () => {
  620. let transformBy = new MoveOperation(
  621. new Position( [ 3, 2 ], root ),
  622. new Position( [ 0, 1 ], root ),
  623. 3,
  624. baseVersion
  625. );
  626. let transOp = op.getTransformedBy( transformBy );
  627. expected.targetPosition.path = [ 0, 2, 3 ];
  628. expectOperation( transOp[ 0 ], expected );
  629. } );
  630. describe( 'when both move operations\' destinations are inside of moved sub-trees', () => {
  631. it( 'should be changed to operation reversing site-on move', () => {
  632. let transformBy = new MoveOperation(
  633. new Position( [ 3, 2 ], root ),
  634. new Position( [ 2, 2, 5, 0 ], root ),
  635. 3,
  636. baseVersion
  637. );
  638. let transOp = op.getTransformedBy( transformBy );
  639. let reversed = transformBy.getReversed();
  640. expected.sourcePosition = reversed.sourcePosition;
  641. expected.targetPosition = reversed.targetPosition;
  642. expected.howMany = reversed.howMany;
  643. expectOperation( transOp[ 0 ], expected );
  644. } );
  645. } );
  646. describe( 'when both move operations have same range', () => {
  647. it( 'should be changed to no-op if incoming operation is weaker', () => {
  648. let transformBy = new MoveOperation(
  649. op.sourcePosition.clone(),
  650. new Position( [ 4, 1, 0 ], root ),
  651. op.howMany,
  652. baseVersion
  653. );
  654. let transOp = op.getTransformedBy( transformBy );
  655. expectOperation( transOp[ 0 ], {
  656. type: NoOperation,
  657. baseVersion: baseVersion + 1
  658. } );
  659. } );
  660. it( 'should have it\'s origin address changed if incoming operation is stronger', () => {
  661. let transformBy = new MoveOperation(
  662. op.sourcePosition.clone(),
  663. new Position( [ 4, 1, 0 ], root ),
  664. op.howMany,
  665. baseVersion
  666. );
  667. let transOp = op.getTransformedBy( transformBy, true );
  668. expected.sourcePosition.path = [ 4, 1, 0 ];
  669. expectOperation( transOp[ 0 ], expected );
  670. } );
  671. } );
  672. describe( 'when incoming range is contained by on-site range', () => {
  673. it( 'should be changed to no-op if incoming operation is weaker', () => {
  674. let transformBy = new MoveOperation(
  675. new Position( [ 2, 2, 3 ], root ),
  676. new Position( [ 4, 1, 0 ], root ),
  677. 4,
  678. baseVersion
  679. );
  680. let transOp = op.getTransformedBy( transformBy );
  681. expectOperation( transOp[ 0 ], {
  682. type: NoOperation,
  683. baseVersion: baseVersion + 1
  684. } );
  685. } );
  686. it( 'should have it\'s origin address changed if incoming operation has higher site id', () => {
  687. let transformBy = new MoveOperation(
  688. new Position( [ 2, 2, 3 ], root ),
  689. new Position( [ 4, 1, 0 ], root ),
  690. 4,
  691. baseVersion
  692. );
  693. let transOp = op.getTransformedBy( transformBy, true );
  694. expected.sourcePosition.path = [ 4, 1, 1 ];
  695. expectOperation( transOp[ 0 ], expected );
  696. } );
  697. } );
  698. describe( 'when incoming range intersects on right-side with on-site range', () => {
  699. it( 'should get shrunk if it is weaker', () => {
  700. let transformBy = new MoveOperation(
  701. new Position( [ 2, 2, 3 ], root ),
  702. new Position( [ 4, 1, 0 ], root ),
  703. 2,
  704. baseVersion
  705. );
  706. let transOp = op.getTransformedBy( transformBy );
  707. expected.sourcePosition.path = [ 2, 2, 3 ];
  708. expected.howMany = 1;
  709. expectOperation( transOp[ 0 ], expected );
  710. } );
  711. it( 'should get split into two operations if it is stronger', () => {
  712. let transformBy = new MoveOperation(
  713. new Position( [ 2, 2, 3 ], root ),
  714. new Position( [ 4, 1, 0 ], root ),
  715. 2,
  716. baseVersion
  717. );
  718. let transOp = op.getTransformedBy( transformBy, true );
  719. expect( transOp ).to.be.instanceof( Array );
  720. expect( transOp.length ).to.equal( 2 );
  721. expected.sourcePosition.path = [ 2, 2, 3 ];
  722. expectOperation( transOp[ 0 ], {
  723. type: MoveOperation,
  724. sourcePosition: new Position( [ 4, 1, 1 ], root ),
  725. targetPosition: expected.targetPosition,
  726. howMany: 1,
  727. baseVersion: expected.baseVersion
  728. } );
  729. expected.howMany = 1;
  730. expected.baseVersion++;
  731. expectOperation( transOp[ 1 ], expected );
  732. } );
  733. } );
  734. describe( 'when incoming range intersects on left-side with on-site range', () => {
  735. it( 'should get shrunk if it has lower site id', () => {
  736. let transformBy = new MoveOperation(
  737. new Position( [ 2, 2, 5 ], root ),
  738. new Position( [ 4, 1, 0 ], root ),
  739. 2,
  740. baseVersion
  741. );
  742. let transOp = op.getTransformedBy( transformBy );
  743. expected.howMany = 1;
  744. expectOperation( transOp[ 0 ], expected );
  745. } );
  746. it( 'should get split into two operations (one of them with updated address and offset) if it has higher site id', () => {
  747. let transformBy = new MoveOperation(
  748. new Position( [ 2, 2, 5 ], root ),
  749. new Position( [ 4, 1, 0 ], root ),
  750. 2,
  751. baseVersion
  752. );
  753. let transOp = op.getTransformedBy( transformBy, true );
  754. expect( transOp ).to.be.instanceof( Array );
  755. expect( transOp.length ).to.equal( 2 );
  756. expectOperation( transOp[ 0 ], {
  757. type: MoveOperation,
  758. sourcePosition: new Position( [ 4, 1, 0 ], root ),
  759. targetPosition: expected.targetPosition,
  760. howMany: 1,
  761. baseVersion: expected.baseVersion
  762. } );
  763. expected.howMany = 1;
  764. expected.baseVersion++;
  765. expectOperation( transOp[ 1 ], expected );
  766. } );
  767. } );
  768. describe( 'when incoming range contains on-site range', () => {
  769. it( 'should get shrunk if it has lower site id', () => {
  770. op.howMany = 4;
  771. let transformBy = new MoveOperation(
  772. new Position( [ 2, 2, 5 ], root ),
  773. new Position( [ 4, 1, 0 ], root ),
  774. 2,
  775. baseVersion
  776. );
  777. let transOp = op.getTransformedBy( transformBy );
  778. expected.howMany = 2;
  779. expectOperation( transOp[ 0 ], expected );
  780. } );
  781. it( 'should get split into two operations if it has higher site id', () => {
  782. op.howMany = 4;
  783. let transformBy = new MoveOperation(
  784. new Position( [ 2, 2, 5 ], root ),
  785. new Position( [ 4, 1, 0 ], root ),
  786. 2,
  787. baseVersion
  788. );
  789. let transOp = op.getTransformedBy( transformBy, true );
  790. expect( transOp ).to.be.instanceof( Array );
  791. expect( transOp.length ).to.equal( 2 );
  792. expectOperation( transOp[ 0 ], {
  793. type: MoveOperation,
  794. sourcePosition: new Position( [ 4, 1, 0 ], root ),
  795. targetPosition: expected.targetPosition,
  796. howMany: 2,
  797. baseVersion: expected.baseVersion
  798. } );
  799. expected.howMany = 2;
  800. expected.baseVersion++;
  801. expectOperation( transOp[ 1 ], expected );
  802. } );
  803. it( 'should get split into three operations if it has higher site id and on-site destination is inside moved range', () => {
  804. op.howMany = 6;
  805. let transformBy = new MoveOperation(
  806. new Position( [ 2, 2, 5 ], root ),
  807. new Position( [ 2, 2, 9 ], root ),
  808. 2,
  809. baseVersion
  810. );
  811. let transOp = op.getTransformedBy( transformBy, true );
  812. expect( transOp ).to.be.instanceof( Array );
  813. expect( transOp.length ).to.equal( 3 );
  814. expected.sourcePosition.path = [ 2, 2, 9 ];
  815. expected.howMany = 1;
  816. expectOperation( transOp[ 0 ], expected );
  817. expected.sourcePosition.path = [ 2, 2, 7 ];
  818. expected.howMany = 2;
  819. expected.baseVersion++;
  820. expectOperation( transOp[ 1 ], expected );
  821. expected.sourcePosition.path = [ 2, 2, 4 ];
  822. expected.howMany = 3;
  823. expected.baseVersion++;
  824. expectOperation( transOp[ 2 ], expected );
  825. } );
  826. } );
  827. } );
  828. } );
  829. } );