move the files to src and tests folders under utils

This commit is contained in:
Aakansha Doshi 2024-05-09 13:30:26 +05:30
parent a76999e9a2
commit 58567dd2b6
13 changed files with 55 additions and 42 deletions

View file

@ -0,0 +1,133 @@
import * as utils from "../src/export";
import { diagramFactory } from "../../excalidraw/tests/fixtures/diagramFixture";
import { vi } from "vitest";
import * as mockedSceneExportUtils from "../../excalidraw/scene/export";
import { MIME_TYPES } from "../../excalidraw/constants";
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", () => {
// afterEach(vi.restoreAllMocks);
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();
});
// FIXME the utils.exportToSvg no longer filters out deleted elements.
// It's already supposed to be passed non-deleted elements by we're not
// type-checking for it correctly.
it.skip("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);
});
});

View file

@ -0,0 +1,256 @@
import {
lineIntersectsLine,
lineRotate,
pointInEllipse,
pointInPolygon,
pointLeftofLine,
pointOnCurve,
pointOnEllipse,
pointOnLine,
pointOnPolygon,
pointOnPolyline,
pointRightofLine,
pointRotate,
} from "../src/geometry/geometry";
import type {
Curve,
Ellipse,
Line,
Point,
Polygon,
Polyline,
} from "../src/geometry/shape";
describe("point and line", () => {
const line: Line = [
[1, 0],
[1, 2],
];
it("point on left or right of line", () => {
expect(pointLeftofLine([0, 1], line)).toBe(true);
expect(pointLeftofLine([1, 1], line)).toBe(false);
expect(pointLeftofLine([2, 1], line)).toBe(false);
expect(pointRightofLine([0, 1], line)).toBe(false);
expect(pointRightofLine([1, 1], line)).toBe(false);
expect(pointRightofLine([2, 1], line)).toBe(true);
});
it("point on the line", () => {
expect(pointOnLine([0, 1], line)).toBe(false);
expect(pointOnLine([1, 1], line, 0)).toBe(true);
expect(pointOnLine([2, 1], line)).toBe(false);
});
});
describe("point and polylines", () => {
const polyline: Polyline = [
[
[1, 0],
[1, 2],
],
[
[1, 2],
[2, 2],
],
[
[2, 2],
[2, 1],
],
[
[2, 1],
[3, 1],
],
];
it("point on the line", () => {
expect(pointOnPolyline([1, 0], polyline)).toBe(true);
expect(pointOnPolyline([1, 2], polyline)).toBe(true);
expect(pointOnPolyline([2, 2], polyline)).toBe(true);
expect(pointOnPolyline([2, 1], polyline)).toBe(true);
expect(pointOnPolyline([3, 1], polyline)).toBe(true);
expect(pointOnPolyline([1, 1], polyline)).toBe(true);
expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
expect(pointOnPolyline([0, 1], polyline)).toBe(false);
expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
});
it("point on the line with rotation", () => {
const truePoints = [
[1, 0],
[1, 2],
[2, 2],
[2, 1],
[3, 1],
] as Point[];
truePoints.forEach((point) => {
const rotation = Math.random() * 360;
const rotatedPoint = pointRotate(point, rotation);
const rotatedPolyline: Polyline = polyline.map((line) =>
lineRotate(line, rotation, [0, 0]),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
});
const falsePoints = [
[0, 1],
[2.1, 1.5],
] as Point[];
falsePoints.forEach((point) => {
const rotation = Math.random() * 360;
const rotatedPoint = pointRotate(point, rotation);
const rotatedPolyline: Polyline = polyline.map((line) =>
lineRotate(line, rotation, [0, 0]),
);
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
});
});
});
describe("point and polygon", () => {
const polygon: Polygon = [
[10, 10],
[50, 10],
[50, 50],
[10, 50],
];
it("point on polygon", () => {
expect(pointOnPolygon([30, 10], polygon)).toBe(true);
expect(pointOnPolygon([50, 30], polygon)).toBe(true);
expect(pointOnPolygon([30, 50], polygon)).toBe(true);
expect(pointOnPolygon([10, 30], polygon)).toBe(true);
expect(pointOnPolygon([30, 30], polygon)).toBe(false);
expect(pointOnPolygon([30, 70], polygon)).toBe(false);
});
it("point in polygon", () => {
const polygon: Polygon = [
[0, 0],
[2, 0],
[2, 2],
[0, 2],
];
expect(pointInPolygon([1, 1], polygon)).toBe(true);
expect(pointInPolygon([3, 3], polygon)).toBe(false);
});
});
describe("point and curve", () => {
const curve: Curve = [
[1.4, 1.65],
[1.9, 7.9],
[5.9, 1.65],
[6.44, 4.84],
];
it("point on curve", () => {
expect(pointOnCurve(curve[0], curve)).toBe(true);
expect(pointOnCurve(curve[3], curve)).toBe(true);
expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
});
});
describe("point and ellipse", () => {
const ellipse: Ellipse = {
center: [0, 0],
angle: 0,
halfWidth: 2,
halfHeight: 1,
};
it("point on ellipse", () => {
[
[0, 1],
[0, -1],
[2, 0],
[-2, 0],
].forEach((point) => {
expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
});
expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([-1.4, 0.71], ellipse, 0.01)).toBe(true);
expect(pointOnEllipse([1.4, 0.7], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([1.4, 0.71], ellipse, 0.01)).toBe(true);
expect(pointOnEllipse([1, -0.86], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([1, -0.86], ellipse, 0.01)).toBe(true);
expect(pointOnEllipse([-1, -0.86], ellipse, 0.1)).toBe(true);
expect(pointOnEllipse([-1, -0.86], ellipse, 0.01)).toBe(true);
expect(pointOnEllipse([-1, 0.8], ellipse)).toBe(false);
expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
});
it("point in ellipse", () => {
[
[0, 1],
[0, -1],
[2, 0],
[-2, 0],
].forEach((point) => {
expect(pointInEllipse(point as Point, ellipse)).toBe(true);
});
expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
});
});
describe("line and line", () => {
const lineA: Line = [
[1, 4],
[3, 4],
];
const lineB: Line = [
[2, 1],
[2, 7],
];
const lineC: Line = [
[1, 8],
[3, 8],
];
const lineD: Line = [
[1, 8],
[3, 8],
];
const lineE: Line = [
[1, 9],
[3, 9],
];
const lineF: Line = [
[1, 2],
[3, 4],
];
const lineG: Line = [
[0, 1],
[2, 3],
];
it("intersection", () => {
expect(lineIntersectsLine(lineA, lineB)).toBe(true);
expect(lineIntersectsLine(lineA, lineC)).toBe(false);
expect(lineIntersectsLine(lineB, lineC)).toBe(false);
expect(lineIntersectsLine(lineC, lineD)).toBe(true);
expect(lineIntersectsLine(lineE, lineD)).toBe(false);
expect(lineIntersectsLine(lineF, lineG)).toBe(true);
});
});

View file

@ -0,0 +1,70 @@
import {
decodePngMetadata,
decodeSvgMetadata,
} from "../../excalidraw/data/image";
import type { ImportedDataState } from "../../excalidraw/data/types";
import * as utils from "../dist/prod";
import { API } from "../../excalidraw/tests/helpers/api";
// 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",
gridSize: null,
exportEmbedScene: true,
},
files: null,
});
const svg = svgNode.outerHTML;
const parsedString = await decodeSvgMetadata({ 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",
gridSize: null,
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),
);
});
});
});

