chore: implement BiDi workers (#11909)

This commit is contained in:
jrandolf 2024-02-13 16:09:33 +01:00 committed by GitHub
parent 0b74d752fb
commit d14c47097d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 196 additions and 48 deletions

View File

@ -21,7 +21,9 @@ import type {BrowsingContext} from './core/BrowsingContext.js';
import {UserContext} from './core/UserContext.js'; import {UserContext} from './core/UserContext.js';
import type {BidiFrame} from './Frame.js'; import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js'; import {BidiPage} from './Page.js';
import {BidiWorkerTarget} from './Target.js';
import {BidiFrameTarget, BidiPageTarget} from './Target.js'; import {BidiFrameTarget, BidiPageTarget} from './Target.js';
import type {BidiWebWorker} from './WebWorker.js';
/** /**
* @internal * @internal
@ -54,7 +56,10 @@ export class BidiBrowserContext extends BrowserContext {
readonly #pages = new WeakMap<BrowsingContext, BidiPage>(); readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
readonly #targets = new Map< readonly #targets = new Map<
BidiPage, BidiPage,
[BidiPageTarget, Map<BidiFrame, BidiFrameTarget>] [
BidiPageTarget,
Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
]
>(); >();
private constructor( private constructor(
@ -91,17 +96,18 @@ export class BidiBrowserContext extends BrowserContext {
// -- Target stuff starts here -- // -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page); const pageTarget = new BidiPageTarget(page);
const frameTargets = new Map(); const pageTargets = new Map();
this.#targets.set(page, [pageTarget, frameTargets]); this.#targets.set(page, [pageTarget, pageTargets]);
page.trustedEmitter.on(PageEvent.FrameAttached, frame => { page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
const bidiFrame = frame as BidiFrame; const bidiFrame = frame as BidiFrame;
const target = new BidiFrameTarget(bidiFrame); const target = new BidiFrameTarget(bidiFrame);
frameTargets.set(bidiFrame, target); pageTargets.set(bidiFrame, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target); this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
}); });
page.trustedEmitter.on(PageEvent.FrameNavigated, frame => { page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
const bidiFrame = frame as BidiFrame; const bidiFrame = frame as BidiFrame;
const target = frameTargets.get(bidiFrame); const target = pageTargets.get(bidiFrame);
// If there is no target, then this is the page's frame. // If there is no target, then this is the page's frame.
if (target === undefined) { if (target === undefined) {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget); this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
@ -111,13 +117,30 @@ export class BidiBrowserContext extends BrowserContext {
}); });
page.trustedEmitter.on(PageEvent.FrameDetached, frame => { page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
const bidiFrame = frame as BidiFrame; const bidiFrame = frame as BidiFrame;
const target = frameTargets.get(bidiFrame); const target = pageTargets.get(bidiFrame);
if (target === undefined) { if (target === undefined) {
return; return;
} }
frameTargets.delete(bidiFrame); pageTargets.delete(bidiFrame);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target); this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
}); });
page.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = new BidiWorkerTarget(bidiWorker);
pageTargets.set(bidiWorker, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
const bidiWorker = worker as BidiWebWorker;
const target = pageTargets.get(bidiWorker);
if (target === undefined) {
return;
}
pageTargets.delete(worker);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
});
page.trustedEmitter.on(PageEvent.Close, () => { page.trustedEmitter.on(PageEvent.Close, () => {
this.#targets.delete(page); this.#targets.delete(page);
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget); this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);

View File

@ -47,6 +47,7 @@ import type {BidiPage} from './Page.js';
import type {BidiRealm} from './Realm.js'; import type {BidiRealm} from './Realm.js';
import {BidiFrameRealm} from './Realm.js'; import {BidiFrameRealm} from './Realm.js';
import {rewriteNavigationError} from './util.js'; import {rewriteNavigationError} from './util.js';
import {BidiWebWorker} from './WebWorker.js';
export class BidiFrame extends Frame { export class BidiFrame extends Frame {
static from( static from(
@ -142,6 +143,7 @@ export class BidiFrame extends Frame {
return; return;
} }
if (isConsoleLogEntry(entry)) { if (isConsoleLogEntry(entry)) {
console.log(entry.args);
const args = entry.args.map(arg => { const args = entry.args.map(arg => {
return this.mainRealm().createHandle(arg); return this.mainRealm().createHandle(arg);
}); });
@ -194,6 +196,14 @@ export class BidiFrame extends Frame {
); );
} }
}); });
this.browsingContext.on('worker', ({realm}) => {
const worker = BidiWebWorker.from(this, realm);
realm.on('destroyed', () => {
this.page().trustedEmitter.emit(PageEvent.WorkerDestroyed, worker);
});
this.page().trustedEmitter.emit(PageEvent.WorkerCreated, worker);
});
} }
#createFrameTarget(browsingContext: BrowsingContext) { #createFrameTarget(browsingContext: BrowsingContext) {

View File

@ -48,6 +48,7 @@ import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js'; import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js'; import type {BidiJSHandle} from './JSHandle.js';
import {rewriteNavigationError} from './util.js'; import {rewriteNavigationError} from './util.js';
import type {BidiWebWorker} from './WebWorker.js';
/** /**
* @internal * @internal
@ -68,6 +69,7 @@ export class BidiPage extends Page {
readonly #browserContext: BidiBrowserContext; readonly #browserContext: BidiBrowserContext;
readonly #frame: BidiFrame; readonly #frame: BidiFrame;
#viewport: Viewport | null = null; #viewport: Viewport | null = null;
readonly #workers = new Set<BidiWebWorker>();
readonly keyboard: BidiKeyboard; readonly keyboard: BidiKeyboard;
readonly mouse: BidiMouse; readonly mouse: BidiMouse;
@ -103,6 +105,13 @@ export class BidiPage extends Page {
this.trustedEmitter.emit(PageEvent.Close, undefined); this.trustedEmitter.emit(PageEvent.Close, undefined);
this.trustedEmitter.removeAllListeners(); this.trustedEmitter.removeAllListeners();
}); });
this.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
this.#workers.add(worker as BidiWebWorker);
});
this.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
this.#workers.delete(worker as BidiWebWorker);
});
} }
override async setUserAgent( override async setUserAgent(
@ -475,8 +484,8 @@ export class BidiPage extends Page {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
override workers(): never { override workers(): BidiWebWorker[] {
throw new UnsupportedOperation(); return [...this.#workers];
} }
override setRequestInterception(): never { override setRequestInterception(): never {

View File

@ -22,7 +22,11 @@ import {
import type PuppeteerUtil from '../injected/injected.js'; import type PuppeteerUtil from '../injected/injected.js';
import {stringifyFunction} from '../util/Function.js'; import {stringifyFunction} from '../util/Function.js';
import type {Realm as BidiRealmCore} from './core/Realm.js'; import type {
Realm as BidiRealmCore,
DedicatedWorkerRealm,
SharedWorkerRealm,
} from './core/Realm.js';
import type {WindowRealm} from './core/Realm.js'; import type {WindowRealm} from './core/Realm.js';
import {BidiDeserializer} from './Deserializer.js'; import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js'; import {BidiElementHandle} from './ElementHandle.js';
@ -30,9 +34,13 @@ import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js'; import {BidiJSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js'; import {createEvaluationError} from './util.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export abstract class BidiRealm extends Realm { export abstract class BidiRealm extends Realm {
realm: BidiRealmCore; readonly realm: BidiRealmCore;
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) { constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
super(timeoutSettings); super(timeoutSettings);
@ -250,6 +258,9 @@ export abstract class BidiRealm extends Realm {
} }
} }
/**
* @internal
*/
export class BidiFrameRealm extends BidiRealm { export class BidiFrameRealm extends BidiRealm {
static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm { static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
const frameRealm = new BidiFrameRealm(realm, frame); const frameRealm = new BidiFrameRealm(realm, frame);
@ -297,3 +308,36 @@ export class BidiFrameRealm extends BidiRealm {
); );
} }
} }
/**
* @internal
*/
export class BidiWorkerRealm extends BidiRealm {
static from(
realm: DedicatedWorkerRealm | SharedWorkerRealm,
worker: BidiWebWorker
): BidiWorkerRealm {
const workerRealm = new BidiWorkerRealm(realm, worker);
workerRealm.initialize();
return workerRealm;
}
declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm;
readonly #worker: BidiWebWorker;
private constructor(
realm: DedicatedWorkerRealm | SharedWorkerRealm,
frame: BidiWebWorker
) {
super(realm, frame.timeoutSettings);
this.#worker = frame;
}
override get environment(): BidiWebWorker {
return this.#worker;
}
override async adoptBackendNode(): Promise<JSHandle<Node>> {
throw new Error('Cannot adopt DOM nodes into a worker.');
}
}

