123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- import type {
- ExcalidrawElement,
- ExcalidrawGenericElement,
- ExcalidrawTextElement,
- ExcalidrawLinearElement,
- ExcalidrawFreeDrawElement,
- ExcalidrawImageElement,
- FileId,
- ExcalidrawFrameElement,
- ExcalidrawElementType,
- ExcalidrawMagicFrameElement,
- } from "../../element/types";
- import { newElement, newTextElement, newLinearElement } from "../../element";
- import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
- import { getDefaultAppState } from "../../appState";
- import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
- import fs from "fs";
- import util from "util";
- import path from "path";
- import { getMimeType } from "../../data/blob";
- import {
- newArrowElement,
- newEmbeddableElement,
- newFrameElement,
- newFreeDrawElement,
- newIframeElement,
- newImageElement,
- newMagicFrameElement,
- } from "../../element/newElement";
- import type { AppState, Point } from "../../types";
- import { getSelectedElements } from "../../scene/selection";
- import { isLinearElementType } from "../../element/typeChecks";
- import type { Mutable } from "../../utility-types";
- import { assertNever } from "../../utils";
- import type App from "../../components/App";
- import { createTestHook } from "../../components/App";
- import type { Action } from "../../actions/types";
- import { mutateElement } from "../../element/mutateElement";
- 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[]) => {
- act(() => {
- h.setState({
- selectedElementIds: elements.reduce((acc, element) => {
- acc[element.id] = true;
- return acc;
- }, {} as Record<ExcalidrawElement["id"], true>),
- });
- });
- };
- static updateElement = (
- ...[element, updates]: Parameters<typeof mutateElement>
- ) => {
- act(() => {
- mutateElement(element, updates);
- });
- };
- 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 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?: string[];
- // 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" ? readonly Point[] : 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"
- ? ExcalidrawLinearElement["startBinding"]
- : never;
- endBinding?: T extends "arrow"
- ? ExcalidrawLinearElement["endBinding"]
- : never;
- elbowed?: boolean;
- }): 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"
- | "width"
- | "height"
- | "type"
- | "seed"
- | "version"
- | "versionNonce"
- | "isDeleted"
- | "groupIds"
- | "link"
- | "updated"
- > = {
- x,
- y,
- frameId: rest.frameId ?? null,
- index: rest.index ?? null,
- angle: rest.angle ?? 0,
- 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",
- width,
- height,
- ...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,
- ...base,
- });
- break;
- case "arrow":
- element = newArrowElement({
- ...base,
- width,
- height,
- type,
- points: rest.points ?? [
- [0, 0],
- [100, 100],
- ],
- elbowed: rest.elbowed ?? false,
- });
- break;
- case "line":
- element = newLinearElement({
- ...base,
- width,
- height,
- type,
- points: rest.points ?? [
- [0, 0],
- [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;
- }
- if (id) {
- element.id = id;
- }
- if (isDeleted) {
- element.isDeleted = isDeleted;
- }
- if (groupIds) {
- element.groupIds = groupIds;
- }
- return element as any;
- };
- 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);
- });
- };
- }
|