chore: refactor Sandbox and IsolatedWorld (#10807)

This commit is contained in:
jrandolf 2023-08-30 12:24:38 +02:00 committed by GitHub
parent 900a1f227d
commit b9744b2c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 416 deletions

View File

@ -34,63 +34,13 @@ import {
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from '../common/types.js';
import {importFSPromises} from '../common/util.js';
import {TaskManager} from '../common/WaitTask.js';
import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {FunctionLocator, Locator, NodeLocator} from './locators/locators.js';
/**
* @internal
*/
export interface Realm {
taskManager: TaskManager;
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
click(selector: string, options: Readonly<ClickOptions>): Promise<void>;
focus(selector: string): Promise<void>;
hover(selector: string): Promise<void>;
select(selector: string, ...values: string[]): Promise<string[]>;
tap(selector: string): Promise<void>;
type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
}
import {Realm} from './Realm.js';
/**
* @public

View File

@ -0,0 +1,228 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from '../common/types.js';
import {getPageContent, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {assert} from '../util/assert.js';
import {ClickOptions, ElementHandle} from './ElementHandle.js';
import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js';
/**
* @internal
*/
export abstract class Realm implements Disposable {
#timeoutSettings: TimeoutSettings;
#taskManager = new TaskManager();
constructor(timeoutSettings: TimeoutSettings) {
this.#timeoutSettings = timeoutSettings;
}
protected get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings;
}
get taskManager(): TaskManager {
return this.#taskManager;
}
abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
abstract evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
abstract evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async document(): Promise<ElementHandle<Document>> {
// TODO(#10813): Implement document caching.
return await this.evaluateHandle(() => {
return document;
});
}
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
using document = await this.document();
return await document.$(selector);
}
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
using document = await this.document();
return await document.$$(selector);
}
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
using document = await this.document();
return await document.$x(expression);
}
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
using document = await this.document();
return await document.$eval(selector, pageFunction, ...args);
}
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
using document = await this.document();
return await document.$$eval(selector, pageFunction, ...args);
}
async content(): Promise<string> {
return await this.evaluate(getPageContent);
}
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.#timeoutSettings.timeout(),
root,
signal,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
polling,
root,
timeout,
signal,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return waitTask.result;
}
async click(
selector: string,
options?: Readonly<ClickOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.click(options);
await handle.dispose();
}
async focus(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.focus();
}
async hover(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.hover();
}
async select(selector: string, ...values: string[]): Promise<string[]> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
return await handle.select(...values);
}
async tap(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.tap();
}
async type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.type(text, options);
}
get disposed(): boolean {
return this.#disposed;
}
#disposed = false;
[Symbol.dispose](): void {
this.#disposed = true;
this.taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')
);
}
}

View File

@ -412,7 +412,7 @@ export class Frame extends BaseFrame {
_detach(): void {
this.#detached = true;
this.worlds[MAIN_WORLD]._detach();
this.worlds[PUPPETEER_WORLD]._detach();
this.worlds[MAIN_WORLD][Symbol.dispose]();
this.worlds[PUPPETEER_WORLD][Symbol.dispose]();
}
}

View File

