snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s

This commit is contained in:
upstream-mirror
2026-09-16 05:31:04 +05:00
commit 1a8d07ca6c
1286 changed files with 360144 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exportToSvg > with default arguments 1`] = `
{
"activeEmbeddable": null,
"activeLockedId": null,
"activeTool": {
"customType": null,
"fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
},
"bindMode": "orbit",
"bindingPreference": "enabled",
"boxSelectionMode": "contain",
"collaborators": Map {},
"colorTopPicks": {
"bucketFill": null,
"elementBackground": null,
"elementStroke": null,
"stickyNoteBackground": null,
"stickyNoteStroke": null,
},
"contextMenu": null,
"croppingElementId": null,
"currentHoveredFontFamily": null,
"currentItemArrowType": "round",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemRoundness": "sharp",
"currentItemStartArrowhead": null,
"currentItemStickynoteBackgroundColor": "#ffdf6b",
"currentItemStickynoteStrokeColor": "#1e1e1e",
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
"exportBackground": true,
"exportEmbedScene": false,
"exportPadding": undefined,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"hoveredArrowTextAnchor": null,
"hoveredElementIds": {},
"inputDevice": "auto",
"isBindingEnabled": true,
"isCropping": false,
"isLoading": false,
"isMidpointSnappingEnabled": true,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"lockedMultiSelections": {},
"multiElement": null,
"name": "name",
"newElement": null,
"objectsSnapModeEnabled": false,
"openDialog": null,
"openMenu": null,
"openPopup": null,
"openSidebar": null,
"originSnapOffset": {
"x": 0,
"y": 0,
},
"penDetected": false,
"penMode": false,
"preferredSelectionTool": {
"initialized": false,
"type": "selection",
},
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollConstraints": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"searchMatches": null,
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHyperlinkPopup": false,
"showWelcomeScreen": false,
"snapLines": [],
"stats": {
"open": false,
"panels": 3,
},
"suggestedBinding": null,
"theme": "light",
"toast": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"zenModeEnabled": false,
"zoom": {
"value": 1,
},
}
`;

View File

@@ -0,0 +1,102 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exportToSvg > with default arguments 1`] = `
{
"activeEmbeddable": null,
"activeTool": {
"customType": null,
"lastActiveTool": null,
"locked": false,
"type": "selection",
},
"collaborators": Map {},
"contextMenu": null,
"currentChartType": "bar",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFontFamily": 1,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemRoundness": "round",
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidthKey": "medium",
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"elementsToHighlight": null,
"errorMessage": null,
"exportBackground": true,
"exportEmbedScene": false,
"exportPadding": undefined,
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"followedBy": Set {},
"frameRendering": {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"isBindingEnabled": true,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "name",
"objectsSnapModeEnabled": false,
"openDialog": null,
"openMenu": null,
"openPopup": null,
"openSidebar": null,
"originSnapOffset": {
"x": 0,
"y": 0,
},
"pasteDialog": {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElementId": null,
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": false,
"snapLines": [],
"startBoundElement": null,
"suggestedBindings": [],
"theme": "light",
"toast": null,
"userToFollow": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"zenModeEnabled": false,
"zoom": {
"value": 1,
},
}
`;

View File

