chore: implement Frames for BiDi (#10121)

This commit is contained in:
Nikolay Vitkov 2023-05-15 16:39:47 +02:00 committed by GitHub
parent 609584a8b8
commit 2808240c71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1521 additions and 766 deletions

View File

@ -194,7 +194,8 @@ jobs:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
- macos-latest # Disabled as BiDi has issue on mac https://bugzilla.mozilla.org/show_bug.cgi?id=1832778
# - macos-latest
suite: suite:
- firefox-bidi - firefox-bidi
- firefox-headful - firefox-headful

16
package-lock.json generated
View File

@ -2710,9 +2710,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "0.4.7", "version": "0.4.9",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.9.tgz",
"integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", "integrity": "sha512-u3DC6XwgLCA9QJ5ak1voPslCmacQdulZNCPsI3qNXxSnEcZS7DFIbww+5RM2bznMEje7cc0oydavRLRvOIZtHw==",
"dependencies": { "dependencies": {
"mitt": "3.0.0" "mitt": "3.0.0"
}, },
@ -9464,7 +9464,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "1.2.0", "@puppeteer/browsers": "1.2.0",
"chromium-bidi": "0.4.7", "chromium-bidi": "0.4.9",
"cross-fetch": "3.1.5", "cross-fetch": "3.1.5",
"debug": "4.3.4", "debug": "4.3.4",
"devtools-protocol": "0.0.1120988", "devtools-protocol": "0.0.1120988",
@ -11441,9 +11441,9 @@
"version": "1.1.4" "version": "1.1.4"
}, },
"chromium-bidi": { "chromium-bidi": {
"version": "0.4.7", "version": "0.4.9",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.9.tgz",
"integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", "integrity": "sha512-u3DC6XwgLCA9QJ5ak1voPslCmacQdulZNCPsI3qNXxSnEcZS7DFIbww+5RM2bznMEje7cc0oydavRLRvOIZtHw==",
"requires": { "requires": {
"mitt": "3.0.0" "mitt": "3.0.0"
} }
@ -14503,7 +14503,7 @@
"version": "file:packages/puppeteer-core", "version": "file:packages/puppeteer-core",
"requires": { "requires": {
"@puppeteer/browsers": "1.2.0", "@puppeteer/browsers": "1.2.0",
"chromium-bidi": "0.4.7", "chromium-bidi": "0.4.9",
"cross-fetch": "3.1.5", "cross-fetch": "3.1.5",
"debug": "4.3.4", "debug": "4.3.4",
"devtools-protocol": "0.0.1120988", "devtools-protocol": "0.0.1120988",

View File

@ -132,7 +132,7 @@
"author": "The Chromium Authors", "author": "The Chromium Authors",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"chromium-bidi": "0.4.7", "chromium-bidi": "0.4.9",
"cross-fetch": "3.1.5", "cross-fetch": "3.1.5",
"debug": "4.3.4", "debug": "4.3.4",
"devtools-protocol": "0.0.1120988", "devtools-protocol": "0.0.1120988",

View File

@ -18,7 +18,6 @@ import {Protocol} from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js'; import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js'; import {ExecutionContext} from '../common/ExecutionContext.js';
import {Frame} from '../common/Frame.js';
import {MouseClickOptions} from '../common/Input.js'; import {MouseClickOptions} from '../common/Input.js';
import {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import { import {
@ -30,6 +29,7 @@ import {
} from '../common/types.js'; } from '../common/types.js';
import {KeyInput} from '../common/USKeyboardLayout.js'; import {KeyInput} from '../common/USKeyboardLayout.js';
import {Frame} from './Frame.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {ScreenshotOptions} from './Page.js'; import {ScreenshotOptions} from './Page.js';

View File

@ -0,0 +1,879 @@
/**
* 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 {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {HTTPResponse} from '../api/HTTPResponse.js';
import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {
IsolatedWorldChart,
WaitForSelectorOptions,
} from '../common/IsolatedWorld.js';
import {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import {
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
NodeFor,
} from '../common/types.js';
/**
* @public
*/
export interface FrameWaitForFunctionOptions {
/**
* An interval at which the `pageFunction` is executed, defaults to `raf`. If
* `polling` is a number, then it is treated as an interval in milliseconds at
* which the function would be executed. If `polling` is a string, then it can
* be one of the following values:
*
* - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame`
* callback. This is the tightest polling mode which is suitable to observe
* styling changes.
*
* - `mutation` - to execute `pageFunction` on every DOM mutation.
*/
polling?: 'raf' | 'mutation' | number;
/**
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
* using {@link Page.setDefaultTimeout}.
*/
timeout?: number;
/**
* A signal object that allows you to cancel a waitForFunction call.
*/
signal?: AbortSignal;
}
/**
* @public
*/
export interface FrameAddScriptTagOptions {
/**
* URL of the script to be added.
*/
url?: string;
/**
* Path to a JavaScript file to be injected into the frame.
*
* @remarks
* If `path` is a relative path, it is resolved relative to the current
* working directory (`process.cwd()` in Node.js).
*/
path?: string;
/**
* JavaScript to be injected into the frame.
*/
content?: string;
/**
* Sets the `type` of the script. Use `module` in order to load an ES2015 module.
*/
type?: string;
/**
* Sets the `id` of the script.
*/
id?: string;
}
/**
* @public
*/
export interface FrameAddStyleTagOptions {
/**
* the URL of the CSS file to be added.
*/
url?: string;
/**
* The path to a CSS file to be injected into the frame.
* @remarks
* If `path` is a relative path, it is resolved relative to the current
* working directory (`process.cwd()` in Node.js).
*/
path?: string;
/**
* Raw CSS content to be injected into the frame.
*/
content?: string;
}
/**
* Represents a DOM frame.
*
* To understand frames, you can think of frames as `<iframe>` elements. Just
* like iframes, frames can be nested, and when JavaScript is executed in a
* frame, the JavaScript does not effect frames inside the ambient frame the
* JavaScript executes in.
*
* @example
* At any point in time, {@link Page | pages} expose their current frame
* tree via the {@link Page.mainFrame} and {@link Frame.childFrames} methods.
*
* @example
* An example of dumping frame tree:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://www.google.com/chrome/browser/canary.html');
* dumpFrameTree(page.mainFrame(), '');
* await browser.close();
*
* function dumpFrameTree(frame, indent) {
* console.log(indent + frame.url());
* for (const child of frame.childFrames()) {
* dumpFrameTree(child, indent + ' ');
* }
* }
* })();
* ```
*
* @example
* An example of getting text from an iframe element:
*
* ```ts
* const frame = page.frames().find(frame => frame.name() === 'myframe');
* const text = await frame.$eval('.selector', element => element.textContent);
* console.log(text);
* ```
*
* @remarks
* Frame lifecycles are controlled by three events that are all dispatched on
* the parent {@link Frame.page | page}:
*
* - {@link PageEmittedEvents.FrameAttached}
* - {@link PageEmittedEvents.FrameNavigated}
* - {@link PageEmittedEvents.FrameDetached}
*
* @public
*/
export class Frame {
/**
* @internal
*/
_id!: string;
/**
* @internal
*/
_parentId?: string;
/**
* @internal
*/
worlds!: IsolatedWorldChart;
/**
* @internal
*/
_name?: string;
/**
* @internal
*/
_hasStartedLoading = false;
/**
* @internal
*/
constructor() {}
/**
* The page associated with the frame.
*/
page(): Page {
throw new Error('Not implemented');
}
/**
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
* `false`.
*/
isOOPFrame(): boolean {
throw new Error('Not implemented');
}
/**
* Navigates a frame to the given url.
*
* @remarks
* Navigation to `about:blank` or navigation to the same URL with a different
* hash will succeed and return `null`.
*
* :::warning
*
* Headless mode doesn't support navigation to a PDF document. See the {@link
* https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream
* issue}.
*
* :::
*
* @param url - the URL to navigate the frame to. This should include the
* scheme, e.g. `https://`.
* @param options - navigation options. `waitUntil` is useful to define when
* the navigation should be considered successful - see the docs for
* {@link PuppeteerLifeCycleEvent} for more details.
*
* @returns A promise which resolves to the main resource response. In case of
* multiple redirects, the navigation will resolve with the response of the
* last redirect.
* @throws This method will throw an error if:
*
* - there's an SSL error (e.g. in case of self-signed certificates).
* - target URL is invalid.
* - the `timeout` is exceeded during navigation.
* - the remote server does not respond or is unreachable.
* - the main resource failed to load.
*
* This method will not throw an error when any valid HTTP status code is
* returned by the remote server, including 404 "Not Found" and 500 "Internal
* Server Error". The status code for such responses can be retrieved by
* calling {@link HTTPResponse.status}.
*/
async goto(
url: string,
options?: {
referer?: string;
referrerPolicy?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<HTTPResponse | null>;
async goto(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
}
/**
* Waits for the frame to navigate. It is useful for when you run code which
* will indirectly cause the frame to navigate.
*
* Usage of the
* {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API}
* to change the URL is considered a navigation.
*
* @example
*
* ```ts
* const [response] = await Promise.all([
* // The navigation promise resolves after navigation has finished
* frame.waitForNavigation(),
* // Clicking the link will indirectly cause a navigation
* frame.click('a.my-link'),
* ]);
* ```
*
* @param options - options to configure when the navigation is consided
* finished.
* @returns a promise that resolves when the frame navigates to a new URL.
*/
async waitForNavigation(options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<HTTPResponse | null>;
async waitForNavigation(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_client(): CDPSession {
throw new Error('Not implemented');
}
/**
* @internal
*/
executionContext(): Promise<ExecutionContext> {
throw new Error('Not implemented');
}
/**
* Behaves identically to {@link Page.evaluateHandle} except it's run within
* the context of this frame.
*
* @see {@link Page.evaluateHandle} for details.
*/
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
throw new Error('Not implemented');
}
/**
* Behaves identically to {@link Page.evaluate} except it's run within the
* the context of this frame.
*
* @see {@link Page.evaluate} for details.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
}
/**
* Queries the frame for an element matching the given selector.
*
* @param selector - The selector to query for.
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async $<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
}
/**
* Queries the frame for all elements matching the given selector.
*
* @param selector - The selector to query for.
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>>;
async $$<Selector extends string>(): Promise<
Array<ElementHandle<NodeFor<Selector>>>
> {
throw new Error('Not implemented');
}
/**
* Runs the given function on the first element matching the given selector in
* the frame.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```ts
* const searchValue = await frame.$eval('#search', el => el.value);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the frame's context.
* The first element matching the selector will be passed to the function as
* its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
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>>>;
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
}
/**
* Runs the given function on an array of elements matching the given selector
* in the frame.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```js
* const divsCounts = await frame.$$eval('div', divs => divs.length);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the frame's context.
* An array of elements matching the given selector will be passed to the
* function as its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
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>>>;
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
}
/**
* @deprecated Use {@link Frame.$$} with the `xpath` prefix.
*
* Example: `await frame.$$('xpath/' + xpathExpression)`
*
* This method evaluates the given XPath expression and returns the results.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
* @param expression - the XPath expression to evaluate.
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>>;
async $x(): Promise<Array<ElementHandle<Node>>> {
throw new Error('Not implemented');
}
/**
* Waits for an element matching the given selector to appear in the frame.
*
* This method works across navigations.
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .mainFrame()
* .waitForSelector('img')
* .then(() => console.log('First URL with image: ' + currentURL));
*
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param selector - The selector to query and wait for.
* @param options - Options for customizing waiting behavior.
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
async waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async waitForSelector<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
}
/**
* @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
*
* Example: `await frame.waitForSelector('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the Frame.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* Wait for the `xpath` to appear in page. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the xpath doesn't appear after the `timeout` milliseconds of waiting, the
* function will throw.
*
* For a code example, see the example for {@link Frame.waitForSelector}. That
* function behaves identically other than taking a CSS selector rather than
* an XPath.
*
* @param xpath - the XPath expression to wait for.
* @param options - options to configure the visibility of the element and how
* long to wait before timing out.
*/
async waitForXPath(
xpath: string,
options?: WaitForSelectorOptions
): Promise<ElementHandle<Node> | null>;
async waitForXPath(): Promise<ElementHandle<Node> | null> {
throw new Error('Not implemented');
}
/**
* @example
* The `waitForFunction` can be used to observe viewport size change:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* . const browser = await puppeteer.launch();
* . const page = await browser.newPage();
* . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100');
* . page.setViewport({width: 50, height: 50});
* . await watchDog;
* . await browser.close();
* })();
* ```
*
* To pass arguments from Node.js to the predicate of `page.waitForFunction` function:
*
* ```ts
* const selector = '.foo';
* await frame.waitForFunction(
* selector => !!document.querySelector(selector),
* {}, // empty options object
* selector
* );
* ```
*
* @param pageFunction - the function to evaluate in the frame context.
* @param options - options to configure the polling method and timeout.
* @param args - arguments to pass to the `pageFunction`.
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options?: FrameWaitForFunctionOptions,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
throw new Error('Not implemented');
}
/**
* The full HTML contents of the frame, including the DOCTYPE.
*/
async content(): Promise<string> {
throw new Error('Not implemented');
}
/**
* Set the content of the frame.
*
* @param html - HTML markup to assign to the page.
* @param options - Options to configure how long before timing out and at
* what point to consider the content setting successful.
*/
async setContent(
html: string,
options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<void>;
async setContent(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The frame's `name` attribute as specified in the tag.
*
* @remarks
* If the name is empty, it returns the `id` attribute instead.
*
* @remarks
* This value is calculated once when the frame is created, and will not
* update if the attribute is changed later.
*/
name(): string {
return this._name || '';
}
/**
* The frame's URL.
*/
url(): string {
throw new Error('Not implemented');
}
/**
* The parent frame, if any. Detached and main frames return `null`.
*/
parentFrame(): Frame | null {
throw new Error('Not implemented');
}
/**
* An array of child frames.
*/
childFrames(): Frame[] {
throw new Error('Not implemented');
}
/**
* Is`true` if the frame has been detached. Otherwise, `false`.
*/
isDetached(): boolean {
throw new Error('Not implemented');
}
/**
* Adds a `<script>` tag into the page with the desired url or content.
*
* @param options - Options for the script.
* @returns An {@link ElementHandle | element handle} to the injected
* `<script>` element.
*/
async addScriptTag(
options: FrameAddScriptTagOptions
): Promise<ElementHandle<HTMLScriptElement>>;
async addScriptTag(): Promise<ElementHandle<HTMLScriptElement>> {
throw new Error('Not implemented');
}
/**
* Adds a `<link rel="stylesheet">` tag into the page with the desired URL or
* a `<style type="text/css">` tag with the content.
*
* @returns An {@link ElementHandle | element handle} to the loaded `<link>`
* or `<style>` element.
*/
async addStyleTag(
options: Omit<FrameAddStyleTagOptions, 'url'>
): Promise<ElementHandle<HTMLStyleElement>>;
async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLLinkElement>>;
async addStyleTag(): Promise<
ElementHandle<HTMLStyleElement | HTMLLinkElement>
> {
throw new Error('Not implemented');
}
/**
* Clicks the first element found that matches `selector`.
*
* @remarks
* If `click()` triggers a navigation event and there's a separate
* `page.waitForNavigation()` promise to be resolved, you may end up with a
* race condition that yields unexpected results. The correct pattern for
* click and wait for navigation is the following:
*
* ```ts
* const [response] = await Promise.all([
* page.waitForNavigation(waitOptions),
* frame.click(selector, clickOptions),
* ]);
* ```
*
* @param selector - The selector to query for.
*/
async click(
selector: string,
options?: Readonly<ClickOptions>
): Promise<void>;
async click(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Focuses the first element that matches the `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async focus(selector: string): Promise<void>;
async focus(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Hovers the pointer over the center of the first element that matches the
* `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async hover(selector: string): Promise<void>;
async hover(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Selects a set of value on the first `<select>` element that matches the
* `selector`.
*
* @example
*
* ```ts
* frame.select('select#colors', 'blue'); // single selection
* frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections
* ```
*
* @param selector - The selector to query for.
* @param values - The array of values to select. If the `<select>` has the
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
* @returns the list of values that were successfully selected.
* @throws Throws if there's no `<select>` matching `selector`.
*/
select(selector: string, ...values: string[]): Promise<string[]>;
select(): Promise<string[]> {
throw new Error('Not implemented');
}
/**
* Taps the first element that matches the `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async tap(selector: string): Promise<void>;
async tap(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character
* in the text.
*
* @remarks
* To press a special key, like `Control` or `ArrowDown`, use
* {@link Keyboard.press}.
*
* @example
*
* ```ts
* await frame.type('#mytextarea', 'Hello'); // Types instantly
* await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user
* ```
*
* @param selector - the selector for the element to type into. If there are
* multiple the first will be used.
* @param text - text to type into the element
* @param options - takes one option, `delay`, which sets the time to wait
* between key presses in milliseconds. Defaults to `0`.
*/
async type(
selector: string,
text: string,
options?: {delay: number}
): Promise<void>;
async type(): Promise<void> {
throw new Error('Not implemented');
}
/**
* @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
*
* Causes your script to wait for the given number of milliseconds.
*
* @remarks
* It's generally recommended to not wait for a number of seconds, but instead
* use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
* {@link Frame.waitForFunction} to wait for exactly the conditions you want.
*
* @example
*
* Wait for 1 second:
*
* ```ts
* await frame.waitForTimeout(1000);
* ```
*
* @param milliseconds - the number of milliseconds to wait.
*/
waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
/**
* The frame's title.
*/
async title(): Promise<string> {
throw new Error('Not implemented');
}
/**
* This method is typically coupled with an action that triggers a device
* request from an api such as WebBluetooth.
*
* :::caution
*
* This must be called before the device request is made. It will not return a
* currently active device prompt.
*
* :::
*
* @example
*
* ```ts
* const [devicePrompt] = Promise.all([
* frame.waitForDevicePrompt(),
* frame.click('#connect-bluetooth'),
* ]);
* await devicePrompt.select(
* await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
* );
* ```
*/
waitForDevicePrompt(
options?: WaitTimeoutOptions
): Promise<DeviceRequestPrompt>;
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
throw new Error('Not implemented');
}
}

View File

@ -16,8 +16,8 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js'; import {CDPSession} from '../common/Connection.js';
import {Frame} from '../common/Frame.js';
import {Frame} from './Frame.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
/** /**

View File

@ -16,9 +16,9 @@
import Protocol from 'devtools-protocol'; import Protocol from 'devtools-protocol';
import {Frame} from '../common/Frame.js';
import {SecurityDetails} from '../common/SecurityDetails.js'; import {SecurityDetails} from '../common/SecurityDetails.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
/** /**

View File

@ -28,12 +28,6 @@ import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js'; import type {Dialog} from '../common/Dialog.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js'; import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js'; import type {FileChooser} from '../common/FileChooser.js';
import type {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from '../common/Frame.js';
import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js'; import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js';
import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js'; import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
@ -60,6 +54,12 @@ import {assert} from '../util/assert.js';
import type {Browser} from './Browser.js'; import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js'; import type {BrowserContext} from './BrowserContext.js';
import type {ClickOptions, ElementHandle} from './ElementHandle.js'; import type {ClickOptions, ElementHandle} from './ElementHandle.js';
import type {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import type {JSHandle} from './JSHandle.js'; import type {JSHandle} from './JSHandle.js';
import {Locator} from './Locator.js'; import {Locator} from './Locator.js';

View File

@ -19,6 +19,7 @@ export * from './BrowserContext.js';
export * from './Page.js'; export * from './Page.js';
export * from './JSHandle.js'; export * from './JSHandle.js';
export * from './ElementHandle.js'; export * from './ElementHandle.js';
export * from './Frame.js';
export * from './HTTPResponse.js'; export * from './HTTPResponse.js';
export * from './HTTPRequest.js'; export * from './HTTPRequest.js';
export * from './Locator.js'; export * from './Locator.js';

View File

@ -253,7 +253,7 @@ export class CDPElementHandle<
override async contentFrame(): Promise<Frame | null> { override async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', { const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.remoteObject().objectId, objectId: this.id,
}); });
if (typeof nodeInfo.node.frameId !== 'string') { if (typeof nodeInfo.node.frameId !== 'string') {
return null; return null;
@ -268,7 +268,7 @@ export class CDPElementHandle<
try { try {
await this.client.send('DOM.scrollIntoViewIfNeeded', { await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.remoteObject().objectId, objectId: this.id,
}); });
} catch (error) { } catch (error) {
debugError(error); debugError(error);
@ -333,7 +333,7 @@ export class CDPElementHandle<
const [result, layoutMetrics] = await Promise.all([ const [result, layoutMetrics] = await Promise.all([
this.client this.client
.send('DOM.getContentQuads', { .send('DOM.getContentQuads', {
objectId: this.remoteObject().objectId, objectId: this.id,
}) })
.catch(debugError), .catch(debugError),
(this.#page as CDPPage)._client().send('Page.getLayoutMetrics'), (this.#page as CDPPage)._client().send('Page.getLayoutMetrics'),
@ -583,9 +583,8 @@ export class CDPElementHandle<
return path.resolve(filePath); return path.resolve(filePath);
} }
}); });
const {objectId} = this.remoteObject();
const {node} = await this.client.send('DOM.describeNode', { const {node} = await this.client.send('DOM.describeNode', {
objectId, objectId: this.id,
}); });
const {backendNodeId} = node; const {backendNodeId} = node;
@ -603,7 +602,7 @@ export class CDPElementHandle<
}); });
} else { } else {
await this.client.send('DOM.setFileInputFiles', { await this.client.send('DOM.setFileInputFiles', {
objectId, objectId: this.id,
files, files,
backendNodeId, backendNodeId,
}); });

View File

@ -17,6 +17,12 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {type ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import {type ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {
Frame as BaseFrame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from '../api/Frame.js';
import {HTTPResponse} from '../api/HTTPResponse.js'; import {HTTPResponse} from '../api/HTTPResponse.js';
import {Page, WaitTimeoutOptions} from '../api/Page.js'; import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
@ -42,185 +48,29 @@ import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
import {importFSPromises, withSourcePuppeteerURLIfNone} from './util.js'; import {importFSPromises, withSourcePuppeteerURLIfNone} from './util.js';
/** /**
* @public * @internal
*/ */
export interface FrameWaitForFunctionOptions { export class Frame extends BaseFrame {
/**
* An interval at which the `pageFunction` is executed, defaults to `raf`. If
* `polling` is a number, then it is treated as an interval in milliseconds at
* which the function would be executed. If `polling` is a string, then it can
* be one of the following values:
*
* - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame`
* callback. This is the tightest polling mode which is suitable to observe
* styling changes.
*
* - `mutation` - to execute `pageFunction` on every DOM mutation.
*/
polling?: 'raf' | 'mutation' | number;
/**
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
* using {@link Page.setDefaultTimeout}.
*/
timeout?: number;
/**
* A signal object that allows you to cancel a waitForFunction call.
*/
signal?: AbortSignal;
}
/**
* @public
*/
export interface FrameAddScriptTagOptions {
/**
* URL of the script to be added.
*/
url?: string;
/**
* Path to a JavaScript file to be injected into the frame.
*
* @remarks
* If `path` is a relative path, it is resolved relative to the current
* working directory (`process.cwd()` in Node.js).
*/
path?: string;
/**
* JavaScript to be injected into the frame.
*/
content?: string;
/**
* Sets the `type` of the script. Use `module` in order to load an ES2015 module.
*/
type?: string;
/**
* Sets the `id` of the script.
*/
id?: string;
}
/**
* @public
*/
export interface FrameAddStyleTagOptions {
/**
* the URL of the CSS file to be added.
*/
url?: string;
/**
* The path to a CSS file to be injected into the frame.
* @remarks
* If `path` is a relative path, it is resolved relative to the current
* working directory (`process.cwd()` in Node.js).
*/
path?: string;
/**
* Raw CSS content to be injected into the frame.
*/
content?: string;
}
/**
* Represents a DOM frame.
*
* To understand frames, you can think of frames as `<iframe>` elements. Just
* like iframes, frames can be nested, and when JavaScript is executed in a
* frame, the JavaScript does not effect frames inside the ambient frame the
* JavaScript executes in.
*
* @example
* At any point in time, {@link Page | pages} expose their current frame
* tree via the {@link Page.mainFrame} and {@link Frame.childFrames} methods.
*
* @example
* An example of dumping frame tree:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://www.google.com/chrome/browser/canary.html');
* dumpFrameTree(page.mainFrame(), '');
* await browser.close();
*
* function dumpFrameTree(frame, indent) {
* console.log(indent + frame.url());
* for (const child of frame.childFrames()) {
* dumpFrameTree(child, indent + ' ');
* }
* }
* })();
* ```
*
* @example
* An example of getting text from an iframe element:
*
* ```ts
* const frame = page.frames().find(frame => frame.name() === 'myframe');
* const text = await frame.$eval('.selector', element => element.textContent);
* console.log(text);
* ```
*
* @remarks
* Frame lifecycles are controlled by three events that are all dispatched on
* the parent {@link Frame.page | page}:
*
* - {@link PageEmittedEvents.FrameAttached}
* - {@link PageEmittedEvents.FrameNavigated}
* - {@link PageEmittedEvents.FrameDetached}
*
* @public
*/
export class Frame {
#url = ''; #url = '';
#detached = false; #detached = false;
#client!: CDPSession; #client!: CDPSession;
/** override worlds!: IsolatedWorldChart;
* @internal
*/
worlds!: IsolatedWorldChart;
/**
* @internal
*/
_frameManager: FrameManager; _frameManager: FrameManager;
/** override _id: string;
* @internal
*/
_id: string;
/**
* @internal
*/
_loaderId = ''; _loaderId = '';
/** override _name?: string;
* @internal override _hasStartedLoading = false;
*/
_name?: string;
/**
* @internal
*/
_hasStartedLoading = false;
/**
* @internal
*/
_lifecycleEvents = new Set<string>(); _lifecycleEvents = new Set<string>();
/** override _parentId?: string;
* @internal
*/
_parentId?: string;
/**
* @internal
*/
constructor( constructor(
frameManager: FrameManager, frameManager: FrameManager,
frameId: string, frameId: string,
parentFrameId: string | undefined, parentFrameId: string | undefined,
client: CDPSession client: CDPSession
) { ) {
super();
this._frameManager = frameManager; this._frameManager = frameManager;
this.#url = ''; this.#url = '';
this._id = frameId; this._id = frameId;
@ -232,9 +82,6 @@ export class Frame {
this.updateClient(client); this.updateClient(client);
} }
/**
* @internal
*/
updateClient(client: CDPSession): void { updateClient(client: CDPSession): void {
this.#client = client; this.#client = client;
this.worlds = { this.worlds = {
@ -243,59 +90,15 @@ export class Frame {
}; };
} }
/** override page(): Page {
* The page associated with the frame.
*/
page(): Page {
return this._frameManager.page(); return this._frameManager.page();
} }
/** override isOOPFrame(): boolean {
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
* `false`.
*/
isOOPFrame(): boolean {
return this.#client !== this._frameManager.client; return this.#client !== this._frameManager.client;
} }
/** override async goto(
* Navigates a frame to the given url.
*
* @remarks
* Navigation to `about:blank` or navigation to the same URL with a different
* hash will succeed and return `null`.
*
* :::warning
*
* Headless mode doesn't support navigation to a PDF document. See the {@link
* https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream
* issue}.
*
* :::
*
* @param url - the URL to navigate the frame to. This should include the
* scheme, e.g. `https://`.
* @param options - navigation options. `waitUntil` is useful to define when
* the navigation should be considered successful - see the docs for
* {@link PuppeteerLifeCycleEvent} for more details.
*
* @returns A promise which resolves to the main resource response. In case of
* multiple redirects, the navigation will resolve with the response of the
* last redirect.
* @throws This method will throw an error if:
*
* - there's an SSL error (e.g. in case of self-signed certificates).
* - target URL is invalid.
* - the `timeout` is exceeded during navigation.
* - the remote server does not respond or is unreachable.
* - the main resource failed to load.
*
* This method will not throw an error when any valid HTTP status code is
* returned by the remote server, including 404 "Not Found" and 500 "Internal
* Server Error". The status code for such responses can be retrieved by
* calling {@link HTTPResponse.status}.
*/
async goto(
url: string, url: string,
options: { options: {
referer?: string; referer?: string;
@ -378,30 +181,7 @@ export class Frame {
} }
} }
/** override async waitForNavigation(
* Waits for the frame to navigate. It is useful for when you run code which
* will indirectly cause the frame to navigate.
*
* Usage of the
* {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API}
* to change the URL is considered a navigation.
*
* @example
*
* ```ts
* const [response] = await Promise.all([
* // The navigation promise resolves after navigation has finished
* frame.waitForNavigation(),
* // Clicking the link will indirectly cause a navigation
* frame.click('a.my-link'),
* ]);
* ```
*
* @param options - options to configure when the navigation is consided
* finished.
* @returns a promise that resolves when the frame navigates to a new URL.
*/
async waitForNavigation(
options: { options: {
timeout?: number; timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
@ -432,27 +212,15 @@ export class Frame {
} }
} }
/** override _client(): CDPSession {
* @internal
*/
_client(): CDPSession {
return this.#client; return this.#client;
} }
/** override executionContext(): Promise<ExecutionContext> {
* @internal
*/
executionContext(): Promise<ExecutionContext> {
return this.worlds[MAIN_WORLD].executionContext(); return this.worlds[MAIN_WORLD].executionContext();
} }
/** override async evaluateHandle<
* Behaves identically to {@link Page.evaluateHandle} except it's run within
* the context of this frame.
*
* @see {@link Page.evaluateHandle} for details.
*/
async evaluateHandle<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>( >(
@ -466,13 +234,7 @@ export class Frame {
return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args); return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args);
} }
/** override async evaluate<
* Behaves identically to {@link Page.evaluate} except it's run within the
* the context of this frame.
*
* @see {@link Page.evaluate} for details.
*/
async evaluate<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>( >(
@ -486,53 +248,19 @@ export class Frame {
return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args); return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args);
} }
/** override async $<Selector extends string>(
* Queries the frame for an element matching the given selector.
*
* @param selector - The selector to query for.
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
async $<Selector extends string>(
selector: Selector selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
return this.worlds[MAIN_WORLD].$(selector); return this.worlds[MAIN_WORLD].$(selector);
} }
/** override async $$<Selector extends string>(
* Queries the frame for all elements matching the given selector.
*
* @param selector - The selector to query for.
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
async $$<Selector extends string>(
selector: Selector selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> { ): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
return this.worlds[MAIN_WORLD].$$(selector); return this.worlds[MAIN_WORLD].$$(selector);
} }
/** override async $eval<
* Runs the given function on the first element matching the given selector in
* the frame.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```ts
* const searchValue = await frame.$eval('#search', el => el.value);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the frame's context.
* The first element matching the selector will be passed to the function as
* its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $eval<
Selector extends string, Selector extends string,
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
@ -548,27 +276,7 @@ export class Frame {
return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args); return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args);
} }
/** override async $$eval<
* Runs the given function on an array of elements matching the given selector
* in the frame.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```js
* const divsCounts = await frame.$$eval('div', divs => divs.length);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the frame's context.
* An array of elements matching the given selector will be passed to the
* function as its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $$eval<
Selector extends string, Selector extends string,
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFuncWith< Func extends EvaluateFuncWith<
@ -584,56 +292,11 @@ export class Frame {
return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args); return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args);
} }
/** override async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
* @deprecated Use {@link Frame.$$} with the `xpath` prefix.
*
* Example: `await frame.$$('xpath/' + xpathExpression)`
*
* This method evaluates the given XPath expression and returns the results.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
* @param expression - the XPath expression to evaluate.
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.worlds[MAIN_WORLD].$x(expression); return this.worlds[MAIN_WORLD].$x(expression);
} }
/** override async waitForSelector<Selector extends string>(
* Waits for an element matching the given selector to appear in the frame.
*
* This method works across navigations.
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .mainFrame()
* .waitForSelector('img')
* .then(() => console.log('First URL with image: ' + currentURL));
*
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param selector - The selector to query and wait for.
* @param options - Options for customizing waiting behavior.
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
async waitForSelector<Selector extends string>(
selector: Selector, selector: Selector,
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
@ -646,29 +309,7 @@ export class Frame {
)) as ElementHandle<NodeFor<Selector>> | null; )) as ElementHandle<NodeFor<Selector>> | null;
} }
/** override async waitForXPath(
* @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
*
* Example: `await frame.waitForSelector('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the Frame.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* Wait for the `xpath` to appear in page. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the xpath doesn't appear after the `timeout` milliseconds of waiting, the
* function will throw.
*
* For a code example, see the example for {@link Frame.waitForSelector}. That
* function behaves identically other than taking a CSS selector rather than
* an XPath.
*
* @param xpath - the XPath expression to wait for.
* @param options - options to configure the visibility of the element and how
* long to wait before timing out.
*/
async waitForXPath(
xpath: string, xpath: string,
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<Node> | null> { ): Promise<ElementHandle<Node> | null> {
@ -678,40 +319,7 @@ export class Frame {
return this.waitForSelector(`xpath/${xpath}`, options); return this.waitForSelector(`xpath/${xpath}`, options);
} }
/** override waitForFunction<
* @example
* The `waitForFunction` can be used to observe viewport size change:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* . const browser = await puppeteer.launch();
* . const page = await browser.newPage();
* . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100');
* . page.setViewport({width: 50, height: 50});
* . await watchDog;
* . await browser.close();
* })();
* ```
*
* To pass arguments from Node.js to the predicate of `page.waitForFunction` function:
*
* ```ts
* const selector = '.foo';
* await frame.waitForFunction(
* selector => !!document.querySelector(selector),
* {}, // empty options object
* selector
* );
* ```
*
* @param pageFunction - the function to evaluate in the frame context.
* @param options - options to configure the polling method and timeout.
* @param args - arguments to pass to the `pageFunction`.
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/
waitForFunction<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>( >(
@ -726,21 +334,11 @@ export class Frame {
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>; ) as Promise<HandleFor<Awaited<ReturnType<Func>>>>;
} }
/** override async content(): Promise<string> {
* The full HTML contents of the frame, including the DOCTYPE.
*/
async content(): Promise<string> {
return this.worlds[PUPPETEER_WORLD].content(); return this.worlds[PUPPETEER_WORLD].content();
} }
/** override async setContent(
* Set the content of the frame.
*
* @param html - HTML markup to assign to the page.
* @param options - Options to configure how long before timing out and at
* what point to consider the content setting successful.
*/
async setContent(
html: string, html: string,
options: { options: {
timeout?: number; timeout?: number;
@ -750,56 +348,27 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].setContent(html, options); return this.worlds[PUPPETEER_WORLD].setContent(html, options);
} }
/** override name(): string {
* The frame's `name` attribute as specified in the tag.
*
* @remarks
* If the name is empty, it returns the `id` attribute instead.
*
* @remarks
* This value is calculated once when the frame is created, and will not
* update if the attribute is changed later.
*/
name(): string {
return this._name || ''; return this._name || '';
} }
/** override url(): string {
* The frame's URL.
*/
url(): string {
return this.#url; return this.#url;
} }
/** override parentFrame(): Frame | null {
* The parent frame, if any. Detached and main frames return `null`.
*/
parentFrame(): Frame | null {
return this._frameManager._frameTree.parentFrame(this._id) || null; return this._frameManager._frameTree.parentFrame(this._id) || null;
} }
/** override childFrames(): Frame[] {
* An array of child frames.
*/
childFrames(): Frame[] {
return this._frameManager._frameTree.childFrames(this._id); return this._frameManager._frameTree.childFrames(this._id);
} }
/** override isDetached(): boolean {
* Is`true` if the frame has been detached. Otherwise, `false`.
*/
isDetached(): boolean {
return this.#detached; return this.#detached;
} }
/** override async addScriptTag(
* Adds a `<script>` tag into the page with the desired url or content.
*
* @param options - Options for the script.
* @returns An {@link ElementHandle | element handle} to the injected
* `<script>` element.
*/
async addScriptTag(
options: FrameAddScriptTagOptions options: FrameAddScriptTagOptions
): Promise<ElementHandle<HTMLScriptElement>> { ): Promise<ElementHandle<HTMLScriptElement>> {
let {content = '', type} = options; let {content = '', type} = options;
@ -861,20 +430,13 @@ export class Frame {
); );
} }
/** override async addStyleTag(
* Adds a `<link rel="stylesheet">` tag into the page with the desired URL or
* a `<style type="text/css">` tag with the content.
*
* @returns An {@link ElementHandle | element handle} to the loaded `<link>`
* or `<style>` element.
*/
async addStyleTag(
options: Omit<FrameAddStyleTagOptions, 'url'> options: Omit<FrameAddStyleTagOptions, 'url'>
): Promise<ElementHandle<HTMLStyleElement>>; ): Promise<ElementHandle<HTMLStyleElement>>;
async addStyleTag( override async addStyleTag(
options: FrameAddStyleTagOptions options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLLinkElement>>; ): Promise<ElementHandle<HTMLLinkElement>>;
async addStyleTag( override async addStyleTag(
options: FrameAddStyleTagOptions options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> { ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
let {content = ''} = options; let {content = ''} = options;
@ -937,106 +499,30 @@ export class Frame {
); );
} }
/** override async click(
* Clicks the first element found that matches `selector`.
*
* @remarks
* If `click()` triggers a navigation event and there's a separate
* `page.waitForNavigation()` promise to be resolved, you may end up with a
* race condition that yields unexpected results. The correct pattern for
* click and wait for navigation is the following:
*
* ```ts
* const [response] = await Promise.all([
* page.waitForNavigation(waitOptions),
* frame.click(selector, clickOptions),
* ]);
* ```
*
* @param selector - The selector to query for.
*/
async click(
selector: string, selector: string,
options: Readonly<ClickOptions> = {} options: Readonly<ClickOptions> = {}
): Promise<void> { ): Promise<void> {
return this.worlds[PUPPETEER_WORLD].click(selector, options); return this.worlds[PUPPETEER_WORLD].click(selector, options);
} }
/** override async focus(selector: string): Promise<void> {
* Focuses the first element that matches the `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async focus(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].focus(selector); return this.worlds[PUPPETEER_WORLD].focus(selector);
} }
/** override async hover(selector: string): Promise<void> {
* Hovers the pointer over the center of the first element that matches the
* `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async hover(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].hover(selector); return this.worlds[PUPPETEER_WORLD].hover(selector);
} }
/** override select(selector: string, ...values: string[]): Promise<string[]> {
* Selects a set of value on the first `<select>` element that matches the
* `selector`.
*
* @example
*
* ```ts
* frame.select('select#colors', 'blue'); // single selection
* frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections
* ```
*
* @param selector - The selector to query for.
* @param values - The array of values to select. If the `<select>` has the
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
* @returns the list of values that were successfully selected.
* @throws Throws if there's no `<select>` matching `selector`.
*/
select(selector: string, ...values: string[]): Promise<string[]> {
return this.worlds[PUPPETEER_WORLD].select(selector, ...values); return this.worlds[PUPPETEER_WORLD].select(selector, ...values);
} }
/** override async tap(selector: string): Promise<void> {
* Taps the first element that matches the `selector`.
*
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
async tap(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].tap(selector); return this.worlds[PUPPETEER_WORLD].tap(selector);
} }
/** override async type(
* Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character
* in the text.
*
* @remarks
* To press a special key, like `Control` or `ArrowDown`, use
* {@link Keyboard.press}.
*
* @example
*
* ```ts
* await frame.type('#mytextarea', 'Hello'); // Types instantly
* await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user
* ```
*
* @param selector - the selector for the element to type into. If there are
* multiple the first will be used.
* @param text - text to type into the element
* @param options - takes one option, `delay`, which sets the time to wait
* between key presses in milliseconds. Defaults to `0`.
*/
async type(
selector: string, selector: string,
text: string, text: string,
options?: {delay: number} options?: {delay: number}
@ -1044,42 +530,16 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].type(selector, text, options); return this.worlds[PUPPETEER_WORLD].type(selector, text, options);
} }
/** override waitForTimeout(milliseconds: number): Promise<void> {
* @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
*
* Causes your script to wait for the given number of milliseconds.
*
* @remarks
* It's generally recommended to not wait for a number of seconds, but instead
* use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or
* {@link Frame.waitForFunction} to wait for exactly the conditions you want.
*
* @example
*
* Wait for 1 second:
*
* ```ts
* await frame.waitForTimeout(1000);
* ```
*
* @param milliseconds - the number of milliseconds to wait.
*/
waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(resolve, milliseconds); setTimeout(resolve, milliseconds);
}); });
} }
/** override async title(): Promise<string> {
* The frame's title.
*/
async title(): Promise<string> {
return this.worlds[PUPPETEER_WORLD].title(); return this.worlds[PUPPETEER_WORLD].title();
} }
/**
* @internal
*/
_deviceRequestPromptManager(): DeviceRequestPromptManager { _deviceRequestPromptManager(): DeviceRequestPromptManager {
if (this.isOOPFrame()) { if (this.isOOPFrame()) {
return this._frameManager._deviceRequestPromptManager(this.#client); return this._frameManager._deviceRequestPromptManager(this.#client);
@ -1089,53 +549,21 @@ export class Frame {
return parentFrame._deviceRequestPromptManager(); return parentFrame._deviceRequestPromptManager();
} }
/** override waitForDevicePrompt(
* This method is typically coupled with an action that triggers a device
* request from an api such as WebBluetooth.
*
* :::caution
*
* This must be called before the device request is made. It will not return a
* currently active device prompt.
*
* :::
*
* @example
*
* ```ts
* const [devicePrompt] = Promise.all([
* frame.waitForDevicePrompt(),
* frame.click('#connect-bluetooth'),
* ]);
* await devicePrompt.select(
* await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
* );
* ```
*/
waitForDevicePrompt(
options: WaitTimeoutOptions = {} options: WaitTimeoutOptions = {}
): Promise<DeviceRequestPrompt> { ): Promise<DeviceRequestPrompt> {
return this._deviceRequestPromptManager().waitForDevicePrompt(options); return this._deviceRequestPromptManager().waitForDevicePrompt(options);
} }
/**
* @internal
*/
_navigated(framePayload: Protocol.Page.Frame): void { _navigated(framePayload: Protocol.Page.Frame): void {
this._name = framePayload.name; this._name = framePayload.name;
this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`; this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
} }
/**
* @internal
*/
_navigatedWithinDocument(url: string): void { _navigatedWithinDocument(url: string): void {
this.#url = url; this.#url = url;
} }
/**
* @internal
*/
_onLifecycleEvent(loaderId: string, name: string): void { _onLifecycleEvent(loaderId: string, name: string): void {
if (name === 'init') { if (name === 'init') {
this._loaderId = loaderId; this._loaderId = loaderId;
@ -1144,24 +572,15 @@ export class Frame {
this._lifecycleEvents.add(name); this._lifecycleEvents.add(name);
} }
/**
* @internal
*/
_onLoadingStopped(): void { _onLoadingStopped(): void {
this._lifecycleEvents.add('DOMContentLoaded'); this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load'); this._lifecycleEvents.add('load');
} }
/**
* @internal
*/
_onLoadingStarted(): void { _onLoadingStarted(): void {
this._hasStartedLoading = true; this._hasStartedLoading = true;
} }
/**
* @internal
*/
_detach(): void { _detach(): void {
this.#detached = true; this.#detached = true;
this.worlds[MAIN_WORLD]._detach(); this.worlds[MAIN_WORLD]._detach();

View File

@ -25,6 +25,7 @@ import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {Frame as CDPFrame} from './Frame.js';
import {FrameTree} from './FrameTree.js'; import {FrameTree} from './FrameTree.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
@ -69,7 +70,7 @@ export class FrameManager extends EventEmitter {
/** /**
* @internal * @internal
*/ */
_frameTree = new FrameTree(); _frameTree = new FrameTree<Frame>();
/** /**
* Set of frame IDs stored to indicate if a frame has received a * Set of frame IDs stored to indicate if a frame has received a
@ -305,7 +306,7 @@ export class FrameManager extends EventEmitter {
return; return;
} }
frame = new Frame(this, frameId, parentFrameId, session); frame = new CDPFrame(this, frameId, parentFrameId, session);
this._frameTree.addFrame(frame); this._frameTree.addFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame); this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
} }
@ -331,7 +332,7 @@ export class FrameManager extends EventEmitter {
frame._id = frameId; frame._id = frameId;
} else { } else {
// Initial main frame navigation. // Initial main frame navigation.
frame = new Frame(this, frameId, undefined, this.#client); frame = new CDPFrame(this, frameId, undefined, this.#client);
} }
this._frameTree.addFrame(frame); this._frameTree.addFrame(frame);
} }

View File

@ -14,13 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import {Frame as BaseFrame} from '../api/Frame.js';
import { import {
createDeferredPromise, createDeferredPromise,
DeferredPromise, DeferredPromise,
} from '../util/DeferredPromise.js'; } from '../util/DeferredPromise.js';
import type {Frame} from './Frame.js';
/** /**
* Keeps track of the page frame tree and it's is managed by * Keeps track of the page frame tree and it's is managed by
* {@link FrameManager}. FrameTree uses frame IDs to reference frame and it * {@link FrameManager}. FrameTree uses frame IDs to reference frame and it
@ -28,7 +27,7 @@ import type {Frame} from './Frame.js';
* structure is eventually consistent. * structure is eventually consistent.
* @internal * @internal
*/ */
export class FrameTree { export class FrameTree<Frame extends BaseFrame> {
#frames = new Map<string, Frame>(); #frames = new Map<string, Frame>();
// frameID -> parentFrameID // frameID -> parentFrameID
#parentIds = new Map<string, string>(); #parentIds = new Map<string, string>();

View File

@ -15,6 +15,7 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import { import {
ContinueRequestOverrides, ContinueRequestOverrides,
ErrorCode, ErrorCode,
@ -31,7 +32,6 @@ import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ProtocolError} from './Errors.js'; import {ProtocolError} from './Errors.js';
import {Frame} from './Frame.js';
import {debugError, isString} from './util.js'; import {debugError, isString} from './util.js';
/** /**

View File

@ -15,6 +15,7 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import { import {
HTTPResponse as BaseHTTPResponse, HTTPResponse as BaseHTTPResponse,
RemoteAddress, RemoteAddress,
@ -23,7 +24,6 @@ import {createDeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ProtocolError} from './Errors.js'; import {ProtocolError} from './Errors.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {SecurityDetails} from './SecurityDetails.js'; import {SecurityDetails} from './SecurityDetails.js';

View File

@ -21,6 +21,12 @@ import {Protocol} from 'devtools-protocol';
import type {Browser} from '../api/Browser.js'; import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js'; import type {BrowserContext} from '../api/BrowserContext.js';
import {ClickOptions, ElementHandle} from '../api/ElementHandle.js'; import {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from '../api/Frame.js';
import {HTTPRequest} from '../api/HTTPRequest.js'; import {HTTPRequest} from '../api/HTTPRequest.js';
import {HTTPResponse} from '../api/HTTPResponse.js'; import {HTTPResponse} from '../api/HTTPResponse.js';
import {JSHandle} from '../api/JSHandle.js'; import {JSHandle} from '../api/JSHandle.js';
@ -55,12 +61,6 @@ import {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
import {Dialog} from './Dialog.js'; import {Dialog} from './Dialog.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
import {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {Keyboard, Mouse, Touchscreen} from './Input.js'; import {Keyboard, Mouse, Touchscreen} from './Input.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js';
@ -895,21 +895,21 @@ export class CDPPage extends Page {
} }
override async content(): Promise<string> { override async content(): Promise<string> {
return await this.#frameManager.mainFrame().content(); return await this.mainFrame().content();
} }
override async setContent( override async setContent(
html: string, html: string,
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<void> { ): Promise<void> {
await this.#frameManager.mainFrame().setContent(html, options); await this.mainFrame().setContent(html, options);
} }
override async goto( override async goto(
url: string, url: string,
options: WaitForOptions & {referer?: string; referrerPolicy?: string} = {} options: WaitForOptions & {referer?: string; referrerPolicy?: string} = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
return await this.#frameManager.mainFrame().goto(url, options); return await this.mainFrame().goto(url, options);
} }
override async reload( override async reload(
@ -926,7 +926,7 @@ export class CDPPage extends Page {
override async waitForNavigation( override async waitForNavigation(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
return await this.#frameManager.mainFrame().waitForNavigation(options); return await this.mainFrame().waitForNavigation(options);
} }
#sessionClosePromise(): Promise<Error> { #sessionClosePromise(): Promise<Error> {
@ -1163,7 +1163,7 @@ export class CDPPage extends Page {
'Throttling rate should be greater or equal to 1' 'Throttling rate should be greater or equal to 1'
); );
await this.#client.send('Emulation.setCPUThrottlingRate', { await this.#client.send('Emulation.setCPUThrottlingRate', {
rate: factor !== null ? factor : 1, rate: factor ?? 1,
}); });
} }
@ -1265,7 +1265,7 @@ export class CDPPage extends Page {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
return this.#frameManager.mainFrame().evaluate(pageFunction, ...args); return this.mainFrame().evaluate(pageFunction, ...args);
} }
override async evaluateOnNewDocument< override async evaluateOnNewDocument<

View File

@ -15,12 +15,12 @@
*/ */
import {ElementHandle} from '../api/ElementHandle.js'; import {ElementHandle} from '../api/ElementHandle.js';
import type {Frame} from '../api/Frame.js';
import type PuppeteerUtil from '../injected/injected.js'; import type PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js'; import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import type {Frame} from './Frame.js';
import {transposeIterableHandle} from './HandleIterator.js'; import {transposeIterableHandle} from './HandleIterator.js';
import type {WaitForSelectorOptions} from './IsolatedWorld.js'; import type {WaitForSelectorOptions} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';

View File

@ -33,16 +33,18 @@ import {Connection} from './Connection.js';
* @internal * @internal
*/ */
export class Browser extends BrowserBase { export class Browser extends BrowserBase {
static readonly subscribeModules = ['browsingContext'];
static async create(opts: Options): Promise<Browser> { static async create(opts: Options): Promise<Browser> {
// TODO: await until the connection is established. // TODO: await until the connection is established.
try { try {
await opts.connection.send('session.new', {}); await opts.connection.send('session.new', {});
} catch {} } catch {}
await opts.connection.send('session.subscribe', { await opts.connection.send('session.subscribe', {
events: [ events: Browser.subscribeModules as Bidi.Message.EventNames[],
'browsingContext.contextCreated',
] as Bidi.Session.SubscribeParametersEvent[],
}); });
return new Browser(opts); return new Browser(opts);
} }

View File

@ -14,13 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js'; import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Page as PageBase} from '../../api/Page.js'; import {Page as PageBase} from '../../api/Page.js';
import {Viewport} from '../PuppeteerViewport.js'; import {Viewport} from '../PuppeteerViewport.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {Context} from './Context.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
import {debugError} from './utils.js';
interface BrowserContextOptions { interface BrowserContextOptions {
defaultViewport: Viewport | null; defaultViewport: Viewport | null;
@ -32,19 +34,34 @@ interface BrowserContextOptions {
export class BrowserContext extends BrowserContextBase { export class BrowserContext extends BrowserContextBase {
#connection: Connection; #connection: Connection;
#defaultViewport: Viewport | null; #defaultViewport: Viewport | null;
#pages = new Map<string, Page>();
#onContextDestroyedBind = this.#onContextDestroyed.bind(this);
constructor(connection: Connection, options: BrowserContextOptions) { constructor(connection: Connection, options: BrowserContextOptions) {
super(); super();
this.#connection = connection; this.#connection = connection;
this.#defaultViewport = options.defaultViewport; this.#defaultViewport = options.defaultViewport;
this.#connection.on(
'browsingContext.contextDestroyed',
this.#onContextDestroyedBind
);
}
async #onContextDestroyed(
event: Bidi.BrowsingContext.ContextDestroyedEvent['params']
) {
const page = this.#pages.get(event.context);
await page?.close().catch(error => {
debugError(error);
});
this.#pages.delete(event.context);
} }
override async newPage(): Promise<PageBase> { override async newPage(): Promise<PageBase> {
const {result} = await this.#connection.send('browsingContext.create', { const {result} = await this.#connection.send('browsingContext.create', {
type: 'tab', type: 'tab',
}); });
const context = this.#connection.context(result.context) as Context; const page = await Page._create(this.#connection, result);
const page = new Page(context);
if (this.#defaultViewport) { if (this.#defaultViewport) {
try { try {
await page.setViewport(this.#defaultViewport); await page.setViewport(this.#defaultViewport);
@ -52,8 +69,18 @@ export class BrowserContext extends BrowserContextBase {
// No support for setViewport in Firefox. // No support for setViewport in Firefox.
} }
} }
this.#pages.set(result.context, page);
return page; return page;
} }
override async close(): Promise<void> {} override async close(): Promise<void> {
for (const page of this.#pages.values()) {
await page?.close().catch(error => {
debugError(error);
});
}
this.#pages = new Map();
}
} }

View File

@ -49,7 +49,11 @@ interface Commands {
}; };
'browsingContext.close': { 'browsingContext.close': {
params: Bidi.BrowsingContext.CloseParameters; params: Bidi.BrowsingContext.CloseParameters;
returnType: Bidi.BrowsingContext.CloseResult; returnType: Bidi.Message.EmptyResult;
};
'browsingContext.getTree': {
params: Bidi.BrowsingContext.GetTreeParameters;
returnType: Bidi.BrowsingContext.GetTreeResult;
}; };
'browsingContext.navigate': { 'browsingContext.navigate': {
params: Bidi.BrowsingContext.NavigateParameters; params: Bidi.BrowsingContext.NavigateParameters;
@ -73,11 +77,11 @@ interface Commands {
returnType: Bidi.Session.StatusResult; returnType: Bidi.Session.StatusResult;
}; };
'session.subscribe': { 'session.subscribe': {
params: Bidi.Session.SubscribeParameters; params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.Session.SubscribeResult; returnType: Bidi.Session.SubscribeResult;
}; };
'session.unsubscribe': { 'session.unsubscribe': {
params: Bidi.Session.SubscribeParameters; params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.Session.UnsubscribeResult; returnType: Bidi.Session.UnsubscribeResult;
}; };
'cdp.sendCommand': { 'cdp.sendCommand': {
@ -159,7 +163,6 @@ export class Connection extends EventEmitter {
this.#callbacks.resolve(object.id, object); this.#callbacks.resolve(object.id, object);
} }
} else { } else {
this.#handleSpecialEvents(object);
this.#maybeEmitOnContext(object); this.#maybeEmitOnContext(object);
this.emit(object.method, object.params); this.emit(object.method, object.params);
} }
@ -177,14 +180,12 @@ export class Connection extends EventEmitter {
context?.emit(event.method, event.params); context?.emit(event.method, event.params);
} }
#handleSpecialEvents(event: Bidi.Message.EventMessage) { registerContext(context: Context): void {
switch (event.method) { this.#contexts.set(context.id, context);
case 'browsingContext.contextCreated': }
this.#contexts.set(
event.params.context, unregisterContext(context: Context): void {
new Context(this, event.params) this.#contexts.delete(context.id);
);
}
} }
#onClose(): void { #onClose(): void {

View File

@ -24,7 +24,6 @@ import {ProtocolMapping} from '../Connection.js';
import {ProtocolError, TimeoutError} from '../Errors.js'; import {ProtocolError, TimeoutError} from '../Errors.js';
import {EventEmitter} from '../EventEmitter.js'; import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js'; import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import { import {
getSourcePuppeteerURLIfAvailable, getSourcePuppeteerURLIfAvailable,
@ -36,6 +35,7 @@ import {
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {FrameManager} from './FrameManager.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js'; import {createEvaluationError} from './utils.js';
@ -71,14 +71,28 @@ const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([
export class Context extends EventEmitter { export class Context extends EventEmitter {
#connection: Connection; #connection: Connection;
#url: string; #url: string;
_contextId: string; #id: string;
_timeoutSettings = new TimeoutSettings(); #parentId?: string | null;
_frameManager: FrameManager;
constructor(connection: Connection, result: Bidi.BrowsingContext.Info) { constructor(
connection: Connection,
frameManager: FrameManager,
result: Bidi.BrowsingContext.Info
) {
super(); super();
this.#connection = connection; this.#connection = connection;
this._contextId = result.context; this._frameManager = frameManager;
this.#id = result.context;
this.#parentId = result.parent;
this.#url = result.url; this.#url = result.url;
this.on(
'browsingContext.fragmentNavigated',
(info: Bidi.BrowsingContext.NavigationInfo) => {
this.#url = info.url;
}
);
} }
get connection(): Connection { get connection(): Connection {
@ -86,7 +100,11 @@ export class Context extends EventEmitter {
} }
get id(): string { get id(): string {
return this._contextId; return this.#id;
}
get parentId(): string | undefined | null {
return this.#parentId;
} }
async evaluateHandle< async evaluateHandle<
@ -146,8 +164,8 @@ export class Context extends EventEmitter {
: `${pageFunction}\n${sourceUrlComment}\n`; : `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.#connection.send('script.evaluate', { responsePromise = this.#connection.send('script.evaluate', {
expression: expression, expression,
target: {context: this._contextId}, target: {context: this.#id},
resultOwnership, resultOwnership,
awaitPromise: true, awaitPromise: true,
}); });
@ -163,7 +181,7 @@ export class Context extends EventEmitter {
return BidiSerializer.serialize(arg, this); return BidiSerializer.serialize(arg, this);
}) })
), ),
target: {context: this._contextId}, target: {context: this.#id},
resultOwnership, resultOwnership,
awaitPromise: true, awaitPromise: true,
}); });
@ -189,7 +207,7 @@ export class Context extends EventEmitter {
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
const { const {
waitUntil = 'load', waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(), timeout = this._frameManager._timeoutSettings.navigationTimeout(),
} = options; } = options;
const readinessState = lifeCycleToReadinessState.get( const readinessState = lifeCycleToReadinessState.get(
@ -229,7 +247,7 @@ export class Context extends EventEmitter {
): Promise<void> { ): Promise<void> {
const { const {
waitUntil = 'load', waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(), timeout = this._frameManager._timeoutSettings.navigationTimeout(),
} = options; } = options;
const waitUntilCommand = lifeCycleToSubscribedEvent.get( const waitUntilCommand = lifeCycleToSubscribedEvent.get(
@ -250,12 +268,25 @@ export class Context extends EventEmitter {
]); ]);
} }
async content(): Promise<string> {
return await this.evaluate(() => {
let retVal = '';
if (document.doctype) {
retVal = new XMLSerializer().serializeToString(document.doctype);
}
if (document.documentElement) {
retVal += document.documentElement.outerHTML;
}
return retVal;
});
}
async sendCDPCommand( async sendCDPCommand(
method: keyof ProtocolMapping.Commands, method: keyof ProtocolMapping.Commands,
params: object = {} params: object = {}
): Promise<unknown> { ): Promise<unknown> {
const session = await this.#connection.send('cdp.getSession', { const session = await this.#connection.send('cdp.getSession', {
context: this._contextId, context: this.id,
}); });
// TODO: remove any once chromium-bidi types are updated. // TODO: remove any once chromium-bidi types are updated.
const sessionId = (session.result as any).cdpSession; const sessionId = (session.result as any).cdpSession;

