Files
Triple-C/app/src/lib/dropTarget.test.ts
T
shadowdaoandClaude Opus 5 c0e4c87cec
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-linux (pull_request) Successful in 5m8s
Build App (Preview) / build-windows (pull_request) Successful in 6m28s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Build Container / build-container (pull_request) Failing after 14m49s
Give the mouse back, retire the follow controls, update Claude per session
Three things the terminal was getting wrong.

**A program that grabs the mouse and dies used to freeze the tab.** A TUI sets
DECSET ?1000/?1002/?1003; if it exits without resetting them, xterm keeps
routing clicks, drags and — under ?1003 — every pointer *move* to the PTY.
Text selection dies and escape bytes flood the prompt. The only exit was
closing the tab. `TerminalView` now reconciles a flag against
`term.modes.mouseTrackingMode` in the `term.write()` callback — the mode only
changes because the container printed a sequence, so one check per write
catches every transition with no polling — and `Ctrl+Shift+X` or a status-bar
button writes the resets back through `term.write`, never `sendInput`: the
reset belongs to xterm's parser, and a still-live TUI told about it would just
re-grab on its next repaint.

The control is in the status bar deliberately. Mouse tracking is the *normal*
state of htop, vim, lazygit and Claude Code, so a badge over the terminal
would be on screen for the whole life of those programs and would swallow
clicks aimed at their own top-right corner. `macOptionClickForcesSelection` is
also on now: xterm's force-select is Shift everywhere except macOS, where it
is Option and is gated behind that option, which defaults to false — so until
now Mac users had no way to select text while a program held the mouse.

**"Following" and "Jump to Current" are gone.** Claude Code draws on the
alternate screen, which has no scrollback, so `viewportY` always equalled
`baseY` and neither control could do anything. They did still work in bash
tabs; xterm's native follow covers that, and the per-write `scrollToBottom()`
went with them because it fought exactly that. What remains, on activate and
after a refit, now samples `viewportY >= baseY` *before* the fit, so opening
the Notes dock no longer yanks a reader to the tail.

**`claude update` runs before every Claude session, not just at container
start.** Containers here stop/start and often just keep running, so a
long-lived one never re-checked. Both copies take the same flock: the
entrypoint prints "container ready" only after its own update finishes, so
opening a tab immediately would otherwise run two updaters against the same
~/.claude/bin, with `|| echo` hiding a half-written install one line before
`exec claude` ran it.

This turns the non-Bedrock path from a bare argv into a `bash -c` wrapper, so
flags and session names are shell-interpolated now and must go through
`shell_quote_arg`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n
2026-09-08 11:02:33 -07:00

363 lines
15 KiB
TypeScript

