2026-08-23 15:31:13 -07:00
|
|
|
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
2026-08-23 13:03:57 -07:00
|
|
|
import { classifyDrop, dropIsBlocked, isDropTarget } from "./dropTarget";
|
2026-08-23 11:11:43 -07:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 13:03:57 -07:00
|
|
|
/**
|
|
|
|
|
* **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
|
2026-08-23 15:31:13 -07:00
|
|
|
* 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.
|
2026-08-23 13:03:57 -07:00
|
|
|
*/
|
2026-08-23 15:31:13 -07:00
|
|
|
function stubElementFromPoint(top: Element | null): ReturnType<typeof vi.fn> {
|
|
|
|
|
const spy = vi.fn(() => top);
|
2026-08-23 13:03:57 -07:00
|
|
|
Object.defineProperty(document, "elementFromPoint", {
|
|
|
|
|
configurable: true,
|
|
|
|
|
writable: true,
|
2026-08-23 15:31:13 -07:00
|
|
|
value: spy,
|
2026-08-23 13:03:57 -07:00
|
|
|
});
|
2026-08-23 15:31:13 -07:00
|
|
|
return spy;
|
2026-08-23 13:03:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 11:11:43 -07:00
|
|
|
describe("dropTarget", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
document.body.innerHTML = "";
|
|
|
|
|
});
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
document.body.innerHTML = "";
|
2026-08-23 13:03:57 -07:00
|
|
|
removeElementFromPoint();
|
|
|
|
|
withUserAgent(REAL_UA);
|
|
|
|
|
withDevicePixelRatio(REAL_DPR);
|
2026-08-23 11:11:43 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
2026-08-23 13:03:57 -07:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
2026-08-23 15:31:13 -07:00
|
|
|
// The gate itself: document-wide, and provably not geometric
|
2026-08-23 13:03:57 -07:00
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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.
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
|
|
|
|
const child = document.createElement("span");
|
|
|
|
|
el.appendChild(child);
|
2026-08-23 15:31:13 -07:00
|
|
|
const spy = stubElementFromPoint(child);
|
2026-08-23 13:03:57 -07:00
|
|
|
|
|
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
|
2026-08-23 15:31:13 -07:00
|
|
|
expect(spy).not.toHaveBeenCalled();
|
2026-08-23 13:03:57 -07:00
|
|
|
});
|
|
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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.
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
2026-08-23 15:31:13 -07:00
|
|
|
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);
|
2026-08-23 13:03:57 -07:00
|
|
|
|
|
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
|
2026-08-23 15:31:13 -07:00
|
|
|
expect(spy).not.toHaveBeenCalled();
|
2026-08-23 13:03:57 -07:00
|
|
|
});
|
|
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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.
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
2026-08-23 15:31:13 -07:00
|
|
|
const { backdrop } = openModal();
|
|
|
|
|
stubElementFromPoint(document.createElement("div")); // "nothing here"
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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");
|
|
|
|
|
});
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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);
|
2026-08-23 13:03:57 -07:00
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("blocked");
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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.
|
2026-08-23 13:03:57 -07:00
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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.
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
2026-08-23 15:31:13 -07:00
|
|
|
const wrapper = document.createElement("div");
|
|
|
|
|
wrapper.setAttribute("aria-hidden", "true");
|
2026-08-23 13:03:57 -07:00
|
|
|
const overlay = document.createElement("div");
|
|
|
|
|
overlay.setAttribute("data-blocks-drop", "true");
|
2026-08-23 15:31:13 -07:00
|
|
|
wrapper.appendChild(overlay);
|
|
|
|
|
document.body.appendChild(wrapper);
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
expect(dropIsBlocked()).toBe(true);
|
2026-08-23 13:03:57 -07:00
|
|
|
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
|
2026-08-23 15:31:13 -07:00
|
|
|
// 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.
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
|
|
|
|
const { backdrop } = openModal();
|
|
|
|
|
backdrop.setAttribute("hidden", "");
|
|
|
|
|
|
|
|
|
|
expect(dropIsBlocked()).toBe(false);
|
|
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
|
|
|
|
|
});
|
2026-08-23 15:31:13 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// Round 1's shape: chrome painted over a pane must never refuse a drop
|
|
|
|
|
// ---------------------------------------------------------------------
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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 "▼ Following / ▽ Paused" toggle: `absolute top-2
|
|
|
|
|
// right-4 z-50`, rendered unconditionally, 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 Following/Paused toggle", () => 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", () => {
|
2026-08-23 13:03:57 -07:00
|
|
|
const el = pane({});
|
2026-08-23 15:31:13 -07:00
|
|
|
const child = document.createElement("span");
|
|
|
|
|
el.appendChild(child);
|
|
|
|
|
stubElementFromPoint(child);
|
|
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
stubElementFromPoint(el); // the gutter: the wrapper itself is topmost
|
|
|
|
|
expect(classifyDrop(el, { x: 1, y: 99 }, { devicePixelRatio: 1 })).toBe("accept");
|
|
|
|
|
});
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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");
|
2026-08-23 13:03:57 -07:00
|
|
|
stubElementFromPoint(document.body);
|
2026-08-23 15:31:13 -07:00
|
|
|
expect(classifyDrop(el, { x: 50, y: 50 }, { devicePixelRatio: 1 })).toBe("accept");
|
2026-08-23 13:03:57 -07:00
|
|
|
});
|
2026-08-23 15:31:13 -07:00
|
|
|
});
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// Routing: exactly one listener speaks for a given drop
|
|
|
|
|
// ---------------------------------------------------------------------
|
2026-08-23 13:03:57 -07:00
|
|
|
|
2026-08-23 15:31:13 -07:00
|
|
|
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",
|
|
|
|
|
);
|
2026-08-23 13:03:57 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-08-23 11:11:43 -07:00
|
|
|
});
|