update appState on copy-styles & improve paste
This commit is contained in:
parent
9f6e3c5a9d
commit
ef82e15ee8
@ -1,28 +1,110 @@
|
|||||||
import {
|
import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isExcalidrawElement,
|
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
|
getNonDeletedElements,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_SIZE,
|
ExcalidrawElement,
|
||||||
DEFAULT_FONT_FAMILY,
|
ExcalidrawElementPossibleProps,
|
||||||
DEFAULT_TEXT_ALIGN,
|
} from "../element/types";
|
||||||
} from "../constants";
|
import { AppState } from "../types";
|
||||||
|
import {
|
||||||
|
canChangeSharpness,
|
||||||
|
getSelectedElements,
|
||||||
|
hasBackground,
|
||||||
|
hasStroke,
|
||||||
|
hasText,
|
||||||
|
} from "../scene";
|
||||||
|
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||||
|
|
||||||
|
type AppStateStyles = {
|
||||||
|
[K in AssertSubset<
|
||||||
|
keyof AppState,
|
||||||
|
typeof copyableStyles[number][0]
|
||||||
|
>]: AppState[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ElementStyles = {
|
||||||
|
[K in AssertSubset<
|
||||||
|
keyof ExcalidrawElementPossibleProps,
|
||||||
|
typeof copyableStyles[number][1]
|
||||||
|
>]: ExcalidrawElementPossibleProps[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ElemelementStylesByType = Record<ExcalidrawElement["type"], ElementStyles>;
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
let COPIED_STYLES: {
|
||||||
|
appStateStyles: Partial<AppStateStyles>;
|
||||||
|
elementStyles: Partial<ElementStyles>;
|
||||||
|
elementStylesByType: Partial<ElemelementStylesByType>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
/* [AppState prop, ExcalidrawElement prop, predicate] */
|
||||||
|
const copyableStyles = [
|
||||||
|
["currentItemOpacity", "opacity", () => true],
|
||||||
|
["currentItemStrokeColor", "strokeColor", () => true],
|
||||||
|
["currentItemStrokeStyle", "strokeStyle", hasStroke],
|
||||||
|
["currentItemStrokeWidth", "strokeWidth", hasStroke],
|
||||||
|
["currentItemRoughness", "roughness", hasStroke],
|
||||||
|
["currentItemBackgroundColor", "backgroundColor", hasBackground],
|
||||||
|
["currentItemFillStyle", "fillStyle", hasBackground],
|
||||||
|
["currentItemStrokeSharpness", "strokeSharpness", canChangeSharpness],
|
||||||
|
["currentItemLinearStrokeSharpness", "strokeSharpness", isLinearElementType],
|
||||||
|
["currentItemStartArrowhead", "startArrowhead", isLinearElementType],
|
||||||
|
["currentItemEndArrowhead", "endArrowhead", isLinearElementType],
|
||||||
|
["currentItemFontFamily", "fontFamily", hasText],
|
||||||
|
["currentItemFontSize", "fontSize", hasText],
|
||||||
|
["currentItemTextAlign", "textAlign", hasText],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const getCommonStyleProps = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): Exclude<typeof COPIED_STYLES, null> => {
|
||||||
|
const appStateStyles = {} as AppStateStyles;
|
||||||
|
const elementStyles = {} as ElementStyles;
|
||||||
|
|
||||||
|
const elementStylesByType = elements.reduce((acc, element) => {
|
||||||
|
// only use the first element of given type
|
||||||
|
if (!acc[element.type]) {
|
||||||
|
acc[element.type] = {} as ElementStyles;
|
||||||
|
copyableStyles.forEach(([appStateProp, prop, predicate]) => {
|
||||||
|
const value = (element as any)[prop];
|
||||||
|
if (value !== undefined && predicate(element.type)) {
|
||||||
|
if (appStateStyles[appStateProp] === undefined) {
|
||||||
|
(appStateStyles as any)[appStateProp] = value;
|
||||||
|
}
|
||||||
|
if (elementStyles[prop] === undefined) {
|
||||||
|
(elementStyles as any)[prop] = value;
|
||||||
|
}
|
||||||
|
(acc as any)[element.type][prop] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as ElemelementStylesByType);
|
||||||
|
|
||||||
|
// clone in case we ever make some of the props into non-primitives
|
||||||
|
return JSON.parse(
|
||||||
|
JSON.stringify({ appStateStyles, elementStyles, elementStylesByType }),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const actionCopyStyles = register({
|
export const actionCopyStyles = register({
|
||||||
name: "copyStyles",
|
name: "copyStyles",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
COPIED_STYLES = getCommonStyleProps(
|
||||||
if (element) {
|
getSelectedElements(getNonDeletedElements(elements), appState),
|
||||||
copiedStyles = JSON.stringify(element);
|
);
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...COPIED_STYLES.appStateStyles,
|
||||||
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -35,31 +117,49 @@ export const actionCopyStyles = register({
|
|||||||
export const actionPasteStyles = register({
|
export const actionPasteStyles = register({
|
||||||
name: "pasteStyles",
|
name: "pasteStyles",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const pastedElement = JSON.parse(copiedStyles);
|
if (!COPIED_STYLES) {
|
||||||
if (!isExcalidrawElement(pastedElement)) {
|
|
||||||
return { elements, commitToHistory: false };
|
return { elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
const getStyle = <T extends ExcalidrawElement, K extends keyof T>(
|
||||||
|
element: T,
|
||||||
|
prop: K,
|
||||||
|
) => {
|
||||||
|
return (COPIED_STYLES?.elementStylesByType[element.type]?.[
|
||||||
|
prop as keyof ElementStyles
|
||||||
|
] ??
|
||||||
|
COPIED_STYLES?.elementStyles[prop as keyof ElementStyles] ??
|
||||||
|
element[prop]) as T[K];
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
|
const commonProps = {
|
||||||
|
backgroundColor: getStyle(element, "backgroundColor"),
|
||||||
|
strokeWidth: getStyle(element, "strokeWidth"),
|
||||||
|
strokeColor: getStyle(element, "strokeColor"),
|
||||||
|
strokeStyle: getStyle(element, "strokeStyle"),
|
||||||
|
fillStyle: getStyle(element, "fillStyle"),
|
||||||
|
opacity: getStyle(element, "opacity"),
|
||||||
|
roughness: getStyle(element, "roughness"),
|
||||||
|
strokeSharpness: getStyle(element, "strokeSharpness"),
|
||||||
|
};
|
||||||
|
if (isTextElement(element)) {
|
||||||
const newElement = newElementWith(element, {
|
const newElement = newElementWith(element, {
|
||||||
backgroundColor: pastedElement?.backgroundColor,
|
...commonProps,
|
||||||
strokeWidth: pastedElement?.strokeWidth,
|
fontSize: getStyle(element, "fontSize"),
|
||||||
strokeColor: pastedElement?.strokeColor,
|
fontFamily: getStyle(element, "fontFamily"),
|
||||||
strokeStyle: pastedElement?.strokeStyle,
|
textAlign: getStyle(element, "textAlign"),
|
||||||
fillStyle: pastedElement?.fillStyle,
|
|
||||||
opacity: pastedElement?.opacity,
|
|
||||||
roughness: pastedElement?.roughness,
|
|
||||||
});
|
|
||||||
if (isTextElement(newElement)) {
|
|
||||||
mutateElement(newElement, {
|
|
||||||
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
|
|
||||||
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
|
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(newElement);
|
redrawTextBoundingBox(newElement);
|
||||||
}
|
|
||||||
return newElement;
|
return newElement;
|
||||||
|
} else if (isLinearElement(element)) {
|
||||||
|
return newElementWith(element, {
|
||||||
|
...commonProps,
|
||||||
|
startArrowhead: getStyle(element, "startArrowhead"),
|
||||||
|
endArrowhead: getStyle(element, "endArrowhead"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newElementWith(element, commonProps);
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}),
|
}),
|
||||||
|
@ -110,3 +110,16 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawElementTypes = Pick<ExcalidrawElement, "type">["type"];
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
type __ExcalidrawElementPossibleProps_withoutType<T> = T extends any
|
||||||
|
? { [K in keyof Omit<T, "type">]: T[K] }
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/** Do not use for anything unless you really need it for some abstract
|
||||||
|
API types */
|
||||||
|
export type ExcalidrawElementPossibleProps = UnionToIntersection<
|
||||||
|
__ExcalidrawElementPossibleProps_withoutType<ExcalidrawElement>
|
||||||
|
> & { type: ExcalidrawElementTypes };
|
||||||
|
9
src/global.d.ts
vendored
9
src/global.d.ts
vendored
@ -46,6 +46,15 @@ type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|||||||
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
||||||
Required<Pick<T, RK>>;
|
Required<Pick<T, RK>>;
|
||||||
|
|
||||||
|
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
|
||||||
|
x: infer R,
|
||||||
|
) => any
|
||||||
|
? R
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/** Assert K is a subset of T, and returns K */
|
||||||
|
type AssertSubset<T, K extends T> = K;
|
||||||
|
|
||||||
// PNG encoding/decoding
|
// PNG encoding/decoding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -81,6 +81,12 @@ export class API {
|
|||||||
verticalAlign?: T extends "text"
|
verticalAlign?: T extends "text"
|
||||||
? ExcalidrawTextElement["verticalAlign"]
|
? ExcalidrawTextElement["verticalAlign"]
|
||||||
: never;
|
: never;
|
||||||
|
startArrowhead?: T extends "arrow" | "line" | "draw"
|
||||||
|
? ExcalidrawLinearElement["startArrowhead"]
|
||||||
|
: never;
|
||||||
|
endArrowhead?: T extends "arrow" | "line" | "draw"
|
||||||
|
? ExcalidrawLinearElement["endArrowhead"]
|
||||||
|
: never;
|
||||||
}): T extends "arrow" | "line" | "draw"
|
}): T extends "arrow" | "line" | "draw"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "text"
|
: T extends "text"
|
||||||
@ -130,8 +136,8 @@ export class API {
|
|||||||
case "draw":
|
case "draw":
|
||||||
element = newLinearElement({
|
element = newLinearElement({
|
||||||
type: type as "arrow" | "line" | "draw",
|
type: type as "arrow" | "line" | "draw",
|
||||||
startArrowhead: null,
|
startArrowhead: rest.startArrowhead ?? null,
|
||||||
endArrowhead: null,
|
endArrowhead: rest.endArrowhead ?? null,
|
||||||
...base,
|
...base,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { copiedStyles } from "../actions/actionStyles";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { setLanguage, t } from "../i18n";
|
import { setLanguage, t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
@ -768,82 +768,224 @@ describe("regression tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selecting 'Copy styles' in context menu copies styles", () => {
|
it("copy-styles updates appState defaults", () => {
|
||||||
UI.clickTool("rectangle");
|
h.app.updateScene({
|
||||||
mouse.down(10, 10);
|
elements: [
|
||||||
mouse.up(20, 20);
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "A",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
opacity: 90,
|
||||||
|
strokeColor: "#FF0000",
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 10,
|
||||||
|
roughness: 2,
|
||||||
|
backgroundColor: "#00FF00",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "B",
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
startArrowhead: "bar",
|
||||||
|
endArrowhead: "bar",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "C",
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
fontFamily: 3,
|
||||||
|
fontSize: 200,
|
||||||
|
textAlign: "center",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.setState({
|
||||||
|
selectedElementIds: { A: true, B: true, C: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultAppState = getDefaultAppState();
|
||||||
|
|
||||||
|
expect(h.state).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currentItemOpacity: defaultAppState.currentItemOpacity,
|
||||||
|
currentItemStrokeColor: defaultAppState.currentItemStrokeColor,
|
||||||
|
currentItemStrokeStyle: defaultAppState.currentItemStrokeStyle,
|
||||||
|
currentItemStrokeWidth: defaultAppState.currentItemStrokeWidth,
|
||||||
|
currentItemRoughness: defaultAppState.currentItemRoughness,
|
||||||
|
currentItemBackgroundColor: defaultAppState.currentItemBackgroundColor,
|
||||||
|
currentItemFillStyle: defaultAppState.currentItemFillStyle,
|
||||||
|
currentItemStrokeSharpness: defaultAppState.currentItemStrokeSharpness,
|
||||||
|
currentItemStartArrowhead: defaultAppState.currentItemStartArrowhead,
|
||||||
|
currentItemEndArrowhead: defaultAppState.currentItemEndArrowhead,
|
||||||
|
currentItemFontFamily: defaultAppState.currentItemFontFamily,
|
||||||
|
currentItemFontSize: defaultAppState.currentItemFontSize,
|
||||||
|
currentItemTextAlign: defaultAppState.currentItemTextAlign,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
const contextMenu = document.querySelector(".context-menu");
|
||||||
expect(copiedStyles).toBe("{}");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
||||||
expect(copiedStyles).not.toBe("{}");
|
|
||||||
const element = JSON.parse(copiedStyles);
|
expect(h.state).toEqual(
|
||||||
expect(element).toEqual(API.getSelectedElement());
|
expect.objectContaining({
|
||||||
|
currentItemOpacity: 90,
|
||||||
|
currentItemStrokeColor: "#FF0000",
|
||||||
|
currentItemStrokeStyle: "solid",
|
||||||
|
currentItemStrokeWidth: 10,
|
||||||
|
currentItemRoughness: 2,
|
||||||
|
currentItemBackgroundColor: "#00FF00",
|
||||||
|
currentItemFillStyle: "solid",
|
||||||
|
currentItemStrokeSharpness: "sharp",
|
||||||
|
currentItemStartArrowhead: "bar",
|
||||||
|
currentItemEndArrowhead: "bar",
|
||||||
|
currentItemFontFamily: 3,
|
||||||
|
currentItemFontSize: 200,
|
||||||
|
currentItemTextAlign: "center",
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selecting 'Paste styles' in context menu pastes styles", () => {
|
it("paste-styles action", () => {
|
||||||
UI.clickTool("rectangle");
|
h.app.updateScene({
|
||||||
mouse.down(10, 10);
|
elements: [
|
||||||
mouse.up(20, 20);
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
UI.clickTool("rectangle");
|
id: "A",
|
||||||
mouse.down(10, 10);
|
x: 0,
|
||||||
mouse.up(20, 20);
|
y: 0,
|
||||||
|
opacity: 90,
|
||||||
// Change some styles of second rectangle
|
strokeColor: "#FF0000",
|
||||||
clickLabeledElement("Stroke");
|
strokeStyle: "solid",
|
||||||
clickLabeledElement("#c92a2a");
|
strokeWidth: 10,
|
||||||
clickLabeledElement("Background");
|
roughness: 2,
|
||||||
clickLabeledElement("#e64980");
|
backgroundColor: "#00FF00",
|
||||||
// Fill style
|
fillStyle: "solid",
|
||||||
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
strokeSharpness: "sharp",
|
||||||
// Stroke width
|
}),
|
||||||
fireEvent.click(screen.getByTitle("Bold"));
|
API.createElement({
|
||||||
// Stroke style
|
type: "arrow",
|
||||||
fireEvent.click(screen.getByTitle("Dotted"));
|
id: "B",
|
||||||
// Roughness
|
x: 0,
|
||||||
fireEvent.click(screen.getByTitle("Cartoonist"));
|
y: 0,
|
||||||
// Opacity
|
startArrowhead: "bar",
|
||||||
fireEvent.change(screen.getByLabelText("Opacity"), {
|
endArrowhead: "bar",
|
||||||
target: { value: "60" },
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "C",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fontFamily: 3,
|
||||||
|
fontSize: 200,
|
||||||
|
textAlign: "center",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "D",
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "E",
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "F",
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
h.app.setState({
|
||||||
|
selectedElementIds: { A: true, B: true, C: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
// Copy styles of second rectangle
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 1,
|
||||||
clientY: 40,
|
clientY: 1,
|
||||||
});
|
});
|
||||||
let contextMenu = document.querySelector(".context-menu");
|
fireEvent.click(
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
queryByText(
|
||||||
const secondRect = JSON.parse(copiedStyles);
|
document.querySelector(".context-menu") as HTMLElement,
|
||||||
expect(secondRect.id).toBe(h.elements[1].id);
|
"Copy styles",
|
||||||
|
)!,
|
||||||
|
);
|
||||||
|
|
||||||
mouse.reset();
|
h.app.setState({
|
||||||
// Paste styles to first rectangle
|
selectedElementIds: { D: true, E: true, F: true },
|
||||||
|
});
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 201,
|
||||||
clientY: 10,
|
clientY: 201,
|
||||||
});
|
});
|
||||||
contextMenu = document.querySelector(".context-menu");
|
fireEvent.click(
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
|
queryByText(
|
||||||
|
document.querySelector(".context-menu") as HTMLElement,
|
||||||
|
"Paste styles",
|
||||||
|
)!,
|
||||||
|
);
|
||||||
|
|
||||||
const firstRect = API.getSelectedElement();
|
const defaultAppState = getDefaultAppState();
|
||||||
expect(firstRect.id).toBe(h.elements[0].id);
|
|
||||||
expect(firstRect.strokeColor).toBe("#c92a2a");
|
expect(h.elements.find((element) => element.id === "D")).toEqual(
|
||||||
expect(firstRect.backgroundColor).toBe("#e64980");
|
expect.objectContaining({
|
||||||
expect(firstRect.fillStyle).toBe("cross-hatch");
|
opacity: 90,
|
||||||
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
strokeColor: "#FF0000",
|
||||||
expect(firstRect.strokeStyle).toBe("dotted");
|
strokeStyle: "solid",
|
||||||
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
|
strokeWidth: 10,
|
||||||
expect(firstRect.opacity).toBe(60);
|
roughness: 2,
|
||||||
|
backgroundColor: "#00FF00",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(h.elements.find((element) => element.id === "E")).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
opacity: defaultAppState.currentItemOpacity,
|
||||||
|
strokeColor: defaultAppState.currentItemStrokeColor,
|
||||||
|
strokeStyle: defaultAppState.currentItemStrokeStyle,
|
||||||
|
strokeWidth: defaultAppState.currentItemStrokeWidth,
|
||||||
|
roughness: defaultAppState.currentItemRoughness,
|
||||||
|
backgroundColor: "#00FF00",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
startArrowhead: "bar",
|
||||||
|
endArrowhead: "bar",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(h.elements.find((element) => element.id === "F")).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
opacity: defaultAppState.currentItemOpacity,
|
||||||
|
strokeColor: defaultAppState.currentItemStrokeColor,
|
||||||
|
strokeStyle: defaultAppState.currentItemStrokeStyle,
|
||||||
|
strokeWidth: 10,
|
||||||
|
roughness: 2,
|
||||||
|
backgroundColor: "#00FF00",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
fontFamily: 3,
|
||||||
|
fontSize: 200,
|
||||||
|
textAlign: "center",
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selecting 'Delete' in context menu deletes element", () => {
|
it("selecting 'Delete' in context menu deletes element", () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user