123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- import fs from "fs";
- import path from "path";
- import util from "util";
- import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
- import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
- import { mutateElement } from "@excalidraw/element/mutateElement";
- import {
- newArrowElement,
- newElement,
- newEmbeddableElement,
- newFrameElement,
- newFreeDrawElement,
- newIframeElement,
- newImageElement,
- newLinearElement,
- newMagicFrameElement,
- newTextElement,
- } from "@excalidraw/element/newElement";
- import { isLinearElementType } from "@excalidraw/element/typeChecks";
- import { getSelectedElements } from "@excalidraw/element/selection";
- import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
- import type {
- ExcalidrawElement,
- ExcalidrawGenericElement,
- ExcalidrawTextElement,
- ExcalidrawLinearElement,
- ExcalidrawFreeDrawElement,
- ExcalidrawImageElement,
- FileId,
- ExcalidrawFrameElement,
- ExcalidrawElementType,
- ExcalidrawMagicFrameElement,
- ExcalidrawElbowArrowElement,
- ExcalidrawArrowElement,
- FixedSegment,
- } from "@excalidraw/element/types";
- import type { Mutable } from "@excalidraw/common/utility-types";
- import { getMimeType } from "../../data/blob";
- import { createTestHook } from "../../components/App";
- import { getDefaultAppState } from "../../appState";
- import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
- import type { Action } from "../../actions/types";
- import type App from "../../components/App";
- import type { AppState } from "../../types";
- const readFile = util.promisify(fs.readFile);
- // so that window.h is available when App.tsx is not imported as well.
- createTestHook();
- const { h } = window;
- export class API {
- static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => {
- act(() => {
- h.app.updateScene(...args);
- });
- };
- static setAppState: React.Component<any, AppState>["setState"] = (
- state,
- cb,
- ) => {
- act(() => {
- h.setState(state, cb);
- });
- };
- static setElements = (elements: readonly ExcalidrawElement[]) => {
- act(() => {
- h.elements = elements;
- });
- };
- static setSelectedElements = (elements: ExcalidrawElement[], editingGroupId?: string | null) => {
- act(() => {
- h.setState({
- ...selectGroupsForSelectedElements(
- {
- editingGroupId: editingGroupId ?? null,
- selectedElementIds: elements.reduce((acc, element) => {
- acc[element.id] = true;
- return acc;
- }, {} as Record<ExcalidrawElement["id"], true>),
- },
- elements,
- h.state,
- h.app,
- )
- });
- });
- };
- // eslint-disable-next-line prettier/prettier
- static updateElement = <T extends ExcalidrawElement>(
- ...args: Parameters<typeof mutateElement<T>>
- ) => {
- act(() => {
- mutateElement<T>(...args);
- });
- };
- static getSelectedElements = (
- includeBoundTextElement: boolean = false,
- includeElementsInFrames: boolean = false,
- ): ExcalidrawElement[] => {
- return getSelectedElements(h.elements, h.state, {
- includeBoundTextElement,
- includeElementsInFrames,
- });
- };
- static getSelectedElement = (): ExcalidrawElement => {
- const selectedElements = API.getSelectedElements();
- if (selectedElements.length !== 1) {
- throw new Error(
- `expected 1 selected element; got ${selectedElements.length}`,
- );
- }
- return selectedElements[0];
- };
- static getUndoStack = () => {
- // @ts-ignore
- return h.history.undoStack;
- };
- static getRedoStack = () => {
- // @ts-ignore
- return h.history.redoStack;
- };
- static getSnapshot = () => {
- return Array.from(h.store.snapshot.elements.values());
- };
- static clearSelection = () => {
- act(() => {
- // @ts-ignore
- h.app.clearSelection(null);
- });
- expect(API.getSelectedElements().length).toBe(0);
- };
- static getElement = <T extends ExcalidrawElement>(element: T): T => {
- return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element;
- }
- static createElement = <
- T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
- >({
- // @ts-ignore
- type = "rectangle",
- id,
- x = 0,
- y = x,
- width = 100,
- height = width,
- isDeleted = false,
- groupIds = [],
- ...rest
- }: {
- type?: T;
- x?: number;
- y?: number;
- height?: number;
- width?: number;
- angle?: number;
- id?: string;
- isDeleted?: boolean;
- frameId?: ExcalidrawElement["id"] | null;
- index?: ExcalidrawElement["index"];
- groupIds?: ExcalidrawElement["groupIds"];
- // generic element props
- strokeColor?: ExcalidrawGenericElement["strokeColor"];
- backgroundColor?: ExcalidrawGenericElement["backgroundColor"];
- fillStyle?: ExcalidrawGenericElement["fillStyle"];
- strokeWidth?: ExcalidrawGenericElement["strokeWidth"];
- strokeStyle?: ExcalidrawGenericElement["strokeStyle"];
- roundness?: ExcalidrawGenericElement["roundness"];
- roughness?: ExcalidrawGenericElement["roughness"];
- opacity?: ExcalidrawGenericElement["opacity"];
- // text props
- text?: T extends "text" ? ExcalidrawTextElement["text"] : never;
- fontSize?: T extends "text" ? ExcalidrawTextElement["fontSize"] : never;
- fontFamily?: T extends "text" ? ExcalidrawTextElement["fontFamily"] : never;
- textAlign?: T extends "text" ? ExcalidrawTextElement["textAlign"] : never;
- verticalAlign?: T extends "text"
- ? ExcalidrawTextElement["verticalAlign"]
- : never;
- boundElements?: ExcalidrawGenericElement["boundElements"];
- containerId?: T extends "text"
- ? ExcalidrawTextElement["containerId"]
- : never;
- points?: T extends "arrow" | "line" | "freedraw" ? readonly LocalPoint[] : never;
- locked?: boolean;
- fileId?: T extends "image" ? string : never;
- scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
- status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
- startBinding?: T extends "arrow"
- ? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
- : never;
- endBinding?: T extends "arrow"
- ? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"]
- : never;
- startArrowhead?: T extends "arrow"
- ? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"]
- : never;
- endArrowhead?: T extends "arrow"
- ? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
- : never;
- elbowed?: boolean;
- fixedSegments?: FixedSegment[] | null;
- }): T extends "arrow" | "line"
- ? ExcalidrawLinearElement
- : T extends "freedraw"
- ? ExcalidrawFreeDrawElement
- : T extends "text"
- ? ExcalidrawTextElement
- : T extends "image"
- ? ExcalidrawImageElement
- : T extends "frame"
- ? ExcalidrawFrameElement
- : T extends "magicframe"
- ? ExcalidrawMagicFrameElement
- : ExcalidrawGenericElement => {
- let element: Mutable<ExcalidrawElement> = null!;
- const appState = h?.state || getDefaultAppState();
- const base: Omit<
- ExcalidrawGenericElement,
- | "id"
- | "type"
- | "version"
- | "versionNonce"
- | "isDeleted"
- | "groupIds"
- | "link"
- | "updated"
- > = {
- seed: 1,
- x,
- y,
- width,
- height,
- frameId: rest.frameId ?? null,
- index: rest.index ?? null,
- angle: (rest.angle ?? 0) as Radians,
- strokeColor: rest.strokeColor ?? appState.currentItemStrokeColor,
- backgroundColor:
- rest.backgroundColor ?? appState.currentItemBackgroundColor,
- fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
- strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
- strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
- roundness: (
- rest.roundness === undefined
- ? appState.currentItemRoundness === "round"
- : rest.roundness
- )
- ? {
- type: isLinearElementType(type)
- ? ROUNDNESS.PROPORTIONAL_RADIUS
- : ROUNDNESS.ADAPTIVE_RADIUS,
- }
- : null,
- roughness: rest.roughness ?? appState.currentItemRoughness,
- opacity: rest.opacity ?? appState.currentItemOpacity,
- boundElements: rest.boundElements ?? null,
- locked: rest.locked ?? false,
- };
- switch (type) {
- case "rectangle":
- case "diamond":
- case "ellipse":
- element = newElement({
- type: type as "rectangle" | "diamond" | "ellipse",
- ...base,
- });
- break;
- case "embeddable":
- element = newEmbeddableElement({
- type: "embeddable",
- ...base,
- });
- break;
- case "iframe":
- element = newIframeElement({
- type: "iframe",
- ...base,
- });
- break;
- case "text":
- const fontSize = rest.fontSize ?? appState.currentItemFontSize;
- const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
- element = newTextElement({
- ...base,
- text: rest.text || "test",
- fontSize,
- fontFamily,
- textAlign: rest.textAlign ?? appState.currentItemTextAlign,
- verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
- containerId: rest.containerId ?? undefined,
- });
- element.width = width;
- element.height = height;
- break;
- case "freedraw":
- element = newFreeDrawElement({
- type: type as "freedraw",
- simulatePressure: true,
- points: rest.points,
- ...base,
- });
- break;
- case "arrow":
- element = newArrowElement({
- ...base,
- width,
- height,
- type,
- points: rest.points ?? [
- pointFrom<LocalPoint>(0, 0),
- pointFrom<LocalPoint>(100, 100),
- ],
- elbowed: rest.elbowed ?? false,
- });
- break;
- case "line":
- element = newLinearElement({
- ...base,
- width,
- height,
- type,
- points: rest.points ?? [
- pointFrom<LocalPoint>(0, 0),
- pointFrom<LocalPoint>(100, 100),
- ],
- });
- break;
- case "image":
- element = newImageElement({
- ...base,
- width,
- height,
- type,
- fileId: (rest.fileId as string as FileId) ?? null,
- status: rest.status || "saved",
- scale: rest.scale || [1, 1],
- });
- break;
- case "frame":
- element = newFrameElement({ ...base, width, height });
- break;
- case "magicframe":
- element = newMagicFrameElement({ ...base, width, height });
- break;
- default:
- assertNever(
- type,
- `API.createElement: unimplemented element type ${type}}`,
- );
- break;
- }
- if (element.type === "arrow") {
- element.startBinding = rest.startBinding ?? null;
- element.endBinding = rest.endBinding ?? null;
- element.startArrowhead = rest.startArrowhead ?? null;
- element.endArrowhead = rest.endArrowhead ?? null;
- }
- if (id) {
- element.id = id;
- }
- if (isDeleted) {
- element.isDeleted = isDeleted;
- }
- if (groupIds) {
- element.groupIds = groupIds;
- }
- return element as any;
- };
- static createTextContainer = (opts?: {
- frameId?: ExcalidrawElement["id"];
- groupIds?: ExcalidrawElement["groupIds"];
- label?: {
- text?: string;
- frameId?: ExcalidrawElement["id"] | null;
- groupIds?: ExcalidrawElement["groupIds"];
- };
- }) => {
- const rectangle = API.createElement({
- type: "rectangle",
- frameId: opts?.frameId || null,
- groupIds: opts?.groupIds,
- });
- const text = API.createElement({
- type: "text",
- text: opts?.label?.text || "sample-text",
- width: 50,
- height: 20,
- fontSize: 16,
- containerId: rectangle.id,
- frameId:
- opts?.label?.frameId === undefined
- ? opts?.frameId ?? null
- : opts?.label?.frameId ?? null,
- groupIds: opts?.label?.groupIds === undefined
- ? opts?.groupIds
- : opts?.label?.groupIds ,
- });
- mutateElement(
- rectangle,
- {
- boundElements: [{ type: "text", id: text.id }],
- },
- false,
- );
- return [rectangle, text];
- };
- static createLabeledArrow = (opts?: {
- frameId?: ExcalidrawElement["id"];
- label?: {
- text?: string;
- frameId?: ExcalidrawElement["id"] | null;
- };
- }) => {
- const arrow = API.createElement({
- type: "arrow",
- frameId: opts?.frameId || null,
- });
- const text = API.createElement({
- type: "text",
- width: 50,
- height: 20,
- containerId: arrow.id,
- frameId:
- opts?.label?.frameId === undefined
- ? opts?.frameId ?? null
- : opts?.label?.frameId ?? null,
- });
- mutateElement(
- arrow,
- {
- boundElements: [{ type: "text", id: text.id }],
- },
- false,
- );
- return [arrow, text];
- };
- static readFile = async <T extends "utf8" | null>(
- filepath: string,
- encoding?: T,
- ): Promise<T extends "utf8" ? string : Buffer> => {
- filepath = path.isAbsolute(filepath)
- ? filepath
- : path.resolve(path.join(__dirname, "../", filepath));
- return readFile(filepath, { encoding }) as any;
- };
- static loadFile = async (filepath: string) => {
- const { base, ext } = path.parse(filepath);
- return new File([await API.readFile(filepath, null)], base, {
- type: getMimeType(ext),
- });
- };
- static drop = async (blob: Blob) => {
- const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas);
- const text = await new Promise<string>((resolve, reject) => {
- try {
- const reader = new FileReader();
- reader.onload = () => {
- resolve(reader.result as string);
- };
- reader.readAsText(blob);
- } catch (error: any) {
- reject(error);
- }
- });
- const files = [blob] as File[] & { item: (index: number) => File };
- files.item = (index: number) => files[index];
- Object.defineProperty(fileDropEvent, "dataTransfer", {
- value: {
- files,
- getData: (type: string) => {
- if (type === blob.type) {
- return text;
- }
- return "";
- },
- },
- });
- await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
- };
- static executeAction = (action: Action) => {
- act(() => {
- h.app.actionManager.executeAction(action);
- });
- };
- }
|