snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
653
packages/excalidraw/tests/arrowBinding.test.tsx
Normal file
653
packages/excalidraw/tests/arrowBinding.test.tsx
Normal file
@@ -0,0 +1,653 @@
|
||||
import { reseed } from "@excalidraw/common";
|
||||
import {
|
||||
isElbowArrow,
|
||||
projectFixedPointOntoDiagonal,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { actionToggleArrowBinding } from "../actions/actionToggleArrowBinding";
|
||||
import { Excalidraw, sceneCoordsToViewportCoords } from "../index";
|
||||
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
mockBoundingClientRect,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
waitFor,
|
||||
unmountComponent,
|
||||
} from "./test-utils";
|
||||
|
||||
unmountComponent();
|
||||
|
||||
const { h } = window;
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Fire Ctrl (or Meta on Mac) keydown on document. */
|
||||
const ctrlKeyDown = (extra: Partial<KeyboardEventInit> = {}) =>
|
||||
fireEvent.keyDown(document, {
|
||||
key: "Control",
|
||||
code: "ControlLeft",
|
||||
ctrlKey: true,
|
||||
repeat: false,
|
||||
...extra,
|
||||
});
|
||||
|
||||
/** Fire Ctrl (or Meta on Mac) keyup on document (ctrlKey is false on keyup). */
|
||||
const ctrlKeyUp = () =>
|
||||
fireEvent.keyUp(document, {
|
||||
key: "Control",
|
||||
code: "ControlLeft",
|
||||
ctrlKey: false,
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Arrow binding – non-default case (bindingPreference: disabled)", () => {
|
||||
beforeAll(() => {
|
||||
mockBoundingClientRect();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreOriginalGetBoundingClientRect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
reseed(7);
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
h.state.width = 1920;
|
||||
h.state.height = 1080;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mouse.reset();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// actionToggleArrowBinding
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe("actionToggleArrowBinding", () => {
|
||||
it("isBindingEnabled defaults to true", () => {
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("checked() reflects the current bindingPreference value", () => {
|
||||
expect(actionToggleArrowBinding.checked!(h.state)).toBe(true);
|
||||
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
expect(actionToggleArrowBinding.checked!(h.state)).toBe(false);
|
||||
});
|
||||
|
||||
it("executing the action toggles binding from enabled → disabled", () => {
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
API.executeAction(actionToggleArrowBinding);
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
expect(h.state.bindingPreference).toBe("disabled");
|
||||
});
|
||||
|
||||
it("executing the action toggles binding from disabled → enabled", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
API.executeAction(actionToggleArrowBinding);
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
expect(h.state.bindingPreference).toBe("enabled");
|
||||
});
|
||||
|
||||
it("checked() returns false after action disables binding", () => {
|
||||
API.executeAction(actionToggleArrowBinding); // true → false
|
||||
expect(actionToggleArrowBinding.checked!(h.state)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Arrow does not bind when isBindingEnabled is false
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe("Arrow drawing with binding disabled", () => {
|
||||
/**
|
||||
* Baseline: verify binding IS created with binding enabled so we know the
|
||||
* spatial setup is correct.
|
||||
*/
|
||||
it("arrow startBinding is set when binding is enabled", async () => {
|
||||
API.setElements([
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "baselineRect",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 400,
|
||||
height: 200,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
// Start inside the rectangle so startBinding can be created
|
||||
mouse.down(200, 200);
|
||||
mouse.up(700, 200);
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(el): el is ExcalidrawArrowElement => el.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.startBinding).not.toBeNull();
|
||||
expect(arrow!.startBinding!.elementId).toBe("baselineRect");
|
||||
});
|
||||
});
|
||||
|
||||
it("arrow has no startBinding when binding is disabled", async () => {
|
||||
API.setElements([
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect1",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 400,
|
||||
height: 200,
|
||||
}),
|
||||
]);
|
||||
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
UI.clickTool("arrow");
|
||||
// Start inside the rectangle – binding is off, so no startBinding
|
||||
mouse.down(200, 200);
|
||||
mouse.up(700, 200);
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(el): el is ExcalidrawArrowElement => el.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.startBinding).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("arrow has no endBinding when binding is disabled", async () => {
|
||||
API.setElements([
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect2",
|
||||
x: 500,
|
||||
y: 100,
|
||||
width: 300,
|
||||
height: 200,
|
||||
}),
|
||||
]);
|
||||
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
UI.clickTool("arrow");
|
||||
// End inside the target rectangle – binding off -> no endBinding
|
||||
mouse.down(100, 200);
|
||||
mouse.up(600, 200);
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(el): el is ExcalidrawArrowElement => el.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.endBinding).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("re-enabling binding via action causes new arrows to bind again", async () => {
|
||||
API.setElements([
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect3",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 400,
|
||||
height: 200,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Disable then re-enable binding
|
||||
API.executeAction(actionToggleArrowBinding); // true -> false
|
||||
API.executeAction(actionToggleArrowBinding); // false -> true
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
mouse.down(200, 200);
|
||||
mouse.up(700, 200);
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(el): el is ExcalidrawArrowElement => el.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.startBinding).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("elbow arrow does not snap to rectangle when binding is disabled", async () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect-elbow-no-snap",
|
||||
x: 900,
|
||||
y: 500,
|
||||
width: 200,
|
||||
height: 120,
|
||||
}) as ExcalidrawBindableElement;
|
||||
API.setElements([rect]);
|
||||
|
||||
// Turn off arrow binding
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
isMidpointSnappingEnabled: false,
|
||||
});
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
|
||||
// Create the elbow arrow
|
||||
UI.clickTool("arrow");
|
||||
API.setAppState({ currentItemArrowType: "elbow" });
|
||||
mouse.downAt(700, 400);
|
||||
mouse.upAt(760, 460);
|
||||
|
||||
let arrow: ExcalidrawArrowElement;
|
||||
await waitFor(() => {
|
||||
const maybeArrow = h.elements.find(isElbowArrow);
|
||||
expect(maybeArrow).toBeDefined();
|
||||
arrow = maybeArrow!;
|
||||
});
|
||||
|
||||
// Move the elbow arrow
|
||||
UI.clickTool("selection");
|
||||
|
||||
const insideRectanglePoint = pointFrom<GlobalPoint>(1010, 570);
|
||||
const originalArrowEndGlobal = pointFrom<GlobalPoint>(
|
||||
arrow!.x + arrow!.points[arrow!.points.length - 1][0],
|
||||
arrow!.y + arrow!.points[arrow!.points.length - 1][1],
|
||||
);
|
||||
const originalArrowEndViewport = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: originalArrowEndGlobal[0],
|
||||
sceneY: originalArrowEndGlobal[1],
|
||||
},
|
||||
h.state,
|
||||
);
|
||||
const insideRectangleViewport = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: insideRectanglePoint[0],
|
||||
sceneY: insideRectanglePoint[1],
|
||||
},
|
||||
h.state,
|
||||
);
|
||||
|
||||
// End point dragged inside the rectangle; with snapping enabled this
|
||||
// would snap to the rectangle outline.
|
||||
mouse.moveTo(originalArrowEndViewport.x, originalArrowEndViewport.y);
|
||||
mouse.down();
|
||||
mouse.moveTo(insideRectangleViewport.x, insideRectangleViewport.y);
|
||||
mouse.up();
|
||||
|
||||
const updatedEnd = arrow!.points[arrow!.points.length - 1];
|
||||
const updatedEndGlobal = pointFrom<GlobalPoint>(
|
||||
arrow!.x + updatedEnd[0],
|
||||
arrow!.y + updatedEnd[1],
|
||||
);
|
||||
|
||||
expect(arrow!.startBinding).toBeNull();
|
||||
expect(arrow!.endBinding).toBeNull();
|
||||
expect(updatedEndGlobal).not.toEqual(originalArrowEndGlobal);
|
||||
expect(updatedEndGlobal).toEqual(insideRectanglePoint);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Arrow does snap to midpoint when isMidpointSnappingEnabled is true
|
||||
// -------------------------------------------------------------------------
|
||||
describe("Midpoint snapping", () => {
|
||||
it("does not snap to midpoint when midpoint snapping is turned off", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rectNoMidSnap",
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 400,
|
||||
height: 200,
|
||||
}) as ExcalidrawBindableElement;
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
x: 0,
|
||||
y: 250,
|
||||
width: 502,
|
||||
height: -48,
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(502, -48)],
|
||||
}) as ExcalidrawArrowElement;
|
||||
const elementsMap = new Map<string, ExcalidrawElement>([
|
||||
[rect.id, rect],
|
||||
[arrow.id, arrow],
|
||||
]);
|
||||
const point = pointFrom<GlobalPoint>(502, 202);
|
||||
|
||||
const snappedWithMidpoint = projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
point,
|
||||
rect,
|
||||
"end",
|
||||
elementsMap,
|
||||
h.state.zoom,
|
||||
true,
|
||||
);
|
||||
const snappedWithoutMidpoint = projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
point,
|
||||
rect,
|
||||
"end",
|
||||
elementsMap,
|
||||
h.state.zoom,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(snappedWithMidpoint).toEqual([500, 200]);
|
||||
expect(snappedWithoutMidpoint).not.toEqual([500, 200]);
|
||||
});
|
||||
|
||||
it("does not snap an angle-locked elbow binding to midpoint on finalize", async () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect-elbow-angle-lock",
|
||||
x: 600,
|
||||
y: 300,
|
||||
width: 200,
|
||||
height: 200,
|
||||
}) as ExcalidrawBindableElement;
|
||||
API.setElements([rect]);
|
||||
API.setAppState({
|
||||
currentItemArrowType: "elbow",
|
||||
gridModeEnabled: false,
|
||||
isMidpointSnappingEnabled: true,
|
||||
});
|
||||
|
||||
const start = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 480, sceneY: 392 },
|
||||
h.state,
|
||||
);
|
||||
const target = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 596, sceneY: 397 },
|
||||
h.state,
|
||||
);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
mouse.downAt(start.x, start.y);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(target.x, target.y);
|
||||
mouse.upAt();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(isElbowArrow);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.endBinding?.elementId).toBe(rect.id);
|
||||
|
||||
const endY = arrow!.y + arrow!.points.at(-1)![1];
|
||||
expect(endY).toBeCloseTo(397, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grid binding interactions", () => {
|
||||
it("does not snap angle-locked binding to grid when grid mode is disabled", async () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
id: "rect-angle-lock-no-grid",
|
||||
x: 600,
|
||||
y: 300,
|
||||
width: 200,
|
||||
height: 200,
|
||||
}) as ExcalidrawBindableElement;
|
||||
API.setElements([rect]);
|
||||
API.setAppState({ gridModeEnabled: false, gridSize: 20 });
|
||||
|
||||
const start = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 300, sceneY: 333 },
|
||||
h.state,
|
||||
);
|
||||
const target = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 596, sceneY: 347 },
|
||||
h.state,
|
||||
);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
mouse.downAt(start.x, start.y);
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(target.x, target.y);
|
||||
mouse.upAt();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(element): element is ExcalidrawArrowElement =>
|
||||
element.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.endBinding?.elementId).toBe(rect.id);
|
||||
|
||||
const startY = arrow!.y + arrow!.points[0][1];
|
||||
const endY = arrow!.y + arrow!.points.at(-1)![1];
|
||||
expect(endY).toBeCloseTo(startY);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the incoming direction to choose the grid axis for rotated bindables", async () => {
|
||||
const ellipse = API.createElement({
|
||||
type: "ellipse",
|
||||
id: "ellipse-rotated-grid-binding",
|
||||
x: 610,
|
||||
y: 300,
|
||||
width: 240,
|
||||
height: 120,
|
||||
angle: Math.PI / 3,
|
||||
}) as ExcalidrawBindableElement;
|
||||
API.setElements([ellipse]);
|
||||
API.setAppState({ gridModeEnabled: true, gridSize: 20 });
|
||||
|
||||
const start = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 400, sceneY: 360 },
|
||||
h.state,
|
||||
);
|
||||
const target = sceneCoordsToViewportCoords(
|
||||
{ sceneX: 658, sceneY: 360 },
|
||||
h.state,
|
||||
);
|
||||
|
||||
UI.clickTool("arrow");
|
||||
mouse.downAt(start.x, start.y);
|
||||
mouse.moveTo(target.x, target.y);
|
||||
mouse.upAt();
|
||||
|
||||
await waitFor(() => {
|
||||
const arrow = h.elements.find(
|
||||
(element): element is ExcalidrawArrowElement =>
|
||||
element.type === "arrow",
|
||||
);
|
||||
expect(arrow).toBeDefined();
|
||||
expect(arrow!.endBinding?.elementId).toBe(ellipse.id);
|
||||
|
||||
const startY = arrow!.y + arrow!.points[0][1];
|
||||
const endY = arrow!.y + arrow!.points.at(-1)![1];
|
||||
expect(endY).toBeCloseTo(startY, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Ctrl / Cmd key toggle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe("Ctrl key toggle when binding preference is disabled", () => {
|
||||
it("Ctrl keydown temporarily enables binding when preference is disabled", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
ctrlKeyDown();
|
||||
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("Ctrl keyup restores isBindingEnabled to false after the temporary toggle", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("full round-trip: off → Ctrl down → on → Ctrl up → off", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("full round-trip can be repeated after Ctrl release resets state", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
// First cycle
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
|
||||
// Second cycle
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ctrl key toggle when binding starts ON", () => {
|
||||
it("Ctrl keydown temporarily disables binding when it was on", () => {
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("Ctrl keyup restores isBindingEnabled to true after the temporary toggle", () => {
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// event.repeat guard
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe("event.repeat guard", () => {
|
||||
it("Ctrl keydown with repeat=true does not toggle binding (preference: disabled)", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
});
|
||||
|
||||
ctrlKeyDown({ repeat: true });
|
||||
|
||||
// Must remain off – repeat events are ignored
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("Ctrl keydown with repeat=true does not toggle binding (default: on)", () => {
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyDown({ repeat: true });
|
||||
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("pressing another key while Ctrl held does not change binding state", () => {
|
||||
// Start ON, first Ctrl keydown flips to false
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
|
||||
ctrlKeyDown();
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
|
||||
// A second keydown with Ctrl (e.g. pressing another key while Ctrl held)
|
||||
// should leave state unchanged
|
||||
fireEvent.keyDown(document, {
|
||||
key: "z",
|
||||
ctrlKey: true,
|
||||
repeat: false,
|
||||
});
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
|
||||
// Ctrl keyup restores to preference value
|
||||
ctrlKeyUp();
|
||||
expect(h.state.isBindingEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// View-mode guard
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
describe("View-mode guard", () => {
|
||||
it("Ctrl keydown in viewMode does not toggle isBindingEnabled", () => {
|
||||
API.setAppState({
|
||||
bindingPreference: "disabled",
|
||||
isBindingEnabled: false,
|
||||
viewModeEnabled: true,
|
||||
});
|
||||
|
||||
ctrlKeyDown();
|
||||
|
||||
// Handler returns early in viewMode — state must stay false
|
||||
expect(h.state.isBindingEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user