diff options
| -rw-r--r-- | src/cdp/session.ts | 8 | ||||
| -rw-r--r-- | test/cdp/session-events-test.ts | 100 |
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); + }); +}); |
