operation.test.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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. },
  748. ],
  749. },
  750. })
  751. })
  752. it('transforms TextOperation-AddCommentOperation', function () {
  753. runConcurrently(
  754. Operation.editFile(
  755. 'foo',
  756. TextOperation.fromJSON({ textOperation: ['xyz'] })
  757. ),
  758. Operation.editFile(
  759. 'foo',
  760. AddCommentOperation.fromJSON({
  761. commentId: '1',
  762. ranges: [
  763. {
  764. pos: 0,
  765. length: 1,
  766. },
  767. ],
  768. })
  769. ),
  770. makeOneFileSnapshot()
  771. )
  772. .expectTransform()
  773. .expectFiles({
  774. foo: {
  775. content: 'xyz',
  776. metadata: {},
  777. comments: [{ id: '1', ranges: [{ pos: 3, length: 1 }] }],
  778. },
  779. })
  780. .expectSymmetry()
  781. })
  782. it('transforms TextOperation-AddCommentOperation (insert with commentId)', function () {
  783. runConcurrently(
  784. Operation.editFile(
  785. 'foo',
  786. TextOperation.fromJSON({
  787. textOperation: [{ i: 'xyz', commentIds: ['1'] }],
  788. })
  789. ),
  790. Operation.editFile(
  791. 'foo',
  792. AddCommentOperation.fromJSON({
  793. commentId: '1',
  794. ranges: [
  795. {
  796. pos: 0,
  797. length: 1,
  798. },
  799. ],
  800. })
  801. ),
  802. makeOneFileSnapshot()
  803. )
  804. .expectTransform()
  805. .expectFiles({
  806. foo: {
  807. content: 'xyz',
  808. metadata: {},
  809. comments: [{ id: '1', ranges: [{ pos: 0, length: 4 }] }],
  810. },
  811. })
  812. .expectSymmetry()
  813. })
  814. it('transforms AddCommentOperation-SetCommentStateOperation', function () {
  815. runConcurrently(
  816. Operation.editFile(
  817. 'foo',
  818. AddCommentOperation.fromJSON({
  819. commentId: '1',
  820. ranges: [{ pos: 1, length: 2 }],
  821. })
  822. ),
  823. Operation.editFile(
  824. 'foo',
  825. SetCommentStateOperation.fromJSON({
  826. commentId: '1',
  827. resolved: true,
  828. })
  829. ),
  830. makeOneFileSnapshot()
  831. )
  832. .expectTransform()
  833. .expectFiles({
  834. foo: {
  835. content: '',
  836. metadata: {},
  837. comments: [
  838. { id: '1', ranges: [{ pos: 1, length: 2 }], resolved: true },
  839. ],
  840. },
  841. })
  842. .expectSymmetry()
  843. })
  844. it('transforms AddCommentOperation-DeleteCommentOperation ', function () {
  845. runConcurrently(
  846. Operation.editFile(
  847. 'foo',
  848. AddCommentOperation.fromJSON({
  849. commentId: '1',
  850. ranges: [{ pos: 1, length: 2 }],
  851. })
  852. ),
  853. Operation.editFile(
  854. 'foo',
  855. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  856. ),
  857. makeOneFileSnapshot()
  858. )
  859. .expectTransform()
  860. .expectFiles({
  861. foo: {
  862. content: '',
  863. metadata: {},
  864. comments: [],
  865. },
  866. })
  867. .expectSymmetry()
  868. })
  869. it('transforms DeleteCommentOperation-SetCommentStateOperation ', function () {
  870. runConcurrently(
  871. Operation.editFile(
  872. 'foo',
  873. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  874. ),
  875. Operation.editFile(
  876. 'foo',
  877. SetCommentStateOperation.fromJSON({
  878. commentId: '1',
  879. resolved: true,
  880. })
  881. ),
  882. makeOneFileSnapshot()
  883. )
  884. .expectTransform()
  885. .expectFiles({
  886. foo: {
  887. content: '',
  888. metadata: {},
  889. comments: [],
  890. },
  891. })
  892. .expectSymmetry()
  893. })
  894. it('transforms DeleteCommentOperation-DeleteCommentOperation ', function () {
  895. runConcurrently(
  896. Operation.editFile(
  897. 'foo',
  898. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  899. ),
  900. Operation.editFile(
  901. 'foo',
  902. DeleteCommentOperation.fromJSON({ deleteComment: '1' })
  903. ),
  904. makeOneFileSnapshot()
  905. )
  906. .expectTransform()
  907. .expectFiles({
  908. foo: {
  909. content: '',
  910. metadata: {},
  911. comments: [],
  912. },
  913. })
  914. .expectSymmetry()
  915. })
  916. it('transforms SetCommentStateOperation-SetCommentStateOperation to resolved comment', function () {
  917. const snapshot = makeEmptySnapshot()
  918. const file = new File(
  919. new StringFileData('xyz', [
  920. { id: '1', ranges: [{ pos: 0, length: 3 }] },
  921. ])
  922. )
  923. snapshot.addFile('foo', file)
  924. runConcurrently(
  925. Operation.editFile(
  926. 'foo',
  927. SetCommentStateOperation.fromJSON({
  928. commentId: '1',
  929. resolved: true,
  930. })
  931. ),
  932. Operation.editFile(
  933. 'foo',
  934. SetCommentStateOperation.fromJSON({
  935. commentId: '1',
  936. resolved: true,
  937. })
  938. ),
  939. snapshot
  940. )
  941. .expectTransform()
  942. .expectFiles({
  943. foo: {
  944. content: 'xyz',
  945. metadata: {},
  946. comments: [
  947. { id: '1', ranges: [{ pos: 0, length: 3 }], resolved: true },
  948. ],
  949. },
  950. })
  951. .expectSymmetry()
  952. })
  953. it('transforms SetCommentStateOperation-SetCommentStateOperation to unresolved comment', function () {
  954. const snapshot = makeEmptySnapshot()
  955. const file = new File(
  956. new StringFileData('xyz', [
  957. { id: '1', ranges: [{ pos: 0, length: 3 }] },
  958. ])
  959. )
  960. snapshot.addFile('foo', file)
  961. runConcurrently(
  962. Operation.editFile(
  963. 'foo',
  964. SetCommentStateOperation.fromJSON({
  965. commentId: '1',
  966. resolved: true,
  967. })
  968. ),
  969. Operation.editFile(
  970. 'foo',
  971. SetCommentStateOperation.fromJSON({
  972. commentId: '1',
  973. resolved: false,
  974. })
  975. ),
  976. snapshot
  977. )
  978. .expectTransform()
  979. .expectFiles({
  980. foo: {
  981. content: 'xyz',
  982. metadata: {},
  983. comments: [{ id: '1', ranges: [{ pos: 0, length: 3 }] }],
  984. },
  985. })
  986. .expectSymmetry()
  987. })
  988. })
  989. })