aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/tests/export.test.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/tests/export.test.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/tests/export.test.tsx')
-rw-r--r--packages/excalidraw/tests/export.test.tsx197
1 files changed, 197 insertions, 0 deletions
diff --git a/packages/excalidraw/tests/export.test.tsx b/packages/excalidraw/tests/export.test.tsx
new file mode 100644
index 0000000..3547b29
--- /dev/null
+++ b/packages/excalidraw/tests/export.test.tsx
@@ -0,0 +1,197 @@
+import React from "react";
+import { render, waitFor } from "./test-utils";
+import { Excalidraw } from "../index";
+import { API } from "./helpers/api";
+import { encodePngMetadata } from "../data/image";
+import { serializeAsJSON } from "../data/json";
+import {
+ decodeSvgBase64Payload,
+ encodeSvgBase64Payload,
+ exportToSvg,
+} from "../scene/export";
+import type { FileId } from "../element/types";
+import { getDataURL } from "../data/blob";
+import { getDefaultAppState } from "../appState";
+import { SVG_NS } from "../constants";
+
+const { h } = window;
+
+const testElements = [
+ {
+ ...API.createElement({
+ type: "text",
+ id: "A",
+ text: "😀",
+ }),
+ // can't get jsdom text measurement to work so this is a temp hack
+ // to ensure the element isn't stripped as invisible
+ width: 16,
+ height: 16,
+ },
+];
+
+// tiny polyfill for TextDecoder.decode on which we depend
+Object.defineProperty(window, "TextDecoder", {
+ value: class TextDecoder {
+ decode(ab: ArrayBuffer) {
+ return new Uint8Array(ab).reduce(
+ (acc, c) => acc + String.fromCharCode(c),
+ "",
+ );
+ }
+ },
+});
+
+describe("export", () => {
+ beforeEach(async () => {
+ await render(<Excalidraw />);
+ });
+
+ it("export embedded png and reimport", async () => {
+ const pngBlob = await API.loadFile("./fixtures/smiley.png");
+ const pngBlobEmbedded = await encodePngMetadata({
+ blob: pngBlob,
+ metadata: serializeAsJSON(testElements, h.state, {}, "local"),
+ });
+ await API.drop(pngBlobEmbedded);
+
+ await waitFor(() => {
+ expect(h.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "😀" }),
+ ]);
+ });
+ });
+
+ it("test encoding/decoding scene for SVG export", async () => {
+ const metadataElement = document.createElementNS(SVG_NS, "metadata");
+
+ encodeSvgBase64Payload({
+ metadataElement,
+ payload: serializeAsJSON(testElements, h.state, {}, "local"),
+ });
+
+ const decoded = JSON.parse(
+ decodeSvgBase64Payload({ svg: metadataElement.innerHTML }),
+ );
+ expect(decoded.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "😀" }),
+ ]);
+ });
+
+ it("export svg-embedded scene", async () => {
+ const svg = await exportToSvg(
+ testElements,
+ { ...getDefaultAppState(), exportEmbedScene: true },
+ {},
+ );
+ const svgText = svg.outerHTML;
+
+ expect(svgText).toMatchSnapshot(`svg-embdedded scene export output`);
+ });
+
+ it("import embedded png (legacy v1)", async () => {
+ await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
+ await waitFor(() => {
+ expect(h.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "test" }),
+ ]);
+ });
+ });
+
+ it("import embedded png (v2)", async () => {
+ await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
+ await waitFor(() => {
+ expect(h.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "😀" }),
+ ]);
+ });
+ });
+
+ it("import embedded svg (legacy v1)", async () => {
+ await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
+ await waitFor(() => {
+ expect(h.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "test" }),
+ ]);
+ });
+ });
+
+ it("import embedded svg (v2)", async () => {
+ await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
+ await waitFor(() => {
+ expect(h.elements).toEqual([
+ expect.objectContaining({ type: "text", text: "😀" }),
+ ]);
+ });
+ });
+
+ it("exporting svg containing transformed images", async () => {
+ const normalizeAngle = (angle: number) => (angle / 180) * Math.PI;
+
+ const elements = [
+ API.createElement({
+ type: "image",
+ fileId: "file_A",
+ x: 0,
+ y: 0,
+ scale: [1, 1],
+ width: 100,
+ height: 100,
+ angle: normalizeAngle(315),
+ }),
+ API.createElement({
+ type: "image",
+ fileId: "file_A",
+ x: 100,
+ y: 0,
+ scale: [-1, 1],
+ width: 50,
+ height: 50,
+ angle: normalizeAngle(45),
+ }),
+ API.createElement({
+ type: "image",
+ fileId: "file_A",
+ x: 0,
+ y: 100,
+ scale: [1, -1],
+ width: 100,
+ height: 100,
+ angle: normalizeAngle(45),
+ }),
+ API.createElement({
+ type: "image",
+ fileId: "file_A",
+ x: 100,
+ y: 100,
+ scale: [-1, -1],
+ width: 50,
+ height: 50,
+ angle: normalizeAngle(315),
+ }),
+ ];
+ const appState = { ...getDefaultAppState(), exportBackground: false };
+ const files = {
+ file_A: {
+ id: "file_A" as FileId,
+ dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")),
+ mimeType: "image/png",
+ created: Date.now(),
+ lastRetrieved: Date.now(),
+ },
+ } as const;
+
+ const svg = await exportToSvg(elements, appState, files);
+
+ const svgText = svg.outerHTML;
+
+ // expect 1 <image> element (deduped)
+ expect(svgText.match(/<image/g)?.length).toBe(1);
+ // expect 4 <use> elements (one for each excalidraw image element)
+ expect(svgText.match(/<use/g)?.length).toBe(4);
+
+ // in case of regressions, save the SVG to a file and visually compare to:
+ // src/tests/fixtures/svg-image-exporting-reference.svg
+ expect(svgText).toMatchSnapshot(`svg export output`);
+ });
+});