@ -16,11 +16,8 @@
import {Protocol} from 'devtools-protocol';
import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {Realm} from '../api/Frame.js';
import {KeyboardTypeOptions} from '../api/Input.js';
import {JSHandle} from '../api/JSHandle.js';
import {assert} from '../util/assert.js';
import {Realm} from '../api/Realm.js';
import {Deferred} from '../util/Deferred.js';
import {Binding} from './Binding.js';
@ -31,24 +28,14 @@ import {FrameManager} from './FrameManager.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {CDPJSHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {
BindingPayload,
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from './types.js';
import {BindingPayload, EvaluateFunc, HandleFor} from './types.js';
import {
addPageBinding,
createJSHandle,
debugError,
getPageContent,
setPageContent,
withSourcePuppeteerURLIfNone,
} from './util.js';
import {TaskManager, WaitTask} from './WaitTask.js';
/**
* @public
@ -102,27 +89,22 @@ export interface IsolatedWorldChart {
/**
* @internal
*/
export class IsolatedWorld implements Realm {
export class IsolatedWorld extends Realm {
#frame: Frame;
#context = Deferred.create<ExecutionContext>();
#detached = false;
// Set of bindings that have been registered in the current context.
#contextBindings = new Set<string>();
// Contains mapping from functions that should be bound to Puppeteer functions.
#bindings = new Map<string, Binding>();
#taskManager = new TaskManager();
get taskManager(): TaskManager {
return this.#taskManager;
}
get _bindings(): Map<string, Binding> {
return this.#bindings;
}
constructor(frame: Frame) {
super(frame._frameManager.timeoutSettings);
this.#frame = frame;
this.frameUpdated();
}
@ -139,10 +121,6 @@ export class IsolatedWorld implements Realm {
return this.#frame._frameManager;
}
get #timeoutSettings(): TimeoutSettings {
return this.#frameManager.timeoutSettings;
}
frame(): Frame {
return this.#frame;
}
@ -154,23 +132,15 @@ export class IsolatedWorld implements Realm {
setContext(context: ExecutionContext): void {
this.#contextBindings.clear();
this.#context.resolve(context);
void this.#taskManager.rerunAll();
void this.taskManager.rerunAll();
}
hasContext(): boolean {
return this.#context.resolved();
}
_detach(): void {
this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
this.#taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')
);
}
executionContext(): Promise<ExecutionContext> {
if (this.#detached) {
if (this.disposed) {
throw new Error(
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`
);
@ -211,70 +181,6 @@ export class IsolatedWorld implements Realm {
return context.evaluate(pageFunction, ...args);
}
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
using document = await this.document();
return await document.$(selector);
}
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
using document = await this.document();
return await document.$$(selector);
}
async document(): Promise<ElementHandle<Document>> {
// TODO(#10813): Implement document caching.
return await this.evaluateHandle(() => {
return document;
});
}
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
using document = await this.document();
return await document.$x(expression);
}
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
using document = await this.document();
return await document.$eval(selector, pageFunction, ...args);
}
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
using document = await this.document();
return await document.$$eval(selector, pageFunction, ...args);
}
async content(): Promise<string> {
return await this.evaluate(getPageContent);
}
async setContent(
html: string,
options: {
@ -284,7 +190,7 @@ export class IsolatedWorld implements Realm {
): Promise<void> {
const {
waitUntil = ['load'],
timeout = this.#timeoutSettings.navigationTimeout(),
timeout = this.timeoutSettings.navigationTimeout(),
} = options;
await setPageContent(this, html);
@ -305,50 +211,6 @@ export class IsolatedWorld implements Realm {
}
}
async click(
selector: string,
options?: Readonly<ClickOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.click(options);
await handle.dispose();
}
async focus(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.focus();
}
async hover(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.hover();
}
async select(selector: string, ...values: string[]): Promise<string[]> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
return await handle.select(...values);
}
async tap(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.tap();
}
async type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.type(text, options);
}
// If multiple waitFor are set up asynchronously, we need to wait for the
// first one to set up the binding in the page before running the others.
#mutex = new Mutex();
@ -431,46 +293,6 @@ export class IsolatedWorld implements Realm {
}
};
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.#timeoutSettings.timeout(),
root,
signal,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
polling,
root,
timeout,
signal,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return waitTask.result;
}
async title(): Promise<string> {
return this.evaluate(() => {
return document.title;
@ -522,6 +344,11 @@ export class IsolatedWorld implements Realm {
await handle.dispose();
return newHandle;
}
[Symbol.dispose](): void {
super[Symbol.dispose]();
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
}
}
class Mutex {

View File

@ -15,8 +15,8 @@
*/
import {ElementHandle} from '../api/ElementHandle.js';
import {Realm} from '../api/Frame.js';
import {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import type {Poller} from '../injected/Poller.js';
import {Deferred} from '../util/Deferred.js';
import {isErrorLike} from '../util/ErrorLike.js';

View File

@ -279,7 +279,7 @@ export class Frame extends BaseFrame {
this.#detached = true;
this.#abortDeferred.reject(new Error('Frame detached'));
this.#context.dispose();
this.sandboxes[MAIN_SANDBOX].dispose();
this.sandboxes[PUPPETEER_SANDBOX].dispose();
this.sandboxes[MAIN_SANDBOX][Symbol.dispose]();
this.sandboxes[PUPPETEER_SANDBOX][Symbol.dispose]();
}
}

View File

@ -16,21 +16,11 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {ClickOptions, ElementHandle} from '../../api/ElementHandle.js';
import {Realm as RealmBase} from '../../api/Frame.js';
import {KeyboardTypeOptions} from '../../api/Input.js';
import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js';
import {assert} from '../../util/assert.js';
import {Realm as RealmApi} from '../../api/Realm.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from '../types.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js';
import {TaskManager, WaitTask} from '../WaitTask.js';
import {BrowsingContext} from './BrowsingContext.js';
import {JSHandle} from './JSHandle.js';
@ -62,99 +52,26 @@ export interface SandboxChart {
/**
* @internal
*/
export class Sandbox implements RealmBase {
export class Sandbox extends RealmApi {
#realm: Realm;
#timeoutSettings: TimeoutSettings;
#taskManager = new TaskManager();
constructor(
// TODO: We should split the Realm and BrowsingContext
realm: Realm | BrowsingContext,
timeoutSettings: TimeoutSettings
) {
super(timeoutSettings);
this.#realm = realm;
this.#timeoutSettings = timeoutSettings;
// TODO: Tack correct realm similar to BrowsingContexts
this.#realm.connection.on(
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
() => {
void this.#taskManager.rerunAll();
void this.taskManager.rerunAll();
}
);
}
dispose(): void {
this.#taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')
);
}
get taskManager(): TaskManager {
return this.#taskManager;
}
async document(): Promise<ElementHandle<Document>> {
// TODO(#10813): Implement document caching.
return await this.#realm.evaluateHandle(() => {
return document;
});
}
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
using document = await this.document();
return await document.$(selector);
}
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
using document = await this.document();
return await document.$$(selector);
}
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
using document = await this.document();
return await document.$eval(selector, pageFunction, ...args);
}
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
using document = await this.document();
return await document.$$eval(selector, pageFunction, ...args);
}
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
using document = await this.document();
return await document.$x(expression);
}
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
@ -200,91 +117,4 @@ export class Sandbox implements RealmBase {
await handle.dispose();
return transferredHandle as unknown as T;
}
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.#timeoutSettings.timeout(),
root,
signal,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
polling,
root,
timeout,
signal,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return waitTask.result;
}
// ///////////////////
// // Input methods //
// ///////////////////
async click(
selector: string,
options?: Readonly<ClickOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.click(options);
}
async focus(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.focus();
}
async hover(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.hover();
}
async select(selector: string, ...values: string[]): Promise<string[]> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
const result = await handle.select(...values);
return result;
}
async tap(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.tap();
}
async type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.type(text, options);
}
}