aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cdp/session.ts8
-rw-r--r--test/cdp/session-events-test.ts100
2 files changed, 104 insertions, 4 deletions
diff --git a/src/cdp/session.ts b/src/cdp/session.ts
index 39cd87e..2e0e71f 100644
--- a/src/cdp/session.ts
+++ b/src/cdp/session.ts
@@ -90,11 +90,11 @@ export const createCdpSession = (
// Filter: skip events from sessions we're not currently tracking.
// Target.targetDestroyed is browser-level (no sessionId) — always process.
if (ev.method !== "Target.targetDestroyed") {
- if (ev.sessionId && ev.sessionId !== sessionId) continue;
+ if (ev.sessionId && !sessionIdToTargetId.has(ev.sessionId)) continue;
}
switch (ev.method) {
case "Page.javascriptDialogOpening": {
- const tab = targetId ? tabs.get(targetId) : undefined;
+ const tab = resolveTab(ev.sessionId);
if (!tab) break;
const decoded = decodeEvent(ev.method, ev.params);
tab.dialog = decoded.success
@@ -114,7 +114,7 @@ export const createCdpSession = (
// was about to read. (Fix for spec §7 predictability bug #2.)
case "Page.frameNavigated":
case "Page.loadEventFired": {
- const tab = targetId ? tabs.get(targetId) : undefined;
+ const tab = resolveTab(ev.sessionId);
if (tab) tab.pageInfoDirty = true;
break;
}
@@ -333,7 +333,6 @@ export const createCdpSession = (
const attachedSessionId = attached.data;
sessionId = attachedSessionId;
targetId = pickTargetId;
- await enableDomains(attachedSessionId);
tabs.set(pickTargetId, {
sessionId: attachedSessionId,
targetId: pickTargetId,
@@ -345,6 +344,7 @@ export const createCdpSession = (
refSig: new Map(),
});
sessionIdToTargetId.set(attachedSessionId, pickTargetId);
+ await enableDomains(attachedSessionId);
return ok({ targetId: pickTargetId, sessionId: attachedSessionId });
},
async switchTo(tid) {
diff --git a/test/cdp/session-events-test.ts b/test/cdp/session-events-test.ts
new file mode 100644
index 0000000..73f849e
--- /dev/null
+++ b/test/cdp/session-events-test.ts
@@ -0,0 +1,100 @@
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
+import { setImmediate as tick } from "node:timers/promises";
+import { createCdpSession } from "../../src/cdp/session";
+import { createOwnershipRegistry } from "../../src/cdp/ownership";
+import { createStubTransport } from "../domains/fake-client";
+import { ok } from "../../src/util/result";
+
+const flush = async (): Promise<void> => {
+ await tick();
+ await tick();
+};
+
+const twoTabSession = async () => {
+ const stub = createStubTransport({
+ "Target.setDiscoverTargets": ok({}),
+ "Target.getTargets": ok({
+ targetInfos: [
+ { targetId: "t1", type: "page", title: "One", url: "https://one.test/" },
+ { targetId: "t2", type: "page", title: "Two", url: "https://two.test/" },
+ ],
+ }),
+ "Target.attachToTarget": [ok({ sessionId: "s1" }), ok({ sessionId: "s2" }), ok({ sessionId: "s3" })],
+ "Browser.getWindowForTarget": ok({ windowId: 7 }),
+ "Target.activateTarget": ok({}),
+ });
+ const ownership = createOwnershipRegistry({ ownedTargetIds: ["t1", "t2"] });
+ const session = createCdpSession(stub.transport, ownership);
+ const attached = await session.attachFirstPage();
+ assert.equal(attached.success, true);
+ await session.switchTo("t2");
+ return { stub, session };
+};
+
+describe("CdpSession event routing across tabs", () => {
+ test("a console event from a background tab reaches that tab's buffer", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Runtime.consoleAPICalled",
+ sessionId: "s1",
+ params: { type: "error", args: [{ type: "string", value: "boom" }] },
+ });
+ await flush();
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ await session.switchTo("t1");
+ const drained = session.drainConsoleBuffer({});
+ assert.equal(drained.records.length, 1);
+ assert.equal(drained.records[0]?.text, "boom");
+ });
+
+ test("a network event from a background tab reaches that tab's buffer", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Network.requestWillBeSent",
+ sessionId: "s1",
+ params: { requestId: "r1", request: { url: "https://one.test/api", method: "GET" }, type: "XHR" },
+ });
+ await flush();
+ await session.switchTo("t1");
+ const drained = session.drainNetworkBuffer({});
+ assert.equal(drained.records.length, 1);
+ assert.equal(drained.records[0]?.url, "https://one.test/api");
+ });
+
+ test("a background tab's dialog is recorded against that tab, not the active one", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Page.javascriptDialogOpening",
+ sessionId: "s1",
+ params: { type: "confirm", message: "really?" },
+ });
+ await flush();
+ assert.equal(session.takeDialog(), null);
+ await session.switchTo("t1");
+ assert.equal(session.takeDialog()?.message, "really?");
+ });
+
+ test("a background tab's navigation marks that tab's page info dirty, not the active one", async () => {
+ const { stub, session } = await twoTabSession();
+ session.drainPageInfoInvalidations();
+ stub.emit({ method: "Page.loadEventFired", sessionId: "s1", params: {} });
+ await flush();
+ assert.equal(session.drainPageInfoInvalidations(), false);
+ await session.switchTo("t1");
+ assert.equal(session.drainPageInfoInvalidations(), true);
+ });
+
+ test("an event from a session this instance never attached is ignored", async () => {
+ const { stub, session } = await twoTabSession();
+ stub.emit({
+ method: "Runtime.consoleAPICalled",
+ sessionId: "unknown-session",
+ params: { type: "error", args: [{ type: "string", value: "stranger" }] },
+ });
+ await flush();
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ await session.switchTo("t1");
+ assert.equal(session.drainConsoleBuffer({}).records.length, 0);
+ });
+});