View file

@ -0,0 +1,262 @@
import type { Bounds } from "../../excalidraw/element/bounds";
import { API } from "../../excalidraw/tests/helpers/api";
import {
elementPartiallyOverlapsWithOrContainsBBox,
elementsOverlappingBBox,
isElementInsideBBox,
} from "../src/withinBounds";
const makeElement = (x: number, y: number, width: number, height: number) =>
API.createElement({
type: "rectangle",
x,
y,
width,
height,
});
const makeBBox = (
minX: number,
minY: number,
maxX: number,
maxY: number,
): Bounds => [minX, minY, maxX, maxY];
describe("isElementInsideBBox()", () => {
it("should return true if element is fully inside", () => {
const bbox = makeBBox(0, 0, 100, 100);
// bbox contains element
expect(isElementInsideBBox(makeElement(0, 0, 100, 100), bbox)).toBe(true);
expect(isElementInsideBBox(makeElement(10, 10, 90, 90), bbox)).toBe(true);
});
it("should return false if element is only partially overlapping", () => {
const bbox = makeBBox(0, 0, 100, 100);
// element contains bbox
expect(isElementInsideBBox(makeElement(-10, -10, 110, 110), bbox)).toBe(
false,
);
// element overlaps bbox from top-left
expect(isElementInsideBBox(makeElement(-10, -10, 100, 100), bbox)).toBe(
false,
);
// element overlaps bbox from top-right
expect(isElementInsideBBox(makeElement(90, -10, 100, 100), bbox)).toBe(
false,
);
// element overlaps bbox from bottom-left
expect(isElementInsideBBox(makeElement(-10, 90, 100, 100), bbox)).toBe(
false,
);
// element overlaps bbox from bottom-right
expect(isElementInsideBBox(makeElement(90, 90, 100, 100), bbox)).toBe(
false,
);
});
it("should return false if element outside", () => {
const bbox = makeBBox(0, 0, 100, 100);
// outside diagonally
expect(isElementInsideBBox(makeElement(110, 110, 100, 100), bbox)).toBe(
false,
);
// outside on the left
expect(isElementInsideBBox(makeElement(-110, 10, 50, 50), bbox)).toBe(
false,
);
// outside on the right
expect(isElementInsideBBox(makeElement(110, 10, 50, 50), bbox)).toBe(false);
// outside on the top
expect(isElementInsideBBox(makeElement(10, -110, 50, 50), bbox)).toBe(
false,
);
// outside on the bottom
expect(isElementInsideBBox(makeElement(10, 110, 50, 50), bbox)).toBe(false);
});
it("should return true if bbox contains element and flag enabled", () => {
const bbox = makeBBox(0, 0, 100, 100);
// element contains bbox
expect(
isElementInsideBBox(makeElement(-10, -10, 110, 110), bbox, true),
).toBe(true);
// bbox contains element
expect(isElementInsideBBox(makeElement(0, 0, 100, 100), bbox)).toBe(true);
expect(isElementInsideBBox(makeElement(10, 10, 90, 90), bbox)).toBe(true);
});
});
describe("elementPartiallyOverlapsWithOrContainsBBox()", () => {
it("should return true if element overlaps, is inside, or contains", () => {
const bbox = makeBBox(0, 0, 100, 100);
// bbox contains element
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(0, 0, 100, 100),
bbox,
),
).toBe(true);
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(10, 10, 90, 90),
bbox,
),
).toBe(true);
// element contains bbox
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(-10, -10, 110, 110),
bbox,
),
).toBe(true);
// element overlaps bbox from top-left
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(-10, -10, 100, 100),
bbox,
),
).toBe(true);
// element overlaps bbox from top-right
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(90, -10, 100, 100),
bbox,
),
).toBe(true);
// element overlaps bbox from bottom-left
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(-10, 90, 100, 100),
bbox,
),
).toBe(true);
// element overlaps bbox from bottom-right
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(90, 90, 100, 100),
bbox,
),
).toBe(true);
});
it("should return false if element does not overlap", () => {
const bbox = makeBBox(0, 0, 100, 100);
// outside diagonally
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(110, 110, 100, 100),
bbox,
),
).toBe(false);
// outside on the left
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(-110, 10, 50, 50),
bbox,
),
).toBe(false);
// outside on the right
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(110, 10, 50, 50),
bbox,
),
).toBe(false);
// outside on the top
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(10, -110, 50, 50),
bbox,
),
).toBe(false);
// outside on the bottom
expect(
elementPartiallyOverlapsWithOrContainsBBox(
makeElement(10, 110, 50, 50),
bbox,
),
).toBe(false);
});
});
describe("elementsOverlappingBBox()", () => {
it("should return elements that overlap bbox", () => {
const bbox = makeBBox(0, 0, 100, 100);
const rectOutside = makeElement(110, 110, 100, 100);
const rectInside = makeElement(10, 10, 90, 90);
const rectContainingBBox = makeElement(-10, -10, 110, 110);
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
expect(
elementsOverlappingBBox({
bounds: bbox,
type: "overlap",
elements: [
rectOutside,
rectInside,
rectContainingBBox,
rectOverlappingTopLeft,
],
}),
).toEqual([rectInside, rectContainingBBox, rectOverlappingTopLeft]);
});
it("should return elements inside/containing bbox", () => {
const bbox = makeBBox(0, 0, 100, 100);
const rectOutside = makeElement(110, 110, 100, 100);
const rectInside = makeElement(10, 10, 90, 90);
const rectContainingBBox = makeElement(-10, -10, 110, 110);
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
expect(
elementsOverlappingBBox({
bounds: bbox,
type: "contain",
elements: [
rectOutside,
rectInside,
rectContainingBBox,
rectOverlappingTopLeft,
],
}),
).toEqual([rectInside, rectContainingBBox]);
});
it("should return elements inside bbox", () => {
const bbox = makeBBox(0, 0, 100, 100);
const rectOutside = makeElement(110, 110, 100, 100);
const rectInside = makeElement(10, 10, 90, 90);
const rectContainingBBox = makeElement(-10, -10, 110, 110);
const rectOverlappingTopLeft = makeElement(-10, -10, 50, 50);
expect(
elementsOverlappingBBox({
bounds: bbox,
type: "inside",
elements: [
rectOutside,
rectInside,
rectContainingBBox,
rectOverlappingTopLeft,
],
}),
).toEqual([rectInside]);
});
// TODO test linear, freedraw, and diamond element types (+rotated)
});