changeoperation.js 34 KB

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