Файли
openidea/src/ui/useSelectionBoundary.test.tsx
T
l0ng-ai 195356c4b1 fix(agents): keep text selection inside the conversation (#437)
A drag that started in the conversation could extend the selection into neighbouring panes, and one dragged up from another pane spilled into the conversation. While a drag starts in the conversation everything outside it is unselectable, and a selection crossing the conversation's edge is clamped to it from either direction.
2026-10-30 19:44:28 +08:00

90 рядки
3.1 KiB
TypeScript

import { useRef } from "react";
import { cleanup, render } from "@testing-library/react";
import { afterEach, expect, it } from "vitest";
import { useSelectionBoundary } from "./useSelectionBoundary";
afterEach(() => {
window.getSelection()?.removeAllRanges();
cleanup();
});
function Pane() {
const ref = useRef<HTMLDivElement>(null);
useSelectionBoundary(ref);
return (
<div ref={ref}>
<p>First message</p>
<p>Last message</p>
</div>
);
}
function fixture() {
const view = render(
<>
<div>Header</div>
<Pane />
<div>Reply to the agent</div>
<div>Branch or tag</div>
</>,
);
const text = (value: string) => view.getByText(value).firstChild!;
const select = (anchor: Node, start: number, focus: Node, end: number) => {
const selection = window.getSelection()!;
selection.setBaseAndExtent(anchor, start, focus, end);
document.dispatchEvent(new Event("selectionchange"));
return selection;
};
return { ...view, text, select };
}
it("clamps a downward selection before the composer and Git panel", () => {
const { text, select } = fixture();
const selection = select(text("Last message"), 5, text("Branch or tag"), 13);
expect(selection.toString()).toBe("message");
expect(selection.anchorNode).toBe(text("Last message"));
expect(selection.anchorOffset).toBe(5);
});
it("clamps an upward selection while retaining its direction", () => {
const { text, select } = fixture();
const selection = select(text("First message"), 5, text("Header"), 0);
expect(selection.toString()).toBe("First");
expect(selection.anchorNode).toBe(text("First message"));
expect(selection.anchorOffset).toBe(5);
});
it("preserves cross-message selection and lets the endpoint return inside", () => {
const { text, select } = fixture();
select(text("First message"), 6, text("Branch or tag"), 13);
const selection = select(text("First message"), 6, text("Last message"), 4);
expect(selection.toString()).toBe("messageLast");
expect(selection.focusNode).toBe(text("Last message"));
});
it("clamps a selection dragged up from another pane into the conversation", () => {
const { text, select } = fixture();
const selection = select(text("Branch or tag"), 6, text("Last message"), 4);
expect(selection.toString()).toBe(" message");
expect(selection.focusNode).toBe(text("Last message"));
});
it("leaves selections that do not touch the conversation alone", () => {
const { text, select } = fixture();
const selection = select(text("Reply to the agent"), 0, text("Branch or tag"), 13);
expect(selection.toString()).toBe("Reply to the agentBranch or tag");
});
it("removes the boundary listener when the pane unmounts", () => {
const { text, select, unmount } = fixture();
const first = text("First message");
const last = text("Branch or tag");
const container = first.parentElement!.parentElement!.parentElement!;
const children = [...container.childNodes];
unmount();
container.append(...children);
document.body.append(container);
expect(select(first, 0, last, 13).toString()).toContain("Branch or tag");
container.remove();
});