DiffGeneratorTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*
  2. * decaffeinate suggestions:
  3. * DS101: Remove unnecessary use of Array.from
  4. * DS102: Remove unnecessary code created because of implicit returns
  5. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  6. */
  7. const sinon = require('sinon');
  8. const chai = require('chai');
  9. const should = chai.should();
  10. const { expect } = chai;
  11. const modulePath = "../../../../app/js/DiffGenerator.js";
  12. const SandboxedModule = require('sandboxed-module');
  13. describe("DiffGenerator", function() {
  14. beforeEach(function() {
  15. this.DiffGenerator = SandboxedModule.require(modulePath, { requires: {
  16. "logger-sharelatex": { warn: sinon.stub() }
  17. }
  18. });
  19. this.ts = Date.now();
  20. this.user_id = "mock-user-id";
  21. this.user_id_2 = "mock-user-id-2";
  22. return this.meta = {
  23. start_ts: this.ts, end_ts: this.ts, user_id: this.user_id
  24. };});
  25. describe("rewindOp", function() {
  26. describe("rewinding an insert", () =>
  27. it("should undo the insert", function() {
  28. const content = "hello world";
  29. const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, i: "wo" });
  30. return rewoundContent.should.equal("hello rld");
  31. })
  32. );
  33. describe("rewinding a delete", () =>
  34. it("should undo the delete", function() {
  35. const content = "hello rld";
  36. const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 6, d: "wo" });
  37. return rewoundContent.should.equal("hello world");
  38. })
  39. );
  40. describe("with an inconsistent update", () =>
  41. it("should throw an error", function() {
  42. const content = "hello world";
  43. return expect( () => {
  44. return this.DiffGenerator.rewindOp(content, { p: 6, i: "foo" });
  45. }).to.throw(this.DiffGenerator.ConsistencyError);
  46. })
  47. );
  48. return describe("with an update which is beyond the length of the content", () =>
  49. it("should undo the insert as if it were at the end of the content", function() {
  50. const content = "foobar";
  51. const rewoundContent = this.DiffGenerator.rewindOp(content, { p: 4, i: "bar" });
  52. return rewoundContent.should.equal("foo");
  53. })
  54. );
  55. });
  56. describe("rewindUpdate", () =>
  57. it("should rewind ops in reverse", function() {
  58. const content = "aaabbbccc";
  59. const update =
  60. {op: [{ p: 3, i: "bbb" }, { p: 6, i: "ccc" }]};
  61. const rewoundContent = this.DiffGenerator.rewindUpdate(content, update);
  62. return rewoundContent.should.equal("aaa");
  63. })
  64. );
  65. describe("rewindUpdates", () =>
  66. it("should rewind updates in reverse", function() {
  67. const content = "aaabbbccc";
  68. const updates = [
  69. { op: [{ p: 3, i: "bbb" }] },
  70. { op: [{ p: 6, i: "ccc" }] }
  71. ];
  72. const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates);
  73. return rewoundContent.should.equal("aaa");
  74. })
  75. );
  76. describe("buildDiff", function() {
  77. beforeEach(function() {
  78. this.diff = [ {u: "mock-diff"} ];
  79. this.content = "Hello world";
  80. this.updates = [
  81. { i: "mock-update-1" },
  82. { i: "mock-update-2" },
  83. { i: "mock-update-3" }
  84. ];
  85. this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff);
  86. this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff);
  87. return this.result = this.DiffGenerator.buildDiff(this.content, this.updates);
  88. });
  89. it("should return the diff", function() {
  90. return this.result.should.deep.equal(this.diff);
  91. });
  92. it("should build the content into an initial diff", function() {
  93. return this.DiffGenerator.applyUpdateToDiff
  94. .calledWith([{
  95. u: this.content
  96. }], this.updates[0])
  97. .should.equal(true);
  98. });
  99. it("should apply each update", function() {
  100. return Array.from(this.updates).map((update) =>
  101. this.DiffGenerator.applyUpdateToDiff
  102. .calledWith(sinon.match.any, update)
  103. .should.equal(true));
  104. });
  105. return it("should compress the diff", function() {
  106. return this.DiffGenerator.compressDiff
  107. .calledWith(this.diff)
  108. .should.equal(true);
  109. });
  110. });
  111. describe("compressDiff", function() {
  112. describe("with adjacent inserts with the same user_id", () =>
  113. it("should create one update with combined meta data and min/max timestamps", function() {
  114. const diff = this.DiffGenerator.compressDiff([
  115. { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
  116. { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }}
  117. ]);
  118. return expect(diff).to.deep.equal([
  119. { i: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }}
  120. ]);
  121. })
  122. );
  123. describe("with adjacent inserts with different user_ids", () =>
  124. it("should leave the inserts unchanged", function() {
  125. const input = [
  126. { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
  127. { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }}
  128. ];
  129. const output = this.DiffGenerator.compressDiff(input);
  130. return expect(output).to.deep.equal(input);
  131. })
  132. );
  133. describe("with adjacent deletes with the same user_id", () =>
  134. it("should create one update with combined meta data and min/max timestamps", function() {
  135. const diff = this.DiffGenerator.compressDiff([
  136. { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
  137. { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }}
  138. ]);
  139. return expect(diff).to.deep.equal([
  140. { d: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }}
  141. ]);
  142. })
  143. );
  144. return describe("with adjacent deletes with different user_ids", () =>
  145. it("should leave the deletes unchanged", function() {
  146. const input = [
  147. { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }},
  148. { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }}
  149. ];
  150. const output = this.DiffGenerator.compressDiff(input);
  151. return expect(output).to.deep.equal(input);
  152. })
  153. );
  154. });
  155. return describe("applyUpdateToDiff", function() {
  156. describe("an insert", function() {
  157. it("should insert into the middle of (u)nchanged text", function() {
  158. const diff = this.DiffGenerator.applyUpdateToDiff(
  159. [ { u: "foobar" } ],
  160. { op: [{ p: 3, i: "baz" }], meta: this.meta }
  161. );
  162. return expect(diff).to.deep.equal([
  163. { u: "foo" },
  164. { i: "baz", meta: this.meta },
  165. { u: "bar" }
  166. ]);
  167. });
  168. it("should insert into the start of (u)changed text", function() {
  169. const diff = this.DiffGenerator.applyUpdateToDiff(
  170. [ { u: "foobar" } ],
  171. { op: [{ p: 0, i: "baz" }], meta: this.meta }
  172. );
  173. return expect(diff).to.deep.equal([
  174. { i: "baz", meta: this.meta },
  175. { u: "foobar" }
  176. ]);
  177. });
  178. it("should insert into the end of (u)changed text", function() {
  179. const diff = this.DiffGenerator.applyUpdateToDiff(
  180. [ { u: "foobar" } ],
  181. { op: [{ p: 6, i: "baz" }], meta: this.meta }
  182. );
  183. return expect(diff).to.deep.equal([
  184. { u: "foobar" },
  185. { i: "baz", meta: this.meta }
  186. ]);
  187. });
  188. it("should insert into the middle of (i)inserted text", function() {
  189. const diff = this.DiffGenerator.applyUpdateToDiff(
  190. [ { i: "foobar", meta: this.meta } ],
  191. { op: [{ p: 3, i: "baz" }], meta: this.meta }
  192. );
  193. return expect(diff).to.deep.equal([
  194. { i: "foo", meta: this.meta },
  195. { i: "baz", meta: this.meta },
  196. { i: "bar", meta: this.meta }
  197. ]);
  198. });
  199. return it("should not count deletes in the running length total", function() {
  200. const diff = this.DiffGenerator.applyUpdateToDiff(
  201. [
  202. { d: "deleted", meta: this.meta },
  203. { u: "foobar" }
  204. ],
  205. { op: [{ p: 3, i: "baz" }], meta: this.meta }
  206. );
  207. return expect(diff).to.deep.equal([
  208. { d: "deleted", meta: this.meta },
  209. { u: "foo" },
  210. { i: "baz", meta: this.meta },
  211. { u: "bar" }
  212. ]);
  213. });
  214. });
  215. return describe("a delete", function() {
  216. describe("deleting unchanged text", function() {
  217. it("should delete from the middle of (u)nchanged text", function() {
  218. const diff = this.DiffGenerator.applyUpdateToDiff(
  219. [ { u: "foobazbar" } ],
  220. { op: [{ p: 3, d: "baz" }], meta: this.meta }
  221. );
  222. return expect(diff).to.deep.equal([
  223. { u: "foo" },
  224. { d: "baz", meta: this.meta },
  225. { u: "bar" }
  226. ]);
  227. });
  228. it("should delete from the start of (u)nchanged text", function() {
  229. const diff = this.DiffGenerator.applyUpdateToDiff(
  230. [ { u: "foobazbar" } ],
  231. { op: [{ p: 0, d: "foo" }], meta: this.meta }
  232. );
  233. return expect(diff).to.deep.equal([
  234. { d: "foo", meta: this.meta },
  235. { u: "bazbar" }
  236. ]);
  237. });
  238. it("should delete from the end of (u)nchanged text", function() {
  239. const diff = this.DiffGenerator.applyUpdateToDiff(
  240. [ { u: "foobazbar" } ],
  241. { op: [{ p: 6, d: "bar" }], meta: this.meta }
  242. );
  243. return expect(diff).to.deep.equal([
  244. { u: "foobaz" },
  245. { d: "bar", meta: this.meta }
  246. ]);
  247. });
  248. return it("should delete across multiple (u)changed text parts", function() {
  249. const diff = this.DiffGenerator.applyUpdateToDiff(
  250. [ { u: "foo" }, { u: "baz" }, { u: "bar" } ],
  251. { op: [{ p: 2, d: "obazb" }], meta: this.meta }
  252. );
  253. return expect(diff).to.deep.equal([
  254. { u: "fo" },
  255. { d: "o", meta: this.meta },
  256. { d: "baz", meta: this.meta },
  257. { d: "b", meta: this.meta },
  258. { u: "ar" }
  259. ]);
  260. });
  261. });
  262. describe("deleting inserts", function() {
  263. it("should delete from the middle of (i)nserted text", function() {
  264. const diff = this.DiffGenerator.applyUpdateToDiff(
  265. [ { i: "foobazbar", meta: this.meta } ],
  266. { op: [{ p: 3, d: "baz" }], meta: this.meta }
  267. );
  268. return expect(diff).to.deep.equal([
  269. { i: "foo", meta: this.meta },
  270. { i: "bar", meta: this.meta }
  271. ]);
  272. });
  273. it("should delete from the start of (u)nchanged text", function() {
  274. const diff = this.DiffGenerator.applyUpdateToDiff(
  275. [ { i: "foobazbar", meta: this.meta } ],
  276. { op: [{ p: 0, d: "foo" }], meta: this.meta }
  277. );
  278. return expect(diff).to.deep.equal([
  279. { i: "bazbar", meta: this.meta }
  280. ]);
  281. });
  282. it("should delete from the end of (u)nchanged text", function() {
  283. const diff = this.DiffGenerator.applyUpdateToDiff(
  284. [ { i: "foobazbar", meta: this.meta } ],
  285. { op: [{ p: 6, d: "bar" }], meta: this.meta }
  286. );
  287. return expect(diff).to.deep.equal([
  288. { i: "foobaz", meta: this.meta }
  289. ]);
  290. });
  291. return it("should delete across multiple (u)changed and (i)nserted text parts", function() {
  292. const diff = this.DiffGenerator.applyUpdateToDiff(
  293. [ { u: "foo" }, { i: "baz", meta: this.meta }, { u: "bar" } ],
  294. { op: [{ p: 2, d: "obazb" }], meta: this.meta }
  295. );
  296. return expect(diff).to.deep.equal([
  297. { u: "fo" },
  298. { d: "o", meta: this.meta },
  299. { d: "b", meta: this.meta },
  300. { u: "ar" }
  301. ]);
  302. });
  303. });
  304. describe("deleting over existing deletes", () =>
  305. it("should delete across multiple (u)changed and (d)deleted text parts", function() {
  306. const diff = this.DiffGenerator.applyUpdateToDiff(
  307. [ { u: "foo" }, { d: "baz", meta: this.meta }, { u: "bar" } ],
  308. { op: [{ p: 2, d: "ob" }], meta: this.meta }
  309. );
  310. return expect(diff).to.deep.equal([
  311. { u: "fo" },
  312. { d: "o", meta: this.meta },
  313. { d: "baz", meta: this.meta },
  314. { d: "b", meta: this.meta },
  315. { u: "ar" }
  316. ]);
  317. })
  318. );
  319. describe("deleting when the text doesn't match", function() {
  320. it("should throw an error when deleting from the middle of (u)nchanged text", function() {
  321. return expect(
  322. () => this.DiffGenerator.applyUpdateToDiff(
  323. [ { u: "foobazbar" } ],
  324. { op: [{ p: 3, d: "xxx" }], meta: this.meta }
  325. )
  326. ).to.throw(this.DiffGenerator.ConsistencyError);
  327. });
  328. it("should throw an error when deleting from the start of (u)nchanged text", function() {
  329. return expect(
  330. () => this.DiffGenerator.applyUpdateToDiff(
  331. [ { u: "foobazbar" } ],
  332. { op: [{ p: 0, d: "xxx" }], meta: this.meta }
  333. )
  334. ).to.throw(this.DiffGenerator.ConsistencyError);
  335. });
  336. return it("should throw an error when deleting from the end of (u)nchanged text", function() {
  337. return expect(
  338. () => this.DiffGenerator.applyUpdateToDiff(
  339. [ { u: "foobazbar" } ],
  340. { op: [{ p: 6, d: "xxx" }] , meta: this.meta }
  341. )
  342. ).to.throw(this.DiffGenerator.ConsistencyError);
  343. });
  344. });
  345. describe("when the last update in the existing diff is a delete", () =>
  346. it("should insert the new update before the delete", function() {
  347. const diff = this.DiffGenerator.applyUpdateToDiff(
  348. [ { u: "foo" }, { d: "bar", meta: this.meta } ],
  349. { op: [{ p: 3, i: "baz" }], meta: this.meta }
  350. );
  351. return expect(diff).to.deep.equal([
  352. { u: "foo" },
  353. { i: "baz", meta: this.meta },
  354. { d: "bar", meta: this.meta }
  355. ]);
  356. })
  357. );
  358. return describe("when the only update in the existing diff is a delete", () =>
  359. it("should insert the new update after the delete", function() {
  360. const diff = this.DiffGenerator.applyUpdateToDiff(
  361. [ { d: "bar", meta: this.meta } ],
  362. { op: [{ p: 0, i: "baz" }], meta: this.meta }
  363. );
  364. return expect(diff).to.deep.equal([
  365. { d: "bar", meta: this.meta },
  366. { i: "baz", meta: this.meta }
  367. ]);
  368. })
  369. );
  370. });
  371. });
  372. });