UpdateTranslatorTests.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  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. },
  760. {
  761. pathname: 'main.tex',
  762. commentId: 'comment-id-2',
  763. ranges: [{ pos: 7, length: 15 }],
  764. },
  765. {
  766. pathname: 'main.tex',
  767. textOperation: [10, 'baz', 10],
  768. },
  769. ],
  770. v2Authors: [this.user_id],
  771. timestamp: this.timestamp,
  772. v2DocVersions: {
  773. '59bfd450e3028c4d40a1e9ab': {
  774. pathname: 'main.tex',
  775. v: 0,
  776. },
  777. },
  778. },
  779. ])
  780. })
  781. it('handles insertions after the end of the document', function () {
  782. const updates = [
  783. {
  784. update: {
  785. doc: this.doc_id,
  786. op: [{ p: 3, i: '\\' }],
  787. v: this.version,
  788. meta: {
  789. user_id: this.user_id,
  790. ts: new Date(this.timestamp).getTime(),
  791. pathname: '/main.tex',
  792. doc_length: 2,
  793. },
  794. },
  795. },
  796. ]
  797. const changes = this.UpdateTranslator.convertToChanges(
  798. this.project_id,
  799. updates
  800. ).map(change => change.toRaw())
  801. expect(changes).to.deep.equal([
  802. {
  803. authors: [],
  804. operations: [
  805. {
  806. pathname: 'main.tex',
  807. textOperation: [2, '\\'],
  808. },
  809. ],
  810. v2Authors: [this.user_id],
  811. timestamp: this.timestamp,
  812. v2DocVersions: {
  813. '59bfd450e3028c4d40a1e9ab': {
  814. pathname: 'main.tex',
  815. v: 0,
  816. },
  817. },
  818. },
  819. ])
  820. })
  821. it('translates external source metadata into an origin', function () {
  822. const updates = [
  823. {
  824. update: {
  825. doc: this.doc_id,
  826. op: [{ p: 3, i: 'foo' }],
  827. v: this.version,
  828. meta: {
  829. user_id: this.user_id,
  830. ts: new Date(this.timestamp).getTime(),
  831. pathname: '/main.tex',
  832. doc_length: 20,
  833. type: 'external',
  834. source: 'dropbox',
  835. },
  836. },
  837. },
  838. ]
  839. const changes = this.UpdateTranslator.convertToChanges(
  840. this.project_id,
  841. updates
  842. ).map(change => change.toRaw())
  843. expect(changes).to.deep.equal([
  844. {
  845. authors: [],
  846. operations: [
  847. {
  848. pathname: 'main.tex',
  849. textOperation: [3, 'foo', 17],
  850. },
  851. ],
  852. v2Authors: [this.user_id],
  853. timestamp: this.timestamp,
  854. v2DocVersions: {
  855. '59bfd450e3028c4d40a1e9ab': {
  856. pathname: 'main.tex',
  857. v: 0,
  858. },
  859. },
  860. origin: { kind: 'dropbox' },
  861. },
  862. ])
  863. })
  864. it('errors on unexpected ops', function () {
  865. const updates = [
  866. {
  867. update: {
  868. doc: this.doc_id,
  869. op: [{ p: 5, z: 'bar' }],
  870. v: this.version,
  871. meta: {
  872. user_id: this.user_id,
  873. ts: new Date(this.timestamp).getTime(),
  874. pathname: '/main.tex',
  875. doc_length: 20,
  876. },
  877. },
  878. },
  879. ]
  880. expect(() => {
  881. this.UpdateTranslator.convertToChanges(this.project_id, updates)
  882. }).to.throw('unexpected op type')
  883. })
  884. })
  885. describe('text updates with history metadata', function () {
  886. it('handles deletes over tracked deletes', function () {
  887. const updates = [
  888. {
  889. update: {
  890. doc: this.doc_id,
  891. op: [
  892. { i: 'foo', p: 3, hpos: 5 },
  893. {
  894. d: 'quux',
  895. p: 10,
  896. hpos: 15,
  897. trackedChanges: [
  898. { type: 'delete', offset: 2, length: 3 },
  899. { type: 'delete', offset: 3, length: 1 },
  900. ],
  901. },
  902. { c: 'noteworthy', p: 8, t: 'comment-id', hpos: 11, hlen: 14 },
  903. ],
  904. v: this.version,
  905. meta: {
  906. user_id: this.user_id,
  907. ts: new Date(this.timestamp).getTime(),
  908. pathname: '/main.tex',
  909. doc_length: 20,
  910. history_doc_length: 30,
  911. source: 'some-editor-id',
  912. },
  913. },
  914. },
  915. ]
  916. const changes = this.UpdateTranslator.convertToChanges(
  917. this.project_id,
  918. updates
  919. ).map(change => change.toRaw())
  920. expect(changes).to.deep.equal([
  921. {
  922. authors: [],
  923. operations: [
  924. {
  925. pathname: 'main.tex',
  926. textOperation: [5, 'foo', 7, -2, 3, -1, 1, -1, 10],
  927. },
  928. {
  929. pathname: 'main.tex',
  930. commentId: 'comment-id',
  931. ranges: [{ pos: 11, length: 14 }],
  932. },
  933. ],
  934. v2Authors: [this.user_id],
  935. timestamp: this.timestamp,
  936. v2DocVersions: {
  937. '59bfd450e3028c4d40a1e9ab': {
  938. pathname: 'main.tex',
  939. v: 0,
  940. },
  941. },
  942. },
  943. ])
  944. })
  945. it('handles tracked delete rejections specially', function () {
  946. const updates = [
  947. {
  948. update: {
  949. doc: this.doc_id,
  950. op: [{ i: 'foo', p: 3, trackedDeleteRejection: true }],
  951. v: this.version,
  952. meta: {
  953. user_id: this.user_id,
  954. ts: new Date(this.timestamp).getTime(),
  955. pathname: '/main.tex',
  956. doc_length: 20,
  957. source: 'some-editor-id',
  958. },
  959. },
  960. },
  961. ]
  962. const changes = this.UpdateTranslator.convertToChanges(
  963. this.project_id,
  964. updates
  965. ).map(change => change.toRaw())
  966. expect(changes).to.deep.equal([
  967. {
  968. authors: [],
  969. operations: [
  970. {
  971. pathname: 'main.tex',
  972. textOperation: [
  973. 3,
  974. {
  975. r: 3,
  976. tracking: {
  977. type: 'none',
  978. userId: this.user_id,
  979. ts: new Date(this.timestamp).toISOString(),
  980. },
  981. },
  982. 14,
  983. ],
  984. },
  985. ],
  986. v2Authors: [this.user_id],
  987. timestamp: this.timestamp,
  988. v2DocVersions: {
  989. '59bfd450e3028c4d40a1e9ab': {
  990. pathname: 'main.tex',
  991. v: 0,
  992. },
  993. },
  994. },
  995. ])
  996. })
  997. it('handles tracked changes', function () {
  998. const updates = [
  999. {
  1000. update: {
  1001. doc: this.doc_id,
  1002. op: [
  1003. { i: 'inserted', p: 5 },
  1004. { d: 'deleted', p: 20 },
  1005. { i: 'rejected deletion', p: 30, trackedDeleteRejection: true },
  1006. { d: 'rejected insertion', p: 50, u: true },
  1007. ],
  1008. v: this.version,
  1009. meta: {
  1010. tc: 'tracked-change-id',
  1011. user_id: this.user_id,
  1012. ts: new Date(this.timestamp).getTime(),
  1013. pathname: '/main.tex',
  1014. doc_length: 70,
  1015. source: 'some-editor-id',
  1016. },
  1017. },
  1018. },
  1019. ]
  1020. const changes = this.UpdateTranslator.convertToChanges(
  1021. this.project_id,
  1022. updates
  1023. ).map(change => change.toRaw())
  1024. expect(changes).to.deep.equal([
  1025. {
  1026. authors: [],
  1027. operations: [
  1028. {
  1029. pathname: 'main.tex',
  1030. textOperation: [
  1031. 5,
  1032. {
  1033. i: 'inserted',
  1034. tracking: {
  1035. type: 'insert',
  1036. userId: this.user_id,
  1037. ts: new Date(this.timestamp).toISOString(),
  1038. },
  1039. },
  1040. 7,
  1041. {
  1042. r: 7,
  1043. tracking: {
  1044. type: 'delete',
  1045. userId: this.user_id,
  1046. ts: new Date(this.timestamp).toISOString(),
  1047. },
  1048. },
  1049. 3,
  1050. {
  1051. r: 17,
  1052. tracking: {
  1053. type: 'none',
  1054. userId: this.user_id,
  1055. ts: new Date(this.timestamp).toISOString(),
  1056. },
  1057. },
  1058. 3,
  1059. -18,
  1060. 10,
  1061. ],
  1062. },
  1063. ],
  1064. v2Authors: [this.user_id],
  1065. timestamp: this.timestamp,
  1066. v2DocVersions: {
  1067. '59bfd450e3028c4d40a1e9ab': {
  1068. pathname: 'main.tex',
  1069. v: 0,
  1070. },
  1071. },
  1072. },
  1073. ])
  1074. })
  1075. it('handles a delete over a mix of tracked inserts and tracked deletes', function () {
  1076. const updates = [
  1077. {
  1078. update: {
  1079. doc: this.doc_id,
  1080. op: [
  1081. {
  1082. d: 'abcdef',
  1083. p: 10,
  1084. trackedChanges: [
  1085. { type: 'insert', offset: 0, length: 3 },
  1086. { type: 'delete', offset: 2, length: 10 },
  1087. { type: 'insert', offset: 2, length: 2 },
  1088. ],
  1089. },
  1090. ],
  1091. v: this.version,
  1092. meta: {
  1093. tc: 'tracking-id',
  1094. user_id: this.user_id,
  1095. ts: new Date(this.timestamp).getTime(),
  1096. pathname: '/main.tex',
  1097. doc_length: 20,
  1098. history_doc_length: 30,
  1099. source: 'some-editor-id',
  1100. },
  1101. },
  1102. },
  1103. ]
  1104. const changes = this.UpdateTranslator.convertToChanges(
  1105. this.project_id,
  1106. updates
  1107. ).map(change => change.toRaw())
  1108. expect(changes).to.deep.equal([
  1109. {
  1110. authors: [],
  1111. operations: [
  1112. {
  1113. pathname: 'main.tex',
  1114. textOperation: [
  1115. 10,
  1116. -3,
  1117. 10,
  1118. -2,
  1119. {
  1120. r: 1,
  1121. tracking: {
  1122. type: 'delete',
  1123. userId: this.user_id,
  1124. ts: this.timestamp,
  1125. },
  1126. },
  1127. 4,
  1128. ],
  1129. },
  1130. ],
  1131. v2Authors: [this.user_id],
  1132. timestamp: this.timestamp,
  1133. v2DocVersions: {
  1134. '59bfd450e3028c4d40a1e9ab': {
  1135. pathname: 'main.tex',
  1136. v: 0,
  1137. },
  1138. },
  1139. },
  1140. ])
  1141. })
  1142. })
  1143. })
  1144. })