@@ -0,0 +1,219 @@
import { MIME_TYPES } from "@excalidraw/common";
import * as clipboardModule from "@excalidraw/excalidraw/clipboard";
import * as imageModule from "@excalidraw/excalidraw/data/image";
import * as jsonModule from "@excalidraw/excalidraw/data/json";
import * as mockedSceneExportUtils from "@excalidraw/excalidraw/scene/export";
import { diagramFactory } from "@excalidraw/excalidraw/tests/fixtures/diagramFixture";
import { vi } from "vitest";
import * as utils from "../src";
const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
describe("exportToCanvas", async () => {
const EXPORT_PADDING = 10;
it("with default arguments", async () => {
const canvas = await 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", async () => {
const canvas = await 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", async () => {
describe("mime type", () => {
it("should change image/jpg to image/jpeg", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
getDimensions: (width, height) => ({ width, height, scale: 1 }),
// testing typo in MIME type (jpg → jpeg)
mimeType: "image/jpg",
appState: {
exportBackground: true,
},
});
expect(blob?.type).toBe(MIME_TYPES.jpg);
});
it("should default to image/png", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
});
expect(blob?.type).toBe(MIME_TYPES.png);
});
it("should warn when using quality with image/png", async () => {
const consoleSpy = vi
.spyOn(console, "warn")
.mockImplementationOnce(() => void 0);
await utils.exportToBlob({
...diagramFactory(),
mimeType: MIME_TYPES.png,
quality: 1,
});
expect(consoleSpy).toHaveBeenCalledWith(
`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
);
});
});
});
describe("exportToSvg", () => {
const passedElements = () => exportToSvgSpy.mock.calls[0][0];
const passedOptions = () => exportToSvgSpy.mock.calls[0][1];
afterEach(() => {
vi.clearAllMocks();
});
it("with default arguments", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: { appState: void 0 },
}),
});
const passedOptionsWhenDefault = {
...passedOptions(),
// To avoid varying snapshots
name: "name",
};
expect(passedElements().length).toBe(3);
expect(passedOptionsWhenDefault).toMatchSnapshot();
});
// Regression test: when all elements are deleted, exportToSvg should pass
// zero elements to the lower-level export (line 184-188 of export.ts calls
// getNonDeletedElements which filters them out).
it("with deleted elements", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: { appState: void 0 },
elementOverrides: { isDeleted: true },
}),
});
expect(passedElements().length).toBe(0);
});
it("with exportPadding", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: { appState: { name: "diagram name" } },
}),
exportPadding: 0,
});
expect(passedElements().length).toBe(3);
expect(passedOptions()).toEqual(
expect.objectContaining({ exportPadding: 0 }),
);
});
it("with exportEmbedScene", async () => {
await utils.exportToSvg({
...diagramFactory({
overrides: {
appState: { name: "diagram name", exportEmbedScene: true },
},
}),
});
expect(passedElements().length).toBe(3);
expect(passedOptions().exportEmbedScene).toBe(true);
});
});
// Elements persisted before `created` existed omit it (modeled with the cast
// below, as the declared input type is a complete element). Every public
// export entry point restores its input, so such elements must reach the
// lower-level code restored, with `created: null`.
describe("elements lacking creation metadata", () => {
const legacyDiagram = () => {
const { elements, ...rest } = diagramFactory({
overrides: { appState: void 0 },
});
return {
...rest,
elements: elements.map(
({ created, ...element }) => element,
) as unknown as typeof elements,
};
};
const createdOf = (elements: readonly { created: number | null }[]) =>
elements.map((element) => element.created);
afterEach(() => {
vi.restoreAllMocks();
});
it("exportToSvg and exportToCanvas restore the elements", async () => {
const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
const diagram = legacyDiagram();
await utils.exportToSvg(diagram);
expect(createdOf(exportToSvgSpy.mock.calls[0][0])).toEqual([
null,
null,
null,
]);
expect(diagram.elements[0]).not.toHaveProperty("created");
const exportToCanvasSpy = vi.spyOn(
mockedSceneExportUtils,
"exportToCanvas",
);
const canvas = await utils.exportToCanvas(diagram);
expect(createdOf(exportToCanvasSpy.mock.calls[0][0])).toEqual([
null,
null,
null,
]);
expect(canvas.width).toBeGreaterThan(0);
});
it("exportToBlob embeds the restored scene into png metadata", async () => {
const serializeSpy = vi.spyOn(jsonModule, "serializeAsJSON");
vi.spyOn(imageModule, "encodePngMetadata").mockImplementation(
async ({ blob }) => blob,
);
await utils.exportToBlob({
...legacyDiagram(),
mimeType: MIME_TYPES.png,
appState: { exportEmbedScene: true },
});
expect(serializeSpy).toHaveBeenCalledTimes(1);
expect(createdOf(serializeSpy.mock.calls[0][0])).toEqual([
null,
null,
null,
]);
});
it("exportToClipboard copies the restored elements as json", async () => {
const copySpy = vi
.spyOn(clipboardModule, "copyToClipboard")
.mockResolvedValue(undefined);
await utils.exportToClipboard({ ...legacyDiagram(), type: "json" });
expect(copySpy).toHaveBeenCalledTimes(1);
expect(createdOf(copySpy.mock.calls[0][0])).toEqual([null, null, null]);
});
});

View File

@@ -0,0 +1,146 @@
import {
pointFrom,
lineSegment,
polygon,
pointOnLineSegment,
polygonIncludesPoint,
segmentsIntersectAt,
} from "@excalidraw/math";
import type {
GlobalPoint,
LineSegment,
Polygon,
Radians,
} from "@excalidraw/math";
import { pointInEllipse, pointOnEllipse, type Ellipse } from "../src/shape";
describe("point and line", () => {
// const l: Line<GlobalPoint> = line(point(1, 0), point(1, 2));
// it("point on left or right of line", () => {
// expect(pointLeftofLine(point(0, 1), l)).toBe(true);
// expect(pointLeftofLine(point(1, 1), l)).toBe(false);
// expect(pointLeftofLine(point(2, 1), l)).toBe(false);
// expect(pointRightofLine(point(0, 1), l)).toBe(false);
// expect(pointRightofLine(point(1, 1), l)).toBe(false);
// expect(pointRightofLine(point(2, 1), l)).toBe(true);
// });
const s: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 0),
pointFrom(1, 2),
);
it("point on the line", () => {
expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
});
});
describe("point and polygon", () => {
it("point in polygon", () => {
const poly: Polygon<GlobalPoint> = polygon(
pointFrom(0, 0),
pointFrom(2, 0),
pointFrom(2, 2),
pointFrom(0, 2),
);
expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
});
});
describe("point and ellipse", () => {
const ellipse: Ellipse<GlobalPoint> = {
center: pointFrom(0, 0),
angle: 0 as Radians,
halfWidth: 2,
halfHeight: 1,
};
it("point on ellipse", () => {
[
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointOnEllipse(p, ellipse)).toBe(true);
});
expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
});
it("point in ellipse", () => {
[
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointInEllipse(p, ellipse)).toBe(true);
});
expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
});
});
describe("line and line", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 4),
pointFrom(3, 4),
);
const lineB: LineSegment<GlobalPoint> = lineSegment(
pointFrom(2, 1),
pointFrom(2, 7),
);
const lineC: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineD: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineE: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 9),
pointFrom(3, 9),
);
const lineF: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 2),
pointFrom(3, 4),
);
const lineG: LineSegment<GlobalPoint> = lineSegment(
pointFrom(0, 1),
pointFrom(2, 3),
);
it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
});
});

View File

@@ -0,0 +1,70 @@
import { decodePngMetadata } from "@excalidraw/excalidraw/data/image";
import { decodeSvgBase64Payload } from "@excalidraw/excalidraw/scene/export";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import * as utils from "../src";
// NOTE this test file is using the actual API, unmocked. Hence splitting it
// from the other test file, because I couldn't figure out how to test
// mocked and unmocked API in the same file.
describe("embedding scene data", () => {
describe("exportToSvg", () => {
it("embedding scene data shouldn't modify them", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const ellipse = API.createElement({ type: "ellipse" });
const sourceElements = [rectangle, ellipse];
const svgNode = await utils.exportToSvg({
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridModeEnabled: false,
exportEmbedScene: true,
},
files: null,
});
const svg = svgNode.outerHTML;
const parsedString = decodeSvgBase64Payload({ svg });
const importedData: ImportedDataState = JSON.parse(parsedString);
expect(sourceElements.map((x) => x.id)).toEqual(
importedData.elements?.map((el) => el.id),
);
});
});
// skipped because we can't test png encoding right now
// (canvas.toBlob not supported in jsdom)
describe.skip("exportToBlob", () => {
it("embedding scene data shouldn't modify them", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const ellipse = API.createElement({ type: "ellipse" });
const sourceElements = [rectangle, ellipse];
const blob = await utils.exportToBlob({
mimeType: "image/png",
elements: sourceElements,
appState: {
viewBackgroundColor: "#ffffff",
gridModeEnabled: false,
exportEmbedScene: true,
},
files: null,
});
const parsedString = await decodePngMetadata(blob);
const importedData: ImportedDataState = JSON.parse(parsedString);
expect(sourceElements.map((x) => x.id)).toEqual(
importedData.elements?.map((el) => el.id),
);
});
});
});