You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
|
import * as exportUtils from "../../scene/export";
|
|
import {
|
|
diamondFixture,
|
|
ellipseFixture,
|
|
rectangleWithLinkFixture,
|
|
} from "../fixtures/elementFixture";
|
|
import { API } from "../helpers/api";
|
|
import { exportToCanvas, exportToSvg } from "../../../utils";
|
|
import { FRAME_STYLE } from "../../constants";
|
|
import { prepareElementsForExport } from "../../data";
|
|
|
|
describe("exportToSvg", () => {
|
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
|
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",
|
|
},
|
|
] 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 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,
|
|
);
|
|
|
|
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
|
|
'"_themeFilter_1883f3"',
|
|
);
|
|
});
|
|
|
|
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],
|
|
DEFAULT_OPTIONS,
|
|
null,
|
|
);
|
|
expect(svgElement.innerHTML).toMatchSnapshot();
|
|
});
|
|
});
|
|
|
|
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());
|
|
});
|
|
});
|
|
});
|