operation.test.js 30 KB

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