diff --git a/src/bidiMapper/domains/context/BrowsingContextImpl.ts b/src/bidiMapper/domains/context/BrowsingContextImpl.ts index dd752e8d2a..776bb5777a 100644 --- a/src/bidiMapper/domains/context/BrowsingContextImpl.ts +++ b/src/bidiMapper/domains/context/BrowsingContextImpl.ts @@ -447,32 +447,44 @@ export class BrowsingContextImpl { this.#cdpTarget.cdpClient.on( 'Runtime.executionContextCreated', (params: Protocol.Runtime.ExecutionContextCreatedEvent) => { - if (params.context.auxData.frameId !== this.id) { + const {auxData, name, uniqueId, id} = params.context; + if (!auxData || auxData.frameId !== this.id) { return; } - // Only this execution contexts are supported for now. - if (!['default', 'isolated'].includes(params.context.auxData.type)) { - return; + + let origin: string; + let sandbox: string | undefined; + // Only these execution contexts are supported for now. + switch (auxData.type) { + case 'isolated': + sandbox = name; + // Sandbox should have the same origin as the context itself, but in CDP + // it has an empty one. + origin = this.#defaultRealm.origin; + break; + case 'default': + origin = serializeOrigin(params.context.origin); + break; + default: + return; } const realm = new Realm( this.#realmStorage, this.#browsingContextStorage, - params.context.uniqueId, + uniqueId, this.id, - params.context.id, - this.#getOrigin(params), + id, + origin, // XXX: differentiate types. 'window', // Sandbox name for isolated world. - params.context.auxData.type === 'isolated' - ? params.context.name - : undefined, + sandbox, this.#cdpTarget.cdpClient, this.#eventManager, this.#logger ); - if (params.context.auxData.isDefault) { + if (auxData.isDefault) { this.#maybeDefaultRealm = realm; // Initialize ChannelProxy listeners for all the channels of all the @@ -541,18 +553,6 @@ export class BrowsingContextImpl { }); } - #getOrigin(params: Protocol.Runtime.ExecutionContextCreatedEvent) { - if (params.context.auxData.type === 'isolated') { - // Sandbox should have the same origin as the context itself, but in CDP - // it has an empty one. - return this.#defaultRealm.origin; - } - // https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin - return ['://', ''].includes(params.context.origin) - ? 'null' - : params.context.origin; - } - #documentChanged(loaderId?: Protocol.Network.LoaderId) { // Same document navigation. if (loaderId === undefined || this.#loaderId === loaderId) { @@ -1001,6 +1001,14 @@ export class BrowsingContextImpl { } } +export function serializeOrigin(origin: string) { + // https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin + if (['://', ''].includes(origin)) { + origin = 'null'; + } + return origin; +} + function getImageFormatParameters( params: Readonly ) { diff --git a/src/bidiMapper/domains/context/BrowsingContextProcessor.ts b/src/bidiMapper/domains/context/BrowsingContextProcessor.ts index 57c3d90ae0..40d6680c17 100644 --- a/src/bidiMapper/domains/context/BrowsingContextProcessor.ts +++ b/src/bidiMapper/domains/context/BrowsingContextProcessor.ts @@ -20,17 +20,18 @@ import type {ICdpClient} from '../../../cdp/CdpClient.js'; import type {ICdpConnection} from '../../../cdp/CdpConnection.js'; import { BrowsingContext, - type EmptyResult, InvalidArgumentException, + type EmptyResult, } from '../../../protocol/protocol.js'; import {CdpErrorConstants} from '../../../utils/CdpErrorConstants.js'; -import {type LoggerFn, LogType} from '../../../utils/log.js'; +import {LogType, type LoggerFn} from '../../../utils/log.js'; import type {EventManager} from '../events/EventManager.js'; import type {NetworkStorage} from '../network/NetworkStorage.js'; import type {PreloadScriptStorage} from '../script/PreloadScriptStorage.js'; +import {Realm} from '../script/Realm.js'; import type {RealmStorage} from '../script/RealmStorage.js'; -import {BrowsingContextImpl} from './BrowsingContextImpl.js'; +import {BrowsingContextImpl, serializeOrigin} from './BrowsingContextImpl.js'; import type {BrowsingContextStorage} from './BrowsingContextStorage.js'; import {CdpTarget} from './CdpTarget.js'; @@ -335,89 +336,136 @@ export class BrowsingContextProcessor { parentSessionCdpClient: ICdpClient ) { const {sessionId, targetInfo} = params; - const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId); - if (!this.#isValidTarget(targetInfo)) { - // DevTools or some other not supported by BiDi target. Just release - // debugger and ignore them. - targetCdpClient - .sendCommand('Runtime.runIfWaitingForDebugger') - .then(() => - parentSessionCdpClient.sendCommand('Target.detachFromTarget', params) - ) - .catch((error) => this.#logger?.(LogType.debugError, error)); - return; - } - this.#logger?.( LogType.debugInfo, 'AttachedToTarget event received:', params ); - this.#setEventListeners(targetCdpClient); + switch (targetInfo.type) { + case 'page': + case 'iframe': { + if (targetInfo.targetId === this.#selfTargetId) { + break; + } + + this.#setEventListeners(targetCdpClient); + + const cdpTarget = CdpTarget.create( + targetInfo.targetId, + targetCdpClient, + this.#browserCdpClient, + sessionId, + this.#realmStorage, + this.#eventManager, + this.#preloadScriptStorage, + this.#networkStorage, + this.#acceptInsecureCerts + ); - const maybeContext = this.#browsingContextStorage.findContext( - targetInfo.targetId - ); + const maybeContext = this.#browsingContextStorage.findContext( + targetInfo.targetId + ); + if (maybeContext) { + // OOPiF. + maybeContext.updateCdpTarget(cdpTarget); + } else { + // New context. + BrowsingContextImpl.create( + cdpTarget, + this.#realmStorage, + targetInfo.targetId, + null, + this.#eventManager, + this.#browsingContextStorage, + this.#logger + ); + } + return; + } + case 'worker': { + this.#setEventListeners(targetCdpClient); + + const cdpTarget = CdpTarget.create( + targetInfo.targetId, + targetCdpClient, + this.#browserCdpClient, + sessionId, + this.#realmStorage, + this.#eventManager, + this.#preloadScriptStorage, + this.#networkStorage, + this.#acceptInsecureCerts + ); - const cdpTarget = CdpTarget.create( - targetInfo.targetId, - targetCdpClient, - this.#browserCdpClient, - sessionId, - this.#realmStorage, - this.#eventManager, - this.#preloadScriptStorage, - this.#networkStorage, - this.#acceptInsecureCerts - ); + this.#createWorkerRealm(cdpTarget); + return; + } + } - if (maybeContext) { - // OOPiF. - maybeContext.updateCdpTarget(cdpTarget); - } else { - // New context. - BrowsingContextImpl.create( - cdpTarget, + // DevTools or some other not supported by BiDi target. Just release + // debugger and ignore them. + targetCdpClient + .sendCommand('Runtime.runIfWaitingForDebugger') + .then(() => + parentSessionCdpClient.sendCommand('Target.detachFromTarget', params) + ) + .catch((error) => this.#logger?.(LogType.debugError, error)); + } + + #workers = new Map(); + #createWorkerRealm(cdpTarget: CdpTarget) { + cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => { + const {uniqueId, id, origin} = params.context; + const realm = new Realm( this.#realmStorage, - targetInfo.targetId, - null, - this.#eventManager, this.#browsingContextStorage, + uniqueId, + cdpTarget.targetId, + id, + serializeOrigin(origin), + 'dedicated-worker', + undefined, + cdpTarget.cdpClient, + this.#eventManager, this.#logger ); - } + this.#workers.set(cdpTarget.cdpSessionId, realm); + }); } #handleDetachedFromTargetEvent( params: Protocol.Target.DetachedFromTargetEvent ) { - // XXX: params.targetId is deprecated. Update this class to track using - // params.sessionId instead. - // https://github.com/GoogleChromeLabs/chromium-bidi/issues/60 - const contextId = params.targetId!; - this.#browsingContextStorage.findContext(contextId)?.dispose(); - - this.#preloadScriptStorage - .find({targetId: contextId}) - .map((preloadScript) => preloadScript.dispose(contextId)); + const context = this.#browsingContextStorage + .getAllContexts() + .find((context) => context.cdpTarget.cdpSessionId === params.sessionId); + if (context) { + context.dispose(); + this.#preloadScriptStorage + .find({targetId: context.id}) + .map((preloadScript) => preloadScript.dispose(context.id)); + return; + } + + const worker = this.#workers.get(params.sessionId); + if (worker) { + this.#realmStorage.deleteRealms({ + cdpSessionId: worker.cdpClient.sessionId, + }); + } } #handleTargetInfoChangedEvent( params: Protocol.Target.TargetInfoChangedEvent ) { - const contextId = params.targetInfo.targetId; - this.#browsingContextStorage - .findContext(contextId) - ?.onTargetInfoChanged(params); - } - - #isValidTarget(target: Protocol.Target.TargetInfo) { - if (target.targetId === this.#selfTargetId) { - return false; + const context = this.#browsingContextStorage.findContext( + params.targetInfo.targetId + ); + if (context) { + context.onTargetInfoChanged(params); } - return ['page', 'iframe'].includes(target.type); } } diff --git a/tests/script/test_realm.py b/tests/script/test_realm.py index de77f9acf0..81b3f153d6 100644 --- a/tests/script/test_realm.py +++ b/tests/script/test_realm.py @@ -82,6 +82,54 @@ async def test_realm_realmCreated_sandbox(websocket, context_id): } == response +@pytest.mark.asyncio +async def test_realm_realmCreated_worker(websocket, context_id, html): + worker_url = 'data:application/javascript,while(true){}' + url = html(f"") + + await subscribe(websocket, ["script.realmCreated"]) + + await send_JSON_command( + websocket, { + "method": "browsingContext.navigate", + "params": { + "context": context_id, + "url": url, + "wait": "complete", + } + }) + + # Realm created + assert { + "type": "event", + "method": "script.realmCreated", + "params": { + "type": "window", + "origin": "null", + "realm": ANY_STR, + "context": context_id, + } + } == await read_JSON_message(websocket) + + # Navigation + assert { + "navigation": ANY_STR, + "url": url + } == (await read_JSON_message(websocket))['result'] + + # Worker realm is created from the HTML script. + assert { + "type": "event", + "method": "script.realmCreated", + "params": { + "type": "dedicated-worker", + "origin": worker_url, + "realm": ANY_STR, + "context": ANY_STR + } + } == await read_JSON_message(websocket) + + @pytest.mark.asyncio async def test_realm_realmDestroyed(websocket, context_id): @@ -149,3 +197,50 @@ async def test_realm_realmDestroyed_sandbox(websocket, context_id): "realm": ANY_STR, } } == response + + +@pytest.mark.asyncio +async def test_realm_realmDestroyed_worker(websocket, context_id, html): + worker_url = 'data:application/javascript,while(true){}' + url = html(f"") + + await subscribe(websocket, ["script.realmDestroyed"]) + + await send_JSON_command( + websocket, { + "method": "browsingContext.navigate", + "params": { + "context": context_id, + "url": url, + "wait": "complete", + } + }) + + assert { + "type": "event", + "method": "script.realmDestroyed", + "params": { + "realm": ANY_STR, + } + } == await read_JSON_message(websocket) + + await execute_command( + websocket, { + "method": "script.evaluate", + "params": { + "target": { + "context": context_id + }, + "expression": "window.w.terminate()", + "awaitPromise": True + } + }) + + # Worker realm is destroyed from the HTML script. + assert { + "type": "event", + "method": "script.realmDestroyed", + "params": { + "realm": ANY_STR + } + } == await read_JSON_message(websocket)