fix: popover positioning (#3399)
This commit is contained in:
parent
189b721eed
commit
9733ecb3df
@ -474,6 +474,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
UIOptions={this.props.UIOptions}
|
UIOptions={this.props.UIOptions}
|
||||||
/>
|
/>
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
{this.state.showStats && (
|
{this.state.showStats && (
|
||||||
<Stats
|
<Stats
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
@ -3675,12 +3676,20 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const type = element ? "element" : "canvas";
|
const type = element ? "element" : "canvas";
|
||||||
|
|
||||||
|
const container = this.excalidrawContainerRef.current!;
|
||||||
|
const {
|
||||||
|
top: offsetTop,
|
||||||
|
left: offsetLeft,
|
||||||
|
} = container.getBoundingClientRect();
|
||||||
|
const left = event.clientX - offsetLeft;
|
||||||
|
const top = event.clientY - offsetTop;
|
||||||
|
|
||||||
if (element && !this.state.selectedElementIds[element.id]) {
|
if (element && !this.state.selectedElementIds[element.id]) {
|
||||||
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
|
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
|
||||||
this._openContextMenu(event, type);
|
this._openContextMenu({ top, left }, type);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._openContextMenu(event, type);
|
this._openContextMenu({ top, left }, type);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3774,11 +3783,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
/** @private use this.handleCanvasContextMenu */
|
/** @private use this.handleCanvasContextMenu */
|
||||||
private _openContextMenu = (
|
private _openContextMenu = (
|
||||||
{
|
{
|
||||||
clientX,
|
left,
|
||||||
clientY,
|
top,
|
||||||
}: {
|
}: {
|
||||||
clientX: number;
|
left: number;
|
||||||
clientY: number;
|
top: number;
|
||||||
},
|
},
|
||||||
type: "canvas" | "element",
|
type: "canvas" | "element",
|
||||||
) => {
|
) => {
|
||||||
@ -3829,10 +3838,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: viewModeOptions,
|
options: viewModeOptions,
|
||||||
top: clientY,
|
top,
|
||||||
left: clientX,
|
left,
|
||||||
actionManager: this.actionManager,
|
actionManager: this.actionManager,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
@ -3872,10 +3882,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionToggleViewMode,
|
actionToggleViewMode,
|
||||||
actionToggleStats,
|
actionToggleStats,
|
||||||
],
|
],
|
||||||
top: clientY,
|
top,
|
||||||
left: clientX,
|
left,
|
||||||
actionManager: this.actionManager,
|
actionManager: this.actionManager,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3883,10 +3894,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [navigator.clipboard && actionCopy, ...options],
|
options: [navigator.clipboard && actionCopy, ...options],
|
||||||
top: clientY,
|
top,
|
||||||
left: clientX,
|
left,
|
||||||
actionManager: this.actionManager,
|
actionManager: this.actionManager,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3928,10 +3940,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionDuplicateSelection,
|
actionDuplicateSelection,
|
||||||
actionDeleteSelected,
|
actionDeleteSelected,
|
||||||
],
|
],
|
||||||
top: clientY,
|
top,
|
||||||
left: clientX,
|
left,
|
||||||
actionManager: this.actionManager,
|
actionManager: this.actionManager,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,67 +32,63 @@ const ContextMenu = ({
|
|||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
}: ContextMenuProps) => {
|
}: ContextMenuProps) => {
|
||||||
const isDarkTheme = !!document
|
|
||||||
.querySelector(".excalidraw")
|
|
||||||
?.classList.contains("theme--dark");
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Popover
|
||||||
className={clsx("excalidraw", {
|
onCloseRequest={onCloseRequest}
|
||||||
"theme--dark theme--dark-background-none": isDarkTheme,
|
top={top}
|
||||||
})}
|
left={left}
|
||||||
|
fitInViewport={true}
|
||||||
>
|
>
|
||||||
<Popover
|
<ul
|
||||||
onCloseRequest={onCloseRequest}
|
className="context-menu"
|
||||||
top={top}
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
left={left}
|
|
||||||
fitInViewport={true}
|
|
||||||
>
|
>
|
||||||
<ul
|
{options.map((option, idx) => {
|
||||||
className="context-menu"
|
if (option === "separator") {
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
return <hr key={idx} className="context-menu-option-separator" />;
|
||||||
>
|
}
|
||||||
{options.map((option, idx) => {
|
|
||||||
if (option === "separator") {
|
|
||||||
return <hr key={idx} className="context-menu-option-separator" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionName = option.name;
|
const actionName = option.name;
|
||||||
const label = option.contextItemLabel
|
const label = option.contextItemLabel
|
||||||
? t(option.contextItemLabel)
|
? t(option.contextItemLabel)
|
||||||
: "";
|
: "";
|
||||||
return (
|
return (
|
||||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||||
<button
|
<button
|
||||||
className={clsx("context-menu-option", {
|
className={clsx("context-menu-option", {
|
||||||
dangerous: actionName === "deleteSelectedElements",
|
dangerous: actionName === "deleteSelectedElements",
|
||||||
checkmark: option.checked?.(appState),
|
checkmark: option.checked?.(appState),
|
||||||
})}
|
})}
|
||||||
onClick={() => actionManager.executeAction(option)}
|
onClick={() => actionManager.executeAction(option)}
|
||||||
>
|
>
|
||||||
<div className="context-menu-option__label">{label}</div>
|
<div className="context-menu-option__label">{label}</div>
|
||||||
<kbd className="context-menu-option__shortcut">
|
<kbd className="context-menu-option__shortcut">
|
||||||
{actionName
|
{actionName
|
||||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||||
: ""}
|
: ""}
|
||||||
</kbd>
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let contextMenuNode: HTMLDivElement;
|
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
|
||||||
const getContextMenuNode = (): HTMLDivElement => {
|
|
||||||
|
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
|
||||||
|
let contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||||
if (contextMenuNode) {
|
if (contextMenuNode) {
|
||||||
return contextMenuNode;
|
return contextMenuNode;
|
||||||
}
|
}
|
||||||
const div = document.createElement("div");
|
contextMenuNode = document.createElement("div");
|
||||||
document.body.appendChild(div);
|
container
|
||||||
return (contextMenuNode = div);
|
.querySelector(".excalidraw-contextMenuContainer")!
|
||||||
|
.appendChild(contextMenuNode);
|
||||||
|
contextMenuNodeByContainer.set(container, contextMenuNode);
|
||||||
|
return contextMenuNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextMenuParams = {
|
type ContextMenuParams = {
|
||||||
@ -101,10 +97,16 @@ type ContextMenuParams = {
|
|||||||
left: ContextMenuProps["left"];
|
left: ContextMenuProps["left"];
|
||||||
actionManager: ContextMenuProps["actionManager"];
|
actionManager: ContextMenuProps["actionManager"];
|
||||||
appState: Readonly<AppState>;
|
appState: Readonly<AppState>;
|
||||||
|
container: HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = (container: HTMLElement) => {
|
||||||
unmountComponentAtNode(getContextMenuNode());
|
const contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||||
|
if (contextMenuNode) {
|
||||||
|
unmountComponentAtNode(contextMenuNode);
|
||||||
|
contextMenuNode.remove();
|
||||||
|
contextMenuNodeByContainer.delete(container);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -121,11 +123,11 @@ export default {
|
|||||||
top={params.top}
|
top={params.top}
|
||||||
left={params.left}
|
left={params.left}
|
||||||
options={options}
|
options={options}
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={() => handleClose(params.container)}
|
||||||
actionManager={params.actionManager}
|
actionManager={params.actionManager}
|
||||||
appState={params.appState}
|
appState={params.appState}
|
||||||
/>,
|
/>,
|
||||||
getContextMenuNode(),
|
getContextMenuNode(params.container),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.popover {
|
.popover {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4582
src/tests/__snapshots__/contextmenu.test.tsx.snap
Normal file
4582
src/tests/__snapshots__/contextmenu.test.tsx.snap
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
531
src/tests/contextmenu.test.tsx
Normal file
531
src/tests/contextmenu.test.tsx
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
fireEvent,
|
||||||
|
mockBoundingClientRect,
|
||||||
|
restoreOriginalGetBoundingClientRect,
|
||||||
|
GlobalTestState,
|
||||||
|
screen,
|
||||||
|
queryByText,
|
||||||
|
queryAllByText,
|
||||||
|
waitFor,
|
||||||
|
} from "./test-utils";
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
import * as Renderer from "../renderer/renderScene";
|
||||||
|
import { reseed } from "../random";
|
||||||
|
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||||
|
import { CODES } from "../keys";
|
||||||
|
import { ShortcutName } from "../actions/shortcuts";
|
||||||
|
import { copiedStyles } from "../actions/actionStyles";
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
import { setDateTimeForTests } from "../utils";
|
||||||
|
|
||||||
|
const checkpoint = (name: string) => {
|
||||||
|
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
||||||
|
`[${name}] number of renders`,
|
||||||
|
);
|
||||||
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
|
expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
|
||||||
|
expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
|
||||||
|
h.elements.forEach((element, i) =>
|
||||||
|
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
const queryContextMenu = () => {
|
||||||
|
return GlobalTestState.renderResult.container.querySelector(".context-menu");
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickLabeledElement = (label: string) => {
|
||||||
|
const element = document.querySelector(`[aria-label='${label}']`);
|
||||||
|
if (!element) {
|
||||||
|
throw new Error(`No labeled element found: ${label}`);
|
||||||
|
}
|
||||||
|
fireEvent.click(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
|
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
renderScene.mockClear();
|
||||||
|
reseed(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("contextMenu element", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
renderScene.mockClear();
|
||||||
|
h.history.clear();
|
||||||
|
reseed(7);
|
||||||
|
setDateTimeForTests("201933152653");
|
||||||
|
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
restoreOriginalGetBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
checkpoint("end of test");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.down(0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows context menu for canvas", () => {
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const contextMenuOptions = contextMenu?.querySelectorAll(
|
||||||
|
".context-menu li",
|
||||||
|
);
|
||||||
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"selectAll",
|
||||||
|
"gridMode",
|
||||||
|
"zenMode",
|
||||||
|
"viewMode",
|
||||||
|
"stats",
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(contextMenu).not.toBeNull();
|
||||||
|
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||||
|
expectedShortcutNames.forEach((shortcutName) => {
|
||||||
|
expect(
|
||||||
|
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows context menu for element", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const contextMenuOptions = contextMenu?.querySelectorAll(
|
||||||
|
".context-menu li",
|
||||||
|
);
|
||||||
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"copyStyles",
|
||||||
|
"pasteStyles",
|
||||||
|
"deleteSelectedElements",
|
||||||
|
"addToLibrary",
|
||||||
|
"flipHorizontal",
|
||||||
|
"flipVertical",
|
||||||
|
"sendBackward",
|
||||||
|
"bringForward",
|
||||||
|
"sendToBack",
|
||||||
|
"bringToFront",
|
||||||
|
"duplicateSelection",
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(contextMenu).not.toBeNull();
|
||||||
|
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||||
|
expectedShortcutNames.forEach((shortcutName) => {
|
||||||
|
expect(
|
||||||
|
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows 'Group selection' in context menu for multiple selected elements", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, -10);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click(10, 10);
|
||||||
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(20, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const contextMenuOptions = contextMenu?.querySelectorAll(
|
||||||
|
".context-menu li",
|
||||||
|
);
|
||||||
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"copyStyles",
|
||||||
|
"pasteStyles",
|
||||||
|
"deleteSelectedElements",
|
||||||
|
"group",
|
||||||
|
"addToLibrary",
|
||||||
|
"sendBackward",
|
||||||
|
"bringForward",
|
||||||
|
"sendToBack",
|
||||||
|
"bringToFront",
|
||||||
|
"duplicateSelection",
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(contextMenu).not.toBeNull();
|
||||||
|
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||||
|
expectedShortcutNames.forEach((shortcutName) => {
|
||||||
|
expect(
|
||||||
|
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows 'Ungroup selection' in context menu for group inside selected elements", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, -10);
|
||||||
|
mouse.up(10, 10);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.click(10, 10);
|
||||||
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(20, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.codePress(CODES.G);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const contextMenuOptions = contextMenu?.querySelectorAll(
|
||||||
|
".context-menu li",
|
||||||
|
);
|
||||||
|
const expectedShortcutNames: ShortcutName[] = [
|
||||||
|
"copyStyles",
|
||||||
|
"pasteStyles",
|
||||||
|
"deleteSelectedElements",
|
||||||
|
"ungroup",
|
||||||
|
"addToLibrary",
|
||||||
|
"sendBackward",
|
||||||
|
"bringForward",
|
||||||
|
"sendToBack",
|
||||||
|
"bringToFront",
|
||||||
|
"duplicateSelection",
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(contextMenu).not.toBeNull();
|
||||||
|
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||||
|
expectedShortcutNames.forEach((shortcutName) => {
|
||||||
|
expect(
|
||||||
|
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||||
|
).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Copy styles' in context menu copies styles", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
expect(copiedStyles).toBe("{}");
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
||||||
|
expect(copiedStyles).not.toBe("{}");
|
||||||
|
const element = JSON.parse(copiedStyles);
|
||||||
|
expect(element).toEqual(API.getSelectedElement());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Paste styles' in context menu pastes styles", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
// Change some styles of second rectangle
|
||||||
|
clickLabeledElement("Stroke");
|
||||||
|
clickLabeledElement("#c92a2a");
|
||||||
|
clickLabeledElement("Background");
|
||||||
|
clickLabeledElement("#e64980");
|
||||||
|
// Fill style
|
||||||
|
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
||||||
|
// Stroke width
|
||||||
|
fireEvent.click(screen.getByTitle("Bold"));
|
||||||
|
// Stroke style
|
||||||
|
fireEvent.click(screen.getByTitle("Dotted"));
|
||||||
|
// Roughness
|
||||||
|
fireEvent.click(screen.getByTitle("Cartoonist"));
|
||||||
|
// Opacity
|
||||||
|
fireEvent.change(screen.getByLabelText("Opacity"), {
|
||||||
|
target: { value: "60" },
|
||||||
|
});
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
// Copy styles of second rectangle
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 40,
|
||||||
|
clientY: 40,
|
||||||
|
});
|
||||||
|
let contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
||||||
|
const secondRect = JSON.parse(copiedStyles);
|
||||||
|
expect(secondRect.id).toBe(h.elements[1].id);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
// Paste styles to first rectangle
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 10,
|
||||||
|
clientY: 10,
|
||||||
|
});
|
||||||
|
contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
|
||||||
|
|
||||||
|
const firstRect = API.getSelectedElement();
|
||||||
|
expect(firstRect.id).toBe(h.elements[0].id);
|
||||||
|
expect(firstRect.strokeColor).toBe("#c92a2a");
|
||||||
|
expect(firstRect.backgroundColor).toBe("#e64980");
|
||||||
|
expect(firstRect.fillStyle).toBe("cross-hatch");
|
||||||
|
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
||||||
|
expect(firstRect.strokeStyle).toBe("dotted");
|
||||||
|
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
|
||||||
|
expect(firstRect.opacity).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Delete' in context menu deletes element", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]);
|
||||||
|
expect(API.getSelectedElements()).toHaveLength(0);
|
||||||
|
expect(h.elements[0].isDeleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const library = localStorage.getItem("excalidraw-library");
|
||||||
|
expect(library).not.toBeNull();
|
||||||
|
const addedElement = JSON.parse(library!)[0][0];
|
||||||
|
expect(addedElement).toEqual(h.elements[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!);
|
||||||
|
expect(h.elements).toHaveLength(2);
|
||||||
|
const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0];
|
||||||
|
const { id: _id1, seed: _seed1, x: _x1, y: _y1, ...rect2 } = h.elements[1];
|
||||||
|
expect(rect1).toEqual(rect2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Send backward' in context menu sends element backward", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 40,
|
||||||
|
clientY: 40,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const elementsBefore = h.elements;
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!);
|
||||||
|
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
||||||
|
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Bring forward' in context menu brings element forward", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 10,
|
||||||
|
clientY: 10,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const elementsBefore = h.elements;
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!);
|
||||||
|
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
||||||
|
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Send to back' in context menu sends element to back", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 40,
|
||||||
|
clientY: 40,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const elementsBefore = h.elements;
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!);
|
||||||
|
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Bring to front' in context menu brings element to front", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 10,
|
||||||
|
clientY: 10,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
const elementsBefore = h.elements;
|
||||||
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!);
|
||||||
|
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Group selection' in context menu groups selected elements", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(10, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
fireEvent.click(
|
||||||
|
queryByText(contextMenu as HTMLElement, "Group selection")!,
|
||||||
|
);
|
||||||
|
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
||||||
|
expect(h.elements[0].groupIds).toEqual(selectedGroupIds);
|
||||||
|
expect(h.elements[1].groupIds).toEqual(selectedGroupIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selecting 'Ungroup selection' in context menu ungroups selected group", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.click(10, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.codePress(CODES.G);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextMenu = queryContextMenu();
|
||||||
|
expect(contextMenu).not.toBeNull();
|
||||||
|
fireEvent.click(
|
||||||
|
queryByText(contextMenu as HTMLElement, "Ungroup selection")!,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
||||||
|
expect(selectedGroupIds).toHaveLength(0);
|
||||||
|
expect(h.elements[0].groupIds).toHaveLength(0);
|
||||||
|
expect(h.elements[1].groupIds).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
@ -1,8 +1,4 @@
|
|||||||
import { queryAllByText, queryByText } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { copiedStyles } from "../actions/actionStyles";
|
|
||||||
import { ShortcutName } from "../actions/shortcuts";
|
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
@ -11,13 +7,7 @@ import * as Renderer from "../renderer/renderScene";
|
|||||||
import { setDateTimeForTests } from "../utils";
|
import { setDateTimeForTests } from "../utils";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||||
import {
|
import { fireEvent, render, screen, waitFor } from "./test-utils";
|
||||||
fireEvent,
|
|
||||||
GlobalTestState,
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
waitFor,
|
|
||||||
} from "./test-utils";
|
|
||||||
import { defaultLang } from "../i18n";
|
import { defaultLang } from "../i18n";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@ -612,441 +602,6 @@ describe("regression tests", () => {
|
|||||||
expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
|
expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows context menu for canvas", () => {
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const contextMenuOptions = document.querySelectorAll(".context-menu li");
|
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
|
||||||
"selectAll",
|
|
||||||
"gridMode",
|
|
||||||
"zenMode",
|
|
||||||
"viewMode",
|
|
||||||
"stats",
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
|
||||||
expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
|
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
|
||||||
expect(
|
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows context menu for element", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const contextMenuOptions = document.querySelectorAll(".context-menu li");
|
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
|
||||||
"copyStyles",
|
|
||||||
"pasteStyles",
|
|
||||||
"deleteSelectedElements",
|
|
||||||
"addToLibrary",
|
|
||||||
"flipHorizontal",
|
|
||||||
"flipVertical",
|
|
||||||
"sendBackward",
|
|
||||||
"bringForward",
|
|
||||||
"sendToBack",
|
|
||||||
"bringToFront",
|
|
||||||
"duplicateSelection",
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
|
||||||
expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
|
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
|
||||||
expect(
|
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows 'Group selection' in context menu for multiple selected elements", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(10, 10);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, -10);
|
|
||||||
mouse.up(10, 10);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
mouse.click(10, 10);
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
|
||||||
mouse.click(20, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const contextMenuOptions = document.querySelectorAll(".context-menu li");
|
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
|
||||||
"copyStyles",
|
|
||||||
"pasteStyles",
|
|
||||||
"deleteSelectedElements",
|
|
||||||
"group",
|
|
||||||
"addToLibrary",
|
|
||||||
"sendBackward",
|
|
||||||
"bringForward",
|
|
||||||
"sendToBack",
|
|
||||||
"bringToFront",
|
|
||||||
"duplicateSelection",
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
|
||||||
expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
|
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
|
||||||
expect(
|
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows 'Ungroup selection' in context menu for group inside selected elements", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(10, 10);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, -10);
|
|
||||||
mouse.up(10, 10);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
mouse.click(10, 10);
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
|
||||||
mouse.click(20, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
|
||||||
Keyboard.codePress(CODES.G);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const contextMenuOptions = document.querySelectorAll(".context-menu li");
|
|
||||||
const expectedShortcutNames: ShortcutName[] = [
|
|
||||||
"copyStyles",
|
|
||||||
"pasteStyles",
|
|
||||||
"deleteSelectedElements",
|
|
||||||
"ungroup",
|
|
||||||
"addToLibrary",
|
|
||||||
"sendBackward",
|
|
||||||
"bringForward",
|
|
||||||
"sendToBack",
|
|
||||||
"bringToFront",
|
|
||||||
"duplicateSelection",
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(contextMenu).not.toBeNull();
|
|
||||||
expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
|
|
||||||
expectedShortcutNames.forEach((shortcutName) => {
|
|
||||||
expect(
|
|
||||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Copy styles' in context menu copies styles", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
expect(copiedStyles).toBe("{}");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
|
||||||
expect(copiedStyles).not.toBe("{}");
|
|
||||||
const element = JSON.parse(copiedStyles);
|
|
||||||
expect(element).toEqual(API.getSelectedElement());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Paste styles' in context menu pastes styles", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
// Change some styles of second rectangle
|
|
||||||
clickLabeledElement("Stroke");
|
|
||||||
clickLabeledElement("#c92a2a");
|
|
||||||
clickLabeledElement("Background");
|
|
||||||
clickLabeledElement("#e64980");
|
|
||||||
// Fill style
|
|
||||||
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
|
||||||
// Stroke width
|
|
||||||
fireEvent.click(screen.getByTitle("Bold"));
|
|
||||||
// Stroke style
|
|
||||||
fireEvent.click(screen.getByTitle("Dotted"));
|
|
||||||
// Roughness
|
|
||||||
fireEvent.click(screen.getByTitle("Cartoonist"));
|
|
||||||
// Opacity
|
|
||||||
fireEvent.change(screen.getByLabelText("Opacity"), {
|
|
||||||
target: { value: "60" },
|
|
||||||
});
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
// Copy styles of second rectangle
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 40,
|
|
||||||
clientY: 40,
|
|
||||||
});
|
|
||||||
let contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
|
|
||||||
const secondRect = JSON.parse(copiedStyles);
|
|
||||||
expect(secondRect.id).toBe(h.elements[1].id);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
// Paste styles to first rectangle
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 10,
|
|
||||||
});
|
|
||||||
contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
|
|
||||||
|
|
||||||
const firstRect = API.getSelectedElement();
|
|
||||||
expect(firstRect.id).toBe(h.elements[0].id);
|
|
||||||
expect(firstRect.strokeColor).toBe("#c92a2a");
|
|
||||||
expect(firstRect.backgroundColor).toBe("#e64980");
|
|
||||||
expect(firstRect.fillStyle).toBe("cross-hatch");
|
|
||||||
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
|
||||||
expect(firstRect.strokeStyle).toBe("dotted");
|
|
||||||
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
|
|
||||||
expect(firstRect.opacity).toBe(60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Delete' in context menu deletes element", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]);
|
|
||||||
expect(API.getSelectedElements()).toHaveLength(0);
|
|
||||||
expect(h.elements[0].isDeleted).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const library = localStorage.getItem("excalidraw-library");
|
|
||||||
expect(library).not.toBeNull();
|
|
||||||
const addedElement = JSON.parse(library!)[0][0];
|
|
||||||
expect(addedElement).toEqual(h.elements[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!);
|
|
||||||
expect(h.elements).toHaveLength(2);
|
|
||||||
const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0];
|
|
||||||
const { id: _id1, seed: _seed1, x: _x1, y: _y1, ...rect2 } = h.elements[1];
|
|
||||||
expect(rect1).toEqual(rect2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Send backward' in context menu sends element backward", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 40,
|
|
||||||
clientY: 40,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const elementsBefore = h.elements;
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!);
|
|
||||||
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
|
||||||
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Bring forward' in context menu brings element forward", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 10,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const elementsBefore = h.elements;
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!);
|
|
||||||
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
|
||||||
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Send to back' in context menu sends element to back", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 40,
|
|
||||||
clientY: 40,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const elementsBefore = h.elements;
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!);
|
|
||||||
expect(elementsBefore[1].id).toEqual(h.elements[0].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Bring to front' in context menu brings element to front", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 10,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
const elementsBefore = h.elements;
|
|
||||||
fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!);
|
|
||||||
expect(elementsBefore[0].id).toEqual(h.elements[1].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Group selection' in context menu groups selected elements", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
|
||||||
mouse.click(10, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(
|
|
||||||
queryByText(contextMenu as HTMLElement, "Group selection")!,
|
|
||||||
);
|
|
||||||
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
|
||||||
expect(h.elements[0].groupIds).toEqual(selectedGroupIds);
|
|
||||||
expect(h.elements[1].groupIds).toEqual(selectedGroupIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("selecting 'Ungroup selection' in context menu ungroups selected group", () => {
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
|
||||||
mouse.down(10, 10);
|
|
||||||
mouse.up(20, 20);
|
|
||||||
|
|
||||||
mouse.reset();
|
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
|
||||||
mouse.click(10, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
|
||||||
Keyboard.codePress(CODES.G);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = document.querySelector(".context-menu");
|
|
||||||
fireEvent.click(
|
|
||||||
queryByText(contextMenu as HTMLElement, "Ungroup selection")!,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
|
|
||||||
expect(selectedGroupIds).toHaveLength(0);
|
|
||||||
expect(h.elements[0].groupIds).toHaveLength(0);
|
|
||||||
expect(h.elements[1].groupIds).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
|
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
|
||||||
UI.clickTool("ellipse");
|
UI.clickTool("ellipse");
|
||||||
mouse.down();
|
mouse.down();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user