diff --git a/packages/puppeteer-core/src/api/Frame.ts b/packages/puppeteer-core/src/api/Frame.ts index e7889dd1d87..5bae83e46d0 100644 --- a/packages/puppeteer-core/src/api/Frame.ts +++ b/packages/puppeteer-core/src/api/Frame.ts @@ -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> = EvaluateFunc< - InnerLazyParams - >, - >( - pageFunction: Func | string, - options: { - polling?: 'raf' | 'mutation' | number; - timeout?: number; - root?: ElementHandle; - signal?: AbortSignal; - }, - ...args: Params - ): Promise>>>; - adoptHandle>(handle: T): Promise; - transferHandle>(handle: T): Promise; - evaluateHandle< - Params extends unknown[], - Func extends EvaluateFunc = EvaluateFunc, - >( - pageFunction: Func | string, - ...args: Params - ): Promise>>>; - evaluate< - Params extends unknown[], - Func extends EvaluateFunc = EvaluateFunc, - >( - pageFunction: Func | string, - ...args: Params - ): Promise>>; - click(selector: string, options: Readonly): Promise; - focus(selector: string): Promise; - hover(selector: string): Promise; - select(selector: string, ...values: string[]): Promise; - tap(selector: string): Promise; - type( - selector: string, - text: string, - options?: Readonly - ): Promise; -} +import {Realm} from './Realm.js'; /** * @public diff --git a/packages/puppeteer-core/src/api/Realm.ts b/packages/puppeteer-core/src/api/Realm.ts new file mode 100644 index 00000000000..7332427a097 --- /dev/null +++ b/packages/puppeteer-core/src/api/Realm.ts @@ -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>(handle: T): Promise; + abstract transferHandle>(handle: T): Promise; + abstract evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc, + >( + pageFunction: Func | string, + ...args: Params + ): Promise>>>; + abstract evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc, + >( + pageFunction: Func | string, + ...args: Params + ): Promise>>; + + async document(): Promise> { + // TODO(#10813): Implement document caching. + return await this.evaluateHandle(() => { + return document; + }); + } + + async $( + selector: Selector + ): Promise> | null> { + using document = await this.document(); + return await document.$(selector); + } + + async $$( + selector: Selector + ): Promise>>> { + using document = await this.document(); + return await document.$$(selector); + } + + async $x(expression: string): Promise>> { + using document = await this.document(); + return await document.$x(expression); + } + + async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith, Params> = EvaluateFuncWith< + NodeFor, + Params + >, + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise>> { + 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>, + Params + > = EvaluateFuncWith>, Params>, + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise>> { + pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction); + using document = await this.document(); + return await document.$$eval(selector, pageFunction, ...args); + } + + async content(): Promise { + return await this.evaluate(getPageContent); + } + + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc> = EvaluateFunc< + InnerLazyParams + >, + >( + pageFunction: Func | string, + options: { + polling?: 'raf' | 'mutation' | number; + timeout?: number; + root?: ElementHandle; + signal?: AbortSignal; + } = {}, + ...args: Params + ): Promise>>> { + 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>>) + | string, + ...args + ); + return waitTask.result; + } + + async click( + selector: string, + options?: Readonly + ): Promise { + 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 { + using handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.focus(); + } + + async hover(selector: string): Promise { + using handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.hover(); + } + + async select(selector: string, ...values: string[]): Promise { + using handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + return await handle.select(...values); + } + + async tap(selector: string): Promise { + using handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.tap(); + } + + async type( + selector: string, + text: string, + options?: Readonly + ): Promise { + 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.') + ); + } +} diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts index 3197fe97e41..1d9e90b1d4b 100644 --- a/packages/puppeteer-core/src/common/Frame.ts +++ b/packages/puppeteer-core/src/common/Frame.ts @@ -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](); } } diff --git a/packages/puppeteer-core/src/common/IsolatedWorld.ts b/packages/puppeteer-core/src/common/IsolatedWorld.ts index 0a64aac3bbe..eb95bc08dad 100644 --- a/packages/puppeteer-core/src/common/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -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(); - #detached = false; // Set of bindings that have been registered in the current context. #contextBindings = new Set(); // Contains mapping from functions that should be bound to Puppeteer functions. #bindings = new Map(); - #taskManager = new TaskManager(); - - get taskManager(): TaskManager { - return this.#taskManager; - } get _bindings(): Map { 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 { - 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: Selector - ): Promise> | null> { - using document = await this.document(); - return await document.$(selector); - } - - async $$( - selector: Selector - ): Promise>>> { - using document = await this.document(); - return await document.$$(selector); - } - - async document(): Promise> { - // TODO(#10813): Implement document caching. - return await this.evaluateHandle(() => { - return document; - }); - } - - async $x(expression: string): Promise>> { - using document = await this.document(); - return await document.$x(expression); - } - - async $eval< - Selector extends string, - Params extends unknown[], - Func extends EvaluateFuncWith, Params> = EvaluateFuncWith< - NodeFor, - Params - >, - >( - selector: Selector, - pageFunction: Func | string, - ...args: Params - ): Promise>> { - 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>, - Params - > = EvaluateFuncWith>, Params>, - >( - selector: Selector, - pageFunction: Func | string, - ...args: Params - ): Promise>> { - pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction); - using document = await this.document(); - return await document.$$eval(selector, pageFunction, ...args); - } - - async content(): Promise { - return await this.evaluate(getPageContent); - } - async setContent( html: string, options: { @@ -284,7 +190,7 @@ export class IsolatedWorld implements Realm { ): Promise { 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 - ): Promise { - 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 { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.focus(); - } - - async hover(selector: string): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.hover(); - } - - async select(selector: string, ...values: string[]): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - return await handle.select(...values); - } - - async tap(selector: string): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.tap(); - } - - async type( - selector: string, - text: string, - options?: Readonly - ): Promise { - 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> = EvaluateFunc< - InnerLazyParams - >, - >( - pageFunction: Func | string, - options: { - polling?: 'raf' | 'mutation' | number; - timeout?: number; - root?: ElementHandle; - signal?: AbortSignal; - } = {}, - ...args: Params - ): Promise>>> { - 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>>) - | string, - ...args - ); - return waitTask.result; - } - async title(): Promise { 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 { diff --git a/packages/puppeteer-core/src/common/WaitTask.ts b/packages/puppeteer-core/src/common/WaitTask.ts index 1357c1f88a0..52619ff3807 100644 --- a/packages/puppeteer-core/src/common/WaitTask.ts +++ b/packages/puppeteer-core/src/common/WaitTask.ts @@ -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'; diff --git a/packages/puppeteer-core/src/common/bidi/Frame.ts b/packages/puppeteer-core/src/common/bidi/Frame.ts index 88824ca0c9b..54d729da694 100644 --- a/packages/puppeteer-core/src/common/bidi/Frame.ts +++ b/packages/puppeteer-core/src/common/bidi/Frame.ts @@ -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](); } } diff --git a/packages/puppeteer-core/src/common/bidi/Sandbox.ts b/packages/puppeteer-core/src/common/bidi/Sandbox.ts index 9ab9dcd4d05..662256e4995 100644 --- a/packages/puppeteer-core/src/common/bidi/Sandbox.ts +++ b/packages/puppeteer-core/src/common/bidi/Sandbox.ts @@ -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> { - // TODO(#10813): Implement document caching. - return await this.#realm.evaluateHandle(() => { - return document; - }); - } - - async $( - selector: Selector - ): Promise> | null> { - using document = await this.document(); - return await document.$(selector); - } - - async $$( - selector: Selector - ): Promise>>> { - using document = await this.document(); - return await document.$$(selector); - } - - async $eval< - Selector extends string, - Params extends unknown[], - Func extends EvaluateFuncWith, Params> = EvaluateFuncWith< - NodeFor, - Params - >, - >( - selector: Selector, - pageFunction: Func | string, - ...args: Params - ): Promise>> { - 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>, - Params - > = EvaluateFuncWith>, Params>, - >( - selector: Selector, - pageFunction: Func | string, - ...args: Params - ): Promise>> { - pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction); - using document = await this.document(); - return await document.$$eval(selector, pageFunction, ...args); - } - - async $x(expression: string): Promise>> { - using document = await this.document(); - return await document.$x(expression); - } - async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc, @@ -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> = EvaluateFunc< - InnerLazyParams - >, - >( - pageFunction: Func | string, - options: { - polling?: 'raf' | 'mutation' | number; - timeout?: number; - root?: ElementHandle; - signal?: AbortSignal; - } = {}, - ...args: Params - ): Promise>>> { - 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>>) - | string, - ...args - ); - return waitTask.result; - } - - // /////////////////// - // // Input methods // - // /////////////////// - async click( - selector: string, - options?: Readonly - ): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.click(options); - } - - async focus(selector: string): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.focus(); - } - - async hover(selector: string): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.hover(); - } - - async select(selector: string, ...values: string[]): Promise { - 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 { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.tap(); - } - - async type( - selector: string, - text: string, - options?: Readonly - ): Promise { - using handle = await this.$(selector); - assert(handle, `No element found for selector: ${selector}`); - await handle.type(text, options); - } }