restore.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. import * as restore from "../../data/restore";
  2. import {
  3. ExcalidrawElement,
  4. ExcalidrawFreeDrawElement,
  5. ExcalidrawLinearElement,
  6. ExcalidrawTextElement,
  7. } from "../../element/types";
  8. import * as sizeHelpers from "../../element/sizeHelpers";
  9. import { API } from "../helpers/api";
  10. import { getDefaultAppState } from "../../appState";
  11. import { ImportedDataState } from "../../data/types";
  12. import { NormalizedZoomValue } from "../../types";
  13. import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
  14. import { newElementWith } from "../../element/mutateElement";
  15. describe("restoreElements", () => {
  16. const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
  17. beforeEach(() => {
  18. mockSizeHelper.mockReset();
  19. });
  20. afterAll(() => {
  21. mockSizeHelper.mockRestore();
  22. });
  23. it("should return empty array when element is null", () => {
  24. expect(restore.restoreElements(null, null)).toStrictEqual([]);
  25. });
  26. it("should not call isInvisiblySmallElement when element is a selection element", () => {
  27. const selectionEl = { type: "selection" } as ExcalidrawElement;
  28. const restoreElements = restore.restoreElements([selectionEl], null);
  29. expect(restoreElements.length).toBe(0);
  30. expect(sizeHelpers.isInvisiblySmallElement).toBeCalledTimes(0);
  31. });
  32. it("should return empty array when input type is not supported", () => {
  33. const dummyNotSupportedElement: any = API.createElement({
  34. type: "text",
  35. });
  36. dummyNotSupportedElement.type = "not supported";
  37. expect(
  38. restore.restoreElements([dummyNotSupportedElement], null).length,
  39. ).toBe(0);
  40. });
  41. it("should return empty array when isInvisiblySmallElement is true", () => {
  42. const rectElement = API.createElement({ type: "rectangle" });
  43. mockSizeHelper.mockImplementation(() => true);
  44. expect(restore.restoreElements([rectElement], null).length).toBe(0);
  45. });
  46. it("should restore text element correctly passing value for each attribute", () => {
  47. const textElement = API.createElement({
  48. type: "text",
  49. fontSize: 14,
  50. fontFamily: FONT_FAMILY.Virgil,
  51. text: "text",
  52. textAlign: "center",
  53. verticalAlign: "middle",
  54. id: "id-text01",
  55. });
  56. const restoredText = restore.restoreElements(
  57. [textElement],
  58. null,
  59. )[0] as ExcalidrawTextElement;
  60. expect(restoredText).toMatchSnapshot({
  61. seed: expect.any(Number),
  62. });
  63. });
  64. it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
  65. const textElement: any = API.createElement({
  66. type: "text",
  67. textAlign: undefined,
  68. verticalAlign: undefined,
  69. id: "id-text01",
  70. });
  71. textElement.text = null;
  72. textElement.font = "10 unknown";
  73. const restoredText = restore.restoreElements(
  74. [textElement],
  75. null,
  76. )[0] as ExcalidrawTextElement;
  77. expect(restoredText).toMatchSnapshot({
  78. seed: expect.any(Number),
  79. });
  80. });
  81. it("should restore freedraw element correctly", () => {
  82. const freedrawElement = API.createElement({
  83. type: "freedraw",
  84. id: "id-freedraw01",
  85. });
  86. const restoredFreedraw = restore.restoreElements(
  87. [freedrawElement],
  88. null,
  89. )[0] as ExcalidrawFreeDrawElement;
  90. expect(restoredFreedraw).toMatchSnapshot({ seed: expect.any(Number) });
  91. });
  92. it("should restore line and draw elements correctly", () => {
  93. const lineElement = API.createElement({ type: "line", id: "id-line01" });
  94. const drawElement: any = API.createElement({
  95. type: "line",
  96. id: "id-draw01",
  97. });
  98. drawElement.type = "draw";
  99. const restoredElements = restore.restoreElements(
  100. [lineElement, drawElement],
  101. null,
  102. );
  103. const restoredLine = restoredElements[0] as ExcalidrawLinearElement;
  104. const restoredDraw = restoredElements[1] as ExcalidrawLinearElement;
  105. expect(restoredLine).toMatchSnapshot({ seed: expect.any(Number) });
  106. expect(restoredDraw).toMatchSnapshot({ seed: expect.any(Number) });
  107. });
  108. it("should restore arrow element correctly", () => {
  109. const arrowElement = API.createElement({ type: "arrow", id: "id-arrow01" });
  110. const restoredElements = restore.restoreElements([arrowElement], null);
  111. const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
  112. expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) });
  113. });
  114. it("when arrow element has defined endArrowHead", () => {
  115. const arrowElement = API.createElement({ type: "arrow" });
  116. const restoredElements = restore.restoreElements([arrowElement], null);
  117. const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
  118. expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead);
  119. });
  120. it("when arrow element has undefined endArrowHead", () => {
  121. const arrowElement = API.createElement({ type: "arrow" });
  122. Object.defineProperty(arrowElement, "endArrowhead", {
  123. get: jest.fn(() => undefined),
  124. });
  125. const restoredElements = restore.restoreElements([arrowElement], null);
  126. const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
  127. expect(restoredArrow.endArrowhead).toBe("arrow");
  128. });
  129. it("when element.points of a line element is not an array", () => {
  130. const lineElement: any = API.createElement({
  131. type: "line",
  132. width: 100,
  133. height: 200,
  134. });
  135. lineElement.points = "not an array";
  136. const expectedLinePoints = [
  137. [0, 0],
  138. [lineElement.width, lineElement.height],
  139. ];
  140. const restoredLine = restore.restoreElements(
  141. [lineElement],
  142. null,
  143. )[0] as ExcalidrawLinearElement;
  144. expect(restoredLine.points).toMatchObject(expectedLinePoints);
  145. });
  146. it("when the number of points of a line is greater or equal 2", () => {
  147. const lineElement_0 = API.createElement({
  148. type: "line",
  149. width: 100,
  150. height: 200,
  151. x: 10,
  152. y: 20,
  153. });
  154. const lineElement_1 = API.createElement({
  155. type: "line",
  156. width: 200,
  157. height: 400,
  158. x: 30,
  159. y: 40,
  160. });
  161. const pointsEl_0 = [
  162. [0, 0],
  163. [1, 1],
  164. ];
  165. Object.defineProperty(lineElement_0, "points", {
  166. get: jest.fn(() => pointsEl_0),
  167. });
  168. const pointsEl_1 = [
  169. [3, 4],
  170. [5, 6],
  171. ];
  172. Object.defineProperty(lineElement_1, "points", {
  173. get: jest.fn(() => pointsEl_1),
  174. });
  175. const restoredElements = restore.restoreElements(
  176. [lineElement_0, lineElement_1],
  177. null,
  178. );
  179. const restoredLine_0 = restoredElements[0] as ExcalidrawLinearElement;
  180. const restoredLine_1 = restoredElements[1] as ExcalidrawLinearElement;
  181. expect(restoredLine_0.points).toMatchObject(pointsEl_0);
  182. const offsetX = pointsEl_1[0][0];
  183. const offsetY = pointsEl_1[0][1];
  184. const restoredPointsEl1 = [
  185. [pointsEl_1[0][0] - offsetX, pointsEl_1[0][1] - offsetY],
  186. [pointsEl_1[1][0] - offsetX, pointsEl_1[1][1] - offsetY],
  187. ];
  188. expect(restoredLine_1.points).toMatchObject(restoredPointsEl1);
  189. expect(restoredLine_1.x).toBe(lineElement_1.x + offsetX);
  190. expect(restoredLine_1.y).toBe(lineElement_1.y + offsetY);
  191. });
  192. it("should restore correctly with rectangle, ellipse and diamond elements", () => {
  193. const types = ["rectangle", "ellipse", "diamond"];
  194. const elements: ExcalidrawElement[] = [];
  195. let idCount = 0;
  196. types.forEach((elType) => {
  197. idCount += 1;
  198. const element = API.createElement({
  199. type: elType as "rectangle" | "ellipse" | "diamond" | "embeddable",
  200. id: idCount.toString(),
  201. fillStyle: "cross-hatch",
  202. strokeWidth: 2,
  203. strokeStyle: "dashed",
  204. roughness: 2,
  205. opacity: 10,
  206. x: 10,
  207. y: 20,
  208. strokeColor: "red",
  209. backgroundColor: "blue",
  210. width: 100,
  211. height: 200,
  212. groupIds: ["1", "2", "3"],
  213. roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
  214. });
  215. elements.push(element);
  216. });
  217. const restoredElements = restore.restoreElements(elements, null);
  218. expect(restoredElements[0]).toMatchSnapshot({ seed: expect.any(Number) });
  219. expect(restoredElements[1]).toMatchSnapshot({ seed: expect.any(Number) });
  220. expect(restoredElements[2]).toMatchSnapshot({ seed: expect.any(Number) });
  221. });
  222. it("bump versions of local duplicate elements when supplied", () => {
  223. const rectangle = API.createElement({ type: "rectangle" });
  224. const ellipse = API.createElement({ type: "ellipse" });
  225. const rectangle_modified = newElementWith(rectangle, { isDeleted: true });
  226. const restoredElements = restore.restoreElements(
  227. [rectangle, ellipse],
  228. [rectangle_modified],
  229. );
  230. expect(restoredElements[0].id).toBe(rectangle.id);
  231. expect(restoredElements[0].versionNonce).not.toBe(rectangle.versionNonce);
  232. expect(restoredElements).toEqual([
  233. expect.objectContaining({
  234. id: rectangle.id,
  235. version: rectangle_modified.version + 1,
  236. }),
  237. expect.objectContaining({
  238. id: ellipse.id,
  239. version: ellipse.version,
  240. versionNonce: ellipse.versionNonce,
  241. }),
  242. ]);
  243. });
  244. });
  245. describe("restoreAppState", () => {
  246. it("should restore with imported data", () => {
  247. const stubImportedAppState = getDefaultAppState();
  248. stubImportedAppState.activeTool.type = "selection";
  249. stubImportedAppState.cursorButton = "down";
  250. stubImportedAppState.name = "imported app state";
  251. const stubLocalAppState = getDefaultAppState();
  252. stubLocalAppState.activeTool.type = "rectangle";
  253. stubLocalAppState.cursorButton = "up";
  254. stubLocalAppState.name = "local app state";
  255. const restoredAppState = restore.restoreAppState(
  256. stubImportedAppState,
  257. stubLocalAppState,
  258. );
  259. expect(restoredAppState.activeTool).toEqual(
  260. stubImportedAppState.activeTool,
  261. );
  262. expect(restoredAppState.cursorButton).toBe("up");
  263. expect(restoredAppState.name).toBe(stubImportedAppState.name);
  264. });
  265. it("should restore with current app state when imported data state is undefined", () => {
  266. const stubImportedAppState = {
  267. ...getDefaultAppState(),
  268. cursorButton: undefined,
  269. name: undefined,
  270. };
  271. const stubLocalAppState = getDefaultAppState();
  272. stubLocalAppState.cursorButton = "down";
  273. stubLocalAppState.name = "local app state";
  274. const restoredAppState = restore.restoreAppState(
  275. stubImportedAppState,
  276. stubLocalAppState,
  277. );
  278. expect(restoredAppState.cursorButton).toBe(stubLocalAppState.cursorButton);
  279. expect(restoredAppState.name).toBe(stubLocalAppState.name);
  280. });
  281. it("should return imported data when local app state is null", () => {
  282. const stubImportedAppState = getDefaultAppState();
  283. stubImportedAppState.cursorButton = "down";
  284. stubImportedAppState.name = "imported app state";
  285. const restoredAppState = restore.restoreAppState(
  286. stubImportedAppState,
  287. null,
  288. );
  289. expect(restoredAppState.cursorButton).toBe("up");
  290. expect(restoredAppState.name).toBe(stubImportedAppState.name);
  291. });
  292. it("should return local app state when imported data state is null", () => {
  293. const stubLocalAppState = getDefaultAppState();
  294. stubLocalAppState.cursorButton = "down";
  295. stubLocalAppState.name = "local app state";
  296. const restoredAppState = restore.restoreAppState(null, stubLocalAppState);
  297. expect(restoredAppState.cursorButton).toBe(stubLocalAppState.cursorButton);
  298. expect(restoredAppState.name).toBe(stubLocalAppState.name);
  299. });
  300. it("should return default app state when imported data state and local app state are undefined", () => {
  301. const stubImportedAppState = {
  302. ...getDefaultAppState(),
  303. cursorButton: undefined,
  304. };
  305. const stubLocalAppState = {
  306. ...getDefaultAppState(),
  307. cursorButton: undefined,
  308. };
  309. const restoredAppState = restore.restoreAppState(
  310. stubImportedAppState,
  311. stubLocalAppState,
  312. );
  313. expect(restoredAppState.cursorButton).toBe(
  314. getDefaultAppState().cursorButton,
  315. );
  316. });
  317. it("should return default app state when imported data state and local app state are null", () => {
  318. const restoredAppState = restore.restoreAppState(null, null);
  319. expect(restoredAppState.cursorButton).toBe(
  320. getDefaultAppState().cursorButton,
  321. );
  322. });
  323. it("when imported data state has a not allowed Excalidraw Element Types", () => {
  324. const stubImportedAppState: any = getDefaultAppState();
  325. stubImportedAppState.activeTool = "not allowed Excalidraw Element Types";
  326. const stubLocalAppState = getDefaultAppState();
  327. const restoredAppState = restore.restoreAppState(
  328. stubImportedAppState,
  329. stubLocalAppState,
  330. );
  331. expect(restoredAppState.activeTool.type).toBe("selection");
  332. });
  333. describe("with zoom in imported data state", () => {
  334. it("when imported data state has zoom as a number", () => {
  335. const stubImportedAppState: any = getDefaultAppState();
  336. stubImportedAppState.zoom = 10;
  337. const stubLocalAppState = getDefaultAppState();
  338. const restoredAppState = restore.restoreAppState(
  339. stubImportedAppState,
  340. stubLocalAppState,
  341. );
  342. expect(restoredAppState.zoom.value).toBe(10);
  343. });
  344. it("when the zoom of imported data state is not a number", () => {
  345. const stubImportedAppState = getDefaultAppState();
  346. stubImportedAppState.zoom = {
  347. value: 10 as NormalizedZoomValue,
  348. };
  349. const stubLocalAppState = getDefaultAppState();
  350. const restoredAppState = restore.restoreAppState(
  351. stubImportedAppState,
  352. stubLocalAppState,
  353. );
  354. expect(restoredAppState.zoom.value).toBe(10);
  355. expect(restoredAppState.zoom).toMatchObject(stubImportedAppState.zoom);
  356. });
  357. it("when the zoom of imported data state zoom is null", () => {
  358. const stubImportedAppState = getDefaultAppState();
  359. Object.defineProperty(stubImportedAppState, "zoom", {
  360. get: jest.fn(() => null),
  361. });
  362. const stubLocalAppState = getDefaultAppState();
  363. const restoredAppState = restore.restoreAppState(
  364. stubImportedAppState,
  365. stubLocalAppState,
  366. );
  367. expect(restoredAppState.zoom).toMatchObject(getDefaultAppState().zoom);
  368. });
  369. });
  370. it("should handle appState.openSidebar legacy values", () => {
  371. expect(restore.restoreAppState({}, null).openSidebar).toBe(null);
  372. expect(
  373. restore.restoreAppState({ openSidebar: "library" } as any, null)
  374. .openSidebar,
  375. ).toEqual({ name: DEFAULT_SIDEBAR.name });
  376. expect(
  377. restore.restoreAppState({ openSidebar: "xxx" } as any, null).openSidebar,
  378. ).toEqual({ name: DEFAULT_SIDEBAR.name });
  379. // while "library" was our legacy sidebar name, we can't assume it's legacy
  380. // value as it may be some host app's custom sidebar name ¯\_(ツ)_/¯
  381. expect(
  382. restore.restoreAppState({ openSidebar: { name: "library" } } as any, null)
  383. .openSidebar,
  384. ).toEqual({ name: "library" });
  385. expect(
  386. restore.restoreAppState(
  387. { openSidebar: { name: DEFAULT_SIDEBAR.name, tab: "ola" } } as any,
  388. null,
  389. ).openSidebar,
  390. ).toEqual({ name: DEFAULT_SIDEBAR.name, tab: "ola" });
  391. });
  392. });
  393. describe("restore", () => {
  394. it("when imported data state is null it should return an empty array of elements", () => {
  395. const stubLocalAppState = getDefaultAppState();
  396. const restoredData = restore.restore(null, stubLocalAppState, null);
  397. expect(restoredData.elements.length).toBe(0);
  398. });
  399. it("when imported data state is null it should return the local app state property", () => {
  400. const stubLocalAppState = getDefaultAppState();
  401. stubLocalAppState.cursorButton = "down";
  402. stubLocalAppState.name = "local app state";
  403. const restoredData = restore.restore(null, stubLocalAppState, null);
  404. expect(restoredData.appState.cursorButton).toBe(
  405. stubLocalAppState.cursorButton,
  406. );
  407. expect(restoredData.appState.name).toBe(stubLocalAppState.name);
  408. });
  409. it("when imported data state has elements", () => {
  410. const stubLocalAppState = getDefaultAppState();
  411. const textElement = API.createElement({ type: "text" });
  412. const rectElement = API.createElement({ type: "rectangle" });
  413. const elements = [textElement, rectElement];
  414. const importedDataState = {} as ImportedDataState;
  415. importedDataState.elements = elements;
  416. const restoredData = restore.restore(
  417. importedDataState,
  418. stubLocalAppState,
  419. null,
  420. );
  421. expect(restoredData.elements.length).toBe(elements.length);
  422. });
  423. it("when local app state is null but imported app state is supplied", () => {
  424. const stubImportedAppState = getDefaultAppState();
  425. stubImportedAppState.cursorButton = "down";
  426. stubImportedAppState.name = "imported app state";
  427. const importedDataState = {} as ImportedDataState;
  428. importedDataState.appState = stubImportedAppState;
  429. const restoredData = restore.restore(importedDataState, null, null);
  430. expect(restoredData.appState.cursorButton).toBe("up");
  431. expect(restoredData.appState.name).toBe(stubImportedAppState.name);
  432. });
  433. it("bump versions of local duplicate elements when supplied", () => {
  434. const rectangle = API.createElement({ type: "rectangle" });
  435. const ellipse = API.createElement({ type: "ellipse" });
  436. const rectangle_modified = newElementWith(rectangle, { isDeleted: true });
  437. const restoredData = restore.restore(
  438. { elements: [rectangle, ellipse] },
  439. null,
  440. [rectangle_modified],
  441. );
  442. expect(restoredData.elements[0].id).toBe(rectangle.id);
  443. expect(restoredData.elements[0].versionNonce).not.toBe(
  444. rectangle.versionNonce,
  445. );
  446. expect(restoredData.elements).toEqual([
  447. expect.objectContaining({ version: rectangle_modified.version + 1 }),
  448. expect.objectContaining({
  449. id: ellipse.id,
  450. version: ellipse.version,
  451. versionNonce: ellipse.versionNonce,
  452. }),
  453. ]);
  454. });
  455. });
  456. describe("repairing bindings", () => {
  457. it("should repair container boundElements when repair is true", () => {
  458. const container = API.createElement({
  459. type: "rectangle",
  460. boundElements: [],
  461. });
  462. const boundElement = API.createElement({
  463. type: "text",
  464. containerId: container.id,
  465. });
  466. expect(container.boundElements).toEqual([]);
  467. let restoredElements = restore.restoreElements(
  468. [container, boundElement],
  469. null,
  470. );
  471. expect(restoredElements).toEqual([
  472. expect.objectContaining({
  473. id: container.id,
  474. boundElements: [],
  475. }),
  476. expect.objectContaining({
  477. id: boundElement.id,
  478. containerId: container.id,
  479. }),
  480. ]);
  481. restoredElements = restore.restoreElements(
  482. [container, boundElement],
  483. null,
  484. { repairBindings: true },
  485. );
  486. expect(restoredElements).toEqual([
  487. expect.objectContaining({
  488. id: container.id,
  489. boundElements: [{ type: boundElement.type, id: boundElement.id }],
  490. }),
  491. expect.objectContaining({
  492. id: boundElement.id,
  493. containerId: container.id,
  494. }),
  495. ]);
  496. });
  497. it("should repair containerId of boundElements when repair is true", () => {
  498. const boundElement = API.createElement({
  499. type: "text",
  500. containerId: null,
  501. });
  502. const container = API.createElement({
  503. type: "rectangle",
  504. boundElements: [{ type: boundElement.type, id: boundElement.id }],
  505. });
  506. let restoredElements = restore.restoreElements(
  507. [container, boundElement],
  508. null,
  509. );
  510. expect(restoredElements).toEqual([
  511. expect.objectContaining({
  512. id: container.id,
  513. boundElements: [{ type: boundElement.type, id: boundElement.id }],
  514. }),
  515. expect.objectContaining({
  516. id: boundElement.id,
  517. containerId: null,
  518. }),
  519. ]);
  520. restoredElements = restore.restoreElements(
  521. [container, boundElement],
  522. null,
  523. { repairBindings: true },
  524. );
  525. expect(restoredElements).toEqual([
  526. expect.objectContaining({
  527. id: container.id,
  528. boundElements: [{ type: boundElement.type, id: boundElement.id }],
  529. }),
  530. expect.objectContaining({
  531. id: boundElement.id,
  532. containerId: container.id,
  533. }),
  534. ]);
  535. });
  536. it("should ignore bound element if deleted", () => {
  537. const container = API.createElement({
  538. type: "rectangle",
  539. boundElements: [],
  540. });
  541. const boundElement = API.createElement({
  542. type: "text",
  543. containerId: container.id,
  544. isDeleted: true,
  545. });
  546. expect(container.boundElements).toEqual([]);
  547. const restoredElements = restore.restoreElements(
  548. [container, boundElement],
  549. null,
  550. );
  551. expect(restoredElements).toEqual([
  552. expect.objectContaining({
  553. id: container.id,
  554. boundElements: [],
  555. }),
  556. expect.objectContaining({
  557. id: boundElement.id,
  558. containerId: container.id,
  559. }),
  560. ]);
  561. });
  562. it("should remove bindings of deleted elements from boundElements when repair is true", () => {
  563. const container = API.createElement({
  564. type: "rectangle",
  565. boundElements: [],
  566. });
  567. const boundElement = API.createElement({
  568. type: "text",
  569. containerId: container.id,
  570. isDeleted: true,
  571. });
  572. const invisibleBoundElement = API.createElement({
  573. type: "text",
  574. containerId: container.id,
  575. width: 0,
  576. height: 0,
  577. });
  578. const obsoleteBinding = { type: boundElement.type, id: boundElement.id };
  579. const invisibleBinding = {
  580. type: invisibleBoundElement.type,
  581. id: invisibleBoundElement.id,
  582. };
  583. expect(container.boundElements).toEqual([]);
  584. const nonExistentBinding = { type: "text", id: "non-existent" };
  585. // @ts-ignore
  586. container.boundElements = [
  587. obsoleteBinding,
  588. invisibleBinding,
  589. nonExistentBinding,
  590. ];
  591. let restoredElements = restore.restoreElements(
  592. [container, invisibleBoundElement, boundElement],
  593. null,
  594. );
  595. expect(restoredElements).toEqual([
  596. expect.objectContaining({
  597. id: container.id,
  598. boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
  599. }),
  600. expect.objectContaining({
  601. id: boundElement.id,
  602. containerId: container.id,
  603. }),
  604. ]);
  605. restoredElements = restore.restoreElements(
  606. [container, invisibleBoundElement, boundElement],
  607. null,
  608. { repairBindings: true },
  609. );
  610. expect(restoredElements).toEqual([
  611. expect.objectContaining({
  612. id: container.id,
  613. boundElements: [],
  614. }),
  615. expect.objectContaining({
  616. id: boundElement.id,
  617. containerId: container.id,
  618. }),
  619. ]);
  620. });
  621. it("should remove containerId if container not exists when repair is true", () => {
  622. const boundElement = API.createElement({
  623. type: "text",
  624. containerId: "non-existent",
  625. });
  626. const boundElementDeleted = API.createElement({
  627. type: "text",
  628. containerId: "non-existent",
  629. isDeleted: true,
  630. });
  631. let restoredElements = restore.restoreElements(
  632. [boundElement, boundElementDeleted],
  633. null,
  634. );
  635. expect(restoredElements).toEqual([
  636. expect.objectContaining({
  637. id: boundElement.id,
  638. containerId: "non-existent",
  639. }),
  640. expect.objectContaining({
  641. id: boundElementDeleted.id,
  642. containerId: "non-existent",
  643. }),
  644. ]);
  645. restoredElements = restore.restoreElements(
  646. [boundElement, boundElementDeleted],
  647. null,
  648. { repairBindings: true },
  649. );
  650. expect(restoredElements).toEqual([
  651. expect.objectContaining({
  652. id: boundElement.id,
  653. containerId: null,
  654. }),
  655. expect.objectContaining({
  656. id: boundElementDeleted.id,
  657. containerId: null,
  658. }),
  659. ]);
  660. });
  661. });