View File

@ -0,0 +1,125 @@
/**
* 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 {Frame as BaseFrame} from '../../api/Frame.js';
import {HTTPResponse} from '../../api/HTTPResponse.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {Context} from './Context.js';
import {FrameManager} from './FrameManager.js';
import {Page} from './Page.js';
/**
* @internal
*/
export class Frame extends BaseFrame {
_frameManager: FrameManager;
_context: Context;
/**
* @internal
*/
constructor(frameManager: FrameManager, context: Context) {
super();
this._frameManager = frameManager;
this._context = context;
this._id = context.id;
this._parentId = context.parentId ?? undefined;
}
override page(): Page {
return this._frameManager.page();
}
override async goto(
url: string,
options?: {
referer?: string;
referrerPolicy?: string;
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<HTTPResponse | null> {
return this._context.goto(url, options);
}
override async waitForNavigation(options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<HTTPResponse | null>;
override async waitForNavigation(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
}
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this._context.evaluateHandle(pageFunction, ...args);
}
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return this._context.evaluate(pageFunction, ...args);
}
override async content(): Promise<string> {
return this._context.content();
}
override async setContent(
html: string,
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<void> {
return this._context.setContent(html, options);
}
override name(): string {
return this._name || '';
}
override url(): string {
return this._context.url();
}
override parentFrame(): Frame | null {
return this._frameManager._frameTree.parentFrame(this._id) ?? null;
}
override childFrames(): Frame[] {
return this._frameManager._frameTree.childFrames(this._id);
}
override isDetached(): boolean {
throw new Error('Not implemented');
}
override async title(): Promise<string> {
throw new Error('Not implemented');
}
}

View File

@ -0,0 +1,156 @@
/**
* Copyright 2017 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {assert} from '../../util/assert.js';
import {EventEmitter, Handler} from '../EventEmitter.js';
import {FrameManagerEmittedEvents} from '../FrameManager.js';
import {FrameTree} from '../FrameTree.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {Connection} from './Connection.js';
import {Context} from './Context.js';
import {Frame} from './Frame.js';
import {Page} from './Page.js';
/**
* A frame manager manages the frames for a given {@link Page | page}.
*
* @internal
*/
export class FrameManager extends EventEmitter {
#page: Page;
#connection: Connection;
_contextId: string;
_frameTree = new FrameTree<Frame>();
_timeoutSettings: TimeoutSettings;
get client(): Connection {
return this.#connection;
}
// TODO: switch string to (typeof Browser.events)[number]
#subscribedEvents = new Map<string, Handler<any>>([
['browsingContext.contextCreated', this.#onFrameAttached.bind(this)],
['browsingContext.contextDestroyed', this.#onFrameDetached.bind(this)],
['browsingContext.fragmentNavigated', this.#onFrameNavigated.bind(this)],
]);
constructor(
connection: Connection,
page: Page,
info: Bidi.BrowsingContext.Info,
timeoutSettings: TimeoutSettings
) {
super();
this.#connection = connection;
this.#page = page;
this._contextId = info.context;
this._timeoutSettings = timeoutSettings;
this.#handleFrameTree(info);
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.on(event, subscriber);
}
}
page(): Page {
return this.#page;
}
mainFrame(): Frame {
const mainFrame = this._frameTree.getMainFrame();
assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
}
frames(): Frame[] {
return Array.from(this._frameTree.frames());
}
frame(frameId: string): Frame | null {
return this._frameTree.getById(frameId) || null;
}
#handleFrameTree(info: Bidi.BrowsingContext.Info): void {
if (info) {
this.#onFrameAttached(info);
}
if (!info.children) {
return;
}
for (const child of info.children) {
this.#handleFrameTree(child);
}
}
#onFrameAttached(info: Bidi.BrowsingContext.Info): void {
if (
!this.frame(info.context) &&
(this.frame(info.parent ?? '') || !this._frameTree.getMainFrame())
) {
const context = new Context(this.#connection, this, info);
this.#connection.registerContext(context);
const frame = new Frame(this, context);
this._frameTree.addFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
}
}
async #onFrameNavigated(
info: Bidi.BrowsingContext.NavigationInfo
): Promise<void> {
const frameId = info.context;
let frame = this._frameTree.getById(frameId);
// Detach all child frames first.
if (frame) {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
}
frame = await this._frameTree.waitForFrame(frameId);
// frame._navigated(info);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
}
#onFrameDetached(info: Bidi.BrowsingContext.Info): void {
const frame = this.frame(info.context);
if (frame) {
this.#removeFramesRecursively(frame);
}
}
#removeFramesRecursively(frame: Frame): void {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
this.#connection.unregisterContext(frame._context);
this._frameTree.removeFrame(frame);
this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
}
dispose(): void {
this.removeAllListeners();
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.off(event, subscriber);
}
}
}

