返回 DeepSeek-Reasonix
mouseInput.test.ts
根目录 / desktop / electron / src / main / browser / mouseInput.test.ts
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3 import { FakeFrame, FakePage } from "./fakeGuestViews.js";
4 import { dispatchMouseInput } from "./mouseInput.js";
5 import { withFrameOperationSignal } from "./frameRuntime.js";
6 import { disposeDebugger } from "./debuggerLease.js";
7
8 function fixture(kind: "main" | "local" | "remote") {
9 const page = new FakePage(1);
10 const child = new FakeFrame(2, "https://child.test", () => undefined);
11 child.parent = page.mainFrame; page.mainFrame.children.push(child);
12 page.run = source => source.includes("reasonix:pageInputReady") ? true : kind === "main" ? null : { index: 0, x: 40, y: 20 };
13 page.debugger.respond = (method, raw) => {
14 const params = raw as Record<string, unknown>;
15 if (method === "Page.getFrameTree") return { frameTree: { frame: { id: "root" } } };
16 if (method === "Page.createIsolatedWorld") return { executionContextId: params.frameId === "root" ? 1 : 2 };
17 if (method === "Runtime.evaluate") return params.returnByValue ? { result: { value: String(params.expression).includes("reasonix:pageInputReady") ? true : null } } : { result: { objectId: "owner" } };
18 if (method === "DOM.describeNode") return { node: { frameId: "child", backendNodeId: 10 } };
19 if (method === "Target.getTargets") return { targetInfos: kind === "remote" ? [{ targetId: "child", type: "iframe" }] : [] };
20 if (method === "Target.attachToTarget") return { sessionId: "child-session" };
21 return {};
22 };
23 const routed: Array<{ sessionId?: string; params: unknown }> = [];
24 const send = page.debugger.sendCommand.bind(page.debugger);
25 page.debugger.sendCommand = async (method, params, sessionId?: string) => {
26 if (method === "Input.dispatchMouseEvent") {
27 assert.equal(sessionId, "child-session", "root CDP hit-test routing must never choose the target renderer");
28 routed.push({ sessionId, params });
29 }
30 return send(method, params);
31 };
32 return { page, child, routed };
33 }
34
35 test("mouse input selects its renderer and applies native zoom and display scale once", async () => {
36 for (const kind of ["main", "local", "remote"] as const) for (const [zoom, scale] of [[1.25, 1.25], [1, 0.5], [1, 0.75]]) {
37 const f = fixture(kind); f.page.zoom = zoom;
38 try {
39 for (const type of ["mouseMove", "mouseDown", "mouseUp"] as const) await dispatchMouseInput(f.page, { type, x: 100, y: 120, ...(type === "mouseMove" ? {} : { button: "left" as const, clickCount: 1 }) }, () => scale);
40 if (kind === "remote") {
41 assert.equal(f.page.inputs.length, 0);
42 assert.deepEqual(f.routed.map(row => row.params), [
43 { type: "mouseMoved", x: 40, y: 20, button: "none", buttons: 0, clickCount: 0 },
44 { type: "mousePressed", x: 40, y: 20, button: "left", buttons: 1, clickCount: 1 },
45 { type: "mouseReleased", x: 40, y: 20, button: "left", buttons: 0, clickCount: 1 },
46 ]);
47 assert.equal(f.page.debugger.commands.filter(row => row.method === "Target.attachToTarget").length, 1, "move, press and release share the owning target session");
48 assert.equal(f.page.debugger.commands.filter(row => row.method === "Target.detachFromTarget").length, 0);
49 } else {
50 assert.equal(f.routed.length, 0);
51 assert.deepEqual(f.page.inputs.map(event => "x" in event ? [event.x, event.y] : []), [[100, 120], [100, 120], [100, 120]]);
52 }
53 } finally { disposeDebugger(f.page.debugger); }
54 }
55 });
56
57 test("cancelled render preparation consumes late results without dispatching", async () => {
58 const f = fixture("remote"), controller = new AbortController();
59 let entered!: () => void, finish!: (value: boolean) => void;
60 const started = new Promise<void>(resolve => { entered = resolve; });
61 f.page.run = () => { entered(); return new Promise<boolean>(resolve => { finish = resolve; }); };
62 const running = withFrameOperationSignal(controller.signal, () => dispatchMouseInput(f.page, { type: "mouseMove", x: 30, y: 40 }, () => 1));
63 const rejected = assert.rejects(running, /cancelled/);
64 await started; controller.abort(); await rejected;
65 finish(true); await new Promise<void>(resolve => setImmediate(resolve));
66 assert.equal(f.page.inputs.length, 0); assert.equal(f.routed.length, 0);
67 assert.equal(f.page.debugger.commands.length, 0);
68 });
69
70 test("same-process descendants of an OOPIF use their renderer owner's coordinates", async () => {
71 const f = fixture("remote"), respond = f.page.debugger.respond;
72 const inner = new FakeFrame(3, f.child.url, () => undefined);
73 inner.parent = f.child; f.child.children.push(inner);
74 f.page.debugger.respond = (method, raw) => {
75 const params = raw as Record<string, unknown>;
76 if (method === "Page.createIsolatedWorld") return { executionContextId: params.frameId === "root" ? 1 : params.frameId === "child" ? 2 : 3 };
77 if (method === "Runtime.evaluate") {
78 if (!params.returnByValue) return { result: { objectId: `owner-${params.contextId}` } };
79 return { result: { value: params.contextId === 2 ? { index: 0, x: 4, y: 5 } : null } };
80 }
81 if (method === "DOM.describeNode") return { node: { frameId: params.objectId === "owner-1" ? "child" : "inner", backendNodeId: params.objectId === "owner-1" ? 10 : 11 } };
82 return respond(method, raw);
83 };
84 try {
85 await dispatchMouseInput(f.page, { type: "mouseDown", x: 100, y: 120, button: "left" }, () => 0.5);
86 assert.equal(f.page.inputs.length, 0);
87 assert.deepEqual(f.routed, [{ sessionId: "child-session", params: { type: "mousePressed", x: 40, y: 20, button: "left", buttons: 1, clickCount: 0 } }]);
88 } finally { disposeDebugger(f.page.debugger); }
89 });
90
91 test("frame replacement and viewport changes during routing refuse input without fallback", async () => {
92 for (const change of ["frame", "zoom", "scale", "grant"] as const) {
93 const f = fixture("remote"), respond = f.page.debugger.respond;
94 let scale = 1, valid = true;
95 f.page.debugger.respond = (method, raw) => {
96 if (method === "Runtime.evaluate" && (raw as { returnByValue?: boolean }).returnByValue) {
97 if (change === "frame") f.child.detached = true;
98 if (change === "zoom") f.page.zoom = 2;
99 if (change === "scale") scale = 0.5;
100 if (change === "grant") valid = false;
101 }
102 return respond(method, raw);
103 };
104 try {
105 await assert.rejects(dispatchMouseInput(f.page, { type: "mouseDown", x: 30, y: 40, button: "left" }, () => scale, () => { if (!valid) throw new Error("grant revoked"); }));
106 assert.equal(f.page.inputs.length, 0); assert.equal(f.routed.length, 0);
107 assert.ok(f.page.debugger.commands.some(row => row.method === "Target.detachFromTarget"));
108 } finally { disposeDebugger(f.page.debugger); }
109 }
110 });
111
111 lines TYPESCRIPT