mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 17:02:17 +00:00
Root cause: body is user-select: none and each feature had to opt back in; the Markdown preview (and other read-only views) never did, and the app-wide contextmenu guard left no Copy item. Fix: shared selectableText class (src/ui) for read-only content, and a fallback Copy context menu from browserGuards when right-clicking a selection or a selectable area (component menus still win). Siblings covered: Quick Documentation / hover / completion docs, hover errors, Quick Definition, debugger value tooltip, inspection messages, message dialogs (no drag on body text), notification titles, About dialog; existing selectable consoles now get Copy too. Verified in-app on macOS: select in Markdown preview, right-click Copy puts the text on the clipboard; tab context menu unchanged. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { copyMenuFor, copyText, selectableText } from "./selectableText";
|
|
|
|
function mount(html: string) {
|
|
document.body.innerHTML = html;
|
|
return document.body;
|
|
}
|
|
|
|
function select(node: Node) {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
const sel = window.getSelection()!;
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
}
|
|
|
|
afterEach(() => {
|
|
window.getSelection()?.removeAllRanges();
|
|
document.body.innerHTML = "";
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("selectableText", () => {
|
|
it("is a real class name", () => {
|
|
expect(selectableText).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("copyMenuFor", () => {
|
|
it("offers Copy of the selection in a selectable pane (issue #36: Markdown preview)", () => {
|
|
mount(`<div class="${selectableText}" role="document"><p id="p">Hello <b id="b">world</b></p></div>`);
|
|
select(document.getElementById("p")!);
|
|
const items = copyMenuFor(document.getElementById("b"));
|
|
expect(items).toHaveLength(1);
|
|
expect(items![0]).toMatchObject({ label: "Copy", disabled: false });
|
|
});
|
|
|
|
it("offers a disabled Copy in a selectable pane with nothing selected", () => {
|
|
mount(`<div class="${selectableText}"><p id="p">Hello</p></div>`);
|
|
expect(copyMenuFor(document.getElementById("p"))![0]).toMatchObject({ label: "Copy", disabled: true });
|
|
});
|
|
|
|
it("resolves text-node targets", () => {
|
|
mount(`<div class="${selectableText}"><p id="p">Hello</p></div>`);
|
|
expect(copyMenuFor(document.getElementById("p")!.firstChild)).not.toBeNull();
|
|
});
|
|
|
|
it("offers Copy on a selection in any pane that opted in with its own CSS", () => {
|
|
mount(`<pre id="console">line 1</pre>`);
|
|
select(document.getElementById("console")!);
|
|
expect(copyMenuFor(document.getElementById("console"))![0]).toMatchObject({ disabled: false });
|
|
});
|
|
|
|
it("stays out of chrome without a selection", () => {
|
|
mount(`<button id="b">Run</button><div id="row">tree row</div>`);
|
|
expect(copyMenuFor(document.getElementById("b"))).toBeNull();
|
|
expect(copyMenuFor(document.getElementById("row"))).toBeNull();
|
|
});
|
|
|
|
it("ignores a selection elsewhere when right-clicking chrome", () => {
|
|
mount(`<div class="${selectableText}"><p id="p">Hello</p></div><div id="row">tree row</div>`);
|
|
select(document.getElementById("p")!);
|
|
expect(copyMenuFor(document.getElementById("row"))).toBeNull();
|
|
});
|
|
|
|
it.each(["<input id='t'>", "<textarea id='t'></textarea>", "<div id='t' contenteditable='true'>x</div>"])(
|
|
"leaves editable targets alone: %s",
|
|
(html) => {
|
|
mount(`<div class="${selectableText}">${html}</div>`);
|
|
expect(copyMenuFor(document.getElementById("t"))).toBeNull();
|
|
},
|
|
);
|
|
|
|
it("copies the text captured when the menu opened", async () => {
|
|
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
|
|
mount(`<div class="${selectableText}"><p id="p">Hello</p></div>`);
|
|
select(document.getElementById("p")!);
|
|
const items = copyMenuFor(document.getElementById("p"))!;
|
|
window.getSelection()!.removeAllRanges();
|
|
(items[0] as { run: () => void }).run();
|
|
await Promise.resolve();
|
|
expect(writeText).toHaveBeenCalledWith("Hello");
|
|
});
|
|
|
|
it("falls back to execCommand when the clipboard API is missing", async () => {
|
|
Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
|
|
const exec = vi.fn().mockReturnValue(true);
|
|
document.execCommand = exec;
|
|
await copyText("x");
|
|
expect(exec).toHaveBeenCalledWith("copy");
|
|
});
|
|
});
|