Skip to content
Closed
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
64 changes: 64 additions & 0 deletions apps/extension/src/tools/__tests__/fill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,67 @@ describe("fill result verification", () => {
expect(await h.fill()).toMatchObject({ value_length: 5 });
});
});

describe("fill OOPIF session routing", () => {
it("routes Input.insertText through the OOPIF session, not the root tab", async () => {
const h = await setup();
const manager = new SessionManager({
agentWindow: {
create: async () => ({ windowId: 100, initialTabIds: [] }),
remove: async () => {},
ensureActiveTab: async () => 4,
},
});
const ctx = await manager.start("bb22");
ctx.refStore.set("e1", 12, {
tabId: 4,
frameId: "child-frame",
cdpSessionId: "child-session",
});
const rootInput: string[] = [];
const childInput: string[] = [];
const send = vi.fn(async (_tabId: number, method: string, params?: object) => {
if (method.startsWith("Input.")) rootInput.push(method);
return h.send(4, method, params);
});
const sendToTarget = vi.fn(async (target: { sessionId?: string }, method: string, params?: object) => {
if (method.startsWith("Input.")) {
childInput.push(`${target.sessionId}:${method}`);
}
return h.send(4, method, params);
});
const result = await handleFill(
manager,
{ session_id: "bb22", ref: "e1", value: "hello", clear_before: true },
{
cdp: {
send: send as CdpRunner["send"],
sendToTarget: sendToTarget as CdpRunner["sendToTarget"],
getFrameGraph: async () => ({
frames: [
{
frameId: "root",
parentFrameId: undefined,
target: { tabId: 4 },
},
{
frameId: "child-frame",
parentFrameId: "root",
ownerBackendNodeId: 99,
target: { tabId: 4, sessionId: "child-session" },
},
],
}),
},
tabsApi: {
get: async (id) => ({ id, windowId: 100, active: true }) as chrome.tabs.Tab,
query: async () => [{ id: 4, windowId: 100, active: true } as chrome.tabs.Tab],
},
},
);
expect(result).toMatchObject({ value_length: 5, used_ref: "e1" });
expect(childInput).toContain("child-session:Input.insertText");
expect(rootInput.filter((m) => m === "Input.insertText")).toHaveLength(0);
expect(h.element.value).toBe("hello");
});
});
19 changes: 13 additions & 6 deletions apps/extension/src/tools/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1278,12 +1278,14 @@ 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", {
// Route through nodeCdp so OOPIF targets receive the caret command
// on their child session (deps.cdp.send is always the root target).
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 +1311,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 @@ -1602,12 +1604,17 @@ export async function handlePress(
// Suppress `text` when any non-shift modifier is held — `Ctrl+a`
// should not also type the character "a" into the focused field.
const suppressText = mods.some((m) => m === "ctrl" || m === "meta" || m === "alt");
// When a ref was focused in an OOPIF, key events must share that child
// session. Without a ref, keep the root-session dispatch.
const inputCdp = node
? cdpRunnerForTarget(deps.cdp, node.cdpTarget)
: deps.cdp;
try {
let cancelled = false;
deps.cdp.trackSessionTab?.(ctx.sessionId, target.tabId);
deps.onInputSent?.(target.tabId);
input.markSent();
await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", {
await inputCdp.send(target.tabId, "Input.dispatchKeyEvent", {
type: "rawKeyDown",
key: descriptor.key,
code: descriptor.code,
Expand All @@ -1621,7 +1628,7 @@ export async function handlePress(
typeof descriptor.text === "string" &&
descriptor.text.length > 0
) {
await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", {
await inputCdp.send(target.tabId, "Input.dispatchKeyEvent", {
type: "char",
key: descriptor.key,
code: descriptor.code,
Expand All @@ -1639,7 +1646,7 @@ export async function handlePress(
}
}
deps.onInputSent?.(target.tabId);
await deps.cdp.send(target.tabId, "Input.dispatchKeyEvent", {
await inputCdp.send(target.tabId, "Input.dispatchKeyEvent", {
type: "keyUp",
key: descriptor.key,
code: descriptor.code,
Expand Down