fix(studio): input that carries a point is scaled with a fitted page or refused

This commit is contained in:
esengine committed 2026-10-18 05:52:52 -07:00
1 parent 50c27314e4
commit a9b47f614f
2 files changed
+55 -5

No files matched your search

+29 -5
View File
@@ -142,10 +142,11 @@ class BrowserViews {
send: (method, params) => {
if (method === "Emulation.setDeviceMetricsOverride") return this.emulate(entry, params);
if (method === "Emulation.clearDeviceMetricsOverride") return this.emulate(entry, null);
// The page reads input in the viewport it was told it has; the view
// draws that viewport scaled, and a point meant for the page lands
// where the page is drawn only after the same scaling.
if (method === "Input.dispatchMouseEvent" && entry.scale !== 1) params = { ...params, x: params.x * entry.scale, y: params.y * entry.scale };
try {
params = scaleInput(method, params, entry.scale);
} catch (e) {
return Promise.reject(e);
}
if (!method.startsWith("Input.")) return contents.debugger.sendCommand(method, params);
entry.synthUntil = Date.now() + 250;
return this.wake.run(() => contents.debugger.sendCommand(method, params));
@@ -482,6 +483,29 @@ function placement(panel, emulated) {
return { at: { x, y, width, height }, scale };
}
// Input that carries a point. The page reads it in the viewport it was told it
// has, the view draws that viewport scaled, and a point meant for the page lands
// where it is drawn only after the same scaling. The mouse is measured to; the
// rest are not, and a point that silently lands elsewhere is worse than a
// refusal, so they are refused while the page is scaled.
const POINTED_INPUT = new Set([
"Input.dispatchTouchEvent",
"Input.dispatchDragEvent",
"Input.emulateTouchFromMouseEvent",
"Input.synthesizeTapGesture",
"Input.synthesizeScrollGesture",
"Input.synthesizePinchGesture",
]);
function scaleInput(method, params, scale) {
if (scale === 1) return params;
if (method === "Input.dispatchMouseEvent") return { ...params, x: params.x * scale, y: params.y * scale };
if (POINTED_INPUT.has(method)) {
throw Object.assign(new Error(`${method} is not available while the page is scaled to fit the panel`), { code: "input_unscaled" });
}
return params;
}
function sameRect(a, b) {
return !!a && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
@@ -490,4 +514,4 @@ function validRect(rect) {
return rect && [rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) && rect.width > 0 && rect.height > 0;
}
module.exports = { BrowserViews, AGENT_VIEWPORT, LEAVE_LIMIT_MS, placement };
module.exports = { BrowserViews, AGENT_VIEWPORT, LEAVE_LIMIT_MS, placement, scaleInput };
+26
View File
@@ -2304,6 +2304,32 @@ test("a page given its own viewport is fitted whole into its panel, centred, and
assert.deepEqual(placement({ x: 20, y: 20, width: 1400, height: 1000 }, { width: 1280, height: 900 }), { at: { x: 80, y: 70, width: 1280, height: 900 }, scale: 1 });
});
test("input that carries a point is scaled with the page or refused, never sent to the wrong place", () => {
const Module = require("node:module");
const load = Module._load;
Module._load = function (request, ...rest) {
if (request === "electron") return { WebContentsView: class {}, session: { fromPartition: () => ({}) } };
return load.call(this, request, ...rest);
};
let scaleInput;
try {
({ scaleInput } = require("../src/browserviews.js"));
} finally {
Module._load = load;
}
const mouse = { type: "mousePressed", x: 1200, y: 800, button: "left" };
assert.equal(scaleInput("Input.dispatchMouseEvent", mouse, 1), mouse, "an unscaled page gets the point as it is");
assert.deepEqual(scaleInput("Input.dispatchMouseEvent", mouse, 0.5), { type: "mousePressed", x: 600, y: 400, button: "left" });
for (const method of ["Input.dispatchTouchEvent", "Input.dispatchDragEvent", "Input.emulateTouchFromMouseEvent", "Input.synthesizeTapGesture", "Input.synthesizeScrollGesture", "Input.synthesizePinchGesture"]) {
const params = { x: 10, y: 20, touchPoints: [{ x: 10, y: 20 }] };
assert.equal(scaleInput(method, params, 1), params, `${method} passes on an unscaled page`);
assert.throws(() => scaleInput(method, params, 0.5), { code: "input_unscaled" }, `${method} must not reach a scaled page unscaled`);
}
const key = { type: "keyDown", key: "a" };
assert.equal(scaleInput("Input.dispatchKeyEvent", key, 0.5), key, "keys carry no point");
assert.equal(scaleInput("Input.insertText", { text: "x" }, 0.5).text, "x");
});
test("rewind notice dismissal survives the desktop preference disk round trip", (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rewind-notice-prefs-"));
const file = path.join(dir, "prefs.json");