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 type {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
import {BidiWorkerTarget} from './Target.js';
import {BidiFrameTarget, BidiPageTarget} from './Target.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
@ -54,7 +56,10 @@ export class BidiBrowserContext extends BrowserContext {
readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
readonly #targets = new Map<
BidiPage,
[BidiPageTarget, Map<BidiFrame, BidiFrameTarget>]
[
BidiPageTarget,
Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
]
>();
private constructor(
@ -91,17 +96,18 @@ export class BidiBrowserContext extends BrowserContext {
// -- Target stuff starts here --
const pageTarget = new BidiPageTarget(page);
const frameTargets = new Map();
this.#targets.set(page, [pageTarget, frameTargets]);
const pageTargets = new Map();
this.#targets.set(page, [pageTarget, pageTargets]);
page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
const bidiFrame = frame as BidiFrame;
const target = new BidiFrameTarget(bidiFrame);
frameTargets.set(bidiFrame, target);
pageTargets.set(bidiFrame, target);
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
});
page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
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 (target === undefined) {
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
@ -111,13 +117,30 @@ export class BidiBrowserContext extends BrowserContext {
});
page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
const bidiFrame = frame as BidiFrame;
const target = frameTargets.get(bidiFrame);
const target = pageTargets.get(bidiFrame);
if (target === undefined) {
return;
}
frameTargets.delete(bidiFrame);
pageTargets.delete(bidiFrame);
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, () => {
this.#targets.delete(page);
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 {BidiFrameRealm} from './Realm.js';
import {rewriteNavigationError} from './util.js';
import {BidiWebWorker} from './WebWorker.js';
export class BidiFrame extends Frame {
static from(
@ -142,6 +143,7 @@ export class BidiFrame extends Frame {
return;
}
if (isConsoleLogEntry(entry)) {
console.log(entry.args);
const args = entry.args.map(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) {

View File

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

View File

@ -22,7 +22,11 @@ import {
import type PuppeteerUtil from '../injected/injected.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 {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
@ -30,9 +34,13 @@ import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './util.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
*/
export abstract class BidiRealm extends Realm {
realm: BidiRealmCore;
readonly realm: BidiRealmCore;
constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
super(timeoutSettings);
@ -250,6 +258,9 @@ export abstract class BidiRealm extends Realm {
}
}
/**
* @internal
*/
export class BidiFrameRealm extends BidiRealm {
static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
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 {BidiFrame} from './Frame.js';
import {BidiPage} from './Page.js';
import type {BidiWebWorker} from './WebWorker.js';
/**
* @internal
@ -130,3 +131,40 @@ export class BidiFrameTarget extends Target {
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] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[worker.spec] *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox"],
"expectations": ["SKIP"]
},
{
@ -1308,12 +1314,6 @@
"parameters": ["firefox", "webDriverBiDi"],
"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",
"platforms": ["darwin", "linux", "win32"],
@ -4613,34 +4613,10 @@
"expectations": ["SKIP"]
},
{
"testIdPattern": "[worker.spec] Workers should report console logs",
"testIdPattern": "[worker.spec] Workers can be closed",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"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"]
"expectations": ["FAIL"]
},
{
"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 => {
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 () => {
const {page} = await getTestState();
@ -70,7 +73,7 @@ describe('Workers', function () {
columnNumber: 8,
});
});
it('should have JSHandles for console logs', async () => {
it('should work with console logs', async () => {
const {page} = await getTestState();
const logPromise = waitEvent<ConsoleMessage>(page, 'console');
@ -80,9 +83,6 @@ describe('Workers', function () {
const log = await logPromise;
expect(log.text()).toBe('1 2 3 JSHandle@object');
expect(log.args()).toHaveLength(4);
expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe(
'null'
);
});
it('should have an execution context', async () => {
const {page} = await getTestState();