operation.test.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. 'use strict'
  2. const _ = require('lodash')
  3. const { expect } = require('chai')
  4. const ot = require('..')
  5. const StringFileData = require('../lib/file_data/string_file_data')
  6. const File = ot.File
  7. const AddFileOperation = ot.AddFileOperation
  8. const MoveFileOperation = ot.MoveFileOperation
  9. const EditFileOperation = ot.EditFileOperation
  10. const NoOperation = ot.NoOperation
  11. const Operation = ot.Operation
  12. const TextOperation = ot.TextOperation
  13. const AddCommentOperation = ot.AddCommentOperation
  14. const DeleteCommentOperation = ot.DeleteCommentOperation
  15. const SetCommentStateOperation = ot.SetCommentStateOperation
  16. const Snapshot = ot.Snapshot
  17. describe('Operation', function () {
  18. function makeEmptySnapshot() {
  19. return new Snapshot()
  20. }
  21. function makeOneFileSnapshot() {
  22. const snapshot = makeEmptySnapshot()
  23. snapshot.addFile('foo', File.fromString(''))
  24. return snapshot
  25. }
  26. function makeTwoFileSnapshot() {
  27. const snapshot = makeOneFileSnapshot()
  28. snapshot.addFile('bar', File.fromString('a'))
  29. return snapshot
  30. }
  31. function addFile(pathname, content) {
  32. return new AddFileOperation(pathname, File.fromString(content))
  33. }
  34. function roundTripOperation(operation) {
  35. return Operation.fromRaw(operation.toRaw())
  36. }
  37. function deepCopySnapshot(snapshot) {
  38. return Snapshot.fromRaw(snapshot.toRaw())
  39. }
  40. function runConcurrently(operation0, operation1, snapshot) {
  41. const operations = [
  42. // make sure they survive serialization
  43. roundTripOperation(operation0),
  44. roundTripOperation(operation1),
  45. ]
  46. const primeOperations = Operation.transform(operation0, operation1)
  47. const originalSnapshot = snapshot || makeEmptySnapshot()
  48. const snapshotA = deepCopySnapshot(originalSnapshot)
  49. const snapshotB = deepCopySnapshot(originalSnapshot)
  50. operations[0].applyTo(snapshotA)
  51. operations[1].applyTo(snapshotB)
  52. primeOperations[0].applyTo(snapshotB)
  53. primeOperations[1].applyTo(snapshotA)
  54. expect(snapshotA).to.eql(snapshotB)
  55. return {
  56. snapshot: snapshotA,
  57. operations,
  58. primeOperations,
  59. log() {
  60. console.log(this)
  61. return this
  62. },
  63. expectNoTransform() {
  64. expect(this.operations).to.deep.eql(this.primeOperations)
  65. return this
  66. },
  67. expectTransform() {
  68. expect(this.operations).to.not.deep.eql(this.primeOperations)
  69. return this
  70. },
  71. swap() {
  72. return runConcurrently(operation1, operation0, originalSnapshot)
  73. },
  74. expectFiles(files) {
  75. this.expectedFiles = files
  76. expect(this.snapshot.countFiles()).to.equal(_.size(files))
  77. _.forOwn(files, (expectedFile, pathname) => {
  78. if (_.isString(expectedFile)) {
  79. expectedFile = { content: expectedFile, metadata: {}, comments: [] }
  80. }
  81. const file = this.snapshot.getFile(pathname)
  82. expect(file.getContent()).to.equal(expectedFile.content)
  83. expect(file.getMetadata()).to.eql(expectedFile.metadata)
  84. expect(file.getComments()).to.deep.equal(expectedFile.comments)
  85. })
  86. return this
  87. },
  88. expectSymmetry() {
  89. if (!this.expectedFiles) {
  90. throw new Error('must call expectFiles before expectSymmetry')
  91. }
  92. this.swap().expectFiles(this.expectedFiles)
  93. return this
  94. },
  95. expectPrime(index, klass) {
  96. expect(this.primeOperations[index]).to.be.an.instanceof(klass)
  97. return this
  98. },
  99. tap(fn) {
  100. fn.call(this)
  101. return this
  102. },
  103. }
  104. }
  105. // shorthand for creating an edit operation
  106. function edit(pathname, textOperationOps) {
  107. return Operation.editFile(
  108. pathname,
  109. TextOperation.fromJSON({ textOperation: textOperationOps })
  110. )
  111. }
  112. it('transforms AddFile-AddFile with different names', function () {
  113. runConcurrently(addFile('foo', ''), addFile('bar', 'a'))
  114. .expectNoTransform()
  115. .expectFiles({ bar: 'a', foo: '' })
  116. .expectSymmetry()
  117. })
  118. it('transforms AddFile-AddFile with same name', function () {
  119. // the second file 'wins'
  120. runConcurrently(addFile('foo', ''), addFile('foo', 'a'))
  121. .expectFiles({ foo: 'a' })
  122. // if the first add was committed first, the second add overwrites it
  123. .expectPrime(1, AddFileOperation)
  124. // if the second add was committed first, the first add becomes a no-op
  125. .expectPrime(0, NoOperation)
  126. .swap()
  127. .expectFiles({ foo: '' })
  128. })
  129. it('transforms AddFile-MoveFile with no conflict', function () {
  130. runConcurrently(
  131. Operation.moveFile('foo', 'baz'),
  132. addFile('bar', 'a'),
  133. makeOneFileSnapshot()
  134. )
  135. .expectNoTransform()
  136. .expectFiles({ bar: 'a', baz: '' })
  137. .expectSymmetry()
  138. })
  139. it('transforms AddFile-MoveFile with move from new file', function () {
  140. runConcurrently(
  141. Operation.moveFile('foo', 'baz'),
  142. addFile('foo', 'a'),
  143. makeOneFileSnapshot()
  144. )
  145. .expectFiles({ baz: 'a' })
  146. // if the move was committed first, the add overwrites it
  147. .expectPrime(1, AddFileOperation)
  148. // if the add was committed first, the move appears in the history
  149. .expectPrime(0, MoveFileOperation)
  150. .expectSymmetry()
  151. })
  152. it('transforms AddFile-MoveFile with move to new file', function () {
  153. runConcurrently(
  154. Operation.moveFile('foo', 'baz'),
  155. addFile('baz', 'a'),
  156. makeOneFileSnapshot()
  157. )
  158. .expectFiles({ baz: 'a' })
  159. // if the move was committed first, the add overwrites it
  160. .expectPrime(1, AddFileOperation)
  161. // if the add was committed first, the move becomes a delete
  162. .expectPrime(0, MoveFileOperation)
  163. .tap(function () {
  164. expect(this.primeOperations[0].isRemoveFile()).to.be.true
  165. })
  166. .expectSymmetry()
  167. })
  168. it('transforms AddFile-RemoveFile with no conflict', function () {
  169. runConcurrently(
  170. Operation.removeFile('foo'),
  171. addFile('bar', 'a'),
  172. makeOneFileSnapshot()
  173. )
  174. .expectNoTransform()
  175. .expectFiles({ bar: 'a' })
  176. .expectSymmetry()
  177. })
  178. it('transforms AddFile-RemoveFile that removes added file', function () {
  179. runConcurrently(
  180. Operation.removeFile('foo'),
  181. addFile('foo', 'a'),
  182. makeOneFileSnapshot()
  183. )
  184. .expectFiles({ foo: 'a' })
  185. // if the move was committed first, the add overwrites it
  186. .expectPrime(1, AddFileOperation)
  187. // if the add was committed first, the move gets dropped
  188. .expectPrime(0, NoOperation)
  189. .expectSymmetry()
  190. })
  191. it('transforms AddFile-EditFile with no conflict', function () {
  192. runConcurrently(
  193. edit('foo', ['x']),
  194. addFile('bar', 'a'),
  195. makeOneFileSnapshot()
  196. )
  197. .expectNoTransform()
  198. .expectFiles({ bar: 'a', foo: 'x' })
  199. .expectSymmetry()
  200. })
  201. it('transforms AddFile-EditFile when new file is edited', function () {
  202. runConcurrently(
  203. edit('foo', ['x']),
  204. addFile('foo', 'a'),
  205. makeOneFileSnapshot()
  206. )
  207. .expectFiles({ foo: 'a' })
  208. // if the edit was committed first, the add overwrites it
  209. .expectPrime(1, AddFileOperation)
  210. // if the add was committed first, the edit gets dropped
  211. .expectPrime(0, NoOperation)
  212. .expectSymmetry()
  213. })
  214. it('transforms AddFile-SetFileMetadata with no conflict', function () {
  215. const testMetadata = { baz: 1 }
  216. runConcurrently(
  217. addFile('bar', 'a'),
  218. Operation.setFileMetadata('foo', testMetadata),
  219. makeOneFileSnapshot()
  220. )
  221. .expectNoTransform()
  222. .expectFiles({
  223. foo: { content: '', metadata: testMetadata, comments: [] },
  224. bar: 'a',
  225. })
  226. .expectSymmetry()
  227. })
  228. it('transforms AddFile-SetFileMetadata with same name', function () {
  229. const testMetadata = { baz: 1 }
  230. runConcurrently(
  231. addFile('foo', 'x'),
  232. Operation.setFileMetadata('foo', testMetadata),
  233. makeEmptySnapshot()
  234. )
  235. .expectFiles({
  236. foo: { content: 'x', metadata: testMetadata, comments: [] },
  237. })
  238. .expectSymmetry()
  239. })
  240. it('transforms MoveFile-MoveFile with no conflict', function () {
  241. runConcurrently(
  242. Operation.moveFile('foo', 'baz'),
  243. Operation.moveFile('bar', 'bat'),
  244. makeTwoFileSnapshot()
  245. )
  246. .expectFiles({ bat: 'a', baz: '' })
  247. .expectNoTransform()
  248. .expectSymmetry()
  249. })
  250. it('transforms MoveFile-MoveFile same move foo->foo, foo->foo', function () {
  251. runConcurrently(
  252. Operation.moveFile('foo', 'foo'),
  253. Operation.moveFile('foo', 'foo'),
  254. makeOneFileSnapshot()
  255. )
  256. .expectFiles({ foo: '' })
  257. .expectPrime(1, NoOperation)
  258. .expectPrime(0, NoOperation)
  259. .expectSymmetry()
  260. })
  261. it('transforms MoveFile-MoveFile no-op foo->foo, foo->bar', function () {
  262. runConcurrently(
  263. Operation.moveFile('foo', 'foo'),
  264. Operation.moveFile('foo', 'bar'),
  265. makeOneFileSnapshot()
  266. )
  267. .expectFiles({ bar: '' })
  268. .expectPrime(1, MoveFileOperation)
  269. .expectPrime(0, NoOperation)
  270. .expectSymmetry()
  271. })
  272. it('transforms MoveFile-MoveFile no-op foo->foo, bar->foo', function () {
  273. runConcurrently(
  274. Operation.moveFile('foo', 'foo'),
  275. Operation.moveFile('foo', 'bar'),
  276. makeTwoFileSnapshot()
  277. )
  278. .expectFiles({ bar: '' })
  279. .expectPrime(1, MoveFileOperation)
  280. .expectPrime(0, NoOperation)
  281. .expectSymmetry()
  282. })
  283. it('transforms MoveFile-MoveFile no-op foo->foo, bar->bar', function () {
  284. runConcurrently(
  285. Operation.moveFile('foo', 'foo'),
  286. Operation.moveFile('bar', 'bar'),
  287. makeTwoFileSnapshot()
  288. )
  289. .expectFiles({ bar: 'a', foo: '' })
  290. .expectPrime(1, NoOperation)
  291. .expectPrime(0, NoOperation)
  292. .expectSymmetry()
  293. })
  294. it('transforms MoveFile-MoveFile same move foo->bar, foo->bar', function () {
  295. runConcurrently(
  296. Operation.moveFile('foo', 'bar'),
  297. Operation.moveFile('foo', 'bar'),
  298. makeOneFileSnapshot()
  299. )
  300. .expectFiles({ bar: '' })
  301. .expectPrime(1, NoOperation)
  302. .expectPrime(0, NoOperation)
  303. .expectSymmetry()
  304. })
  305. it('transforms MoveFile-MoveFile opposite foo->bar, bar->foo', function () {
  306. runConcurrently(
  307. Operation.moveFile('foo', 'bar'),
  308. Operation.moveFile('bar', 'foo'),
  309. makeTwoFileSnapshot()
  310. )
  311. .expectFiles([])
  312. .expectPrime(1, MoveFileOperation)
  313. .expectPrime(0, MoveFileOperation)
  314. .tap(function () {
  315. expect(this.primeOperations[1].isRemoveFile()).to.be.true
  316. expect(this.primeOperations[1].getPathname()).to.equal('bar')
  317. expect(this.primeOperations[0].isRemoveFile()).to.be.true
  318. expect(this.primeOperations[0].getPathname()).to.equal('foo')
  319. })
  320. .expectSymmetry()
  321. })
  322. it('transforms MoveFile-MoveFile no-op foo->foo, bar->baz', function () {
  323. runConcurrently(
  324. Operation.moveFile('foo', 'foo'),
  325. Operation.moveFile('bar', 'baz'),
  326. makeTwoFileSnapshot()
  327. )
  328. .expectFiles({ baz: 'a', foo: '' })
  329. .expectPrime(1, MoveFileOperation)
  330. .expectPrime(0, NoOperation)
  331. .expectSymmetry()
  332. })
  333. it('transforms MoveFile-MoveFile diverge foo->bar, foo->baz', function () {
  334. runConcurrently(
  335. Operation.moveFile('foo', 'bar'),
  336. Operation.moveFile('foo', 'baz'),
  337. makeOneFileSnapshot()
  338. )
  339. .expectFiles({ baz: '' })
  340. // if foo->bar was committed first, the second move becomes bar->baz
  341. .expectPrime(1, MoveFileOperation)
  342. // if foo->baz was committed first, the second move becomes a no-op
  343. .expectPrime(0, NoOperation)
  344. .tap(function () {
  345. expect(this.primeOperations[1].getPathname()).to.equal('bar')
  346. expect(this.primeOperations[1].getNewPathname()).to.equal('baz')
  347. })
  348. .swap()
  349. .expectFiles({ bar: '' })
  350. })
  351. it('transforms MoveFile-MoveFile transitive foo->baz, bar->foo', function () {
  352. runConcurrently(
  353. Operation.moveFile('foo', 'baz'),
  354. Operation.moveFile('bar', 'foo'),
  355. makeTwoFileSnapshot()
  356. )
  357. .expectFiles({ baz: 'a' })
  358. .expectPrime(1, MoveFileOperation)
  359. .expectPrime(0, MoveFileOperation)
  360. .expectSymmetry()
  361. })
  362. it('transforms MoveFile-MoveFile transitive foo->bar, bar->baz', function () {
  363. runConcurrently(
  364. Operation.moveFile('foo', 'bar'),
  365. Operation.moveFile('bar', 'baz'),
  366. makeTwoFileSnapshot()
  367. )
  368. .expectFiles({ baz: '' })
  369. .expectPrime(1, MoveFileOperation)
  370. .expectPrime(0, MoveFileOperation)
  371. .expectSymmetry()
  372. })
  373. it('transforms MoveFile-MoveFile converge foo->baz, bar->baz', function () {
  374. runConcurrently(
  375. Operation.moveFile('foo', 'baz'),
  376. Operation.moveFile('bar', 'baz'),
  377. makeTwoFileSnapshot()
  378. )
  379. .expectFiles({ baz: 'a' })
  380. .expectPrime(1, MoveFileOperation)
  381. .expectPrime(0, MoveFileOperation)
  382. .tap(function () {
  383. // if foo->baz was committed first, we just apply the move
  384. expect(this.primeOperations[1]).to.eql(this.operations[1])
  385. // if bar->baz was committed first, the other move becomes a remove
  386. expect(this.primeOperations[0].isRemoveFile()).to.be.true
  387. expect(this.primeOperations[0].getPathname()).to.equal('foo')
  388. })
  389. .swap()
  390. .expectFiles({ baz: '' })
  391. })
  392. it('transforms MoveFile-RemoveFile no-op foo->foo, foo->', function () {
  393. runConcurrently(
  394. Operation.moveFile('foo', 'foo'),
  395. Operation.removeFile('foo'),
  396. makeOneFileSnapshot()
  397. )
  398. .expectFiles([])
  399. .expectPrime(1, MoveFileOperation)
  400. .expectPrime(0, NoOperation)
  401. .tap(function () {
  402. expect(this.primeOperations[1].isRemoveFile()).to.be.true
  403. })
  404. .expectSymmetry()
  405. })
  406. it('transforms MoveFile-RemoveFile same move foo->, foo->', function () {
  407. runConcurrently(
  408. Operation.removeFile('foo'),
  409. Operation.removeFile('foo'),
  410. makeOneFileSnapshot()
  411. )
  412. .expectFiles([])
  413. .expectPrime(1, NoOperation)
  414. .expectPrime(0, NoOperation)
  415. .expectSymmetry()
  416. })
  417. it('transforms MoveFile-RemoveFile no conflict foo->, bar->', function () {
  418. runConcurrently(
  419. Operation.removeFile('foo'),
  420. Operation.removeFile('bar'),
  421. makeTwoFileSnapshot()
  422. )
  423. .expectFiles([])
  424. .expectNoTransform()
  425. .expectSymmetry()
  426. })
  427. it('transforms MoveFile-RemoveFile no conflict foo->foo, bar->', function () {
  428. runConcurrently(
  429. Operation.moveFile('foo', 'foo'),
  430. Operation.removeFile('bar'),
  431. makeTwoFileSnapshot()
  432. )
  433. .expectFiles({ foo: '' })
  434. .expectPrime(1, MoveFileOperation)
  435. .expectPrime(0, NoOperation)
  436. .tap(function () {
  437. expect(this.primeOperations[1].isRemoveFile()).to.be.true
  438. })
  439. .expectSymmetry()
  440. })
  441. it('transforms MoveFile-RemoveFile transitive foo->, bar->foo', function () {
  442. runConcurrently(
  443. Operation.removeFile('foo'),
  444. Operation.moveFile('bar', 'foo'),
  445. makeTwoFileSnapshot()
  446. )
  447. .expectFiles([])
  448. .expectPrime(1, MoveFileOperation)
  449. .expectPrime(0, MoveFileOperation)
  450. .tap(function () {
  451. expect(this.primeOperations[1].isRemoveFile()).to.be.true
  452. expect(this.primeOperations[1].getPathname()).to.equal('bar')
  453. expect(this.primeOperations[0].isRemoveFile()).to.be.true
  454. expect(this.primeOperations[0].getPathname()).to.equal('foo')
  455. })
  456. .expectSymmetry()
  457. })
  458. it('transforms MoveFile-RemoveFile transitive foo->bar, bar->', function () {
  459. runConcurrently(
  460. Operation.moveFile('foo', 'bar'),
  461. Operation.removeFile('bar'),
  462. makeTwoFileSnapshot()
  463. )
  464. .expectFiles({})
  465. .expectPrime(1, MoveFileOperation)
  466. .expectPrime(0, MoveFileOperation)
  467. .tap(function () {
  468. expect(this.primeOperations[1].isRemoveFile()).to.be.true
  469. expect(this.primeOperations[1].getPathname()).to.equal('bar')
  470. expect(this.primeOperations[0].isRemoveFile()).to.be.true
  471. expect(this.primeOperations[0].getPathname()).to.equal('foo')
  472. })
  473. .expectSymmetry()
  474. })
  475. it('transforms MoveFile-EditFile with no conflict', function () {
  476. runConcurrently(
  477. Operation.moveFile('bar', 'baz'),
  478. edit('foo', ['x']),
  479. makeTwoFileSnapshot()
  480. )
  481. .expectFiles({ baz: 'a', foo: 'x' })
  482. .expectNoTransform()
  483. .expectSymmetry()
  484. })
  485. it('transforms MoveFile-EditFile with edit on pathname', function () {
  486. runConcurrently(
  487. Operation.moveFile('foo', 'bar'),
  488. edit('foo', ['x']),
  489. makeOneFileSnapshot()
  490. )
  491. .expectFiles({ bar: 'x' })
  492. .expectPrime(1, EditFileOperation)
  493. .expectPrime(0, MoveFileOperation)
  494. .tap(function () {
  495. expect(this.primeOperations[1].getPathname()).to.equal('bar')
  496. expect(this.primeOperations[0].getPathname()).to.equal('foo')
  497. expect(this.primeOperations[0].getNewPathname()).to.equal('bar')
  498. })
  499. .expectSymmetry()
  500. })
  501. it('transforms MoveFile-EditFile with edit on new pathname', function () {
  502. runConcurrently(
  503. Operation.moveFile('bar', 'foo'),
  504. edit('foo', ['x']),
  505. makeTwoFileSnapshot()
  506. )
  507. .expectFiles({ foo: 'a' })
  508. .expectPrime(1, NoOperation)
  509. .tap(function () {
  510. expect(this.primeOperations[0]).to.eql(this.operations[0])
  511. })
  512. .expectSymmetry()
  513. })
  514. it('transforms MoveFile-EditFile with no-op move', function () {
  515. runConcurrently(
  516. Operation.moveFile('foo', 'foo'),
  517. edit('foo', ['x']),
  518. makeOneFileSnapshot()
  519. )
  520. .expectFiles({ foo: 'x' })
  521. .expectNoTransform()
  522. .expectSymmetry()
  523. })
  524. it('transforms MoveFile-SetFileMetadata with no conflict', function () {
  525. const testMetadata = { baz: 1 }
  526. runConcurrently(
  527. Operation.moveFile('foo', 'baz'),
  528. Operation.setFileMetadata('bar', testMetadata),
  529. makeTwoFileSnapshot()
  530. )
  531. .expectNoTransform()
  532. .expectFiles({
  533. bar: { content: 'a', metadata: testMetadata, comments: [] },
  534. baz: '',
  535. })
  536. .expectSymmetry()
  537. })
  538. it('transforms MoveFile-SetFileMetadata with set on pathname', function () {
  539. const testMetadata = { baz: 1 }
  540. runConcurrently(
  541. Operation.moveFile('foo', 'bar'),
  542. Operation.setFileMetadata('foo', testMetadata),
  543. makeOneFileSnapshot()
  544. )
  545. .expectFiles({
  546. bar: { content: '', metadata: testMetadata, comments: [] },
  547. })
  548. .expectSymmetry()
  549. })
  550. it('transforms MoveFile-SetFileMetadata w/ set on new pathname', function () {
  551. const testMetadata = { baz: 1 }
  552. runConcurrently(
  553. Operation.moveFile('foo', 'bar'),
  554. Operation.setFileMetadata('bar', testMetadata),
  555. makeTwoFileSnapshot()
  556. )
  557. // move wins
  558. .expectFiles({ bar: { content: '', metadata: {}, comments: [] } })
  559. .expectSymmetry()
  560. })
  561. it('transforms MoveFile-SetFileMetadata with no-op move', function () {
  562. const testMetadata = { baz: 1 }
  563. runConcurrently(
  564. Operation.moveFile('foo', 'foo'),
  565. Operation.setFileMetadata('foo', testMetadata),
  566. makeOneFileSnapshot()
  567. )
  568. .expectFiles({
  569. foo: { content: '', metadata: testMetadata, comments: [] },
  570. })
  571. .expectSymmetry()
  572. })
  573. it('transforms EditFile-EditFile with no conflict', function () {
  574. runConcurrently(
  575. edit('foo', ['x']),
  576. edit('bar', [1, 'x']),
  577. makeTwoFileSnapshot()
  578. )
  579. .expectFiles({ bar: 'ax', foo: 'x' })
  580. .expectNoTransform()
  581. .expectSymmetry()
  582. })
  583. it('transforms EditFile-EditFile on same file', function () {
  584. runConcurrently(
  585. edit('foo', ['x']),
  586. edit('foo', ['y']),
  587. makeOneFileSnapshot()
  588. )
  589. .expectFiles({ foo: 'xy' })
  590. .expectPrime(1, EditFileOperation)
  591. .expectPrime(0, EditFileOperation)
  592. .tap(function () {
  593. expect(this.primeOperations[1].getOperation().toJSON()).to.eql({
  594. textOperation: [1, 'y'],
  595. })
  596. expect(this.primeOperations[0].getOperation().toJSON()).to.eql({
  597. textOperation: ['x', 1],
  598. })
  599. })
  600. .swap()
  601. .expectFiles({ foo: 'yx' })
  602. })
  603. it('transforms EditFile-RemoveFile with no conflict', function () {
  604. runConcurrently(
  605. edit('foo', ['x']),
  606. Operation.removeFile('bar'),
  607. makeTwoFileSnapshot()
  608. )
  609. .expectFiles({ foo: 'x' })
  610. .expectNoTransform()
  611. .expectSymmetry()
  612. })
  613. it('transforms EditFile-RemoveFile on same file', function () {
  614. runConcurrently(
  615. edit('foo', ['x']),
  616. Operation.removeFile('foo'),
  617. makeOneFileSnapshot()
  618. )
  619. .expectFiles({})
  620. .expectSymmetry()
  621. })
  622. it('transforms EditFile-SetFileMetadata with no conflict', function () {
  623. const testMetadata = { baz: 1 }
  624. runConcurrently(
  625. edit('foo', ['x']),
  626. Operation.setFileMetadata('bar', testMetadata),
  627. makeTwoFileSnapshot()
  628. )
  629. .expectNoTransform()
  630. .expectFiles({
  631. foo: { content: 'x', metadata: {}, comments: [] },
  632. bar: { content: 'a', metadata: testMetadata, comments: [] },
  633. })
  634. .expectSymmetry()
  635. })
  636. it('transforms EditFile-SetFileMetadata on same file', function () {
  637. const testMetadata = { baz: 1 }
  638. runConcurrently(
  639. edit('foo', ['x']),
  640. Operation.setFileMetadata('foo', testMetadata),
  641. makeOneFileSnapshot()
  642. )
  643. .expectNoTransform()
  644. .expectFiles({
  645. foo: { content: 'x', metadata: testMetadata, comments: [] },
  646. })
  647. .expectSymmetry()
  648. })
  649. it('transforms SetFileMetadata-SetFileMetadata w/ no conflict', function () {
  650. runConcurrently(
  651. Operation.setFileMetadata('foo', { baz: 1 }),
  652. Operation.setFileMetadata('bar', { baz: 2 }),
  653. makeTwoFileSnapshot()
  654. )
  655. .expectNoTransform()
  656. .expectFiles({
  657. foo: { content: '', metadata: { baz: 1 }, comments: [] },
  658. bar: { content: 'a', metadata: { baz: 2 }, comments: [] },
  659. })
  660. .expectSymmetry()
  661. })
  662. it('transforms SetFileMetadata-SetFileMetadata on same file', function () {
  663. runConcurrently(
  664. Operation.setFileMetadata('foo', { baz: 1 }),
  665. Operation.setFileMetadata('foo', { baz: 2 }),
  666. makeOneFileSnapshot()
  667. )
  668. // second op wins
  669. .expectFiles({ foo: { content: '', metadata: { baz: 2 }, comments: [] } })
  670. .swap()
  671. // first op wins
  672. .expectFiles({ foo: { content: '', metadata: { baz: 1 }, comments: [] } })
  673. })
  674. it('transforms SetFileMetadata-RemoveFile with no conflict', function () {
  675. const testMetadata = { baz: 1 }
  676. runConcurrently(
  677. Operation.setFileMetadata('foo', testMetadata),
  678. Operation.removeFile('bar'),
  679. makeTwoFileSnapshot()
  680. )
  681. .expectNoTransform()
  682. .expectFiles({
  683. foo: { content: '', metadata: testMetadata, comments: [] },
  684. })
  685. .expectSymmetry()
  686. })
  687. it('transforms SetFileMetadata-RemoveFile on same file', function () {
  688. const testMetadata = { baz: 1 }
  689. runConcurrently(
  690. Operation.setFileMetadata('foo', testMetadata),
  691. Operation.removeFile('foo'),
  692. makeOneFileSnapshot()
  693. )
  694. .expectFiles({})
  695. .expectSymmetry()
  696. })
  697. it('transforms no-op with other operation', function () {
  698. runConcurrently(Operation.NO_OP, addFile('foo', 'test')).expectFiles({
  699. foo: 'test',
  700. })
  701. })
  702. describe('EditFile sub operations', function () {
  703. it('transforms AddCommentOperation-AddCommentOperation', function () {
  704. runConcurrently(
  705. Operation.editFile(
  706. 'foo',
  707. AddCommentOperation.fromJSON({
  708. commentId: '1',
  709. ranges: [
  710. {
  711. pos: 10,
  712. length: 2,
  713. },
  714. ],
  715. })
  716. ),
  717. Operation.editFile(
  718. 'foo',
  719. AddCommentOperation.fromJSON({
  720. commentId: '1',
  721. ranges: [
  722. {
  723. pos: 0,
  724. length: 1,
  725. },
  726. ],
  727. })
  728. ),
  729. makeOneFileSnapshot()
  730. )
  731. .expectTransform()
  732. .expectFiles({
  733. foo: {
  734. content: '',
  735. metadata: {},
  736. comments: [
  737. {
  738. id: '1',
  739. ranges: [
  740. {
  741. pos: 0,
  742. length: 1,
  743. },
  744. ],
  745. resolved: false,
  746. },
  747. ],
  748. },
  749. })
  750. })
  751. it('transforms TextOperation-AddCommentOperation', function () {
  752. runConcurrently(
  753. Operation.editFile(
  754. 'foo',
  755. TextOperation.fromJSON({ textOperation: ['xyz'] })
  756. ),
  757. Operation.editFile(
  758. 'foo',
  759. AddCommentOperation.fromJSON({
  760. commentId: '1',
  761. ranges: [
  762. {
  763. pos: 0,
  764. length: 1,
  765. },
  766. ],
  767. })
  768. ),
  769. makeOneFileSnapshot()
  770. )
  771. .expectTransform()
  772. .expectFiles({
  773. foo: {
  774. content: 'xyz',
  775. metadata: {},
  776. comments: [
  777. { id: '1', ranges: [{ pos: 3, length: 1 }], resolved: false },
  778. ],
  779. },
  780. })
  781. .expectSymmetry()
  782. })
  783. it('transforms TextOperation-AddCommentOperation (insert with commentId)', function () {
  784. runConcurrently(
  785. Operation.editFile(
  786. 'foo',
  787. TextOperation.fromJSON({
  788. textOperation: [{ i: 'xyz', commentIds: ['1'] }],
  789. })
  790. ),
  791. Operation.editFile(
  792. 'foo',
  793. AddCommentOperation.fromJSON({
  794. commentId: '1',
  795. ranges: [
  796. {
  797. pos: 0,
  798. length: 1,
  799. },
  800. ],
  801. })
  802. ),
  803. makeOneFileSnapshot()
  804. )
  805. .expectTransform()
  806. .expectFiles({
  807. foo: {
  808. content: 'xyz',
  809. metadata: {},
  810. comments: [
  811. { id: '1', ranges: [{ pos: 0, length: 4 }], resolved: false },
  812. ],
  813. },
  814. })
  815. .expectSymmetry()
  816. })
  817. it('transforms AddCommentOperation-SetCommentStateOperation', function () {
  818. runConcurrently(
  819. Operation.editFile(
  820. 'foo',
  821. AddCommentOperation.fromJSON({
  822. commentId: '1',
  823. ranges: [{ pos: 1, length: 2 }],
  824. })
  825. ),
  826. Operation.editFile(
  827. 'foo',
  828. SetCommentStateOperation.fromJSON({
  829. commentId: '1',
  830. resolved: true,
  831. })
  832. ),
  833. makeOneFileSnapshot()
  834. )
  835. .expectTransform()
  836. .expectFiles({
  837. foo: {
  838. content: '',
  839. metadata: {},
  840. comments: [
  841. { id: '1', ranges: [{ pos: 1, length: 2 }], resolved: true },
  842. ],
  843. },
  844. })
  845. .expectSymmetry()
  846. })
  847. it('transforms AddCommentOperation-DeleteCommentOperation ', function () {
  848. runConcurrently(
  849. Operation.editFile(
  850. 'foo',
  851. AddCommentOperation.fromJSON({
  852. commentId: '1',
  853. ranges: [{ pos: 1, length: 2 }],
  854. })
  855. ),
  856. Operation.editFile(
  857. 'foo',
  858. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  859. ),
  860. makeOneFileSnapshot()
  861. )
  862. .expectTransform()
  863. .expectFiles({
  864. foo: {
  865. content: '',
  866. metadata: {},
  867. comments: [],
  868. },
  869. })
  870. .expectSymmetry()
  871. })
  872. it('transforms DeleteCommentOperation-SetCommentStateOperation ', function () {
  873. runConcurrently(
  874. Operation.editFile(
  875. 'foo',
  876. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  877. ),
  878. Operation.editFile(
  879. 'foo',
  880. SetCommentStateOperation.fromJSON({
  881. commentId: '1',
  882. resolved: true,
  883. })
  884. ),
  885. makeOneFileSnapshot()
  886. )
  887. .expectTransform()
  888. .expectFiles({
  889. foo: {
  890. content: '',
  891. metadata: {},
  892. comments: [],
  893. },
  894. })
  895. .expectSymmetry()
  896. })
  897. it('transforms DeleteCommentOperation-DeleteCommentOperation ', function () {
  898. runConcurrently(
  899. Operation.editFile(
  900. 'foo',
  901. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  902. ),
  903. Operation.editFile(
  904. 'foo',
  905. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  906. ),
  907. makeOneFileSnapshot()
  908. )
  909. .expectTransform()
  910. .expectFiles({
  911. foo: {
  912. content: '',
  913. metadata: {},
  914. comments: [],
  915. },
  916. })
  917. .expectSymmetry()
  918. })
  919. it('transforms SetCommentStateOperation-SetCommentStateOperation to resolved comment', function () {
  920. const snapshot = makeEmptySnapshot()
  921. const file = new File(
  922. new StringFileData('xyz', [
  923. { id: '1', ranges: [{ pos: 0, length: 3 }] },
  924. ])
  925. )
  926. snapshot.addFile('foo', file)
  927. runConcurrently(
  928. Operation.editFile(
  929. 'foo',
  930. SetCommentStateOperation.fromJSON({
  931. commentId: '1',
  932. resolved: true,
  933. })
  934. ),
  935. Operation.editFile(
  936. 'foo',
  937. SetCommentStateOperation.fromJSON({
  938. commentId: '1',
  939. resolved: true,
  940. })
  941. ),
  942. snapshot
  943. )
  944. .expectTransform()
  945. .expectFiles({
  946. foo: {
  947. content: 'xyz',
  948. metadata: {},
  949. comments: [
  950. { id: '1', ranges: [{ pos: 0, length: 3 }], resolved: true },
  951. ],
  952. },
  953. })
  954. .expectSymmetry()
  955. })
  956. it('transforms SetCommentStateOperation-SetCommentStateOperation to unresolved comment', function () {
  957. const snapshot = makeEmptySnapshot()
  958. const file = new File(
  959. new StringFileData('xyz', [
  960. { id: '1', ranges: [{ pos: 0, length: 3 }] },
  961. ])
  962. )
  963. snapshot.addFile('foo', file)
  964. runConcurrently(
  965. Operation.editFile(
  966. 'foo',
  967. SetCommentStateOperation.fromJSON({
  968. commentId: '1',
  969. resolved: true,
  970. })
  971. ),
  972. Operation.editFile(
  973. 'foo',
  974. SetCommentStateOperation.fromJSON({
  975. commentId: '1',
  976. resolved: false,
  977. })
  978. ),
  979. snapshot
  980. )
  981. .expectTransform()
  982. .expectFiles({
  983. foo: {
  984. content: 'xyz',
  985. metadata: {},
  986. comments: [
  987. { id: '1', ranges: [{ pos: 0, length: 3 }], resolved: false },
  988. ],
  989. },
  990. })
  991. .expectSymmetry()
  992. })
  993. })
  994. })