mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
Bug 1907533 - [puppeteer] Sync vendored puppeteer to v22.13.0 r=webdriver-reviewers,Sasha
Differential Revision: https://phabricator.services.mozilla.com/D216413
This commit is contained in:
parent
6133380bc0
commit
43cdd1333e
@ -53,4 +53,6 @@ packages/ng-schematics/src/**/files/
|
||||
|
||||
# examples
|
||||
examples/puppeteer-in-browser/out/**/*
|
||||
examples/puppeteer-in-browser/node_modules/**/*
|
||||
examples/puppeteer-in-browser/node_modules/**/*
|
||||
examples/puppeteer-in-extension/out/**/*
|
||||
examples/puppeteer-in-extension/node_modules/**/*
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"packages/puppeteer": "22.9.0",
|
||||
"packages/puppeteer-core": "22.9.0",
|
||||
"packages/testserver": "0.6.0",
|
||||
"packages/ng-schematics": "0.6.0",
|
||||
"packages/puppeteer": "22.13.0",
|
||||
"packages/puppeteer-core": "22.13.0",
|
||||
"packages/testserver": "0.6.1",
|
||||
"packages/ng-schematics": "0.6.1",
|
||||
"packages/browsers": "2.2.3"
|
||||
}
|
||||
|
@ -6,48 +6,50 @@
|
||||
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/>
|
||||
|
||||
> Puppeteer is a Node.js library which provides a high-level API to control
|
||||
> Chrome/Chromium over the
|
||||
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
|
||||
> Puppeteer runs in
|
||||
> [headless](https://developer.chrome.com/docs/chromium/new-headless/)
|
||||
> mode by default, but can be configured to run in full ("headful")
|
||||
> Chrome/Chromium.
|
||||
> Chrome or Firefox over the
|
||||
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [WebDriver BiDi](https://pptr.dev/webdriver-bidi).
|
||||
> Puppeteer runs in the headless (no visible UI) by default
|
||||
> but can be configured to run in a visible ("headful") browser.
|
||||
|
||||
## [Get started](https://pptr.dev/docs) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash npm2yarn
|
||||
npm i puppeteer # Downloads compatible Chrome during installation.
|
||||
npm i puppeteer-core # Alternatively, install as a library, without downloading Chrome.
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import puppeteer from 'puppeteer';
|
||||
// Or import puppeteer from 'puppeteer-core';
|
||||
|
||||
(async () => {
|
||||
// Launch the browser and open a new blank page
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
// Launch the browser and open a new blank page
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Navigate the page to a URL
|
||||
await page.goto('https://developer.chrome.com/');
|
||||
// Navigate the page to a URL.
|
||||
await page.goto('https://developer.chrome.com/');
|
||||
|
||||
// Set screen size
|
||||
await page.setViewport({width: 1080, height: 1024});
|
||||
// Set screen size.
|
||||
await page.setViewport({width: 1080, height: 1024});
|
||||
|
||||
// Type into search box
|
||||
await page.type('.devsite-search-field', 'automate beyond recorder');
|
||||
// Type into search box.
|
||||
await page.locator('.devsite-search-field').fill('automate beyond recorder');
|
||||
|
||||
// Wait and click on first result
|
||||
const searchResultSelector = '.devsite-result-item-link';
|
||||
await page.waitForSelector(searchResultSelector);
|
||||
await page.click(searchResultSelector);
|
||||
// Wait and click on first result.
|
||||
await page.locator('.devsite-result-item-link').click();
|
||||
|
||||
// Locate the full title with a unique string
|
||||
const textSelector = await page.waitForSelector(
|
||||
'text/Customize and automate'
|
||||
);
|
||||
const fullTitle = await textSelector?.evaluate(el => el.textContent);
|
||||
// Locate the full title with a unique string.
|
||||
const textSelector = await page
|
||||
.locator('text/Customize and automate')
|
||||
.waitHandle();
|
||||
const fullTitle = await textSelector?.evaluate(el => el.textContent);
|
||||
|
||||
// Print the full title
|
||||
console.log('The title of this blog post is "%s".', fullTitle);
|
||||
// Print the full title.
|
||||
console.log('The title of this blog post is "%s".', fullTitle);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
await browser.close();
|
||||
```
|
||||
|
2
remote/test/puppeteer/examples/puppeteer-in-extension/.gitignore
vendored
Normal file
2
remote/test/puppeteer/examples/puppeteer-in-extension/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
out
|
||||
node_modules
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {
|
||||
connect,
|
||||
ExtensionTransport,
|
||||
} from 'puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js';
|
||||
|
||||
globalThis.testConnect = async url => {
|
||||
const tab = await chrome.tabs.create({
|
||||
url,
|
||||
});
|
||||
|
||||
// Wait for the new tab to load before connecting.
|
||||
await new Promise(resolve => {
|
||||
function listener(tabId, changeInfo) {
|
||||
if (tabId === tab.id && changeInfo.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
|
||||
const browser = await connect({
|
||||
transport: await ExtensionTransport.connectTab(tab.id),
|
||||
});
|
||||
const [page] = await browser.pages();
|
||||
const title = await page.evaluate(() => {
|
||||
return document.title;
|
||||
});
|
||||
await browser.disconnect();
|
||||
return title;
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Puppeteer in extension",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"permissions": ["debugger", "background"]
|
||||
}
|
461
remote/test/puppeteer/examples/puppeteer-in-extension/package-lock.json
generated
Normal file
461
remote/test/puppeteer/examples/puppeteer-in-extension/package-lock.json
generated
Normal file
@ -0,0 +1,461 @@
|
||||
{
|
||||
"name": "puppeteer-in-extension",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "puppeteer-in-extension",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"rollup": "^4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"@types/resolve": "1.20.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
|
||||
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
|
||||
"integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
|
||||
"integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
|
||||
"integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
|
||||
"integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
|
||||
"integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
|
||||
"integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
|
||||
"integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
|
||||
"integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
|
||||
"integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
|
||||
"integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
|
||||
"integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.14.3",
|
||||
"@rollup/rollup-android-arm64": "4.14.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.3",
|
||||
"@rollup/rollup-darwin-x64": "4.14.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.14.3",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.14.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.14.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.14.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.14.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "puppeteer-in-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c && cp manifest.json out/"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"rollup": "^4.14.3"
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<!doctype html> <title>Playground</title>
|
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {nodeResolve} from '@rollup/plugin-node-resolve';
|
||||
|
||||
export default {
|
||||
input: 'background.js',
|
||||
output: {
|
||||
format: 'esm',
|
||||
dir: 'out',
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
resolveOnly: ['puppeteer-core'],
|
||||
}),
|
||||
],
|
||||
};
|
@ -5,6 +5,6 @@ origin:
|
||||
description: Headless Chrome Node API
|
||||
license: Apache-2.0
|
||||
name: puppeteer
|
||||
release: 9939c891b3ab1bb25160913129b202a71cef86b3
|
||||
url: https://github.com/puppeteer/puppeteer.git
|
||||
release: puppeteer-v22.13.0
|
||||
url: /Users/juliandescottes/Development/git/puppeteer
|
||||
schema: 1
|
||||
|
3676
remote/test/puppeteer/package-lock.json
generated
3676
remote/test/puppeteer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,8 +8,7 @@
|
||||
"scripts": {
|
||||
"build": "wireit",
|
||||
"build:tools": "wireit",
|
||||
"check": "npm run check --workspaces --if-present && run-p check:*",
|
||||
"check:pinned-deps": "tsx tools/ensure-pinned-deps",
|
||||
"check": "npm run check --workspaces --if-present",
|
||||
"clean": "npm run clean --workspaces --if-present",
|
||||
"debug": "mocha --inspect-brk",
|
||||
"docs": "wireit",
|
||||
@ -138,13 +137,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "1.10.1",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/mocha": "10.0.7",
|
||||
"@types/node": "20.8.4",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.8.0",
|
||||
"@typescript-eslint/parser": "7.8.0",
|
||||
"esbuild": "0.21.2",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"esbuild": "0.23.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
@ -152,23 +151,23 @@
|
||||
"eslint-plugin-mocha": "10.4.3",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-rulesdir": "0.2.2",
|
||||
"eslint-plugin-tsdoc": "0.2.17",
|
||||
"eslint-plugin-tsdoc": "0.3.0",
|
||||
"eslint-plugin-unused-imports": "3.2.0",
|
||||
"execa": "9.0.2",
|
||||
"execa": "9.3.0",
|
||||
"expect": "29.7.0",
|
||||
"gts": "5.3.0",
|
||||
"gts": "5.3.1",
|
||||
"hereby": "1.8.9",
|
||||
"license-checker": "25.0.1",
|
||||
"mocha": "10.4.0",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"prettier": "3.2.5",
|
||||
"mocha": "10.6.0",
|
||||
"npm-run-all2": "6.2.2",
|
||||
"prettier": "3.3.2",
|
||||
"semver": "7.6.2",
|
||||
"sinon": "17.0.2",
|
||||
"sinon": "18.0.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"spdx-satisfies": "5.0.1",
|
||||
"tsd": "0.31.0",
|
||||
"tsx": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"tsd": "0.31.1",
|
||||
"tsx": "4.16.2",
|
||||
"typescript": "5.4.5",
|
||||
"wireit": "0.14.4"
|
||||
},
|
||||
"overrides": {
|
||||
|
@ -95,14 +95,14 @@
|
||||
"!*.tsbuildinfo"
|
||||
],
|
||||
"dependencies": {
|
||||
"debug": "4.3.4",
|
||||
"extract-zip": "2.0.1",
|
||||
"progress": "2.0.3",
|
||||
"proxy-agent": "6.4.0",
|
||||
"tar-fs": "3.0.6",
|
||||
"unbzip2-stream": "1.4.3",
|
||||
"yargs": "17.7.2",
|
||||
"semver": "7.6.2"
|
||||
"debug": "^4.3.5",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.4.0",
|
||||
"tar-fs": "^3.0.6",
|
||||
"unbzip2-stream": "^1.4.3",
|
||||
"yargs": "^17.7.2",
|
||||
"semver": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "4.1.12",
|
||||
|
@ -6,6 +6,6 @@
|
||||
|
||||
export const testChromeBuildId = '121.0.6167.85';
|
||||
export const testChromiumBuildId = '1083080';
|
||||
export const testFirefoxBuildId = '127.0a1';
|
||||
export const testFirefoxBuildId = '129.0a1';
|
||||
export const testChromeDriverBuildId = '121.0.6167.85';
|
||||
export const testChromeHeadlessShellBuildId = '121.0.6167.85';
|
||||
|
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [0.6.1](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.6.0...ng-schematics-v0.6.1) (2024-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use NodeNext in ng-schematics default tsconfig ([#12622](https://github.com/puppeteer/puppeteer/issues/12622)) ([8d40b27](https://github.com/puppeteer/puppeteer/commit/8d40b2748347db11ee119c6fd5aa56f72824450b))
|
||||
|
||||
## [0.6.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.6...ng-schematics-v0.6.0) (2024-02-05)
|
||||
|
||||
|
||||
|
@ -46,7 +46,7 @@ ng generate @puppeteer/ng-schematics:e2e "<TestName>"
|
||||
### Running test server and dev server at the same time
|
||||
|
||||
By default the E2E test will run the app on the same port as `ng start`.
|
||||
To avoid this you can specify the port the an the `angular.json`
|
||||
To avoid this you can specify the port in the `angular.json`
|
||||
Update either `e2e` or `puppeteer` (depending on the initial setup) to:
|
||||
|
||||
```json
|
||||
@ -75,7 +75,7 @@ Check out our [contributing guide](https://pptr.dev/contributing) to get an over
|
||||
|
||||
### Sandbox smoke tests
|
||||
|
||||
To make integration easier smoke test can be run with a single command, that will create a fresh install of Angular (single application and a milti application projects). Then it will install the schematics inside them and run the initial e2e tests:
|
||||
To make integration easier smoke test can be run with a single command, that will create a fresh install of Angular (single application and a multi application projects). Then it will install the schematics inside them and run the initial e2e tests:
|
||||
|
||||
```bash
|
||||
node tools/smoke.mjs
|
||||
@ -94,7 +94,7 @@ npm run test
|
||||
### Entry point
|
||||
|
||||
Puppeteer has its own [`browser`](https://pptr.dev/api/puppeteer.browser) that exposes the browser process.
|
||||
A more closes comparison for Protractor's `browser` would be Puppeteer's [`page`](https://pptr.dev/api/puppeteer.page).
|
||||
A more close comparison for Protractor's `browser` would be Puppeteer's [`page`](https://pptr.dev/api/puppeteer.page).
|
||||
|
||||
```ts
|
||||
// Testing framework specific imports
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@puppeteer/ng-schematics",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Puppeteer Angular schematics",
|
||||
"scripts": {
|
||||
"build": "wireit",
|
||||
@ -51,13 +51,13 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.1702.2",
|
||||
"@angular-devkit/core": "17.2.2",
|
||||
"@angular-devkit/schematics": "17.2.2"
|
||||
"@angular-devkit/architect": "0.1800.4",
|
||||
"@angular-devkit/core": "18.0.4",
|
||||
"@angular-devkit/schematics": "18.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@schematics/angular": "17.2.2",
|
||||
"@angular/cli": "17.2.2"
|
||||
"@schematics/angular": "18.0.4",
|
||||
"@angular/cli": "18.0.4"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
|
@ -89,7 +89,7 @@ async function executeCommand(
|
||||
command[0] = updateExecutablePath(command[0]!, String(project['root']));
|
||||
}
|
||||
|
||||
await new Promise(async (resolve, reject) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
context.logger.debug(`Trying to execute command - ${command.join(' ')}.`);
|
||||
const {executable, args, debugError, error} = getExecutable(command);
|
||||
let path = context.workspaceRoot;
|
||||
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": "<%= tsConfigPath %>",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "tests/",
|
||||
"outDir": "build/",
|
||||
"types": ["<%= testRunner %>"]
|
||||
|
@ -109,7 +109,8 @@ void describe('@puppeteer/ng-schematics: ng-add', () => {
|
||||
expect(tsConfig).toMatchObject({
|
||||
extends: '../tsconfig.json',
|
||||
compilerOptions: {
|
||||
module: 'CommonJS',
|
||||
module: 'NodeNext',
|
||||
moduleResolution: 'NodeNext',
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -222,7 +223,8 @@ void describe('@puppeteer/ng-schematics: ng-add', () => {
|
||||
expect(tsConfig).toMatchObject({
|
||||
extends: '../../../tsconfig.json',
|
||||
compilerOptions: {
|
||||
module: 'CommonJS',
|
||||
module: 'NodeNext',
|
||||
moduleResolution: 'NodeNext',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -50,6 +50,8 @@ class AngularProject {
|
||||
/** E2E test runner to use */
|
||||
#runner;
|
||||
|
||||
type = '';
|
||||
|
||||
constructor(runner, name) {
|
||||
this.#runner = runner ?? 'node';
|
||||
this.#name = name ?? randomUUID();
|
||||
@ -75,8 +77,11 @@ class AngularProject {
|
||||
data = data
|
||||
.toString()
|
||||
// Replace new lines with a prefix including the test runner
|
||||
.replace(/(?:\r\n?|\n)(?=.*[\r\n])/g, `\n${this.#runner} - `);
|
||||
console.log(`${this.#runner} - ${data}`);
|
||||
.replace(
|
||||
/(?:\r\n?|\n)(?=.*[\r\n])/g,
|
||||
`\n${this.#runner}:${this.type} - `
|
||||
);
|
||||
console.log(`${this.#runner}:${this.type} - ${data}`);
|
||||
});
|
||||
|
||||
createProcess.on('error', message => {
|
||||
@ -140,6 +145,8 @@ class AngularProject {
|
||||
}
|
||||
|
||||
export class AngularProjectSingle extends AngularProject {
|
||||
type = 'single';
|
||||
|
||||
async createProject() {
|
||||
await this.executeCommand(
|
||||
`ng new ${this.name} --directory=sandbox/${this.name} --defaults --skip-git`
|
||||
@ -148,6 +155,8 @@ export class AngularProjectSingle extends AngularProject {
|
||||
}
|
||||
|
||||
export class AngularProjectMulti extends AngularProject {
|
||||
type = 'multi';
|
||||
|
||||
async createProject() {
|
||||
await this.executeCommand(
|
||||
`ng new ${this.name} --create-application=false --directory=sandbox/${this.name} --defaults --skip-git`
|
||||
|
@ -12,9 +12,9 @@ import {AngularProjectMulti, AngularProjectSingle} from './projects.mjs';
|
||||
|
||||
const {values: args} = parseArgs({
|
||||
options: {
|
||||
testRunner: {
|
||||
runner: {
|
||||
type: 'string',
|
||||
short: 't',
|
||||
short: 'r',
|
||||
default: undefined,
|
||||
},
|
||||
name: {
|
||||
@ -25,9 +25,30 @@ const {values: args} = parseArgs({
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.CI) {
|
||||
// Need to install in CI
|
||||
execSync('npm install -g @angular/cli@latest @angular-devkit/schematics-cli');
|
||||
function verifyAngularCliInstalled() {
|
||||
if (process.env.CI) {
|
||||
// Need to install in CI
|
||||
execSync(
|
||||
'npm install -g @angular/cli@latest @angular-devkit/schematics-cli'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const userDeps = execSync('npm list -g --depth=0');
|
||||
|
||||
if (
|
||||
!userDeps.includes('@angular/cli') ||
|
||||
!userDeps.includes('@angular-devkit/schematics-cli')
|
||||
) {
|
||||
console.error(
|
||||
'Angular CLI not installed run `npm install -g @angular/cli @angular-devkit/schematics-cli`'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
verifyAngularCliInstalled();
|
||||
|
||||
if (!args.runner) {
|
||||
const runners = ['node', 'jest', 'jasmine', 'mocha'];
|
||||
const groups = [];
|
||||
|
||||
@ -70,3 +91,8 @@ if (process.env.CI) {
|
||||
await Promise.all([single.create(), multi.create()]);
|
||||
await Promise.all([single.runSmoke(), multi.runSmoke()]);
|
||||
}
|
||||
|
||||
console.log(`
|
||||
<---------------->
|
||||
Smoke test passed!
|
||||
`);
|
||||
|
@ -20,6 +20,112 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
* dependencies
|
||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
||||
|
||||
## [22.13.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.12.1...puppeteer-core-v22.13.0) (2024-07-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **webdriver:** implement page.setCacheEnabled ([#12691](https://github.com/puppeteer/puppeteer/issues/12691)) ([e44d900](https://github.com/puppeteer/puppeteer/commit/e44d900c0cb7c725f88a477375f7b9658ef92eb8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add an option to not wait for fonts when pdf printing ([#12675](https://github.com/puppeteer/puppeteer/issues/12675)) ([a573dbd](https://github.com/puppeteer/puppeteer/commit/a573dbd7ed858651b92dc5deafe2ebdbe86b5f4c))
|
||||
* add browser entrypoint to package.json of puppeteer-core ([#12729](https://github.com/puppeteer/puppeteer/issues/12729)) ([669c86b](https://github.com/puppeteer/puppeteer/commit/669c86b203e7ad18e7be3d6fc847872c48d05617))
|
||||
* **cli:** puppeteer CLI should read the project configuration ([#12730](https://github.com/puppeteer/puppeteer/issues/12730)) ([bca750a](https://github.com/puppeteer/puppeteer/commit/bca750afe204cc3bafb0a34a0f92b0bac5a6a55f))
|
||||
* correct validation of the quality parameter in page.screenshot ([#12725](https://github.com/puppeteer/puppeteer/issues/12725)) ([2f8abd7](https://github.com/puppeteer/puppeteer/commit/2f8abd7a6c9be7f3ee5123e55da76c51ea132c58))
|
||||
* do not allow switching tabs while the screenshot operation is in progress ([#12724](https://github.com/puppeteer/puppeteer/issues/12724)) ([a3345f6](https://github.com/puppeteer/puppeteer/commit/a3345f6686c7634904fbd72df12588f3e230878f))
|
||||
* don't rely on Buffer to be present ([#12702](https://github.com/puppeteer/puppeteer/issues/12702)) ([3c02cef](https://github.com/puppeteer/puppeteer/commit/3c02ceffa366f747c84fa38af058c8b2dab7e3c5))
|
||||
* ensure existing targets are attached to pages ([#12677](https://github.com/puppeteer/puppeteer/issues/12677)) ([d1d8489](https://github.com/puppeteer/puppeteer/commit/d1d8489a9616375f5195ea226b7123345402030b))
|
||||
* make sure bindings are working after a page is restored from bfcache ([#12663](https://github.com/puppeteer/puppeteer/issues/12663)) ([570b1a8](https://github.com/puppeteer/puppeteer/commit/570b1a862eed1ce86dba318e143d7d4191a89c3b))
|
||||
* support evaluateOnNewDocument for out-of-process frames ([#12714](https://github.com/puppeteer/puppeteer/issues/12714)) ([eac7cda](https://github.com/puppeteer/puppeteer/commit/eac7cda537255eedb61e4ac689c1c919f892d491))
|
||||
* support out-of-process iframes in exposeFunction ([#12722](https://github.com/puppeteer/puppeteer/issues/12722)) ([b6b536b](https://github.com/puppeteer/puppeteer/commit/b6b536bb2f38b052b12a8902be348132c78a04f6))
|
||||
|
||||
## [22.12.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.12.0...puppeteer-core-v22.12.1) (2024-06-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* execution contexts might be created before previous is destroyed ([#12666](https://github.com/puppeteer/puppeteer/issues/12666)) ([db642d1](https://github.com/puppeteer/puppeteer/commit/db642d1d6975a9b12700a471f6cacc8daf6bd04d))
|
||||
* reset the viewport after taking a fullPage screenshot if defaultViewport is null ([#12650](https://github.com/puppeteer/puppeteer/issues/12650)) ([0a32283](https://github.com/puppeteer/puppeteer/commit/0a32283cfccba306fa20dc5b5c31487a6d8fb201))
|
||||
* roll to Chrome 126.0.6478.126 (r1300313) ([#12656](https://github.com/puppeteer/puppeteer/issues/12656)) ([32ed82c](https://github.com/puppeteer/puppeteer/commit/32ed82c623905755944b1cf2d9e0cd9d952c8f94))
|
||||
* use RAF-based polling for ARIA selectors ([#12664](https://github.com/puppeteer/puppeteer/issues/12664)) ([56d1d3f](https://github.com/puppeteer/puppeteer/commit/56d1d3f8b731d18c6aa9cc3d6de9c722b93a7a1e))
|
||||
|
||||
## [22.12.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.11.2...puppeteer-core-v22.12.0) (2024-06-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support AbortSignal in page.waitForRequest/Response/NetworkIdle/Frame ([#12621](https://github.com/puppeteer/puppeteer/issues/12621)) ([54ecea7](https://github.com/puppeteer/puppeteer/commit/54ecea7db5180ec024d81a7ac14c73387550d1d6))
|
||||
* **webdriver:** support for `PageEvent.Popup` ([#12612](https://github.com/puppeteer/puppeteer/issues/12612)) ([293926b](https://github.com/puppeteer/puppeteer/commit/293926b61a3552f9ec7e9a62383688e775f12df0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **performance:** clear targets on browser context close ([#12609](https://github.com/puppeteer/puppeteer/issues/12609)) ([6609758](https://github.com/puppeteer/puppeteer/commit/660975824ac94b85a260e99b95db0a11bb5a2e07))
|
||||
* roll to Chrome 126.0.6478.62 (r1300313) ([#12615](https://github.com/puppeteer/puppeteer/issues/12615)) ([80dd131](https://github.com/puppeteer/puppeteer/commit/80dd1316a09e87dda65f68e5cbe299d335147599))
|
||||
* roll to Chrome 126.0.6478.63 (r1300313) ([#12632](https://github.com/puppeteer/puppeteer/issues/12632)) ([20ed8fc](https://github.com/puppeteer/puppeteer/commit/20ed8fcb1415501525368305a9bc509af03d63ff))
|
||||
|
||||
## [22.11.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.11.1...puppeteer-core-v22.11.2) (2024-06-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** bump ws to 8.17.1 ([#12605](https://github.com/puppeteer/puppeteer/issues/12605)) ([49bcb25](https://github.com/puppeteer/puppeteer/commit/49bcb2537e45c903e6c1d5d360b0077f0153c5d2))
|
||||
|
||||
## [22.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.11.0...puppeteer-core-v22.11.1) (2024-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* connection closed error should be a rejected promise ([#12575](https://github.com/puppeteer/puppeteer/issues/12575)) ([e36ce8b](https://github.com/puppeteer/puppeteer/commit/e36ce8bee18b4a8c7bf4c0692269d0095d186d06))
|
||||
* ensure selector parser falls back to CSS ([#12585](https://github.com/puppeteer/puppeteer/issues/12585)) ([80783fe](https://github.com/puppeteer/puppeteer/commit/80783fef5a298d2c57f64415f1882d0b051625ef))
|
||||
* implement nested selector parsing ([#12587](https://github.com/puppeteer/puppeteer/issues/12587)) ([3874300](https://github.com/puppeteer/puppeteer/commit/38743007159beedcad8571c08c3320235eb93f76))
|
||||
* roll to Chrome 126.0.6478.61 (r1300313) ([#12586](https://github.com/puppeteer/puppeteer/issues/12586)) ([772e088](https://github.com/puppeteer/puppeteer/commit/772e088f9cc566832b36066c3a6627b5afd47769))
|
||||
|
||||
## [22.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.10.1...puppeteer-core-v22.11.0) (2024-06-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow creating ElementHandles from the accessibility tree snapshot ([#12233](https://github.com/puppeteer/puppeteer/issues/12233)) ([0057f3f](https://github.com/puppeteer/puppeteer/commit/0057f3fe0a8d179cacb18495c96987310f83d5d9))
|
||||
* roll to Chrome 126.0.6478.55 (r1300313) ([#12572](https://github.com/puppeteer/puppeteer/issues/12572)) ([f5bc2b5](https://github.com/puppeteer/puppeteer/commit/f5bc2b53aea0d159dd2b7f4c7a0f7a8a224ae6e8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not wait for extension page targets on connect ([#12574](https://github.com/puppeteer/puppeteer/issues/12574)) ([5f2ee98](https://github.com/puppeteer/puppeteer/commit/5f2ee98c5b93b0a52a98a1d8237189b8b0d15a10))
|
||||
|
||||
## [22.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.10.0...puppeteer-core-v22.10.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add a way to run page.$$ without the isolation ([#12539](https://github.com/puppeteer/puppeteer/issues/12539)) ([03e10a7](https://github.com/puppeteer/puppeteer/commit/03e10a7559f184f8b1adfef83714f36ee26007ca))
|
||||
* align network conditions presets with DevTools ([#12542](https://github.com/puppeteer/puppeteer/issues/12542)) ([ee10745](https://github.com/puppeteer/puppeteer/commit/ee1074559d5290eaa91e7757ecc048e81022fe48))
|
||||
* exposed functions should only be called once ([#12560](https://github.com/puppeteer/puppeteer/issues/12560)) ([8aac8b1](https://github.com/puppeteer/puppeteer/commit/8aac8b1ccb1704f0a67165a7e06427c7db0b4b2f))
|
||||
* **performance:** use Runtime.getProperties for improved performance ([#12561](https://github.com/puppeteer/puppeteer/issues/12561)) ([8b2059f](https://github.com/puppeteer/puppeteer/commit/8b2059f82a801daaa9d27f48d1925bd1335020c6))
|
||||
* roll to Chrome 125.0.6422.141 (r1287751) ([#12509](https://github.com/puppeteer/puppeteer/issues/12509)) ([c4fdd10](https://github.com/puppeteer/puppeteer/commit/c4fdd102e9dd163e5797b2de9024e52ba6efe3bb))
|
||||
* waitForSelector should work for pseudo classes ([#12545](https://github.com/puppeteer/puppeteer/issues/12545)) ([0b2999f](https://github.com/puppeteer/puppeteer/commit/0b2999f7b17d54f368f0a03a45c095e879b7245b))
|
||||
* **webdriver:** default values for touch events ([#12554](https://github.com/puppeteer/puppeteer/issues/12554)) ([4d62988](https://github.com/puppeteer/puppeteer/commit/4d6298837fa85cce39394bfd63b04358b826db53))
|
||||
* **webdriver:** frame url should not be updated on navigationStarted ([#12536](https://github.com/puppeteer/puppeteer/issues/12536)) ([7d0423b](https://github.com/puppeteer/puppeteer/commit/7d0423b12cb5987caf0cc0cd84976986ffc93c98))
|
||||
* **webdriver:** HTTPRequest redirect chain from first request ([#12506](https://github.com/puppeteer/puppeteer/issues/12506)) ([68fd771](https://github.com/puppeteer/puppeteer/commit/68fd7712932f94730b6186107a0509c233938084))
|
||||
|
||||
## [22.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.9.0...puppeteer-core-v22.10.0) (2024-05-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support running Puppeteer in extensions ([#12459](https://github.com/puppeteer/puppeteer/issues/12459)) ([3c6f01a](https://github.com/puppeteer/puppeteer/commit/3c6f01a31dbaef0fdd7f477302b7daa95e0c0929))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* providing null to page.authenticate should disable authentication ([#12203](https://github.com/puppeteer/puppeteer/issues/12203)) ([f375267](https://github.com/puppeteer/puppeteer/commit/f375267e790f61ee2a93d1f2811bef7539fc58d4))
|
||||
* roll to Chrome 125.0.6422.76 (r1287751) ([#12477](https://github.com/puppeteer/puppeteer/issues/12477)) ([d83d9a6](https://github.com/puppeteer/puppeteer/commit/d83d9a6ae2b66b165a4aef5ae59ef3885bfbcff9))
|
||||
* roll to Chrome 125.0.6422.78 (r1287751) ([#12484](https://github.com/puppeteer/puppeteer/issues/12484)) ([f30977f](https://github.com/puppeteer/puppeteer/commit/f30977f8172e3cca605514295fff2086bcd154be))
|
||||
* **webdriver:** emit single HTTPRequest for Auth requests ([#12455](https://github.com/puppeteer/puppeteer/issues/12455)) ([637e827](https://github.com/puppeteer/puppeteer/commit/637e82796b492bcbc82d26753a019972b31a26fd))
|
||||
|
||||
## [22.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v22.8.2...puppeteer-core-v22.9.0) (2024-05-16)
|
||||
|
||||
|
||||
|
@ -48,9 +48,10 @@ export const generateInjectedTask = task({
|
||||
entryPoints: ['src/injected/injected.ts'],
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
target: ['chrome117', 'firefox118'],
|
||||
target: ['chrome125', 'firefox125'],
|
||||
minify: true,
|
||||
write: false,
|
||||
legalComments: 'none',
|
||||
});
|
||||
const template = await readFile('src/templates/injected.ts.tmpl', 'utf8');
|
||||
await mkdir('src/generated', {recursive: true});
|
||||
@ -136,6 +137,16 @@ export const buildTask = task({
|
||||
'utf-8'
|
||||
);
|
||||
break;
|
||||
case 'parsel-js':
|
||||
license = await readFile(
|
||||
path.join(
|
||||
path.dirname(require.resolve('parsel-js')),
|
||||
'..',
|
||||
'LICENSE'
|
||||
),
|
||||
'utf-8'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Add license handling for ${path}`);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "puppeteer-core",
|
||||
"version": "22.9.0",
|
||||
"version": "22.13.0",
|
||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||
"keywords": [
|
||||
"puppeteer",
|
||||
@ -11,6 +11,7 @@
|
||||
"type": "commonjs",
|
||||
"main": "./lib/cjs/puppeteer/puppeteer-core.js",
|
||||
"types": "./lib/types.d.ts",
|
||||
"browser": "./lib/esm/puppeteer/puppeteer-core-browser.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types.d.ts",
|
||||
@ -120,10 +121,10 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.2.3",
|
||||
"chromium-bidi": "0.5.19",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1286932",
|
||||
"ws": "8.17.0"
|
||||
"chromium-bidi": "0.6.0",
|
||||
"debug": "^4.3.5",
|
||||
"devtools-protocol": "0.0.1299070",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "4.1.12",
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
timeout,
|
||||
} from '../common/util.js';
|
||||
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
|
||||
import {Mutex} from '../util/Mutex.js';
|
||||
|
||||
import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
|
||||
import type {Page} from './Page.js';
|
||||
@ -104,6 +105,37 @@ export abstract class BrowserContext extends EventEmitter<BrowserContextEvents>
|
||||
*/
|
||||
abstract targets(): Target[];
|
||||
|
||||
/**
|
||||
* If defined, indicates an ongoing screenshot opereation.
|
||||
*/
|
||||
#pageScreenshotMutex?: Mutex;
|
||||
#screenshotOperationsCount = 0;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
startScreenshot(): Promise<InstanceType<typeof Mutex.Guard>> {
|
||||
const mutex = this.#pageScreenshotMutex || new Mutex();
|
||||
this.#pageScreenshotMutex = mutex;
|
||||
this.#screenshotOperationsCount++;
|
||||
return mutex.acquire(() => {
|
||||
this.#screenshotOperationsCount--;
|
||||
if (this.#screenshotOperationsCount === 0) {
|
||||
// Remove the mutex to indicate no ongoing screenshot operation.
|
||||
this.#pageScreenshotMutex = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
waitForScreenshotOperations():
|
||||
| Promise<InstanceType<typeof Mutex.Guard>>
|
||||
| undefined {
|
||||
return this.#pageScreenshotMutex?.acquire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until a {@link Target | target} matching the given `predicate`
|
||||
* appears and returns it.
|
||||
|
@ -30,7 +30,11 @@ import type {
|
||||
MouseClickOptions,
|
||||
} from './Input.js';
|
||||
import {JSHandle} from './JSHandle.js';
|
||||
import type {ScreenshotOptions, WaitForSelectorOptions} from './Page.js';
|
||||
import type {
|
||||
QueryOptions,
|
||||
ScreenshotOptions,
|
||||
WaitForSelectorOptions,
|
||||
} from './Page.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -345,7 +349,21 @@ export abstract class ElementHandle<
|
||||
/**
|
||||
* Queries the current element for an element matching the given selector.
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @returns A {@link ElementHandle | element handle} to the first element
|
||||
* matching the given selector. Otherwise, `null`.
|
||||
*/
|
||||
@ -365,13 +383,53 @@ export abstract class ElementHandle<
|
||||
/**
|
||||
* Queries the current element for all elements matching the given selector.
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @returns An array of {@link ElementHandle | element handles} that point to
|
||||
* elements matching the given selector.
|
||||
*/
|
||||
@throwIfDisposed()
|
||||
@ElementHandle.bindIsolatedHandle
|
||||
async $$<Selector extends string>(
|
||||
selector: Selector,
|
||||
options?: QueryOptions
|
||||
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
|
||||
if (options?.isolate === false) {
|
||||
return await this.#$$impl(selector);
|
||||
}
|
||||
return await this.#$$(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolates {@link ElementHandle.$$} if needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@ElementHandle.bindIsolatedHandle
|
||||
async #$$<Selector extends string>(
|
||||
selector: Selector
|
||||
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
|
||||
return await this.#$$impl(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation for {@link ElementHandle.$$}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
async #$$impl<Selector extends string>(
|
||||
selector: Selector
|
||||
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
|
||||
const {updatedSelector, QueryHandler} =
|
||||
@ -400,7 +458,21 @@ export abstract class ElementHandle<
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @param pageFunction - The function to be evaluated in this element's page's
|
||||
* context. The first element matching the selector will be passed in as the
|
||||
* first argument.
|
||||
@ -455,7 +527,21 @@ export abstract class ElementHandle<
|
||||
* ).toEqual(['Hello!', 'Hi!']);
|
||||
* ```
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @param pageFunction - The function to be evaluated in the element's page's
|
||||
* context. An array of elements matching the given selector will be passed to
|
||||
* the function as its first argument.
|
||||
@ -534,13 +620,12 @@ export abstract class ElementHandle<
|
||||
selector: Selector,
|
||||
options: WaitForSelectorOptions = {}
|
||||
): Promise<ElementHandle<NodeFor<Selector>> | null> {
|
||||
const {updatedSelector, QueryHandler} =
|
||||
const {updatedSelector, QueryHandler, polling} =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
return (await QueryHandler.waitFor(
|
||||
this,
|
||||
updatedSelector,
|
||||
options
|
||||
)) as ElementHandle<NodeFor<Selector>> | null;
|
||||
return (await QueryHandler.waitFor(this, updatedSelector, {
|
||||
polling,
|
||||
...options,
|
||||
})) as ElementHandle<NodeFor<Selector>> | null;
|
||||
}
|
||||
|
||||
async #checkVisibility(visibility: boolean): Promise<boolean> {
|
||||
@ -556,8 +641,17 @@ export abstract class ElementHandle<
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an element is visible using the same mechanism as
|
||||
* {@link ElementHandle.waitForSelector}.
|
||||
* An element is considered to be visible if all of the following is
|
||||
* true:
|
||||
*
|
||||
* - the element has
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle | computed styles}.
|
||||
*
|
||||
* - the element has a non-empty
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | bounding client rect}.
|
||||
*
|
||||
* - the element's {@link https://developer.mozilla.org/en-US/docs/Web/CSS/visibility | visibility}
|
||||
* is not `hidden` or `collapse`.
|
||||
*/
|
||||
@throwIfDisposed()
|
||||
@ElementHandle.bindIsolatedHandle
|
||||
@ -566,8 +660,16 @@ export abstract class ElementHandle<
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an element is hidden using the same mechanism as
|
||||
* {@link ElementHandle.waitForSelector}.
|
||||
* An element is considered to be hidden if at least one of the following is true:
|
||||
*
|
||||
* - the element has no
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle | computed styles}.
|
||||
*
|
||||
* - the element has an empty
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | bounding client rect}.
|
||||
*
|
||||
* - the element's {@link https://developer.mozilla.org/en-US/docs/Web/CSS/visibility | visibility}
|
||||
* is `hidden` or `collapse`.
|
||||
*/
|
||||
@throwIfDisposed()
|
||||
@ElementHandle.bindIsolatedHandle
|
||||
|
@ -10,9 +10,11 @@ import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {
|
||||
Page,
|
||||
QueryOptions,
|
||||
WaitForSelectorOptions,
|
||||
WaitTimeoutOptions,
|
||||
} from '../api/Page.js';
|
||||
import type {Accessibility} from '../cdp/Accessibility.js';
|
||||
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
|
||||
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
|
||||
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
|
||||
@ -297,11 +299,15 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
/**
|
||||
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
|
||||
* `false`.
|
||||
*
|
||||
* @deprecated Generally, there should be no difference between local and
|
||||
* out-of-process frames from the Puppeteer API perspective. This is an
|
||||
* implementation detail that should not have been exposed.
|
||||
*/
|
||||
abstract isOOPFrame(): boolean;
|
||||
|
||||
/**
|
||||
* Navigates the frame to the given `url`.
|
||||
* Navigates the frame or page to the given `url`.
|
||||
*
|
||||
* @remarks
|
||||
* Navigation to `about:blank` or navigation to the same URL with a different
|
||||
@ -309,12 +315,16 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
*
|
||||
* :::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}.
|
||||
* Headless shell mode doesn't support navigation to a PDF document. See the
|
||||
* {@link https://crbug.com/761295 | upstream issue}.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* In headless shell, 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}.
|
||||
*
|
||||
* @param url - URL to navigate the frame to. The URL should include scheme,
|
||||
* e.g. `https://`
|
||||
* @param options - Options to configure waiting behavior.
|
||||
@ -324,15 +334,14 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* @throws 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}.
|
||||
* - 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.
|
||||
*/
|
||||
abstract goto(
|
||||
url: string,
|
||||
@ -370,6 +379,11 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
*/
|
||||
abstract get client(): CDPSession;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract get accessibility(): Accessibility;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -387,13 +401,9 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
*/
|
||||
#document(): Promise<ElementHandle<Document>> {
|
||||
if (!this.#_document) {
|
||||
this.#_document = this.isolatedRealm()
|
||||
.evaluateHandle(() => {
|
||||
return document;
|
||||
})
|
||||
.then(handle => {
|
||||
return this.mainRealm().transferHandle(handle);
|
||||
});
|
||||
this.#_document = this.mainRealm().evaluateHandle(() => {
|
||||
return document;
|
||||
});
|
||||
}
|
||||
return this.#_document;
|
||||
}
|
||||
@ -432,7 +442,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* Behaves identically to {@link Page.evaluateHandle} except it's run within
|
||||
* the context of this frame.
|
||||
*
|
||||
* @see {@link Page.evaluateHandle} for details.
|
||||
* See {@link Page.evaluateHandle} for details.
|
||||
*/
|
||||
@throwIfDetached
|
||||
async evaluateHandle<
|
||||
@ -453,7 +463,7 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* Behaves identically to {@link Page.evaluate} except it's run within
|
||||
* the context of this frame.
|
||||
*
|
||||
* @see {@link Page.evaluate} for details.
|
||||
* See {@link Page.evaluate} for details.
|
||||
*/
|
||||
@throwIfDetached
|
||||
async evaluate<
|
||||
@ -474,9 +484,21 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* Creates a locator for the provided selector. See {@link Locator} for
|
||||
* details and supported actions.
|
||||
*
|
||||
* @remarks
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*/
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
@ -485,10 +507,6 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
/**
|
||||
* Creates a locator for the provided function. See {@link Locator} for
|
||||
* details and supported actions.
|
||||
*
|
||||
* @remarks
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
*/
|
||||
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
|
||||
|
||||
@ -508,7 +526,22 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
/**
|
||||
* Queries the frame for an element matching the given selector.
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*
|
||||
* @returns A {@link ElementHandle | element handle} to the first element
|
||||
* matching the given selector. Otherwise, `null`.
|
||||
*/
|
||||
@ -524,17 +557,33 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
/**
|
||||
* Queries the frame for all elements matching the given selector.
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*
|
||||
* @returns An array of {@link ElementHandle | element handles} that point to
|
||||
* elements matching the given selector.
|
||||
*/
|
||||
@throwIfDetached
|
||||
async $$<Selector extends string>(
|
||||
selector: Selector
|
||||
selector: Selector,
|
||||
options?: QueryOptions
|
||||
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
|
||||
// eslint-disable-next-line rulesdir/use-using -- This is cached.
|
||||
const document = await this.#document();
|
||||
return await document.$$(selector);
|
||||
return await document.$$(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -550,7 +599,21 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* const searchValue = await frame.$eval('#search', el => el.value);
|
||||
* ```
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @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.
|
||||
@ -589,7 +652,21 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
* const divsCounts = await frame.$$eval('div', divs => divs.length);
|
||||
* ```
|
||||
*
|
||||
* @param selector - The selector to query for.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @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.
|
||||
@ -655,13 +732,12 @@ export abstract class Frame extends EventEmitter<FrameEvents> {
|
||||
selector: Selector,
|
||||
options: WaitForSelectorOptions = {}
|
||||
): Promise<ElementHandle<NodeFor<Selector>> | null> {
|
||||
const {updatedSelector, QueryHandler} =
|
||||
const {updatedSelector, QueryHandler, polling} =
|
||||
getQueryHandlerAndSelector(selector);
|
||||
return (await QueryHandler.waitFor(
|
||||
this,
|
||||
updatedSelector,
|
||||
options
|
||||
)) as ElementHandle<NodeFor<Selector>> | null;
|
||||
return (await QueryHandler.waitFor(this, updatedSelector, {
|
||||
polling,
|
||||
...options,
|
||||
})) as ElementHandle<NodeFor<Selector>> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {HTTPRequest} from './HTTPRequest.js';
|
||||
|
||||
describe('HTTPRequest', () => {
|
||||
describe('getResponse', () => {
|
||||
it('should get body length from empty string', async () => {
|
||||
const response = HTTPRequest.getResponse('');
|
||||
|
||||
expect(response.contentLength).toBe(Buffer.from('').byteLength);
|
||||
});
|
||||
it('should get body length from latin string', async () => {
|
||||
const body = 'Lorem ipsum dolor sit amet';
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.contentLength).toBe(Buffer.from(body).byteLength);
|
||||
});
|
||||
it('should get body length from string with emoji', async () => {
|
||||
const body = 'How Long is this string in bytes 📏?';
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.contentLength).toBe(Buffer.from(body).byteLength);
|
||||
});
|
||||
it('should get body length from Uint8Array', async () => {
|
||||
const body = Buffer.from('How Long is this string in bytes 📏?');
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.contentLength).toBe(body.byteLength);
|
||||
});
|
||||
it('should get base64 from empty string', async () => {
|
||||
const response = HTTPRequest.getResponse('');
|
||||
|
||||
expect(response.base64).toBe(Buffer.from('').toString('base64'));
|
||||
});
|
||||
it('should get base64 from latin string', async () => {
|
||||
const body = 'Lorem ipsum dolor sit amet';
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.base64).toBe(Buffer.from(body).toString('base64'));
|
||||
});
|
||||
it('should get base64 from string with emoji', async () => {
|
||||
const body = 'What am I in base64 🤔?';
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.base64).toBe(Buffer.from(body).toString('base64'));
|
||||
});
|
||||
it('should get base64 length from Uint8Array', async () => {
|
||||
const body = Buffer.from('What am I in base64 🤔?');
|
||||
const response = HTTPRequest.getResponse(body);
|
||||
|
||||
expect(response.base64).toBe(body.toString('base64'));
|
||||
});
|
||||
});
|
||||
});
|
@ -6,7 +6,7 @@
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {ProtocolError} from '../common/Errors.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {debugError, isString} from '../common/util.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import type {CDPSession} from './CDPSession.js';
|
||||
@ -46,7 +46,7 @@ export interface ResponseForRequest {
|
||||
*/
|
||||
headers: Record<string, unknown>;
|
||||
contentType: string;
|
||||
body: string | Buffer;
|
||||
body: string | Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -548,6 +548,29 @@ export abstract class HTTPRequest {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static getResponse(body: string | Uint8Array): {
|
||||
contentLength: number;
|
||||
base64: string;
|
||||
} {
|
||||
// Needed to get the correct byteLength
|
||||
const byteBody: Uint8Array = isString(body)
|
||||
? new TextEncoder().encode(body)
|
||||
: body;
|
||||
|
||||
const bytes = [];
|
||||
for (const byte of byteBody) {
|
||||
bytes.push(String.fromCharCode(byte));
|
||||
}
|
||||
|
||||
return {
|
||||
contentLength: byteBody.byteLength,
|
||||
base64: btoa(bytes.join('')),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -707,7 +730,9 @@ export function handleError(error: ProtocolError): void {
|
||||
// 'Expected "header" [...]'.
|
||||
if (
|
||||
error.originalMessage.includes('Invalid header') ||
|
||||
error.originalMessage.includes('Expected "header"')
|
||||
error.originalMessage.includes('Expected "header"') ||
|
||||
// WebDriver BiDi error for invalid values, for example, headers.
|
||||
error.originalMessage.includes('invalid argument')
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
@ -276,8 +276,9 @@ export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton];
|
||||
/**
|
||||
* The Mouse class operates in main-frame CSS pixels
|
||||
* relative to the top-left corner of the viewport.
|
||||
*
|
||||
* @remarks
|
||||
* Every `page` object has its own Mouse, accessible with [`page.mouse`](#pagemouse).
|
||||
* Every `page` object has its own Mouse, accessible with {@link Page.mouse}.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
|
@ -68,6 +68,7 @@ import {
|
||||
NETWORK_IDLE_TIME,
|
||||
timeout,
|
||||
withSourcePuppeteerURLIfNone,
|
||||
fromAbortSignal,
|
||||
} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import type {ScreenRecorder} from '../node/ScreenRecorder.js';
|
||||
@ -167,9 +168,13 @@ export interface WaitTimeoutOptions {
|
||||
* The default value can be changed by using the
|
||||
* {@link Page.setDefaultTimeout} method.
|
||||
*
|
||||
* @defaultValue `30000`
|
||||
* @defaultValue `30_000`
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* A signal object that allows you to cancel a waitFor call.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,15 +182,16 @@ export interface WaitTimeoutOptions {
|
||||
*/
|
||||
export interface WaitForSelectorOptions {
|
||||
/**
|
||||
* Wait for the selected element to be present in DOM and to be visible, i.e.
|
||||
* to not have `display: none` or `visibility: hidden` CSS properties.
|
||||
* Wait for the selected element to be present in DOM and to be visible. See
|
||||
* {@link ElementHandle.isVisible} for the definition of element visibility.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
visible?: boolean;
|
||||
/**
|
||||
* Wait for the selected element to not be found in the DOM or to be hidden,
|
||||
* i.e. have `display: none` or `visibility: hidden` CSS properties.
|
||||
* Wait for the selected element to not be found in the DOM or to be hidden.
|
||||
* See {@link ElementHandle.isHidden} for the definition of element
|
||||
* invisibility.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
@ -223,10 +229,18 @@ export interface GeolocationOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* A media feature to emulate.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MediaFeature {
|
||||
/**
|
||||
* A name of the feature, for example, 'prefers-reduced-motion'.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A value for the feature, for example, 'reduce'.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
@ -331,13 +345,28 @@ export interface ScreencastOptions {
|
||||
*/
|
||||
speed?: number;
|
||||
/**
|
||||
* Path to the [ffmpeg](https://ffmpeg.org/).
|
||||
* Path to the {@link https://ffmpeg.org/ | ffmpeg}.
|
||||
*
|
||||
* Required if `ffmpeg` is not in your PATH.
|
||||
*/
|
||||
ffmpegPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface QueryOptions {
|
||||
/**
|
||||
* Whether to run the query in isolation. When returning many elements
|
||||
* from {@link Page.$$} or similar methods, it might be useful to turn
|
||||
* off the isolation to improve performance. By default, the querying
|
||||
* code will be executed in a separate sandbox realm.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
isolate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the events that a page instance may emit.
|
||||
*
|
||||
@ -727,6 +756,13 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* :::caution
|
||||
*
|
||||
* Interception of file dialogs triggered via DOM APIs such as
|
||||
* window.showOpenFilePicker is currently not supported.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* @remarks
|
||||
* In the "headful" browser, this method results in the native file picker
|
||||
* dialog `not showing up` for the user.
|
||||
@ -782,9 +818,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
|
||||
/**
|
||||
* The page's main frame.
|
||||
*
|
||||
* @remarks
|
||||
* Page is guaranteed to have a main frame which persists during navigations.
|
||||
*/
|
||||
abstract mainFrame(): Frame;
|
||||
|
||||
@ -816,7 +849,9 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
/**
|
||||
* {@inheritDoc Accessibility}
|
||||
*/
|
||||
abstract get accessibility(): Accessibility;
|
||||
get accessibility(): Accessibility {
|
||||
return this.mainFrame().accessibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of all frames attached to the page.
|
||||
@ -960,9 +995,21 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* Creates a locator for the provided selector. See {@link Locator} for
|
||||
* details and supported actions.
|
||||
*
|
||||
* @remarks
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*/
|
||||
locator<Selector extends string>(
|
||||
selector: Selector
|
||||
@ -972,9 +1019,21 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* Creates a locator for the provided function. See {@link Locator} for
|
||||
* details and supported actions.
|
||||
*
|
||||
* @remarks
|
||||
* Locators API is experimental and we will not follow semver for breaking
|
||||
* change in the Locators API.
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*/
|
||||
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
|
||||
locator<Selector extends string, Ret>(
|
||||
@ -999,12 +1058,28 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `document.querySelector` within the page. If no element matches the
|
||||
* selector, the return value resolves to `null`.
|
||||
* Finds the first element that matches the selector. If no element matches
|
||||
* the selector, the return value resolves to `null`.
|
||||
*
|
||||
* @param selector - A `selector` to query page for
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Shortcut for {@link Frame.$ | Page.mainFrame().$(selector) }.
|
||||
*/
|
||||
async $<Selector extends string>(
|
||||
selector: Selector
|
||||
@ -1013,19 +1088,34 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The method runs `document.querySelectorAll` within the page. If no elements
|
||||
* Finds elements on the page that match the selector. If no elements
|
||||
* match the selector, the return value resolves to `[]`.
|
||||
*
|
||||
* @param selector - A `selector` to query page for
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }.
|
||||
*/
|
||||
async $$<Selector extends string>(
|
||||
selector: Selector
|
||||
selector: Selector,
|
||||
options?: QueryOptions
|
||||
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
|
||||
return await this.mainFrame().$$(selector);
|
||||
return await this.mainFrame().$$(selector, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1127,8 +1217,8 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
): Promise<JSHandle<Prototype[]>>;
|
||||
|
||||
/**
|
||||
* This method runs `document.querySelector` within the page and passes the
|
||||
* result as the first argument to the `pageFunction`.
|
||||
* This method finds the first element within the page that matches the selector
|
||||
* and passes the result as the first argument to the `pageFunction`.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
@ -1176,11 +1266,23 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @param selector - the
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
|
||||
* to query for
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @param pageFunction - the function to be evaluated in the page context.
|
||||
* Will be passed the result of `document.querySelector(selector)` as its
|
||||
* Will be passed the result of the element matching the selector as its
|
||||
* first argument.
|
||||
* @param args - any additional arguments to pass through to `pageFunction`.
|
||||
*
|
||||
@ -1205,8 +1307,8 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method runs `Array.from(document.querySelectorAll(selector))` within
|
||||
* the page and passes the result as the first argument to the `pageFunction`.
|
||||
* This method returns all elements matching the selector and passes the
|
||||
* resulting array as the first argument to the `pageFunction`.
|
||||
*
|
||||
* @remarks
|
||||
* If `pageFunction` returns a promise `$$eval` will wait for the promise to
|
||||
@ -1249,12 +1351,23 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @param selector - the
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
|
||||
* to query for
|
||||
* @param selector -
|
||||
* {@link https://pptr.dev/guides/page-interactions#selectors | selector}
|
||||
* to query page for.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | CSS selectors}
|
||||
* can be passed as-is and a
|
||||
* {@link https://pptr.dev/guides/page-interactions#non-css-selectors | Puppeteer-specific selector syntax}
|
||||
* allows quering by
|
||||
* {@link https://pptr.dev/guides/page-interactions#text-selectors--p-text | text},
|
||||
* {@link https://pptr.dev/guides/page-interactions#aria-selectors--p-aria | a11y role and name},
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#xpath-selectors--p-xpath | xpath}
|
||||
* and
|
||||
* {@link https://pptr.dev/guides/page-interactions#querying-elements-in-shadow-dom | combining these queries across shadow roots}.
|
||||
* Alternatively, you can specify the selector type using a
|
||||
* {@link https://pptr.dev/guides/page-interactions#prefixed-selector-syntax | prefix}.
|
||||
* @param pageFunction - the function to be evaluated in the page context.
|
||||
* Will be passed the result of
|
||||
* `Array.from(document.querySelectorAll(selector))` as its first argument.
|
||||
* Will be passed an array of matching elements as its first argument.
|
||||
* @param args - any additional arguments to pass through to `pageFunction`.
|
||||
*
|
||||
* @returns The result of calling `pageFunction`. If it returns an element it
|
||||
@ -1417,10 +1530,17 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
/**
|
||||
* Provide credentials for `HTTP authentication`.
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
* Request interception will be turned on behind the scenes to
|
||||
* implement authentication. This might affect performance.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* @remarks
|
||||
* To disable authentication, pass `null`.
|
||||
*/
|
||||
abstract authenticate(credentials: Credentials): Promise<void>;
|
||||
abstract authenticate(credentials: Credentials | null): Promise<void>;
|
||||
|
||||
/**
|
||||
* The extra HTTP headers will be sent with every request the page initiates.
|
||||
@ -1516,69 +1636,13 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
*
|
||||
* @param html - HTML markup to assign to the page.
|
||||
* @param options - Parameters that has some properties.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The parameter `options` might have the following options.
|
||||
*
|
||||
* - `timeout` : Maximum time in milliseconds for resources to load, defaults
|
||||
* to 30 seconds, pass `0` to disable timeout. The default value can be
|
||||
* changed by using the {@link Page.setDefaultNavigationTimeout} or
|
||||
* {@link Page.setDefaultTimeout} methods.
|
||||
*
|
||||
* - `waitUntil`: When to consider setting markup succeeded, defaults to
|
||||
* `load`. Given an array of event strings, setting content is considered
|
||||
* to be successful after all events have been fired. Events can be
|
||||
* either:<br/>
|
||||
* - `load` : consider setting content to be finished when the `load` event
|
||||
* is fired.<br/>
|
||||
* - `domcontentloaded` : consider setting content to be finished when the
|
||||
* `DOMContentLoaded` event is fired.<br/>
|
||||
* - `networkidle0` : consider setting content to be finished when there are
|
||||
* no more than 0 network connections for at least `500` ms.<br/>
|
||||
* - `networkidle2` : consider setting content to be finished when there are
|
||||
* no more than 2 network connections for at least `500` ms.
|
||||
*/
|
||||
async setContent(html: string, options?: WaitForOptions): Promise<void> {
|
||||
await this.mainFrame().setContent(html, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates the page 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}.
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* Shortcut for {@link Frame.goto | page.mainFrame().goto(url, options)}.
|
||||
*
|
||||
* @param url - URL to navigate page to. The URL should include scheme, e.g.
|
||||
* `https://`
|
||||
* @param options - Options to configure waiting behavior.
|
||||
* @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 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}.
|
||||
* {@inheritDoc Frame.goto}
|
||||
*/
|
||||
async goto(url: string, options?: GoToOptions): Promise<HTTPResponse | null> {
|
||||
return await this.mainFrame().goto(url, options);
|
||||
@ -1655,7 +1719,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
urlOrPredicate: string | AwaitablePredicate<HTTPRequest>,
|
||||
options: WaitTimeoutOptions = {}
|
||||
): Promise<HTTPRequest> {
|
||||
const {timeout: ms = this._timeoutSettings.timeout()} = options;
|
||||
const {timeout: ms = this._timeoutSettings.timeout(), signal} = options;
|
||||
if (typeof urlOrPredicate === 'string') {
|
||||
const url = urlOrPredicate;
|
||||
urlOrPredicate = (request: HTTPRequest) => {
|
||||
@ -1666,6 +1730,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
filterAsync(urlOrPredicate),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
fromAbortSignal(signal),
|
||||
fromEmitterEvent(this, PageEvent.Close).pipe(
|
||||
map(() => {
|
||||
throw new TargetCloseError('Page closed!');
|
||||
@ -1707,7 +1772,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
urlOrPredicate: string | AwaitablePredicate<HTTPResponse>,
|
||||
options: WaitTimeoutOptions = {}
|
||||
): Promise<HTTPResponse> {
|
||||
const {timeout: ms = this._timeoutSettings.timeout()} = options;
|
||||
const {timeout: ms = this._timeoutSettings.timeout(), signal} = options;
|
||||
if (typeof urlOrPredicate === 'string') {
|
||||
const url = urlOrPredicate;
|
||||
urlOrPredicate = (response: HTTPResponse) => {
|
||||
@ -1718,6 +1783,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
filterAsync(urlOrPredicate),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
fromAbortSignal(signal),
|
||||
fromEmitterEvent(this, PageEvent.Close).pipe(
|
||||
map(() => {
|
||||
throw new TargetCloseError('Page closed!');
|
||||
@ -1748,6 +1814,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
timeout: ms = this._timeoutSettings.timeout(),
|
||||
idleTime = NETWORK_IDLE_TIME,
|
||||
concurrency = 0,
|
||||
signal,
|
||||
} = options;
|
||||
|
||||
return this.#inflight$.pipe(
|
||||
@ -1760,6 +1827,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
map(() => {}),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
fromAbortSignal(signal),
|
||||
fromEmitterEvent(this, PageEvent.Close).pipe(
|
||||
map(() => {
|
||||
throw new TargetCloseError('Page closed!');
|
||||
@ -1784,7 +1852,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
urlOrPredicate: string | ((frame: Frame) => Awaitable<boolean>),
|
||||
options: WaitTimeoutOptions = {}
|
||||
): Promise<Frame> {
|
||||
const {timeout: ms = this.getDefaultTimeout()} = options;
|
||||
const {timeout: ms = this.getDefaultTimeout(), signal} = options;
|
||||
|
||||
if (isString(urlOrPredicate)) {
|
||||
urlOrPredicate = (frame: Frame) => {
|
||||
@ -1802,6 +1870,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
first(),
|
||||
raceWith(
|
||||
timeout(ms),
|
||||
fromAbortSignal(signal),
|
||||
fromEmitterEvent(this, PageEvent.Close).pipe(
|
||||
map(() => {
|
||||
throw new TargetCloseError('Page closed.');
|
||||
@ -1818,25 +1887,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* @returns Promise which resolves to the main resource response. In case of
|
||||
* multiple redirects, the navigation will resolve with the response of the
|
||||
* last redirect. If can not go back, resolves to `null`.
|
||||
* @remarks
|
||||
* The argument `options` might have the following properties:
|
||||
*
|
||||
* - `timeout` : Maximum navigation time in milliseconds, defaults to 30
|
||||
* seconds, pass 0 to disable timeout. The default value can be changed by
|
||||
* using the {@link Page.setDefaultNavigationTimeout} or
|
||||
* {@link Page.setDefaultTimeout} methods.
|
||||
*
|
||||
* - `waitUntil` : When to consider navigation succeeded, defaults to `load`.
|
||||
* Given an array of event strings, navigation is considered to be
|
||||
* successful after all events have been fired. Events can be either:<br/>
|
||||
* - `load` : consider navigation to be finished when the load event is
|
||||
* fired.<br/>
|
||||
* - `domcontentloaded` : consider navigation to be finished when the
|
||||
* DOMContentLoaded event is fired.<br/>
|
||||
* - `networkidle0` : consider navigation to be finished when there are no
|
||||
* more than 0 network connections for at least `500` ms.<br/>
|
||||
* - `networkidle2` : consider navigation to be finished when there are no
|
||||
* more than 2 network connections for at least `500` ms.
|
||||
*/
|
||||
abstract goBack(options?: WaitForOptions): Promise<HTTPResponse | null>;
|
||||
|
||||
@ -1846,25 +1896,6 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* @returns Promise which resolves to the main resource response. In case of
|
||||
* multiple redirects, the navigation will resolve with the response of the
|
||||
* last redirect. If can not go forward, resolves to `null`.
|
||||
* @remarks
|
||||
* The argument `options` might have the following properties:
|
||||
*
|
||||
* - `timeout` : Maximum navigation time in milliseconds, defaults to 30
|
||||
* seconds, pass 0 to disable timeout. The default value can be changed by
|
||||
* using the {@link Page.setDefaultNavigationTimeout} or
|
||||
* {@link Page.setDefaultTimeout} methods.
|
||||
*
|
||||
* - `waitUntil`: When to consider navigation succeeded, defaults to `load`.
|
||||
* Given an array of event strings, navigation is considered to be
|
||||
* successful after all events have been fired. Events can be either:<br/>
|
||||
* - `load` : consider navigation to be finished when the load event is
|
||||
* fired.<br/>
|
||||
* - `domcontentloaded` : consider navigation to be finished when the
|
||||
* DOMContentLoaded event is fired.<br/>
|
||||
* - `networkidle0` : consider navigation to be finished when there are no
|
||||
* more than 0 network connections for at least `500` ms.<br/>
|
||||
* - `networkidle2` : consider navigation to be finished when there are no
|
||||
* more than 2 network connections for at least `500` ms.
|
||||
*/
|
||||
abstract goForward(options?: WaitForOptions): Promise<HTTPResponse | null>;
|
||||
|
||||
@ -2093,7 +2124,9 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* the page.
|
||||
*
|
||||
* In the case of multiple pages in a single browser, each page can have its
|
||||
* own viewport size.
|
||||
* own viewport size. Setting the viewport to `null` resets the viewport to
|
||||
* its default value.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
@ -2111,7 +2144,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* NOTE: in certain cases, setting viewport will reload the page in order to
|
||||
* set the isMobile or hasTouch properties.
|
||||
*/
|
||||
abstract setViewport(viewport: Viewport): Promise<void>;
|
||||
abstract setViewport(viewport: Viewport | null): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the current page viewport settings without checking the actual page
|
||||
@ -2438,6 +2471,18 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
* Captures a screenshot of this {@link Page | page}.
|
||||
*
|
||||
* @param options - Configures screenshot behavior.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* While a screenshot is being taken in a {@link BrowserContext}, the
|
||||
* following methods will automatically wait for the screenshot to
|
||||
* finish to prevent interference with the screenshot process:
|
||||
* {@link BrowserContext.newPage}, {@link Browser.newPage},
|
||||
* {@link Page.close}.
|
||||
*
|
||||
* Calling {@link Page.bringToFront} will not wait for existing
|
||||
* screenshot operations.
|
||||
*
|
||||
*/
|
||||
async screenshot(
|
||||
options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
|
||||
@ -2449,6 +2494,8 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
async screenshot(
|
||||
userOptions: Readonly<ScreenshotOptions> = {}
|
||||
): Promise<Buffer | string> {
|
||||
using _guard = await this.browserContext().startScreenshot();
|
||||
|
||||
await this.bringToFront();
|
||||
|
||||
// TODO: use structuredClone after Node 16 support is dropped.
|
||||
@ -2480,7 +2527,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
}
|
||||
}
|
||||
if (options.quality !== undefined) {
|
||||
if (options.quality < 0 && options.quality > 100) {
|
||||
if (options.quality < 0 || options.quality > 100) {
|
||||
throw new Error(
|
||||
`Expected 'quality' (${options.quality}) to be between 0 and 100, inclusive.`
|
||||
);
|
||||
@ -2533,14 +2580,7 @@ export abstract class Page extends EventEmitter<PageEvents> {
|
||||
...scrollDimensions,
|
||||
});
|
||||
stack.defer(async () => {
|
||||
if (viewport) {
|
||||
await this.setViewport(viewport).catch(debugError);
|
||||
} else {
|
||||
await this.setViewport({
|
||||
width: 0,
|
||||
height: 0,
|
||||
}).catch(debugError);
|
||||
}
|
||||
await this.setViewport(viewport).catch(debugError);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
first,
|
||||
firstValueFrom,
|
||||
from,
|
||||
fromEvent,
|
||||
identity,
|
||||
ignoreElements,
|
||||
map,
|
||||
@ -33,7 +32,7 @@ import {
|
||||
import type {EventType} from '../../common/EventEmitter.js';
|
||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||
import type {Awaitable, HandleFor, NodeFor} from '../../common/types.js';
|
||||
import {debugError, timeout} from '../../common/util.js';
|
||||
import {debugError, fromAbortSignal, timeout} from '../../common/util.js';
|
||||
import type {
|
||||
BoundingBox,
|
||||
ClickOptions,
|
||||
@ -43,48 +42,22 @@ import type {Frame} from '../Frame.js';
|
||||
import type {Page} from '../Page.js';
|
||||
|
||||
/**
|
||||
* Whether to wait for the element to be
|
||||
* {@link ElementHandle.isVisible | visible} or
|
||||
* {@link ElementHandle.isHidden | hidden}.
|
||||
* `null` to disable visibility checks.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type VisibilityOption = 'hidden' | 'visible' | null;
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface LocatorOptions {
|
||||
/**
|
||||
* Whether to wait for the element to be `visible` or `hidden`. `null` to
|
||||
* disable visibility checks.
|
||||
*/
|
||||
visibility: VisibilityOption;
|
||||
/**
|
||||
* Total timeout for the entire locator operation.
|
||||
*
|
||||
* Pass `0` to disable timeout.
|
||||
*
|
||||
* @defaultValue `Page.getDefaultTimeout()`
|
||||
*/
|
||||
timeout: number;
|
||||
/**
|
||||
* Whether to scroll the element into viewport if not in the viewprot already.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
ensureElementIsInTheViewport: boolean;
|
||||
/**
|
||||
* Whether to wait for input elements to become enabled before the action.
|
||||
* Applicable to `click` and `fill` actions.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
waitForEnabled: boolean;
|
||||
/**
|
||||
* Whether to wait for the element's bounding box to be same between two
|
||||
* animation frames.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
waitForStableBoundingBox: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ActionOptions {
|
||||
/**
|
||||
* A signal to abort the locator action.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/**
|
||||
@ -123,12 +96,14 @@ export interface LocatorEvents extends Record<EventType, unknown> {
|
||||
* whole operation is retried. Various preconditions for a successful action are
|
||||
* checked automatically.
|
||||
*
|
||||
* See {@link https://pptr.dev/guides/page-interactions#locators} for details.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
/**
|
||||
* Creates a race between multiple locators but ensures that only a single one
|
||||
* acts.
|
||||
* Creates a race between multiple locators trying to locate elements in
|
||||
* parallel but ensures that only a single element receives the action.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@ -177,16 +152,7 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
): OperatorFunction<T, T> => {
|
||||
const candidates = [];
|
||||
if (signal) {
|
||||
candidates.push(
|
||||
fromEvent(signal, 'abort').pipe(
|
||||
map(() => {
|
||||
if (signal.reason instanceof Error) {
|
||||
signal.reason.cause = cause;
|
||||
}
|
||||
throw signal.reason;
|
||||
})
|
||||
)
|
||||
);
|
||||
candidates.push(fromAbortSignal(signal, cause));
|
||||
}
|
||||
candidates.push(timeout(this._timeout, cause));
|
||||
return pipe(
|
||||
@ -201,12 +167,24 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return this._timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new locator instance by cloning the current locator and setting
|
||||
* the total timeout for the locator actions.
|
||||
*
|
||||
* Pass `0` to disable timeout.
|
||||
*
|
||||
* @defaultValue `Page.getDefaultTimeout()`
|
||||
*/
|
||||
setTimeout(timeout: number): Locator<T> {
|
||||
const locator = this._clone();
|
||||
locator._timeout = timeout;
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new locator instance by cloning the current locator with the
|
||||
* visibility property changed to the specified value.
|
||||
*/
|
||||
setVisibility<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
visibility: VisibilityOption
|
||||
@ -216,6 +194,13 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new locator instance by cloning the current locator and
|
||||
* specifying whether to wait for input elements to become enabled before the
|
||||
* action. Applicable to `click` and `fill` actions.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
setWaitForEnabled<NodeType extends Node>(
|
||||
this: Locator<NodeType>,
|
||||
value: boolean
|
||||
@ -225,6 +210,13 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new locator instance by cloning the current locator and
|
||||
* specifying whether the locator should scroll the element into viewport if
|
||||
* it is not in the viewport already.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
setEnsureElementIsInTheViewport<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
@ -234,6 +226,13 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new locator instance by cloning the current locator and
|
||||
* specifying whether the locator has to wait for the element's bounding box
|
||||
* to be same between two consecutive animation frames.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
setWaitForStableBoundingBox<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
value: boolean
|
||||
@ -697,6 +696,9 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return new MappedLocator(this._clone(), mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the located element.
|
||||
*/
|
||||
click<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorClickOptions>
|
||||
@ -707,8 +709,8 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
/**
|
||||
* Fills out the input identified by the locator using the provided value. The
|
||||
* type of the input is determined at runtime and the appropriate fill-out
|
||||
* method is chosen based on the type. contenteditable, selector, inputs are
|
||||
* supported.
|
||||
* method is chosen based on the type. `contenteditable`, select, textarea and
|
||||
* input elements are supported.
|
||||
*/
|
||||
fill<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
@ -718,6 +720,9 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return firstValueFrom(this.#fill(value, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hovers over the located element.
|
||||
*/
|
||||
hover<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<ActionOptions>
|
||||
@ -725,6 +730,9 @@ export abstract class Locator<T> extends EventEmitter<LocatorEvents> {
|
||||
return firstValueFrom(this.#hover(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the located element.
|
||||
*/
|
||||
scroll<ElementType extends Element>(
|
||||
this: Locator<ElementType>,
|
||||
options?: Readonly<LocatorScrollOptions>
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
|
||||
|
||||
import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
|
||||
@ -59,7 +59,14 @@ export async function connectBidiOverCdp(
|
||||
// TODO: most likely need a little bit of refactoring
|
||||
cdpConnectionAdapter.browserClient(),
|
||||
'',
|
||||
options,
|
||||
{
|
||||
// Override Mapper's `unhandledPromptBehavior` default value of `dismiss` to
|
||||
// `ignore`, so that user can handle the prompt instead of just closing it.
|
||||
unhandledPromptBehavior: {
|
||||
default: Bidi.Session.UserPromptHandlerType.Ignore,
|
||||
},
|
||||
...options,
|
||||
},
|
||||
undefined,
|
||||
bidiServerLogger
|
||||
);
|
||||
|
@ -83,7 +83,21 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
this.userContext.on('browsingcontext', ({browsingContext}) => {
|
||||
this.#createPage(browsingContext);
|
||||
const page = this.#createPage(browsingContext);
|
||||
|
||||
// We need to wait for the DOMContentLoaded as the
|
||||
// browsingContext still may be navigating from the about:blank
|
||||
browsingContext.once('DOMContentLoaded', () => {
|
||||
if (browsingContext.originalOpener) {
|
||||
for (const context of this.userContext.browsingContexts) {
|
||||
if (context.id === browsingContext.originalOpener) {
|
||||
this.#pages
|
||||
.get(context)!
|
||||
.trustedEmitter.emit(PageEvent.Popup, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this.userContext.on('closed', () => {
|
||||
this.trustedEmitter.removeAllListeners();
|
||||
@ -161,6 +175,8 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
override async newPage(): Promise<Page> {
|
||||
using _guard = await this.waitForScreenshotOperations();
|
||||
|
||||
const context = await this.userContext.createBrowsingContext(
|
||||
Bidi.BrowsingContext.CreateType.Tab
|
||||
);
|
||||
@ -189,6 +205,8 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
}
|
||||
|
||||
this.#targets.clear();
|
||||
}
|
||||
|
||||
override browser(): BidiBrowser {
|
||||
|
@ -136,7 +136,7 @@ export class BidiConnection
|
||||
this.#callbacks.reject(
|
||||
object.id,
|
||||
createProtocolError(object),
|
||||
object.message
|
||||
`${object.error}: ${object.message}`
|
||||
);
|
||||
return;
|
||||
case 'event':
|
||||
|
@ -243,6 +243,11 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
|
||||
realm.evaluate(name => {
|
||||
delete (globalThis as any)[name];
|
||||
}, this.name),
|
||||
...frame.childFrames().map(childFrame => {
|
||||
return childFrame.evaluate(name => {
|
||||
delete (globalThis as any)[name];
|
||||
}, this.name);
|
||||
}),
|
||||
frame.browsingContext.removePreloadScript(script),
|
||||
]);
|
||||
} catch (error) {
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
type WaitForOptions,
|
||||
} from '../api/Frame.js';
|
||||
import {PageEvent} from '../api/Page.js';
|
||||
import {Accessibility} from '../cdp/Accessibility.js';
|
||||
import {
|
||||
ConsoleMessage,
|
||||
type ConsoleMessageLocation,
|
||||
@ -71,6 +72,7 @@ export class BidiFrame extends Frame {
|
||||
|
||||
override readonly _id: string;
|
||||
override readonly client: BidiCdpSession;
|
||||
override readonly accessibility: Accessibility;
|
||||
|
||||
private constructor(
|
||||
parent: BidiPage | BidiFrame,
|
||||
@ -91,6 +93,7 @@ export class BidiFrame extends Frame {
|
||||
this
|
||||
),
|
||||
};
|
||||
this.accessibility = new Accessibility(this.realms.default);
|
||||
}
|
||||
|
||||
#initialize(): void {
|
||||
|
@ -38,7 +38,8 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
request.#initialize();
|
||||
return request;
|
||||
}
|
||||
#redirectBy: BidiHTTPRequest | undefined;
|
||||
|
||||
#redirectChain: BidiHTTPRequest[];
|
||||
#response: BidiHTTPResponse | null = null;
|
||||
override readonly id: string;
|
||||
readonly #frame: BidiFrame;
|
||||
@ -47,7 +48,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
private constructor(
|
||||
request: Request,
|
||||
frame: BidiFrame,
|
||||
redirectBy?: BidiHTTPRequest
|
||||
redirect?: BidiHTTPRequest
|
||||
) {
|
||||
super();
|
||||
requests.set(request, this);
|
||||
@ -56,7 +57,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
|
||||
this.#request = request;
|
||||
this.#frame = frame;
|
||||
this.#redirectBy = redirectBy;
|
||||
this.#redirectChain = redirect ? redirect.#redirectChain : [];
|
||||
this.id = request.id;
|
||||
}
|
||||
|
||||
@ -67,6 +68,8 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
#initialize() {
|
||||
this.#request.on('redirect', request => {
|
||||
const httpRequest = BidiHTTPRequest.from(request, this.#frame, this);
|
||||
this.#redirectChain.push(this);
|
||||
|
||||
request.once('success', () => {
|
||||
this.#frame
|
||||
.page()
|
||||
@ -170,16 +173,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
}
|
||||
|
||||
override redirectChain(): BidiHTTPRequest[] {
|
||||
if (this.#redirectBy === undefined) {
|
||||
return [];
|
||||
}
|
||||
const redirects = [this.#redirectBy];
|
||||
for (const redirect of redirects) {
|
||||
if (redirect.#redirectBy !== undefined) {
|
||||
redirects.push(redirect.#redirectBy);
|
||||
}
|
||||
}
|
||||
return redirects;
|
||||
return this.#redirectChain.slice();
|
||||
}
|
||||
|
||||
override frame(): BidiFrame {
|
||||
@ -236,12 +230,16 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
_priority?: number
|
||||
): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
const responseBody: string | undefined =
|
||||
response.body && response.body instanceof Uint8Array
|
||||
? response.body.toString('base64')
|
||||
: response.body
|
||||
? btoa(response.body)
|
||||
: undefined;
|
||||
|
||||
let parsedBody:
|
||||
| {
|
||||
contentLength: number;
|
||||
base64: string;
|
||||
}
|
||||
| undefined;
|
||||
if (response.body) {
|
||||
parsedBody = HTTPRequest.getResponse(response.body);
|
||||
}
|
||||
|
||||
const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
|
||||
const hasContentLength = headers.some(header => {
|
||||
@ -258,13 +256,12 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
});
|
||||
}
|
||||
|
||||
if (responseBody && !hasContentLength) {
|
||||
const encoder = new TextEncoder();
|
||||
if (parsedBody?.contentLength && !hasContentLength) {
|
||||
headers.push({
|
||||
name: 'content-length',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: String(encoder.encode(responseBody).byteLength),
|
||||
value: String(parsedBody.contentLength),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -275,10 +272,10 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
statusCode: status,
|
||||
headers: headers.length > 0 ? headers : undefined,
|
||||
reasonPhrase: STATUS_TEXTS[status],
|
||||
body: responseBody
|
||||
body: parsedBody?.base64
|
||||
? {
|
||||
type: 'base64',
|
||||
value: responseBody,
|
||||
value: parsedBody?.base64,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
@ -643,6 +643,10 @@ export class BidiTouchscreen extends Touchscreen {
|
||||
{
|
||||
type: ActionType.PointerDown,
|
||||
button: 0,
|
||||
width: 0.5 * 2, // 2 times default touch radius.
|
||||
height: 0.5 * 2, // 2 times default touch radius.
|
||||
pressure: 0.5,
|
||||
altitudeAngle: Math.PI / 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -667,6 +671,10 @@ export class BidiTouchscreen extends Touchscreen {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
origin: options.origin,
|
||||
width: 0.5 * 2, // 2 times default touch radius.
|
||||
height: 0.5 * 2, // 2 times default touch radius.
|
||||
pressure: 0.5,
|
||||
altitudeAngle: Math.PI / 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -24,9 +24,12 @@ import {
|
||||
type NewDocumentScriptEvaluation,
|
||||
type ScreenshotOptions,
|
||||
} from '../api/Page.js';
|
||||
import {Accessibility} from '../cdp/Accessibility.js';
|
||||
import {Coverage} from '../cdp/Coverage.js';
|
||||
import {EmulationManager} from '../cdp/EmulationManager.js';
|
||||
import type {
|
||||
InternalNetworkConditions,
|
||||
NetworkConditions,
|
||||
} from '../cdp/NetworkManager.js';
|
||||
import {Tracing} from '../cdp/Tracing.js';
|
||||
import type {
|
||||
Cookie,
|
||||
@ -86,11 +89,12 @@ export class BidiPage extends Page {
|
||||
readonly keyboard: BidiKeyboard;
|
||||
readonly mouse: BidiMouse;
|
||||
readonly touchscreen: BidiTouchscreen;
|
||||
readonly accessibility: Accessibility;
|
||||
readonly tracing: Tracing;
|
||||
readonly coverage: Coverage;
|
||||
readonly #cdpEmulationManager: EmulationManager;
|
||||
|
||||
#emulatedNetworkConditions?: InternalNetworkConditions;
|
||||
|
||||
_client(): BidiCdpSession {
|
||||
return this.#frame.client;
|
||||
}
|
||||
@ -104,7 +108,6 @@ export class BidiPage extends Page {
|
||||
this.#frame = BidiFrame.from(this, browsingContext);
|
||||
|
||||
this.#cdpEmulationManager = new EmulationManager(this.#frame.client);
|
||||
this.accessibility = new Accessibility(this.#frame.client);
|
||||
this.tracing = new Tracing(this.#frame.client);
|
||||
this.coverage = new Coverage(this.#frame.client);
|
||||
this.keyboard = new BidiKeyboard(this);
|
||||
@ -264,6 +267,7 @@ export class BidiPage extends Page {
|
||||
}
|
||||
|
||||
override async close(options?: {runBeforeUnload?: boolean}): Promise<void> {
|
||||
using _guard = await this.#browserContext.waitForScreenshotOperations();
|
||||
try {
|
||||
await this.#frame.browsingContext.close(options?.runBeforeUnload);
|
||||
} catch {
|
||||
@ -341,17 +345,17 @@ export class BidiPage extends Page {
|
||||
return await this.#cdpEmulationManager.emulateVisionDeficiency(type);
|
||||
}
|
||||
|
||||
override async setViewport(viewport: Viewport): Promise<void> {
|
||||
override async setViewport(viewport: Viewport | null): Promise<void> {
|
||||
if (!this.browser().cdpSupported) {
|
||||
await this.#frame.browsingContext.setViewport({
|
||||
viewport:
|
||||
viewport.width && viewport.height
|
||||
viewport?.width && viewport?.height
|
||||
? {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
}
|
||||
: null,
|
||||
devicePixelRatio: viewport.deviceScaleFactor
|
||||
devicePixelRatio: viewport?.deviceScaleFactor
|
||||
? viewport.deviceScaleFactor
|
||||
: null,
|
||||
});
|
||||
@ -534,6 +538,12 @@ export class BidiPage extends Page {
|
||||
}
|
||||
|
||||
override async setCacheEnabled(enabled?: boolean): Promise<void> {
|
||||
if (!this.#browserContext.browser().cdpSupported) {
|
||||
await this.#frame.browsingContext.setCacheBehavior(
|
||||
enabled ? 'default' : 'bypass'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO: handle CDP-specific cases such as mprach.
|
||||
await this._client().send('Network.setCacheDisabled', {
|
||||
cacheDisabled: !enabled,
|
||||
@ -648,12 +658,59 @@ export class BidiPage extends Page {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
override setOfflineMode(): never {
|
||||
throw new UnsupportedOperation();
|
||||
override async setOfflineMode(enabled: boolean): Promise<void> {
|
||||
if (!this.#browserContext.browser().cdpSupported) {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
|
||||
if (!this.#emulatedNetworkConditions) {
|
||||
this.#emulatedNetworkConditions = {
|
||||
offline: false,
|
||||
upload: -1,
|
||||
download: -1,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
this.#emulatedNetworkConditions.offline = enabled;
|
||||
return await this.#applyNetworkConditions();
|
||||
}
|
||||
|
||||
override emulateNetworkConditions(): never {
|
||||
throw new UnsupportedOperation();
|
||||
override async emulateNetworkConditions(
|
||||
networkConditions: NetworkConditions | null
|
||||
): Promise<void> {
|
||||
if (!this.#browserContext.browser().cdpSupported) {
|
||||
throw new UnsupportedOperation();
|
||||
}
|
||||
if (!this.#emulatedNetworkConditions) {
|
||||
this.#emulatedNetworkConditions = {
|
||||
offline: false,
|
||||
upload: -1,
|
||||
download: -1,
|
||||
latency: 0,
|
||||
};
|
||||
}
|
||||
this.#emulatedNetworkConditions.upload = networkConditions
|
||||
? networkConditions.upload
|
||||
: -1;
|
||||
this.#emulatedNetworkConditions.download = networkConditions
|
||||
? networkConditions.download
|
||||
: -1;
|
||||
this.#emulatedNetworkConditions.latency = networkConditions
|
||||
? networkConditions.latency
|
||||
: 0;
|
||||
return await this.#applyNetworkConditions();
|
||||
}
|
||||
|
||||
async #applyNetworkConditions(): Promise<void> {
|
||||
if (!this.#emulatedNetworkConditions) {
|
||||
return;
|
||||
}
|
||||
await this._client().send('Network.emulateNetworkConditions', {
|
||||
offline: this.#emulatedNetworkConditions.offline,
|
||||
latency: this.#emulatedNetworkConditions.latency,
|
||||
uploadThroughput: this.#emulatedNetworkConditions.upload,
|
||||
downloadThroughput: this.#emulatedNetworkConditions.download,
|
||||
});
|
||||
}
|
||||
|
||||
override async setCookie(...cookies: CookieParam[]): Promise<void> {
|
||||
|
@ -120,7 +120,7 @@ export class BidiSerializer {
|
||||
}
|
||||
|
||||
throw new UnserializableError(
|
||||
'Custom object sterilization not possible. Use plain objects instead.'
|
||||
'Custom object serialization not possible. Use plain objects instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +122,16 @@ export class BrowsingContext extends EventEmitter<{
|
||||
userContext: UserContext,
|
||||
parent: BrowsingContext | undefined,
|
||||
id: string,
|
||||
url: string
|
||||
url: string,
|
||||
originalOpener: string | null
|
||||
): BrowsingContext {
|
||||
const browsingContext = new BrowsingContext(userContext, parent, id, url);
|
||||
const browsingContext = new BrowsingContext(
|
||||
userContext,
|
||||
parent,
|
||||
id,
|
||||
url,
|
||||
originalOpener
|
||||
);
|
||||
browsingContext.#initialize();
|
||||
return browsingContext;
|
||||
}
|
||||
@ -140,12 +147,14 @@ export class BrowsingContext extends EventEmitter<{
|
||||
readonly id: string;
|
||||
readonly parent: BrowsingContext | undefined;
|
||||
readonly userContext: UserContext;
|
||||
readonly originalOpener: string | null;
|
||||
|
||||
private constructor(
|
||||
context: UserContext,
|
||||
parent: BrowsingContext | undefined,
|
||||
id: string,
|
||||
url: string
|
||||
url: string,
|
||||
originalOpener: string | null
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -153,6 +162,7 @@ export class BrowsingContext extends EventEmitter<{
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
this.userContext = context;
|
||||
this.originalOpener = originalOpener;
|
||||
|
||||
this.defaultRealm = this.#createWindowRealm();
|
||||
}
|
||||
@ -177,7 +187,8 @@ export class BrowsingContext extends EventEmitter<{
|
||||
this.userContext,
|
||||
this,
|
||||
info.context,
|
||||
info.url
|
||||
info.url,
|
||||
info.originalOpener
|
||||
);
|
||||
this.#children.set(info.context, browsingContext);
|
||||
|
||||
@ -219,7 +230,8 @@ export class BrowsingContext extends EventEmitter<{
|
||||
if (info.context !== this.id) {
|
||||
return;
|
||||
}
|
||||
this.#url = info.url;
|
||||
// Note: we should not update this.#url at this point since the context
|
||||
// has not finished navigating to the info.url yet.
|
||||
|
||||
for (const [id, request] of this.#requests) {
|
||||
if (request.disposed) {
|
||||
@ -252,8 +264,9 @@ export class BrowsingContext extends EventEmitter<{
|
||||
if (event.context !== this.id) {
|
||||
return;
|
||||
}
|
||||
if (event.redirectCount !== 0) {
|
||||
if (this.#requests.has(event.request.request)) {
|
||||
// Means the request is a redirect. This is handled in Request.
|
||||
// Or an Auth event was issued
|
||||
return;
|
||||
}
|
||||
|
||||
@ -404,6 +417,18 @@ export class BrowsingContext extends EventEmitter<{
|
||||
});
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
})
|
||||
async setCacheBehavior(cacheBehavior: 'default' | 'bypass'): Promise<void> {
|
||||
// @ts-expect-error not in BiDi types yet.
|
||||
await this.#session.send('network.setCacheBehavior', {
|
||||
contexts: [this.id],
|
||||
cacheBehavior,
|
||||
});
|
||||
}
|
||||
|
||||
@throwIfDisposed<BrowsingContext>(context => {
|
||||
// SAFETY: Disposal implies this exists.
|
||||
return context.#reason!;
|
||||
|
@ -7,7 +7,6 @@
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||
import {debugError} from '../../common/util.js';
|
||||
import {
|
||||
bubble,
|
||||
inertIfDisposed,
|
||||
@ -52,29 +51,9 @@ export class Session
|
||||
// throw new Error(status.message);
|
||||
// }
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = (
|
||||
await connection.send('session.new', {
|
||||
capabilities,
|
||||
})
|
||||
).result;
|
||||
} catch (err) {
|
||||
// Chrome does not support session.new.
|
||||
debugError(err);
|
||||
result = {
|
||||
sessionId: '',
|
||||
capabilities: {
|
||||
acceptInsecureCerts: false,
|
||||
browserName: '',
|
||||
browserVersion: '',
|
||||
platformName: '',
|
||||
setWindowRect: false,
|
||||
webSocketUrl: '',
|
||||
userAgent: '',
|
||||
},
|
||||
} satisfies Bidi.Session.NewResult;
|
||||
}
|
||||
const {result} = await connection.send('session.new', {
|
||||
capabilities,
|
||||
});
|
||||
|
||||
const session = new Session(connection, result);
|
||||
await session.#initialize();
|
||||
|
@ -90,7 +90,8 @@ export class UserContext extends EventEmitter<{
|
||||
this,
|
||||
undefined,
|
||||
info.context,
|
||||
info.url
|
||||
info.url,
|
||||
info.originalOpener
|
||||
);
|
||||
this.#browsingContexts.set(browsingContext.id, browsingContext);
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {ElementHandle} from '../api/ElementHandle.js';
|
||||
import type {Realm} from '../api/Realm.js';
|
||||
|
||||
/**
|
||||
* Represents a Node and the properties of it that are relevant to Accessibility.
|
||||
@ -80,6 +80,14 @@ export interface SerializedAXNode {
|
||||
* Children of this node, if there are any.
|
||||
*/
|
||||
children?: SerializedAXNode[];
|
||||
|
||||
/**
|
||||
* Get an ElementHandle for this AXNode if available.
|
||||
*
|
||||
* If the underlying DOM element has been disposed, the method might return an
|
||||
* error.
|
||||
*/
|
||||
elementHandle(): Promise<ElementHandle | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,20 +129,13 @@ export interface SnapshotOptions {
|
||||
* @public
|
||||
*/
|
||||
export class Accessibility {
|
||||
#client: CDPSession;
|
||||
#realm: Realm;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
updateClient(client: CDPSession): void {
|
||||
this.#client = client;
|
||||
constructor(realm: Realm) {
|
||||
this.#realm = realm;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,15 +181,20 @@ export class Accessibility {
|
||||
options: SnapshotOptions = {}
|
||||
): Promise<SerializedAXNode | null> {
|
||||
const {interestingOnly = true, root = null} = options;
|
||||
const {nodes} = await this.#client.send('Accessibility.getFullAXTree');
|
||||
const {nodes} = await this.#realm.environment.client.send(
|
||||
'Accessibility.getFullAXTree'
|
||||
);
|
||||
let backendNodeId: number | undefined;
|
||||
if (root) {
|
||||
const {node} = await this.#client.send('DOM.describeNode', {
|
||||
objectId: root.id,
|
||||
});
|
||||
const {node} = await this.#realm.environment.client.send(
|
||||
'DOM.describeNode',
|
||||
{
|
||||
objectId: root.id,
|
||||
}
|
||||
);
|
||||
backendNodeId = node.backendNodeId;
|
||||
}
|
||||
const defaultRoot = AXNode.createTree(nodes);
|
||||
const defaultRoot = AXNode.createTree(this.#realm, nodes);
|
||||
let needle: AXNode | null = defaultRoot;
|
||||
if (backendNodeId) {
|
||||
needle = defaultRoot.find(node => {
|
||||
@ -260,13 +266,14 @@ class AXNode {
|
||||
#role: string;
|
||||
#ignored: boolean;
|
||||
#cachedHasFocusableChild?: boolean;
|
||||
#realm: Realm;
|
||||
|
||||
constructor(payload: Protocol.Accessibility.AXNode) {
|
||||
constructor(realm: Realm, payload: Protocol.Accessibility.AXNode) {
|
||||
this.payload = payload;
|
||||
this.#name = this.payload.name ? this.payload.name.value : '';
|
||||
this.#role = this.payload.role ? this.payload.role.value : 'Unknown';
|
||||
this.#ignored = this.payload.ignored;
|
||||
|
||||
this.#realm = realm;
|
||||
for (const property of this.payload.properties || []) {
|
||||
if (property.name === 'editable') {
|
||||
this.#richlyEditable = property.value.value === 'richtext';
|
||||
@ -441,6 +448,14 @@ class AXNode {
|
||||
|
||||
const node: SerializedAXNode = {
|
||||
role: this.#role,
|
||||
elementHandle: async (): Promise<ElementHandle | null> => {
|
||||
if (!this.payload.backendDOMNodeId) {
|
||||
return null;
|
||||
}
|
||||
return (await this.#realm.adoptBackendNode(
|
||||
this.payload.backendDOMNodeId
|
||||
)) as ElementHandle<Element>;
|
||||
},
|
||||
};
|
||||
|
||||
type UserStringProperty =
|
||||
@ -561,10 +576,13 @@ class AXNode {
|
||||
return node;
|
||||
}
|
||||
|
||||
public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode {
|
||||
public static createTree(
|
||||
realm: Realm,
|
||||
payloads: Protocol.Accessibility.AXNode[]
|
||||
): AXNode {
|
||||
const nodeById = new Map<string, AXNode>();
|
||||
for (const payload of payloads) {
|
||||
nodeById.set(payload.nodeId, new AXNode(payload));
|
||||
nodeById.set(payload.nodeId, new AXNode(realm, payload));
|
||||
}
|
||||
for (const node of nodeById.values()) {
|
||||
for (const childId of node.payload.childIds || []) {
|
||||
|
@ -16,15 +16,25 @@ import type {ExecutionContext} from './ExecutionContext.js';
|
||||
export class Binding {
|
||||
#name: string;
|
||||
#fn: (...args: unknown[]) => unknown;
|
||||
constructor(name: string, fn: (...args: unknown[]) => unknown) {
|
||||
#initSource: string;
|
||||
constructor(
|
||||
name: string,
|
||||
fn: (...args: unknown[]) => unknown,
|
||||
initSource: string
|
||||
) {
|
||||
this.#name = name;
|
||||
this.#fn = fn;
|
||||
this.#initSource = initSource;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
get initSource(): string {
|
||||
return this.#initSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context - Context to run the binding in; the context should have
|
||||
* the binding added to it beforehand.
|
||||
|
@ -89,8 +89,9 @@ export class CdpBrowserContext extends BrowserContext {
|
||||
});
|
||||
}
|
||||
|
||||
override newPage(): Promise<Page> {
|
||||
return this.#browser._createPageInContext(this.#id);
|
||||
override async newPage(): Promise<Page> {
|
||||
using _guard = await this.waitForScreenshotOperations();
|
||||
return await this.#browser._createPageInContext(this.#id);
|
||||
}
|
||||
|
||||
override browser(): CdpBrowser {
|
||||
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {CdpFrame} from './Frame.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CdpPreloadScript {
|
||||
/**
|
||||
* This is the ID of the preload script returned by
|
||||
* Page.addScriptToEvaluateOnNewDocument in the main frame.
|
||||
*
|
||||
* Sub-frames would get a different CDP ID because
|
||||
* addScriptToEvaluateOnNewDocument is called for each subframe. But
|
||||
* users only see this ID and subframe IDs are internal to Puppeteer.
|
||||
*/
|
||||
#id: string;
|
||||
#source: string;
|
||||
#frameToId = new WeakMap<CdpFrame, string>();
|
||||
|
||||
constructor(mainFrame: CdpFrame, id: string, source: string) {
|
||||
this.#id = id;
|
||||
this.#source = source;
|
||||
this.#frameToId.set(mainFrame, id);
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.#id;
|
||||
}
|
||||
|
||||
get source(): string {
|
||||
return this.#source;
|
||||
}
|
||||
|
||||
getIdForFrame(frame: CdpFrame): string | undefined {
|
||||
return this.#frameToId.get(frame);
|
||||
}
|
||||
|
||||
setIdForFrame(frame: CdpFrame, identifier: string): void {
|
||||
this.#frameToId.set(frame, identifier);
|
||||
}
|
||||
}
|
@ -122,10 +122,16 @@ export class ChromeTargetManager
|
||||
this,
|
||||
undefined
|
||||
);
|
||||
// Targets from extensions and the browser that will not be
|
||||
// auto-attached. Therefore, we should not add them to
|
||||
// #targetsIdsForInit.
|
||||
const skipTarget =
|
||||
targetInfo.type === 'browser' ||
|
||||
targetInfo.url.startsWith('chrome-extension://');
|
||||
if (
|
||||
(!this.#targetFilterCallback ||
|
||||
this.#targetFilterCallback(targetForFilter)) &&
|
||||
targetInfo.type !== 'browser'
|
||||
!skipTarget
|
||||
) {
|
||||
this.#targetsIdsForInit.add(targetId);
|
||||
}
|
||||
@ -156,6 +162,10 @@ export class ChromeTargetManager
|
||||
await this.#initializeDeferred.valueOrThrow();
|
||||
}
|
||||
|
||||
getChildTargets(target: CdpTarget): ReadonlySet<CdpTarget> {
|
||||
return target._childTargets();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#connection.off('Target.targetCreated', this.#onTargetCreated);
|
||||
this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
|
||||
@ -371,6 +381,12 @@ export class ChromeTargetManager
|
||||
this.#attachedTargetsBySessionId.set(session.id(), target);
|
||||
}
|
||||
|
||||
const parentTarget =
|
||||
parentSession instanceof CDPSession
|
||||
? (parentSession as CdpCDPSession)._target()
|
||||
: null;
|
||||
parentTarget?._addChildTarget(target);
|
||||
|
||||
parentSession.emit(CDPSessionEvent.Ready, session);
|
||||
|
||||
this.#targetsIdsForInit.delete(target._targetId);
|
||||
@ -400,7 +416,7 @@ export class ChromeTargetManager
|
||||
}
|
||||
|
||||
#onDetachedFromTarget = (
|
||||
_parentSession: Connection | CDPSession,
|
||||
parentSession: Connection | CDPSession,
|
||||
event: Protocol.Target.DetachedFromTargetEvent
|
||||
) => {
|
||||
const target = this.#attachedTargetsBySessionId.get(event.sessionId);
|
||||
@ -411,6 +427,9 @@ export class ChromeTargetManager
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentSession instanceof CDPSession) {
|
||||
(parentSession as CdpCDPSession)._target()._removeChildTarget(target);
|
||||
}
|
||||
this.#attachedTargetsByTargetId.delete(target._targetId);
|
||||
this.emit(TargetManagerEvent.TargetGone, target);
|
||||
};
|
||||
|
@ -18,7 +18,6 @@ import type {ConnectionTransport} from '../common/ConnectionTransport.js';
|
||||
import {debug} from '../common/Debug.js';
|
||||
import {TargetCloseError} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
|
||||
|
||||
import {CdpCDPSession} from './CDPSession.js';
|
||||
@ -120,8 +119,9 @@ export class Connection extends EventEmitter<CDPSessionEvents> {
|
||||
sessionId?: string,
|
||||
options?: CommandOptions
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']> {
|
||||
assert(!this.#closed, 'Protocol error: Connection closed.');
|
||||
|
||||
if (this.#closed) {
|
||||
return Promise.reject(new Error('Protocol error: Connection closed.'));
|
||||
}
|
||||
return callbacks.create(method, options?.timeout ?? this.#timeout, id => {
|
||||
const stringifiedMessage = JSON.stringify({
|
||||
method,
|
||||
|
@ -119,6 +119,9 @@ export class Coverage {
|
||||
#jsCoverage: JSCoverage;
|
||||
#cssCoverage: CSSCoverage;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#jsCoverage = new JSCoverage(client);
|
||||
this.#cssCoverage = new CSSCoverage(client);
|
||||
@ -196,6 +199,9 @@ export class JSCoverage {
|
||||
#reportAnonymousScripts = false;
|
||||
#includeRawScriptCoverage = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(client: CDPSession) {
|
||||
this.#client = client;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ export class EmulatedState<T extends {active: boolean}> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class EmulationManager {
|
||||
export class EmulationManager implements ClientProvider {
|
||||
#client: CDPSession;
|
||||
|
||||
#emulatingMobile = false;
|
||||
@ -231,14 +231,24 @@ export class EmulationManager {
|
||||
return this.#javascriptEnabledState.state.javaScriptEnabled;
|
||||
}
|
||||
|
||||
async emulateViewport(viewport: Viewport): Promise<boolean> {
|
||||
await this.#viewportState.setState({
|
||||
viewport,
|
||||
active: true,
|
||||
});
|
||||
async emulateViewport(viewport: Viewport | null): Promise<boolean> {
|
||||
const currentState = this.#viewportState.state;
|
||||
if (!viewport && !currentState.active) {
|
||||
return false;
|
||||
}
|
||||
await this.#viewportState.setState(
|
||||
viewport
|
||||
? {
|
||||
viewport,
|
||||
active: true,
|
||||
}
|
||||
: {
|
||||
active: false,
|
||||
}
|
||||
);
|
||||
|
||||
const mobile = viewport.isMobile || false;
|
||||
const hasTouch = viewport.hasTouch || false;
|
||||
const mobile = viewport?.isMobile || false;
|
||||
const hasTouch = viewport?.hasTouch || false;
|
||||
const reloadNeeded =
|
||||
this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch;
|
||||
this.#emulatingMobile = mobile;
|
||||
@ -253,6 +263,12 @@ export class EmulationManager {
|
||||
viewportState: ViewportState
|
||||
): Promise<void> {
|
||||
if (!viewportState.viewport) {
|
||||
await Promise.all([
|
||||
client.send('Emulation.clearDeviceMetricsOverride'),
|
||||
client.send('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: false,
|
||||
}),
|
||||
]).catch(debugError);
|
||||
return;
|
||||
}
|
||||
const {viewport} = viewportState;
|
||||
|
@ -34,13 +34,15 @@ import type {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {CdpJSHandle} from './JSHandle.js';
|
||||
import {
|
||||
addPageBinding,
|
||||
CDP_BINDING_PREFIX,
|
||||
createEvaluationError,
|
||||
valueFromRemoteObject,
|
||||
} from './utils.js';
|
||||
|
||||
const ariaQuerySelectorBinding = new Binding(
|
||||
'__ariaQuerySelector',
|
||||
ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown
|
||||
ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown,
|
||||
'' // custom init
|
||||
);
|
||||
|
||||
const ariaQuerySelectorAllBinding = new Binding(
|
||||
@ -56,7 +58,8 @@ const ariaQuerySelectorAllBinding = new Binding(
|
||||
},
|
||||
...(await AsyncIterableUtil.collect(results))
|
||||
);
|
||||
}) as (...args: unknown[]) => unknown
|
||||
}) as (...args: unknown[]) => unknown,
|
||||
'' // custom init
|
||||
);
|
||||
|
||||
/**
|
||||
@ -124,16 +127,21 @@ export class ExecutionContext
|
||||
'Runtime.addBinding',
|
||||
this.#name
|
||||
? {
|
||||
name: binding.name,
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
executionContextName: this.#name,
|
||||
}
|
||||
: {
|
||||
name: binding.name,
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
executionContextId: this.#id,
|
||||
}
|
||||
);
|
||||
|
||||
await this.evaluate(addPageBinding, 'internal', binding.name);
|
||||
await this.evaluate(
|
||||
addPageBinding,
|
||||
'internal',
|
||||
binding.name,
|
||||
CDP_BINDING_PREFIX
|
||||
);
|
||||
|
||||
this.#bindings.set(binding.name, binding);
|
||||
} catch (error) {
|
||||
@ -158,6 +166,10 @@ export class ExecutionContext
|
||||
async #onBindingCalled(
|
||||
event: Protocol.Runtime.BindingCalledEvent
|
||||
): Promise<void> {
|
||||
if (event.executionContextId !== this.#id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: BindingPayload;
|
||||
try {
|
||||
payload = JSON.parse(event.payload);
|
||||
@ -177,10 +189,6 @@ export class ExecutionContext
|
||||
}
|
||||
|
||||
try {
|
||||
if (event.executionContextId !== this.#id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const binding = this.#bindings.get(name);
|
||||
await binding?.run(this, seq, args, isTrivial);
|
||||
} catch (err) {
|
||||
|
@ -30,7 +30,7 @@ const pageTargetInfo = {
|
||||
* implements missing commands and events.
|
||||
*
|
||||
* @experimental
|
||||
* @internal
|
||||
* @public
|
||||
*/
|
||||
export class ExtensionTransport implements ConnectionTransport {
|
||||
static async connectTab(tabId: number): Promise<ExtensionTransport> {
|
||||
@ -178,5 +178,6 @@ export class ExtensionTransport implements ConnectionTransport {
|
||||
|
||||
close(): void {
|
||||
chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler);
|
||||
void chrome.debugger.detach({tabId: this.#tabId});
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,10 @@ export class FirefoxTargetManager
|
||||
return this.#availableTargetsByTargetId;
|
||||
}
|
||||
|
||||
getChildTargets(_target: CdpTarget): ReadonlySet<CdpTarget> {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#connection.off('Target.targetCreated', this.#onTargetCreated);
|
||||
this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
|
||||
|
@ -11,10 +11,14 @@ import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {WaitTimeoutOptions} from '../api/Page.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import {Deferred} from '../util/Deferred.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {Accessibility} from './Accessibility.js';
|
||||
import type {Binding} from './Binding.js';
|
||||
import type {CdpPreloadScript} from './CdpPreloadScript.js';
|
||||
import type {
|
||||
DeviceRequestPrompt,
|
||||
DeviceRequestPromptManager,
|
||||
@ -29,6 +33,7 @@ import {
|
||||
type PuppeteerLifeCycleEvent,
|
||||
} from './LifecycleWatcher.js';
|
||||
import type {CdpPage} from './Page.js';
|
||||
import {CDP_BINDING_PREFIX} from './utils.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -44,6 +49,7 @@ export class CdpFrame extends Frame {
|
||||
|
||||
override _id: string;
|
||||
override _parentId?: string;
|
||||
override accessibility: Accessibility;
|
||||
|
||||
worlds: IsolatedWorldChart;
|
||||
|
||||
@ -70,6 +76,8 @@ export class CdpFrame extends Frame {
|
||||
),
|
||||
};
|
||||
|
||||
this.accessibility = new Accessibility(this.worlds[MAIN_WORLD]);
|
||||
|
||||
this.on(FrameEvent.FrameSwappedByActivation, () => {
|
||||
// Emulate loading process for swapped frames.
|
||||
this._onLoadingStarted();
|
||||
@ -322,6 +330,57 @@ export class CdpFrame extends Frame {
|
||||
}
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
async addPreloadScript(preloadScript: CdpPreloadScript): Promise<void> {
|
||||
if (!this.isOOPFrame() && this !== this._frameManager.mainFrame()) {
|
||||
return;
|
||||
}
|
||||
if (preloadScript.getIdForFrame(this)) {
|
||||
return;
|
||||
}
|
||||
const {identifier} = await this.#client.send(
|
||||
'Page.addScriptToEvaluateOnNewDocument',
|
||||
{
|
||||
source: preloadScript.source,
|
||||
}
|
||||
);
|
||||
preloadScript.setIdForFrame(this, identifier);
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
async addExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
// If a frame has not started loading, it might never start. Rely on
|
||||
// addScriptToEvaluateOnNewDocument in that case.
|
||||
if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
this.#client.send('Runtime.addBinding', {
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
}),
|
||||
this.evaluate(binding.initSource).catch(debugError),
|
||||
]);
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
async removeExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
// If a frame has not started loading, it might never start. Rely on
|
||||
// addScriptToEvaluateOnNewDocument in that case.
|
||||
if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
|
||||
return;
|
||||
}
|
||||
await Promise.all([
|
||||
this.#client.send('Runtime.removeBinding', {
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
}),
|
||||
this.evaluate(name => {
|
||||
// Removes the dangling Puppeteer binding wrapper.
|
||||
// @ts-expect-error: In a different context.
|
||||
globalThis[name] = undefined;
|
||||
}, binding.name).catch(debugError),
|
||||
]);
|
||||
}
|
||||
|
||||
@throwIfDetached
|
||||
override async waitForDevicePrompt(
|
||||
options: WaitTimeoutOptions = {}
|
||||
|
@ -8,6 +8,7 @@ import type {Protocol} from 'devtools-protocol';
|
||||
|
||||
import {type CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
|
||||
import {FrameEvent} from '../api/Frame.js';
|
||||
import type {NewDocumentScriptEvaluation} from '../api/Page.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import {debugError, PuppeteerURL, UTILITY_WORLD_NAME} from '../common/util.js';
|
||||
@ -16,6 +17,8 @@ import {Deferred} from '../util/Deferred.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {Binding} from './Binding.js';
|
||||
import {CdpPreloadScript} from './CdpPreloadScript.js';
|
||||
import {CdpCDPSession} from './CDPSession.js';
|
||||
import {isTargetClosedError} from './Connection.js';
|
||||
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
|
||||
@ -43,6 +46,8 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
#timeoutSettings: TimeoutSettings;
|
||||
#isolatedWorlds = new Set<string>();
|
||||
#client: CDPSession;
|
||||
#scriptsToEvaluateOnNewDocument = new Map<string, CdpPreloadScript>();
|
||||
#bindings = new Set<Binding>();
|
||||
|
||||
_frameTree = new FrameTree<CdpFrame>();
|
||||
|
||||
@ -138,7 +143,7 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
client.once(CDPSessionEvent.Disconnected, () => {
|
||||
this.#onClientDisconnect().catch(debugError);
|
||||
});
|
||||
await this.initialize(client);
|
||||
await this.initialize(client, frame);
|
||||
await this.#networkManager.addClient(client);
|
||||
if (frame) {
|
||||
frame.emit(FrameEvent.FrameSwappedByActivation, undefined);
|
||||
@ -191,7 +196,7 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(client: CDPSession): Promise<void> {
|
||||
async initialize(client: CDPSession, frame?: CdpFrame | null): Promise<void> {
|
||||
try {
|
||||
this.#frameTreeHandled?.resolve();
|
||||
this.#frameTreeHandled = Deferred.create();
|
||||
@ -210,6 +215,15 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
client.send('Runtime.enable').then(() => {
|
||||
return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
|
||||
}),
|
||||
...(frame
|
||||
? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
|
||||
: []
|
||||
).map(script => {
|
||||
return frame?.addPreloadScript(script);
|
||||
}),
|
||||
...(frame ? Array.from(this.#bindings.values()) : []).map(binding => {
|
||||
return frame?.addExposedFunctionBinding(binding);
|
||||
}),
|
||||
]);
|
||||
} catch (error) {
|
||||
this.#frameTreeHandled?.resolve();
|
||||
@ -240,6 +254,76 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
return this._frameTree.getById(frameId) || null;
|
||||
}
|
||||
|
||||
async addExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
this.#bindings.add(binding);
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.addExposedFunctionBinding(binding);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async removeExposedFunctionBinding(binding: Binding): Promise<void> {
|
||||
this.#bindings.delete(binding);
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.removeExposedFunctionBinding(binding);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async evaluateOnNewDocument(
|
||||
source: string
|
||||
): Promise<NewDocumentScriptEvaluation> {
|
||||
const {identifier} = await this.mainFrame()
|
||||
._client()
|
||||
.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source,
|
||||
});
|
||||
|
||||
const preloadScript = new CdpPreloadScript(
|
||||
this.mainFrame(),
|
||||
identifier,
|
||||
source
|
||||
);
|
||||
|
||||
this.#scriptsToEvaluateOnNewDocument.set(identifier, preloadScript);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(async frame => {
|
||||
return await frame.addPreloadScript(preloadScript);
|
||||
})
|
||||
);
|
||||
|
||||
return {identifier};
|
||||
}
|
||||
|
||||
async removeScriptToEvaluateOnNewDocument(identifier: string): Promise<void> {
|
||||
const preloadScript = this.#scriptsToEvaluateOnNewDocument.get(identifier);
|
||||
if (!preloadScript) {
|
||||
throw new Error(
|
||||
`Script to evaluate on new document with id ${identifier} not found`
|
||||
);
|
||||
}
|
||||
|
||||
this.#scriptsToEvaluateOnNewDocument.delete(identifier);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(frame => {
|
||||
const identifier = preloadScript.getIdForFrame(frame);
|
||||
if (!identifier) {
|
||||
return;
|
||||
}
|
||||
return frame
|
||||
._client()
|
||||
.send('Page.removeScriptToEvaluateOnNewDocument', {
|
||||
identifier,
|
||||
})
|
||||
.catch(debugError);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onAttachedToTarget(target: CdpTarget): void {
|
||||
if (target._getTargetInfo().type !== 'iframe') {
|
||||
return;
|
||||
@ -250,7 +334,7 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
frame.updateClient(target._session()!);
|
||||
}
|
||||
this.setupEventListeners(target._session()!);
|
||||
void this.initialize(target._session()!);
|
||||
void this.initialize(target._session()!, frame);
|
||||
}
|
||||
|
||||
_deviceRequestPromptManager(client: CDPSession): DeviceRequestPromptManager {
|
||||
@ -454,10 +538,7 @@ export class FrameManager extends EventEmitter<FrameManagerEvents> {
|
||||
}
|
||||
if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
|
||||
world = frame.worlds[MAIN_WORLD];
|
||||
} else if (
|
||||
contextPayload.name === UTILITY_WORLD_NAME &&
|
||||
!frame.worlds[PUPPETEER_WORLD].hasContext()
|
||||
) {
|
||||
} else if (contextPayload.name === UTILITY_WORLD_NAME) {
|
||||
// In case of multiple sessions to the same target, there's a race between
|
||||
// connections so we might end up creating multiple isolated worlds.
|
||||
// We can use either.
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
STATUS_TEXTS,
|
||||
handleError,
|
||||
} from '../api/HTTPRequest.js';
|
||||
import {debugError, isString} from '../common/util.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
|
||||
import type {CdpHTTPResponse} from './HTTPResponse.js';
|
||||
|
||||
@ -172,9 +172,7 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||
const {url, method, postData, headers} = overrides;
|
||||
this.interception.handled = true;
|
||||
|
||||
const postDataBinaryBase64 = postData
|
||||
? Buffer.from(postData).toString('base64')
|
||||
: undefined;
|
||||
const postDataBinaryBase64 = postData ? btoa(postData) : undefined;
|
||||
|
||||
if (this._interceptionId === undefined) {
|
||||
throw new Error(
|
||||
@ -198,10 +196,15 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||
async _respond(response: Partial<ResponseForRequest>): Promise<void> {
|
||||
this.interception.handled = true;
|
||||
|
||||
const responseBody: Buffer | null =
|
||||
response.body && isString(response.body)
|
||||
? Buffer.from(response.body)
|
||||
: (response.body as Buffer) || null;
|
||||
let parsedBody:
|
||||
| {
|
||||
contentLength: number;
|
||||
base64: string;
|
||||
}
|
||||
| undefined;
|
||||
if (response.body) {
|
||||
parsedBody = HTTPRequest.getResponse(response.body);
|
||||
}
|
||||
|
||||
const responseHeaders: Record<string, string | string[]> = {};
|
||||
if (response.headers) {
|
||||
@ -218,10 +221,8 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||
if (response.contentType) {
|
||||
responseHeaders['content-type'] = response.contentType;
|
||||
}
|
||||
if (responseBody && !('content-length' in responseHeaders)) {
|
||||
responseHeaders['content-length'] = String(
|
||||
Buffer.byteLength(responseBody)
|
||||
);
|
||||
if (parsedBody?.contentLength && !('content-length' in responseHeaders)) {
|
||||
responseHeaders['content-length'] = String(parsedBody.contentLength);
|
||||
}
|
||||
|
||||
const status = response.status || 200;
|
||||
@ -236,7 +237,7 @@ export class CdpHTTPRequest extends HTTPRequest {
|
||||
responseCode: status,
|
||||
responsePhrase: STATUS_TEXTS[status],
|
||||
responseHeaders: headersArray(responseHeaders),
|
||||
body: responseBody ? responseBody.toString('base64') : undefined,
|
||||
body: parsedBody?.base64,
|
||||
})
|
||||
.catch(error => {
|
||||
this.interception.handled = false;
|
||||
|
@ -16,6 +16,7 @@ import type {TimeoutSettings} from '../common/TimeoutSettings.js';
|
||||
import type {EvaluateFunc, HandleFor} from '../common/types.js';
|
||||
import {
|
||||
fromEmitterEvent,
|
||||
timeout,
|
||||
withSourcePuppeteerURLIfNone,
|
||||
} from '../common/util.js';
|
||||
import {disposeSymbol} from '../util/disposable.js';
|
||||
@ -143,7 +144,8 @@ export class IsolatedWorld extends Realm {
|
||||
// The message has to match the CDP message expected by the WaitTask class.
|
||||
throw new Error('Execution context was destroyed');
|
||||
})
|
||||
)
|
||||
),
|
||||
timeout(this.timeoutSettings.timeout())
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -87,6 +87,23 @@ export class CdpJSHandle<T = unknown> extends JSHandle<T> {
|
||||
override remoteObject(): Protocol.Runtime.RemoteObject {
|
||||
return this.#remoteObject;
|
||||
}
|
||||
|
||||
override async getProperties(): Promise<Map<string, JSHandle<unknown>>> {
|
||||
// We use Runtime.getProperties rather than iterative version for
|
||||
// improved performance as it allows getting everything at once.
|
||||
const response = await this.client.send('Runtime.getProperties', {
|
||||
objectId: this.#remoteObject.objectId!,
|
||||
ownProperties: true,
|
||||
});
|
||||
const result = new Map<string, JSHandle>();
|
||||
for (const property of response.result) {
|
||||
if (!property.enumerable || !property.value) {
|
||||
continue;
|
||||
}
|
||||
result.set(property.name, this.#world.createCdpHandle(property.value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +64,7 @@ export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
||||
#frameManager: FrameProvider;
|
||||
#networkEventManager = new NetworkEventManager();
|
||||
#extraHTTPHeaders?: Record<string, string>;
|
||||
#credentials?: Credentials;
|
||||
#credentials: Credentials | null = null;
|
||||
#attemptedAuthentications = new Set<string>();
|
||||
#userRequestInterceptionEnabled = false;
|
||||
#protocolRequestInterceptionEnabled = false;
|
||||
@ -121,7 +121,7 @@ export class NetworkManager extends EventEmitter<NetworkManagerEvents> {
|
||||
this.#clients.delete(client);
|
||||
}
|
||||
|
||||
async authenticate(credentials?: Credentials): Promise<void> {
|
||||
async authenticate(credentials: Credentials | null): Promise<void> {
|
||||
this.#credentials = credentials;
|
||||
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
|
||||
if (enabled === this.#protocolRequestInterceptionEnabled) {
|
||||
|
@ -56,7 +56,6 @@ import {Deferred} from '../util/Deferred.js';
|
||||
import {AsyncDisposableStack} from '../util/disposable.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import {Accessibility} from './Accessibility.js';
|
||||
import {Binding} from './Binding.js';
|
||||
import {CdpCDPSession} from './CDPSession.js';
|
||||
import {isTargetClosedError} from './Connection.js';
|
||||
@ -128,7 +127,6 @@ export class CdpPage extends Page {
|
||||
#keyboard: CdpKeyboard;
|
||||
#mouse: CdpMouse;
|
||||
#touchscreen: CdpTouchscreen;
|
||||
#accessibility: Accessibility;
|
||||
#frameManager: FrameManager;
|
||||
#emulationManager: EmulationManager;
|
||||
#tracing: Tracing;
|
||||
@ -237,7 +235,6 @@ export class CdpPage extends Page {
|
||||
this.#keyboard = new CdpKeyboard(client);
|
||||
this.#mouse = new CdpMouse(client, this.#keyboard);
|
||||
this.#touchscreen = new CdpTouchscreen(client, this.#keyboard);
|
||||
this.#accessibility = new Accessibility(client);
|
||||
this.#frameManager = new FrameManager(client, this, this._timeoutSettings);
|
||||
this.#emulationManager = new EmulationManager(client);
|
||||
this.#tracing = new Tracing(client);
|
||||
@ -302,6 +299,28 @@ export class CdpPage extends Page {
|
||||
.catch(debugError);
|
||||
|
||||
this.#setupPrimaryTargetListeners();
|
||||
this.#attachExistingTargets();
|
||||
}
|
||||
|
||||
#attachExistingTargets(): void {
|
||||
const queue = [];
|
||||
for (const childTarget of this.#targetManager.getChildTargets(
|
||||
this.#primaryTarget
|
||||
)) {
|
||||
queue.push(childTarget);
|
||||
}
|
||||
let idx = 0;
|
||||
while (idx < queue.length) {
|
||||
const next = queue[idx] as CdpTarget;
|
||||
idx++;
|
||||
const session = next._session();
|
||||
if (session) {
|
||||
this.#onAttachedToTarget(session);
|
||||
}
|
||||
for (const childTarget of this.#targetManager.getChildTargets(next)) {
|
||||
queue.push(childTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #onActivation(newSession: CDPSession): Promise<void> {
|
||||
@ -315,7 +334,6 @@ export class CdpPage extends Page {
|
||||
this.#keyboard.updateClient(newSession);
|
||||
this.#mouse.updateClient(newSession);
|
||||
this.#touchscreen.updateClient(newSession);
|
||||
this.#accessibility.updateClient(newSession);
|
||||
this.#emulationManager.updateClient(newSession);
|
||||
this.#tracing.updateClient(newSession);
|
||||
this.#coverage.updateClient(newSession);
|
||||
@ -523,10 +541,6 @@ export class CdpPage extends Page {
|
||||
return this.#tracing;
|
||||
}
|
||||
|
||||
override get accessibility(): Accessibility {
|
||||
return this.#accessibility;
|
||||
}
|
||||
|
||||
override frames(): Frame[] {
|
||||
return this.#frameManager.frames();
|
||||
}
|
||||
@ -666,84 +680,48 @@ export class CdpPage extends Page {
|
||||
`Failed to add page binding with name ${name}: window['${name}'] already exists!`
|
||||
);
|
||||
}
|
||||
|
||||
const source = pageBindingInitString('exposedFun', name);
|
||||
let binding: Binding;
|
||||
switch (typeof pptrFunction) {
|
||||
case 'function':
|
||||
binding = new Binding(
|
||||
name,
|
||||
pptrFunction as (...args: unknown[]) => unknown
|
||||
pptrFunction as (...args: unknown[]) => unknown,
|
||||
source
|
||||
);
|
||||
break;
|
||||
default:
|
||||
binding = new Binding(
|
||||
name,
|
||||
pptrFunction.default as (...args: unknown[]) => unknown
|
||||
pptrFunction.default as (...args: unknown[]) => unknown,
|
||||
source
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
this.#bindings.set(name, binding);
|
||||
|
||||
const expression = pageBindingInitString('exposedFun', name);
|
||||
await this.#primaryTargetClient.send('Runtime.addBinding', {name});
|
||||
// TODO: investigate this as it appears to only apply to the main frame and
|
||||
// local subframes instead of the entire frame tree (including future
|
||||
// frame).
|
||||
const {identifier} = await this.#primaryTargetClient.send(
|
||||
'Page.addScriptToEvaluateOnNewDocument',
|
||||
{
|
||||
source: expression,
|
||||
}
|
||||
);
|
||||
|
||||
const [{identifier}] = await Promise.all([
|
||||
this.#frameManager.evaluateOnNewDocument(source),
|
||||
this.#frameManager.addExposedFunctionBinding(binding),
|
||||
]);
|
||||
this.#exposedFunctions.set(name, identifier);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(frame => {
|
||||
// If a frame has not started loading, it might never start. Rely on
|
||||
// addScriptToEvaluateOnNewDocument in that case.
|
||||
if (frame !== this.mainFrame() && !frame._hasStartedLoading) {
|
||||
return;
|
||||
}
|
||||
return frame.evaluate(expression).catch(debugError);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override async removeExposedFunction(name: string): Promise<void> {
|
||||
const exposedFun = this.#exposedFunctions.get(name);
|
||||
if (!exposedFun) {
|
||||
throw new Error(
|
||||
`Failed to remove page binding with name ${name}: window['${name}'] does not exists!`
|
||||
);
|
||||
const exposedFunctionId = this.#exposedFunctions.get(name);
|
||||
if (!exposedFunctionId) {
|
||||
throw new Error(`Function with name "${name}" does not exist`);
|
||||
}
|
||||
|
||||
await this.#primaryTargetClient.send('Runtime.removeBinding', {name});
|
||||
await this.removeScriptToEvaluateOnNewDocument(exposedFun);
|
||||
|
||||
await Promise.all(
|
||||
this.frames().map(frame => {
|
||||
// If a frame has not started loading, it might never start. Rely on
|
||||
// addScriptToEvaluateOnNewDocument in that case.
|
||||
if (frame !== this.mainFrame() && !frame._hasStartedLoading) {
|
||||
return;
|
||||
}
|
||||
return frame
|
||||
.evaluate(name => {
|
||||
// Removes the dangling Puppeteer binding wrapper.
|
||||
// @ts-expect-error: In a different context.
|
||||
globalThis[name] = undefined;
|
||||
}, name)
|
||||
.catch(debugError);
|
||||
})
|
||||
);
|
||||
|
||||
// #bindings must be updated together with #exposedFunctions.
|
||||
const binding = this.#bindings.get(name)!;
|
||||
this.#exposedFunctions.delete(name);
|
||||
this.#bindings.delete(name);
|
||||
await Promise.all([
|
||||
this.#frameManager.removeScriptToEvaluateOnNewDocument(exposedFunctionId),
|
||||
this.#frameManager.removeExposedFunctionBinding(binding),
|
||||
]);
|
||||
}
|
||||
|
||||
override async authenticate(credentials: Credentials): Promise<void> {
|
||||
override async authenticate(credentials: Credentials | null): Promise<void> {
|
||||
return await this.#frameManager.networkManager.authenticate(credentials);
|
||||
}
|
||||
|
||||
@ -982,7 +960,7 @@ export class CdpPage extends Page {
|
||||
return await this.#emulationManager.emulateVisionDeficiency(type);
|
||||
}
|
||||
|
||||
override async setViewport(viewport: Viewport): Promise<void> {
|
||||
override async setViewport(viewport: Viewport | null): Promise<void> {
|
||||
const needsReload = await this.#emulationManager.emulateViewport(viewport);
|
||||
this.#viewport = viewport;
|
||||
if (needsReload) {
|
||||
@ -1002,24 +980,14 @@ export class CdpPage extends Page {
|
||||
...args: Params
|
||||
): Promise<NewDocumentScriptEvaluation> {
|
||||
const source = evaluationString(pageFunction, ...args);
|
||||
const {identifier} = await this.#primaryTargetClient.send(
|
||||
'Page.addScriptToEvaluateOnNewDocument',
|
||||
{
|
||||
source,
|
||||
}
|
||||
);
|
||||
|
||||
return {identifier};
|
||||
return await this.#frameManager.evaluateOnNewDocument(source);
|
||||
}
|
||||
|
||||
override async removeScriptToEvaluateOnNewDocument(
|
||||
identifier: string
|
||||
): Promise<void> {
|
||||
await this.#primaryTargetClient.send(
|
||||
'Page.removeScriptToEvaluateOnNewDocument',
|
||||
{
|
||||
identifier,
|
||||
}
|
||||
return await this.#frameManager.removeScriptToEvaluateOnNewDocument(
|
||||
identifier
|
||||
);
|
||||
}
|
||||
|
||||
@ -1104,21 +1072,24 @@ export class CdpPage extends Page {
|
||||
omitBackground,
|
||||
tagged: generateTaggedPDF,
|
||||
outline: generateDocumentOutline,
|
||||
waitForFonts,
|
||||
} = parsePDFOptions(options);
|
||||
|
||||
if (omitBackground) {
|
||||
await this.#emulationManager.setTransparentBackgroundColor();
|
||||
}
|
||||
|
||||
await firstValueFrom(
|
||||
from(
|
||||
this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluate(() => {
|
||||
return document.fonts.ready;
|
||||
})
|
||||
).pipe(raceWith(timeout(ms)))
|
||||
);
|
||||
if (waitForFonts) {
|
||||
await firstValueFrom(
|
||||
from(
|
||||
this.mainFrame()
|
||||
.isolatedRealm()
|
||||
.evaluate(() => {
|
||||
return document.fonts.ready;
|
||||
})
|
||||
).pipe(raceWith(timeout(ms)))
|
||||
);
|
||||
}
|
||||
|
||||
const printCommandPromise = this.#primaryTargetClient.send(
|
||||
'Page.printToPDF',
|
||||
@ -1169,6 +1140,7 @@ export class CdpPage extends Page {
|
||||
override async close(
|
||||
options: {runBeforeUnload?: boolean} = {runBeforeUnload: undefined}
|
||||
): Promise<void> {
|
||||
using _guard = await this.browserContext().waitForScreenshotOperations();
|
||||
const connection = this.#primaryTargetClient.connection();
|
||||
assert(
|
||||
connection,
|
||||
|
@ -7,19 +7,31 @@
|
||||
import type {NetworkConditions} from './NetworkManager.js';
|
||||
|
||||
/**
|
||||
* A list of network conditions to be used with
|
||||
* A list of pre-defined network conditions to be used with
|
||||
* {@link Page.emulateNetworkConditions}.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import {PredefinedNetworkConditions} from 'puppeteer';
|
||||
* const slow3G = PredefinedNetworkConditions['Slow 3G'];
|
||||
*
|
||||
* (async () => {
|
||||
* const browser = await puppeteer.launch();
|
||||
* const page = await browser.newPage();
|
||||
* await page.emulateNetworkConditions(slow3G);
|
||||
* await page.emulateNetworkConditions(
|
||||
* PredefinedNetworkConditions['Slow 3G']
|
||||
* );
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.emulateNetworkConditions(
|
||||
* PredefinedNetworkConditions['Fast 3G']
|
||||
* );
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.emulateNetworkConditions(
|
||||
* PredefinedNetworkConditions['Slow 4G']
|
||||
* ); // alias to Fast 3G.
|
||||
* await page.goto('https://www.google.com');
|
||||
* await page.emulateNetworkConditions(
|
||||
* PredefinedNetworkConditions['Fast 4G']
|
||||
* );
|
||||
* await page.goto('https://www.google.com');
|
||||
* // other actions...
|
||||
* await browser.close();
|
||||
@ -29,14 +41,40 @@ import type {NetworkConditions} from './NetworkManager.js';
|
||||
* @public
|
||||
*/
|
||||
export const PredefinedNetworkConditions = Object.freeze({
|
||||
// Generally aligned with DevTools
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/core/sdk/NetworkManager.ts;l=398;drc=225e1240f522ca684473f541ae6dae6cd766dd33.
|
||||
'Slow 3G': {
|
||||
// ~500Kbps down
|
||||
download: ((500 * 1000) / 8) * 0.8,
|
||||
// ~500Kbps up
|
||||
upload: ((500 * 1000) / 8) * 0.8,
|
||||
// 400ms RTT
|
||||
latency: 400 * 5,
|
||||
} as NetworkConditions,
|
||||
'Fast 3G': {
|
||||
// ~1.6 Mbps down
|
||||
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
|
||||
// ~0.75 Mbps up
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
// 150ms RTT
|
||||
latency: 150 * 3.75,
|
||||
} as NetworkConditions,
|
||||
// alias to Fast 3G to align with Lighthouse (crbug.com/342406608)
|
||||
// and DevTools (crbug.com/342406608),
|
||||
'Slow 4G': {
|
||||
// ~1.6 Mbps down
|
||||
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
|
||||
// ~0.75 Mbps up
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
// 150ms RTT
|
||||
latency: 150 * 3.75,
|
||||
} as NetworkConditions,
|
||||
'Fast 4G': {
|
||||
// 9 Mbps down
|
||||
download: ((9 * 1000 * 1000) / 8) * 0.9,
|
||||
// 1.5 Mbps up
|
||||
upload: ((1.5 * 1000 * 1000) / 8) * 0.9,
|
||||
// 60ms RTT
|
||||
latency: 60 * 2.75,
|
||||
} as NetworkConditions,
|
||||
});
|
||||
|
@ -39,7 +39,7 @@ export class CdpTarget extends Target {
|
||||
#sessionFactory:
|
||||
| ((isAutoAttachEmulated: boolean) => Promise<CDPSession>)
|
||||
| undefined;
|
||||
|
||||
#childTargets = new Set<CdpTarget>();
|
||||
_initializedDeferred = Deferred.create<InitializationStatus>();
|
||||
_isClosedDeferred = Deferred.create<void>();
|
||||
_targetId: string;
|
||||
@ -88,6 +88,18 @@ export class CdpTarget extends Target {
|
||||
return this.#session;
|
||||
}
|
||||
|
||||
_addChildTarget(target: CdpTarget): void {
|
||||
this.#childTargets.add(target);
|
||||
}
|
||||
|
||||
_removeChildTarget(target: CdpTarget): void {
|
||||
this.#childTargets.delete(target);
|
||||
}
|
||||
|
||||
_childTargets(): ReadonlySet<CdpTarget> {
|
||||
return this.#childTargets;
|
||||
}
|
||||
|
||||
protected _sessionFactory(): (
|
||||
isAutoAttachEmulated: boolean
|
||||
) => Promise<CDPSession> {
|
||||
|
@ -60,6 +60,7 @@ export interface TargetManagerEvents extends Record<EventType, unknown> {
|
||||
*/
|
||||
export interface TargetManager extends EventEmitter<TargetManagerEvents> {
|
||||
getAvailableTargets(): ReadonlyMap<string, CdpTarget>;
|
||||
getChildTargets(target: CdpTarget): ReadonlySet<CdpTarget>;
|
||||
initialize(): Promise<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ export * from './AriaQueryHandler.js';
|
||||
export * from './Binding.js';
|
||||
export * from './Browser.js';
|
||||
export * from './BrowserConnector.js';
|
||||
export * from './cdp.js';
|
||||
export * from './CDPSession.js';
|
||||
export * from './ChromeTargetManager.js';
|
||||
export * from './Connection.js';
|
||||
|
@ -169,15 +169,16 @@ export function valueFromRemoteObject(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function addPageBinding(type: string, name: string): void {
|
||||
// This is the CDP binding.
|
||||
// @ts-expect-error: In a different context.
|
||||
const callCdp = globalThis[name];
|
||||
|
||||
export function addPageBinding(
|
||||
type: string,
|
||||
name: string,
|
||||
prefix: string
|
||||
): void {
|
||||
// Depending on the frame loading state either Runtime.evaluate or
|
||||
// Page.addScriptToEvaluateOnNewDocument might succeed. Let's check that we
|
||||
// don't re-wrap Puppeteer's binding.
|
||||
if (callCdp[Symbol.toStringTag] === 'PuppeteerBinding') {
|
||||
// @ts-expect-error: In a different context.
|
||||
if (globalThis[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -194,7 +195,9 @@ export function addPageBinding(type: string, name: string): void {
|
||||
callPuppeteer.lastSeq = seq;
|
||||
callPuppeteer.args.set(seq, args);
|
||||
|
||||
callCdp(
|
||||
// @ts-expect-error: In a different context.
|
||||
// Needs to be the same as CDP_BINDING_PREFIX.
|
||||
globalThis[prefix + name](
|
||||
JSON.stringify({
|
||||
type,
|
||||
name,
|
||||
@ -220,13 +223,16 @@ export function addPageBinding(type: string, name: string): void {
|
||||
});
|
||||
},
|
||||
});
|
||||
// @ts-expect-error: In a different context.
|
||||
globalThis[name][Symbol.toStringTag] = 'PuppeteerBinding';
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const CDP_BINDING_PREFIX = 'puppeteer_';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function pageBindingInitString(type: string, name: string): string {
|
||||
return evaluationString(addPageBinding, type, name);
|
||||
return evaluationString(addPageBinding, type, name, CDP_BINDING_PREFIX);
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type PuppeteerUtil from '../injected/injected.js';
|
||||
|
||||
import {QueryHandler} from './QueryHandler.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class CSSQueryHandler extends QueryHandler {
|
||||
static override querySelector = (
|
||||
element: Node,
|
||||
selector: string,
|
||||
{cssQuerySelector}: PuppeteerUtil
|
||||
): Node | null => {
|
||||
return cssQuerySelector(element, selector);
|
||||
};
|
||||
static override querySelectorAll = (
|
||||
element: Node,
|
||||
selector: string,
|
||||
{cssQuerySelectorAll}: PuppeteerUtil
|
||||
): Iterable<Node> => {
|
||||
return cssQuerySelectorAll(element, selector);
|
||||
};
|
||||
}
|
@ -32,10 +32,6 @@ export interface Configuration {
|
||||
* See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path
|
||||
* is inferred.
|
||||
*
|
||||
* Use a specific browser version (e.g., 119.0.6045.105). If you use an alias
|
||||
* such `stable` or `canary` it will only work during the installation of
|
||||
* Puppeteer and it will fail when launching the browser.
|
||||
*
|
||||
* @example 119.0.6045.105
|
||||
* @defaultValue The pinned browser version supported by the current Puppeteer
|
||||
* version.
|
||||
|
@ -62,7 +62,7 @@ export class ConsoleMessage {
|
||||
#stackTraceLocations: ConsoleMessageLocation[];
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @internal
|
||||
*/
|
||||
constructor(
|
||||
type: ConsoleMessageType,
|
||||
|
@ -9,8 +9,7 @@ import type Debug from 'debug';
|
||||
import {isNode} from '../environment.js';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __PUPPETEER_DEBUG: string;
|
||||
const __PUPPETEER_DEBUG: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,10 +6,13 @@
|
||||
|
||||
import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
|
||||
|
||||
import {CSSQueryHandler} from './CSSQueryHandler.js';
|
||||
import {customQueryHandlers} from './CustomQueryHandler.js';
|
||||
import {PierceQueryHandler} from './PierceQueryHandler.js';
|
||||
import {PQueryHandler} from './PQueryHandler.js';
|
||||
import {parsePSelectors} from './PSelectorParser.js';
|
||||
import type {QueryHandler} from './QueryHandler.js';
|
||||
import {PollingOptions} from './QueryHandler.js';
|
||||
import {TextQueryHandler} from './TextQueryHandler.js';
|
||||
import {XPathQueryHandler} from './XPathQueryHandler.js';
|
||||
|
||||
@ -27,6 +30,7 @@ const QUERY_SEPARATORS = ['=', '/'];
|
||||
*/
|
||||
export function getQueryHandlerAndSelector(selector: string): {
|
||||
updatedSelector: string;
|
||||
polling: PollingOptions;
|
||||
QueryHandler: typeof QueryHandler;
|
||||
} {
|
||||
for (const handlerMap of [
|
||||
@ -40,10 +44,38 @@ export function getQueryHandlerAndSelector(selector: string): {
|
||||
const prefix = `${name}${separator}`;
|
||||
if (selector.startsWith(prefix)) {
|
||||
selector = selector.slice(prefix.length);
|
||||
return {updatedSelector: selector, QueryHandler};
|
||||
return {
|
||||
updatedSelector: selector,
|
||||
polling:
|
||||
name === 'aria' ? PollingOptions.RAF : PollingOptions.MUTATION,
|
||||
QueryHandler,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {updatedSelector: selector, QueryHandler: PQueryHandler};
|
||||
try {
|
||||
const [pSelector, isPureCSS, hasPseudoClasses, hasAria] =
|
||||
parsePSelectors(selector);
|
||||
if (isPureCSS) {
|
||||
return {
|
||||
updatedSelector: selector,
|
||||
polling: hasPseudoClasses
|
||||
? PollingOptions.RAF
|
||||
: PollingOptions.MUTATION,
|
||||
QueryHandler: CSSQueryHandler,
|
||||
};
|
||||
}
|
||||
return {
|
||||
updatedSelector: JSON.stringify(pSelector),
|
||||
polling: hasAria ? PollingOptions.RAF : PollingOptions.MUTATION,
|
||||
QueryHandler: PQueryHandler,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
updatedSelector: selector,
|
||||
polling: PollingOptions.MUTATION,
|
||||
QueryHandler: CSSQueryHandler,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -158,6 +158,7 @@ export interface PDFOptions {
|
||||
omitBackground?: boolean;
|
||||
/**
|
||||
* Generate tagged (accessible) PDF.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
* @experimental
|
||||
*/
|
||||
@ -165,20 +166,26 @@ export interface PDFOptions {
|
||||
/**
|
||||
* Generate document outline.
|
||||
*
|
||||
* @remarks
|
||||
* If this is enabled the PDF will also be tagged (accessible)
|
||||
* Currently only works in old Headless (headless = 'shell')
|
||||
* {@link https://issues.chromium.org/issues/41387522#comment48 | Chromium feature request}
|
||||
*
|
||||
* @defaultValue `false`
|
||||
* @experimental
|
||||
*/
|
||||
outline?: boolean;
|
||||
/**
|
||||
* Timeout in milliseconds. Pass `0` to disable timeout.
|
||||
*
|
||||
* The default value can be changed by using {@link Page.setDefaultTimeout}
|
||||
*
|
||||
* @defaultValue `30_000`
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* If true, waits for `document.fonts.ready` to resolve. This might require
|
||||
* activating the page using {@link Page.bringToFront} if the page is in the
|
||||
* background.
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
waitForFonts?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {parsePSelectors} from './PSelectorParser.js';
|
||||
|
||||
describe('PSelectorParser', () => {
|
||||
describe('parsePSelectors', () => {
|
||||
it('parses nested selectors', () => {
|
||||
const [updatedSelector, isPureCSS, hasPseudoClasses] =
|
||||
parsePSelectors('& > div');
|
||||
expect(updatedSelector).toEqual([[['&>div']]]);
|
||||
expect(isPureCSS).toBeTruthy();
|
||||
expect(hasPseudoClasses).toBeFalsy();
|
||||
});
|
||||
|
||||
it('parses nested selectors with p-selector syntax', () => {
|
||||
const [updatedSelector, isPureCSS, hasPseudoClasses] =
|
||||
parsePSelectors('& > div >>> button');
|
||||
expect(updatedSelector).toEqual([[['&>div'], '>>>', ['button']]]);
|
||||
expect(isPureCSS).toBeFalsy();
|
||||
expect(hasPseudoClasses).toBeFalsy();
|
||||
});
|
||||
|
||||
it('parses selectors with pseudo classes', () => {
|
||||
const [updatedSelector, isPureCSS, hasPseudoClasses] =
|
||||
parsePSelectors('div:focus');
|
||||
expect(updatedSelector).toEqual([[['div:focus']]]);
|
||||
expect(isPureCSS).toBeTruthy();
|
||||
expect(hasPseudoClasses).toBeTruthy();
|
||||
});
|
||||
|
||||
it('parses nested selectors with pseudo classes and p-selector syntax', () => {
|
||||
const [updatedSelector, isPureCSS, hasPseudoClasses] = parsePSelectors(
|
||||
'& > div:focus >>>> button:focus'
|
||||
);
|
||||
expect(updatedSelector).toEqual([
|
||||
[['&>div:focus'], '>>>>', ['button:focus']],
|
||||
]);
|
||||
expect(isPureCSS).toBeFalsy();
|
||||
expect(hasPseudoClasses).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('hasAria', () => {
|
||||
it('returns false if no aria query is present', () => {
|
||||
const [, , , hasAria] = parsePSelectors('div:focus');
|
||||
expect(hasAria).toEqual(false);
|
||||
});
|
||||
it('returns true if an aria query is present', () => {
|
||||
const [, , , hasAria] = parsePSelectors(
|
||||
'div:focus >>> ::-p-aria(Text)'
|
||||
);
|
||||
expect(hasAria).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -4,21 +4,20 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {type Token, tokenize, TOKENS, stringify} from 'parsel-js';
|
||||
|
||||
export type CSSSelector = string;
|
||||
export interface PPseudoSelector {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export const enum PCombinator {
|
||||
Descendent = '>>>',
|
||||
Child = '>>>>',
|
||||
}
|
||||
export type CompoundPSelector = Array<CSSSelector | PPseudoSelector>;
|
||||
export type ComplexPSelector = Array<CompoundPSelector | PCombinator>;
|
||||
export type ComplexPSelectorList = ComplexPSelector[];
|
||||
import {
|
||||
type Token,
|
||||
tokenize,
|
||||
TOKENS,
|
||||
stringify,
|
||||
} from '../../third_party/parsel-js/parsel-js.js';
|
||||
import type {
|
||||
ComplexPSelector,
|
||||
ComplexPSelectorList,
|
||||
CompoundPSelector,
|
||||
} from '../injected/PQuerySelector.js';
|
||||
import {PCombinator} from '../injected/PQuerySelector.js';
|
||||
|
||||
TOKENS['nesting'] = /&/g;
|
||||
TOKENS['combinator'] = /\s*(>>>>?|[\s>+~])\s*/g;
|
||||
|
||||
const ESCAPE_REGEXP = /\\[\s\S]/g;
|
||||
@ -34,13 +33,23 @@ const unquote = (text: string): string => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function parsePSelectors(
|
||||
selector: string
|
||||
): [selector: ComplexPSelectorList, isPureCSS: boolean] {
|
||||
): [
|
||||
selector: ComplexPSelectorList,
|
||||
isPureCSS: boolean,
|
||||
hasPseudoClasses: boolean,
|
||||
hasAria: boolean,
|
||||
] {
|
||||
let isPureCSS = true;
|
||||
let hasAria = false;
|
||||
let hasPseudoClasses = false;
|
||||
const tokens = tokenize(selector);
|
||||
if (tokens.length === 0) {
|
||||
return [[], isPureCSS];
|
||||
return [[], isPureCSS, hasPseudoClasses, false];
|
||||
}
|
||||
let compoundSelector: CompoundPSelector = [];
|
||||
let complexSelector: ComplexPSelector = [compoundSelector];
|
||||
@ -81,11 +90,18 @@ export function parsePSelectors(
|
||||
compoundSelector.push(stringify(storage));
|
||||
storage.splice(0);
|
||||
}
|
||||
const name = token.name.slice(3);
|
||||
if (name === 'aria') {
|
||||
hasAria = true;
|
||||
}
|
||||
compoundSelector.push({
|
||||
name: token.name.slice(3),
|
||||
name,
|
||||
value: unquote(token.argument ?? ''),
|
||||
});
|
||||
continue;
|
||||
case 'pseudo-class':
|
||||
hasPseudoClasses = true;
|
||||
break;
|
||||
case 'comma':
|
||||
if (storage.length) {
|
||||
compoundSelector.push(stringify(storage));
|
||||
@ -101,5 +117,5 @@ export function parsePSelectors(
|
||||
if (storage.length) {
|
||||
compoundSelector.push(stringify(storage));
|
||||
}
|
||||
return [selectors, isPureCSS];
|
||||
return [selectors, isPureCSS, hasPseudoClasses, hasAria];
|
||||
}
|
@ -34,6 +34,14 @@ export type QuerySelector = (
|
||||
PuppeteerUtil: PuppeteerUtil
|
||||
) => Awaitable<Node | null>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const enum PollingOptions {
|
||||
RAF = 'raf',
|
||||
MUTATION = 'mutation',
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -139,7 +147,9 @@ export class QueryHandler {
|
||||
static async waitFor(
|
||||
elementOrFrame: ElementHandle<Node> | Frame,
|
||||
selector: string,
|
||||
options: WaitForSelectorOptions
|
||||
options: WaitForSelectorOptions & {
|
||||
polling?: PollingOptions;
|
||||
}
|
||||
): Promise<ElementHandle<Node> | null> {
|
||||
let frame!: Frame;
|
||||
using element = await (async () => {
|
||||
@ -152,6 +162,9 @@ export class QueryHandler {
|
||||
})();
|
||||
|
||||
const {visible = false, hidden = false, timeout, signal} = options;
|
||||
const polling =
|
||||
options.polling ??
|
||||
(visible || hidden ? PollingOptions.RAF : PollingOptions.MUTATION);
|
||||
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
@ -169,7 +182,7 @@ export class QueryHandler {
|
||||
return PuppeteerUtil.checkVisibility(node, visible);
|
||||
},
|
||||
{
|
||||
polling: visible || hidden ? 'raf' : 'mutation',
|
||||
polling,
|
||||
root: element,
|
||||
timeout,
|
||||
signal,
|
||||
|
@ -25,6 +25,7 @@ export * from './PDFOptions.js';
|
||||
export * from './PierceQueryHandler.js';
|
||||
export * from './PQueryHandler.js';
|
||||
export * from './Product.js';
|
||||
export * from './PSelectorParser.js';
|
||||
export * from './Puppeteer.js';
|
||||
export * from './QueryHandler.js';
|
||||
export * from './ScriptInjector.js';
|
||||
|
@ -10,6 +10,7 @@ import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
|
||||
import {
|
||||
filter,
|
||||
from,
|
||||
fromEvent,
|
||||
map,
|
||||
mergeMap,
|
||||
NEVER,
|
||||
@ -362,6 +363,7 @@ export function parsePDFOptions(
|
||||
omitBackground: false,
|
||||
outline: false,
|
||||
tagged: true,
|
||||
waitForFonts: true,
|
||||
};
|
||||
|
||||
let width = 8.5;
|
||||
@ -463,6 +465,27 @@ export function fromEmitterEvent<
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function fromAbortSignal(
|
||||
signal?: AbortSignal,
|
||||
cause?: Error
|
||||
): Observable<never> {
|
||||
return signal
|
||||
? fromEvent(signal, 'abort').pipe(
|
||||
map(() => {
|
||||
if (signal.reason instanceof Error) {
|
||||
signal.reason.cause = cause;
|
||||
throw signal.reason;
|
||||
}
|
||||
|
||||
throw new Error(signal.reason, {cause});
|
||||
})
|
||||
)
|
||||
: NEVER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const cssQuerySelector = (
|
||||
root: Node,
|
||||
selector: string
|
||||
): Element | null => {
|
||||
// @ts-expect-error assume element root
|
||||
return root.querySelector(selector);
|
||||
};
|
||||
export const cssQuerySelectorAll = function (
|
||||
root: Node,
|
||||
selector: string
|
||||
): Iterable<Element> {
|
||||
// @ts-expect-error assume element root
|
||||
return root.querySelectorAll(selector);
|
||||
};
|
@ -9,21 +9,43 @@ import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
|
||||
|
||||
import {ariaQuerySelectorAll} from './ARIAQuerySelector.js';
|
||||
import {customQuerySelectors} from './CustomQuerySelector.js';
|
||||
import {
|
||||
type ComplexPSelector,
|
||||
type ComplexPSelectorList,
|
||||
type CompoundPSelector,
|
||||
type CSSSelector,
|
||||
parsePSelectors,
|
||||
PCombinator,
|
||||
type PPseudoSelector,
|
||||
} from './PSelectorParser.js';
|
||||
import {textQuerySelectorAll} from './TextQuerySelector.js';
|
||||
import {pierce, pierceAll} from './util.js';
|
||||
import {xpathQuerySelectorAll} from './XPathQuerySelector.js';
|
||||
|
||||
const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type CSSSelector = string;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface PPseudoSelector {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const enum PCombinator {
|
||||
Descendent = '>>>',
|
||||
Child = '>>>>',
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type CompoundPSelector = Array<CSSSelector | PPseudoSelector>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type ComplexPSelector = Array<CompoundPSelector | PCombinator>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type ComplexPSelectorList = ComplexPSelector[];
|
||||
|
||||
interface QueryableNode extends Node {
|
||||
querySelectorAll: typeof Document.prototype.querySelectorAll;
|
||||
}
|
||||
@ -32,24 +54,15 @@ const isQueryableNode = (node: Node): node is QueryableNode => {
|
||||
return 'querySelectorAll' in node;
|
||||
};
|
||||
|
||||
class SelectorError extends Error {
|
||||
constructor(selector: string, message: string) {
|
||||
super(`${selector} is not a valid selector: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
class PQueryEngine {
|
||||
#input: string;
|
||||
|
||||
#complexSelector: ComplexPSelector;
|
||||
#compoundSelector: CompoundPSelector = [];
|
||||
#selector: CSSSelector | PPseudoSelector | undefined = undefined;
|
||||
|
||||
elements: AwaitableIterable<Node>;
|
||||
|
||||
constructor(element: Node, input: string, complexSelector: ComplexPSelector) {
|
||||
constructor(element: Node, complexSelector: ComplexPSelector) {
|
||||
this.elements = [element];
|
||||
this.#input = input;
|
||||
this.#complexSelector = complexSelector;
|
||||
this.#next();
|
||||
}
|
||||
@ -71,7 +84,6 @@ class PQueryEngine {
|
||||
|
||||
for (; this.#selector !== undefined; this.#next()) {
|
||||
const selector = this.#selector;
|
||||
const input = this.#input;
|
||||
if (typeof selector === 'string') {
|
||||
// The regular expression tests if the selector is a type/universal
|
||||
// selector. Any other case means we want to apply the selector onto
|
||||
@ -128,10 +140,7 @@ class PQueryEngine {
|
||||
default:
|
||||
const querySelector = customQuerySelectors.get(selector.name);
|
||||
if (!querySelector) {
|
||||
throw new SelectorError(
|
||||
input,
|
||||
`Unknown selector type: ${selector.name}`
|
||||
);
|
||||
throw new Error(`Unknown selector type: ${selector.name}`);
|
||||
}
|
||||
yield* querySelector.querySelectorAll(element, selector.value);
|
||||
}
|
||||
@ -240,17 +249,7 @@ export const pQuerySelectorAll = function (
|
||||
root: Node,
|
||||
selector: string
|
||||
): AwaitableIterable<Node> {
|
||||
let selectors: ComplexPSelectorList;
|
||||
let isPureCSS: boolean;
|
||||
try {
|
||||
[selectors, isPureCSS] = parsePSelectors(selector);
|
||||
} catch (error) {
|
||||
return (root as unknown as QueryableNode).querySelectorAll(selector);
|
||||
}
|
||||
|
||||
if (isPureCSS) {
|
||||
return (root as unknown as QueryableNode).querySelectorAll(selector);
|
||||
}
|
||||
const selectors = JSON.parse(selector) as ComplexPSelectorList;
|
||||
// If there are any empty elements, then this implies the selector has
|
||||
// contiguous combinators (e.g. `>>> >>>>`) or starts/ends with one which we
|
||||
// treat as illegal, similar to existing behavior.
|
||||
@ -267,15 +266,12 @@ export const pQuerySelectorAll = function (
|
||||
});
|
||||
})
|
||||
) {
|
||||
throw new SelectorError(
|
||||
selector,
|
||||
'Multiple deep combinators found in sequence.'
|
||||
);
|
||||
throw new Error('Multiple deep combinators found in sequence.');
|
||||
}
|
||||
|
||||
return domSort(
|
||||
AsyncIterableUtil.flatMap(selectors, selectorParts => {
|
||||
const query = new PQueryEngine(root, selector, selectorParts);
|
||||
const query = new PQueryEngine(root, selectorParts);
|
||||
void query.run();
|
||||
return query.elements;
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ import {Deferred} from '../util/Deferred.js';
|
||||
import {createFunction} from '../util/Function.js';
|
||||
|
||||
import * as ARIAQuerySelector from './ARIAQuerySelector.js';
|
||||
import * as CSSSelector from './CSSSelector.js';
|
||||
import * as CustomQuerySelectors from './CustomQuerySelector.js';
|
||||
import * as PierceQuerySelector from './PierceQuerySelector.js';
|
||||
import {IntervalPoller, MutationPoller, RAFPoller} from './Poller.js';
|
||||
@ -31,6 +32,7 @@ const PuppeteerUtil = Object.freeze({
|
||||
...TextQuerySelector,
|
||||
...util,
|
||||
...XPathQuerySelector,
|
||||
...CSSSelector,
|
||||
Deferred,
|
||||
createFunction,
|
||||
createTextContent,
|
||||
|
@ -120,7 +120,7 @@ export class FirefoxLauncher extends ProductLauncher {
|
||||
|
||||
if (profileArgIndex !== -1) {
|
||||
userDataDir = firefoxArguments[profileArgIndex + 1];
|
||||
if (!userDataDir || !fs.existsSync(userDataDir)) {
|
||||
if (!userDataDir) {
|
||||
throw new Error(`Firefox profile not found at '${userDataDir}'`);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 Google Inc.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import {describe, it, beforeEach, afterEach} from 'node:test';
|
||||
|
||||
import expect from 'expect';
|
||||
import type {WebSocket} from 'ws';
|
||||
import {WebSocketServer} from 'ws';
|
||||
|
||||
import {NodeWebSocketTransport} from './NodeWebSocketTransport.js';
|
||||
|
||||
describe('NodeWebSocketTransport', () => {
|
||||
let wss: WebSocketServer;
|
||||
let transport: NodeWebSocketTransport;
|
||||
let connection: WebSocket;
|
||||
|
||||
beforeEach(async () => {
|
||||
wss = new WebSocketServer({port: 8080});
|
||||
wss.on('connection', c => {
|
||||
connection = c;
|
||||
});
|
||||
transport = await NodeWebSocketTransport.create('ws://127.0.0.1:8080');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
transport.close();
|
||||
wss.close();
|
||||
});
|
||||
|
||||
it('should dispatch messages in order handling microtasks for each message first', async () => {
|
||||
const log: string[] = [];
|
||||
const result = new Promise<void>(resolve => {
|
||||
transport.onmessage = message => {
|
||||
log.push('message received ' + message);
|
||||
return Promise.resolve().then(() => {
|
||||
log.push('microtask1 ' + message);
|
||||
return Promise.resolve().then(() => {
|
||||
log.push('microtask2 ' + message);
|
||||
if (log.length === 6) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
connection.send('m1');
|
||||
connection.send('m2');
|
||||
await result;
|
||||
expect(log).toEqual([
|
||||
'message received m1',
|
||||
'microtask1 m1',
|
||||
'microtask2 m1',
|
||||
'message received m2',
|
||||
'microtask1 m2',
|
||||
'microtask2 m2',
|
||||
]);
|
||||
});
|
||||
});
|
@ -20,6 +20,8 @@ export class NodeWebSocketTransport implements ConnectionTransport {
|
||||
const ws = new NodeWebSocket(url, [], {
|
||||
followRedirects: true,
|
||||
perMessageDeflate: false,
|
||||
// @ts-expect-error https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketaddress-protocols-options
|
||||
allowSynchronousEvents: false,
|
||||
maxPayload: 256 * 1024 * 1024, // 256Mb
|
||||
headers: {
|
||||
'User-Agent': `Puppeteer ${packageVersion}`,
|
||||
@ -41,18 +43,14 @@ export class NodeWebSocketTransport implements ConnectionTransport {
|
||||
constructor(ws: NodeWebSocket) {
|
||||
this.#ws = ws;
|
||||
this.#ws.addEventListener('message', event => {
|
||||
setImmediate(() => {
|
||||
if (this.onmessage) {
|
||||
this.onmessage.call(null, event.data);
|
||||
}
|
||||
});
|
||||
if (this.onmessage) {
|
||||
this.onmessage.call(null, event.data);
|
||||
}
|
||||
});
|
||||
this.#ws.addEventListener('close', () => {
|
||||
setImmediate(() => {
|
||||
if (this.onclose) {
|
||||
this.onclose.call(null);
|
||||
}
|
||||
});
|
||||
if (this.onclose) {
|
||||
this.onclose.call(null);
|
||||
}
|
||||
});
|
||||
// Silently ignore all errors - we don't know what to do with them.
|
||||
this.#ws.addEventListener('error', () => {});
|
||||
|
@ -181,9 +181,6 @@ export abstract class ProductLauncher {
|
||||
cdpConnection,
|
||||
browserCloseCallback,
|
||||
{
|
||||
timeout,
|
||||
protocolTimeout,
|
||||
slowMo,
|
||||
defaultViewport,
|
||||
ignoreHTTPSErrors,
|
||||
}
|
||||
@ -209,7 +206,7 @@ export abstract class ProductLauncher {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (waitForInitialPage && protocol !== 'webDriverBiDi') {
|
||||
if (waitForInitialPage) {
|
||||
await this.waitForPageTarget(browser, timeout);
|
||||
}
|
||||
|
||||
@ -340,14 +337,10 @@ export abstract class ProductLauncher {
|
||||
connection: Connection,
|
||||
closeCallback: BrowserCloseCallback,
|
||||
opts: {
|
||||
timeout: number;
|
||||
protocolTimeout: number | undefined;
|
||||
slowMo: number;
|
||||
defaultViewport: Viewport | null;
|
||||
ignoreHTTPSErrors?: boolean;
|
||||
}
|
||||
): Promise<Browser> {
|
||||
// TODO: use other options too.
|
||||
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
|
||||
const bidiConnection = await BiDi.connectBidiOverCdp(connection, {
|
||||
acceptInsecureCerts: opts.ignoreHTTPSErrors ?? false,
|
||||
@ -388,7 +381,6 @@ export abstract class ProductLauncher {
|
||||
opts.slowMo,
|
||||
opts.protocolTimeout
|
||||
);
|
||||
// TODO: use other options too.
|
||||
return await BiDi.BidiBrowser.create({
|
||||
connection: bidiConnection,
|
||||
closeCallback,
|
||||
|
@ -8,7 +8,7 @@
|
||||
* @internal
|
||||
*/
|
||||
export const PUPPETEER_REVISIONS = Object.freeze({
|
||||
chrome: '125.0.6422.60',
|
||||
'chrome-headless-shell': '125.0.6422.60',
|
||||
chrome: '126.0.6478.126',
|
||||
'chrome-headless-shell': '126.0.6478.126',
|
||||
firefox: 'latest',
|
||||
});
|
||||
|
@ -31,7 +31,16 @@ export function stringifyFunction(fn: (...args: never) => unknown): string {
|
||||
let value = fn.toString();
|
||||
try {
|
||||
new Function(`(${value})`);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (
|
||||
(err as Error).message.includes(
|
||||
`Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`
|
||||
)
|
||||
) {
|
||||
// The content security policy does not allow Function eval. Let's
|
||||
// assume the value might be valid as is.
|
||||
return value;
|
||||
}
|
||||
// This means we might have a function shorthand (e.g. `test(){}`). Let's
|
||||
// try prefixing.
|
||||
let prefix = 'function ';
|
||||
|
@ -12,10 +12,13 @@ import {disposeSymbol} from './disposable.js';
|
||||
export class Mutex {
|
||||
static Guard = class Guard {
|
||||
#mutex: Mutex;
|
||||
constructor(mutex: Mutex) {
|
||||
#onRelease?: () => void;
|
||||
constructor(mutex: Mutex, onRelease?: () => void) {
|
||||
this.#mutex = mutex;
|
||||
this.#onRelease = onRelease;
|
||||
}
|
||||
[disposeSymbol](): void {
|
||||
this.#onRelease?.();
|
||||
return this.#mutex.release();
|
||||
}
|
||||
};
|
||||
@ -24,7 +27,9 @@ export class Mutex {
|
||||
#acquirers: Array<() => void> = [];
|
||||
|
||||
// This is FIFO.
|
||||
async acquire(): Promise<InstanceType<typeof Mutex.Guard>> {
|
||||
async acquire(
|
||||
onRelease?: () => void
|
||||
): Promise<InstanceType<typeof Mutex.Guard>> {
|
||||
if (!this.#locked) {
|
||||
this.#locked = true;
|
||||
return new Mutex.Guard(this);
|
||||
@ -32,7 +37,7 @@ export class Mutex {
|
||||
const deferred = Deferred.create<void>();
|
||||
this.#acquirers.push(deferred.resolve.bind(deferred));
|
||||
await deferred.valueOrThrow();
|
||||
return new Mutex.Guard(this);
|
||||
return new Mutex.Guard(this, onRelease);
|
||||
}
|
||||
|
||||
release(): void {
|
||||
|
@ -87,20 +87,19 @@ describe('decorators', function () {
|
||||
}
|
||||
|
||||
const t = new Test();
|
||||
let a = false;
|
||||
t.on('a', (value: boolean) => {
|
||||
a = value;
|
||||
});
|
||||
const spy = sinon.spy();
|
||||
t.on('a', spy);
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
expect(spy.callCount).toBe(1);
|
||||
expect(spy.calledWithExactly(true)).toBeTruthy();
|
||||
|
||||
// Set a new emitter.
|
||||
t.field = new EventEmitter();
|
||||
a = false;
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
t.field.emit('a', false);
|
||||
expect(spy.callCount).toBe(2);
|
||||
expect(spy.calledWithExactly(false)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not bubble down', () => {
|
||||
@ -110,16 +109,36 @@ describe('decorators', function () {
|
||||
}
|
||||
|
||||
const t = new Test();
|
||||
let a = false;
|
||||
t.field.on('a', (value: boolean) => {
|
||||
a = value;
|
||||
});
|
||||
const spy = sinon.spy();
|
||||
t.field.on('a', spy);
|
||||
|
||||
t.emit('a', true);
|
||||
expect(a).toBeFalsy();
|
||||
expect(spy.callCount).toBe(0);
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
expect(spy.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should be assignable during construction', () => {
|
||||
class Test extends EventEmitter<any> {
|
||||
@bubble()
|
||||
accessor field: EventEmitter<any>;
|
||||
|
||||
constructor(emitter: EventEmitter<any>) {
|
||||
super();
|
||||
this.field = emitter;
|
||||
}
|
||||
}
|
||||
|
||||
const t = new Test(new EventEmitter());
|
||||
const spy = sinon.spy();
|
||||
t.field.on('a', spy);
|
||||
|
||||
t.emit('a', true);
|
||||
expect(spy.callCount).toBe(0);
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(spy.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -142,7 +142,29 @@ export function guarded<T extends object>(
|
||||
}
|
||||
|
||||
const bubbleHandlers = new WeakMap<object, Map<any, any>>();
|
||||
const bubbleInitializer = function <
|
||||
T extends EventType[],
|
||||
This extends EventEmitter<any>,
|
||||
>(this: This, events?: T) {
|
||||
const handlers = bubbleHandlers.get(this) ?? new Map();
|
||||
if (handlers.has(events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler =
|
||||
events !== undefined
|
||||
? (type: EventType, event: unknown) => {
|
||||
if (events.includes(type)) {
|
||||
this.emit(type, event);
|
||||
}
|
||||
}
|
||||
: (type: EventType, event: unknown) => {
|
||||
this.emit(type, event);
|
||||
};
|
||||
|
||||
handlers.set(events, handler);
|
||||
bubbleHandlers.set(this, handlers);
|
||||
};
|
||||
/**
|
||||
* Event emitter fields marked with `bubble` will have their events bubble up
|
||||
* the field owner.
|
||||
@ -155,24 +177,7 @@ export function bubble<T extends EventType[]>(events?: T) {
|
||||
context: ClassAccessorDecoratorContext<This, Value>
|
||||
): ClassAccessorDecoratorResult<This, Value> => {
|
||||
context.addInitializer(function () {
|
||||
const handlers = bubbleHandlers.get(this) ?? new Map();
|
||||
if (handlers.has(events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler =
|
||||
events !== undefined
|
||||
? (type: EventType, event: unknown) => {
|
||||
if (events.includes(type)) {
|
||||
this.emit(type, event);
|
||||
}
|
||||
}
|
||||
: (type: EventType, event: unknown) => {
|
||||
this.emit(type, event);
|
||||
};
|
||||
|
||||
handlers.set(events, handler);
|
||||
bubbleHandlers.set(this, handlers);
|
||||
return bubbleInitializer.apply(this, [events]);
|
||||
});
|
||||
return {
|
||||
set(emitter) {
|
||||
@ -190,15 +195,15 @@ export function bubble<T extends EventType[]>(events?: T) {
|
||||
emitter.on('*', handler);
|
||||
set.call(this, emitter);
|
||||
},
|
||||
// @ts-expect-error -- TypeScript incorrectly types init to require a
|
||||
// return.
|
||||
init(emitter) {
|
||||
if (emitter === undefined) {
|
||||
return;
|
||||
return emitter;
|
||||
}
|
||||
const handler = bubbleHandlers.get(this)!.get(events)!;
|
||||
|
||||
emitter.on('*', handler);
|
||||
bubbleInitializer.apply(this, [events]);
|
||||
|
||||
const handler = bubbleHandlers.get(this)!.get(events)!;
|
||||
emitter.on('*', handler as any);
|
||||
return emitter;
|
||||
},
|
||||
};
|
||||
|
3
remote/test/puppeteer/packages/puppeteer-core/third_party/parsel-js/package.json
vendored
Normal file
3
remote/test/puppeteer/packages/puppeteer-core/third_party/parsel-js/package.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
4
remote/test/puppeteer/packages/puppeteer-core/third_party/parsel-js/parsel-js.ts
vendored
Normal file
4
remote/test/puppeteer/packages/puppeteer-core/third_party/parsel-js/parsel-js.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// esline-disable rulesdir/check-license
|
||||
export {tokenize, TOKENS, stringify} from 'parsel-js';
|
||||
|
||||
export type * from 'parsel-js';
|
@ -29,6 +29,118 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
* puppeteer-core bumped from 21.0.2 to 21.0.3
|
||||
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
|
||||
|
||||
## [22.13.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.12.1...puppeteer-v22.13.0) (2024-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** puppeteer CLI should read the project configuration ([#12730](https://github.com/puppeteer/puppeteer/issues/12730)) ([bca750a](https://github.com/puppeteer/puppeteer/commit/bca750afe204cc3bafb0a34a0f92b0bac5a6a55f))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.12.1 to 22.13.0
|
||||
|
||||
## [22.12.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.12.0...puppeteer-v22.12.1) (2024-06-26)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.12.0 to 22.12.1
|
||||
|
||||
## [22.12.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.11.2...puppeteer-v22.12.0) (2024-06-21)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.11.2 to 22.12.0
|
||||
|
||||
## [22.11.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.11.1...puppeteer-v22.11.2) (2024-06-18)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.11.1 to 22.11.2
|
||||
|
||||
## [22.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.11.0...puppeteer-v22.11.1) (2024-06-17)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.11.0 to 22.11.1
|
||||
|
||||
## [22.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.10.1...puppeteer-v22.11.0) (2024-06-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* roll to Chrome 126.0.6478.55 (r1300313) ([#12572](https://github.com/puppeteer/puppeteer/issues/12572)) ([f5bc2b5](https://github.com/puppeteer/puppeteer/commit/f5bc2b53aea0d159dd2b7f4c7a0f7a8a224ae6e8))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.10.1 to 22.11.0
|
||||
|
||||
## [22.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.10.0...puppeteer-v22.10.1) (2024-06-11)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.10.0 to 22.10.1
|
||||
|
||||
## [22.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.9.0...puppeteer-v22.10.0) (2024-05-24)
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* **puppeteer:** Synchronize puppeteer versions
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* The following workspace dependencies were updated
|
||||
* dependencies
|
||||
* puppeteer-core bumped from 22.9.0 to 22.10.0
|
||||
|
||||
## [22.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v22.8.2...puppeteer-v22.9.0) (2024-05-16)
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "puppeteer",
|
||||
"version": "22.9.0",
|
||||
"version": "22.13.0",
|
||||
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
|
||||
"keywords": [
|
||||
"puppeteer",
|
||||
@ -123,10 +123,10 @@
|
||||
"author": "The Chromium Authors",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cosmiconfig": "9.0.0",
|
||||
"puppeteer-core": "22.9.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"puppeteer-core": "22.13.0",
|
||||
"@puppeteer/browsers": "2.2.3",
|
||||
"devtools-protocol": "0.0.1286932"
|
||||
"devtools-protocol": "0.0.1299070"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.17.15"
|
||||
|
@ -22,8 +22,17 @@ void new CLI({
|
||||
},
|
||||
allowCachePathOverride: false,
|
||||
pinnedBrowsers: {
|
||||
[Browser.CHROME]: PUPPETEER_REVISIONS.chrome,
|
||||
[Browser.FIREFOX]: PUPPETEER_REVISIONS.firefox,
|
||||
[Browser.CHROMEHEADLESSSHELL]: PUPPETEER_REVISIONS['chrome-headless-shell'],
|
||||
[Browser.CHROME]:
|
||||
puppeteer.configuration.browserRevision ||
|
||||
PUPPETEER_REVISIONS['chrome'] ||
|
||||
'latest',
|
||||
[Browser.FIREFOX]:
|
||||
puppeteer.configuration.browserRevision ||
|
||||
PUPPETEER_REVISIONS['firefox'] ||
|
||||
'latest',
|
||||
[Browser.CHROMEHEADLESSSHELL]:
|
||||
puppeteer.configuration.browserRevision ||
|
||||
PUPPETEER_REVISIONS['chrome-headless-shell'] ||
|
||||
'latest',
|
||||
},
|
||||
}).run(process.argv);
|
||||
|
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [0.6.1](https://github.com/puppeteer/puppeteer/compare/testserver-v0.6.0...testserver-v0.6.1) (2024-06-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** bump ws to 8.17.1 ([#12605](https://github.com/puppeteer/puppeteer/issues/12605)) ([49bcb25](https://github.com/puppeteer/puppeteer/commit/49bcb2537e45c903e6c1d5d360b0077f0153c5d2))
|
||||
|
||||
## [0.6.0](https://github.com/puppeteer/puppeteer/compare/testserver-v0.5.0...testserver-v0.6.0) (2022-10-05)
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pptr/testserver",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "testing server",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
@ -28,7 +28,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mime": "3.0.0",
|
||||
"ws": "8.17.0"
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime": "3.0.4"
|
||||
|
@ -1,16 +1 @@
|
||||
[
|
||||
{
|
||||
"testIdPattern": "[pdf.spec] Page.pdf can print to PDF with outline",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "headful", "cdp"],
|
||||
"expectations": ["PASS"],
|
||||
"comment": "fixed in canary"
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[pdf.spec] Page.pdf can print to PDF with outline",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "headless", "cdp"],
|
||||
"expectations": ["PASS"],
|
||||
"comment": "fixed in canary"
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user