View File

@ -12,6 +12,7 @@ import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js'; import type {BidiBrowserContext} from './BrowserContext.js';
import type {BidiFrame} from './Frame.js'; import type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js'; import {BidiPage} from './Page.js';
import type {BidiWebWorker} from './WebWorker.js';
/** /**
* @internal * @internal
@ -130,3 +131,40 @@ export class BidiFrameTarget extends Target {
throw new UnsupportedOperation(); throw new UnsupportedOperation();
} }
} }
/**
* @internal
*/
export class BidiWorkerTarget extends Target {
#worker: BidiWebWorker;
constructor(worker: BidiWebWorker) {
super();
this.#worker = worker;
}
override async page(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override async asPage(): Promise<BidiPage> {
throw new UnsupportedOperation();
}
override url(): string {
return this.#worker.url();
}
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
override type(): TargetType {
return TargetType.OTHER;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#worker.frame.page().browserContext();
}
override opener(): Target | undefined {
throw new UnsupportedOperation();
}
}

View File

@ -0,0 +1,48 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {WebWorker} from '../api/WebWorker.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {CDPSession} from '../puppeteer-core.js';
import type {DedicatedWorkerRealm, SharedWorkerRealm} from './core/Realm.js';
import type {BidiFrame} from './Frame.js';
import {BidiWorkerRealm} from './Realm.js';
/**
* @internal
*/
export class BidiWebWorker extends WebWorker {
static from(
frame: BidiFrame,
realm: DedicatedWorkerRealm | SharedWorkerRealm
): BidiWebWorker {
const worker = new BidiWebWorker(frame, realm);
return worker;
}
readonly #frame: BidiFrame;
readonly #realm: BidiWorkerRealm;
private constructor(
frame: BidiFrame,
realm: DedicatedWorkerRealm | SharedWorkerRealm
) {
super(realm.origin);
this.#frame = frame;
this.#realm = BidiWorkerRealm.from(realm, this);
}
get frame(): BidiFrame {
return this.#frame;
}
mainRealm(): BidiWorkerRealm {
return this.#realm;
}
get client(): CDPSession {
throw new UnsupportedOperation();
}
}

View File

@ -327,6 +327,12 @@
"testIdPattern": "[worker.spec] *", "testIdPattern": "[worker.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[worker.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{ {
@ -1308,12 +1314,6 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[worker.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["SKIP"]
},
{ {
"testIdPattern": "[accessibility.spec] Accessibility filtering children of leaf nodes rich text editable fields should have children", "testIdPattern": "[accessibility.spec] Accessibility filtering children of leaf nodes rich text editable fields should have children",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -4613,34 +4613,10 @@
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{ {
"testIdPattern": "[worker.spec] Workers should report console logs", "testIdPattern": "[worker.spec] Workers can be closed",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"], "parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["FAIL"]
},
{
"testIdPattern": "[worker.spec] Workers should report console logs",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[worker.spec] Workers should report errors",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[worker.spec] Workers should report errors",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[worker.spec] Workers should report errors",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
}, },
{ {
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",

View File

@ -52,7 +52,10 @@ describe('Workers', function () {
const error = await workerThisObj.getProperty('self').catch(error => { const error = await workerThisObj.getProperty('self').catch(error => {
return error; return error;
}); });
expect(error.message).toContain('Most likely the worker has been closed.'); expect(error.message).atLeastOneToContain([
'Most likely the worker has been closed.',
'Realm already destroyed.',
]);
}); });
it('should report console logs', async () => { it('should report console logs', async () => {
const {page} = await getTestState(); const {page} = await getTestState();
@ -70,7 +73,7 @@ describe('Workers', function () {
columnNumber: 8, columnNumber: 8,
}); });
}); });
it('should have JSHandles for console logs', async () => { it('should work with console logs', async () => {
const {page} = await getTestState(); const {page} = await getTestState();
const logPromise = waitEvent<ConsoleMessage>(page, 'console'); const logPromise = waitEvent<ConsoleMessage>(page, 'console');
@ -80,9 +83,6 @@ describe('Workers', function () {
const log = await logPromise; const log = await logPromise;
expect(log.text()).toBe('1 2 3 JSHandle@object'); expect(log.text()).toBe('1 2 3 JSHandle@object');
expect(log.args()).toHaveLength(4); expect(log.args()).toHaveLength(4);
expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe(
'null'
);
}); });
it('should have an execution context', async () => { it('should have an execution context', async () => {
const {page} = await getTestState(); const {page} = await getTestState();