delta.test.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  2. import type { ObservedAppState } from "@excalidraw/excalidraw/types";
  3. import type { LinearElementEditor } from "@excalidraw/element";
  4. import type { SceneElementsMap } from "@excalidraw/element/types";
  5. import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
  6. describe("ElementsDelta", () => {
  7. describe("elements delta calculation", () => {
  8. it("should not throw when element gets removed but was already deleted", () => {
  9. const element = API.createElement({
  10. type: "rectangle",
  11. x: 100,
  12. y: 100,
  13. isDeleted: true,
  14. });
  15. const prevElements = new Map([[element.id, element]]);
  16. const nextElements = new Map();
  17. expect(() =>
  18. ElementsDelta.calculate(prevElements, nextElements),
  19. ).not.toThrow();
  20. });
  21. it("should not throw when adding element as already deleted", () => {
  22. const element = API.createElement({
  23. type: "rectangle",
  24. x: 100,
  25. y: 100,
  26. isDeleted: true,
  27. });
  28. const prevElements = new Map();
  29. const nextElements = new Map([[element.id, element]]);
  30. expect(() =>
  31. ElementsDelta.calculate(prevElements, nextElements),
  32. ).not.toThrow();
  33. });
  34. it("should create updated delta even when there is only version and versionNonce change", () => {
  35. const baseElement = API.createElement({
  36. type: "rectangle",
  37. x: 100,
  38. y: 100,
  39. strokeColor: "#000000",
  40. backgroundColor: "#ffffff",
  41. });
  42. const modifiedElement = {
  43. ...baseElement,
  44. version: baseElement.version + 1,
  45. versionNonce: baseElement.versionNonce + 1,
  46. };
  47. // Create maps for the delta calculation
  48. const prevElements = new Map([[baseElement.id, baseElement]]);
  49. const nextElements = new Map([[modifiedElement.id, modifiedElement]]);
  50. // Calculate the delta
  51. const delta = ElementsDelta.calculate(
  52. prevElements as SceneElementsMap,
  53. nextElements as SceneElementsMap,
  54. );
  55. expect(delta).toEqual(
  56. ElementsDelta.create(
  57. {},
  58. {},
  59. {
  60. [baseElement.id]: Delta.create(
  61. {
  62. version: baseElement.version,
  63. versionNonce: baseElement.versionNonce,
  64. },
  65. {
  66. version: baseElement.version + 1,
  67. versionNonce: baseElement.versionNonce + 1,
  68. },
  69. ),
  70. },
  71. ),
  72. );
  73. });
  74. });
  75. describe("squash", () => {
  76. it("should not squash when second delta is empty", () => {
  77. const updatedDelta = Delta.create(
  78. { x: 100, version: 1, versionNonce: 1 },
  79. { x: 200, version: 2, versionNonce: 2 },
  80. );
  81. const elementsDelta1 = ElementsDelta.create(
  82. {},
  83. {},
  84. { id1: updatedDelta },
  85. );
  86. const elementsDelta2 = ElementsDelta.empty();
  87. const elementsDelta = elementsDelta1.squash(elementsDelta2);
  88. expect(elementsDelta.isEmpty()).toBeFalsy();
  89. expect(elementsDelta).toBe(elementsDelta1);
  90. expect(elementsDelta.updated.id1).toBe(updatedDelta);
  91. });
  92. it("should squash mutually exclusive delta types", () => {
  93. const addedDelta = Delta.create(
  94. { x: 100, version: 1, versionNonce: 1, isDeleted: true },
  95. { x: 200, version: 2, versionNonce: 2, isDeleted: false },
  96. );
  97. const removedDelta = Delta.create(
  98. { x: 100, version: 1, versionNonce: 1, isDeleted: false },
  99. { x: 200, version: 2, versionNonce: 2, isDeleted: true },
  100. );
  101. const updatedDelta = Delta.create(
  102. { x: 100, version: 1, versionNonce: 1 },
  103. { x: 200, version: 2, versionNonce: 2 },
  104. );
  105. const elementsDelta1 = ElementsDelta.create(
  106. { id1: addedDelta },
  107. { id2: removedDelta },
  108. {},
  109. );
  110. const elementsDelta2 = ElementsDelta.create(
  111. {},
  112. {},
  113. { id3: updatedDelta },
  114. );
  115. const elementsDelta = elementsDelta1.squash(elementsDelta2);
  116. expect(elementsDelta.isEmpty()).toBeFalsy();
  117. expect(elementsDelta).toBe(elementsDelta1);
  118. expect(elementsDelta.added.id1).toBe(addedDelta);
  119. expect(elementsDelta.removed.id2).toBe(removedDelta);
  120. expect(elementsDelta.updated.id3).toBe(updatedDelta);
  121. });
  122. it("should squash the same delta types", () => {
  123. const elementsDelta1 = ElementsDelta.create(
  124. {
  125. id1: Delta.create(
  126. { x: 100, version: 1, versionNonce: 1, isDeleted: true },
  127. { x: 200, version: 2, versionNonce: 2, isDeleted: false },
  128. ),
  129. },
  130. {
  131. id2: Delta.create(
  132. { x: 100, version: 1, versionNonce: 1, isDeleted: false },
  133. { x: 200, version: 2, versionNonce: 2, isDeleted: true },
  134. ),
  135. },
  136. {
  137. id3: Delta.create(
  138. { x: 100, version: 1, versionNonce: 1 },
  139. { x: 200, version: 2, versionNonce: 2 },
  140. ),
  141. },
  142. );
  143. const elementsDelta2 = ElementsDelta.create(
  144. {
  145. id1: Delta.create(
  146. { y: 100, version: 2, versionNonce: 2, isDeleted: true },
  147. { y: 200, version: 3, versionNonce: 3, isDeleted: false },
  148. ),
  149. },
  150. {
  151. id2: Delta.create(
  152. { y: 100, version: 2, versionNonce: 2, isDeleted: false },
  153. { y: 200, version: 3, versionNonce: 3, isDeleted: true },
  154. ),
  155. },
  156. {
  157. id3: Delta.create(
  158. { y: 100, version: 2, versionNonce: 2 },
  159. { y: 200, version: 3, versionNonce: 3 },
  160. ),
  161. },
  162. );
  163. const elementsDelta = elementsDelta1.squash(elementsDelta2);
  164. expect(elementsDelta.isEmpty()).toBeFalsy();
  165. expect(elementsDelta).toBe(elementsDelta1);
  166. expect(elementsDelta.added.id1).toEqual(
  167. Delta.create(
  168. { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
  169. { x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: false },
  170. ),
  171. );
  172. expect(elementsDelta.removed.id2).toEqual(
  173. Delta.create(
  174. { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: false },
  175. { x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: true },
  176. ),
  177. );
  178. expect(elementsDelta.updated.id3).toEqual(
  179. Delta.create(
  180. { x: 100, y: 100, version: 2, versionNonce: 2 },
  181. { x: 200, y: 200, version: 3, versionNonce: 3 },
  182. ),
  183. );
  184. });
  185. it("should squash different delta types ", () => {
  186. // id1: added -> updated => added
  187. // id2: removed -> added => added
  188. // id3: updated -> removed => removed
  189. const elementsDelta1 = ElementsDelta.create(
  190. {
  191. id1: Delta.create(
  192. { x: 100, version: 1, versionNonce: 1, isDeleted: true },
  193. { x: 101, version: 2, versionNonce: 2, isDeleted: false },
  194. ),
  195. },
  196. {
  197. id2: Delta.create(
  198. { x: 200, version: 1, versionNonce: 1, isDeleted: false },
  199. { x: 201, version: 2, versionNonce: 2, isDeleted: true },
  200. ),
  201. },
  202. {
  203. id3: Delta.create(
  204. { x: 300, version: 1, versionNonce: 1 },
  205. { x: 301, version: 2, versionNonce: 2 },
  206. ),
  207. },
  208. );
  209. const elementsDelta2 = ElementsDelta.create(
  210. {
  211. id2: Delta.create(
  212. { y: 200, version: 2, versionNonce: 2, isDeleted: true },
  213. { y: 201, version: 3, versionNonce: 3, isDeleted: false },
  214. ),
  215. },
  216. {
  217. id3: Delta.create(
  218. { y: 300, version: 2, versionNonce: 2, isDeleted: false },
  219. { y: 301, version: 3, versionNonce: 3, isDeleted: true },
  220. ),
  221. },
  222. {
  223. id1: Delta.create(
  224. { y: 100, version: 2, versionNonce: 2 },
  225. { y: 101, version: 3, versionNonce: 3 },
  226. ),
  227. },
  228. );
  229. const elementsDelta = elementsDelta1.squash(elementsDelta2);
  230. expect(elementsDelta.isEmpty()).toBeFalsy();
  231. expect(elementsDelta).toBe(elementsDelta1);
  232. expect(elementsDelta.added).toEqual({
  233. id1: Delta.create(
  234. { x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
  235. { x: 101, y: 101, version: 3, versionNonce: 3, isDeleted: false },
  236. ),
  237. id2: Delta.create(
  238. { x: 200, y: 200, version: 2, versionNonce: 2, isDeleted: true },
  239. { x: 201, y: 201, version: 3, versionNonce: 3, isDeleted: false },
  240. ),
  241. });
  242. expect(elementsDelta.removed).toEqual({
  243. id3: Delta.create(
  244. { x: 300, y: 300, version: 2, versionNonce: 2, isDeleted: false },
  245. { x: 301, y: 301, version: 3, versionNonce: 3, isDeleted: true },
  246. ),
  247. });
  248. expect(elementsDelta.updated).toEqual({});
  249. });
  250. it("should squash bound elements", () => {
  251. const elementsDelta1 = ElementsDelta.create(
  252. {},
  253. {},
  254. {
  255. id1: Delta.create(
  256. {
  257. version: 1,
  258. versionNonce: 1,
  259. boundElements: [{ id: "t1", type: "text" }],
  260. },
  261. {
  262. version: 2,
  263. versionNonce: 2,
  264. boundElements: [{ id: "t2", type: "text" }],
  265. },
  266. ),
  267. },
  268. );
  269. const elementsDelta2 = ElementsDelta.create(
  270. {},
  271. {},
  272. {
  273. id1: Delta.create(
  274. {
  275. version: 2,
  276. versionNonce: 2,
  277. boundElements: [{ id: "a1", type: "arrow" }],
  278. },
  279. {
  280. version: 3,
  281. versionNonce: 3,
  282. boundElements: [{ id: "a2", type: "arrow" }],
  283. },
  284. ),
  285. },
  286. );
  287. const elementsDelta = elementsDelta1.squash(elementsDelta2);
  288. expect(elementsDelta.updated.id1.deleted.boundElements).toEqual([
  289. { id: "t1", type: "text" },
  290. { id: "a1", type: "arrow" },
  291. ]);
  292. expect(elementsDelta.updated.id1.inserted.boundElements).toEqual([
  293. { id: "t2", type: "text" },
  294. { id: "a2", type: "arrow" },
  295. ]);
  296. });
  297. });
  298. });
  299. describe("AppStateDelta", () => {
  300. describe("ensure stable delta properties order", () => {
  301. it("should maintain stable order for root properties", () => {
  302. const name = "untitled scene";
  303. const selectedLinearElement = {
  304. elementId: "id1" as LinearElementEditor["elementId"],
  305. isEditing: false,
  306. };
  307. const commonAppState = {
  308. viewBackgroundColor: "#ffffff",
  309. selectedElementIds: {},
  310. selectedGroupIds: {},
  311. editingGroupId: null,
  312. croppingElementId: null,
  313. editingLinearElementId: null,
  314. selectedLinearElementIsEditing: null,
  315. lockedMultiSelections: {},
  316. activeLockedId: null,
  317. };
  318. const prevAppState1: ObservedAppState = {
  319. ...commonAppState,
  320. name: "",
  321. selectedLinearElement: null,
  322. };
  323. const nextAppState1: ObservedAppState = {
  324. ...commonAppState,
  325. name,
  326. selectedLinearElement,
  327. };
  328. const prevAppState2: ObservedAppState = {
  329. selectedLinearElement: null,
  330. name: "",
  331. ...commonAppState,
  332. };
  333. const nextAppState2: ObservedAppState = {
  334. selectedLinearElement,
  335. name,
  336. ...commonAppState,
  337. };
  338. const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
  339. const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
  340. expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
  341. });
  342. it("should maintain stable order for selectedElementIds", () => {
  343. const commonAppState = {
  344. name: "",
  345. viewBackgroundColor: "#ffffff",
  346. selectedGroupIds: {},
  347. editingGroupId: null,
  348. croppingElementId: null,
  349. selectedLinearElement: null,
  350. activeLockedId: null,
  351. lockedMultiSelections: {},
  352. };
  353. const prevAppState1: ObservedAppState = {
  354. ...commonAppState,
  355. selectedElementIds: { id5: true, id2: true, id4: true },
  356. };
  357. const nextAppState1: ObservedAppState = {
  358. ...commonAppState,
  359. selectedElementIds: {
  360. id1: true,
  361. id2: true,
  362. id3: true,
  363. },
  364. };
  365. const prevAppState2: ObservedAppState = {
  366. ...commonAppState,
  367. selectedElementIds: { id4: true, id2: true, id5: true },
  368. };
  369. const nextAppState2: ObservedAppState = {
  370. ...commonAppState,
  371. selectedElementIds: {
  372. id3: true,
  373. id2: true,
  374. id1: true,
  375. },
  376. };
  377. const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
  378. const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
  379. expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
  380. });
  381. it("should maintain stable order for selectedGroupIds", () => {
  382. const commonAppState = {
  383. name: "",
  384. viewBackgroundColor: "#ffffff",
  385. selectedElementIds: {},
  386. editingGroupId: null,
  387. croppingElementId: null,
  388. selectedLinearElement: null,
  389. activeLockedId: null,
  390. lockedMultiSelections: {},
  391. };
  392. const prevAppState1: ObservedAppState = {
  393. ...commonAppState,
  394. selectedGroupIds: { id5: false, id2: true, id4: true, id0: true },
  395. };
  396. const nextAppState1: ObservedAppState = {
  397. ...commonAppState,
  398. selectedGroupIds: {
  399. id0: true,
  400. id1: true,
  401. id2: false,
  402. id3: true,
  403. },
  404. };
  405. const prevAppState2: ObservedAppState = {
  406. ...commonAppState,
  407. selectedGroupIds: { id0: true, id4: true, id2: true, id5: false },
  408. };
  409. const nextAppState2: ObservedAppState = {
  410. ...commonAppState,
  411. selectedGroupIds: {
  412. id3: true,
  413. id2: false,
  414. id1: true,
  415. id0: true,
  416. },
  417. };
  418. const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
  419. const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
  420. expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
  421. });
  422. });
  423. describe("squash", () => {
  424. it("should not squash when second delta is empty", () => {
  425. const delta = Delta.create(
  426. { name: "untitled scene" },
  427. { name: "titled scene" },
  428. );
  429. const appStateDelta1 = AppStateDelta.create(delta);
  430. const appStateDelta2 = AppStateDelta.empty();
  431. const appStateDelta = appStateDelta1.squash(appStateDelta2);
  432. expect(appStateDelta.isEmpty()).toBeFalsy();
  433. expect(appStateDelta).toBe(appStateDelta1);
  434. expect(appStateDelta.delta).toBe(delta);
  435. });
  436. it("should squash exclusive properties", () => {
  437. const delta1 = Delta.create(
  438. { name: "untitled scene" },
  439. { name: "titled scene" },
  440. );
  441. const delta2 = Delta.create(
  442. { viewBackgroundColor: "#ffffff" },
  443. { viewBackgroundColor: "#000000" },
  444. );
  445. const appStateDelta1 = AppStateDelta.create(delta1);
  446. const appStateDelta2 = AppStateDelta.create(delta2);
  447. const appStateDelta = appStateDelta1.squash(appStateDelta2);
  448. expect(appStateDelta.isEmpty()).toBeFalsy();
  449. expect(appStateDelta).toBe(appStateDelta1);
  450. expect(appStateDelta.delta).toEqual(
  451. Delta.create(
  452. { name: "untitled scene", viewBackgroundColor: "#ffffff" },
  453. { name: "titled scene", viewBackgroundColor: "#000000" },
  454. ),
  455. );
  456. });
  457. it("should squash selectedElementIds, selectedGroupIds and lockedMultiSelections", () => {
  458. const delta1 = Delta.create<Partial<ObservedAppState>>(
  459. {
  460. name: "untitled scene",
  461. selectedElementIds: { id1: true },
  462. selectedGroupIds: {},
  463. lockedMultiSelections: { g1: true },
  464. },
  465. {
  466. name: "titled scene",
  467. selectedElementIds: { id2: true },
  468. selectedGroupIds: { g1: true },
  469. lockedMultiSelections: {},
  470. },
  471. );
  472. const delta2 = Delta.create<Partial<ObservedAppState>>(
  473. {
  474. selectedElementIds: { id3: true },
  475. selectedGroupIds: { g1: true },
  476. lockedMultiSelections: {},
  477. },
  478. {
  479. selectedElementIds: { id2: true },
  480. selectedGroupIds: { g2: true, g3: true },
  481. lockedMultiSelections: { g3: true },
  482. },
  483. );
  484. const appStateDelta1 = AppStateDelta.create(delta1);
  485. const appStateDelta2 = AppStateDelta.create(delta2);
  486. const appStateDelta = appStateDelta1.squash(appStateDelta2);
  487. expect(appStateDelta.isEmpty()).toBeFalsy();
  488. expect(appStateDelta).toBe(appStateDelta1);
  489. expect(appStateDelta.delta).toEqual(
  490. Delta.create<Partial<ObservedAppState>>(
  491. {
  492. name: "untitled scene",
  493. selectedElementIds: { id1: true, id3: true },
  494. selectedGroupIds: { g1: true },
  495. lockedMultiSelections: { g1: true },
  496. },
  497. {
  498. name: "titled scene",
  499. selectedElementIds: { id2: true },
  500. selectedGroupIds: { g1: true, g2: true, g3: true },
  501. lockedMultiSelections: { g3: true },
  502. },
  503. ),
  504. );
  505. });
  506. });
  507. });