UpdateTranslatorTests.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { strict as esmock } from 'esmock'
  4. import Core from 'overleaf-editor-core'
  5. const MODULE_PATH = '../../../../app/js/UpdateTranslator.js'
  6. describe('UpdateTranslator', function () {
  7. beforeEach(async function () {
  8. this.UpdateTranslator = await esmock(MODULE_PATH, {
  9. 'overleaf-editor-core': Core,
  10. })
  11. this.callback = sinon.stub()
  12. this.project_id = '59bfd450e3028c4d40a1e9aa'
  13. this.doc_id = '59bfd450e3028c4d40a1e9ab'
  14. this.file_id = '59bfd450e3028c4d40a1easd'
  15. this.user_id = '59bb9051abf6e8682a269b64'
  16. this.version = 0
  17. this.timestamp = new Date().toJSON()
  18. this.mockBlobHash = '12345abc12345abc12345abc12345abc12345abc'
  19. })
  20. describe('convertToChanges', function () {
  21. it('can translate doc additions', function () {
  22. const updates = [
  23. {
  24. update: {
  25. doc: this.doc_id,
  26. pathname: '/main.tex',
  27. docLines: 'a\nb',
  28. meta: {
  29. user_id: this.user_id,
  30. ts: this.timestamp,
  31. },
  32. },
  33. blobHash: this.mockBlobHash,
  34. },
  35. ]
  36. const changes = this.UpdateTranslator.convertToChanges(
  37. this.project_id,
  38. updates
  39. ).map(change => change.toRaw())
  40. expect(changes).to.deep.equal([
  41. {
  42. authors: [],
  43. operations: [
  44. {
  45. pathname: 'main.tex',
  46. file: {
  47. hash: this.mockBlobHash,
  48. },
  49. },
  50. ],
  51. v2Authors: [this.user_id],
  52. timestamp: this.timestamp,
  53. },
  54. ])
  55. })
  56. it('can translate file additions', function () {
  57. const updates = [
  58. {
  59. update: {
  60. file: this.file_id,
  61. pathname: '/test.png',
  62. url: 'filestore.example.com/test.png',
  63. meta: {
  64. user_id: this.user_id,
  65. ts: this.timestamp,
  66. },
  67. },
  68. blobHash: this.mockBlobHash,
  69. },
  70. ]
  71. const changes = this.UpdateTranslator.convertToChanges(
  72. this.project_id,
  73. updates
  74. ).map(change => change.toRaw())
  75. expect(changes).to.deep.equal([
  76. {
  77. authors: [],
  78. operations: [
  79. {
  80. pathname: 'test.png',
  81. file: {
  82. hash: this.mockBlobHash,
  83. },
  84. },
  85. ],
  86. v2Authors: [this.user_id],
  87. timestamp: this.timestamp,
  88. },
  89. ])
  90. })
  91. it('can translate doc renames', function () {
  92. const updates = [
  93. {
  94. update: {
  95. doc: this.doc_id,
  96. pathname: '/main.tex',
  97. new_pathname: '/new_main.tex',
  98. meta: {
  99. user_id: this.user_id,
  100. ts: this.timestamp,
  101. },
  102. },
  103. },
  104. ]
  105. const changes = this.UpdateTranslator.convertToChanges(
  106. this.project_id,
  107. updates
  108. ).map(change => change.toRaw())
  109. expect(changes).to.deep.equal([
  110. {
  111. authors: [],
  112. operations: [
  113. {
  114. pathname: 'main.tex',
  115. newPathname: 'new_main.tex',
  116. },
  117. ],
  118. v2Authors: [this.user_id],
  119. timestamp: this.timestamp,
  120. },
  121. ])
  122. })
  123. it('can translate file renames', function () {
  124. const updates = [
  125. {
  126. update: {
  127. file: this.file_id,
  128. pathname: '/test.png',
  129. new_pathname: '/new_test.png',
  130. meta: {
  131. user_id: this.user_id,
  132. ts: this.timestamp,
  133. },
  134. },
  135. },
  136. ]
  137. const changes = this.UpdateTranslator.convertToChanges(
  138. this.project_id,
  139. updates
  140. ).map(change => change.toRaw())
  141. expect(changes).to.deep.equal([
  142. {
  143. authors: [],
  144. operations: [
  145. {
  146. pathname: 'test.png',
  147. newPathname: 'new_test.png',
  148. },
  149. ],
  150. v2Authors: [this.user_id],
  151. timestamp: this.timestamp,
  152. },
  153. ])
  154. })
  155. it('can translate multiple updates with the correct versions', function () {
  156. const updates = [
  157. {
  158. update: {
  159. doc: this.doc_id,
  160. pathname: '/main.tex',
  161. docLines: 'a\nb',
  162. meta: {
  163. user_id: this.user_id,
  164. ts: this.timestamp,
  165. },
  166. },
  167. blobHash: this.mockBlobHash,
  168. },
  169. {
  170. update: {
  171. file: this.file_id,
  172. pathname: '/test.png',
  173. url: 'filestore.example.com/test.png',
  174. meta: {
  175. user_id: this.user_id,
  176. ts: this.timestamp,
  177. },
  178. },
  179. blobHash: this.mockBlobHash,
  180. },
  181. ]
  182. const changes = this.UpdateTranslator.convertToChanges(
  183. this.project_id,
  184. updates
  185. ).map(change => change.toRaw())
  186. expect(changes).to.deep.equal([
  187. {
  188. authors: [],
  189. operations: [
  190. {
  191. pathname: 'main.tex',
  192. file: {
  193. hash: this.mockBlobHash,
  194. },
  195. },
  196. ],
  197. v2Authors: [this.user_id],
  198. timestamp: this.timestamp,
  199. },
  200. {
  201. authors: [],
  202. operations: [
  203. {
  204. pathname: 'test.png',
  205. file: {
  206. hash: this.mockBlobHash,
  207. },
  208. },
  209. ],
  210. v2Authors: [this.user_id],
  211. timestamp: this.timestamp,
  212. },
  213. ])
  214. })
  215. it('returns an error if the update has an unknown format', function () {
  216. const updates = [
  217. {
  218. update: {
  219. foo: 'bar',
  220. },
  221. },
  222. ]
  223. expect(() =>
  224. this.UpdateTranslator.convertToChanges(this.project_id, updates)
  225. ).to.throw('update with unknown format')
  226. })
  227. it('replaces backslashes with underscores in pathnames', function () {
  228. const updates = [
  229. {
  230. update: {
  231. doc: this.doc_id,
  232. pathname: '/\\main\\foo.tex',
  233. new_pathname: '/\\new_main\\foo\\bar.tex',
  234. meta: {
  235. user_id: this.user_id,
  236. ts: this.timestamp,
  237. },
  238. },
  239. },
  240. ]
  241. const changes = this.UpdateTranslator.convertToChanges(
  242. this.project_id,
  243. updates
  244. ).map(change => change.toRaw())
  245. expect(changes).to.deep.equal([
  246. {
  247. authors: [],
  248. operations: [
  249. {
  250. pathname: '_main_foo.tex',
  251. newPathname: '_new_main_foo_bar.tex',
  252. },
  253. ],
  254. v2Authors: [this.user_id],
  255. timestamp: this.timestamp,
  256. },
  257. ])
  258. })
  259. it('replaces leading asterisks with __ASTERISK__ in pathnames', function () {
  260. const updates = [
  261. {
  262. update: {
  263. file: this.file_id,
  264. pathname: '/test*test.png',
  265. meta: {
  266. user_id: this.user_id,
  267. ts: this.timestamp,
  268. },
  269. url: 'filestore.example.com/test*test.png',
  270. },
  271. blobHash: this.mockBlobHash,
  272. },
  273. ]
  274. const changes = this.UpdateTranslator.convertToChanges(
  275. this.project_id,
  276. updates
  277. ).map(change => change.toRaw())
  278. expect(changes).to.deep.equal([
  279. {
  280. authors: [],
  281. operations: [
  282. {
  283. pathname: 'test__ASTERISK__test.png',
  284. file: {
  285. hash: this.mockBlobHash,
  286. },
  287. },
  288. ],
  289. v2Authors: [this.user_id],
  290. timestamp: this.timestamp,
  291. },
  292. ])
  293. })
  294. it('replaces a leading space for top-level files with __SPACE__', function () {
  295. const updates = [
  296. {
  297. update: {
  298. file: this.file_id,
  299. pathname: '/ test.png',
  300. meta: {
  301. user_id: this.user_id,
  302. ts: this.timestamp,
  303. },
  304. url: 'filestore.example.com/test.png',
  305. },
  306. blobHash: this.mockBlobHash,
  307. },
  308. ]
  309. const changes = this.UpdateTranslator.convertToChanges(
  310. this.project_id,
  311. updates
  312. ).map(change => change.toRaw())
  313. expect(changes).to.deep.equal([
  314. {
  315. authors: [],
  316. operations: [
  317. {
  318. pathname: '__SPACE__test.png',
  319. file: {
  320. hash: this.mockBlobHash,
  321. },
  322. },
  323. ],
  324. v2Authors: [this.user_id],
  325. timestamp: this.timestamp,
  326. },
  327. ])
  328. })
  329. it('replaces leading spaces of files in subfolders with __SPACE__', function () {
  330. const updates = [
  331. {
  332. update: {
  333. file: this.file_id,
  334. pathname: '/folder/ test.png',
  335. meta: {
  336. user_id: this.user_id,
  337. ts: this.timestamp,
  338. },
  339. url: 'filestore.example.com/folder/test.png',
  340. },
  341. blobHash: this.mockBlobHash,
  342. },
  343. ]
  344. const changes = this.UpdateTranslator.convertToChanges(
  345. this.project_id,
  346. updates
  347. ).map(change => change.toRaw())
  348. expect(changes).to.deep.equal([
  349. {
  350. authors: [],
  351. operations: [
  352. {
  353. pathname: 'folder/__SPACE__test.png',
  354. file: {
  355. hash: this.mockBlobHash,
  356. },
  357. },
  358. ],
  359. v2Authors: [this.user_id],
  360. timestamp: this.timestamp,
  361. },
  362. ])
  363. })
  364. it('sets a null author when user_id is "anonymous-user"', function () {
  365. const updates = [
  366. {
  367. update: {
  368. doc: this.doc_id,
  369. pathname: '/main.tex',
  370. docLines: 'a\nb',
  371. meta: {
  372. user_id: 'anonymous-user',
  373. ts: this.timestamp,
  374. },
  375. },
  376. blobHash: this.mockBlobHash,
  377. },
  378. ]
  379. const changes = this.UpdateTranslator.convertToChanges(
  380. this.project_id,
  381. updates
  382. ).map(change => change.toRaw())
  383. expect(changes).to.deep.equal([
  384. {
  385. authors: [],
  386. operations: [
  387. {
  388. pathname: 'main.tex',
  389. file: {
  390. hash: this.mockBlobHash,
  391. },
  392. },
  393. ],
  394. v2Authors: [null],
  395. timestamp: this.timestamp,
  396. },
  397. ])
  398. })
  399. it('sets an empty array as author when there is no meta.user_id', function () {
  400. const updates = [
  401. {
  402. update: {
  403. doc: this.doc_id,
  404. pathname: '/main.tex',
  405. docLines: 'a\nb',
  406. meta: {
  407. ts: this.timestamp,
  408. },
  409. },
  410. blobHash: this.mockBlobHash,
  411. },
  412. ]
  413. const changes = this.UpdateTranslator.convertToChanges(
  414. this.project_id,
  415. updates
  416. ).map(change => change.toRaw())
  417. expect(changes).to.deep.equal([
  418. {
  419. authors: [],
  420. operations: [
  421. {
  422. pathname: 'main.tex',
  423. file: {
  424. hash: this.mockBlobHash,
  425. },
  426. },
  427. ],
  428. v2Authors: [],
  429. timestamp: this.timestamp,
  430. },
  431. ])
  432. })
  433. describe('text updates', function () {
  434. it('can translate insertions', function () {
  435. const updates = [
  436. {
  437. update: {
  438. doc: this.doc_id,
  439. op: [
  440. { p: 3, i: 'foo' },
  441. { p: 15, i: 'bar', commentIds: ['comment1'] },
  442. ],
  443. v: this.version,
  444. meta: {
  445. user_id: this.user_id,
  446. ts: new Date(this.timestamp).getTime(),
  447. pathname: '/main.tex',
  448. doc_length: 20,
  449. source: 'some-editor-id',
  450. },
  451. },
  452. },
  453. ]
  454. const changes = this.UpdateTranslator.convertToChanges(
  455. this.project_id,
  456. updates
  457. ).map(change => change.toRaw())
  458. expect(changes).to.deep.equal([
  459. {
  460. authors: [],
  461. operations: [
  462. {
  463. pathname: 'main.tex',
  464. textOperation: [
  465. 3,
  466. 'foo',
  467. 9,
  468. { i: 'bar', commentIds: ['comment1'] },
  469. 8,
  470. ],
  471. },
  472. ],
  473. v2Authors: [this.user_id],
  474. timestamp: this.timestamp,
  475. v2DocVersions: {
  476. '59bfd450e3028c4d40a1e9ab': {
  477. pathname: 'main.tex',
  478. v: 0,
  479. },
  480. },
  481. },
  482. ])
  483. })
  484. it('can translate deletions', function () {
  485. const updates = [
  486. {
  487. update: {
  488. doc: this.doc_id,
  489. op: [
  490. { p: 3, d: 'lo' },
  491. { p: 10, d: 'bar' },
  492. ],
  493. v: this.version,
  494. meta: {
  495. user_id: this.user_id,
  496. ts: new Date(this.timestamp).getTime(),
  497. pathname: '/main.tex',
  498. doc_length: 20,
  499. },
  500. },
  501. },
  502. ]
  503. const changes = this.UpdateTranslator.convertToChanges(
  504. this.project_id,
  505. updates
  506. ).map(change => change.toRaw())
  507. expect(changes).to.deep.equal([
  508. {
  509. authors: [],
  510. operations: [
  511. {
  512. pathname: 'main.tex',
  513. textOperation: [3, -2, 7, -3, 5],
  514. },
  515. ],
  516. v2Authors: [this.user_id],
  517. timestamp: this.timestamp,
  518. v2DocVersions: {
  519. '59bfd450e3028c4d40a1e9ab': {
  520. pathname: 'main.tex',
  521. v: 0,
  522. },
  523. },
  524. },
  525. ])
  526. })
  527. it('should translate retains without tracking data', function () {
  528. const updates = [
  529. {
  530. update: {
  531. doc: this.doc_id,
  532. op: [
  533. {
  534. p: 3,
  535. r: 'lo',
  536. },
  537. ],
  538. v: this.version,
  539. meta: {
  540. user_id: this.user_id,
  541. ts: new Date(this.timestamp).getTime(),
  542. pathname: '/main.tex',
  543. doc_length: 20,
  544. },
  545. },
  546. },
  547. ]
  548. const changes = this.UpdateTranslator.convertToChanges(
  549. this.project_id,
  550. updates
  551. ).map(change => change.toRaw())
  552. expect(changes).to.deep.equal([
  553. {
  554. authors: [],
  555. operations: [
  556. {
  557. pathname: 'main.tex',
  558. textOperation: [20],
  559. },
  560. ],
  561. v2Authors: [this.user_id],
  562. timestamp: this.timestamp,
  563. v2DocVersions: {
  564. [this.doc_id]: {
  565. pathname: 'main.tex',
  566. v: 0,
  567. },
  568. },
  569. },
  570. ])
  571. })
  572. it('can translate retains with tracking data', function () {
  573. const updates = [
  574. {
  575. update: {
  576. doc: this.doc_id,
  577. op: [
  578. {
  579. p: 3,
  580. r: 'lo',
  581. tracking: {
  582. type: 'none',
  583. ts: this.timestamp,
  584. userId: this.user_id,
  585. },
  586. },
  587. ],
  588. v: this.version,
  589. meta: {
  590. user_id: this.user_id,
  591. ts: new Date(this.timestamp).getTime(),
  592. pathname: '/main.tex',
  593. doc_length: 20,
  594. },
  595. },
  596. },
  597. ]
  598. const changes = this.UpdateTranslator.convertToChanges(
  599. this.project_id,
  600. updates
  601. ).map(change => change.toRaw())
  602. expect(changes).to.deep.equal([
  603. {
  604. authors: [],
  605. operations: [
  606. {
  607. pathname: 'main.tex',
  608. textOperation: [
  609. 3,
  610. {
  611. r: 2,
  612. tracking: {
  613. type: 'none',
  614. ts: this.timestamp,
  615. userId: this.user_id,
  616. },
  617. },
  618. 15,
  619. ],
  620. },
  621. ],
  622. v2Authors: [this.user_id],
  623. timestamp: this.timestamp,
  624. v2DocVersions: {
  625. '59bfd450e3028c4d40a1e9ab': {
  626. pathname: 'main.tex',
  627. v: 0,
  628. },
  629. },
  630. },
  631. ])
  632. })
  633. it('can translate insertions at the start and end (with zero retained)', function () {
  634. const updates = [
  635. {
  636. update: {
  637. doc: this.doc_id,
  638. op: [
  639. { p: 0, i: 'foo' },
  640. { p: 23, i: 'bar' },
  641. { p: 0, d: 'foo' },
  642. { p: 20, d: 'bar' },
  643. ],
  644. v: this.version,
  645. meta: {
  646. user_id: this.user_id,
  647. ts: new Date(this.timestamp).getTime(),
  648. pathname: '/main.tex',
  649. doc_length: 20,
  650. },
  651. },
  652. },
  653. ]
  654. const changes = this.UpdateTranslator.convertToChanges(
  655. this.project_id,
  656. updates
  657. ).map(change => change.toRaw())
  658. expect(changes).to.deep.equal([
  659. {
  660. authors: [],
  661. operations: [
  662. {
  663. pathname: 'main.tex',
  664. textOperation: [20],
  665. },
  666. ],
  667. v2Authors: [this.user_id],
  668. timestamp: this.timestamp,
  669. v2DocVersions: {
  670. '59bfd450e3028c4d40a1e9ab': {
  671. pathname: 'main.tex',
  672. v: 0,
  673. },
  674. },
  675. },
  676. ])
  677. })
  678. it('can handle operations in non-linear offset order', function () {
  679. const updates = [
  680. {
  681. update: {
  682. doc: this.doc_id,
  683. op: [
  684. { p: 15, i: 'foo' },
  685. { p: 3, i: 'bar' },
  686. ],
  687. v: this.version,
  688. meta: {
  689. user_id: this.user_id,
  690. ts: this.timestamp,
  691. pathname: '/main.tex',
  692. doc_length: 20,
  693. },
  694. },
  695. },
  696. ]
  697. const changes = this.UpdateTranslator.convertToChanges(
  698. this.project_id,
  699. updates
  700. ).map(change => change.toRaw())
  701. expect(changes).to.deep.equal([
  702. {
  703. authors: [],
  704. operations: [
  705. {
  706. pathname: 'main.tex',
  707. textOperation: [3, 'bar', 12, 'foo', 5],
  708. },
  709. ],
  710. v2Authors: [this.user_id],
  711. timestamp: this.timestamp,
  712. v2DocVersions: {
  713. '59bfd450e3028c4d40a1e9ab': {
  714. pathname: 'main.tex',
  715. v: 0,
  716. },
  717. },
  718. },
  719. ])
  720. })
  721. it('handles comment ops', function () {
  722. const updates = [
  723. {
  724. update: {
  725. doc: this.doc_id,
  726. op: [
  727. { p: 0, i: 'foo' },
  728. { p: 3, d: 'bar' },
  729. { p: 5, c: 'comment this', t: 'comment-id-1' },
  730. { p: 7, c: 'another comment', t: 'comment-id-2' },
  731. { p: 10, i: 'baz' },
  732. ],
  733. v: this.version,
  734. meta: {
  735. user_id: this.user_id,
  736. ts: new Date(this.timestamp).getTime(),
  737. pathname: '/main.tex',
  738. doc_length: 20,
  739. },
  740. },
  741. },
  742. ]
  743. const changes = this.UpdateTranslator.convertToChanges(
  744. this.project_id,
  745. updates
  746. ).map(change => change.toRaw())
  747. expect(changes).to.deep.equal([
  748. {
  749. authors: [],
  750. operations: [
  751. {
  752. pathname: 'main.tex',
  753. textOperation: ['foo', -3, 17],
  754. },
  755. {
  756. pathname: 'main.tex',
  757. commentId: 'comment-id-1',
  758. ranges: [{ pos: 5, length: 12 }],
  759. resolved: false,
  760. },
  761. {
  762. pathname: 'main.tex',
  763. commentId: 'comment-id-2',
  764. ranges: [{ pos: 7, length: 15 }],
  765. resolved: false,
  766. },
  767. {
  768. pathname: 'main.tex',
  769. textOperation: [10, 'baz', 10],
  770. },
  771. ],
  772. v2Authors: [this.user_id],
  773. timestamp: this.timestamp,
  774. v2DocVersions: {
  775. '59bfd450e3028c4d40a1e9ab': {
  776. pathname: 'main.tex',
  777. v: 0,
  778. },
  779. },
  780. },
  781. ])
  782. })
  783. it('handles insertions after the end of the document', function () {
  784. const updates = [
  785. {
  786. update: {
  787. doc: this.doc_id,
  788. op: [{ p: 3, i: '\\' }],
  789. v: this.version,
  790. meta: {
  791. user_id: this.user_id,
  792. ts: new Date(this.timestamp).getTime(),
  793. pathname: '/main.tex',
  794. doc_length: 2,
  795. },
  796. },
  797. },
  798. ]
  799. const changes = this.UpdateTranslator.convertToChanges(
  800. this.project_id,
  801. updates
  802. ).map(change => change.toRaw())
  803. expect(changes).to.deep.equal([
  804. {
  805. authors: [],
  806. operations: [
  807. {
  808. pathname: 'main.tex',
  809. textOperation: [2, '\\'],
  810. },
  811. ],
  812. v2Authors: [this.user_id],
  813. timestamp: this.timestamp,
  814. v2DocVersions: {
  815. '59bfd450e3028c4d40a1e9ab': {
  816. pathname: 'main.tex',
  817. v: 0,
  818. },
  819. },
  820. },
  821. ])
  822. })
  823. it('translates external source metadata into an origin', function () {
  824. const updates = [
  825. {
  826. update: {
  827. doc: this.doc_id,
  828. op: [{ p: 3, i: 'foo' }],
  829. v: this.version,
  830. meta: {
  831. user_id: this.user_id,
  832. ts: new Date(this.timestamp).getTime(),
  833. pathname: '/main.tex',
  834. doc_length: 20,
  835. type: 'external',
  836. source: 'dropbox',
  837. },
  838. },
  839. },
  840. ]
  841. const changes = this.UpdateTranslator.convertToChanges(
  842. this.project_id,
  843. updates
  844. ).map(change => change.toRaw())
  845. expect(changes).to.deep.equal([
  846. {
  847. authors: [],
  848. operations: [
  849. {
  850. pathname: 'main.tex',
  851. textOperation: [3, 'foo', 17],
  852. },
  853. ],
  854. v2Authors: [this.user_id],
  855. timestamp: this.timestamp,
  856. v2DocVersions: {
  857. '59bfd450e3028c4d40a1e9ab': {
  858. pathname: 'main.tex',
  859. v: 0,
  860. },
  861. },
  862. origin: { kind: 'dropbox' },
  863. },
  864. ])
  865. })
  866. it('errors on unexpected ops', function () {
  867. const updates = [
  868. {
  869. update: {
  870. doc: this.doc_id,
  871. op: [{ p: 5, z: 'bar' }],
  872. v: this.version,
  873. meta: {
  874. user_id: this.user_id,
  875. ts: new Date(this.timestamp).getTime(),
  876. pathname: '/main.tex',
  877. doc_length: 20,
  878. },
  879. },
  880. },
  881. ]
  882. expect(() => {
  883. this.UpdateTranslator.convertToChanges(this.project_id, updates)
  884. }).to.throw('unexpected op type')
  885. })
  886. })
  887. describe('text updates with history metadata', function () {
  888. it('handles deletes over tracked deletes', function () {
  889. const updates = [
  890. {
  891. update: {
  892. doc: this.doc_id,
  893. op: [
  894. { i: 'foo', p: 3, hpos: 5 },
  895. {
  896. d: 'quux',
  897. p: 10,
  898. hpos: 15,
  899. trackedChanges: [
  900. { type: 'delete', offset: 2, length: 3 },
  901. { type: 'delete', offset: 3, length: 1 },
  902. ],
  903. },
  904. { c: 'noteworthy', p: 8, t: 'comment-id', hpos: 11, hlen: 14 },
  905. ],
  906. v: this.version,
  907. meta: {
  908. user_id: this.user_id,
  909. ts: new Date(this.timestamp).getTime(),
  910. pathname: '/main.tex',
  911. doc_length: 20,
  912. history_doc_length: 30,
  913. source: 'some-editor-id',
  914. },
  915. },
  916. },
  917. ]
  918. const changes = this.UpdateTranslator.convertToChanges(
  919. this.project_id,
  920. updates
  921. ).map(change => change.toRaw())
  922. expect(changes).to.deep.equal([
  923. {
  924. authors: [],
  925. operations: [
  926. {
  927. pathname: 'main.tex',
  928. textOperation: [5, 'foo', 7, -2, 3, -1, 1, -1, 10],
  929. },
  930. {
  931. pathname: 'main.tex',
  932. commentId: 'comment-id',
  933. ranges: [{ pos: 11, length: 14 }],
  934. resolved: false,
  935. },
  936. ],
  937. v2Authors: [this.user_id],
  938. timestamp: this.timestamp,
  939. v2DocVersions: {
  940. '59bfd450e3028c4d40a1e9ab': {
  941. pathname: 'main.tex',
  942. v: 0,
  943. },
  944. },
  945. },
  946. ])
  947. })
  948. it('handles tracked delete rejections specially', function () {
  949. const updates = [
  950. {
  951. update: {
  952. doc: this.doc_id,
  953. op: [{ i: 'foo', p: 3, trackedDeleteRejection: true }],
  954. v: this.version,
  955. meta: {
  956. user_id: this.user_id,
  957. ts: new Date(this.timestamp).getTime(),
  958. pathname: '/main.tex',
  959. doc_length: 20,
  960. source: 'some-editor-id',
  961. },
  962. },
  963. },
  964. ]
  965. const changes = this.UpdateTranslator.convertToChanges(
  966. this.project_id,
  967. updates
  968. ).map(change => change.toRaw())
  969. expect(changes).to.deep.equal([
  970. {
  971. authors: [],
  972. operations: [
  973. {
  974. pathname: 'main.tex',
  975. textOperation: [
  976. 3,
  977. {
  978. r: 3,
  979. tracking: {
  980. type: 'none',
  981. userId: this.user_id,
  982. ts: new Date(this.timestamp).toISOString(),
  983. },
  984. },
  985. 14,
  986. ],
  987. },
  988. ],
  989. v2Authors: [this.user_id],
  990. timestamp: this.timestamp,
  991. v2DocVersions: {
  992. '59bfd450e3028c4d40a1e9ab': {
  993. pathname: 'main.tex',
  994. v: 0,
  995. },
  996. },
  997. },
  998. ])
  999. })
  1000. it('handles tracked changes', function () {
  1001. const updates = [
  1002. {
  1003. update: {
  1004. doc: this.doc_id,
  1005. op: [
  1006. { i: 'inserted', p: 5 },
  1007. { d: 'deleted', p: 20 },
  1008. { i: 'rejected deletion', p: 30, trackedDeleteRejection: true },
  1009. { d: 'rejected insertion', p: 50, u: true },
  1010. ],
  1011. v: this.version,
  1012. meta: {
  1013. tc: 'tracked-change-id',
  1014. user_id: this.user_id,
  1015. ts: new Date(this.timestamp).getTime(),
  1016. pathname: '/main.tex',
  1017. doc_length: 70,
  1018. source: 'some-editor-id',
  1019. },
  1020. },
  1021. },
  1022. ]
  1023. const changes = this.UpdateTranslator.convertToChanges(
  1024. this.project_id,
  1025. updates
  1026. ).map(change => change.toRaw())
  1027. expect(changes).to.deep.equal([
  1028. {
  1029. authors: [],
  1030. operations: [
  1031. {
  1032. pathname: 'main.tex',
  1033. textOperation: [
  1034. 5,
  1035. {
  1036. i: 'inserted',
  1037. tracking: {
  1038. type: 'insert',
  1039. userId: this.user_id,
  1040. ts: new Date(this.timestamp).toISOString(),
  1041. },
  1042. },
  1043. 7,
  1044. {
  1045. r: 7,
  1046. tracking: {
  1047. type: 'delete',
  1048. userId: this.user_id,
  1049. ts: new Date(this.timestamp).toISOString(),
  1050. },
  1051. },
  1052. 3,
  1053. {
  1054. r: 17,
  1055. tracking: {
  1056. type: 'none',
  1057. userId: this.user_id,
  1058. ts: new Date(this.timestamp).toISOString(),
  1059. },
  1060. },
  1061. 3,
  1062. -18,
  1063. 10,
  1064. ],
  1065. },
  1066. ],
  1067. v2Authors: [this.user_id],
  1068. timestamp: this.timestamp,
  1069. v2DocVersions: {
  1070. '59bfd450e3028c4d40a1e9ab': {
  1071. pathname: 'main.tex',
  1072. v: 0,
  1073. },
  1074. },
  1075. },
  1076. ])
  1077. })
  1078. it('handles a delete over a mix of tracked inserts and tracked deletes', function () {
  1079. const updates = [
  1080. {
  1081. update: {
  1082. doc: this.doc_id,
  1083. op: [
  1084. {
  1085. d: 'abcdef',
  1086. p: 10,
  1087. trackedChanges: [
  1088. { type: 'insert', offset: 0, length: 3 },
  1089. { type: 'delete', offset: 2, length: 10 },
  1090. { type: 'insert', offset: 2, length: 2 },
  1091. ],
  1092. },
  1093. ],
  1094. v: this.version,
  1095. meta: {
  1096. tc: 'tracking-id',
  1097. user_id: this.user_id,
  1098. ts: new Date(this.timestamp).getTime(),
  1099. pathname: '/main.tex',
  1100. doc_length: 20,
  1101. history_doc_length: 30,
  1102. source: 'some-editor-id',
  1103. },
  1104. },
  1105. },
  1106. ]
  1107. const changes = this.UpdateTranslator.convertToChanges(
  1108. this.project_id,
  1109. updates
  1110. ).map(change => change.toRaw())
  1111. expect(changes).to.deep.equal([
  1112. {
  1113. authors: [],
  1114. operations: [
  1115. {
  1116. pathname: 'main.tex',
  1117. textOperation: [
  1118. 10,
  1119. -3,
  1120. 10,
  1121. -2,
  1122. {
  1123. r: 1,
  1124. tracking: {
  1125. type: 'delete',
  1126. userId: this.user_id,
  1127. ts: this.timestamp,
  1128. },
  1129. },
  1130. 4,
  1131. ],
  1132. },
  1133. ],
  1134. v2Authors: [this.user_id],
  1135. timestamp: this.timestamp,
  1136. v2DocVersions: {
  1137. '59bfd450e3028c4d40a1e9ab': {
  1138. pathname: 'main.tex',
  1139. v: 0,
  1140. },
  1141. },
  1142. },
  1143. ])
  1144. })
  1145. })
  1146. })
  1147. })