import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
import { classifyDrop, dropIsBlocked, isDropTarget } from "./dropTarget";
function pane(rect: Partial<DOMRect>): HTMLElement {
const el = document.createElement("div");
document.body.appendChild(el);
el.getBoundingClientRect = () =>
({
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
x: 0,
y: 0,
toJSON: () => ({}),
...rect,
}) as DOMRect;
return el;
}
/**
* **jsdom has no `elementFromPoint`.** That single fact is how a z-order gate
* that refused every drop under a button, a toast and a dialog shipped green
* through 81 drop tests: the branch was never entered, in any of them.
*
* The gate no longer asks a per-point question at all, so the gap can no
* longer hide anything here — but the tests below still *install* an
* `elementFromPoint` and hand it the most misleading answer available, because
* "the module ignores it" is now a property worth pinning. `spy.mock.calls`
* proves it directly.
*/
function stubElementFromPoint(top: Element | null): ReturnType<typeof vi.fn> {
const spy = vi.fn(() => top);
Object.defineProperty(document, "elementFromPoint", {
configurable: true,
writable: true,
value: spy,
});
return spy;
}
function removeElementFromPoint(): void {
delete (document as Partial<Document>).elementFromPoint;
}
/** The DOM `ui/Modal` really renders: a marked backdrop around the dialog. */
function openModal(): { backdrop: HTMLElement; panel: HTMLElement; button: HTMLElement } {
const backdrop = document.createElement("div");
backdrop.setAttribute("data-blocks-drop", "true");
const panel = document.createElement("div");
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-modal", "true");
const button = document.createElement("button");
panel.appendChild(button);
backdrop.appendChild(panel);
document.body.appendChild(backdrop);
return { backdrop, panel, button };
}
function withUserAgent(ua: string): void {
Object.defineProperty(window.navigator, "userAgent", {
configurable: true,
value: ua,
});
}
const REAL_UA = window.navigator.userAgent;
const REAL_DPR = window.devicePixelRatio;
function withDevicePixelRatio(value: number): void {
Object.defineProperty(window, "devicePixelRatio", { configurable: true, value });
}
describe("dropTarget", () => {
beforeEach(() => {
document.body.innerHTML = "";
});
afterEach(() => {
document.body.innerHTML = "";
removeElementFromPoint();
withUserAgent(REAL_UA);
withDevicePixelRatio(REAL_DPR);
});
it("accepts a point inside the pane", () => {
expect(isDropTarget(pane({}), { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe(true);
});
it("rejects a point outside the pane", () => {
expect(isDropTarget(pane({}), { x: 400, y: 50 }, { devicePixelRatio: 1 })).toBe(false);
});
it("converts physical pixels to CSS pixels", () => {
const el = pane({});
expect(isDropTarget(el, { x: 150, y: 150 }, { devicePixelRatio: 2 })).toBe(true);
expect(isDropTarget(el, { x: 150, y: 150 }, { devicePixelRatio: 1 })).toBe(false);
});
it("rejects a hidden pane, which has a zero-size rect", () => {
const el = pane({ right: 0, bottom: 0, width: 0, height: 0 });
expect(isDropTarget(el, { x: 0, y: 0 }, { devicePixelRatio: 1 })).toBe(false);
});
it("rejects every drop while a modal is open", () => {
const el = pane({});
const dialog = document.createElement("div");
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
document.body.appendChild(dialog);
expect(dropIsBlocked()).toBe(true);
expect(isDropTarget(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe(false);
dialog.remove();
expect(dropIsBlocked()).toBe(false);
expect(isDropTarget(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe(true);
});
it("rejects every drop while a blocking overlay is up", () => {
const el = pane({});
const overlay = document.createElement("div");
overlay.setAttribute("data-blocks-drop", "true");
document.body.appendChild(overlay);
expect(isDropTarget(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe(false);
});
it("rejects a null pane", () => {
expect(isDropTarget(null, { x: 1, y: 1 }, { devicePixelRatio: 1 })).toBe(false);
});
// ---------------------------------------------------------------------
// The gate itself: document-wide, and provably not geometric
// ---------------------------------------------------------------------
describe("the blocking gate is document-wide, with no z-order in it", () => {
it("never consults elementFromPoint, however tempting its answer", () => {
// Round 2 asked `elementFromPoint` whether a *blocker* was painted at
// the drop point, and trusted the answer absolutely. Anything painted
// above the `z-50` backdrop in the same stacking context — `ToastHost`
// at `z-[60]`, `TerminalContextMenu` at `z-[60]` — answered "no blocker
// here" on a point the dialog was covering. The fix is not a better
// answer, it is not asking: this pins that the call is gone, so a
// future edit that reintroduces it fails here rather than in the wild.
const el = pane({});
const child = document.createElement("span");
el.appendChild(child);
const spy = stubElementFromPoint(child);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
expect(spy).not.toHaveBeenCalled();
});
it("refuses a covered drop even when a toast is painted over the dialog", () => {
// C1, exactly. A refused drop pushes a toast; `ToastHost` is
// `fixed bottom-4 right-4 z-[60]` and an error card stays until
// dismissed; the *next* drop released on that card had a topmost
// element with no blocker in it, and landed in the directory the dialog
// was covering. The gate had armed its own hole.
const el = pane({});
openModal(); // z-50 backdrop, covering the pane
const toastCard = document.createElement("div"); // z-[60], over the backdrop
document.body.appendChild(toastCard);
const spy = stubElementFromPoint(toastCard);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
expect(spy).not.toHaveBeenCalled();
});
it("refuses a drop anywhere in the window while a dialog is open", () => {
// Deliberately stricter than "the points the dialog covers". A dialog
// is a state the user entered on purpose and leaves with Escape, the
// refusal is announced, and nothing is written — whereas being precise
// about coverage has silently uploaded into a covered directory twice.
const el = pane({});
const { backdrop } = openModal();
stubElementFromPoint(document.createElement("div")); // "nothing here"
expect(classifyDrop(el, { x: 5, y: 5 }, { devicePixelRatio: 1 })).toBe("blocked");
expect(classifyDrop(el, { x: 95, y: 95 }, { devicePixelRatio: 1 })).toBe("blocked");
backdrop.remove();
expect(classifyDrop(el, { x: 5, y: 5 }, { devicePixelRatio: 1 })).toBe("accept");
});
it("refuses under the shutdown overlay, which is not a dialog", () => {
const el = pane({});
const overlay = document.createElement("div");
overlay.setAttribute("data-blocks-drop", "true");
document.body.appendChild(overlay);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
});
it("still refuses a dialog built without ui/Modal's marked backdrop", () => {
// Only `aria-modal` is needed; `ui/Modal` is the supported route, but a
// hand-rolled dialog must not be a hole either.
const el = pane({});
const backdrop = document.createElement("div");
const panel = document.createElement("div");
panel.setAttribute("aria-modal", "true");
backdrop.appendChild(panel);
document.body.appendChild(backdrop);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
});
it("counts a blocker that is only decoratively hidden from assistive tech", () => {
// `aria-hidden="true"` used to disqualify a blocker, and it is not a
// visibility statement — `ui/Modal`'s own ✕ glyph carries it while
// perfectly visible. A blocker nested inside such a wrapper would have
// silently stopped blocking. Only `[hidden]` counts now.
const el = pane({});
const wrapper = document.createElement("div");
wrapper.setAttribute("aria-hidden", "true");
const overlay = document.createElement("div");
overlay.setAttribute("data-blocks-drop", "true");
wrapper.appendChild(overlay);
document.body.appendChild(wrapper);
expect(dropIsBlocked()).toBe(true);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
});
it("ignores a blocker whose pane has stepped aside", () => {
// `ui/Modal` marks itself `hidden` when the tab that owns it is not the
// one on screen. A dialog left open in project A must not keep refusing
// drops in project B — this is the one case where over-refusal would be
// unbounded, since the user cannot see the dialog to close it.
const el = pane({});
const { backdrop } = openModal();
backdrop.setAttribute("hidden", "");
expect(dropIsBlocked()).toBe(false);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
});
});
// ---------------------------------------------------------------------
// Round 1's shape: chrome painted over a pane must never refuse a drop
// ---------------------------------------------------------------------
describe("chrome over a pane, with no dialog open", () => {
/** Everything that is painted over a pane and is not a blocker. */
const CHROME: Array<[string, () => HTMLElement]> = [
// `TerminalView`'s mouse-release badge: `absolute top-2 right-4 z-50`,
// and a *sibling* of the xterm host — so "does the pane contain what is
// painted here?" made the terminal's top-right corner a dead zone no
// user action could clear. (The retired Following toggle held the same
// corner and produced the original bug.)
["the mouse-release badge", () => document.createElement("button")],
// `ToastHost`: `fixed bottom-4 right-4 z-[60]`, 24rem wide, over every
// pane, and its error cards stay until dismissed.
["a toast card", () => document.createElement("div")],
// The drop hint is `pointer-events-none`, so a real `elementFromPoint`
// skips it — but nothing may depend on that any more.
["the pane's own drop hint", () => document.createElement("div")],
// `Tooltip` portals to `document.body`, so it is nobody's child.
["a portaled tooltip", () => document.createElement("div")],
];
for (const [name, make] of CHROME) {
it(`accepts a drop released onto ${name}`, () => {
const el = pane({});
const chrome = make();
document.body.appendChild(chrome); // a sibling, not a child of the pane
stubElementFromPoint(chrome);
expect(classifyDrop(el, { x: 90, y: 5 }, { devicePixelRatio: 1 })).toBe("accept");
expect(classifyDrop(el, { x: 95, y: 95 }, { devicePixelRatio: 1 })).toBe("accept");
});
}
it("accepts a drop on the gutter around the terminal, and on its content", () => {
const el = pane({});
const child = document.createElement("span");
el.appendChild(child);
stubElementFromPoint(child);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
stubElementFromPoint(el); // the gutter: the wrapper itself is topmost
expect(classifyDrop(el, { x: 1, y: 99 }, { devicePixelRatio: 1 })).toBe("accept");
});
it("accepts a drop the view could not resolve to any element", () => {
// `elementFromPoint` answers `null` outside the viewport and `<body>`
// over nothing in particular. With no dialog open neither is a reason
// to refuse a drop that is inside the pane's rect.
const el = pane({});
stubElementFromPoint(null);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
stubElementFromPoint(document.body);
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
});
});
// ---------------------------------------------------------------------
// Routing: exactly one listener speaks for a given drop
// ---------------------------------------------------------------------
describe("routing", () => {
it("lets only the pane the drop landed on report a refusal", () => {
// Geometry is asked before the gate for this reason: both listeners are
// live for every drop, and if the gate came first they would both push
// "File drop ignored" for one drop.
const hit = pane({});
const missed = pane({ left: 200, right: 300, top: 200, bottom: 300 });
openModal();
expect(classifyDrop(hit, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
expect(classifyDrop(missed, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe(
"elsewhere",
);
});
it("says elsewhere, not blocked, for a hidden pane's zero-size rect", () => {
const hidden = pane({ right: 0, bottom: 0, width: 0, height: 0 });
openModal();
expect(classifyDrop(hidden, { x: 0, y: 0 }, { devicePixelRatio: 1 })).toBe(
"elsewhere",
);
});
});
// ---------------------------------------------------------------------
// HiDPI
// ---------------------------------------------------------------------
describe("physical vs logical payload coordinates", () => {
it("divides by devicePixelRatio on Windows", () => {
// wry's WebView2 drag-drop handler passes the OS point through in device
// pixels, so a physical (150,150) at dpr 2 is a CSS (75,75).
withUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
withDevicePixelRatio(2);
expect(isDropTarget(pane({}), { x: 150, y: 150 })).toBe(true);
});
it("does not divide on macOS or Linux, where the payload is already logical", () => {
// The macOS and GTK backends deliver logical points and
// `tauri-runtime-wry` does not rescale them. Halving one there aimed the
// hit test at a point the user never touched — harmless while the test
// was a bare rect, and a refused drop once z-order joined in.
withDevicePixelRatio(2);
withUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15");
expect(isDropTarget(pane({}), { x: 150, y: 150 })).toBe(false);
expect(isDropTarget(pane({}), { x: 50, y: 50 })).toBe(true);
withUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36");
expect(isDropTarget(pane({}), { x: 150, y: 150 })).toBe(false);
expect(isDropTarget(pane({}), { x: 50, y: 50 })).toBe(true);
});
it("takes an explicit override over the platform guess", () => {
withUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15");
withDevicePixelRatio(2);
expect(
isDropTarget(pane({}), { x: 150, y: 150 }, { physicalPixelPayload: true }),
).toBe(true);
});
});
});