diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/tests/library.test.tsx | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/tests/library.test.tsx')
| -rw-r--r-- | packages/excalidraw/tests/library.test.tsx | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/packages/excalidraw/tests/library.test.tsx b/packages/excalidraw/tests/library.test.tsx new file mode 100644 index 0000000..7b48407 --- /dev/null +++ b/packages/excalidraw/tests/library.test.tsx @@ -0,0 +1,341 @@ +import React from "react"; +import { vi } from "vitest"; +import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils"; +import { act, queryByTestId } from "@testing-library/react"; + +import { Excalidraw } from "../index"; +import { API } from "./helpers/api"; +import { MIME_TYPES, ORIG_ID } from "../constants"; +import type { LibraryItem, LibraryItems } from "../types"; +import { UI } from "./helpers/ui"; +import { serializeLibraryAsJSON } from "../data/json"; +import { distributeLibraryItemsOnSquareGrid } from "../data/library"; +import type { ExcalidrawGenericElement } from "../element/types"; +import { getCommonBoundingBox } from "../element/bounds"; +import { parseLibraryJSON } from "../data/blob"; + +const { h } = window; + +const libraryJSONPromise = API.readFile( + "./fixtures/fixture_library.excalidrawlib", + "utf8", +); + +const mockLibraryFilePromise = new Promise<Blob>(async (resolve, reject) => { + try { + resolve( + new Blob([await libraryJSONPromise], { type: MIME_TYPES.excalidrawlib }), + ); + } catch (error) { + reject(error); + } +}); + +vi.mock("../data/filesystem.ts", async (importOriginal) => { + const module = await importOriginal(); + return { + __esmodule: true, + //@ts-ignore + ...module, + fileOpen: vi.fn(() => mockLibraryFilePromise), + }; +}); + +describe("library", () => { + beforeEach(async () => { + await render(<Excalidraw />); + await act(() => { + return h.app.library.resetLibrary(); + }); + }); + + it("import library via drag&drop", async () => { + expect(await h.app.library.getLatestLibrary()).toEqual([]); + await API.drop( + await API.loadFile("./fixtures/fixture_library.excalidrawlib"), + ); + await waitFor(async () => { + expect(await h.app.library.getLatestLibrary()).toEqual([ + { + status: "unpublished", + elements: [expect.objectContaining({ id: "A" })], + id: "id0", + created: expect.any(Number), + }, + ]); + }); + }); + + // NOTE: mocked to test logic, not actual drag&drop via UI + it("drop library item onto canvas", async () => { + expect(h.elements).toEqual([]); + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + await API.drop( + new Blob([serializeLibraryAsJSON(libraryItems)], { + type: MIME_TYPES.excalidrawlib, + }), + ); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]); + }); + }); + + it("should regenerate ids but retain bindings on library insert", async () => { + const rectangle = API.createElement({ + id: "rectangle1", + type: "rectangle", + boundElements: [ + { type: "text", id: "text1" }, + { type: "arrow", id: "arrow1" }, + ], + }); + const text = API.createElement({ + id: "text1", + type: "text", + text: "ola", + containerId: "rectangle1", + }); + const arrow = API.createElement({ + id: "arrow1", + type: "arrow", + endBinding: { + elementId: "rectangle1", + focus: -1, + gap: 0, + fixedPoint: [0.5, 1], + }, + }); + + await API.drop( + new Blob( + [ + serializeLibraryAsJSON([ + { + id: "item1", + status: "published", + elements: [rectangle, text, arrow], + created: 1, + }, + ]), + ], + { + type: MIME_TYPES.excalidrawlib, + }, + ), + ); + + await waitFor(() => { + expect(h.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + [ORIG_ID]: "rectangle1", + boundElements: expect.arrayContaining([ + { type: "text", id: getCloneByOrigId("text1").id }, + { type: "arrow", id: getCloneByOrigId("arrow1").id }, + ]), + }), + expect.objectContaining({ + [ORIG_ID]: "text1", + containerId: getCloneByOrigId("rectangle1").id, + }), + expect.objectContaining({ + [ORIG_ID]: "arrow1", + endBinding: expect.objectContaining({ + elementId: getCloneByOrigId("rectangle1").id, + }), + }), + ]), + ); + }); + }); + + it("should fix duplicate ids between items on insert", async () => { + // note, we're not testing for duplicate group ids and such because + // deduplication of that happens upstream in the library component + // which would be very hard to orchestrate in this test + + const elem1 = API.createElement({ + id: "elem1", + type: "rectangle", + }); + const item1: LibraryItem = { + id: "item1", + status: "published", + elements: [elem1], + created: 1, + }; + + await API.drop( + new Blob([serializeLibraryAsJSON([item1, item1])], { + type: MIME_TYPES.excalidrawlib, + }), + ); + + await waitFor(() => { + expect(h.elements).toEqual([ + expect.objectContaining({ + [ORIG_ID]: "elem1", + }), + expect.objectContaining({ + id: expect.not.stringMatching(/^elem1$/), + [ORIG_ID]: expect.not.stringMatching(/^\w+$/), + }), + ]); + }); + }); + + it("inserting library item should revert to selection tool", async () => { + UI.clickTool("rectangle"); + expect(h.elements).toEqual([]); + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + await API.drop( + new Blob([serializeLibraryAsJSON(libraryItems)], { + type: MIME_TYPES.excalidrawlib, + }), + ); + await waitFor(() => { + expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]); + }); + expect(h.state.activeTool.type).toBe("selection"); + }); +}); + +describe("library menu", () => { + it("should load library from file picker", async () => { + const { container } = await render(<Excalidraw />); + + const latestLibrary = await h.app.library.getLatestLibrary(); + expect(latestLibrary.length).toBe(0); + + const libraryButton = container.querySelector(".sidebar-trigger"); + + fireEvent.click(libraryButton!); + fireEvent.click( + queryByTestId( + container.querySelector(".layer-ui__library")!, + "dropdown-menu-button", + )!, + ); + fireEvent.click(queryByTestId(container, "lib-dropdown--load")!); + + const libraryItems = parseLibraryJSON(await libraryJSONPromise); + + await waitFor(async () => { + const latestLibrary = await h.app.library.getLatestLibrary(); + expect(latestLibrary.length).toBeGreaterThan(0); + expect(latestLibrary.length).toBe(libraryItems.length); + const { versionNonce, ...strippedElement } = libraryItems[0]?.elements[0]; // stripped due to mutations + expect(latestLibrary[0].elements).toEqual([ + expect.objectContaining(strippedElement), + ]); + }); + }); +}); + +describe("distributeLibraryItemsOnSquareGrid()", () => { + it("should distribute items on a grid", async () => { + const createLibraryItem = ( + elements: ExcalidrawGenericElement[], + ): LibraryItem => { + return { + id: `id-${Date.now()}`, + elements, + status: "unpublished", + created: Date.now(), + }; + }; + + const PADDING = 50; + + const el1 = API.createElement({ + id: "id1", + width: 100, + height: 100, + x: 0, + y: 0, + }); + + const el2 = API.createElement({ + id: "id2", + width: 100, + height: 80, + x: -100, + y: -50, + }); + + const el3 = API.createElement({ + id: "id3", + width: 40, + height: 50, + x: -100, + y: -50, + }); + + const el4 = API.createElement({ + id: "id4", + width: 50, + height: 50, + x: 0, + y: 0, + }); + + const el5 = API.createElement({ + id: "id5", + width: 70, + height: 100, + x: 40, + y: 0, + }); + + const libraryItems: LibraryItems = [ + createLibraryItem([el1]), + createLibraryItem([el2]), + createLibraryItem([el3]), + createLibraryItem([el4, el5]), + ]; + + const distributed = distributeLibraryItemsOnSquareGrid(libraryItems); + // assert the returned library items are flattened to elements + expect(distributed.length).toEqual( + libraryItems.map((x) => x.elements).flat().length, + ); + expect(distributed).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: el1.id, + x: 0, + y: 0, + }), + expect.objectContaining({ + id: el2.id, + x: + el1.width + + PADDING + + (getCommonBoundingBox([el4, el5]).width - el2.width) / 2, + y: Math.abs(el1.height - el2.height) / 2, + }), + expect.objectContaining({ + id: el3.id, + x: Math.abs(el1.width - el3.width) / 2, + y: + Math.max(el1.height, el2.height) + + PADDING + + Math.abs(el3.height - Math.max(el4.height, el5.height)) / 2, + }), + expect.objectContaining({ + id: el4.id, + x: Math.max(el1.width, el2.width) + PADDING, + y: Math.max(el1.height, el2.height) + PADDING, + }), + expect.objectContaining({ + id: el5.id, + x: Math.max(el1.width, el2.width) + PADDING + Math.abs(el5.x - el4.x), + y: + Math.max(el1.height, el2.height) + + PADDING + + Math.abs(el5.y - el4.y), + }), + ]), + ); + }); +}); |
