batch.js 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Batch from '../../src/model/batch';
  6. import Delta from '../../src/model/delta/delta';
  7. import InsertDelta from '../../src/model/delta/insertdelta';
  8. import WeakInsertDelta from '../../src/model/delta/weakinsertdelta';
  9. import Operation from '../../src/model/operation/operation';
  10. import InsertOperation from '../../src/model/operation/insertoperation';
  11. import MarkerOperation from '../../src/model/operation/markeroperation';
  12. import Document from '../../src/model/document';
  13. import DocumentFragment from '../../src/model/documentfragment';
  14. import Element from '../../src/model/element';
  15. import Text from '../../src/model/text';
  16. import Position from '../../src/model/position';
  17. import Range from '../../src/model/range';
  18. import count from '@ckeditor/ckeditor5-utils/src/count';
  19. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  20. import { stringify } from '../../src/dev-utils/model';
  21. import { getNodesAndText } from '../../tests/model/_utils/utils';
  22. describe( 'Batch', () => {
  23. describe( 'type', () => {
  24. it( 'should default to "default"', () => {
  25. const batch = new Batch( new Document() );
  26. expect( batch.type ).to.equal( 'default' );
  27. } );
  28. it( 'should be set to the value set in constructor', () => {
  29. const batch = new Batch( new Document(), 'ignore' );
  30. expect( batch.type ).to.equal( 'ignore' );
  31. } );
  32. } );
  33. describe( 'baseVersion', () => {
  34. it( 'should return base version of first delta from the batch', () => {
  35. const batch = new Batch( new Document() );
  36. const delta = new Delta();
  37. const operation = new Operation( 2 );
  38. delta.addOperation( operation );
  39. batch.addDelta( delta );
  40. expect( batch.baseVersion ).to.equal( 2 );
  41. } );
  42. it( 'should return null if there are no deltas in batch', () => {
  43. const batch = new Batch( new Document() );
  44. expect( batch.baseVersion ).to.be.null;
  45. } );
  46. } );
  47. describe( 'addDelta()', () => {
  48. it( 'should add delta to the batch', () => {
  49. const batch = new Batch( new Document() );
  50. const deltaA = new Delta();
  51. const deltaB = new Delta();
  52. batch.addDelta( deltaA );
  53. batch.addDelta( deltaB );
  54. expect( batch.deltas.length ).to.equal( 2 );
  55. expect( batch.deltas[ 0 ] ).to.equal( deltaA );
  56. expect( batch.deltas[ 1 ] ).to.equal( deltaB );
  57. } );
  58. } );
  59. describe( 'getOperations()', () => {
  60. it( 'should return collection of operations from all deltas', () => {
  61. const doc = new Document();
  62. const batch = new Batch( doc );
  63. const deltaA = new Delta();
  64. const deltaB = new Delta();
  65. const ops = [
  66. new Operation( doc.version ),
  67. new Operation( doc.version + 1 ),
  68. new Operation( doc.version + 2 )
  69. ];
  70. batch.addDelta( deltaA );
  71. deltaA.addOperation( ops[ 0 ] );
  72. batch.addDelta( deltaB );
  73. deltaA.addOperation( ops[ 1 ] );
  74. deltaA.addOperation( ops[ 2 ] );
  75. expect( Array.from( batch.getOperations() ) ).to.deep.equal( ops );
  76. expect( batch.getOperations() ).to.have.property( 'next' );
  77. } );
  78. } );
  79. describe( 'createText()', () => {
  80. let doc, batch;
  81. beforeEach( () => {
  82. doc = new Document();
  83. batch = doc.batch();
  84. } );
  85. it( 'should create text node', () => {
  86. const text = batch.createText( 'foo' );
  87. expect( text ).to.instanceof( Text );
  88. expect( text.data ).to.equal( 'foo' );
  89. expect( Array.from( text.getAttributes() ) ).to.length( 0 );
  90. } );
  91. it( 'should create text with attributes', () => {
  92. const text = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } );
  93. expect( Array.from( text.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
  94. } );
  95. } );
  96. describe( 'createElement()', () => {
  97. let doc, batch;
  98. beforeEach( () => {
  99. doc = new Document();
  100. batch = doc.batch();
  101. } );
  102. it( 'should create element', () => {
  103. const element = batch.createElement( 'foo' );
  104. expect( element ).to.instanceof( Element );
  105. expect( element.name ).to.equal( 'foo' );
  106. expect( Array.from( element.getAttributes() ) ).to.length( 0 );
  107. } );
  108. it( 'should create element with attributes', () => {
  109. const element = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } );
  110. expect( Array.from( element.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] );
  111. } );
  112. } );
  113. describe( 'createDocumentFragment()', () => {
  114. let doc, batch;
  115. beforeEach( () => {
  116. doc = new Document();
  117. batch = doc.batch();
  118. } );
  119. it( 'should create element', () => {
  120. const element = batch.createDocumentFragment();
  121. expect( element ).to.instanceof( DocumentFragment );
  122. } );
  123. } );
  124. describe( 'insert()', () => {
  125. let doc, batch;
  126. beforeEach( () => {
  127. doc = new Document();
  128. batch = doc.batch();
  129. } );
  130. it( 'should insert node at given position', () => {
  131. const parent = batch.createDocumentFragment();
  132. const child = batch.createElement( 'child' );
  133. const textChild = batch.createText( 'textChild' );
  134. batch.insert( child, new Position( parent, [ 0 ] ) );
  135. batch.insert( textChild, new Position( parent, [ 1 ] ) );
  136. expect( Array.from( parent ) ).to.deep.equal( [ child, textChild ] );
  137. } );
  138. it( 'should insert node at the beginning of given element', () => {
  139. const parent = batch.createDocumentFragment();
  140. const child1 = batch.createElement( 'child' );
  141. const child2 = batch.createElement( 'child' );
  142. batch.insert( child1, parent );
  143. batch.insert( child2, parent );
  144. expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child2, child1 ] );
  145. } );
  146. it( 'should insert node at the end of given element', () => {
  147. const parent = batch.createDocumentFragment();
  148. const child1 = batch.createElement( 'child' );
  149. const child2 = batch.createElement( 'child' );
  150. batch.insert( child1, parent );
  151. batch.insert( child2, parent, 'end' );
  152. expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2 ] );
  153. } );
  154. it( 'should insert node at the given offset of given element', () => {
  155. const parent = batch.createDocumentFragment();
  156. const child1 = batch.createElement( 'child' );
  157. const child2 = batch.createElement( 'child' );
  158. const child3 = batch.createElement( 'child' );
  159. batch.insert( child3, parent );
  160. batch.insert( child1, parent );
  161. batch.insert( child2, parent, 1 );
  162. expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
  163. } );
  164. it( 'should insert node before the given node', () => {
  165. const parent = batch.createDocumentFragment();
  166. const child1 = batch.createElement( 'child' );
  167. const child2 = batch.createElement( 'child' );
  168. const child3 = batch.createElement( 'child' );
  169. batch.insert( child3, parent );
  170. batch.insert( child1, parent );
  171. batch.insert( child2, child3, 'before' );
  172. expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
  173. } );
  174. it( 'should insert node after the given node', () => {
  175. const parent = batch.createDocumentFragment();
  176. const child1 = batch.createElement( 'child' );
  177. const child2 = batch.createElement( 'child' );
  178. const child3 = batch.createElement( 'child' );
  179. batch.insert( child3, parent );
  180. batch.insert( child1, parent );
  181. batch.insert( child2, child1, 'after' );
  182. expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] );
  183. } );
  184. it( 'should create proper delta for inserting element', () => {
  185. const parent = batch.createDocumentFragment();
  186. const element = batch.createElement( 'child' );
  187. const spy = sinon.spy( doc, 'applyOperation' );
  188. batch.insert( element, parent );
  189. sinon.assert.calledOnce( spy );
  190. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  191. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
  192. expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
  193. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  194. } );
  195. it( 'should create proper delta for inserting text', () => {
  196. const parent = batch.createDocumentFragment();
  197. const text = batch.createText( 'child' );
  198. const spy = sinon.spy( doc, 'applyOperation' );
  199. batch.insert( text, parent );
  200. sinon.assert.calledOnce( spy );
  201. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  202. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
  203. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  204. } );
  205. it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
  206. const rootA = doc.createRoot();
  207. const parent1 = batch.createElement( 'parent' );
  208. const parent2 = batch.createElement( 'parent' );
  209. const node = batch.createText( 'foo' );
  210. batch.insert( node, parent1 );
  211. batch.insert( parent1, rootA );
  212. batch.insert( parent2, rootA );
  213. const spy = sinon.spy( doc, 'applyOperation' );
  214. batch.insert( node, parent2 );
  215. // Verify result.
  216. expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
  217. expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
  218. // Verify deltas and operations.
  219. sinon.assert.calledOnce( spy );
  220. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  221. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  222. } );
  223. it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
  224. const rootA = doc.createRoot( '$root', 'A' );
  225. const rootB = doc.createRoot( '$root', 'B' );
  226. const node = batch.createText( 'foo' );
  227. batch.insert( node, rootA );
  228. const spy = sinon.spy( doc, 'applyOperation' );
  229. batch.insert( node, rootB );
  230. // Verify result.
  231. expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
  232. expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
  233. // Verify deltas and operations.
  234. sinon.assert.calledOnce( spy );
  235. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  236. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  237. } );
  238. it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
  239. const docFragA = batch.createDocumentFragment();
  240. const parent1 = batch.createElement( 'parent' );
  241. const parent2 = batch.createElement( 'parent' );
  242. const node = batch.createText( 'foo' );
  243. batch.insert( node, parent1 );
  244. batch.insert( parent1, docFragA );
  245. batch.insert( parent2, docFragA );
  246. const spy = sinon.spy( doc, 'applyOperation' );
  247. batch.insert( node, parent2 );
  248. // Verify result.
  249. expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
  250. expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
  251. // Verify deltas and operations.
  252. sinon.assert.calledOnce( spy );
  253. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  254. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  255. } );
  256. it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
  257. const root = doc.createRoot();
  258. const docFrag = batch.createDocumentFragment();
  259. const node = batch.createText( 'foo' );
  260. batch.insert( node, root );
  261. const spy = sinon.spy( doc, 'applyOperation' );
  262. batch.insert( node, docFrag );
  263. // Verify result.
  264. expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
  265. expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
  266. // Verify deltas and operations.
  267. sinon.assert.calledTwice( spy );
  268. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
  269. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  270. expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
  271. expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
  272. } );
  273. it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
  274. const docFragA = batch.createDocumentFragment();
  275. const docFragB = batch.createDocumentFragment();
  276. const node = batch.createText( 'foo' );
  277. batch.insert( node, docFragA );
  278. const spy = sinon.spy( doc, 'applyOperation' );
  279. batch.insert( node, docFragB );
  280. // Verify result.
  281. expect( Array.from( docFragA ) ).to.deep.equal( [] );
  282. expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
  283. // Verify deltas and operations.
  284. sinon.assert.calledTwice( spy );
  285. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
  286. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  287. expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
  288. expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
  289. } );
  290. it.skip( 'should transfer markers from given DocumentFragment', () => {
  291. const documentFragment = batch.createDocumentFragment();
  292. const li = batch.createElement( 'li' );
  293. batch.insert( batch.createText( 'foo bar' ), li );
  294. batch.insert( li, documentFragment );
  295. const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) );
  296. documentFragment.markers.set( 'marker', marker );
  297. batch.insert( documentFragment, new Position( root, [ 3, 0 ] ) );
  298. expect( Array.from( doc.markers ).length ).to.equal( 1 );
  299. expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab<p></p><ul><li>f[oo b]ar</li></ul>c' );
  300. } );
  301. it.skip( 'should set each marker as separate operation', () => {
  302. sinon.spy( doc, 'applyOperation' );
  303. const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] );
  304. const marker1 = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 2 ] ) );
  305. const marker2 = new Range( new Position( documentFragment, [ 0, 5 ] ), new Position( documentFragment, [ 0, 6 ] ) );
  306. documentFragment.markers.set( 'marker1', marker1 );
  307. documentFragment.markers.set( 'marker2', marker2 );
  308. batch.insert( new Position( root, [ 3, 0 ] ), documentFragment );
  309. expect( doc.applyOperation.calledThrice );
  310. expect( doc.applyOperation.firstCall.calledWith( sinon.match( operation => operation instanceof InsertOperation ) ) );
  311. expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) );
  312. expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) );
  313. } );
  314. it( 'should be chainable', () => {
  315. const parent = batch.createDocumentFragment();
  316. const child = batch.createElement( 'child' );
  317. expect( batch.insert( child, parent ) ).to.equal( batch );
  318. } );
  319. } );
  320. describe( 'insertText()', () => {
  321. let doc, batch;
  322. beforeEach( () => {
  323. doc = new Document();
  324. batch = doc.batch();
  325. } );
  326. it( 'should create and insert text node with attributes at given position', () => {
  327. const parent = batch.createDocumentFragment();
  328. batch.insertText( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );
  329. expect( parent.childCount ).to.equal( 1 );
  330. expect( parent.getChild( 0 ) ).to.instanceof( Text );
  331. expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
  332. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
  333. } );
  334. it( 'should create and insert text node with no attributes at given position', () => {
  335. const parent = batch.createDocumentFragment();
  336. batch.insertText( 'foo', null, new Position( parent, [ 0 ] ) );
  337. expect( parent.childCount ).to.equal( 1 );
  338. expect( parent.getChild( 0 ) ).to.instanceof( Text );
  339. expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
  340. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
  341. } );
  342. it( 'should create and insert text node at the beginning of given element', () => {
  343. const parent = batch.createDocumentFragment();
  344. batch.insert( batch.createElement( 'child' ), parent );
  345. batch.insertText( 'foo', null, parent );
  346. expect( parent.childCount ).to.equal( 2 );
  347. expect( parent.getChild( 0 ) ).to.instanceof( Text );
  348. expect( parent.getChild( 1 ) ).to.instanceof( Element );
  349. } );
  350. it( 'should create and insert text node at the end of given element', () => {
  351. const parent = batch.createDocumentFragment();
  352. batch.insert( batch.createElement( 'child' ), parent );
  353. batch.insertText( 'foo', null, parent, 'end' );
  354. expect( parent.childCount ).to.equal( 2 );
  355. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  356. expect( parent.getChild( 1 ) ).to.instanceof( Text );
  357. } );
  358. it( 'should create and insert text node at the given offset of given element', () => {
  359. const parent = batch.createDocumentFragment();
  360. batch.insert( batch.createElement( 'child' ), parent );
  361. batch.insert( batch.createElement( 'child' ), parent );
  362. batch.insertText( 'foo', null, parent, 1 );
  363. expect( parent.childCount ).to.equal( 3 );
  364. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  365. expect( parent.getChild( 1 ) ).to.instanceof( Text );
  366. expect( parent.getChild( 2 ) ).to.instanceof( Element );
  367. } );
  368. it( 'should create and insert text node before the given node', () => {
  369. const parent = batch.createDocumentFragment();
  370. const child1 = batch.createElement( 'child' );
  371. const child2 = batch.createElement( 'child' );
  372. batch.insert( child1, parent );
  373. batch.insert( child2, parent, 'end' );
  374. batch.insertText( 'foo', null, child2, 'before' );
  375. expect( parent.childCount ).to.equal( 3 );
  376. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  377. expect( parent.getChild( 1 ) ).to.instanceof( Text );
  378. expect( parent.getChild( 2 ) ).to.instanceof( Element );
  379. } );
  380. it( 'should create and insert text node after the given node', () => {
  381. const parent = batch.createDocumentFragment();
  382. const child1 = batch.createElement( 'child' );
  383. const child2 = batch.createElement( 'child' );
  384. batch.insert( child1, parent );
  385. batch.insert( child2, parent, 'end' );
  386. batch.insertText( 'foo', null, child1, 'after' );
  387. expect( parent.childCount ).to.equal( 3 );
  388. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  389. expect( parent.getChild( 1 ) ).to.instanceof( Text );
  390. expect( parent.getChild( 2 ) ).to.instanceof( Element );
  391. } );
  392. it( 'should create proper delta', () => {
  393. const parent = batch.createDocumentFragment();
  394. const spy = sinon.spy( doc, 'applyOperation' );
  395. batch.insertText( 'foo', null, parent );
  396. sinon.assert.calledOnce( spy );
  397. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  398. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
  399. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  400. } );
  401. it( 'should be chainable', () => {
  402. const parent = batch.createDocumentFragment();
  403. expect( batch.insertText( 'foo', null, parent ) ).to.equal( batch );
  404. } );
  405. } );
  406. describe( 'insertElement()', () => {
  407. let doc, batch;
  408. beforeEach( () => {
  409. doc = new Document();
  410. batch = doc.batch();
  411. } );
  412. it( 'should create and insert element with attributes at given position', () => {
  413. const parent = batch.createDocumentFragment();
  414. batch.insertElement( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) );
  415. expect( parent.childCount ).to.equal( 1 );
  416. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  417. expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
  418. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
  419. } );
  420. it( 'should create and insert element with no attributes at given position', () => {
  421. const parent = batch.createDocumentFragment();
  422. batch.insertElement( 'foo', null, new Position( parent, [ 0 ] ) );
  423. expect( parent.childCount ).to.equal( 1 );
  424. expect( parent.getChild( 0 ) ).to.instanceof( Element );
  425. expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
  426. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
  427. } );
  428. it( 'should create and insert element at the beginning of given element', () => {
  429. const parent = batch.createDocumentFragment();
  430. batch.insert( batch.createElement( 'child' ), parent );
  431. batch.insertElement( 'foo', null, parent );
  432. expect( parent.childCount ).to.equal( 2 );
  433. expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
  434. expect( parent.getChild( 1 ).name ).to.equal( 'child' );
  435. } );
  436. it( 'should create and insert element at the end of given element', () => {
  437. const parent = batch.createDocumentFragment();
  438. batch.insert( batch.createElement( 'child' ), parent );
  439. batch.insertElement( 'foo', null, parent, 'end' );
  440. expect( parent.childCount ).to.equal( 2 );
  441. expect( parent.getChild( 0 ).name ).to.equal( 'child' );
  442. expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
  443. } );
  444. it( 'should create and insert element at the given offset of given element', () => {
  445. const parent = batch.createDocumentFragment();
  446. batch.insert( batch.createElement( 'child1' ), parent );
  447. batch.insert( batch.createElement( 'child2' ), parent, 'end' );
  448. batch.insertElement( 'foo', null, parent, 1 );
  449. expect( parent.childCount ).to.equal( 3 );
  450. expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
  451. expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
  452. expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
  453. } );
  454. it( 'should create and insert element before the given node', () => {
  455. const parent = batch.createDocumentFragment();
  456. const child1 = batch.createElement( 'child1' );
  457. const child2 = batch.createElement( 'child2' );
  458. batch.insert( child1, parent );
  459. batch.insert( child2, parent, 'end' );
  460. batch.insertElement( 'foo', null, child2, 'before' );
  461. expect( parent.childCount ).to.equal( 3 );
  462. expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
  463. expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
  464. expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
  465. } );
  466. it( 'should create and insert element after the given node', () => {
  467. const parent = batch.createDocumentFragment();
  468. const child1 = batch.createElement( 'child1' );
  469. const child2 = batch.createElement( 'child2' );
  470. batch.insert( child1, parent );
  471. batch.insert( child2, parent, 'end' );
  472. batch.insertElement( 'foo', null, child1, 'after' );
  473. expect( parent.childCount ).to.equal( 3 );
  474. expect( parent.getChild( 0 ).name ).to.equal( 'child1' );
  475. expect( parent.getChild( 1 ).name ).to.equal( 'foo' );
  476. expect( parent.getChild( 2 ).name ).to.equal( 'child2' );
  477. } );
  478. it( 'should create proper delta', () => {
  479. const parent = batch.createDocumentFragment();
  480. const spy = sinon.spy( doc, 'applyOperation' );
  481. batch.insertText( 'foo', null, parent );
  482. sinon.assert.calledOnce( spy );
  483. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  484. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
  485. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  486. } );
  487. it( 'should be chainable', () => {
  488. const parent = batch.createDocumentFragment();
  489. expect( batch.insertElement( 'foo', null, parent ) ).to.equal( batch );
  490. } );
  491. } );
  492. describe( 'append()', () => {
  493. let doc, batch;
  494. beforeEach( () => {
  495. doc = new Document();
  496. batch = doc.batch();
  497. } );
  498. it( 'should insert element at the end of the parent', () => {
  499. const parent = doc.batch().createDocumentFragment();
  500. const childText = doc.batch().createText( 'foo' );
  501. const childElement = doc.batch().createElement( 'foo' );
  502. batch.append( childText, parent );
  503. batch.append( childElement, parent );
  504. expect( Array.from( parent ) ).to.deep.equal( [ childText, childElement ] );
  505. } );
  506. it( 'should create proper delta', () => {
  507. const parent = batch.createDocumentFragment();
  508. const text = batch.createText( 'foo' );
  509. const spy = sinon.spy( doc, 'applyOperation' );
  510. batch.append( text, parent );
  511. sinon.assert.calledOnce( spy );
  512. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  513. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  514. } );
  515. it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => {
  516. const rootA = doc.createRoot();
  517. const parent1 = batch.createElement( 'parent' );
  518. const parent2 = batch.createElement( 'parent' );
  519. const node = batch.createText( 'foo' );
  520. batch.insert( node, parent1 );
  521. batch.insert( parent1, rootA );
  522. batch.insert( parent2, rootA );
  523. const spy = sinon.spy( doc, 'applyOperation' );
  524. batch.append( node, parent2 );
  525. // Verify result.
  526. expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
  527. expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
  528. // Verify deltas and operations.
  529. sinon.assert.calledOnce( spy );
  530. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  531. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  532. } );
  533. it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => {
  534. const rootA = doc.createRoot( '$root', 'A' );
  535. const rootB = doc.createRoot( '$root', 'B' );
  536. const node = batch.createText( 'foo' );
  537. batch.insert( node, rootA );
  538. const spy = sinon.spy( doc, 'applyOperation' );
  539. batch.append( node, rootB );
  540. // Verify result.
  541. expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] );
  542. expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] );
  543. // Verify deltas and operations.
  544. sinon.assert.calledOnce( spy );
  545. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  546. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  547. } );
  548. it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => {
  549. const docFragA = batch.createDocumentFragment();
  550. const parent1 = batch.createElement( 'parent' );
  551. const parent2 = batch.createElement( 'parent' );
  552. const node = batch.createText( 'foo' );
  553. batch.insert( node, parent1 );
  554. batch.insert( parent1, docFragA );
  555. batch.insert( parent2, docFragA );
  556. const spy = sinon.spy( doc, 'applyOperation' );
  557. batch.append( node, parent2 );
  558. // Verify result.
  559. expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] );
  560. expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] );
  561. // Verify deltas and operations.
  562. sinon.assert.calledOnce( spy );
  563. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' );
  564. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  565. } );
  566. it( 'should move element from one parent to the other within different document (document -> docFrag)', () => {
  567. const root = doc.createRoot();
  568. const docFrag = batch.createDocumentFragment();
  569. const node = batch.createText( 'foo' );
  570. batch.insert( node, root );
  571. const spy = sinon.spy( doc, 'applyOperation' );
  572. batch.append( node, docFrag );
  573. // Verify result.
  574. expect( Array.from( root.getChildren() ) ).to.deep.equal( [] );
  575. expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] );
  576. // Verify deltas and operations.
  577. sinon.assert.calledTwice( spy );
  578. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
  579. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  580. expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
  581. expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
  582. } );
  583. it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => {
  584. const docFragA = batch.createDocumentFragment();
  585. const docFragB = batch.createDocumentFragment();
  586. const node = batch.createText( 'foo' );
  587. batch.insert( node, docFragA );
  588. const spy = sinon.spy( doc, 'applyOperation' );
  589. batch.append( node, docFragB );
  590. // Verify result.
  591. expect( Array.from( docFragA ) ).to.deep.equal( [] );
  592. expect( Array.from( docFragB ) ).to.deep.equal( [ node ] );
  593. // Verify deltas and operations.
  594. sinon.assert.calledTwice( spy );
  595. expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' );
  596. expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch );
  597. expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' );
  598. expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch );
  599. } );
  600. it( 'should be chainable', () => {
  601. expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch );
  602. } );
  603. } );
  604. describe( 'appendText()', () => {
  605. let doc, batch;
  606. beforeEach( () => {
  607. doc = new Document();
  608. batch = doc.batch();
  609. } );
  610. it( 'should create and insert text node with attributes at the end of the parent', () => {
  611. const parent = batch.createDocumentFragment();
  612. batch.appendText( 'foo', { bar: 'biz' }, parent );
  613. batch.appendText( 'bar', { biz: 'bar' }, parent );
  614. expect( parent.childCount ).to.equal( 2 );
  615. expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
  616. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
  617. expect( parent.getChild( 1 ).data ).to.equal( 'bar' );
  618. expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
  619. } );
  620. it( 'should create and insert text node with no attributes at the end of the parent', () => {
  621. const parent = batch.createDocumentFragment();
  622. batch.appendText( 'foo', null, parent );
  623. expect( parent.childCount ).to.equal( 1 );
  624. expect( parent.getChild( 0 ) ).to.instanceof( Text );
  625. expect( parent.getChild( 0 ).data ).to.equal( 'foo' );
  626. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
  627. } );
  628. it( 'should create proper delta', () => {
  629. const parent = batch.createDocumentFragment();
  630. const spy = sinon.spy( doc, 'applyOperation' );
  631. batch.appendText( 'foo', null, parent );
  632. sinon.assert.calledOnce( spy );
  633. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  634. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta );
  635. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  636. } );
  637. it( 'should be chainable', () => {
  638. const parent = batch.createDocumentFragment();
  639. expect( batch.appendText( 'foo', null, parent ) ).to.equal( batch );
  640. } );
  641. } );
  642. describe( 'appendElement()', () => {
  643. let doc, batch;
  644. beforeEach( () => {
  645. doc = new Document();
  646. batch = doc.batch();
  647. } );
  648. it( 'should create and insert element with attributes at the end of the parent', () => {
  649. const parent = batch.createDocumentFragment();
  650. batch.appendElement( 'foo', { bar: 'biz' }, parent );
  651. batch.appendElement( 'bar', { biz: 'bar' }, parent );
  652. expect( parent.childCount ).to.equal( 2 );
  653. expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
  654. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] );
  655. expect( parent.getChild( 1 ).name ).to.equal( 'bar' );
  656. expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] );
  657. } );
  658. it( 'should create and insert element with no attributes at the end of the parent', () => {
  659. const parent = batch.createDocumentFragment();
  660. batch.appendElement( 'foo', null, parent );
  661. expect( parent.childCount ).to.equal( 1 );
  662. expect( parent.getChild( 0 ).name ).to.equal( 'foo' );
  663. expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] );
  664. } );
  665. it( 'should create proper delta', () => {
  666. const parent = batch.createDocumentFragment();
  667. const spy = sinon.spy( doc, 'applyOperation' );
  668. batch.appendElement( 'foo', null, parent );
  669. sinon.assert.calledOnce( spy );
  670. expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' );
  671. expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta );
  672. expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta );
  673. expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch );
  674. } );
  675. it( 'should be chainable', () => {
  676. const parent = batch.createDocumentFragment();
  677. expect( batch.appendElement( 'foo', null, parent ) ).to.equal( batch );
  678. } );
  679. } );
  680. describe( 'setAttribute() / removeAttribute()', () => {
  681. let batch, doc, root, spy;
  682. const correctDeltaMatcher = sinon.match( operation => {
  683. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  684. } );
  685. beforeEach( () => {
  686. doc = new Document();
  687. root = doc.createRoot();
  688. batch = doc.batch();
  689. } );
  690. describe( 'change attribute on node', () => {
  691. let node, text;
  692. beforeEach( () => {
  693. node = batch.createElement( 'p', { a: 1 } );
  694. text = batch.createText( 'c', { a: 1 } );
  695. batch.append( node, root );
  696. batch.append( text, root );
  697. spy = sinon.spy( doc, 'applyOperation' );
  698. } );
  699. describe( 'setAttribute', () => {
  700. it( 'should create the attribute on element', () => {
  701. batch.setAttribute( node, 'b', 2 );
  702. expect( spy.callCount ).to.equal( 1 );
  703. expect( node.getAttribute( 'b' ) ).to.equal( 2 );
  704. } );
  705. it( 'should change the attribute of element', () => {
  706. batch.setAttribute( node, 'a', 2 );
  707. expect( spy.callCount ).to.equal( 1 );
  708. expect( node.getAttribute( 'a' ) ).to.equal( 2 );
  709. } );
  710. it( 'should create the attribute on text node', () => {
  711. batch.setAttribute( text, 'b', 2 );
  712. expect( spy.callCount ).to.equal( 1 );
  713. expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 );
  714. } );
  715. it( 'should change the attribute of text node', () => {
  716. batch.setAttribute( text, 'a', 2 );
  717. expect( spy.callCount ).to.equal( 1 );
  718. expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 );
  719. } );
  720. it( 'should do nothing if the attribute value is the same', () => {
  721. batch.setAttribute( node, 'a', 1 );
  722. expect( spy.callCount ).to.equal( 0 );
  723. expect( node.getAttribute( 'a' ) ).to.equal( 1 );
  724. } );
  725. it( 'should be chainable', () => {
  726. const chain = batch.setAttribute( node, 'b', 2 );
  727. expect( chain ).to.equal( batch );
  728. } );
  729. it( 'should add delta to batch and operation to delta before applying operation', () => {
  730. batch.setAttribute( node, 'b', 2 );
  731. sinon.assert.calledWith( spy, correctDeltaMatcher );
  732. } );
  733. } );
  734. describe( 'removeAttribute', () => {
  735. it( 'should remove the attribute from element', () => {
  736. batch.removeAttribute( node, 'a' );
  737. expect( spy.callCount ).to.equal( 1 );
  738. expect( node.getAttribute( 'a' ) ).to.be.undefined;
  739. } );
  740. it( 'should remove the attribute from character', () => {
  741. batch.removeAttribute( text, 'a' );
  742. expect( spy.callCount ).to.equal( 1 );
  743. expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined;
  744. } );
  745. it( 'should do nothing if the attribute is not set', () => {
  746. batch.removeAttribute( node, 'b' );
  747. expect( spy.callCount ).to.equal( 0 );
  748. } );
  749. it( 'should be chainable', () => {
  750. const chain = batch.removeAttribute( node, 'a' );
  751. expect( chain ).to.equal( batch );
  752. } );
  753. it( 'should add delta to batch and operation to delta before applying operation', () => {
  754. batch.removeAttribute( node, 'a' );
  755. sinon.assert.calledWith( spy, correctDeltaMatcher );
  756. } );
  757. } );
  758. } );
  759. describe( 'change attribute on range', () => {
  760. beforeEach( () => {
  761. const element = batch.createElement( 'e', { a: 2 } );
  762. batch
  763. .appendText( 'xxx', { a: 1 }, root )
  764. .appendText( 'xxx', null, root )
  765. .appendText( 'xxx', { a: 1 }, root )
  766. .appendText( 'xxx', { a: 2 }, root )
  767. .appendText( 'xxx', null, root )
  768. .appendText( 'xxx', { a: 1 }, root )
  769. .appendText( 'xxx', null, element )
  770. .append( element, root )
  771. .appendText( 'xxx', null, root );
  772. spy = sinon.spy( doc, 'applyOperation' );
  773. } );
  774. function getRange( startIndex, endIndex ) {
  775. return new Range(
  776. Position.createFromParentAndOffset( root, startIndex ),
  777. Position.createFromParentAndOffset( root, endIndex )
  778. );
  779. }
  780. function getChangesAttrsCount() {
  781. let totalNumber = 0;
  782. for ( const delta of batch.deltas ) {
  783. for ( const operation of delta.operations ) {
  784. if ( operation.range ) {
  785. totalNumber += count( operation.range.getItems( { singleCharacters: true } ) );
  786. }
  787. }
  788. }
  789. return totalNumber;
  790. }
  791. function getCompressedAttrs() {
  792. // default: 111---111222---1112------
  793. const range = Range.createIn( root );
  794. return Array.from( range.getItems( { singleCharacters: true } ) )
  795. .map( item => item.getAttribute( 'a' ) || '-' )
  796. .join( '' );
  797. }
  798. describe( 'setAttribute', () => {
  799. it( 'should set the attribute on the range', () => {
  800. batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
  801. expect( spy.callCount ).to.equal( 1 );
  802. expect( getChangesAttrsCount() ).to.equal( 3 );
  803. expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' );
  804. } );
  805. it( 'should split the operations if parts of the range have different attributes', () => {
  806. batch.setAttribute( getRange( 4, 14 ), 'a', 3 );
  807. expect( spy.callCount ).to.equal( 4 );
  808. expect( getChangesAttrsCount() ).to.equal( 10 );
  809. expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' );
  810. } );
  811. it( 'should split the operations if parts of the part of the range have the attribute', () => {
  812. batch.setAttribute( getRange( 4, 14 ), 'a', 2 );
  813. expect( spy.callCount ).to.equal( 3 );
  814. expect( getChangesAttrsCount() ).to.equal( 7 );
  815. expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' );
  816. } );
  817. it( 'should strip the range if the beginning have the attribute', () => {
  818. batch.setAttribute( getRange( 1, 5 ), 'a', 1 );
  819. expect( spy.callCount ).to.equal( 1 );
  820. expect( getChangesAttrsCount() ).to.equal( 2 );
  821. expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' );
  822. } );
  823. it( 'should strip the range if the ending have the attribute', () => {
  824. batch.setAttribute( getRange( 13, 17 ), 'a', 1 );
  825. expect( spy.callCount ).to.equal( 1 );
  826. expect( getChangesAttrsCount() ).to.equal( 2 );
  827. expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' );
  828. } );
  829. it( 'should do nothing if the range has attribute', () => {
  830. batch.setAttribute( getRange( 0, 3 ), 'a', 1 );
  831. expect( spy.callCount ).to.equal( 0 );
  832. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  833. } );
  834. it( 'should not check range\'s start position node when creating operations', () => {
  835. const range = new Range(
  836. new Position( root, [ 18, 1 ] ),
  837. new Position( root, [ 19 ] )
  838. );
  839. batch.setAttribute( range, 'a', 1 );
  840. expect( spy.callCount ).to.equal( 1 );
  841. expect( getChangesAttrsCount() ).to.equal( 2 );
  842. expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' );
  843. } );
  844. it( 'should not change elements attribute if range contains closing tag', () => {
  845. const range = new Range(
  846. new Position( root, [ 18, 1 ] ),
  847. new Position( root, [ 21 ] )
  848. );
  849. batch.setAttribute( range, 'a', 1 );
  850. expect( spy.callCount ).to.equal( 1 );
  851. expect( getChangesAttrsCount() ).to.equal( 4 );
  852. expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' );
  853. } );
  854. it( 'should not create an operation if the range contains only closing tag', () => {
  855. const range = new Range(
  856. new Position( root, [ 18, 3 ] ),
  857. new Position( root, [ 19 ] )
  858. );
  859. batch.setAttribute( range, 'a', 3 );
  860. expect( spy.callCount ).to.equal( 0 );
  861. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  862. } );
  863. it( 'should not create an operation if is collapsed', () => {
  864. batch.setAttribute( getRange( 3, 3 ), 'a', 1 );
  865. expect( spy.callCount ).to.equal( 0 );
  866. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  867. } );
  868. it( 'should create a proper operations for the mixed range', () => {
  869. batch.setAttribute( getRange( 0, 20 ), 'a', 1 );
  870. expect( spy.callCount ).to.equal( 5 );
  871. expect( getChangesAttrsCount() ).to.equal( 14 );
  872. expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' );
  873. } );
  874. it( 'should be chainable', () => {
  875. const chain = batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
  876. expect( chain ).to.equal( batch );
  877. } );
  878. it( 'should add delta to batch and operation to delta before applying operation', () => {
  879. batch.setAttribute( getRange( 3, 6 ), 'a', 3 );
  880. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  881. } );
  882. } );
  883. describe( 'removeAttribute', () => {
  884. it( 'should remove the attribute on the range', () => {
  885. batch.removeAttribute( getRange( 0, 2 ), 'a' );
  886. expect( spy.callCount ).to.equal( 1 );
  887. expect( getChangesAttrsCount() ).to.equal( 2 );
  888. expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' );
  889. } );
  890. it( 'should split the operations if parts of the range have different attributes', () => {
  891. batch.removeAttribute( getRange( 7, 11 ), 'a' );
  892. expect( spy.callCount ).to.equal( 2 );
  893. expect( getChangesAttrsCount() ).to.equal( 4 );
  894. expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' );
  895. } );
  896. it( 'should split the operations if parts of the part of the range have no attribute', () => {
  897. batch.removeAttribute( getRange( 1, 7 ), 'a' );
  898. expect( spy.callCount ).to.equal( 2 );
  899. expect( getChangesAttrsCount() ).to.equal( 3 );
  900. expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' );
  901. } );
  902. it( 'should strip the range if the beginning have no attribute', () => {
  903. batch.removeAttribute( getRange( 4, 12 ), 'a' );
  904. expect( spy.callCount ).to.equal( 2 );
  905. expect( getChangesAttrsCount() ).to.equal( 6 );
  906. expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
  907. } );
  908. it( 'should strip the range if the ending have no attribute', () => {
  909. batch.removeAttribute( getRange( 7, 15 ), 'a' );
  910. expect( spy.callCount ).to.equal( 2 );
  911. expect( getChangesAttrsCount() ).to.equal( 5 );
  912. expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' );
  913. } );
  914. it( 'should do nothing if the range has no attribute', () => {
  915. batch.removeAttribute( getRange( 4, 5 ), 'a' );
  916. expect( spy.callCount ).to.equal( 0 );
  917. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  918. } );
  919. it( 'should not check range\'s start position node when creating operations', () => {
  920. const range = new Range(
  921. new Position( root, [ 18, 3 ] ),
  922. new Position( root, [ 19 ] )
  923. );
  924. batch.removeAttribute( range, 'a' );
  925. expect( spy.callCount ).to.equal( 0 );
  926. expect( getChangesAttrsCount() ).to.equal( 0 );
  927. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  928. } );
  929. it( 'should not apply operation twice in the range contains opening and closing tags', () => {
  930. batch.removeAttribute( getRange( 18, 22 ), 'a' );
  931. expect( spy.callCount ).to.equal( 1 );
  932. expect( getChangesAttrsCount() ).to.equal( 1 );
  933. expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' );
  934. } );
  935. it( 'should not create an operation if range is collapsed', () => {
  936. batch.removeAttribute( getRange( 3, 3 ), 'a' );
  937. expect( spy.callCount ).to.equal( 0 );
  938. expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' );
  939. } );
  940. it( 'should create a proper operations for the mixed range', () => {
  941. batch.removeAttribute( getRange( 3, 15 ), 'a' );
  942. expect( spy.callCount ).to.equal( 2 );
  943. expect( getChangesAttrsCount() ).to.equal( 6 );
  944. expect( getCompressedAttrs() ).to.equal( '111------------1112------' );
  945. } );
  946. it( 'should be chainable', () => {
  947. const chain = batch.removeAttribute( getRange( 0, 2 ), 'a' );
  948. expect( chain ).to.equal( batch );
  949. } );
  950. it( 'should add delta to batch and operation to delta before applying operation', () => {
  951. batch.removeAttribute( getRange( 0, 2 ), 'a' );
  952. sinon.assert.calledWith( spy, correctDeltaMatcher );
  953. } );
  954. } );
  955. } );
  956. describe( 'change attribute on root element', () => {
  957. beforeEach( () => {
  958. spy = sinon.spy( doc, 'applyOperation' );
  959. } );
  960. describe( 'setAttribute', () => {
  961. it( 'should create the attribute on root', () => {
  962. batch.setAttribute( root, 'b', 2 );
  963. expect( spy.callCount ).to.equal( 1 );
  964. expect( root.getAttribute( 'b' ) ).to.equal( 2 );
  965. } );
  966. it( 'should change the attribute of root', () => {
  967. batch.setAttribute( root, 'a', 2 );
  968. expect( spy.callCount ).to.equal( 1 );
  969. expect( root.getAttribute( 'a' ) ).to.equal( 2 );
  970. } );
  971. it( 'should do nothing if the attribute value is the same', () => {
  972. batch.setAttribute( root, 'a', 1 );
  973. expect( spy.callCount ).to.equal( 1 );
  974. batch.setAttribute( root, 'a', 1 );
  975. expect( spy.callCount ).to.equal( 1 );
  976. expect( root.getAttribute( 'a' ) ).to.equal( 1 );
  977. } );
  978. } );
  979. describe( 'removeAttribute', () => {
  980. it( 'should remove the attribute from root', () => {
  981. batch.setAttribute( root, 'a', 1 );
  982. batch.removeAttribute( root, 'a' );
  983. expect( spy.callCount ).to.equal( 2 );
  984. expect( root.getAttribute( 'a' ) ).to.be.undefined;
  985. } );
  986. it( 'should do nothing if the attribute is not set', () => {
  987. batch.removeAttribute( root, 'b' );
  988. expect( spy.callCount ).to.equal( 0 );
  989. } );
  990. } );
  991. } );
  992. it( 'should not add empty delta to the batch', () => {
  993. const nodeA = new Element( 'p', { a: 1 } );
  994. const nodeB = new Element( 'p', { b: 2 } );
  995. root.insertChildren( 0, [ nodeA, nodeB ] );
  996. batch.setAttribute( nodeA, 'a', 1 );
  997. expect( batch.deltas.length ).to.equal( 0 );
  998. batch.removeAttribute( Range.createIn( root ), 'x' );
  999. expect( batch.deltas.length ).to.equal( 0 );
  1000. } );
  1001. } );
  1002. describe( 'setAttributes()', () => {
  1003. let doc, batch, frag, item;
  1004. beforeEach( () => {
  1005. doc = new Document();
  1006. batch = doc.batch();
  1007. frag = batch.createDocumentFragment();
  1008. item = batch.createText( 'xxx', { b: 2, c: 3 } );
  1009. batch.appendText( 'xxx', { a: 1 }, frag );
  1010. batch.append( item, frag );
  1011. } );
  1012. it( 'should set attributes one by one on range', () => {
  1013. const range = Range.createIn( frag );
  1014. // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
  1015. // such a big amount of the same tests, so let's use a spy here.
  1016. const spy = sinon.spy( batch, 'setAttribute' );
  1017. batch.setAttributes( range, { a: 3, c: null } );
  1018. // Verify result.
  1019. expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
  1020. expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
  1021. // Verify operations
  1022. sinon.assert.calledTwice( spy );
  1023. sinon.assert.calledWith( spy.firstCall, range, 'a', 3 );
  1024. sinon.assert.calledWith( spy.secondCall, range, 'c', null );
  1025. } );
  1026. it( 'should set attributes one by one on range for map as attributes list', () => {
  1027. const range = Range.createIn( frag );
  1028. // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
  1029. // such a big amount of the same tests, so let's use a spy here.
  1030. const spy = sinon.spy( batch, 'setAttribute' );
  1031. batch.setAttributes( range, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) );
  1032. // Verify result.
  1033. expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] );
  1034. expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
  1035. // Verify operations
  1036. sinon.assert.calledTwice( spy );
  1037. sinon.assert.calledWith( spy.firstCall, range, 'a', 3 );
  1038. sinon.assert.calledWith( spy.secondCall, range, 'c', null );
  1039. } );
  1040. it( 'should set attributes one by one on item', () => {
  1041. // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
  1042. // such a big amount of the same tests, so let's use a spy here.
  1043. const spy = sinon.spy( batch, 'setAttribute' );
  1044. batch.setAttributes( item, { a: 3, c: null } );
  1045. // Verify result.
  1046. expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
  1047. // Verify operations
  1048. sinon.assert.calledTwice( spy );
  1049. sinon.assert.calledWith( spy.firstCall, item, 'a', 3 );
  1050. sinon.assert.calledWith( spy.secondCall, item, 'c', null );
  1051. } );
  1052. it( 'should set attributes one by one on item for maps as attributes list', () => {
  1053. // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate
  1054. // such a big amount of the same tests, so let's use a spy here.
  1055. const spy = sinon.spy( batch, 'setAttribute' );
  1056. batch.setAttributes( item, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) );
  1057. // Verify result.
  1058. expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] );
  1059. // Verify operations
  1060. sinon.assert.calledTwice( spy );
  1061. sinon.assert.calledWith( spy.firstCall, item, 'a', 3 );
  1062. sinon.assert.calledWith( spy.secondCall, item, 'c', null );
  1063. } );
  1064. } );
  1065. describe( 'merge()', () => {
  1066. let doc, root, p1, p2, batch;
  1067. beforeEach( () => {
  1068. doc = new Document();
  1069. root = doc.createRoot();
  1070. p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) );
  1071. p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) );
  1072. root.insertChildren( 0, [ p1, p2 ] );
  1073. } );
  1074. it( 'should merge foo and bar into foobar', () => {
  1075. doc.batch().merge( new Position( root, [ 1 ] ) );
  1076. expect( root.maxOffset ).to.equal( 1 );
  1077. expect( root.getChild( 0 ).name ).to.equal( 'p' );
  1078. expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
  1079. expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
  1080. expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' );
  1081. expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
  1082. } );
  1083. it( 'should throw if there is no element after', () => {
  1084. expect( () => {
  1085. doc.batch().merge( new Position( root, [ 2 ] ) );
  1086. } ).to.throw( CKEditorError, /^batch-merge-no-element-after/ );
  1087. } );
  1088. it( 'should throw if there is no element before', () => {
  1089. expect( () => {
  1090. doc.batch().merge( new Position( root, [ 0, 2 ] ) );
  1091. } ).to.throw( CKEditorError, /^batch-merge-no-element-before/ );
  1092. } );
  1093. it( 'should be chainable', () => {
  1094. batch = doc.batch();
  1095. const chain = batch.merge( new Position( root, [ 1 ] ) );
  1096. expect( chain ).to.equal( batch );
  1097. } );
  1098. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1099. sinon.spy( doc, 'applyOperation' );
  1100. batch = doc.batch().merge( new Position( root, [ 1 ] ) );
  1101. const correctDeltaMatcher = sinon.match( operation => {
  1102. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1103. } );
  1104. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1105. } );
  1106. } );
  1107. describe( 'move()', () => {
  1108. let doc, root, range, div, p, batch, chain;
  1109. beforeEach( () => {
  1110. doc = new Document();
  1111. root = doc.createRoot();
  1112. div = new Element( 'div', [], new Text( 'foobar' ) );
  1113. p = new Element( 'p', [], new Text( 'abcxyz' ) );
  1114. div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
  1115. div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );
  1116. root.insertChildren( 0, [ div, p ] );
  1117. range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) );
  1118. batch = doc.batch();
  1119. } );
  1120. it( 'should move flat range of nodes', () => {
  1121. batch.move( range, new Position( root, [ 1, 3 ] ) );
  1122. expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' );
  1123. expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' );
  1124. } );
  1125. it( 'should throw if given range is not flat', () => {
  1126. const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) );
  1127. expect( () => {
  1128. doc.batch().move( notFlatRange, new Position( root, [ 1, 3 ] ) );
  1129. } ).to.throw( CKEditorError, /^batch-move-range-not-flat/ );
  1130. } );
  1131. it( 'should throw if range is going to be moved to the other document', () => {
  1132. const docFrag = batch.createDocumentFragment();
  1133. expect( () => {
  1134. doc.batch().move( range, docFrag );
  1135. } ).to.throw( CKEditorError, /^batch-move-different-document/ );
  1136. } );
  1137. it( 'should be chainable', () => {
  1138. chain = batch.move( range, new Position( root, [ 1, 3 ] ) );
  1139. expect( chain ).to.equal( batch );
  1140. } );
  1141. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1142. sinon.spy( doc, 'applyOperation' );
  1143. batch.move( range, new Position( root, [ 2 ] ) );
  1144. const correctDeltaMatcher = sinon.match( operation => {
  1145. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1146. } );
  1147. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1148. } );
  1149. } );
  1150. describe( 'remove()', () => {
  1151. let doc, root, div, p, batch, chain, range;
  1152. beforeEach( () => {
  1153. doc = new Document();
  1154. root = doc.createRoot();
  1155. div = new Element( 'div', [], new Text( 'foobar' ) );
  1156. p = new Element( 'p', [], new Text( 'abcxyz' ) );
  1157. div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] );
  1158. div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] );
  1159. root.insertChildren( 0, [ div, p ] );
  1160. batch = doc.batch();
  1161. // Range starts in ROOT > DIV > P > gg|gg.
  1162. // Range ends in ROOT > DIV > ...|ar.
  1163. range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) );
  1164. } );
  1165. it( 'should remove specified node', () => {
  1166. batch.remove( div );
  1167. expect( root.maxOffset ).to.equal( 1 );
  1168. expect( root.childCount ).to.equal( 1 );
  1169. expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' );
  1170. } );
  1171. it( 'should remove any range of nodes', () => {
  1172. batch.remove( range );
  1173. expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' );
  1174. expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' );
  1175. } );
  1176. it( 'should create minimal number of remove deltas, each with only one operation', () => {
  1177. batch.remove( range );
  1178. expect( batch.deltas.length ).to.equal( 2 );
  1179. expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 );
  1180. expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 );
  1181. } );
  1182. it( 'should be chainable', () => {
  1183. chain = batch.remove( range );
  1184. expect( chain ).to.equal( batch );
  1185. } );
  1186. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1187. sinon.spy( doc, 'applyOperation' );
  1188. batch.remove( div );
  1189. const correctDeltaMatcher = sinon.match( operation => {
  1190. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1191. } );
  1192. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1193. } );
  1194. } );
  1195. describe( 'rename()', () => {
  1196. let doc, root, batch, chain;
  1197. beforeEach( () => {
  1198. doc = new Document();
  1199. root = doc.createRoot();
  1200. const p = new Element( 'p', null, new Text( 'abc' ) );
  1201. root.appendChildren( p );
  1202. batch = doc.batch();
  1203. chain = batch.rename( p, 'h' );
  1204. } );
  1205. it( 'should rename given element', () => {
  1206. expect( root.maxOffset ).to.equal( 1 );
  1207. expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
  1208. expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' );
  1209. } );
  1210. it( 'should throw if not an Element instance is passed', () => {
  1211. expect( () => {
  1212. batch.rename( new Text( 'abc' ), 'h' );
  1213. } ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ );
  1214. } );
  1215. it( 'should be chainable', () => {
  1216. expect( chain ).to.equal( batch );
  1217. } );
  1218. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1219. sinon.spy( doc, 'applyOperation' );
  1220. batch.rename( root.getChild( 0 ), 'p' );
  1221. const correctDeltaMatcher = sinon.match( operation => {
  1222. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1223. } );
  1224. expect( doc.applyOperation.alwaysCalledWith( correctDeltaMatcher ) ).to.be.true;
  1225. } );
  1226. } );
  1227. describe( 'split()', () => {
  1228. let doc, root, p;
  1229. beforeEach( () => {
  1230. doc = new Document();
  1231. root = doc.createRoot();
  1232. p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) );
  1233. root.insertChildren( 0, p );
  1234. } );
  1235. it( 'should split foobar to foo and bar', () => {
  1236. doc.batch().split( new Position( root, [ 0, 3 ] ) );
  1237. expect( root.maxOffset ).to.equal( 2 );
  1238. expect( root.getChild( 0 ).name ).to.equal( 'p' );
  1239. expect( root.getChild( 0 ).maxOffset ).to.equal( 3 );
  1240. expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
  1241. expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
  1242. expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
  1243. expect( root.getChild( 1 ).name ).to.equal( 'p' );
  1244. expect( root.getChild( 1 ).maxOffset ).to.equal( 3 );
  1245. expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
  1246. expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
  1247. expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' );
  1248. } );
  1249. it( 'should create an empty paragraph if we split at the end', () => {
  1250. doc.batch().split( new Position( root, [ 0, 6 ] ) );
  1251. expect( root.maxOffset ).to.equal( 2 );
  1252. expect( root.getChild( 0 ).name ).to.equal( 'p' );
  1253. expect( root.getChild( 0 ).maxOffset ).to.equal( 6 );
  1254. expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 );
  1255. expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
  1256. expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' );
  1257. expect( root.getChild( 1 ).name ).to.equal( 'p' );
  1258. expect( root.getChild( 1 ).maxOffset ).to.equal( 0 );
  1259. expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 );
  1260. expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
  1261. } );
  1262. it( 'should throw if we try to split a root', () => {
  1263. expect( () => {
  1264. doc.batch().split( new Position( root, [ 0 ] ) );
  1265. } ).to.throw( CKEditorError, /^batch-split-root/ );
  1266. } );
  1267. it( 'should be chainable', () => {
  1268. const batch = doc.batch();
  1269. const chain = batch.split( new Position( root, [ 0, 3 ] ) );
  1270. expect( chain ).to.equal( batch );
  1271. } );
  1272. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1273. sinon.spy( doc, 'applyOperation' );
  1274. const batch = doc.batch().split( new Position( root, [ 0, 3 ] ) );
  1275. const correctDeltaMatcher = sinon.match( operation => {
  1276. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1277. } );
  1278. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1279. } );
  1280. } );
  1281. describe( 'wrap()', () => {
  1282. let doc, root, range;
  1283. beforeEach( () => {
  1284. doc = new Document();
  1285. root = doc.createRoot();
  1286. root.insertChildren( 0, new Text( 'foobar' ) );
  1287. range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
  1288. } );
  1289. it( 'should wrap flat range with given element', () => {
  1290. const p = new Element( 'p' );
  1291. doc.batch().wrap( range, p );
  1292. expect( root.maxOffset ).to.equal( 5 );
  1293. expect( root.getChild( 0 ).data ).to.equal( 'fo' );
  1294. expect( root.getChild( 1 ) ).to.equal( p );
  1295. expect( p.getChild( 0 ).data ).to.equal( 'ob' );
  1296. expect( root.getChild( 2 ).data ).to.equal( 'ar' );
  1297. } );
  1298. it( 'should wrap flat range with an element of given name', () => {
  1299. doc.batch().wrap( range, 'p' );
  1300. expect( root.maxOffset ).to.equal( 5 );
  1301. expect( root.getChild( 0 ).data ).to.equal( 'fo' );
  1302. expect( root.getChild( 1 ).name ).to.equal( 'p' );
  1303. expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' );
  1304. expect( root.getChild( 2 ).data ).to.equal( 'ar' );
  1305. } );
  1306. it( 'should throw if range to wrap is not flat', () => {
  1307. root.insertChildren( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] );
  1308. const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) );
  1309. expect( () => {
  1310. doc.batch().wrap( notFlatRange, 'p' );
  1311. } ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ );
  1312. } );
  1313. it( 'should throw if element to wrap with has children', () => {
  1314. const p = new Element( 'p', [], new Text( 'a' ) );
  1315. expect( () => {
  1316. doc.batch().wrap( range, p );
  1317. } ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ );
  1318. } );
  1319. it( 'should throw if element to wrap with has children', () => {
  1320. const p = new Element( 'p' );
  1321. root.insertChildren( 0, p );
  1322. expect( () => {
  1323. doc.batch().wrap( range, p );
  1324. } ).to.throw( CKEditorError, /^batch-wrap-element-attached/ );
  1325. } );
  1326. it( 'should be chainable', () => {
  1327. const batch = doc.batch();
  1328. const chain = batch.wrap( range, 'p' );
  1329. expect( chain ).to.equal( batch );
  1330. } );
  1331. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1332. sinon.spy( doc, 'applyOperation' );
  1333. const batch = doc.batch().wrap( range, 'p' );
  1334. const correctDeltaMatcher = sinon.match( operation => {
  1335. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1336. } );
  1337. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1338. } );
  1339. } );
  1340. describe( 'unwrap()', () => {
  1341. let doc, root, p;
  1342. beforeEach( () => {
  1343. doc = new Document();
  1344. root = doc.createRoot();
  1345. p = new Element( 'p', [], new Text( 'xyz' ) );
  1346. root.insertChildren( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] );
  1347. } );
  1348. it( 'should unwrap given element', () => {
  1349. doc.batch().unwrap( p );
  1350. expect( root.maxOffset ).to.equal( 5 );
  1351. expect( root.getChild( 0 ).data ).to.equal( 'axyzb' );
  1352. } );
  1353. it( 'should throw if element to unwrap has no parent', () => {
  1354. const element = new Element( 'p' );
  1355. expect( () => {
  1356. doc.batch().unwrap( element );
  1357. } ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ );
  1358. } );
  1359. it( 'should be chainable', () => {
  1360. const batch = doc.batch();
  1361. const chain = batch.unwrap( p );
  1362. expect( chain ).to.equal( batch );
  1363. } );
  1364. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1365. sinon.spy( doc, 'applyOperation' );
  1366. const batch = doc.batch().unwrap( p );
  1367. const correctDeltaMatcher = sinon.match( operation => {
  1368. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1369. } );
  1370. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1371. } );
  1372. } );
  1373. describe( 'setMarker()', () => {
  1374. let doc, root, range;
  1375. beforeEach( () => {
  1376. doc = new Document();
  1377. root = doc.createRoot();
  1378. root.appendChildren( new Text( 'foo' ) );
  1379. range = Range.createIn( root );
  1380. } );
  1381. it( 'should add marker to the document marker collection', () => {
  1382. doc.batch().setMarker( 'name', range );
  1383. expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true;
  1384. } );
  1385. it( 'should update marker in the document marker collection', () => {
  1386. doc.batch().setMarker( 'name', range );
  1387. const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
  1388. doc.batch().setMarker( 'name', range2 );
  1389. expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
  1390. } );
  1391. it( 'should accept marker instance', () => {
  1392. doc.batch().setMarker( 'name', range );
  1393. const marker = doc.markers.get( 'name' );
  1394. const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
  1395. const batch = doc.batch().setMarker( marker, range2 );
  1396. const op = batch.deltas[ 0 ].operations[ 0 ];
  1397. expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
  1398. expect( op.oldRange.isEqual( range ) ).to.be.true;
  1399. expect( op.newRange.isEqual( range2 ) ).to.be.true;
  1400. } );
  1401. it( 'should accept empty range parameter if marker instance is passed', () => {
  1402. const marker = doc.markers.set( 'name', range );
  1403. sinon.spy( doc, 'fire' );
  1404. doc.on( 'change', ( evt, type, changes ) => {
  1405. if ( type == 'marker' ) {
  1406. expect( changes.type ).to.equal( 'set' );
  1407. expect( changes.name ).to.equal( 'name' );
  1408. }
  1409. } );
  1410. const batch = doc.batch().setMarker( marker );
  1411. const op = batch.deltas[ 0 ].operations[ 0 ];
  1412. expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true;
  1413. expect( op.oldRange ).to.be.null;
  1414. expect( op.newRange.isEqual( range ) ).to.be.true;
  1415. } );
  1416. it( 'should throw if marker with given name does not exist and range is not passed', () => {
  1417. expect( () => {
  1418. doc.batch().setMarker( 'name' );
  1419. } ).to.throw( CKEditorError, /^batch-setMarker-no-range/ );
  1420. } );
  1421. it( 'should be chainable', () => {
  1422. const batch = doc.batch();
  1423. const chain = batch.setMarker( 'name', range );
  1424. expect( chain ).to.equal( batch );
  1425. } );
  1426. } );
  1427. describe( 'removeMarker()', () => {
  1428. let doc, root, range;
  1429. beforeEach( () => {
  1430. doc = new Document();
  1431. root = doc.createRoot();
  1432. root.appendChildren( new Text( 'foo' ) );
  1433. range = Range.createIn( root );
  1434. } );
  1435. it( 'should remove marker from the document marker collection', () => {
  1436. doc.batch().setMarker( 'name', range );
  1437. doc.batch().removeMarker( 'name' );
  1438. expect( doc.markers.get( 'name' ) ).to.be.null;
  1439. } );
  1440. it( 'should throw when trying to remove non existing marker', () => {
  1441. expect( () => {
  1442. doc.batch().removeMarker( 'name' );
  1443. } ).to.throw( CKEditorError, /^batch-removeMarker-no-marker/ );
  1444. } );
  1445. it( 'should accept marker instance', () => {
  1446. doc.batch().setMarker( 'name', range );
  1447. const marker = doc.markers.get( 'name' );
  1448. doc.batch().removeMarker( marker );
  1449. expect( doc.markers.get( 'name' ) ).to.be.null;
  1450. } );
  1451. it( 'should add delta to batch and operation to delta before applying operation', () => {
  1452. sinon.spy( doc, 'applyOperation' );
  1453. const batch = doc.batch().setMarker( 'name', range );
  1454. const correctDeltaMatcher = sinon.match( operation => {
  1455. return operation.delta && operation.delta.batch && operation.delta.batch == batch;
  1456. } );
  1457. expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true;
  1458. } );
  1459. } );
  1460. } );