scan_op.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. // @ts-check
  2. const { expect } = require('chai')
  3. const {
  4. RetainOp,
  5. ScanOp,
  6. InsertOp,
  7. RemoveOp,
  8. } = require('../lib/operation/scan_op')
  9. const { UnprocessableError, ApplyError } = require('../lib/errors')
  10. const TrackingProps = require('../lib/file_data/tracking_props')
  11. describe('ScanOp', function () {
  12. describe('fromJSON', function () {
  13. it('constructs a RetainOp from object', function () {
  14. const op = ScanOp.fromJSON({ r: 1 })
  15. expect(op).to.be.instanceOf(RetainOp)
  16. expect(/** @type {RetainOp} */ (op).length).to.equal(1)
  17. })
  18. it('constructs a RetainOp from number', function () {
  19. const op = ScanOp.fromJSON(2)
  20. expect(op).to.be.instanceOf(RetainOp)
  21. expect(/** @type {RetainOp} */ (op).length).to.equal(2)
  22. })
  23. it('constructs an InsertOp from string', function () {
  24. const op = ScanOp.fromJSON('abc')
  25. expect(op).to.be.instanceOf(InsertOp)
  26. expect(/** @type {InsertOp} */ (op).insertion).to.equal('abc')
  27. })
  28. it('constructs an InsertOp from object', function () {
  29. const op = ScanOp.fromJSON({ i: 'abc' })
  30. expect(op).to.be.instanceOf(InsertOp)
  31. expect(/** @type {InsertOp} */ (op).insertion).to.equal('abc')
  32. })
  33. it('constructs a RemoveOp from number', function () {
  34. const op = ScanOp.fromJSON(-2)
  35. expect(op).to.be.instanceOf(RemoveOp)
  36. expect(/** @type {RemoveOp} */ (op).length).to.equal(2)
  37. })
  38. it('throws an error for invalid input', function () {
  39. expect(() => ScanOp.fromJSON(/** @type {any} */ ({}))).to.throw(
  40. UnprocessableError
  41. )
  42. })
  43. it('throws an error for zero', function () {
  44. expect(() => ScanOp.fromJSON(0)).to.throw(UnprocessableError)
  45. })
  46. })
  47. })
  48. describe('RetainOp', function () {
  49. it('is equal to another RetainOp with the same length', function () {
  50. const op1 = new RetainOp(1)
  51. const op2 = new RetainOp(1)
  52. expect(op1.equals(op2)).to.be.true
  53. })
  54. it('is not equal to another RetainOp with a different length', function () {
  55. const op1 = new RetainOp(1)
  56. const op2 = new RetainOp(2)
  57. expect(op1.equals(op2)).to.be.false
  58. })
  59. it('is not equal to another RetainOp with no tracking info', function () {
  60. const op1 = new RetainOp(
  61. 4,
  62. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  63. )
  64. const op2 = new RetainOp(4)
  65. expect(op1.equals(op2)).to.be.false
  66. })
  67. it('is not equal to another RetainOp with different tracking info', function () {
  68. const op1 = new RetainOp(
  69. 4,
  70. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  71. )
  72. const op2 = new RetainOp(
  73. 4,
  74. new TrackingProps('insert', 'user2', new Date('2024-01-01T00:00:00.000Z'))
  75. )
  76. expect(op1.equals(op2)).to.be.false
  77. })
  78. it('is not equal to an InsertOp', function () {
  79. const op1 = new RetainOp(1)
  80. const op2 = new InsertOp('a')
  81. expect(op1.equals(op2)).to.be.false
  82. })
  83. it('is not equal to a RemoveOp', function () {
  84. const op1 = new RetainOp(1)
  85. const op2 = new RemoveOp(1)
  86. expect(op1.equals(op2)).to.be.false
  87. })
  88. it('can merge with another RetainOp', function () {
  89. const op1 = new RetainOp(1)
  90. const op2 = new RetainOp(2)
  91. expect(op1.canMergeWith(op2)).to.be.true
  92. op1.mergeWith(op2)
  93. expect(op1.equals(new RetainOp(3))).to.be.true
  94. })
  95. it('cannot merge with another RetainOp if the tracking user is different', function () {
  96. const op1 = new RetainOp(
  97. 4,
  98. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  99. )
  100. const op2 = new RetainOp(
  101. 4,
  102. new TrackingProps('insert', 'user2', new Date('2024-01-01T00:00:00.000Z'))
  103. )
  104. expect(op1.canMergeWith(op2)).to.be.false
  105. expect(() => op1.mergeWith(op2)).to.throw(Error)
  106. })
  107. it('can merge with another RetainOp if the tracking user is the same', function () {
  108. const op1 = new RetainOp(
  109. 4,
  110. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  111. )
  112. const op2 = new RetainOp(
  113. 4,
  114. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:01.000Z'))
  115. )
  116. op1.mergeWith(op2)
  117. expect(
  118. op1.equals(
  119. new RetainOp(
  120. 8,
  121. new TrackingProps(
  122. 'insert',
  123. 'user1',
  124. new Date('2024-01-01T00:00:00.000Z')
  125. )
  126. )
  127. )
  128. ).to.be.true
  129. })
  130. it('cannot merge with an InsertOp', function () {
  131. const op1 = new RetainOp(1)
  132. const op2 = new InsertOp('a')
  133. expect(op1.canMergeWith(op2)).to.be.false
  134. expect(() => op1.mergeWith(op2)).to.throw(Error)
  135. })
  136. it('cannot merge with a RemoveOp', function () {
  137. const op1 = new RetainOp(1)
  138. const op2 = new RemoveOp(1)
  139. expect(op1.canMergeWith(op2)).to.be.false
  140. expect(() => op1.mergeWith(op2)).to.throw(Error)
  141. })
  142. it('can be converted to JSON', function () {
  143. const op = new RetainOp(3)
  144. expect(op.toJSON()).to.equal(3)
  145. })
  146. it('adds to the length and cursor when applied to length', function () {
  147. const op = new RetainOp(3)
  148. const { length, inputCursor } = op.applyToLength({
  149. length: 10,
  150. inputCursor: 10,
  151. inputLength: 30,
  152. })
  153. expect(length).to.equal(13)
  154. expect(inputCursor).to.equal(13)
  155. })
  156. })
  157. describe('InsertOp', function () {
  158. it('is equal to another InsertOp with the same insertion', function () {
  159. const op1 = new InsertOp('a')
  160. const op2 = new InsertOp('a')
  161. expect(op1.equals(op2)).to.be.true
  162. })
  163. it('is not equal to another InsertOp with a different insertion', function () {
  164. const op1 = new InsertOp('a')
  165. const op2 = new InsertOp('b')
  166. expect(op1.equals(op2)).to.be.false
  167. })
  168. it('is not equal to another InsertOp with no tracking info', function () {
  169. const op1 = new InsertOp(
  170. 'a',
  171. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  172. )
  173. const op2 = new InsertOp('a')
  174. expect(op1.equals(op2)).to.be.false
  175. })
  176. it('is not equal to another InsertOp with different tracking info', function () {
  177. const op1 = new InsertOp(
  178. 'a',
  179. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  180. )
  181. const op2 = new InsertOp(
  182. 'a',
  183. new TrackingProps('insert', 'user2', new Date('2024-01-01T00:00:00.000Z'))
  184. )
  185. expect(op1.equals(op2)).to.be.false
  186. })
  187. it('is not equal to another InsertOp with no comment ids', function () {
  188. const op1 = new InsertOp('a', undefined, ['1'])
  189. const op2 = new InsertOp('a')
  190. expect(op1.equals(op2)).to.be.false
  191. })
  192. it('is not equal to another InsertOp with tracking info', function () {
  193. const op1 = new InsertOp('a', undefined)
  194. const op2 = new InsertOp(
  195. 'a',
  196. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  197. )
  198. expect(op1.equals(op2)).to.be.false
  199. })
  200. it('is not equal to another InsertOp with comment ids', function () {
  201. const op1 = new InsertOp('a')
  202. const op2 = new InsertOp('a', undefined, ['1'])
  203. expect(op1.equals(op2)).to.be.false
  204. })
  205. it('is not equal to another InsertOp with different comment ids', function () {
  206. const op1 = new InsertOp('a', undefined, ['1'])
  207. const op2 = new InsertOp('a', undefined, ['2'])
  208. expect(op1.equals(op2)).to.be.false
  209. })
  210. it('is not equal to another InsertOp with overlapping comment ids', function () {
  211. const op1 = new InsertOp('a', undefined, ['1'])
  212. const op2 = new InsertOp('a', undefined, ['2', '1'])
  213. expect(op1.equals(op2)).to.be.false
  214. })
  215. it('is not equal to a RetainOp', function () {
  216. const op1 = new InsertOp('a')
  217. const op2 = new RetainOp(1)
  218. expect(op1.equals(op2)).to.be.false
  219. })
  220. it('is not equal to a RemoveOp', function () {
  221. const op1 = new InsertOp('a')
  222. const op2 = new RemoveOp(1)
  223. expect(op1.equals(op2)).to.be.false
  224. })
  225. it('can merge with another InsertOp', function () {
  226. const op1 = new InsertOp('a')
  227. const op2 = new InsertOp('b')
  228. expect(op1.canMergeWith(op2)).to.be.true
  229. op1.mergeWith(op2)
  230. expect(op1.equals(new InsertOp('ab'))).to.be.true
  231. })
  232. it('cannot merge with another InsertOp if comment id info is different', function () {
  233. const op1 = new InsertOp('a', undefined, ['1'])
  234. const op2 = new InsertOp('b', undefined, ['1', '2'])
  235. expect(op1.canMergeWith(op2)).to.be.false
  236. expect(() => op1.mergeWith(op2)).to.throw(Error)
  237. })
  238. it('cannot merge with another InsertOp if comment id info is different while tracking info matches', function () {
  239. const op1 = new InsertOp(
  240. 'a',
  241. new TrackingProps(
  242. 'insert',
  243. 'user1',
  244. new Date('2024-01-01T00:00:00.000Z')
  245. ),
  246. ['1', '2']
  247. )
  248. const op2 = new InsertOp(
  249. 'b',
  250. new TrackingProps(
  251. 'insert',
  252. 'user1',
  253. new Date('2024-01-01T00:00:00.000Z')
  254. ),
  255. ['3']
  256. )
  257. expect(op1.canMergeWith(op2)).to.be.false
  258. expect(() => op1.mergeWith(op2)).to.throw(Error)
  259. })
  260. it('cannot merge with another InsertOp if comment id is present in other and tracking info matches', function () {
  261. const op1 = new InsertOp(
  262. 'a',
  263. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  264. )
  265. const op2 = new InsertOp(
  266. 'b',
  267. new TrackingProps(
  268. 'insert',
  269. 'user1',
  270. new Date('2024-01-01T00:00:00.000Z')
  271. ),
  272. ['1']
  273. )
  274. expect(op1.canMergeWith(op2)).to.be.false
  275. expect(() => op1.mergeWith(op2)).to.throw(Error)
  276. })
  277. it('cannot merge with another InsertOp if tracking user is different', function () {
  278. const op1 = new InsertOp(
  279. 'a',
  280. new TrackingProps('insert', 'user1', new Date('2024-01-01T00:00:00.000Z'))
  281. )
  282. const op2 = new InsertOp(
  283. 'b',
  284. new TrackingProps('insert', 'user2', new Date('2024-01-01T00:00:00.000Z'))
  285. )
  286. expect(op1.canMergeWith(op2)).to.be.false
  287. expect(() => op1.mergeWith(op2)).to.throw(Error)
  288. })
  289. it('can merge with another InsertOp if tracking user and comment info is the same', function () {
  290. const op1 = new InsertOp(
  291. 'a',
  292. new TrackingProps(
  293. 'insert',
  294. 'user1',
  295. new Date('2024-01-01T00:00:00.000Z')
  296. ),
  297. ['1', '2']
  298. )
  299. const op2 = new InsertOp(
  300. 'b',
  301. new TrackingProps(
  302. 'insert',
  303. 'user1',
  304. new Date('2024-01-01T00:00:01.000Z')
  305. ),
  306. ['1', '2']
  307. )
  308. expect(op1.canMergeWith(op2)).to.be.true
  309. op1.mergeWith(op2)
  310. expect(
  311. op1.equals(
  312. new InsertOp(
  313. 'ab',
  314. new TrackingProps(
  315. 'insert',
  316. 'user1',
  317. new Date('2024-01-01T00:00:00.000Z')
  318. ),
  319. ['1', '2']
  320. )
  321. )
  322. ).to.be.true
  323. })
  324. it('cannot merge with a RetainOp', function () {
  325. const op1 = new InsertOp('a')
  326. const op2 = new RetainOp(1)
  327. expect(op1.canMergeWith(op2)).to.be.false
  328. expect(() => op1.mergeWith(op2)).to.throw(Error)
  329. })
  330. it('cannot merge with a RemoveOp', function () {
  331. const op1 = new InsertOp('a')
  332. const op2 = new RemoveOp(1)
  333. expect(op1.canMergeWith(op2)).to.be.false
  334. expect(() => op1.mergeWith(op2)).to.throw(Error)
  335. })
  336. it('can be converted to JSON', function () {
  337. const op = new InsertOp('a')
  338. expect(op.toJSON()).to.equal('a')
  339. })
  340. it('adds to the length when applied to length', function () {
  341. const op = new InsertOp('abc')
  342. const { length, inputCursor } = op.applyToLength({
  343. length: 10,
  344. inputCursor: 20,
  345. inputLength: 40,
  346. })
  347. expect(length).to.equal(13)
  348. expect(inputCursor).to.equal(20)
  349. })
  350. it('can apply a retain of the rest of the input', function () {
  351. const op = new RetainOp(10)
  352. const { length, inputCursor } = op.applyToLength({
  353. length: 10,
  354. inputCursor: 5,
  355. inputLength: 15,
  356. })
  357. expect(length).to.equal(20)
  358. expect(inputCursor).to.equal(15)
  359. })
  360. it('cannot apply to length if the input cursor is at the end', function () {
  361. const op = new RetainOp(10)
  362. expect(() =>
  363. op.applyToLength({
  364. length: 10,
  365. inputCursor: 10,
  366. inputLength: 10,
  367. })
  368. ).to.throw(ApplyError)
  369. })
  370. })
  371. describe('RemoveOp', function () {
  372. it('is equal to another RemoveOp with the same length', function () {
  373. const op1 = new RemoveOp(1)
  374. const op2 = new RemoveOp(1)
  375. expect(op1.equals(op2)).to.be.true
  376. })
  377. it('is not equal to another RemoveOp with a different length', function () {
  378. const op1 = new RemoveOp(1)
  379. const op2 = new RemoveOp(2)
  380. expect(op1.equals(op2)).to.be.false
  381. })
  382. it('is not equal to a RetainOp', function () {
  383. const op1 = new RemoveOp(1)
  384. const op2 = new RetainOp(1)
  385. expect(op1.equals(op2)).to.be.false
  386. })
  387. it('is not equal to an InsertOp', function () {
  388. const op1 = new RemoveOp(1)
  389. const op2 = new InsertOp('a')
  390. expect(op1.equals(op2)).to.be.false
  391. })
  392. it('can merge with another RemoveOp', function () {
  393. const op1 = new RemoveOp(1)
  394. const op2 = new RemoveOp(2)
  395. expect(op1.canMergeWith(op2)).to.be.true
  396. op1.mergeWith(op2)
  397. expect(op1.equals(new RemoveOp(3))).to.be.true
  398. })
  399. it('cannot merge with a RetainOp', function () {
  400. const op1 = new RemoveOp(1)
  401. const op2 = new RetainOp(1)
  402. expect(op1.canMergeWith(op2)).to.be.false
  403. expect(() => op1.mergeWith(op2)).to.throw(Error)
  404. })
  405. it('cannot merge with an InsertOp', function () {
  406. const op1 = new RemoveOp(1)
  407. const op2 = new InsertOp('a')
  408. expect(op1.canMergeWith(op2)).to.be.false
  409. expect(() => op1.mergeWith(op2)).to.throw(Error)
  410. })
  411. it('can be converted to JSON', function () {
  412. const op = new RemoveOp(3)
  413. expect(op.toJSON()).to.equal(-3)
  414. })
  415. it('adds to the input cursor when applied to length', function () {
  416. const op = new RemoveOp(3)
  417. const { length, inputCursor } = op.applyToLength({
  418. length: 10,
  419. inputCursor: 10,
  420. inputLength: 30,
  421. })
  422. expect(length).to.equal(10)
  423. expect(inputCursor).to.equal(13)
  424. })
  425. })