import ReactDOM from "react-dom"; import { Excalidraw } from "../index"; import { render } from "../tests/test-utils"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { KEYS } from "../keys"; import { API } from "../tests/helpers/api"; import { actionSelectAll } from "../actions"; import { t } from "../i18n"; import { mutateElement } from "../element/mutateElement"; ReactDOM.unmountComponentAtNode(document.getElementById("root")!); const mouse = new Pointer("mouse"); const h = window.h; describe("element locking", () => { beforeEach(async () => { await render(); h.elements = []; }); it("click-selecting a locked element is disabled", () => { const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [lockedRectangle]; mouse.clickAt(50, 50); expect(API.getSelectedElements().length).toBe(0); }); it("box-selecting a locked element is disabled", () => { const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, x: 100, y: 100, }); h.elements = [lockedRectangle]; mouse.downAt(50, 50); mouse.moveTo(250, 250); mouse.upAt(250, 250); expect(API.getSelectedElements().length).toBe(0); }); it("dragging a locked element is disabled", () => { const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [lockedRectangle]; mouse.downAt(50, 50); mouse.moveTo(100, 100); mouse.upAt(100, 100); expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); }); it("you can drag element that's below a locked element", () => { const rectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", }); const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [rectangle, lockedRectangle]; mouse.downAt(50, 50); mouse.moveTo(100, 100); mouse.upAt(100, 100); expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); expect(rectangle).toEqual(expect.objectContaining({ x: 50, y: 50 })); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(rectangle.id); }); it("selectAll shouldn't select locked elements", () => { h.elements = [ API.createElement({ type: "rectangle" }), API.createElement({ type: "rectangle", locked: true }), ]; h.app.actionManager.executeAction(actionSelectAll); expect(API.getSelectedElements().length).toBe(1); }); it("clicking on a locked element should select the unlocked element beneath it", () => { const rectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", }); const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [rectangle, lockedRectangle]; expect(API.getSelectedElements().length).toBe(0); mouse.clickAt(50, 50); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(rectangle.id); }); it("right-clicking on a locked element should select it & open its contextMenu", () => { const rectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", }); const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [rectangle, lockedRectangle]; expect(API.getSelectedElements().length).toBe(0); mouse.rightClickAt(50, 50); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(lockedRectangle.id); const contextMenu = UI.queryContextMenu(); expect(contextMenu).not.toBeNull(); expect( contextMenu?.querySelector( `li[data-testid="toggleElementLock"] .context-menu-item__label`, ), ).toHaveTextContent(t("labels.elementLock.unlock")); }); it("right-clicking on element covered by locked element should ignore the locked element", () => { const rectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", }); const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, }); h.elements = [rectangle, lockedRectangle]; API.setSelectedElements([rectangle]); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(rectangle.id); mouse.rightClickAt(50, 50); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(rectangle.id); const contextMenu = UI.queryContextMenu(); expect(contextMenu).not.toBeNull(); }); it("selecting a group selects all elements including locked ones", () => { const rectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", groupIds: ["g1"], }); const lockedRectangle = API.createElement({ type: "rectangle", width: 100, backgroundColor: "red", fillStyle: "solid", locked: true, groupIds: ["g1"], x: 200, y: 200, }); h.elements = [rectangle, lockedRectangle]; mouse.clickAt(250, 250); expect(API.getSelectedElements().length).toBe(0); mouse.clickAt(50, 50); expect(API.getSelectedElements().length).toBe(2); }); it("should ignore locked text element in center of container on ENTER", () => { const container = API.createElement({ type: "rectangle", width: 100, }); const textSize = 20; const text = API.createElement({ type: "text", text: "ola", x: container.width / 2 - textSize / 2, y: container.height / 2 - textSize / 2, width: textSize, height: textSize, containerId: container.id, locked: true, }); h.elements = [container, text]; API.setSelectedElements([container]); Keyboard.keyPress(KEYS.ENTER); expect(h.state.editingElement?.id).not.toBe(text.id); expect(h.state.editingElement?.id).toBe(h.elements[1].id); }); it("should ignore locked text under cursor when clicked with text tool", () => { const text = API.createElement({ type: "text", text: "ola", x: 60, y: 0, width: 100, height: 100, locked: true, }); h.elements = [text]; UI.clickTool("text"); mouse.clickAt(text.x + 50, text.y + 50); const editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; expect(editor).not.toBe(null); expect(h.state.editingElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(2); expect(h.state.editingElement?.id).toBe(h.elements[1].id); }); it("should ignore text under cursor when double-clicked with selection tool", () => { const text = API.createElement({ type: "text", text: "ola", x: 60, y: 0, width: 100, height: 100, locked: true, }); h.elements = [text]; UI.clickTool("selection"); mouse.doubleClickAt(text.x + 50, text.y + 50); const editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; expect(editor).not.toBe(null); expect(h.state.editingElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(2); expect(h.state.editingElement?.id).toBe(h.elements[1].id); }); it("locking should include bound text", () => { const container = API.createElement({ type: "rectangle", width: 100, }); const textSize = 20; const text = API.createElement({ type: "text", text: "ola", x: container.width / 2 - textSize / 2, y: container.height / 2 - textSize / 2, width: textSize, height: textSize, containerId: container.id, }); mutateElement(container, { boundElements: [{ id: text.id, type: "text" }], }); h.elements = [container, text]; UI.clickTool("selection"); mouse.clickAt(container.x + 10, container.y + 10); Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { Keyboard.keyPress(KEYS.L); }); expect(h.elements).toEqual([ expect.objectContaining({ id: container.id, locked: true, }), expect.objectContaining({ id: text.id, locked: true, }), ]); }); it("bound text shouldn't be editable via double-click", () => { const container = API.createElement({ type: "rectangle", width: 100, locked: true, }); const textSize = 20; const text = API.createElement({ type: "text", text: "ola", x: container.width / 2 - textSize / 2, y: container.height / 2 - textSize / 2, width: textSize, height: textSize, containerId: container.id, locked: true, }); mutateElement(container, { boundElements: [{ id: text.id, type: "text" }], }); h.elements = [container, text]; UI.clickTool("selection"); mouse.doubleClickAt(container.width / 2, container.height / 2); const editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; expect(editor).not.toBe(null); expect(h.state.editingElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(3); expect(h.state.editingElement?.id).toBe(h.elements[2].id); }); it("bound text shouldn't be editable via text tool", () => { const container = API.createElement({ type: "rectangle", width: 100, locked: true, }); const textSize = 20; const text = API.createElement({ type: "text", text: "ola", x: container.width / 2 - textSize / 2, y: container.height / 2 - textSize / 2, width: textSize, height: textSize, containerId: container.id, locked: true, }); mutateElement(container, { boundElements: [{ id: text.id, type: "text" }], }); h.elements = [container, text]; UI.clickTool("text"); mouse.clickAt(container.width / 2, container.height / 2); const editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; expect(editor).not.toBe(null); expect(h.state.editingElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(3); expect(h.state.editingElement?.id).toBe(h.elements[2].id); }); });