Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/extension/src/tools/__tests__/interaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,71 @@ 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");
Expand Down Expand Up @@ -1772,6 +1837,66 @@ 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",
Expand Down
16 changes: 10 additions & 6 deletions apps/extension/src/tools/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand All @@ -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"),
});
}
Expand Down Expand Up @@ -1568,6 +1568,10 @@ 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) {
Expand Down Expand Up @@ -1607,7 +1611,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,
Expand All @@ -1621,7 +1625,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,
Expand All @@ -1639,7 +1643,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,
Expand Down
Loading