From 548e84041eaaab1a8467b2fcf2951f67402d088d Mon Sep 17 00:00:00 2001 From: wcy12378 Date: Sat, 24 Oct 2026 17:11:36 +0800 Subject: [PATCH 1/2] fix(interaction): route fill and press typing into the ref's CDP session --- .../src/tools/__tests__/interaction.test.ts | 121 ++++++++++++++++++ apps/extension/src/tools/interaction.ts | 18 ++- 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/apps/extension/src/tools/__tests__/interaction.test.ts b/apps/extension/src/tools/__tests__/interaction.test.ts index 8184b6dc..7be4373f 100644 --- a/apps/extension/src/tools/__tests__/interaction.test.ts +++ b/apps/extension/src/tools/__tests__/interaction.test.ts @@ -1511,6 +1511,69 @@ describe("handleFill", () => { expect(callFns).toHaveLength(5); }); + it("routes fill typing into the ref's OOPIF child session", async () => { + // DOM.focus already resolves the node in its own session; the typing + // that follows must use the same session, or the text lands in the + // main frame while the OOPIF field stays empty (issue #438). + const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); + const ctx = await sm.start("aa11"); + ctx.refStore.set("e3", 1234, { + tabId: 4, + frameId: "child-frame", + cdpSessionId: "child-session", + }); + const fake = makeFakeCdp({ + "DOM.scrollIntoViewIfNeeded": () => ({}), + "Runtime.evaluate": () => ({ result: { value: { width: 200, height: 100 } } }), + "Page.getLayoutMetrics": () => ({ cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }), + }); + fake.cdp.getFrameGraph = vi.fn(async () => ({ + rootFrameId: "main", + frames: [ + { frameId: "main", target: { tabId: 4 } }, + { + frameId: "child-frame", + parentFrameId: "main", + ownerBackendNodeId: 99, + target: { tabId: 4, sessionId: "child-session" }, + }, + ], + })); + const targetCalls: Array<{ sessionId?: string; method: string }> = []; + fake.cdp.sendToTarget = vi.fn(async (target, method, params) => { + targetCalls.push({ sessionId: target.sessionId, method }); + if (method === "DOM.describeNode") { + return { + node: { backendNodeId: 1234, nodeName: "INPUT", attributes: ["type", "text"] }, + }; + } + if (method === "DOM.scrollIntoViewIfNeeded") return {}; + if (method === "DOM.focus") return {}; + if (method === "DOM.resolveNode") return { object: { objectId: "obj-oopif" } }; + if (method === "Runtime.callFunctionOn") return successfulFillScript(params); + if (method === "Runtime.releaseObject") return {}; + if (method === "Input.insertText") return {}; + if (method === "Runtime.evaluate") return { result: { value: { width: 200, height: 100 } } }; + if (method === "Page.getLayoutMetrics") { + return { cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }; + } + throw new Error(`unexpected child CDP call ${method}`); + }) as CdpRunner["sendToTarget"]; + + const res = await handleFill( + sm, + { session_id: "aa11", ref: "@e3", value: "hello" }, + { cdp: fake.cdp, tabsApi: fake.tabsApi }, + ); + if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); + expect(res.value_length).toBe(5); + const childTyping = targetCalls.filter( + (c) => c.method === "Input.insertText" || c.method === "Input.dispatchKeyEvent", + ); + expect(childTyping.map((c) => c.sessionId)).toContain("child-session"); + expect(fake.sent.filter((c) => c.method.startsWith("Input."))).toHaveLength(0); + }); + it("passes clear_before=false to preparation and verifies the result", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); @@ -1772,6 +1835,64 @@ describe("handlePress", () => { expect(res.key).toBe("Enter"); }); + it("routes press key events into the ref's OOPIF child session", async () => { + // A ref inside an OOPIF carries cdpSessionId; key events must reach + // that child session (which owns the focused element), not the root + // one the tab-level send hits (issue #438). + const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); + const ctx = await sm.start("aa11"); + ctx.refStore.set("e3", 1234, { + tabId: 4, + frameId: "child-frame", + cdpSessionId: "child-session", + }); + const fake = makeFakeCdp({ + "Input.dispatchKeyEvent": () => ({}), + "DOM.scrollIntoViewIfNeeded": () => ({}), + "Runtime.evaluate": () => ({ result: { value: { width: 200, height: 100 } } }), + "Page.getLayoutMetrics": () => ({ cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }), + }); + fake.cdp.getFrameGraph = vi.fn(async () => ({ + rootFrameId: "main", + frames: [ + { frameId: "main", target: { tabId: 4 } }, + { + frameId: "child-frame", + parentFrameId: "main", + ownerBackendNodeId: 99, + target: { tabId: 4, sessionId: "child-session" }, + }, + ], + })); + const targetCalls: Array<{ sessionId?: string; method: string }> = []; + fake.cdp.sendToTarget = vi.fn(async (target, method) => { + targetCalls.push({ sessionId: target.sessionId, method }); + if (method === "DOM.scrollIntoViewIfNeeded") return {}; + if (method === "DOM.focus") return {}; + if (method === "Input.dispatchKeyEvent") return {}; + if (method === "Runtime.evaluate") return { result: { value: { width: 200, height: 100 } } }; + if (method === "Page.getLayoutMetrics") { + return { cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }; + } + throw new Error(`unexpected child CDP call ${method}`); + }) as CdpRunner["sendToTarget"]; + + const res = await handlePress( + sm, + { session_id: "aa11", key: "Enter", ref: "@e3" }, + { cdp: fake.cdp, tabsApi: fake.tabsApi }, + ); + expectPressOk(res); + // Every key event went to the child session; none to the root. + const childKeyEvents = targetCalls.filter((c) => c.method === "Input.dispatchKeyEvent"); + expect(childKeyEvents.map((c) => c.sessionId)).toEqual([ + "child-session", + "child-session", + "child-session", + ]); + expect(fake.sent.filter((c) => c.method === "Input.dispatchKeyEvent")).toHaveLength(0); + }); + it.each([ "focus", "key", diff --git a/apps/extension/src/tools/interaction.ts b/apps/extension/src/tools/interaction.ts index a1889d98..43b180e0 100644 --- a/apps/extension/src/tools/interaction.ts +++ b/apps/extension/src/tools/interaction.ts @@ -1278,12 +1278,12 @@ export async function handleFill( // end command can escape a nested editing host. Place its range below. // Neither event carries an End key that could invoke a page shortcut. try { - await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", { + await nodeCdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "rawKeyDown", commands: native ? ["moveToEndOfDocument"] : [], }); } finally { - await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "keyUp" }); + await nodeCdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "keyUp" }); } if (throwIfAborted(deps.signal)) { return { code: "cancelled", message: "fill aborted" }; @@ -1309,7 +1309,7 @@ export async function handleFill( if (params.value !== "") { // Normalize the actual input as well as the expected value: older Chrome // versions retain CR in editors or turn CRLF into two textarea breaks. - await deps.cdp.send(target.tabId, "Input.insertText", { + await nodeCdp.send(target.tabId, "Input.insertText", { text: params.value.replace(/\r\n?/g, "\n"), }); } @@ -1568,6 +1568,12 @@ export async function handlePress( ? await resolveBackendNode(deps.cdp, ctx, target, params, "press") : undefined; if (node && isRpcError(node)) return node; + // Key events must reach the focused element's session: for a ref inside + // an OOPIF that is the child session, not the root one the tab-level + // send would hit (issue #438). Without a ref the root session is correct. + const keyCdp = node + ? cdpRunnerForTarget(deps.cdp, node.cdpTarget) + : deps.cdp; return withInputReady(ctx, target.tabId, { ...deps, deadline }, async (input) => { // Optional focus before key dispatch. if (node) { @@ -1607,7 +1613,7 @@ export async function handlePress( deps.cdp.trackSessionTab?.(ctx.sessionId, target.tabId); deps.onInputSent?.(target.tabId); input.markSent(); - await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", { + await keyCdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "rawKeyDown", key: descriptor.key, code: descriptor.code, @@ -1621,7 +1627,7 @@ export async function handlePress( typeof descriptor.text === "string" && descriptor.text.length > 0 ) { - await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", { + await keyCdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "char", key: descriptor.key, code: descriptor.code, @@ -1639,7 +1645,7 @@ export async function handlePress( } } deps.onInputSent?.(target.tabId); - await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", { + await keyCdp.send(target.tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: descriptor.key, code: descriptor.code, From fc725c99f704bc9229423764a2f350c0d287db2a Mon Sep 17 00:00:00 2001 From: Kris <2040188027@qq.com> Date: Sun, 25 Oct 2026 14:53:44 +0800 Subject: [PATCH 2/2] style: apply biome formatting to interaction changes (#488) Fixes the 'Frontend lint, typecheck, tests, build' CI failure: biome check rejected the hand-formatted Page.getLayoutMetrics mock entries in interaction.test.ts and the line-wrapped keyCdp ternary in interaction.ts. Reformatted both files with biome so 'pnpm lint' passes. --- apps/extension/src/tools/__tests__/interaction.test.ts | 8 ++++++-- apps/extension/src/tools/interaction.ts | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/tools/__tests__/interaction.test.ts b/apps/extension/src/tools/__tests__/interaction.test.ts index 7be4373f..2206f3d9 100644 --- a/apps/extension/src/tools/__tests__/interaction.test.ts +++ b/apps/extension/src/tools/__tests__/interaction.test.ts @@ -1525,7 +1525,9 @@ describe("handleFill", () => { const fake = makeFakeCdp({ "DOM.scrollIntoViewIfNeeded": () => ({}), "Runtime.evaluate": () => ({ result: { value: { width: 200, height: 100 } } }), - "Page.getLayoutMetrics": () => ({ cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }), + "Page.getLayoutMetrics": () => ({ + cssLayoutViewport: { clientWidth: 200, clientHeight: 100 }, + }), }); fake.cdp.getFrameGraph = vi.fn(async () => ({ rootFrameId: "main", @@ -1850,7 +1852,9 @@ describe("handlePress", () => { "Input.dispatchKeyEvent": () => ({}), "DOM.scrollIntoViewIfNeeded": () => ({}), "Runtime.evaluate": () => ({ result: { value: { width: 200, height: 100 } } }), - "Page.getLayoutMetrics": () => ({ cssLayoutViewport: { clientWidth: 200, clientHeight: 100 } }), + "Page.getLayoutMetrics": () => ({ + cssLayoutViewport: { clientWidth: 200, clientHeight: 100 }, + }), }); fake.cdp.getFrameGraph = vi.fn(async () => ({ rootFrameId: "main", diff --git a/apps/extension/src/tools/interaction.ts b/apps/extension/src/tools/interaction.ts index 43b180e0..5df8aeb8 100644 --- a/apps/extension/src/tools/interaction.ts +++ b/apps/extension/src/tools/interaction.ts @@ -1571,9 +1571,7 @@ export async function handlePress( // Key events must reach the focused element's session: for a ref inside // an OOPIF that is the child session, not the root one the tab-level // send would hit (issue #438). Without a ref the root session is correct. - const keyCdp = node - ? cdpRunnerForTarget(deps.cdp, node.cdpTarget) - : deps.cdp; + const keyCdp = node ? cdpRunnerForTarget(deps.cdp, node.cdpTarget) : deps.cdp; return withInputReady(ctx, target.tabId, { ...deps, deadline }, async (input) => { // Optional focus before key dispatch. if (node) {