diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 0f4c1c40..5bb6d2ef 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -11,7 +11,7 @@ import { restore } from "../data/restore"; type ExportOpts = { elements: readonly ExcalidrawElement[]; appState?: Partial>; - getDimensions: ( + getDimensions?: ( width: number, height: number, ) => { width: number; height: number; scale: number }; @@ -83,7 +83,7 @@ export const exportToSvg = ({ appState = getDefaultAppState(), exportPadding, metadata, -}: ExportOpts & { +}: Omit & { exportPadding?: number; metadata?: string; }): SVGSVGElement => { diff --git a/src/tests/fixtures/diagramFixture.ts b/src/tests/fixtures/diagramFixture.ts new file mode 100644 index 00000000..e5019c9a --- /dev/null +++ b/src/tests/fixtures/diagramFixture.ts @@ -0,0 +1,31 @@ +import { + diamondFixture, + ellipseFixture, + rectangleFixture, +} from "./elementFixture"; + +export const diagramFixture = { + type: "excalidraw", + version: 2, + source: "https://excalidraw.com", + elements: [diamondFixture, ellipseFixture, rectangleFixture], + appState: { + viewBackgroundColor: "#ffffff", + gridSize: null, + }, +}; + +export const diagramFactory = ({ + overrides = {}, + elementOverrides = {}, +} = {}) => ({ + ...diagramFixture, + elements: [ + { ...diamondFixture, ...elementOverrides }, + { ...ellipseFixture, ...elementOverrides }, + { ...rectangleFixture, ...elementOverrides }, + ], + ...overrides, +}); + +export default diagramFixture; diff --git a/src/tests/fixtures/elementFixture.ts b/src/tests/fixtures/elementFixture.ts new file mode 100644 index 00000000..d3ec64a7 --- /dev/null +++ b/src/tests/fixtures/elementFixture.ts @@ -0,0 +1,37 @@ +import { ExcalidrawElement } from "../../element/types"; + +const elementBase: Omit = { + id: "vWrqOAfkind2qcm7LDAGZ", + x: 414, + y: 237, + width: 214, + height: 214, + angle: 0, + strokeColor: "#000000", + backgroundColor: "#15aabf", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + strokeSharpness: "sharp", + seed: 1041657908, + version: 120, + versionNonce: 1188004276, + isDeleted: false, + boundElementIds: null, +}; + +export const rectangleFixture: ExcalidrawElement = { + ...elementBase, + type: "rectangle", +}; +export const ellipseFixture: ExcalidrawElement = { + ...elementBase, + type: "ellipse", +}; +export const diamondFixture: ExcalidrawElement = { + ...elementBase, + type: "diamond", +}; diff --git a/src/tests/packages/__snapshots__/utils.test.ts.snap b/src/tests/packages/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000..3d40ee77 --- /dev/null +++ b/src/tests/packages/__snapshots__/utils.test.ts.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`exportToSvg with default arguments 1`] = ` +Object { + "collaborators": Map {}, + "currentChartType": "bar", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "hachure", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemLinearStrokeSharpness": "round", + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#000000", + "currentItemStrokeSharpness": "sharp", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 1, + "currentItemTextAlign": "left", + "cursorButton": "up", + "draggingElement": null, + "editingElement": null, + "editingGroupId": null, + "editingLinearElement": null, + "elementLocked": false, + "elementType": "selection", + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportPadding": undefined, + "exportWithDarkMode": false, + "fileHandle": null, + "gridSize": null, + "height": 768, + "isBindingEnabled": true, + "isLibraryOpen": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "metadata": undefined, + "multiElement": null, + "name": "name", + "offsetLeft": 0, + "offsetTop": 0, + "openMenu": null, + "pasteDialog": Object { + "data": null, + "shown": false, + }, + "previousSelectedElementIds": Object {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "selectedElementIds": Object {}, + "selectedGroupIds": Object {}, + "selectionElement": null, + "shouldAddWatermark": false, + "shouldCacheIgnoreZoom": false, + "showHelpDialog": false, + "showStats": false, + "startBoundElement": null, + "suggestedBindings": Array [], + "theme": "light", + "toastMessage": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": Object { + "translation": Object { + "x": 0, + "y": 0, + }, + "value": 1, + }, +} +`; diff --git a/src/tests/packages/utils.test.ts b/src/tests/packages/utils.test.ts new file mode 100644 index 00000000..1eaad109 --- /dev/null +++ b/src/tests/packages/utils.test.ts @@ -0,0 +1,121 @@ +import * as utils from "../../packages/utils"; +import { diagramFactory } from "../fixtures/diagramFixture"; +import * as mockedSceneExportUtils from "../../scene/export"; + +jest.mock("../../scene/export", () => ({ + __esmodule: true, + ...jest.requireActual("../../scene/export"), + exportToSvg: jest.fn(), +})); + +describe("exportToCanvas", () => { + const EXPORT_PADDING = 10; + + it("with default arguments", () => { + const canvas = utils.exportToCanvas({ + ...diagramFactory({ elementOverrides: { width: 100, height: 100 } }), + }); + + expect(canvas.width).toBe(100 + 2 * EXPORT_PADDING); + expect(canvas.height).toBe(100 + 2 * EXPORT_PADDING); + }); + + it("when custom width and height", () => { + const canvas = utils.exportToCanvas({ + ...diagramFactory({ elementOverrides: { width: 100, height: 100 } }), + getDimensions: () => ({ width: 200, height: 200, scale: 1 }), + }); + + expect(canvas.width).toBe(200); + expect(canvas.height).toBe(200); + }); +}); + +describe("exportToBlob", () => { + describe("mime type", () => { + afterEach(jest.restoreAllMocks); + + it("should change image/jpg to image/jpeg", async () => { + const blob = await utils.exportToBlob({ + ...diagramFactory(), + getDimensions: (width, height) => ({ width, height, scale: 1 }), + mimeType: "image/jpg", + }); + expect(blob?.type).toBe("image/jpeg"); + }); + + it("should default to image/png", async () => { + const blob = await utils.exportToBlob({ + ...diagramFactory(), + }); + expect(blob?.type).toBe("image/png"); + }); + + it("should warn when using quality with image/png", async () => { + const consoleSpy = jest + .spyOn(console, "warn") + .mockImplementationOnce(() => void 0); + + await utils.exportToBlob({ + ...diagramFactory(), + mimeType: "image/png", + quality: 1, + }); + + expect(consoleSpy).toHaveBeenCalledWith( + '"quality" will be ignored for "image/png" mimeType', + ); + }); + }); +}); + +describe("exportToSvg", () => { + const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock; + const passedElements = () => mockedExportUtil.mock.calls[0][0]; + const passedOptions = () => mockedExportUtil.mock.calls[0][1]; + + afterEach(jest.resetAllMocks); + + it("with default arguments", () => { + utils.exportToSvg({ + ...diagramFactory({ + overrides: { appState: void 0 }, + }), + }); + + const passedOptionsWhenDefault = { + ...passedOptions(), + // To avoid varying snapshots + name: "name", + }; + + expect(passedElements().length).toBe(3); + expect(passedOptionsWhenDefault).toMatchSnapshot(); + }); + + it("with deleted elements", () => { + utils.exportToSvg({ + ...diagramFactory({ + overrides: { appState: void 0 }, + elementOverrides: { isDeleted: true }, + }), + }); + + expect(passedElements().length).toBe(0); + }); + + it("with exportPadding and metadata", () => { + const METADATA = "some metada"; + + utils.exportToSvg({ + ...diagramFactory({ overrides: { appState: { name: "diagram name" } } }), + exportPadding: 0, + metadata: METADATA, + }); + + expect(passedElements().length).toBe(3); + expect(passedOptions()).toEqual( + expect.objectContaining({ exportPadding: 0, metadata: METADATA }), + ); + }); +}); diff --git a/src/tests/scene/__snapshots__/export.test.ts.snap b/src/tests/scene/__snapshots__/export.test.ts.snap new file mode 100644 index 00000000..56dfa7b7 --- /dev/null +++ b/src/tests/scene/__snapshots__/export.test.ts.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`exportToSvg with default arguments 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/src/tests/scene/export.test.ts b/src/tests/scene/export.test.ts new file mode 100644 index 00000000..e1ce7a7d --- /dev/null +++ b/src/tests/scene/export.test.ts @@ -0,0 +1,96 @@ +import { NonDeletedExcalidrawElement } from "../../element/types"; +import * as exportUtils from "../../scene/export"; +import { diamondFixture, ellipseFixture } from "../fixtures/elementFixture"; + +describe("exportToSvg", () => { + const ELEMENT_HEIGHT = 100; + const ELEMENT_WIDTH = 100; + const ELEMENTS = [ + { ...diamondFixture, height: ELEMENT_HEIGHT, width: ELEMENT_WIDTH }, + { ...ellipseFixture, height: ELEMENT_HEIGHT, width: ELEMENT_WIDTH }, + ] as NonDeletedExcalidrawElement[]; + + const DEFAULT_OPTIONS = { + exportBackground: false, + viewBackgroundColor: "#ffffff", + shouldAddWatermark: false, + }; + + it("with default arguments", () => { + const svgElement = exportUtils.exportToSvg(ELEMENTS, DEFAULT_OPTIONS); + + expect(svgElement).toMatchSnapshot(); + }); + + it("with background color", () => { + const BACKGROUND_COLOR = "#abcdef"; + + const svgElement = exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + exportBackground: true, + viewBackgroundColor: BACKGROUND_COLOR, + }); + + expect(svgElement.querySelector("rect")).toHaveAttribute( + "fill", + BACKGROUND_COLOR, + ); + }); + + it("with watermark", () => { + const svgElement = exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + shouldAddWatermark: true, + }); + + expect(svgElement.querySelector("text")?.textContent).toMatchInlineSnapshot( + `"Made with Excalidraw"`, + ); + }); + + it("with dark mode", () => { + const svgElement = exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + exportWithDarkMode: true, + }); + + expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot( + `"themeFilter"`, + ); + }); + + it("with exportPadding, metadata", () => { + const svgElement = exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + exportPadding: 0, + metadata: "some metadata", + }); + + expect(svgElement.innerHTML).toMatch(/some metadata/); + expect(svgElement).toHaveAttribute("height", ELEMENT_HEIGHT.toString()); + expect(svgElement).toHaveAttribute("width", ELEMENT_WIDTH.toString()); + expect(svgElement).toHaveAttribute( + "viewBox", + `0 0 ${ELEMENT_WIDTH} ${ELEMENT_HEIGHT}`, + ); + }); + + it("with scale", () => { + const SCALE = 2; + + const svgElement = exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + exportPadding: 0, + scale: SCALE, + }); + + expect(svgElement).toHaveAttribute( + "height", + (ELEMENT_HEIGHT * SCALE).toString(), + ); + expect(svgElement).toHaveAttribute( + "width", + (ELEMENT_WIDTH * SCALE).toString(), + ); + }); +});