snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
File diff suppressed because one or more lines are too long
699
packages/excalidraw/tests/scene/export.test.ts
Normal file
699
packages/excalidraw/tests/scene/export.test.ts
Normal file
@@ -0,0 +1,699 @@
|
||||
import { exportToCanvas, exportToSvg } from "@excalidraw/utils";
|
||||
|
||||
import {
|
||||
applyDarkModeFilter,
|
||||
BOUND_TEXT_PADDING,
|
||||
FONT_FAMILY,
|
||||
FRAME_STYLE,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawTextElement,
|
||||
FractionalIndex,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { prepareElementsForExport } from "../../data";
|
||||
import * as exportUtils from "../../scene/export";
|
||||
import {
|
||||
diamondFixture,
|
||||
ellipseFixture,
|
||||
rectangleWithLinkFixture,
|
||||
textFixture,
|
||||
} from "../fixtures/elementFixture";
|
||||
import { API } from "../helpers/api";
|
||||
|
||||
describe("exportToSvg", () => {
|
||||
const ELEMENT_HEIGHT = 100;
|
||||
const ELEMENT_WIDTH = 100;
|
||||
const ELEMENTS = [
|
||||
{
|
||||
...diamondFixture,
|
||||
height: ELEMENT_HEIGHT,
|
||||
width: ELEMENT_WIDTH,
|
||||
index: "a0",
|
||||
},
|
||||
{
|
||||
...ellipseFixture,
|
||||
height: ELEMENT_HEIGHT,
|
||||
width: ELEMENT_WIDTH,
|
||||
index: "a1",
|
||||
},
|
||||
{
|
||||
...textFixture,
|
||||
height: ELEMENT_HEIGHT,
|
||||
width: ELEMENT_WIDTH,
|
||||
index: "a2",
|
||||
},
|
||||
{
|
||||
...textFixture,
|
||||
fontFamily: FONT_FAMILY.Nunito, // test embedding external font
|
||||
height: ELEMENT_HEIGHT,
|
||||
width: ELEMENT_WIDTH,
|
||||
index: "a3",
|
||||
},
|
||||
] as NonDeletedExcalidrawElement[];
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: "#ffffff",
|
||||
files: {},
|
||||
};
|
||||
|
||||
it("with default arguments", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
DEFAULT_OPTIONS,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(svgElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("with a CJK font", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
[
|
||||
...ELEMENTS,
|
||||
{
|
||||
...textFixture,
|
||||
height: ELEMENT_HEIGHT,
|
||||
width: ELEMENT_WIDTH,
|
||||
text: "中国你好!这是一个测试。中国你好!日本こんにちは!これはテストです。한국 안녕하세요! 이것은 테스트입니다.",
|
||||
originalText:
|
||||
"中国你好!这是一个测试。中国你好!日本こんにちは!これはテストです。한국 안녕하세요! 이것은 테스트입니다.",
|
||||
index: "a4" as FractionalIndex,
|
||||
} as NonDeleted<ExcalidrawTextElement>,
|
||||
],
|
||||
DEFAULT_OPTIONS,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(svgElement).toMatchSnapshot();
|
||||
// extend the timeout, as it needs to first load the fonts from disk and then perform whole woff2 decode, subset and encode (without workers)
|
||||
}, 30_000);
|
||||
|
||||
it("with background color", async () => {
|
||||
const BACKGROUND_COLOR = "#abcdef";
|
||||
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
exportBackground: true,
|
||||
viewBackgroundColor: BACKGROUND_COLOR,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
expect(svgElement.querySelector("rect")).toHaveAttribute(
|
||||
"fill",
|
||||
BACKGROUND_COLOR,
|
||||
);
|
||||
});
|
||||
|
||||
it("with dark mode", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
exportWithDarkMode: true,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
const textElements = svgElement.querySelectorAll("text");
|
||||
expect(textElements.length).toBeGreaterThan(0);
|
||||
|
||||
textElements.forEach((textEl) => {
|
||||
// fill color should be inverted in dark mode
|
||||
expect(textEl.getAttribute("fill")).toBe(
|
||||
applyDarkModeFilter(textFixture.strokeColor),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("with exportPadding", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
exportPadding: 0,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
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", async () => {
|
||||
const SCALE = 2;
|
||||
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
exportPadding: 0,
|
||||
exportScale: SCALE,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
expect(svgElement).toHaveAttribute(
|
||||
"height",
|
||||
(ELEMENT_HEIGHT * SCALE).toString(),
|
||||
);
|
||||
expect(svgElement).toHaveAttribute(
|
||||
"width",
|
||||
(ELEMENT_WIDTH * SCALE).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("with exportEmbedScene", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
ELEMENTS,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
exportEmbedScene: true,
|
||||
},
|
||||
null,
|
||||
);
|
||||
expect(svgElement.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("with elements that have a link", async () => {
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
[rectangleWithLinkFixture] as NonDeletedExcalidrawElement[],
|
||||
DEFAULT_OPTIONS,
|
||||
null,
|
||||
);
|
||||
expect(svgElement.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// #11439: a perfectly horizontal/vertical arrow has a zero-size bounding box.
|
||||
// The bound-text "gap" mask must use userSpaceOnUse units, otherwise its
|
||||
// objectBoundingBox region collapses to zero area and the whole arrow line
|
||||
// disappears from the SVG export (only the label remains).
|
||||
it("keeps a horizontal arrow with a bound label visible (#11439)", async () => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow-11439",
|
||||
width: 200,
|
||||
height: 0,
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(200, 0)],
|
||||
boundElements: [{ type: "text", id: "label-11439" }],
|
||||
});
|
||||
const label = API.createElement({
|
||||
type: "text",
|
||||
id: "label-11439",
|
||||
text: "label",
|
||||
width: 50,
|
||||
height: 20,
|
||||
containerId: "arrow-11439",
|
||||
});
|
||||
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
[arrow, label] as NonDeletedExcalidrawElement[],
|
||||
DEFAULT_OPTIONS,
|
||||
null,
|
||||
);
|
||||
|
||||
const mask = svgElement.querySelector("mask");
|
||||
expect(mask).not.toBeNull();
|
||||
expect(mask?.getAttribute("maskUnits")).toBe("userSpaceOnUse");
|
||||
// a degenerate (objectBoundingBox) region would be zero-area here
|
||||
expect(Number(mask?.getAttribute("width"))).toBeGreaterThan(0);
|
||||
expect(Number(mask?.getAttribute("height"))).toBeGreaterThan(0);
|
||||
|
||||
// the masked arrow group still renders its line (not clipped away)
|
||||
expect(svgElement.querySelector("g[mask] path")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("cuts the same padded hole around an arrow label as the canvas", async () => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow-hole",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 200,
|
||||
height: 0,
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(200, 0)],
|
||||
boundElements: [{ type: "text", id: "label-hole" }],
|
||||
});
|
||||
const label = API.createElement({
|
||||
type: "text",
|
||||
id: "label-hole",
|
||||
text: "label",
|
||||
width: 50,
|
||||
height: 20,
|
||||
containerId: "arrow-hole",
|
||||
});
|
||||
|
||||
const svgElement = await exportUtils.exportToSvg(
|
||||
[arrow, label] as NonDeletedExcalidrawElement[],
|
||||
{ ...DEFAULT_OPTIONS, exportPadding: 0 },
|
||||
null,
|
||||
);
|
||||
|
||||
// the mask's first rect is the visible region, sized to the arrow plus
|
||||
// its offset within the export; the second is the hole cut for the label
|
||||
const [visible, hole] = Array.from(
|
||||
svgElement.querySelectorAll("mask rect"),
|
||||
);
|
||||
expect(hole).toBeDefined();
|
||||
const offsetX = Number(visible.getAttribute("width")) - 100 - arrow.width;
|
||||
const offsetY = Number(visible.getAttribute("height")) - 100 - arrow.height;
|
||||
|
||||
// the label sits centered on the arrow's midpoint; the hole is its box
|
||||
// grown by the padding on every side, in the mask's (arrow-relative)
|
||||
// coordinates
|
||||
const labelLeft = arrow.x + arrow.width / 2 - label.width / 2;
|
||||
const labelTop = arrow.y - label.height / 2;
|
||||
expect(Number(hole.getAttribute("x"))).toBe(
|
||||
offsetX + labelLeft - arrow.x - BOUND_TEXT_PADDING,
|
||||
);
|
||||
expect(Number(hole.getAttribute("y"))).toBe(
|
||||
offsetY + labelTop - arrow.y - BOUND_TEXT_PADDING,
|
||||
);
|
||||
expect(Number(hole.getAttribute("width"))).toBe(
|
||||
label.width + BOUND_TEXT_PADDING * 2,
|
||||
);
|
||||
expect(Number(hole.getAttribute("height"))).toBe(
|
||||
label.height + BOUND_TEXT_PADDING * 2,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exportToCanvas", () => {
|
||||
it("does not paint over elements beneath a labeled arrow's label (#11591)", async () => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect-11591",
|
||||
x: 0,
|
||||
y: -50,
|
||||
width: 200,
|
||||
height: 100,
|
||||
backgroundColor: "#ffc9c9",
|
||||
fillStyle: "solid",
|
||||
});
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
id: "arrow-11591",
|
||||
x: -50,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 0,
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(300, 0)],
|
||||
boundElements: [{ type: "text", id: "label-11591" }],
|
||||
});
|
||||
const label = API.createElement({
|
||||
type: "text",
|
||||
id: "label-11591",
|
||||
text: "label",
|
||||
x: 75,
|
||||
y: -10,
|
||||
width: 50,
|
||||
height: 20,
|
||||
containerId: "arrow-11591",
|
||||
});
|
||||
|
||||
const canvas = await exportToCanvas({
|
||||
elements: [rectangle, arrow, label],
|
||||
files: null,
|
||||
appState: { exportBackground: true, viewBackgroundColor: "#ffffff" },
|
||||
});
|
||||
|
||||
const context = canvas.getContext("2d") as any;
|
||||
const events = context.__getEvents();
|
||||
|
||||
// the label "hole" must be cut out of the arrow's own strokes via an
|
||||
// even-odd clip — erasing (clearRect) or painting the background color
|
||||
// (fillRect) over the hole rect would destroy elements rendered beneath
|
||||
// the arrow
|
||||
expect(
|
||||
events.some(
|
||||
(event: any) =>
|
||||
event.type === "clip" && event.props.fillRule === "evenodd",
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const holeWidth = label.width + BOUND_TEXT_PADDING * 2;
|
||||
const holeHeight = label.height + BOUND_TEXT_PADDING * 2;
|
||||
expect(
|
||||
events.filter(
|
||||
(event: any) =>
|
||||
(event.type === "fillRect" || event.type === "clearRect") &&
|
||||
event.props.width === holeWidth &&
|
||||
event.props.height === holeHeight,
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exporting frames", () => {
|
||||
const getFrameNameHeight = (exportType: "canvas" | "svg") => {
|
||||
const height =
|
||||
FRAME_STYLE.nameFontSize * FRAME_STYLE.nameLineHeight +
|
||||
FRAME_STYLE.nameOffsetY;
|
||||
// canvas truncates dimensions to integers
|
||||
if (exportType === "canvas") {
|
||||
return Math.trunc(height);
|
||||
}
|
||||
return height;
|
||||
};
|
||||
|
||||
// a few tests with exportToCanvas (where we can't inspect elements)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("exportToCanvas", () => {
|
||||
it("exporting canvas with a single frame shouldn't crop if not exporting frame directly", async () => {
|
||||
const elements = [
|
||||
API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}),
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 100,
|
||||
y: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
const canvas = await exportToCanvas({
|
||||
elements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
});
|
||||
|
||||
expect(canvas.width).toEqual(200);
|
||||
expect(canvas.height).toEqual(100 + getFrameNameHeight("canvas"));
|
||||
});
|
||||
|
||||
it("exporting canvas with a single frame should crop when exporting frame directly", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const elements = [
|
||||
frame,
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 100,
|
||||
y: 0,
|
||||
}),
|
||||
];
|
||||
|
||||
const canvas = await exportToCanvas({
|
||||
elements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame: frame,
|
||||
});
|
||||
|
||||
expect(canvas.width).toEqual(frame.width);
|
||||
expect(canvas.height).toEqual(frame.height);
|
||||
});
|
||||
});
|
||||
|
||||
// exportToSvg (so we can test for element existence)
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("exportToSvg", () => {
|
||||
it("exporting frame should include overlapping elements, but crop to frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const frameChild = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 50,
|
||||
frameId: frame.id,
|
||||
});
|
||||
const rectOverlapping = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 50,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: [rectOverlapping, frame, frameChild],
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame: frame,
|
||||
});
|
||||
|
||||
// frame itself isn't exported
|
||||
expect(svg.querySelector(`[data-id="${frame.id}"]`)).toBeNull();
|
||||
// frame child is exported
|
||||
expect(svg.querySelector(`[data-id="${frameChild.id}"]`)).not.toBeNull();
|
||||
// overlapping element is exported
|
||||
expect(
|
||||
svg.querySelector(`[data-id="${rectOverlapping.id}"]`),
|
||||
).not.toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(frame.height.toString());
|
||||
});
|
||||
|
||||
it("should filter non-overlapping elements when exporting a frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const frameChild = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 50,
|
||||
frameId: frame.id,
|
||||
});
|
||||
const elementOutside = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 200,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: [frameChild, frame, elementOutside],
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame: frame,
|
||||
});
|
||||
|
||||
// frame itself isn't exported
|
||||
expect(svg.querySelector(`[data-id="${frame.id}"]`)).toBeNull();
|
||||
// frame child is exported
|
||||
expect(svg.querySelector(`[data-id="${frameChild.id}"]`)).not.toBeNull();
|
||||
// non-overlapping element is not exported
|
||||
expect(svg.querySelector(`[data-id="${elementOutside.id}"]`)).toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(frame.height.toString());
|
||||
});
|
||||
|
||||
it("should export multiple frames when selected, excluding overlapping elements", async () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 200,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const frame1Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 50,
|
||||
frameId: frame1.id,
|
||||
});
|
||||
const frame2Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 200,
|
||||
y: 0,
|
||||
frameId: frame2.id,
|
||||
});
|
||||
const frame2Overlapping = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 350,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
// low-level exportToSvg api expects elements to be pre-filtered, so let's
|
||||
// use the filter we use in the editor
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
[frame1Child, frame1, frame2Child, frame2, frame2Overlapping],
|
||||
{
|
||||
selectedElementIds: { [frame1.id]: true, [frame2.id]: true },
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: exportedElements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame,
|
||||
});
|
||||
|
||||
// frames themselves should be exported when multiple frames selected
|
||||
expect(svg.querySelector(`[data-id="${frame1.id}"]`)).not.toBeNull();
|
||||
expect(svg.querySelector(`[data-id="${frame2.id}"]`)).not.toBeNull();
|
||||
// children should be epxorted
|
||||
expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull();
|
||||
expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).not.toBeNull();
|
||||
// overlapping elements or non-overlapping elements should not be exported
|
||||
expect(
|
||||
svg.querySelector(`[data-id="${frame2Overlapping.id}"]`),
|
||||
).toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(
|
||||
(frame2.x + frame2.width).toString(),
|
||||
);
|
||||
expect(svg.getAttribute("height")).toBe(
|
||||
(frame2.y + frame2.height + getFrameNameHeight("svg")).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should render frame alone when not selected", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
// low-level exportToSvg api expects elements to be pre-filtered, so let's
|
||||
// use the filter we use in the editor
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
[frame],
|
||||
{
|
||||
selectedElementIds: {},
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: exportedElements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame,
|
||||
});
|
||||
|
||||
// frame itself isn't exported
|
||||
expect(svg.querySelector(`[data-id="${frame.id}"]`)).not.toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(
|
||||
(frame.height + getFrameNameHeight("svg")).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not export frame-overlapping elements belonging to different frame", async () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 200,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const frame1Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 150,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 50,
|
||||
frameId: frame1.id,
|
||||
});
|
||||
const frame2Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 150,
|
||||
height: 100,
|
||||
x: 50,
|
||||
y: 0,
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
// low-level exportToSvg api expects elements to be pre-filtered, so let's
|
||||
// use the filter we use in the editor
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
[frame1Child, frame1, frame2Child, frame2],
|
||||
{
|
||||
selectedElementIds: { [frame1.id]: true },
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: exportedElements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame,
|
||||
});
|
||||
|
||||
// frame shouldn't be exported
|
||||
expect(svg.querySelector(`[data-id="${frame1.id}"]`)).toBeNull();
|
||||
// frame1 child should be epxorted
|
||||
expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull();
|
||||
// frame2 child should not be exported even if it physically overlaps with
|
||||
// frame1
|
||||
expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame1.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(frame1.height.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user