700 lines
20 KiB
TypeScript
700 lines
20 KiB
TypeScript
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());
|
|
});
|
|
});
|
|
});
|