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:
os:
- ubuntu-latest
- macos-latest
# Disabled as BiDi has issue on mac https://bugzilla.mozilla.org/show_bug.cgi?id=1832778
# - macos-latest
suite:
- firefox-bidi
- firefox-headful

16
package-lock.json generated
View File

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

View File

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

View File

@ -18,7 +18,6 @@ import {Protocol} from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {Frame} from '../common/Frame.js';
import {MouseClickOptions} from '../common/Input.js';
import {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import {
@ -30,6 +29,7 @@ import {
} from '../common/types.js';
import {KeyInput} from '../common/USKeyboardLayout.js';
import {Frame} from './Frame.js';
import {JSHandle} from './JSHandle.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 {CDPSession} from '../common/Connection.js';
import {Frame} from '../common/Frame.js';
import {Frame} from './Frame.js';
import {HTTPResponse} from './HTTPResponse.js';
/**

View File

@ -16,9 +16,9 @@
import Protocol from 'devtools-protocol';
import {Frame} from '../common/Frame.js';
import {SecurityDetails} from '../common/SecurityDetails.js';
import {Frame} from './Frame.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 {EventEmitter, Handler} from '../common/EventEmitter.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 {WaitForSelectorOptions} from '../common/IsolatedWorld.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 {BrowserContext} from './BrowserContext.js';
import type {ClickOptions, ElementHandle} from './ElementHandle.js';
import type {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import type {JSHandle} from './JSHandle.js';
import {Locator} from './Locator.js';

View File

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

View File

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

View File

@ -17,6 +17,12 @@
import {Protocol} from 'devtools-protocol';
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 {Page, WaitTimeoutOptions} from '../api/Page.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';
/**
* @public
* @internal
*/
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 {
export class Frame extends BaseFrame {
#url = '';
#detached = false;
#client!: CDPSession;
/**
* @internal
*/
worlds!: IsolatedWorldChart;
/**
* @internal
*/
override worlds!: IsolatedWorldChart;
_frameManager: FrameManager;
/**
* @internal
*/
_id: string;
/**
* @internal
*/
override _id: string;
_loaderId = '';
/**
* @internal
*/
_name?: string;
/**
* @internal
*/
_hasStartedLoading = false;
/**
* @internal
*/
override _name?: string;
override _hasStartedLoading = false;
_lifecycleEvents = new Set<string>();
/**
* @internal
*/
_parentId?: string;
override _parentId?: string;
/**
* @internal
*/
constructor(
frameManager: FrameManager,
frameId: string,
parentFrameId: string | undefined,
client: CDPSession
) {
super();
this._frameManager = frameManager;
this.#url = '';
this._id = frameId;
@ -232,9 +82,6 @@ export class Frame {
this.updateClient(client);
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#client = client;
this.worlds = {
@ -243,59 +90,15 @@ export class Frame {
};
}
/**
* The page associated with the frame.
*/
page(): Page {
override page(): Page {
return this._frameManager.page();
}
/**
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
* `false`.
*/
isOOPFrame(): boolean {
override isOOPFrame(): boolean {
return this.#client !== this._frameManager.client;
}
/**
* 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(
override async goto(
url: string,
options: {
referer?: string;
@ -378,30 +181,7 @@ export class Frame {
}
}
/**
* 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(
override async waitForNavigation(
options: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
@ -432,27 +212,15 @@ export class Frame {
}
}
/**
* @internal
*/
_client(): CDPSession {
override _client(): CDPSession {
return this.#client;
}
/**
* @internal
*/
executionContext(): Promise<ExecutionContext> {
override executionContext(): Promise<ExecutionContext> {
return this.worlds[MAIN_WORLD].executionContext();
}
/**
* Behaves identically to {@link Page.evaluateHandle} except it's run within
* the context of this frame.
*
* @see {@link Page.evaluateHandle} for details.
*/
async evaluateHandle<
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
@ -466,13 +234,7 @@ export class Frame {
return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args);
}
/**
* 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<
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
@ -486,53 +248,19 @@ export class Frame {
return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args);
}
/**
* 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>(
override async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
return this.worlds[MAIN_WORLD].$(selector);
}
/**
* 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>(
override async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
return this.worlds[MAIN_WORLD].$$(selector);
}
/**
* 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<
override async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
@ -548,27 +276,7 @@ export class Frame {
return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args);
}
/**
* 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<
override async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
@ -584,56 +292,11 @@ export class Frame {
return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args);
}
/**
* @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>>> {
override async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.worlds[MAIN_WORLD].$x(expression);
}
/**
* 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>(
override async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
@ -646,29 +309,7 @@ export class Frame {
)) as ElementHandle<NodeFor<Selector>> | null;
}
/**
* @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(
override async waitForXPath(
xpath: string,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<Node> | null> {
@ -678,40 +319,7 @@ export class Frame {
return this.waitForSelector(`xpath/${xpath}`, options);
}
/**
* @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<
override waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
@ -726,21 +334,11 @@ export class Frame {
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>;
}
/**
* The full HTML contents of the frame, including the DOCTYPE.
*/
async content(): Promise<string> {
override async content(): Promise<string> {
return this.worlds[PUPPETEER_WORLD].content();
}
/**
* 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(
override async setContent(
html: string,
options: {
timeout?: number;
@ -750,56 +348,27 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].setContent(html, options);
}
/**
* 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 {
override name(): string {
return this._name || '';
}
/**
* The frame's URL.
*/
url(): string {
override url(): string {
return this.#url;
}
/**
* The parent frame, if any. Detached and main frames return `null`.
*/
parentFrame(): Frame | null {
override parentFrame(): Frame | null {
return this._frameManager._frameTree.parentFrame(this._id) || null;
}
/**
* An array of child frames.
*/
childFrames(): Frame[] {
override childFrames(): Frame[] {
return this._frameManager._frameTree.childFrames(this._id);
}
/**
* Is`true` if the frame has been detached. Otherwise, `false`.
*/
isDetached(): boolean {
override isDetached(): boolean {
return this.#detached;
}
/**
* 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(
override async addScriptTag(
options: FrameAddScriptTagOptions
): Promise<ElementHandle<HTMLScriptElement>> {
let {content = '', type} = options;
@ -861,20 +430,13 @@ export class Frame {
);
}
/**
* 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(
override async addStyleTag(
options: Omit<FrameAddStyleTagOptions, 'url'>
): Promise<ElementHandle<HTMLStyleElement>>;
async addStyleTag(
override async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLLinkElement>>;
async addStyleTag(
override async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
let {content = ''} = options;
@ -937,106 +499,30 @@ export class Frame {
);
}
/**
* 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(
override async click(
selector: string,
options: Readonly<ClickOptions> = {}
): Promise<void> {
return this.worlds[PUPPETEER_WORLD].click(selector, options);
}
/**
* 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> {
override async focus(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].focus(selector);
}
/**
* 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> {
override async hover(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].hover(selector);
}
/**
* 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[]> {
override select(selector: string, ...values: string[]): Promise<string[]> {
return this.worlds[PUPPETEER_WORLD].select(selector, ...values);
}
/**
* 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> {
override async tap(selector: string): Promise<void> {
return this.worlds[PUPPETEER_WORLD].tap(selector);
}
/**
* 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(
override async type(
selector: string,
text: string,
options?: {delay: number}
@ -1044,42 +530,16 @@ export class Frame {
return this.worlds[PUPPETEER_WORLD].type(selector, text, options);
}
/**
* @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> {
override waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
/**
* The frame's title.
*/
async title(): Promise<string> {
override async title(): Promise<string> {
return this.worlds[PUPPETEER_WORLD].title();
}
/**
* @internal
*/
_deviceRequestPromptManager(): DeviceRequestPromptManager {
if (this.isOOPFrame()) {
return this._frameManager._deviceRequestPromptManager(this.#client);
@ -1089,53 +549,21 @@ export class Frame {
return parentFrame._deviceRequestPromptManager();
}
/**
* 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(
override waitForDevicePrompt(
options: WaitTimeoutOptions = {}
): Promise<DeviceRequestPrompt> {
return this._deviceRequestPromptManager().waitForDevicePrompt(options);
}
/**
* @internal
*/
_navigated(framePayload: Protocol.Page.Frame): void {
this._name = framePayload.name;
this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
}
/**
* @internal
*/
_navigatedWithinDocument(url: string): void {
this.#url = url;
}
/**
* @internal
*/
_onLifecycleEvent(loaderId: string, name: string): void {
if (name === 'init') {
this._loaderId = loaderId;
@ -1144,24 +572,15 @@ export class Frame {
this._lifecycleEvents.add(name);
}
/**
* @internal
*/
_onLoadingStopped(): void {
this._lifecycleEvents.add('DOMContentLoaded');
this._lifecycleEvents.add('load');
}
/**
* @internal
*/
_onLoadingStarted(): void {
this._hasStartedLoading = true;
}
/**
* @internal
*/
_detach(): void {
this.#detached = true;
this.worlds[MAIN_WORLD]._detach();

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
*/
import {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import {
HTTPResponse as BaseHTTPResponse,
RemoteAddress,
@ -23,7 +24,6 @@ import {createDeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js';
import {ProtocolError} from './Errors.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.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 {BrowserContext} from '../api/BrowserContext.js';
import {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from '../api/Frame.js';
import {HTTPRequest} from '../api/HTTPRequest.js';
import {HTTPResponse} from '../api/HTTPResponse.js';
import {JSHandle} from '../api/JSHandle.js';
@ -55,12 +61,6 @@ import {DeviceRequestPrompt} from './DeviceRequestPrompt.js';
import {Dialog} from './Dialog.js';
import {EmulationManager} from './EmulationManager.js';
import {FileChooser} from './FileChooser.js';
import {
Frame,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {Keyboard, Mouse, Touchscreen} from './Input.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
@ -895,21 +895,21 @@ export class CDPPage extends Page {
}
override async content(): Promise<string> {
return await this.#frameManager.mainFrame().content();
return await this.mainFrame().content();
}
override async setContent(
html: string,
options: WaitForOptions = {}
): Promise<void> {
await this.#frameManager.mainFrame().setContent(html, options);
await this.mainFrame().setContent(html, options);
}
override async goto(
url: string,
options: WaitForOptions & {referer?: string; referrerPolicy?: string} = {}
): Promise<HTTPResponse | null> {
return await this.#frameManager.mainFrame().goto(url, options);
return await this.mainFrame().goto(url, options);
}
override async reload(
@ -926,7 +926,7 @@ export class CDPPage extends Page {
override async waitForNavigation(
options: WaitForOptions = {}
): Promise<HTTPResponse | null> {
return await this.#frameManager.mainFrame().waitForNavigation(options);
return await this.mainFrame().waitForNavigation(options);
}
#sessionClosePromise(): Promise<Error> {
@ -1163,7 +1163,7 @@ export class CDPPage extends Page {
'Throttling rate should be greater or equal to 1'
);
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,
pageFunction
);
return this.#frameManager.mainFrame().evaluate(pageFunction, ...args);
return this.mainFrame().evaluate(pageFunction, ...args);
}
override async evaluateOnNewDocument<

View File

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

View File

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

View File

@ -14,13 +14,15 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Page as PageBase} from '../../api/Page.js';
import {Viewport} from '../PuppeteerViewport.js';
import {Connection} from './Connection.js';
import {Context} from './Context.js';
import {Page} from './Page.js';
import {debugError} from './utils.js';
interface BrowserContextOptions {
defaultViewport: Viewport | null;
@ -32,19 +34,34 @@ interface BrowserContextOptions {
export class BrowserContext extends BrowserContextBase {
#connection: Connection;
#defaultViewport: Viewport | null;
#pages = new Map<string, Page>();
#onContextDestroyedBind = this.#onContextDestroyed.bind(this);
constructor(connection: Connection, options: BrowserContextOptions) {
super();
this.#connection = connection;
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> {
const {result} = await this.#connection.send('browsingContext.create', {
type: 'tab',
});
const context = this.#connection.context(result.context) as Context;
const page = new Page(context);
const page = await Page._create(this.#connection, result);
if (this.#defaultViewport) {
try {
await page.setViewport(this.#defaultViewport);
@ -52,8 +69,18 @@ export class BrowserContext extends BrowserContextBase {
// No support for setViewport in Firefox.
}
}
this.#pages.set(result.context, 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': {
params: Bidi.BrowsingContext.CloseParameters;
returnType: Bidi.BrowsingContext.CloseResult;
returnType: Bidi.Message.EmptyResult;
};
'browsingContext.getTree': {
params: Bidi.BrowsingContext.GetTreeParameters;
returnType: Bidi.BrowsingContext.GetTreeResult;
};
'browsingContext.navigate': {
params: Bidi.BrowsingContext.NavigateParameters;
@ -73,11 +77,11 @@ interface Commands {
returnType: Bidi.Session.StatusResult;
};
'session.subscribe': {
params: Bidi.Session.SubscribeParameters;
params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.Session.SubscribeResult;
};
'session.unsubscribe': {
params: Bidi.Session.SubscribeParameters;
params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.Session.UnsubscribeResult;
};
'cdp.sendCommand': {
@ -159,7 +163,6 @@ export class Connection extends EventEmitter {
this.#callbacks.resolve(object.id, object);
}
} else {
this.#handleSpecialEvents(object);
this.#maybeEmitOnContext(object);
this.emit(object.method, object.params);
}
@ -177,14 +180,12 @@ export class Connection extends EventEmitter {
context?.emit(event.method, event.params);
}
#handleSpecialEvents(event: Bidi.Message.EventMessage) {
switch (event.method) {
case 'browsingContext.contextCreated':
this.#contexts.set(
event.params.context,
new Context(this, event.params)
);
registerContext(context: Context): void {
this.#contexts.set(context.id, context);
}
unregisterContext(context: Context): void {
this.#contexts.delete(context.id);
}
#onClose(): void {

View File

@ -24,7 +24,6 @@ import {ProtocolMapping} from '../Connection.js';
import {ProtocolError, TimeoutError} from '../Errors.js';
import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {
getSourcePuppeteerURLIfAvailable,
@ -36,6 +35,7 @@ import {
import {Connection} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
import {FrameManager} from './FrameManager.js';
import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js';
@ -71,14 +71,28 @@ const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([
export class Context extends EventEmitter {
#connection: Connection;
#url: string;
_contextId: string;
_timeoutSettings = new TimeoutSettings();
#id: string;
#parentId?: string | null;
_frameManager: FrameManager;
constructor(connection: Connection, result: Bidi.BrowsingContext.Info) {
constructor(
connection: Connection,
frameManager: FrameManager,
result: Bidi.BrowsingContext.Info
) {
super();
this.#connection = connection;
this._contextId = result.context;
this._frameManager = frameManager;
this.#id = result.context;
this.#parentId = result.parent;
this.#url = result.url;
this.on(
'browsingContext.fragmentNavigated',
(info: Bidi.BrowsingContext.NavigationInfo) => {
this.#url = info.url;
}
);
}
get connection(): Connection {
@ -86,7 +100,11 @@ export class Context extends EventEmitter {
}
get id(): string {
return this._contextId;
return this.#id;
}
get parentId(): string | undefined | null {
return this.#parentId;
}
async evaluateHandle<
@ -146,8 +164,8 @@ export class Context extends EventEmitter {
: `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.#connection.send('script.evaluate', {
expression: expression,
target: {context: this._contextId},
expression,
target: {context: this.#id},
resultOwnership,
awaitPromise: true,
});
@ -163,7 +181,7 @@ export class Context extends EventEmitter {
return BidiSerializer.serialize(arg, this);
})
),
target: {context: this._contextId},
target: {context: this.#id},
resultOwnership,
awaitPromise: true,
});
@ -189,7 +207,7 @@ export class Context extends EventEmitter {
): Promise<HTTPResponse | null> {
const {
waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this._frameManager._timeoutSettings.navigationTimeout(),
} = options;
const readinessState = lifeCycleToReadinessState.get(
@ -229,7 +247,7 @@ export class Context extends EventEmitter {
): Promise<void> {
const {
waitUntil = 'load',
timeout = this._timeoutSettings.navigationTimeout(),
timeout = this._frameManager._timeoutSettings.navigationTimeout(),
} = options;
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(
method: keyof ProtocolMapping.Commands,
params: object = {}
): Promise<unknown> {
const session = await this.#connection.send('cdp.getSession', {
context: this._contextId,
context: this.id,
});
// TODO: remove any once chromium-bidi types are updated.
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';
import {isErrorLike} from '../../util/ErrorLike.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 {Viewport} from '../PuppeteerViewport.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {
debugError,
@ -37,31 +39,77 @@ import {
withSourcePuppeteerURLIfNone,
} from '../util.js';
import {Connection} from './Connection.js';
import {Context, getBidiHandle} from './Context.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {BidiSerializer} from './Serializer.js';
/**
* @internal
*/
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>>([
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
['browsingContext.load', this.#onLoad.bind(this)],
['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)],
]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>;
#viewport: Viewport | null = null;
]) as Map<Bidi.Session.SubscriptionRequestEvent, Handler>;
#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();
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', {
events: [
...this.#subscribedEvents.keys(),
] as Bidi.Session.SubscribeParameters['events'],
contexts: [this.#context.id],
events: [...page.#subscribedEvents.keys()],
contexts: [info.context],
})
.catch(error => {
if (isErrorLike(error) && !error.message.includes('Target closed')) {
@ -69,15 +117,25 @@ export class Page extends PageBase {
}
});
for (const [event, subscriber] of this.#subscribedEvents) {
this.#context.on(event, subscriber);
return page;
}
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 {
if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => {
return getBidiHandle(this.#context, arg);
return getBidiHandle(this.context(), arg);
});
const text = args
@ -134,18 +192,30 @@ export class Page extends PageBase {
}
override async close(): Promise<void> {
await this.#context.connection.send('session.unsubscribe', {
events: [...this.#subscribedEvents.keys()],
contexts: [this.#context.id],
});
await this.#context.connection.send('browsingContext.close', {
context: this.#context.id,
});
if (this.#closed) {
return;
}
this.#closed = true;
this.removeAllListeners();
this.#frameManager.dispose();
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<
@ -159,7 +229,7 @@ export class Page extends PageBase {
this.evaluateHandle.name,
pageFunction
);
return this.#context.evaluateHandle(pageFunction, ...args);
return this.mainFrame().evaluateHandle(pageFunction, ...args);
}
override async evaluate<
@ -173,7 +243,7 @@ export class Page extends PageBase {
this.evaluate.name,
pageFunction
);
return this.#context.evaluate(pageFunction, ...args);
return this.mainFrame().evaluate(pageFunction, ...args);
}
override async goto(
@ -183,26 +253,26 @@ export class Page extends PageBase {
referrerPolicy?: string | undefined;
}
): Promise<HTTPResponse | null> {
return this.#context.goto(url, options);
return this.mainFrame().goto(url, options);
}
override url(): string {
return this.#context.url();
return this.mainFrame().url();
}
override setDefaultNavigationTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
override setDefaultTimeout(timeout: number): void {
this.#context._timeoutSettings.setDefaultTimeout(timeout);
this._timeoutSettings.setDefaultTimeout(timeout);
}
override async setContent(
html: string,
options: WaitForOptions = {}
): Promise<void> {
await this.#context.setContent(html, options);
await this.mainFrame().setContent(html, options);
}
override async content(): Promise<string> {
@ -226,7 +296,7 @@ export class Page extends PageBase {
const deviceScaleFactor = 1;
const screenOrientation = {angle: 0, type: 'portraitPrimary'};
await this.#context.sendCDPCommand('Emulation.setDeviceMetricsOverride', {
await this.context().sendCDPCommand('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
@ -255,8 +325,8 @@ export class Page extends PageBase {
timeout,
} = this._getPDFOptions(options, 'cm');
const {result} = await waitWithTimeout(
this.#context.connection.send('browsingContext.print', {
context: this.#context._contextId,
this.#connection.send('browsingContext.print', {
context: this.context().id,
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
@ -310,10 +380,10 @@ export class Page extends PageBase {
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',
{
context: this.#context._contextId,
context: this.context().id,
}
);

View File

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

View File

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

View File

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

View File

@ -71,6 +71,12 @@
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.Load *",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[page.spec] Page Page.Events.PageError *",
"platforms": ["darwin", "linux", "win32"],
@ -143,12 +149,6 @@
"parameters": ["webDriverBiDi"],
"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",
"platforms": ["darwin", "linux", "win32"],
@ -245,6 +245,48 @@
"parameters": ["webDriverBiDi"],
"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] *",
"platforms": ["darwin", "linux", "win32"],
@ -2252,7 +2294,13 @@
{
"testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation",
"platforms": ["linux"],
"parameters": ["cdp", "chrome", "headless", "webDriverBiDi"],
"parameters": ["chrome", "headless"],
"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 {Frame} from 'puppeteer-core/internal/api/Frame.js';
import {CDPSession} from 'puppeteer-core/internal/common/Connection.js';
import {Frame} from 'puppeteer-core/internal/common/Frame.js';
import {
getTestState,

View File

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

View File

@ -17,9 +17,9 @@
import path from 'path';
import expect from 'expect';
import {Frame} from 'puppeteer-core/internal/api/Frame.js';
import {Page} from 'puppeteer-core/internal/api/Page.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';

View File

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