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:
Julian Descottes 2024-07-15 10:24:51 +00:00
parent 6133380bc0
commit 43cdd1333e
142 changed files with 5708 additions and 3419 deletions

View File

@ -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/**/*

View File

@ -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"
}

View File

@ -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();
```

View File

@ -0,0 +1,2 @@
out
node_modules

View File

@ -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;
};

View File

@ -0,0 +1,10 @@
{
"name": "Puppeteer in extension",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": ["debugger", "background"]
}

View 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"
}
}
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1 @@
<!doctype html> <title>Playground</title>

View File

@ -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'],
}),
],
};

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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",

View File

@ -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';

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -1,7 +1,8 @@
{
"extends": "<%= tsConfigPath %>",
"compilerOptions": {
"module": "CommonJS",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "tests/",
"outDir": "build/",
"types": ["<%= testRunner %>"]

View File

@ -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',
},
});
});

View File

@ -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`

View File

@ -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!
`);

View File

@ -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)

View File

@ -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}`);
}

View File

@ -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",

View File

@ -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.

View File

@ -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

View File

@ -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;
}
/**

View File

@ -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'));
});
});
});

View File

@ -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;
}

View File

@ -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
*

View File

@ -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 {

View File

@ -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>

View File

@ -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
);

View File

@ -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 {

View File

@ -136,7 +136,7 @@ export class BidiConnection
this.#callbacks.reject(
object.id,
createProtocolError(object),
object.message
`${object.error}: ${object.message}`
);
return;
case 'event':

View File

@ -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) {

View File

@ -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 {

View File

@ -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,
})

View File

@ -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,
},
],
},

View File

@ -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> {

View File

@ -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.'
);
}
}

View File

@ -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!;

View File

@ -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();

View File

@ -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);

View File

@ -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 || []) {

View File

@ -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.

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
};

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -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) {

View File

@ -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});
}
}

View File

@ -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);

View File

@ -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 = {}

View File

@ -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.

View File

@ -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;

View File

@ -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())
)
)
);

View File

@ -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;
}
}
/**

View File

@ -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) {

View File

@ -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,

View File

@ -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,
});

View File

@ -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> {

View File

@ -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;
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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);
};
}

View File

@ -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.

View File

@ -62,7 +62,7 @@ export class ConsoleMessage {
#stackTraceLocations: ConsoleMessageLocation[];
/**
* @public
* @internal
*/
constructor(
type: ConsoleMessageType,

View File

@ -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;
}
/**

View File

@ -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,
};
}
}

View File

@ -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;
}
/**

View File

@ -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);
});
});
});
});

View File

@ -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];
}

View File

@ -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,

View File

@ -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';

View File

@ -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
*/

View File

@ -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);
};

View File

@ -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;
})

View File

@ -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,

View File

@ -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}'`);
}

View File

@ -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',
]);
});
});

View File

@ -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', () => {});

View File

@ -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,

View File

@ -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',
});

View File

@ -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 ';

View File

@ -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 {

View File

@ -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);
});
});
});

View File

@ -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;
},
};

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -0,0 +1,4 @@
// esline-disable rulesdir/check-license
export {tokenize, TOKENS, stringify} from 'parsel-js';
export type * from 'parsel-js';

View File

@ -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)

View File

@ -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"

View File

@ -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);

View File

@ -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)

View File

@ -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"

View File

@ -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