View File

@ -27,9 +27,11 @@ import {
} from '../../api/Page.js'; } from '../../api/Page.js';
import {isErrorLike} from '../../util/ErrorLike.js'; import {isErrorLike} from '../../util/ErrorLike.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import {Handler} from '../EventEmitter.js'; import {EventType, Handler} from '../EventEmitter.js';
import {FrameManagerEmittedEvents} from '../FrameManager.js';
import {PDFOptions} from '../PDFOptions.js'; import {PDFOptions} from '../PDFOptions.js';
import {Viewport} from '../PuppeteerViewport.js'; import {Viewport} from '../PuppeteerViewport.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import { import {
debugError, debugError,
@ -37,31 +39,77 @@ import {
withSourcePuppeteerURLIfNone, withSourcePuppeteerURLIfNone,
} from '../util.js'; } from '../util.js';
import {Connection} from './Connection.js';
import {Context, getBidiHandle} from './Context.js'; import {Context, getBidiHandle} from './Context.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
/** /**
* @internal * @internal
*/ */
export class Page extends PageBase { export class Page extends PageBase {
#context: Context; _timeoutSettings = new TimeoutSettings();
#connection: Connection;
#frameManager: FrameManager;
#viewport: Viewport | null = null;
#closed = false;
#subscribedEvents = new Map<string, Handler<any>>([ #subscribedEvents = new Map<string, Handler<any>>([
['log.entryAdded', this.#onLogEntryAdded.bind(this)], ['log.entryAdded', this.#onLogEntryAdded.bind(this)],
['browsingContext.load', this.#onLoad.bind(this)], ['browsingContext.load', this.#onLoad.bind(this)],
['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)], ['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>; ]) as Map<Bidi.Session.SubscriptionRequestEvent, Handler>;
#viewport: Viewport | null = null; #frameManagerEvents = new Map<EventType, Handler<any>>([
[
FrameManagerEmittedEvents.FrameAttached,
frame => {
return this.emit(PageEmittedEvents.FrameAttached, frame);
},
],
[
FrameManagerEmittedEvents.FrameDetached,
frame => {
return this.emit(PageEmittedEvents.FrameDetached, frame);
},
],
[
FrameManagerEmittedEvents.FrameNavigated,
frame => {
return this.emit(PageEmittedEvents.FrameNavigated, frame);
},
],
]);
constructor(context: Context) { constructor(connection: Connection, info: Bidi.BrowsingContext.Info) {
super(); super();
this.#context = context; this.#connection = connection;
this.#context.connection this.#frameManager = new FrameManager(
this.#connection,
this,
info,
this._timeoutSettings
);
for (const [event, subscriber] of this.#frameManagerEvents) {
this.#frameManager.on(event, subscriber);
}
}
static async _create(
connection: Connection,
info: Bidi.BrowsingContext.Info
): Promise<Page> {
const page = new Page(connection, info);
for (const [event, subscriber] of page.#subscribedEvents) {
page.context().on(event, subscriber);
}
await page.#connection
.send('session.subscribe', { .send('session.subscribe', {
events: [ events: [...page.#subscribedEvents.keys()],
...this.#subscribedEvents.keys(), contexts: [info.context],
] as Bidi.Session.SubscribeParameters['events'],
contexts: [this.#context.id],
}) })
.catch(error => { .catch(error => {
if (isErrorLike(error) && !error.message.includes('Target closed')) { if (isErrorLike(error) && !error.message.includes('Target closed')) {
@ -69,15 +117,25 @@ export class Page extends PageBase {
} }
}); });
for (const [event, subscriber] of this.#subscribedEvents) { return page;
this.#context.on(event, subscriber); }
}
override mainFrame(): Frame {
return this.#frameManager.mainFrame();
}
override frames(): Frame[] {
return this.#frameManager.frames();
}
context(): Context {
return this.#frameManager.mainFrame()._context;
} }
#onLogEntryAdded(event: Bidi.Log.LogEntry): void { #onLogEntryAdded(event: Bidi.Log.LogEntry): void {
if (isConsoleLogEntry(event)) { if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => { const args = event.args.map(arg => {
return getBidiHandle(this.#context, arg); return getBidiHandle(this.context(), arg);
}); });
const text = args const text = args
@ -134,18 +192,30 @@ export class Page extends PageBase {
} }
override async close(): Promise<void> { override async close(): Promise<void> {
await this.#context.connection.send('session.unsubscribe', { if (this.#closed) {
events: [...this.#subscribedEvents.keys()], return;
contexts: [this.#context.id], }
}); this.#closed = true;
this.removeAllListeners();
await this.#context.connection.send('browsingContext.close', { this.#frameManager.dispose();
context: this.#context.id,
});
for (const [event, subscriber] of this.#subscribedEvents) { for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.off(event, subscriber); this.context().off(event, subscriber);
} }
await this.#connection
.send('session.unsubscribe', {
events: [...this.#subscribedEvents.keys()],
contexts: [this.context().id],
})
.catch(() => {
// Suppress the error as we remove the context
// after that anyway.
});
await this.#connection.send('browsingContext.close', {
context: this.context().id,
});
} }
override async evaluateHandle< override async evaluateHandle<
@ -159,7 +229,7 @@ export class Page extends PageBase {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
return this.#context.evaluateHandle(pageFunction, ...args); return this.mainFrame().evaluateHandle(pageFunction, ...args);
} }
override async evaluate< override async evaluate<
@ -173,7 +243,7 @@ export class Page extends PageBase {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
return this.#context.evaluate(pageFunction, ...args); return this.mainFrame().evaluate(pageFunction, ...args);
} }
override async goto( override async goto(
@ -183,26 +253,26 @@ export class Page extends PageBase {
referrerPolicy?: string | undefined; referrerPolicy?: string | undefined;
} }
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
return this.#context.goto(url, options); return this.mainFrame().goto(url, options);
} }
override url(): string { override url(): string {
return this.#context.url(); return this.mainFrame().url();
} }
override setDefaultNavigationTimeout(timeout: number): void { override setDefaultNavigationTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultNavigationTimeout(timeout); this._timeoutSettings.setDefaultNavigationTimeout(timeout);
} }
override setDefaultTimeout(timeout: number): void { override setDefaultTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
} }
override async setContent( override async setContent(
html: string, html: string,
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<void> { ): Promise<void> {
await this.#context.setContent(html, options); await this.mainFrame().setContent(html, options);
} }
override async content(): Promise<string> { override async content(): Promise<string> {
@ -226,7 +296,7 @@ export class Page extends PageBase {
const deviceScaleFactor = 1; const deviceScaleFactor = 1;
const screenOrientation = {angle: 0, type: 'portraitPrimary'}; const screenOrientation = {angle: 0, type: 'portraitPrimary'};
await this.#context.sendCDPCommand('Emulation.setDeviceMetricsOverride', { await this.context().sendCDPCommand('Emulation.setDeviceMetricsOverride', {
mobile, mobile,
width, width,
height, height,
@ -255,8 +325,8 @@ export class Page extends PageBase {
timeout, timeout,
} = this._getPDFOptions(options, 'cm'); } = this._getPDFOptions(options, 'cm');
const {result} = await waitWithTimeout( const {result} = await waitWithTimeout(
this.#context.connection.send('browsingContext.print', { this.#connection.send('browsingContext.print', {
context: this.#context._contextId, context: this.context().id,
background, background,
margin, margin,
orientation: landscape ? 'landscape' : 'portrait', orientation: landscape ? 'landscape' : 'portrait',
@ -310,10 +380,10 @@ export class Page extends PageBase {
throw new Error('BiDi only supports "encoding" and "path" options'); throw new Error('BiDi only supports "encoding" and "path" options');
} }
const {result} = await this.#context.connection.send( const {result} = await this.#connection.send(
'browsingContext.captureScreenshot', 'browsingContext.captureScreenshot',
{ {
context: this.#context._contextId, context: this.context().id,
} }
); );

View File

@ -31,7 +31,7 @@ class UnserializableError extends Error {}
* @internal * @internal
*/ */
export class BidiSerializer { export class BidiSerializer {
static serializeNumber(arg: number): Bidi.CommonDataTypes.LocalOrRemoteValue { static serializeNumber(arg: number): Bidi.CommonDataTypes.LocalValue {
let value: Bidi.CommonDataTypes.SpecialNumber | number; let value: Bidi.CommonDataTypes.SpecialNumber | number;
if (Object.is(arg, -0)) { if (Object.is(arg, -0)) {
value = '-0'; value = '-0';
@ -50,9 +50,7 @@ export class BidiSerializer {
}; };
} }
static serializeObject( static serializeObject(arg: object | null): Bidi.CommonDataTypes.LocalValue {
arg: object | null
): Bidi.CommonDataTypes.LocalOrRemoteValue {
if (arg === null) { if (arg === null) {
return { return {
type: 'null', type: 'null',
@ -111,9 +109,7 @@ export class BidiSerializer {
); );
} }
static serializeRemoveValue( static serializeRemoveValue(arg: unknown): Bidi.CommonDataTypes.LocalValue {
arg: unknown
): Bidi.CommonDataTypes.LocalOrRemoteValue {
switch (typeof arg) { switch (typeof arg) {
case 'symbol': case 'symbol':
case 'function': case 'function':
@ -148,7 +144,7 @@ export class BidiSerializer {
static serialize( static serialize(
arg: unknown, arg: unknown,
context: Context context: Context
): Bidi.CommonDataTypes.LocalOrRemoteValue { ): Bidi.CommonDataTypes.LocalValue | Bidi.CommonDataTypes.RemoteValue {
// TODO: See use case of LazyArgs // TODO: See use case of LazyArgs
const objectHandle = const objectHandle =
arg && (arg instanceof JSHandle || arg instanceof ElementHandle) arg && (arg instanceof JSHandle || arg instanceof ElementHandle)

View File

@ -38,7 +38,7 @@ export async function releaseReference(
} }
await client.connection await client.connection
.send('script.disown', { .send('script.disown', {
target: {context: client._contextId}, target: {context: client.id},
handles: [remoteReference.handle], handles: [remoteReference.handle],
}) })
.catch((error: any) => { .catch((error: any) => {

View File

@ -38,7 +38,6 @@ export * from './ExecutionContext.js';
export * from './fetch.js'; export * from './fetch.js';
export * from './FileChooser.js'; export * from './FileChooser.js';
export * from './FirefoxTargetManager.js'; export * from './FirefoxTargetManager.js';
export * from './Frame.js';
export * from './FrameManager.js'; export * from './FrameManager.js';
export * from './FrameTree.js'; export * from './FrameTree.js';
export * from './Input.js'; export * from './Input.js';

View File

@ -71,6 +71,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[page.spec] Page Page.Events.Load *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[page.spec] Page Page.Events.PageError *", "testIdPattern": "[page.spec] Page Page.Events.PageError *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -143,12 +149,6 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should execute after cross-site navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["SKIP"]
},
{ {
"testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have correct execution contexts", "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have correct execution contexts",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -245,6 +245,48 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should not send attach/detach events for main frame",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should persist mainFrame on cross-process navigation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should report frame from-inside shadow DOM",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame Management should support url fragment",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame.evaluate allows readonly array to be an argument",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame.evaluateHandle should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[frame.spec] Frame specs Frame.page should retrieve the page from a frame",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[headful.spec] *", "testIdPattern": "[headful.spec] *",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2252,7 +2294,13 @@
{ {
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
"platforms": ["linux"], "platforms": ["linux"],
"parameters": ["cdp", "chrome", "headless", "webDriverBiDi"], "parameters": ["chrome", "headless"],
"expectations": ["PASS", "TIMEOUT"] "expectations": ["PASS", "TIMEOUT"]
},
{
"testIdPattern": "[navigation.spec] navigation \"after each\" hook in \"navigation\"",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "headless", "webDriverBiDi"],
"expectations": ["FAIL"]
} }
] ]

View File

@ -15,8 +15,8 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import {Frame} from 'puppeteer-core/internal/api/Frame.js';
import {CDPSession} from 'puppeteer-core/internal/common/Connection.js'; import {CDPSession} from 'puppeteer-core/internal/common/Connection.js';
import {Frame} from 'puppeteer-core/internal/common/Frame.js';
import { import {
getTestState, getTestState,

View File

@ -30,7 +30,7 @@ import {
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {attachFrame, detachFrame, waitEvent} from './utils.js'; import {attachFrame, detachFrame, isFavicon, waitEvent} from './utils.js';
describe('Page', function () { describe('Page', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
@ -135,7 +135,7 @@ describe('Page', function () {
const handler = sinon.spy(); const handler = sinon.spy();
const onResponse = (response: {url: () => string}) => { const onResponse = (response: {url: () => string}) => {
// Ignore default favicon requests. // Ignore default favicon requests.
if (!response.url().endsWith('favicon.ico')) { if (!isFavicon(response)) {
handler(); handler();
} }
}; };
@ -158,7 +158,7 @@ describe('Page', function () {
const handler = sinon.spy(); const handler = sinon.spy();
const onResponse = (response: {url: () => string}) => { const onResponse = (response: {url: () => string}) => {
// Ignore default favicon requests. // Ignore default favicon requests.
if (!response.url().endsWith('favicon.ico')) { if (!isFavicon(response)) {
handler(); handler();
} }
}; };

View File

@ -17,9 +17,9 @@
import path from 'path'; import path from 'path';
import expect from 'expect'; import expect from 'expect';
import {Frame} from 'puppeteer-core/internal/api/Frame.js';
import {Page} from 'puppeteer-core/internal/api/Page.js'; import {Page} from 'puppeteer-core/internal/api/Page.js';
import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js';
import {Frame} from 'puppeteer-core/internal/common/Frame.js';
import {compare} from './golden-utils.js'; import {compare} from './golden-utils.js';

View File

@ -119,6 +119,7 @@ async function main() {
? { ? {
DEBUG: 'puppeteer:*', DEBUG: 'puppeteer:*',
EXTRA_LAUNCH_OPTIONS: JSON.stringify({ EXTRA_LAUNCH_OPTIONS: JSON.stringify({
dumpio: true,
extraPrefsFirefox: { extraPrefsFirefox: {
'remote.log.level': 'Trace', 'remote.log.level': 'Trace',
}, },