Bug 1862701 - [remote] Sync to puppeteer version v21.5.2. r=webdriver-reviewers,Sasha

Differential Revision: https://phabricator.services.mozilla.com/D194190
This commit is contained in:
Henrik Skupin 2023-11-21 22:29:39 +00:00
parent 3037fef913
commit 1cd37ca581
321 changed files with 16611 additions and 21637 deletions

View File

@ -143,7 +143,8 @@ _OPT\.OBJ/
^remote/test/puppeteer/test/output-firefox
^remote/test/puppeteer/test/output-chromium
^remote/test/puppeteer/testserver/lib/
^remote/test/puppeteer/utils/mochaRunner/lib/
^remote/test/puppeteer/tools/internal/
`remote/test/puppeteer/tools/mocha-runner/bin/
^remote/test/puppeteer/website
^third_party/js/PKI.js/node_modules/

2
remote/.gitignore vendored
View File

@ -18,4 +18,6 @@ test/puppeteer/src/generated
test/puppeteer/test/**/build
test/puppeteer/test/output-firefox
test/puppeteer/test/output-chromium
test/puppeteer/tools/internal/
test/puppeteer/tools/mocha-runner/bin/
test/puppeteer/website

View File

@ -5,6 +5,7 @@ node_modules
# Production
build/
lib/
bin/
# Generated files
**/*.tsbuildinfo

View File

@ -1,6 +1,25 @@
const {readdirSync} = require('fs');
const {join} = require('path');
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = 'tools/eslint/lib';
function getThirdPartyPackages() {
return readdirSync(join(__dirname, 'packages/puppeteer-core/third_party'), {
withFileTypes: true,
})
.filter(dirent => {
return dirent.isDirectory();
})
.map(({name}) => {
return {
name,
message: `Import \`${name}\` from the vendored location: third_party/${name}/index.js`,
};
});
}
module.exports = {
root: true,
env: {
@ -12,7 +31,13 @@ module.exports = {
plugins: ['mocha', '@typescript-eslint', 'import'],
extends: ['plugin:prettier/recommended'],
extends: ['plugin:prettier/recommended', 'plugin:import/typescript'],
settings: {
'import/resolver': {
typescript: true,
},
},
rules: {
// Brackets keep code readable.
@ -98,21 +123,6 @@ module.exports = {
// ensure we don't have any it.only or describe.only in prod
'mocha/no-exclusive-tests': 'error',
'no-restricted-imports': [
'error',
{
patterns: ['*Events', '*.test.js'],
paths: [
{
name: 'mitt',
message:
'Import `mitt` from the vendored location: third_party/mitt/index.js',
},
],
},
],
'import/extensions': ['error', 'ignorePackages'],
'import/order': [
'error',
{
@ -121,6 +131,8 @@ module.exports = {
},
],
'import/no-cycle': ['error', {maxDepth: Infinity}],
'no-restricted-syntax': [
'error',
// Don't allow underscored declarations on camelCased variables/properties.
@ -145,6 +157,8 @@ module.exports = {
'rulesdir/prettier-comments': 'error',
// Enforces clean up of used resources.
'rulesdir/use-using': 'error',
// Enforces consistent file extension
'rulesdir/extensions': 'error',
// Brackets keep code readable.
curly: ['error', 'all'],
// Brackets keep code readable and `return` intentions clear.
@ -156,7 +170,7 @@ module.exports = {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{argsIgnorePattern: '^_'},
{argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
],
'func-call-spacing': 'off',
'@typescript-eslint/func-call-spacing': 'error',
@ -220,12 +234,30 @@ module.exports = {
'@typescript-eslint/prefer-ts-expect-error': 'error',
// This is more performant; see https://v8.dev/blog/fast-async.
'@typescript-eslint/return-await': ['error', 'always'],
// This optimizes the dependency tracking for type-only files.
'@typescript-eslint/consistent-type-imports': 'error',
// So type-only exports get elided.
'@typescript-eslint/consistent-type-exports': 'error',
// Don't want to trigger unintended side-effects.
'@typescript-eslint/no-import-type-side-effects': 'error',
},
overrides: [
{
files: 'packages/puppeteer-core/src/**/*.ts',
rules: {
'no-restricted-imports': [
'error',
{
patterns: ['*Events', '*.test.js'],
paths: [...getThirdPartyPackages()],
},
],
},
},
{
files: [
'packages/puppeteer-core/src/**/*.test.ts',
'tools/mochaRunner/src/test.ts',
'tools/mocha-runner/src/test.ts',
],
rules: {
// With the Node.js test runner, `describe` and `it` are technically

View File

@ -22,10 +22,14 @@ module.exports = {
reporter: 'dot',
logLevel: 'debug',
require: ['./test/build/mocha-utils.js', 'source-map-support/register'],
spec: 'test/build/**/*.spec.js',
exit: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
parallel: !!process.env.PARALLEL,
timeout: timeout,
reporter: process.env.CI ? 'spec' : 'dot',
// This should make mocha crash on uncaught errors.
// See https://github.com/mochajs/mocha/blob/master/docs/index.md#--allow-uncaught.
allowUncaught: true,
// See https://github.com/mochajs/mocha/blob/master/docs/index.md#--async-only--a.
asyncOnly: true,
};

View File

@ -5,6 +5,7 @@ node_modules
# Production
build/
lib/
bin/
# Generated files
**/*.tsbuildinfo

View File

@ -1,7 +1,7 @@
{
"packages/puppeteer": "21.2.0",
"packages/puppeteer-core": "21.2.0",
"packages/puppeteer": "21.5.2",
"packages/puppeteer-core": "21.5.2",
"packages/testserver": "0.6.0",
"packages/ng-schematics": "0.5.0",
"packages/browsers": "1.7.0"
"packages/ng-schematics": "0.5.1",
"packages/browsers": "1.8.0"
}

View File

@ -0,0 +1,93 @@
import {copyFile, readFile, writeFile} from 'fs/promises';
import {execa} from 'execa';
import {task} from 'hereby';
import semver from 'semver';
import {docgen, spliceIntoSection} from '@puppeteer/docgen';
export const docsNgSchematicsTask = task({
name: 'docs:ng-schematics',
run: async () => {
const readme = await readFile('packages/ng-schematics/README.md', 'utf-8');
const index = await readFile('docs/integrations/ng-schematics.md', 'utf-8');
await writeFile(
'docs/integrations/ng-schematics.md',
index.replace('# API Reference\n', readme)
);
},
});
/**
* This logic should match the one in `website/docusaurus.config.js`.
*/
function getApiUrl(version) {
if (semver.gte(version, '19.3.0')) {
return `https://github.com/puppeteer/puppeteer/blob/puppeteer-${version}/docs/api/index.md`;
} else if (semver.gte(version, '15.3.0')) {
return `https://github.com/puppeteer/puppeteer/blob/${version}/docs/api/index.md`;
} else {
return `https://github.com/puppeteer/puppeteer/blob/${version}/docs/api.md`;
}
}
export const docsChromiumSupportTask = task({
name: 'docs:chromium-support',
run: async () => {
const content = await readFile('docs/chromium-support.md', {
encoding: 'utf8',
});
const {versionsPerRelease} = await import('./versions.js');
const buffer = [];
for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) {
if (puppeteerVersion === 'NEXT') {
continue;
}
if (semver.gte(puppeteerVersion, '20.0.0')) {
buffer.push(
` * [Chrome for Testing](https://goo.gle/chrome-for-testing) ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](${getApiUrl(
puppeteerVersion
)})`
);
} else {
buffer.push(
` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](${getApiUrl(
puppeteerVersion
)})`
);
}
}
await writeFile(
'docs/chromium-support.md',
spliceIntoSection('version', content, buffer.join('\n'))
);
},
});
export const docsTask = task({
name: 'docs',
dependencies: [docsNgSchematicsTask, docsChromiumSupportTask],
run: async () => {
// Copy main page.
await copyFile('README.md', 'docs/index.md');
// Generate documentation
for (const [name, folder] of [
['browsers', 'browsers-api'],
['puppeteer', 'api'],
]) {
docgen(`docs/${name}.api.json`, `docs/${folder}`);
}
// Update main @puppeteer/browsers page.
const readme = await readFile('packages/browsers/README.md', 'utf-8');
const index = await readFile('docs/browsers-api/index.md', 'utf-8');
await writeFile(
'docs/browsers-api/index.md',
index.replace('# API Reference', readme)
);
// Format everything.
await execa('prettier', ['--ignore-path', 'none', '--write', 'docs']);
},
});

View File

@ -90,7 +90,7 @@ information.
#### `puppeteer-core`
Every release since v1.7.0 we publish two packages:
For every release since v1.7.0 we publish two packages:
- [`puppeteer`](https://www.npmjs.com/package/puppeteer)
- [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core)
@ -110,7 +110,7 @@ You should use `puppeteer-core` if you are
or [managing browsers yourself](https://pptr.dev/browsers-api/).
If you are managing browsers yourself, you will need to call
[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with
an an explicit
an explicit
[`executablePath`](https://pptr.dev/api/puppeteer.launchoptions)
(or [`channel`](https://pptr.dev/api/puppeteer.launchoptions) if it's
installed in a standard location).

View File

@ -1,6 +1,6 @@
# Running the examples
Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, the examples can be run from the root folder like so:
Assuming you have a checkout of the Puppeteer repo and have run `npm i` (or `yarn`) to install the dependencies, and `npm run build` (or `yarn run build`) to build the project, the examples can be run from the root folder like so:
```bash
NODE_PATH=../ node examples/search.js
@ -12,7 +12,7 @@ More complex and use case driven examples can be found at [github.com/GoogleChro
# Other resources
> Other useful tools, articles, and projects that use Puppeteer.
Other useful tools, articles, and projects that use Puppeteer.
## Rendering and web scraping

View File

@ -5,6 +5,6 @@ origin:
description: Headless Chrome Node API
license: Apache-2.0
name: puppeteer
release: puppeteer-v21.2.0
url: https://github.com/puppeteer/puppeteer.git
release: puppeteer-v21.5.2
url: ../puppeteer
schema: 1

File diff suppressed because it is too large Load Diff

View File

@ -7,35 +7,35 @@
},
"scripts": {
"build": "wireit",
"build:docs": "wireit",
"check:pinned-deps": "tsx tools/ensure-pinned-deps",
"check": "npm run check --workspaces --if-present && run-p check:*",
"check:pinned-deps": "tsx tools/ensure-pinned-deps",
"clean": "npm run clean --workspaces --if-present",
"debug": "mocha --inspect-brk",
"docs": "run-s build:docs generate:markdown",
"format:eslint": "eslint --ext js --ext ts --fix .",
"format:prettier": "prettier --write .",
"format:expectations": "node tools/sort-test-expectations.js",
"docs": "wireit",
"doctest": "wireit",
"format": "run-s format:*",
"generate:markdown": "tsx tools/generate_docs.ts",
"lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"format:eslint": "eslint --ext js --ext ts --fix .",
"format:expectations": "node tools/sort-test-expectations.mjs",
"format:prettier": "prettier --write .",
"lint": "run-s lint:*",
"lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet . || eslint --ext js --ext ts .)",
"lint:prettier": "prettier --check .",
"lint": "run-s lint:prettier lint:eslint",
"lint:expectations": "node tools/sort-test-expectations.mjs --lint",
"postinstall": "npm run postinstall --workspaces --if-present",
"prepare": "npm run prepare --workspaces --if-present",
"test": "wireit",
"test-install": "npm run test --workspace @puppeteer-test/installation",
"test-types": "tsd -t packages/puppeteer",
"test:chrome:headful": "wireit",
"test:chrome:new-headless": "wireit",
"test:chrome:headless": "wireit",
"test-types": "wireit",
"test:chrome": "wireit",
"test:chrome:bidi": "wireit",
"test:chrome:bidi-local": "wireit",
"test:chrome": "wireit",
"test:chrome:headful": "wireit",
"test:chrome:headless": "wireit",
"test:chrome:new-headless": "wireit",
"test:firefox": "wireit",
"test:firefox:bidi": "wireit",
"test:firefox:headful": "wireit",
"test:firefox:headless": "wireit",
"test:firefox": "wireit",
"test": "wireit",
"validate-licenses": "tsx tools/third_party/validate-licenses.ts",
"unit": "npm run unit --workspaces --if-present"
},
@ -51,13 +51,36 @@
"./test/installation:build"
]
},
"build:docs": {
"docs": {
"command": "hereby docs",
"dependencies": [
"./packages/browsers:build:docs",
"./packages/puppeteer:build:docs",
"./packages/puppeteer-core:build:docs"
"./packages/puppeteer-core:build:docs",
"./tools/docgen:build"
]
},
"doctest": {
"command": "npx ./tools/doctest 'packages/puppeteer-core/lib/esm/**/*.js'",
"dependencies": [
"./packages/puppeteer-core:build",
"./tools/doctest:build"
]
},
"test:chrome": {
"dependencies": [
"test:chrome:bidi",
"test:chrome:headful",
"test:chrome:headless",
"test:chrome:new-headless"
]
},
"test:chrome:bidi": {
"command": "npm test -- --test-suite chrome-bidi"
},
"test:chrome:bidi-local": {
"command": "PUPPETEER_EXECUTABLE_PATH=$(node tools/download_chrome_bidi.mjs ~/.cache/puppeteer/chrome-canary --shell) npm test -- --test-suite chrome-bidi"
},
"test:chrome:headful": {
"command": "npm test -- --test-suite chrome-headful"
},
@ -67,11 +90,8 @@
"test:chrome:new-headless": {
"command": "npm test -- --test-suite chrome-new-headless"
},
"test:chrome:bidi": {
"command": "npm test -- --test-suite chrome-bidi"
},
"test:chrome:bidi-local": {
"command": "PUPPETEER_EXECUTABLE_PATH=$(node tools/download_chrome_bidi.mjs ~/.cache/puppeteer/chrome-canary --shell) npm test -- --test-suite chrome-bidi"
"test:firefox:bidi": {
"command": "npm test -- --test-suite firefox-bidi"
},
"test:firefox:headful": {
"command": "npm test -- --test-suite firefox-headful"
@ -79,102 +99,79 @@
"test:firefox:headless": {
"command": "npm test -- --test-suite firefox-headless"
},
"test:firefox:bidi": {
"command": "npm test -- --test-suite firefox-bidi"
},
"test:chrome": {
"dependencies": [
"test:chrome:headful",
"test:chrome:headless",
"test:chrome:new-headless",
"test:chrome:bidi"
]
},
"test:firefox": {
"dependencies": [
"test:firefox:bidi",
"test:firefox:headful",
"test:firefox:headless",
"test:firefox:bidi"
"test:firefox:headless"
]
},
"test": {
"command": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node tools/mochaRunner/lib/main.js --min-tests 1003",
"command": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 npx ./tools/mocha-runner --min-tests 1003",
"dependencies": [
"./test:build"
"./test:build",
"./tools/mocha-runner:build"
]
},
"test-types": {
"command": "tsd -t packages/puppeteer",
"dependencies": [
"./packages/puppeteer:build"
]
}
},
"devDependencies": {
"@actions/core": "1.10.0",
"@microsoft/api-documenter": "7.22.33",
"@microsoft/api-extractor": "7.36.4",
"@microsoft/api-extractor-model": "7.27.6",
"@actions/core": "1.10.1",
"@microsoft/api-extractor": "7.38.3",
"@pptr/testserver": "file:packages/testserver",
"@prettier/sync": "0.3.0",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-terser": "0.4.3",
"@types/debug": "4.1.8",
"@types/diff": "5.0.3",
"@types/mime": "3.0.1",
"@types/mocha": "10.0.1",
"@types/node": "20.5.9",
"@types/pixelmatch": "5.2.4",
"@types/pngjs": "6.0.1",
"@types/progress": "2.0.5",
"@types/semver": "7.5.1",
"@types/sinon": "10.0.16",
"@types/tar-fs": "2.0.1",
"@types/unbzip2-stream": "1.4.1",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"c8": "8.0.1",
"commonmark": "0.30.0",
"@puppeteer/docgen": "file:tools/docgen",
"@types/mocha": "10.0.4",
"@types/node": "20.8.4",
"@types/semver": "7.5.5",
"@types/sinon": "17.0.1",
"@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0",
"cross-env": "7.0.3",
"diff": "5.1.0",
"esbuild": "0.19.2",
"eslint": "8.48.0",
"esbuild": "0.19.5",
"eslint-config-prettier": "9.0.0",
"eslint-formatter-codeframe": "7.32.1",
"eslint-plugin-import": "2.28.1",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-mocha": "10.2.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-tsdoc": "0.2.17",
"eslint-plugin-unused-imports": "3.0.0",
"esprima": "4.0.1",
"expect": "29.6.4",
"glob": "10.3.4",
"gts": "5.0.1",
"jpeg-js": "0.4.4",
"eslint": "8.53.0",
"execa": "8.0.1",
"expect": "29.7.0",
"gts": "5.2.0",
"hereby": "1.8.8",
"license-checker": "25.0.1",
"mime": "3.0.0",
"minimist": "1.2.8",
"mocha": "10.2.0",
"ncp": "2.0.0",
"npm-run-all": "4.1.5",
"pixelmatch": "5.3.0",
"pngjs": "7.0.0",
"prettier": "3.0.3",
"prettier": "3.1.0",
"puppeteer": "file:packages/puppeteer",
"rollup": "3.28.1",
"rollup-plugin-polyfill-node": "0.12.0",
"semver": "7.5.4",
"sinon": "15.2.0",
"sinon": "17.0.1",
"source-map-support": "0.5.21",
"spdx-satisfies": "5.0.1",
"text-diff": "1.0.1",
"tsd": "0.29.0",
"tsx": "3.12.8",
"tsx": "4.1.2",
"typescript": "5.2.2",
"wireit": "0.13.0",
"zod": "3.22.2"
"wireit": "0.14.1"
},
"overrides": {
"@microsoft/api-extractor": {
"typescript": "$typescript"
}
},
"workspaces": [
"packages/*",
"test",
"test/installation",
"tools/eslint"
"tools/eslint",
"tools/doctest",
"tools/docgen",
"tools/mocha-runner"
]
}

View File

@ -1,5 +1,19 @@
# Changelog
## [1.8.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.7.1...browsers-v1.8.0) (2023-10-20)
### Features
* enable tab targets ([#11099](https://github.com/puppeteer/puppeteer/issues/11099)) ([8324c16](https://github.com/puppeteer/puppeteer/commit/8324c1634883d97ed83f32a1e62acc9b5e64e0bd))
## [1.7.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.7.0...browsers-v1.7.1) (2023-09-13)
### Bug Fixes
* use supported node range for types ([#10896](https://github.com/puppeteer/puppeteer/issues/10896)) ([2d851c1](https://github.com/puppeteer/puppeteer/commit/2d851c1398e5efcdabdb5304dc78e68cbd3fadd2))
## [1.7.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.6.0...browsers-v1.7.0) (2023-08-18)

View File

@ -1,22 +1,19 @@
{
"name": "@puppeteer/browsers",
"version": "1.7.0",
"version": "1.8.0",
"description": "Download and launch browsers",
"scripts": {
"build:docs": "wireit",
"build": "wireit",
"clean": "git clean -Xdf -e '!node_modules' .",
"clean": "../../tools/clean.js",
"test": "wireit"
},
"type": "commonjs",
"bin": "lib/cjs/main-cli.js",
"main": "./lib/cjs/main.js",
"module": "./lib/esm/main.js",
"type": "commonjs",
"exports": {
".": {
"import": "./lib/esm/main.js",
"require": "./lib/cjs/main.js"
}
"import": "./lib/esm/main.js",
"require": "./lib/cjs/main.js"
},
"wireit": {
"build": {
@ -103,10 +100,13 @@
"proxy-agent": "6.3.1",
"tar-fs": "3.0.4",
"unbzip2-stream": "1.4.3",
"yargs": "17.7.1"
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "^16.11.7",
"@types/yargs": "17.0.22"
"@types/debug": "4.1.12",
"@types/progress": "2.0.7",
"@types/tar-fs": "2.0.4",
"@types/unbzip2-stream": "1.4.3",
"@types/yargs": "17.0.31"
}
}

View File

@ -24,9 +24,9 @@ import yargs from 'yargs/yargs';
import {
resolveBuildId,
Browser,
type Browser,
BrowserPlatform,
ChromeReleaseChannel,
type ChromeReleaseChannel,
} from './browser-data/browser-data.js';
import {Cache} from './Cache.js';
import {detectBrowserPlatform} from './detectPlatform.js';

View File

@ -15,10 +15,15 @@
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import {Browser, BrowserPlatform} from './browser-data/browser-data.js';
import {computeExecutablePath} from './launch.js';
import {
Browser,
type BrowserPlatform,
executablePathByBrowser,
} from './browser-data/browser-data.js';
import {detectBrowserPlatform} from './detectPlatform.js';
/**
* @public
@ -27,6 +32,7 @@ export class InstalledBrowser {
browser: Browser;
buildId: string;
platform: BrowserPlatform;
readonly executablePath: string;
#cache: Cache;
@ -43,6 +49,11 @@ export class InstalledBrowser {
this.browser = browser;
this.buildId = buildId;
this.platform = platform;
this.executablePath = cache.computeExecutablePath({
browser,
buildId,
platform,
});
}
/**
@ -56,15 +67,27 @@ export class InstalledBrowser {
this.buildId
);
}
}
get executablePath(): string {
return computeExecutablePath({
cacheDir: this.#cache.rootDir,
platform: this.platform,
browser: this.browser,
buildId: this.buildId,
});
}
/**
* @internal
*/
export interface ComputeExecutablePathOptions {
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue **Auto-detected.**
*/
platform?: BrowserPlatform;
/**
* Determines which browser to launch.
*/
browser: Browser;
/**
* Determines which buildId to download. BuildId should uniquely identify
* binaries and they are used for caching.
*/
buildId: string;
}
/**
@ -159,6 +182,27 @@ export class Cache {
});
});
}
computeExecutablePath(options: ComputeExecutablePathOptions): string {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
);
}
const installationDir = this.installationDir(
options.browser,
options.platform,
options.buildId
);
return path.join(
installationDir,
executablePathByBrowser[options.browser](
options.platform,
options.buildId
)
);
}
}
function parseFolderPath(

View File

@ -24,10 +24,10 @@ import {
BrowserPlatform,
BrowserTag,
ChromeReleaseChannel,
ProfileOptions,
type ProfileOptions,
} from './types.js';
export {ProfileOptions};
export type {ProfileOptions};
export const downloadUrls = {
[Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl,

View File

@ -19,7 +19,7 @@ import path from 'path';
import {getJSON} from '../httpUtil.js';
import {BrowserPlatform, ProfileOptions} from './types.js';
import {BrowserPlatform, type ProfileOptions} from './types.js';
function archive(platform: BrowserPlatform, buildId: string): string {
switch (platform) {
@ -130,6 +130,7 @@ function defaultProfilePreferences(
'browser.safebrowsing.blockedURIs.enabled': false,
'browser.safebrowsing.downloads.enabled': false,
'browser.safebrowsing.malware.enabled': false,
'browser.safebrowsing.passwords.enabled': false,
'browser.safebrowsing.phishing.enabled': false,
// Disable updates to search engines.

View File

@ -14,9 +14,6 @@
* limitations under the License.
*/
import * as chrome from './chrome.js';
import * as firefox from './firefox.js';
/**
* Supported browsers.
*
@ -31,7 +28,7 @@ export enum Browser {
}
/**
* Platform names used to identify a OS platfrom x architecture combination in the way
* Platform names used to identify a OS platform x architecture combination in the way
* that is relevant for the browser download.
*
* @public
@ -44,12 +41,6 @@ export enum BrowserPlatform {
WIN64 = 'win64',
}
export const downloadUrls = {
[Browser.CHROME]: chrome.resolveDownloadUrl,
[Browser.CHROMIUM]: chrome.resolveDownloadUrl,
[Browser.FIREFOX]: firefox.resolveDownloadUrl,
};
/**
* @public
*/

View File

@ -21,8 +21,8 @@ import os from 'os';
import path from 'path';
import {
Browser,
BrowserPlatform,
type Browser,
type BrowserPlatform,
downloadUrls,
} from './browser-data/browser-data.js';
import {Cache, InstalledBrowser} from './Cache.js';

View File

@ -17,15 +17,13 @@
import childProcess from 'child_process';
import {accessSync} from 'fs';
import os from 'os';
import path from 'path';
import readline from 'readline';
import {
Browser,
BrowserPlatform,
executablePathByBrowser,
type Browser,
type BrowserPlatform,
resolveSystemExecutablePath,
ChromeReleaseChannel,
type ChromeReleaseChannel,
} from './browser-data/browser-data.js';
import {Cache} from './Cache.js';
import {debug} from './debug.js';
@ -64,21 +62,7 @@ export interface ComputeExecutablePathOptions {
export function computeExecutablePath(
options: ComputeExecutablePathOptions
): string {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
);
}
const installationDir = new Cache(options.cacheDir).installationDir(
options.browser,
options.platform,
options.buildId
);
return path.join(
installationDir,
executablePathByBrowser[options.browser](options.platform, options.buildId)
);
return new Cache(options.cacheDir).computeExecutablePath(options);
}
/**
@ -196,9 +180,19 @@ export class Process {
dumpio: opts.dumpio,
});
const env = opts.env || {};
debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, {
detached: opts.detached,
env: opts.env,
env: Object.keys(env).reduce<Record<string, string | undefined>>(
(res, key) => {
if (key.toLowerCase().startsWith('puppeteer_')) {
res[key] = env[key];
}
return res;
},
{}
),
stdio,
});
@ -207,7 +201,7 @@ export class Process {
this.#args,
{
detached: opts.detached,
env: opts.env,
env,
stdio,
}
);

View File

@ -14,35 +14,39 @@
* limitations under the License.
*/
export type {
LaunchOptions,
ComputeExecutablePathOptions as Options,
SystemOptions,
} from './launch.js';
export {
launch,
computeExecutablePath,
computeSystemExecutablePath,
TimeoutError,
LaunchOptions,
ComputeExecutablePathOptions as Options,
SystemOptions,
CDP_WEBSOCKET_ENDPOINT_REGEX,
WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX,
Process,
} from './launch.js';
export type {
InstallOptions,
GetInstalledBrowsersOptions,
UninstallOptions,
} from './install.js';
export {
install,
getInstalledBrowsers,
canDownload,
uninstall,
InstallOptions,
GetInstalledBrowsersOptions,
UninstallOptions,
} from './install.js';
export {detectBrowserPlatform} from './detectPlatform.js';
export type {ProfileOptions} from './browser-data/browser-data.js';
export {
resolveBuildId,
Browser,
BrowserPlatform,
ChromeReleaseChannel,
createProfile,
ProfileOptions,
} from './browser-data/browser-data.js';
export {CLI, makeProgressCallback} from './CLI.js';
export {Cache, InstalledBrowser} from './Cache.js';

View File

@ -16,6 +16,6 @@
export const testChromeBuildId = '113.0.5672.0';
export const testChromiumBuildId = '1083080';
export const testFirefoxBuildId = '119.0a1';
export const testFirefoxBuildId = '121.0a1';
export const testChromeDriverBuildId = '115.0.5763.0';
export const testChromeHeadlessShellBuildId = '118.0.5950.0';

View File

@ -1,5 +1,13 @@
# Changelog
## [0.5.1](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.5.0...ng-schematics-v0.5.1) (2023-11-13)
### Bug Fixes
* multi-app project extend root `tsconfig.json` ([#11374](https://github.com/puppeteer/puppeteer/issues/11374)) ([1b2d920](https://github.com/puppeteer/puppeteer/commit/1b2d920fe638f3aad704ab8f21d1e4f4099b6d44))
* support Angular 17 new template ([#11375](https://github.com/puppeteer/puppeteer/issues/11375)) ([64f7bf0](https://github.com/puppeteer/puppeteer/commit/64f7bf0af442369a07352b11555ec3f612eb62b8))
## [0.5.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.4.0...ng-schematics-v0.5.0) (2023-08-22)

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{
"name": "@puppeteer/ng-schematics",
"version": "0.5.0",
"version": "0.5.1",
"description": "Puppeteer Angular schematics",
"scripts": {
"build": "wireit",
"clean": "git clean -Xdf -e '!node_modules' .",
"clean": "../../tools/clean.js",
"dev:test": "npm run test --watch",
"dev": "npm run build --watch",
"sandbox:test": "node tools/sandbox.js --test",
@ -45,17 +45,17 @@
"author": "The Chromium Authors",
"license": "Apache-2.0",
"engines": {
"node": ">=16.3.0"
"node": ">=16.13.2"
},
"dependencies": {
"@angular-devkit/architect": "^0.1602.0",
"@angular-devkit/core": "^16.2.0",
"@angular-devkit/schematics": "^16.2.0"
"@angular-devkit/architect": "^0.1602.10",
"@angular-devkit/core": "^16.2.10",
"@angular-devkit/schematics": "^16.2.10"
},
"devDependencies": {
"@types/node": "^16.11.7",
"@schematics/angular": "^16.2.0",
"@angular/cli": "^16.2.0",
"@types/node": "^16.18.61",
"@schematics/angular": "^16.2.10",
"@angular/cli": "^16.2.10",
"rxjs": "7.8.1"
},
"files": [

View File

@ -2,16 +2,16 @@ import {spawn} from 'child_process';
import {
createBuilder,
BuilderContext,
BuilderOutput,
type BuilderContext,
type BuilderOutput,
targetFromTargetString,
BuilderRun,
type BuilderRun,
} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import type {JsonObject} from '@angular-devkit/core';
import {TestRunner} from '../../schematics/utils/types.js';
import {PuppeteerBuilderOptions} from './types.js';
import type {PuppeteerBuilderOptions} from './types.js';
const terminalStyles = {
cyan: '\u001b[36;1m',

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
import {JsonObject} from '@angular-devkit/core';
import type {JsonObject} from '@angular-devkit/core';
import {TestRunner} from '../../schematics/utils/types.js';
import type {TestRunner} from '../../schematics/utils/types.js';
export interface PuppeteerBuilderOptions extends JsonObject {
testRunner: TestRunner;

View File

@ -14,10 +14,15 @@
* limitations under the License.
*/
import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {
chain,
type Rule,
type SchematicContext,
type Tree,
} from '@angular-devkit/schematics';
import {addFilesSingle} from '../utils/files.js';
import {TestRunner, AngularProject} from '../utils/types.js';
import {TestRunner, type AngularProject} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule
// factory per file.

View File

@ -16,19 +16,19 @@
import {
chain,
Rule,
SchematicContext,
type Rule,
type SchematicContext,
SchematicsException,
Tree,
type Tree,
} from '@angular-devkit/schematics';
import {addCommonFiles} from '../utils/files.js';
import {getApplicationProjects} from '../utils/json.js';
import {
TestRunner,
SchematicsSpec,
AngularProject,
PuppeteerSchematicsConfig,
type SchematicsSpec,
type AngularProject,
type PuppeteerSchematicsConfig,
} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule

View File

@ -10,7 +10,7 @@ describe('App test', function () {
setupBrowserHooks();
it('is running', async function () {
const {page} = getBrowserState();
const element = await page.waitForSelector('text/<%= project %> app is running!');
const element = await page.waitForSelector('text/<%= project %>');
<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
expect(element).not.toBeNull();

View File

@ -14,7 +14,12 @@
* limitations under the License.
*/
import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {
chain,
type Rule,
type SchematicContext,
type Tree,
} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {of} from 'rxjs';
import {concatMap, map, scan} from 'rxjs/operators';

View File

@ -18,8 +18,8 @@ import {relative, resolve} from 'path';
import {getSystemPath, normalize, strings} from '@angular-devkit/core';
import {
SchematicContext,
Tree,
type SchematicContext,
type Tree,
apply,
applyTemplates,
chain,
@ -28,7 +28,7 @@ import {
url,
} from '@angular-devkit/schematics';
import {AngularProject, TestRunner} from './types.js';
import type {AngularProject, TestRunner} from './types.js';
export interface FilesOptions {
options: {
@ -111,10 +111,21 @@ function getProjectBaseUrl(project: any, port: number): string {
}
function getTsConfigPath(project: AngularProject): string {
const filename = 'tsconfig.json';
if (!project.root) {
return '../tsconfig.json';
return `../${filename}`;
}
return `../tsconfig.app.json`;
const nested = project.root
.split('/')
.map(() => {
return '../';
})
.join('');
// Prepend a single `../` as we put the test inside `e2e` folder
return `../${nested}${filename}`;
}
export function addCommonFiles(

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import {SchematicsException, Tree} from '@angular-devkit/schematics';
import {SchematicsException, type Tree} from '@angular-devkit/schematics';
import type {AngularJson, AngularProject} from './types.js';

View File

@ -16,7 +16,7 @@
import {get} from 'https';
import {Tree} from '@angular-devkit/schematics';
import type {Tree} from '@angular-devkit/schematics';
import {getNgCommandName} from './files.js';
import {
@ -25,7 +25,7 @@ import {
getJsonFileAsObject,
getObjectAsJson,
} from './json.js';
import {SchematicsOptions, TestRunner} from './types.js';
import {type SchematicsOptions, TestRunner} from './types.js';
export interface NodePackage {
name: string;
version: string;

View File

@ -93,6 +93,19 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
expect(tree.files).toContain('/e2e/tests/app.test.ts');
expect(options['testRunner']).toBe('node');
});
it('should create TypeScript files', async () => {
const tree = await buildTestingTree('ng-add', 'single');
const tsConfigPath = '/e2e/tsconfig.json';
const tsConfig = tree.readJson(tsConfigPath);
expect(tree.files).toContain(tsConfigPath);
expect(tsConfig).toMatchObject({
extends: '../tsconfig.json',
compilerOptions: {
module: 'CommonJS',
},
});
});
it('should not create port value', async () => {
const tree = await buildTestingTree('ng-add');
@ -193,6 +206,19 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
);
expect(options['testRunner']).toBe('node');
});
it('should create TypeScript files', async () => {
const tree = await buildTestingTree('ng-add', 'multi');
const tsConfigPath = getMultiApplicationFile('e2e/tsconfig.json');
const tsConfig = tree.readJson(tsConfigPath);
expect(tree.files).toContain(tsConfigPath);
expect(tsConfig).toMatchObject({
extends: '../../../tsconfig.json',
compilerOptions: {
module: 'CommonJS',
},
});
});
it('should not create port value', async () => {
const tree = await buildTestingTree('ng-add');

View File

@ -1,10 +1,10 @@
import https from 'https';
import {join} from 'path';
import {JsonObject} from '@angular-devkit/core';
import type {JsonObject} from '@angular-devkit/core';
import {
SchematicTestRunner,
UnitTestTree,
type UnitTestTree,
} from '@angular-devkit/schematics/testing';
import sinon from 'sinon';

View File

@ -20,6 +20,168 @@ 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
## [21.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.5.1...puppeteer-core-v21.5.2) (2023-11-15)
### Bug Fixes
* add --disable-field-trial-config ([#11352](https://github.com/puppeteer/puppeteer/issues/11352)) ([cbc33be](https://github.com/puppeteer/puppeteer/commit/cbc33bea40b8801b8eeb3277fc15d04900715795))
* add --disable-infobars ([#11377](https://github.com/puppeteer/puppeteer/issues/11377)) ([0a41f8d](https://github.com/puppeteer/puppeteer/commit/0a41f8d01e85ff732fdd2e50468bc746d7bc6475))
* mitt types should not be exported ([#11371](https://github.com/puppeteer/puppeteer/issues/11371)) ([4bf2a09](https://github.com/puppeteer/puppeteer/commit/4bf2a09a13450c530b24288d65791fd5c4d4dce7))
## [21.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.5.0...puppeteer-core-v21.5.1) (2023-11-09)
### Bug Fixes
* better debugging for WaitTask ([#11330](https://github.com/puppeteer/puppeteer/issues/11330)) ([d2480b0](https://github.com/puppeteer/puppeteer/commit/d2480b022d74b7071b515408a31c6e82448e3c9e))
## [21.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.4.1...puppeteer-core-v21.5.0) (2023-11-02)
### Features
* roll to Chrome 119.0.6045.105 (r1204232) ([#11287](https://github.com/puppeteer/puppeteer/issues/11287)) ([325fa8b](https://github.com/puppeteer/puppeteer/commit/325fa8b1b16a9dafd5bb320e49984d24044fa3d7))
### Bug Fixes
* ignore unordered frames ([#11283](https://github.com/puppeteer/puppeteer/issues/11283)) ([ce4e485](https://github.com/puppeteer/puppeteer/commit/ce4e485d1b1e9d4e223890ee0fc2475a1ad71bc3))
* Type for ElementHandle.screenshot ([#11274](https://github.com/puppeteer/puppeteer/issues/11274)) ([22aeff1](https://github.com/puppeteer/puppeteer/commit/22aeff1eac9d22048330a16aa3c41293133911e4))
## [21.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.4.0...puppeteer-core-v21.4.1) (2023-10-23)
### Bug Fixes
* do not pass --{enable,disable}-features twice when user-provided ([#11230](https://github.com/puppeteer/puppeteer/issues/11230)) ([edec7d5](https://github.com/puppeteer/puppeteer/commit/edec7d53f8190381ade7db145ad7e7d6dba2ee13))
* remove circular import in IsolatedWorld ([#11228](https://github.com/puppeteer/puppeteer/issues/11228)) ([3edce3a](https://github.com/puppeteer/puppeteer/commit/3edce3aee9521654d7a285f4068a5e60bfb52245))
* remove import cycle ([#11227](https://github.com/puppeteer/puppeteer/issues/11227)) ([525f13c](https://github.com/puppeteer/puppeteer/commit/525f13cd18b39cc951a84aa51b2d852758e6f0d2))
* remove import cycle in connection ([#11225](https://github.com/puppeteer/puppeteer/issues/11225)) ([60f1b78](https://github.com/puppeteer/puppeteer/commit/60f1b788a6304504f504b0be9f02cb768e2803f8))
* remove import cycle in query handlers ([#11234](https://github.com/puppeteer/puppeteer/issues/11234)) ([954c75f](https://github.com/puppeteer/puppeteer/commit/954c75f9a9879e2e68935c17d7eb777b1f9f808a))
* remove more import cycles ([#11231](https://github.com/puppeteer/puppeteer/issues/11231)) ([b9ce89e](https://github.com/puppeteer/puppeteer/commit/b9ce89e460702ad85314685c600a4e5267f4db9b))
* typo in screencast error message ([#11213](https://github.com/puppeteer/puppeteer/issues/11213)) ([25b90b2](https://github.com/puppeteer/puppeteer/commit/25b90b2b542c4693150b67dc0c690b99f4ccfc95))
## [21.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.8...puppeteer-core-v21.4.0) (2023-10-20)
### Features
* added tagged (accessible) PDFs option ([#11182](https://github.com/puppeteer/puppeteer/issues/11182)) ([0316863](https://github.com/puppeteer/puppeteer/commit/031686339136873c555a19ffb871f7140a2c39d9))
* enable tab targets ([#11099](https://github.com/puppeteer/puppeteer/issues/11099)) ([8324c16](https://github.com/puppeteer/puppeteer/commit/8324c1634883d97ed83f32a1e62acc9b5e64e0bd))
* implement screencasting ([#11084](https://github.com/puppeteer/puppeteer/issues/11084)) ([f060d46](https://github.com/puppeteer/puppeteer/commit/f060d467c00457e6be6878e0789d0df2ac4aae50))
* merge user-provided --{disable,enable}-features in args ([#11152](https://github.com/puppeteer/puppeteer/issues/11152)) ([2b578e4](https://github.com/puppeteer/puppeteer/commit/2b578e4a096aa94d792cc2da2da41fee061a77b8)), closes [#11072](https://github.com/puppeteer/puppeteer/issues/11072)
* roll to Chrome 118.0.5993.70 (r1192594) ([#11123](https://github.com/puppeteer/puppeteer/issues/11123)) ([91d14c8](https://github.com/puppeteer/puppeteer/commit/91d14c8c86f5be48c8e0937fd209bea643d60b45))
### Bug Fixes
* `Page.waitForDevicePrompt` crash ([#11153](https://github.com/puppeteer/puppeteer/issues/11153)) ([257be15](https://github.com/puppeteer/puppeteer/commit/257be15d83a46038a65d47977d4d847c54506517))
* add InlineTextBox as a non-element a11y role ([#11142](https://github.com/puppeteer/puppeteer/issues/11142)) ([8aa6cb3](https://github.com/puppeteer/puppeteer/commit/8aa6cb37d2443ff7fe2a1fd5d5adafdde4e9d165))
* disable ProcessPerSiteUpToMainFrameThreshold in Chrome ([#11139](https://github.com/puppeteer/puppeteer/issues/11139)) ([9347aae](https://github.com/puppeteer/puppeteer/commit/9347aae12e996604cea871acc9d007cbf338542e))
* make sure discovery happens before auto-attach ([#11100](https://github.com/puppeteer/puppeteer/issues/11100)) ([9ce204e](https://github.com/puppeteer/puppeteer/commit/9ce204e27ed091bde5aa5bc9f82da41c80534bde))
* synchronize frame tree with the events processing ([#11112](https://github.com/puppeteer/puppeteer/issues/11112)) ([d63f0cf](https://github.com/puppeteer/puppeteer/commit/d63f0cfc61e8ba2233eee8b2f3b99d8619a0acaf))
* update TextQuerySelector cache on subtree update ([#11200](https://github.com/puppeteer/puppeteer/issues/11200)) ([4206e76](https://github.com/puppeteer/puppeteer/commit/4206e76c3e4647ea6290f16127764d1a2f337dcf))
* xpath queries should be atomic ([#11101](https://github.com/puppeteer/puppeteer/issues/11101)) ([6098bab](https://github.com/puppeteer/puppeteer/commit/6098bab2ba68276c85a974e17c9fe3bdac8c4c58))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.7.1 to 1.8.0
## [21.3.8](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.7...puppeteer-core-v21.3.8) (2023-10-06)
### Bug Fixes
* avoid double subscription to frame manager in Page ([#11091](https://github.com/puppeteer/puppeteer/issues/11091)) ([5887649](https://github.com/puppeteer/puppeteer/commit/5887649891ea9cf1d7b3afbcf7196620ceb20ab2))
* update file chooser events ([#11057](https://github.com/puppeteer/puppeteer/issues/11057)) ([317f820](https://github.com/puppeteer/puppeteer/commit/317f82055b2f4dd68db136a3d52c5712425fa339))
## [21.3.7](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.6...puppeteer-core-v21.3.7) (2023-10-05)
### Bug Fixes
* roll to Chrome 117.0.5938.149 (r1181205) ([#11077](https://github.com/puppeteer/puppeteer/issues/11077)) ([0c0e516](https://github.com/puppeteer/puppeteer/commit/0c0e516d736665a27f7773f66a0f9c362daa73aa))
## [21.3.6](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.5...puppeteer-core-v21.3.6) (2023-09-28)
### Bug Fixes
* remove the flag disabling bfcache ([#11047](https://github.com/puppeteer/puppeteer/issues/11047)) ([b0d7375](https://github.com/puppeteer/puppeteer/commit/b0d73755193e7c60deb70df120859b5db87e7817))
## [21.3.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.4...puppeteer-core-v21.3.5) (2023-09-26)
### Bug Fixes
* set defaults in screenshot ([#11021](https://github.com/puppeteer/puppeteer/issues/11021)) ([ace1230](https://github.com/puppeteer/puppeteer/commit/ace1230e41aad6168dc85b9bc1f7c04d9dce5527))
## [21.3.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.3...puppeteer-core-v21.3.4) (2023-09-22)
### Bug Fixes
* avoid structuredClone for Node 16 ([#11006](https://github.com/puppeteer/puppeteer/issues/11006)) ([25eca9a](https://github.com/puppeteer/puppeteer/commit/25eca9a747c122b3096b0f2d01b3323339d57dd9))
## [21.3.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.2...puppeteer-core-v21.3.3) (2023-09-22)
### Bug Fixes
* do not export bidi and fix import from the entrypoint ([#10998](https://github.com/puppeteer/puppeteer/issues/10998)) ([88c78de](https://github.com/puppeteer/puppeteer/commit/88c78dea41eb7690d67343298c150194fe145763))
## [21.3.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.1...puppeteer-core-v21.3.2) (2023-09-22)
### Bug Fixes
* handle missing detach events for restored bfcache targets ([#10967](https://github.com/puppeteer/puppeteer/issues/10967)) ([7bcdfcb](https://github.com/puppeteer/puppeteer/commit/7bcdfcb7e9e75feca0a8de692926ea25ca8fbed0))
* roll to Chrome 117.0.5938.92 (r1181205) ([#10989](https://github.com/puppeteer/puppeteer/issues/10989)) ([d048cd9](https://github.com/puppeteer/puppeteer/commit/d048cd965f0707dd9b2a3276f02c563b69f6fac4))
## [21.3.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.3.0...puppeteer-core-v21.3.1) (2023-09-19)
### Bug Fixes
* make `CDPSessionEvent.SessionAttached` public ([#10941](https://github.com/puppeteer/puppeteer/issues/10941)) ([cfed7b9](https://github.com/puppeteer/puppeteer/commit/cfed7b93ec23e92ec11632f1cd90f00dac754739))
## [21.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.2.1...puppeteer-core-v21.3.0) (2023-09-19)
### Features
* implement `Browser.connected` ([#10927](https://github.com/puppeteer/puppeteer/issues/10927)) ([a4345a4](https://github.com/puppeteer/puppeteer/commit/a4345a477f58541f5d95da11ffee74abe24c12bf))
* implement `BrowserContext.closed` ([#10928](https://github.com/puppeteer/puppeteer/issues/10928)) ([2292078](https://github.com/puppeteer/puppeteer/commit/2292078969fa46a27d5759989cd44a4d48beb310))
* implement improved Drag n' Drop APIs ([#10651](https://github.com/puppeteer/puppeteer/issues/10651)) ([9342bac](https://github.com/puppeteer/puppeteer/commit/9342bac2639702090f39fc1e3a97d43a934f3f0b))
* implement typed events ([#10889](https://github.com/puppeteer/puppeteer/issues/10889)) ([9b6f1de](https://github.com/puppeteer/puppeteer/commit/9b6f1de8b99445c661c5aebcf041fe90daf469b9))
* roll to Chrome 117.0.5938.62 (r1181205) ([#10893](https://github.com/puppeteer/puppeteer/issues/10893)) ([4b8d20d](https://github.com/puppeteer/puppeteer/commit/4b8d20d0edeccaa3028e0c1c0b63c022cfabcee2))
### Bug Fixes
* fix line/column number in errors ([#10926](https://github.com/puppeteer/puppeteer/issues/10926)) ([a0e57f7](https://github.com/puppeteer/puppeteer/commit/a0e57f7eb230ba6a659c2d418da8d3f67add2d00))
* handle frame manager init without unhandled rejection ([#10902](https://github.com/puppeteer/puppeteer/issues/10902)) ([ea14834](https://github.com/puppeteer/puppeteer/commit/ea14834fdf1c7c1afa45bdd1fb5339380f4631a2))
* remove explicit resource management from types ([#10918](https://github.com/puppeteer/puppeteer/issues/10918)) ([a1b1bff](https://github.com/puppeteer/puppeteer/commit/a1b1bffb7258f1dec3b0a2e9ce068baf2cc3db19))
* roll to Chrome 117.0.5938.88 (r1181205) ([#10920](https://github.com/puppeteer/puppeteer/issues/10920)) ([b7bcc9a](https://github.com/puppeteer/puppeteer/commit/b7bcc9a733a3ac376397a32c3f62eb68101bedf9))
## [21.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.2.0...puppeteer-core-v21.2.1) (2023-09-13)
### Bug Fixes
* use supported node range for types ([#10896](https://github.com/puppeteer/puppeteer/issues/10896)) ([2d851c1](https://github.com/puppeteer/puppeteer/commit/2d851c1398e5efcdabdb5304dc78e68cbd3fadd2))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.7.0 to 1.7.1
## [21.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.1.1...puppeteer-core-v21.2.0) (2023-09-12)

View File

@ -0,0 +1,112 @@
import {mkdir, readFile, readdir, writeFile} from 'fs/promises';
import {join} from 'path/posix';
import esbuild from 'esbuild';
import {execa} from 'execa';
import {task} from 'hereby';
export const generateVersionTask = task({
name: 'generate:version',
run: async () => {
const {version} = JSON.parse(await readFile('package.json', 'utf8'));
await mkdir('src/generated', {recursive: true});
await writeFile(
'src/generated/version.ts',
(await readFile('src/templates/version.ts.tmpl', 'utf8')).replace(
'PACKAGE_VERSION',
version
)
);
if (process.env['PUBLISH']) {
await writeFile(
'../../versions.js',
(
await readFile('../../versions.js', {
encoding: 'utf-8',
})
).replace("'NEXT'", `'v${version}'`)
);
}
},
});
export const generateInjectedTask = task({
name: 'generate:injected',
run: async () => {
const {
outputFiles: [{text}],
} = await esbuild.build({
entryPoints: ['src/injected/injected.ts'],
bundle: true,
format: 'cjs',
target: ['chrome117', 'firefox118'],
minify: true,
write: false,
});
const template = await readFile('src/templates/injected.ts.tmpl', 'utf8');
await mkdir('src/generated', {recursive: true});
await writeFile(
'src/generated/injected.ts',
template.replace('SOURCE_CODE', JSON.stringify(text))
);
},
});
export const generatePackageJsonTask = task({
name: 'generate:package-json',
run: async () => {
await mkdir('lib/esm', {recursive: true});
await writeFile('lib/esm/package.json', JSON.stringify({type: 'module'}));
},
});
export const generateTask = task({
name: 'generate',
dependencies: [
generateVersionTask,
generateInjectedTask,
generatePackageJsonTask,
],
});
export const buildTscTask = task({
name: 'build:tsc',
dependencies: [generateTask],
run: async () => {
await execa('tsc', ['-b']);
},
});
export const buildTask = task({
name: 'build',
dependencies: [buildTscTask],
run: async () => {
const formats = ['esm', 'cjs'];
const packages = (await readdir('third_party', {withFileTypes: true}))
.filter(dirent => {
return dirent.isDirectory();
})
.map(({name}) => {
return name;
});
const builders = [];
for (const format of formats) {
const folder = join('lib', format, 'third_party');
for (const name of packages) {
const path = join(folder, name, `${name}.js`);
builders.push(
await esbuild.build({
entryPoints: [path],
outfile: path,
bundle: true,
allowOverwrite: true,
format,
target: 'node16',
minify: true,
})
);
}
}
await Promise.all(builders);
},
});

View File

@ -1,6 +1,6 @@
{
"name": "puppeteer-core",
"version": "21.2.0",
"version": "21.5.2",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [
"puppeteer",
@ -31,13 +31,13 @@
"url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core"
},
"engines": {
"node": ">=16.3.0"
"node": ">=16.13.2"
},
"scripts": {
"build:docs": "wireit",
"build": "wireit",
"check": "tsx tools/ensure-correct-devtools-protocol-package",
"clean": "git clean -Xdf -e '!node_modules' .",
"clean": "../../tools/clean.js",
"prepack": "wireit",
"unit": "wireit"
},
@ -57,27 +57,6 @@
"build:types"
]
},
"generate:sources": {
"command": "tsx tools/generate_sources.ts",
"clean": "if-file-deleted",
"files": [
"../../versions.js",
"src/{injected,templates}/**",
"tools/generate_sources.ts"
],
"output": [
"src/generated/*.ts"
]
},
"generate:package-json": {
"command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json",
"files": [
"../../tools/generate_module_package_json.ts"
],
"output": [
"lib/esm/package.json"
]
},
"build:docs": {
"command": "api-extractor run --local --config \"./api-extractor.docs.json\"",
"files": [
@ -90,20 +69,18 @@
]
},
"build:tsc": {
"command": "tsc -b && rollup --config rollup.third_party.config.mjs",
"command": "hereby build",
"clean": "if-file-deleted",
"dependencies": [
"generate:package-json",
"generate:sources",
"../browsers:build"
],
"files": [
"{compat,src,third_party}/**",
"rollup.third_party.config.mjs"
"{src,third_party}/**",
"../../versions.js",
"!src/generated"
],
"output": [
"lib/{cjs,esm}/**",
"!lib/esm/package.json"
"lib/{cjs,esm}/**"
]
},
"build:types": {
@ -131,6 +108,7 @@
"files": [
"lib",
"src",
"!*.test.ts",
"!*.test.js",
"!*.test.d.ts",
"!*.test.js.map",
@ -140,15 +118,17 @@
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "1.7.0",
"chromium-bidi": "0.4.26",
"@puppeteer/browsers": "1.8.0",
"chromium-bidi": "0.4.33",
"cross-fetch": "4.0.0",
"debug": "4.3.4",
"devtools-protocol": "0.0.1159816",
"ws": "8.14.0"
"devtools-protocol": "0.0.1203626",
"ws": "8.14.2"
},
"devDependencies": {
"disposablestack": "1.1.1",
"@types/debug": "4.1.12",
"@types/node": "18.17.15",
"@types/ws": "8.5.9",
"mitt": "3.0.1",
"parsel-js": "1.1.2",
"rxjs": "7.8.1"

View File

@ -1,52 +0,0 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import commonjs from '@rollup/plugin-commonjs';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import {globSync} from 'glob';
import nodePolyfills from 'rollup-plugin-polyfill-node';
const configs = [];
// Note we don't use path.join here. We cannot since `glob` does not support
// the backslash path separator.
for (const file of globSync(`lib/esm/third_party/**/*.js`)) {
configs.push({
input: file,
output: [
{
file,
format: 'esm',
},
{
file: file.replace('/esm/', '/cjs/'),
format: 'cjs',
},
],
plugins: [
terser(),
nodeResolve(),
// This is used internally within the polyfill. It gets ignored for the
// most part via this plugin.
nodePolyfills({include: ['util']}),
commonjs({
transformMixedEsModules: true,
}),
],
});
}
export default configs;

View File

@ -14,24 +14,28 @@
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import type {ChildProcess} from 'child_process';
import {ChildProcess} from 'child_process';
import type {Protocol} from 'devtools-protocol';
import {Protocol} from 'devtools-protocol';
import {Symbol} from '../../third_party/disposablestack/disposablestack.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError, waitWithTimeout} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import {
firstValueFrom,
from,
merge,
raceWith,
filterAsync,
fromEvent,
type Observable,
} from '../../third_party/rxjs/rxjs.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {timeout} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {BrowserContext} from './BrowserContext.js';
import type {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* BrowserContext options.
*
* @public
*/
export interface BrowserContextOptions {
@ -120,6 +124,7 @@ export type Permission =
export interface WaitForTargetOptions {
/**
* Maximum wait time in milliseconds. Pass `0` to disable the timeout.
*
* @defaultValue `30_000`
*/
timeout?: number;
@ -130,26 +135,23 @@ export interface WaitForTargetOptions {
*
* @public
*/
export const enum BrowserEmittedEvents {
export const enum BrowserEvent {
/**
* Emitted when Puppeteer gets disconnected from the browser instance. This
* might happen because of one of the following:
* might happen because either:
*
* - browser is closed or crashed
*
* - The {@link Browser.disconnect | browser.disconnect } method was called.
* - The browser closes/crashes or
* - {@link Browser.disconnect} was called.
*/
Disconnected = 'disconnected',
/**
* Emitted when the url of a target changes. Contains a {@link Target} instance.
* Emitted when the URL of a target changes. Contains a {@link Target}
* instance.
*
* @remarks
*
* Note that this includes target changes in incognito browser contexts.
* @remarks Note that this includes target changes in incognito browser
* contexts.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created, for example when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
@ -157,71 +159,85 @@ export const enum BrowserEmittedEvents {
*
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target creations in incognito browser contexts.
* @remarks Note that this includes target creations in incognito browser
* contexts.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed, for example when a page is closed.
* Contains a {@link Target} instance.
*
* @remarks
*
* Note that this includes target destructions in incognito browser contexts.
* @remarks Note that this includes target destructions in incognito browser
* contexts.
*/
TargetDestroyed = 'targetdestroyed',
/**
* @internal
*/
TargetDiscovered = 'targetdiscovered',
}
export {
/**
* @deprecated Use {@link BrowserEvent}.
*/
BrowserEvent as BrowserEmittedEvents,
};
/**
* @public
*/
export interface BrowserEvents extends Record<EventType, unknown> {
[BrowserEvent.Disconnected]: undefined;
[BrowserEvent.TargetCreated]: Target;
[BrowserEvent.TargetDestroyed]: Target;
[BrowserEvent.TargetChanged]: Target;
/**
* @internal
*/
[BrowserEvent.TargetDiscovered]: Protocol.Target.TargetInfo;
}
/**
* A Browser is created when Puppeteer connects to a browser instance, either through
* {@link PuppeteerNode.launch} or {@link Puppeteer.connect}.
* {@link Browser} represents a browser instance that is either:
*
* @remarks
* - connected to via {@link Puppeteer.connect} or
* - launched by {@link PuppeteerNode.launch}.
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and will
* emit various events which are documented in the {@link BrowserEmittedEvents} enum.
* {@link Browser} {@link EventEmitter | emits} various events which are
* documented in the {@link BrowserEvent} enum.
*
* @example
* An example of using a {@link Browser} to create a {@link Page}:
* @example Using a {@link Browser} to create a {@link Page}:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* })();
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* await browser.close();
* ```
*
* @example
* An example of disconnecting from and reconnecting to a {@link Browser}:
* @example Disconnecting from and reconnecting to a {@link Browser}:
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to the browser.
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from the browser.
* browser.disconnect();
* const browser = await puppeteer.launch();
* // Store the endpoint to be able to reconnect to the browser.
* const browserWSEndpoint = browser.wsEndpoint();
* // Disconnect puppeteer from the browser.
* browser.disconnect();
*
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close the browser.
* await browser2.close();
* })();
* // Use the endpoint to reestablish a connection
* const browser2 = await puppeteer.connect({browserWSEndpoint});
* // Close the browser.
* await browser2.close();
* ```
*
* @public
*/
export class Browser
extends EventEmitter
implements AsyncDisposable, Disposable
{
export abstract class Browser extends EventEmitter<BrowserEvents> {
/**
* @internal
*/
@ -230,150 +246,97 @@ export class Browser
}
/**
* @internal
*/
_attach(): Promise<void> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_detach(): void {
throw new Error('Not implemented');
}
/**
* @internal
*/
get _targets(): Map<string, Target> {
throw new Error('Not implemented');
}
/**
* The spawned browser process. Returns `null` if the browser instance was created with
* Gets the associated
* {@link https://nodejs.org/api/child_process.html#class-childprocess | ChildProcess}.
*
* @returns `null` if this instance was connected to via
* {@link Puppeteer.connect}.
*/
process(): ChildProcess | null {
throw new Error('Not implemented');
}
abstract process(): ChildProcess | null;
/**
* @internal
*/
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
throw new Error('Not implemented');
}
/**
* Creates a new incognito browser context. This won't share cookies/cache with other
* browser contexts.
* Creates a new incognito {@link BrowserContext | browser context}.
*
* This won't share cookies/cache with other {@link BrowserContext | browser contexts}.
*
* @example
*
* ```ts
* (async () => {
* const browser = await puppeteer.launch();
* // Create a new incognito browser context.
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* })();
* import puppeteer from 'puppeteer';
*
* const browser = await puppeteer.launch();
* // Create a new incognito browser context.
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* ```
*/
createIncognitoBrowserContext(
abstract createIncognitoBrowserContext(
options?: BrowserContextOptions
): Promise<BrowserContext>;
createIncognitoBrowserContext(): Promise<BrowserContext> {
throw new Error('Not implemented');
}
/**
* Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}.
* Gets a list of open {@link BrowserContext | browser contexts}.
*
* In a newly-created {@link Browser | browser}, this will return a single
* instance of {@link BrowserContext}.
*/
browserContexts(): BrowserContext[] {
throw new Error('Not implemented');
}
abstract browserContexts(): BrowserContext[];
/**
* Returns the default browser context. The default browser context cannot be closed.
* Gets the default {@link BrowserContext | browser context}.
*
* @remarks The default {@link BrowserContext | browser context} cannot be
* closed.
*/
defaultBrowserContext(): BrowserContext {
throw new Error('Not implemented');
}
abstract defaultBrowserContext(): BrowserContext;
/**
* @internal
*/
_disposeContext(contextId?: string): Promise<void>;
_disposeContext(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The browser websocket endpoint which can be used as an argument to
* {@link Puppeteer.connect}.
* Gets the WebSocket URL to connect to this {@link Browser | browser}.
*
* @returns The Browser websocket url.
* This is usually used with {@link Puppeteer.connect}.
*
* @remarks
* You can find the debugger URL (`webSocketDebuggerUrl`) from
* `http://${host}:${port}/json/version`.
*
* The format is `ws://${host}:${port}/devtools/browser/<id>`.
*
* You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`.
* Learn more about the
* {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and
* the {@link
* See {@link
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}.
*/
wsEndpoint(): string {
throw new Error('Not implemented');
}
/**
* Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* @internal
*/
_createPageInContext(contextId?: string): Promise<Page>;
_createPageInContext(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* The target associated with the browser.
*/
target(): Target {
throw new Error('Not implemented');
}
/**
* Searches for a target in all browser contexts.
* | browser endpoint} for more information.
*
* @param predicate - A function to be run for every target.
* @returns The first target found that matches the `predicate` function.
* @remarks The format is always `ws://${host}:${port}/devtools/browser/<id>`.
*/
abstract wsEndpoint(): string;
/**
* Creates a new {@link Page | page} in the
* {@link Browser.defaultBrowserContext | default browser context}.
*/
abstract newPage(): Promise<Page>;
/**
* Gets all active {@link Target | targets}.
*
* @example
* In case of multiple {@link BrowserContext | browser contexts}, this returns
* all {@link Target | targets} in all
* {@link BrowserContext | browser contexts}.
*/
abstract targets(): Target[];
/**
* Gets the {@link Target | target} associated with the
* {@link Browser.defaultBrowserContext | default browser context}).
*/
abstract target(): Target;
/**
* Waits until a {@link Target | target} matching the given `predicate`
* appears and returns it.
*
* An example of finding a target for a page opened via `window.open`:
* This will look all open {@link BrowserContext | browser contexts}.
*
* @example Finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
@ -386,41 +349,25 @@ export class Browser
predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {}
): Promise<Target> {
const {timeout = 30000} = options;
const targetDeferred = Deferred.create<Target | PromiseLike<Target>>();
this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check);
try {
this.targets().forEach(check);
if (!timeout) {
return await targetDeferred.valueOrThrow();
}
return await waitWithTimeout(
targetDeferred.valueOrThrow(),
'target',
timeout
);
} finally {
this.off(BrowserEmittedEvents.TargetCreated, check);
this.off(BrowserEmittedEvents.TargetChanged, check);
}
async function check(target: Target): Promise<void> {
if ((await predicate(target)) && !targetDeferred.resolved()) {
targetDeferred.resolve(target);
}
}
const {timeout: ms = 30000} = options;
return await firstValueFrom(
merge(
fromEvent(this, BrowserEvent.TargetCreated) as Observable<Target>,
fromEvent(this, BrowserEvent.TargetChanged) as Observable<Target>,
from(this.targets())
).pipe(filterAsync(predicate), raceWith(timeout(ms)))
);
}
/**
* An array of all open pages inside the Browser.
* Gets a list of all open {@link Page | pages} inside this {@link Browser}.
*
* @remarks
* If there ar multiple {@link BrowserContext | browser contexts}, this
* returns all {@link Page | pages} in all
* {@link BrowserContext | browser contexts}.
*
* In case of multiple browser contexts, returns an array with all the pages in all
* browser contexts. Non-visible pages, such as `"background_page"`, will not be listed
* here. You can find them using {@link Target.page}.
* @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
* will not be listed here. You can find them using {@link Target.page}.
*/
async pages(): Promise<Page[]> {
const contextPages = await Promise.all(
@ -435,84 +382,64 @@ export class Browser
}
/**
* A string representing the browser name and version.
* Gets a string representing this {@link Browser | browser's} name and
* version.
*
* @remarks
* For headless browser, this is similar to `"HeadlessChrome/61.0.3153.0"`. For
* non-headless or new-headless, this is similar to `"Chrome/61.0.3153.0"`. For
* Firefox, it is similar to `"Firefox/116.0a1"`.
*
* For headless browser, this is similar to `HeadlessChrome/61.0.3153.0`. For
* non-headless or new-headless, this is similar to `Chrome/61.0.3153.0`. For
* Firefox, it is similar to `Firefox/116.0a1`.
*
* The format of browser.version() might change with future releases of
* The format of {@link Browser.version} might change with future releases of
* browsers.
*/
version(): Promise<string> {
throw new Error('Not implemented');
}
abstract version(): Promise<string>;
/**
* The browser's original user agent. Pages can override the browser user agent with
* Gets this {@link Browser | browser's} original user agent.
*
* {@link Page | Pages} can override the user agent with
* {@link Page.setUserAgent}.
*/
userAgent(): Promise<string> {
throw new Error('Not implemented');
}
abstract userAgent(): Promise<string>;
/**
* Closes the browser and all of its pages (if any were opened). The
* {@link Browser} object itself is considered to be disposed and cannot be
* used anymore.
* Closes this {@link Browser | browser} and all associated
* {@link Page | pages}.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
abstract close(): Promise<void>;
/**
* Disconnects Puppeteer from the browser, but leaves the browser process running.
* After calling `disconnect`, the {@link Browser} object is considered disposed and
* cannot be used anymore.
* Disconnects Puppeteer from this {@link Browser | browser}, but leaves the
* process running.
*/
disconnect(): void {
throw new Error('Not implemented');
}
abstract disconnect(): void;
/**
* Indicates that the browser is connected.
* Whether Puppeteer is connected to this {@link Browser | browser}.
*
* @deprecated Use {@link Browser.connected}.
*/
isConnected(): boolean {
throw new Error('Not implemented');
return this.connected;
}
[Symbol.dispose](): void {
/**
* Whether Puppeteer is connected to this {@link Browser | browser}.
*/
abstract get connected(): boolean;
/** @internal */
[disposeSymbol](): void {
return void this.close().catch(debugError);
}
[Symbol.asyncDispose](): Promise<void> {
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
return this.close();
}
}
/**
* @public
*/
export const enum BrowserContextEmittedEvents {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
* @internal
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
abstract get protocol(): 'cdp' | 'webDriverBiDi';
}

View File

@ -14,32 +14,72 @@
* limitations under the License.
*/
import {EventEmitter} from '../common/EventEmitter.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {asyncDisposeSymbol, disposeSymbol} from '../util/disposable.js';
import type {Permission, Browser} from './Browser.js';
import {Page} from './Page.js';
import type {Browser, Permission, WaitForTargetOptions} from './Browser.js';
import type {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* BrowserContexts provide a way to operate multiple independent browser
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
* @public
*/
export const enum BrowserContextEvent {
/**
* Emitted when the url of a target inside the browser context changes.
* Contains a {@link Target} instance.
*/
TargetChanged = 'targetchanged',
/**
* Emitted when a target is created within the browser context, for example
* when a new page is opened by
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open}
* or by {@link BrowserContext.newPage | browserContext.newPage}
*
* Contains a {@link Target} instance.
*/
TargetCreated = 'targetcreated',
/**
* Emitted when a target is destroyed within the browser context, for example
* when a page is closed. Contains a {@link Target} instance.
*/
TargetDestroyed = 'targetdestroyed',
}
export {
/**
* @deprecated Use {@link BrowserContextEvent}
*/
BrowserContextEvent as BrowserContextEmittedEvents,
};
/**
* @public
*/
export interface BrowserContextEvents extends Record<EventType, unknown> {
[BrowserContextEvent.TargetChanged]: Target;
[BrowserContextEvent.TargetCreated]: Target;
[BrowserContextEvent.TargetDestroyed]: Target;
}
/**
* {@link BrowserContext} represents individual sessions within a
* {@link Browser | browser}.
*
* @remarks
* When a {@link Browser | browser} is launched, it has a single
* {@link BrowserContext | browser context} by default. Others can be created
* using {@link Browser.createIncognitoBrowserContext}.
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
* {@link BrowserContext} {@link EventEmitter | emits} various events which are
* documented in the {@link BrowserContextEvent} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
* If a {@link Page | page} opens another {@link Page | page}, e.g. using
* `window.open`, the popup will belong to the parent {@link Page.browserContext
* | page's browser context}.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
* @example Creating an incognito {@link BrowserContext | browser context}:
*
* ```ts
* // Create a new incognito browser context
@ -55,7 +95,7 @@ import type {Target} from './Target.js';
* @public
*/
export class BrowserContext extends EventEmitter {
export abstract class BrowserContext extends EventEmitter<BrowserContextEvents> {
/**
* @internal
*/
@ -64,17 +104,18 @@ export class BrowserContext extends EventEmitter {
}
/**
* An array of all active targets inside the browser context.
* Gets all active {@link Target | targets} inside this
* {@link BrowserContext | browser context}.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
abstract targets(): Target[];
/**
* This searches for a target in this specific browser context.
* Waits until a {@link Target | target} matching the given `predicate`
* appears and returns it.
*
* @example
* An example of finding a target for a page opened via `window.open`:
* This will look all open {@link BrowserContext | browser contexts}.
*
* @example Finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
@ -82,46 +123,35 @@ export class BrowserContext extends EventEmitter {
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timeout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
waitForTarget(
abstract waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number}
options?: WaitForTargetOptions
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all pages inside the browser context.
* Gets a list of all open {@link Page | pages} inside this
* {@link BrowserContext | browser context}.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
* @remarks Non-visible {@link Page | pages}, such as `"background_page"`,
* will not be listed here. You can find them using {@link Target.page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
abstract pages(): Promise<Page[]>;
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
* Whether this {@link BrowserContext | browser context} is incognito.
*
* @remarks
* The default browser context cannot be closed.
* The {@link Browser.defaultBrowserContext | default browser context} is the
* only non-incognito browser context.
*/
isIncognito(): boolean {
throw new Error('Not implemented');
}
abstract isIncognito(): boolean;
/**
* @example
* Grants this {@link BrowserContext | browser context} the given
* `permissions` within the given `origin`.
*
* @example Overriding permissions in the
* {@link Browser.defaultBrowserContext | default browser context}:
*
* ```ts
* const context = browser.defaultBrowserContext();
@ -130,19 +160,22 @@ export class BrowserContext extends EventEmitter {
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
* @param origin - The origin to grant permissions to, e.g.
* "https://example.com".
* @param permissions - An array of permissions to grant. All permissions that
* are not listed here will be automatically denied.
*/
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
overridePermissions(): Promise<void> {
throw new Error('Not implemented');
}
abstract overridePermissions(
origin: string,
permissions: Permission[]
): Promise<void>;
/**
* Clears all permission overrides for the browser context.
* Clears all permission overrides for this
* {@link BrowserContext | browser context}.
*
* @example
* @example Clearing overridden permissions in the
* {@link Browser.defaultBrowserContext | default browser context}:
*
* ```ts
* const context = browser.defaultBrowserContext();
@ -151,36 +184,51 @@ export class BrowserContext extends EventEmitter {
* context.clearPermissionOverrides();
* ```
*/
clearPermissionOverrides(): Promise<void> {
throw new Error('Not implemented');
}
abstract clearPermissionOverrides(): Promise<void>;
/**
* Creates a new page in the browser context.
* Creates a new {@link Page | page} in this
* {@link BrowserContext | browser context}.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
abstract newPage(): Promise<Page>;
/**
* The browser this browser context belongs to.
* Gets the {@link Browser | browser} associated with this
* {@link BrowserContext | browser context}.
*/
browser(): Browser {
throw new Error('Not implemented');
}
abstract browser(): Browser;
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
* Closes this {@link BrowserContext | browser context} and all associated
* {@link Page | pages}.
*
* @remarks
* Only incognito browser contexts can be closed.
* @remarks The
* {@link Browser.defaultBrowserContext | default browser context} cannot be
* closed.
*/
close(): Promise<void> {
throw new Error('Not implemented');
abstract close(): Promise<void>;
/**
* Whether this {@link BrowserContext | browser context} is closed.
*/
get closed(): boolean {
return !this.browser().browserContexts().includes(this);
}
/**
* Identifier for this {@link BrowserContext | browser context}.
*/
get id(): string | undefined {
return undefined;
}
/** @internal */
[disposeSymbol](): void {
return void this.close().catch(debugError);
}
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
return this.close();
}
}

View File

@ -0,0 +1,113 @@
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import type {Connection} from '../cdp/Connection.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
/**
* @public
*/
export type CDPEvents = {
[Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
};
/**
* Events that the CDPSession class emits.
*
* @public
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CDPSessionEvent {
/** @internal */
export const Disconnected = Symbol('CDPSession.Disconnected');
/** @internal */
export const Swapped = Symbol('CDPSession.Swapped');
/**
* Emitted when the session is ready to be configured during the auto-attach
* process. Right after the event is handled, the session will be resumed.
*
* @internal
*/
export const Ready = Symbol('CDPSession.Ready');
export const SessionAttached = 'sessionattached' as const;
export const SessionDetached = 'sessiondetached' as const;
}
/**
* @public
*/
export interface CDPSessionEvents
extends CDPEvents,
Record<EventType, unknown> {
/** @internal */
[CDPSessionEvent.Disconnected]: undefined;
/** @internal */
[CDPSessionEvent.Swapped]: CDPSession;
/** @internal */
[CDPSessionEvent.Ready]: CDPSession;
[CDPSessionEvent.SessionAttached]: CDPSession;
[CDPSessionEvent.SessionDetached]: CDPSession;
}
/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol.
*
* @remarks
*
* Protocol methods can be called with {@link CDPSession.send} method and protocol
* events can be subscribed to with `CDPSession.on` method.
*
* Useful links: {@link https://chromedevtools.github.io/devtools-protocol/ | DevTools Protocol Viewer}
* and {@link https://github.com/aslushnikov/getting-started-with-cdp/blob/HEAD/README.md | Getting Started with DevTools Protocol}.
*
* @example
*
* ```ts
* const client = await page.target().createCDPSession();
* await client.send('Animation.enable');
* client.on('Animation.animationCreated', () =>
* console.log('Animation created!')
* );
* const response = await client.send('Animation.getPlaybackRate');
* console.log('playback rate is ' + response.playbackRate);
* await client.send('Animation.setPlaybackRate', {
* playbackRate: response.playbackRate / 2,
* });
* ```
*
* @public
*/
export abstract class CDPSession extends EventEmitter<CDPSessionEvents> {
/**
* @internal
*/
constructor() {
super();
}
abstract connection(): Connection | undefined;
/**
* Parent session in terms of CDP's auto-attach mechanism.
*
* @internal
*/
parentSession(): CDPSession | undefined {
return undefined;
}
abstract send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']>;
/**
* Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages.
*/
abstract detach(): Promise<void>;
/**
* Returns the session's id.
*/
abstract id(): string;
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';

View File

@ -14,31 +14,32 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import type {Frame} from '../api/Frame.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import {LazyArg} from '../common/LazyArg.js';
import {
import type {
ElementFor,
EvaluateFuncWith,
HandleFor,
HandleOr,
NodeFor,
} from '../common/types.js';
import {KeyInput} from '../common/USKeyboardLayout.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import {isString, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {throwIfDisposed} from '../util/decorators.js';
import {
import {_isElementHandle} from './ElementHandleSymbol.js';
import type {
KeyboardTypeOptions,
KeyPressOptions,
MouseClickOptions,
} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {ScreenshotOptions} from './Page.js';
import type {ScreenshotOptions, WaitForSelectorOptions} from './Page.js';
/**
* @public
@ -103,6 +104,16 @@ export interface Point {
y: number;
}
/**
* @public
*/
export interface ElementScreenshotOptions extends ScreenshotOptions {
/**
* @defaultValue true
*/
scrollIntoView?: boolean;
}
/**
* ElementHandle represents an in-page DOM element.
*
@ -139,6 +150,11 @@ export interface Point {
export abstract class ElementHandle<
ElementType extends Node = Element,
> extends JSHandle<ElementType> {
/**
* @internal
*/
declare [_isElementHandle]: boolean;
/**
* A given method will have it's `this` replaced with an isolated version of
* `this` when decorated with this decorator.
@ -202,6 +218,7 @@ export abstract class ElementHandle<
constructor(handle: JSHandle<ElementType>) {
super();
this.handle = handle;
this[_isElementHandle] = true;
}
/**
@ -221,16 +238,7 @@ export abstract class ElementHandle<
/**
* @internal
*/
override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K>
): Promise<HandleFor<ElementType[K]>>;
/**
* @internal
*/
override async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
/**
* @internal
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K>
@ -241,6 +249,7 @@ export abstract class ElementHandle<
/**
* @internal
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async getProperties(): Promise<Map<string, JSHandle>> {
return await this.handle.getProperties();
@ -259,6 +268,10 @@ export abstract class ElementHandle<
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.handle.evaluate(pageFunction, ...args);
}
@ -275,12 +288,17 @@ export abstract class ElementHandle<
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.handle.evaluateHandle(pageFunction, ...args);
}
/**
* @internal
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async jsonValue(): Promise<ElementType> {
return await this.handle.jsonValue();
@ -326,6 +344,7 @@ export abstract class ElementHandle<
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async $<Selector extends string>(
selector: Selector
@ -345,6 +364,7 @@ export abstract class ElementHandle<
* @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
@ -423,7 +443,7 @@ export abstract class ElementHandle<
*
* JavaScript:
*
* ```js
* ```ts
* const feedHandle = await page.$('.feed');
* expect(
* await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))
@ -478,6 +498,7 @@ export abstract class ElementHandle<
* If there are no such elements, the method will resolve to an empty array.
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
if (expression.startsWith('//')) {
@ -523,6 +544,7 @@ export abstract class ElementHandle<
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async waitForSelector<Selector extends string>(
selector: Selector,
@ -553,6 +575,7 @@ export abstract class ElementHandle<
* Checks if an element is visible using the same mechanism as
* {@link ElementHandle.waitForSelector}.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async isVisible(): Promise<boolean> {
return await this.#checkVisibility(true);
@ -562,6 +585,7 @@ export abstract class ElementHandle<
* Checks if an element is hidden using the same mechanism as
* {@link ElementHandle.waitForSelector}.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async isHidden(): Promise<boolean> {
return await this.#checkVisibility(false);
@ -629,6 +653,7 @@ export abstract class ElementHandle<
* default value can be changed by using the {@link Page.setDefaultTimeout}
* method.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async waitForXPath(
xpath: string,
@ -662,6 +687,7 @@ export abstract class ElementHandle<
* @throws An error if the handle does not match. **The handle will not be
* automatically disposed.**
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
@ -685,6 +711,7 @@ export abstract class ElementHandle<
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async clickablePoint(offset?: Offset): Promise<Point> {
const box = await this.#clickableBox();
@ -708,6 +735,7 @@ export abstract class ElementHandle<
* uses {@link Page} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async hover(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
@ -720,6 +748,7 @@ export abstract class ElementHandle<
* uses {@link Page | Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async click(
this: ElementHandle<Element>,
@ -731,59 +760,134 @@ export abstract class ElementHandle<
}
/**
* This method creates and captures a dragevent from the element.
* Drags an element over the given element or point.
*
* @returns DEPRECATED. When drag interception is enabled, the drag payload is
* returned.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async drag(
this: ElementHandle<Element>,
target: Point
): Promise<Protocol.Input.DragData>;
async drag(this: ElementHandle<Element>): Promise<Protocol.Input.DragData> {
throw new Error('Not implemented');
target: Point | ElementHandle<Element>
): Promise<Protocol.Input.DragData | void> {
await this.scrollIntoViewIfNeeded();
const page = this.frame.page();
if (page.isDragInterceptionEnabled()) {
const source = await this.clickablePoint();
if (target instanceof ElementHandle) {
target = await target.clickablePoint();
}
return await page.mouse.drag(source, target);
}
try {
if (!page._isDragging) {
page._isDragging = true;
await this.hover();
await page.mouse.down();
}
if (target instanceof ElementHandle) {
await target.hover();
} else {
await page.mouse.move(target.x, target.y);
}
} catch (error) {
page._isDragging = false;
throw error;
}
}
/**
* This method creates a `dragenter` event on the element.
* @deprecated Do not use. `dragenter` will automatically be performed during dragging.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragEnter(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragEnter(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
const page = this.frame.page();
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await page.mouse.dragEnter(target, data);
}
/**
* This method creates a `dragover` event on the element.
* @deprecated Do not use. `dragover` will automatically be performed during dragging.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragOver(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragOver(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
const page = this.frame.page();
await this.scrollIntoViewIfNeeded();
const target = await this.clickablePoint();
await page.mouse.dragOver(target, data);
}
/**
* This method triggers a drop on the element.
* Drops the given element onto the current one.
*/
async drop(
this: ElementHandle<Element>,
element: ElementHandle<Element>
): Promise<void>;
/**
* @deprecated No longer supported.
*/
async drop(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async drop(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
/**
* @internal
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async drop(
this: ElementHandle<Element>,
dataOrElement: ElementHandle<Element> | Protocol.Input.DragData = {
items: [],
dragOperationsMask: 1,
}
): Promise<void> {
const page = this.frame.page();
if ('items' in dataOrElement) {
await this.scrollIntoViewIfNeeded();
const destination = await this.clickablePoint();
await page.mouse.drop(destination, dataOrElement);
} else {
// Note if the rest errors, we still want dragging off because the errors
// is most likely something implying the mouse is no longer dragging.
await dataOrElement.drag(this);
page._isDragging = false;
await page.mouse.up();
}
}
/**
* This method triggers a dragenter, dragover, and drop on the element.
* @deprecated Use `ElementHandle.drop` instead.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async dragAndDrop(
this: ElementHandle<Element>,
target: ElementHandle<Node>,
options?: {delay: number}
): Promise<void>;
async dragAndDrop(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
): Promise<void> {
const page = this.frame.page();
assert(
page.isDragInterceptionEnabled(),
'Drag Interception is not enabled!'
);
await this.scrollIntoViewIfNeeded();
const startPoint = await this.clickablePoint();
const targetPoint = await target.clickablePoint();
await page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
/**
@ -802,6 +906,7 @@ export abstract class ElementHandle<
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async select(...values: string[]): Promise<string[]> {
for (const value of values) {
@ -857,28 +962,27 @@ export abstract class ElementHandle<
* {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
* For locals script connecting to remote chrome environments, paths must be
* absolute.
*
*/
async uploadFile(
abstract uploadFile(
this: ElementHandle<HTMLInputElement>,
...paths: string[]
): Promise<void>;
async uploadFile(this: ElementHandle<HTMLInputElement>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method scrolls element into view if needed, and then uses
* {@link Touchscreen.tap} to tap in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async tap(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchStart(x, y);
await this.frame.page().touchscreen.touchEnd();
await this.frame.page().touchscreen.tap(x, y);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async touchStart(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
@ -886,6 +990,7 @@ export abstract class ElementHandle<
await this.frame.page().touchscreen.touchStart(x, y);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async touchMove(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
@ -893,6 +998,7 @@ export abstract class ElementHandle<
await this.frame.page().touchscreen.touchMove(x, y);
}
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async touchEnd(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
@ -902,6 +1008,7 @@ export abstract class ElementHandle<
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async focus(): Promise<void> {
await this.evaluate(element => {
@ -937,6 +1044,7 @@ export abstract class ElementHandle<
*
* @param options - Delay in milliseconds. Defaults to 0.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async type(
text: string,
@ -960,6 +1068,7 @@ export abstract class ElementHandle<
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async press(
key: KeyInput,
@ -1047,8 +1156,10 @@ export abstract class ElementHandle<
/**
* This method returns the bounding box of the element (relative to the main frame),
* or `null` if the element is not visible.
* or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
* (example: `display: none`).
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async boundingBox(): Promise<BoundingBox | null> {
const box = await this.evaluate(element => {
@ -1078,13 +1189,16 @@ export abstract class ElementHandle<
}
/**
* This method returns boxes of the element, or `null` if the element is not visible.
* This method returns boxes of the element,
* or `null` if the element is {@link https://drafts.csswg.org/css-display-4/#box-generation | not part of the layout}
* (example: `display: none`).
*
* @remarks
*
* Boxes are represented as an array of points;
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async boxModel(): Promise<BoxModel | null> {
const model = await this.evaluate(element => {
@ -1219,15 +1333,67 @@ export abstract class ElementHandle<
/**
* This method scrolls element into view if needed, and then uses
* {@link Page.(screenshot:3) } to take a screenshot of the element.
* {@link Page.(screenshot:2) } to take a screenshot of the element.
* If the element is detached from DOM, the method throws an error.
*/
async screenshot(
options: Readonly<ScreenshotOptions> & {encoding: 'base64'}
): Promise<string>;
async screenshot(options?: Readonly<ScreenshotOptions>): Promise<Buffer>;
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async screenshot(
this: ElementHandle<Element>,
options?: ScreenshotOptions
): Promise<string | Buffer>;
async screenshot(this: ElementHandle<Element>): Promise<string | Buffer> {
throw new Error('Not implemented');
options: Readonly<ElementScreenshotOptions> = {}
): Promise<string | Buffer> {
const {
scrollIntoView = true,
captureBeyondViewport = true,
allowViewportExpansion = captureBeyondViewport,
} = options;
let clip = await this.#nonEmptyVisibleBoundingBox();
const page = this.frame.page();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await using _ =
allowViewportExpansion && clip
? await page._createTemporaryViewportContainingBox(clip)
: null;
if (scrollIntoView) {
await this.scrollIntoViewIfNeeded();
// We measure again just in case.
clip = await this.#nonEmptyVisibleBoundingBox();
}
const [pageLeft, pageTop] = await this.evaluate(() => {
if (!window.visualViewport) {
throw new Error('window.visualViewport is not supported.');
}
return [
window.visualViewport.pageLeft,
window.visualViewport.pageTop,
] as const;
});
clip.x += pageLeft;
clip.y += pageTop;
return await page.screenshot({
...options,
captureBeyondViewport: false,
clip,
});
}
async #nonEmptyVisibleBoundingBox() {
const box = await this.boundingBox();
assert(box, 'Node is either not visible or not an HTMLElement');
assert(box.width !== 0, 'Node has 0 width.');
assert(box.height !== 0, 'Node has 0 height.');
return box;
}
/**
@ -1273,6 +1439,7 @@ export abstract class ElementHandle<
* @param options - Threshold for the intersection between 0 (no intersection) and 1
* (full intersection). Defaults to 1.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async isIntersectingViewport(
this: ElementHandle<Element>,
@ -1303,6 +1470,7 @@ export abstract class ElementHandle<
* Scrolls the element into view using either the automation protocol client
* or by calling element.scrollIntoView.
*/
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
await this.assertConnectedElement();

View File

@ -1,5 +1,5 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,8 +14,7 @@
* limitations under the License.
*/
export * from './BidiOverCDP.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './Connection.js';
export * from './Page.js';
/**
* @internal
*/
export const _isElementHandle = Symbol('_isElementHandle');

View File

@ -14,9 +14,8 @@
* limitations under the License.
*/
import {CDPSession} from '../common/Connection.js';
import {Realm} from './Realm.js';
import type {CDPSession} from './CDPSession.js';
import type {Realm} from './Realm.js';
/**
* @internal

View File

@ -14,21 +14,23 @@
* limitations under the License.
*/
import {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import {HTTPResponse} from '../api/HTTPResponse.js';
import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {EventEmitter} from '../common/EventEmitter.js';
import type Protocol from 'devtools-protocol';
import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {
Page,
WaitForSelectorOptions,
WaitTimeoutOptions,
} from '../api/Page.js';
import type {DeviceRequestPrompt} from '../cdp/DeviceRequestPrompt.js';
import type {IsolatedWorldChart} from '../cdp/IsolatedWorld.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {transposeIterableHandle} from '../common/HandleIterator.js';
import {
IsolatedWorldChart,
WaitForSelectorOptions,
} from '../common/IsolatedWorld.js';
import {LazyArg} from '../common/LazyArg.js';
import {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import {
import type {
Awaitable,
EvaluateFunc,
EvaluateFuncWith,
@ -43,9 +45,53 @@ import {
import {assert} from '../util/assert.js';
import {throwIfDisposed} from '../util/decorators.js';
import {KeyboardTypeOptions} from './Input.js';
import {FunctionLocator, Locator, NodeLocator} from './locators/locators.js';
import {Realm} from './Realm.js';
import type {CDPSession} from './CDPSession.js';
import type {KeyboardTypeOptions} from './Input.js';
import {
FunctionLocator,
type Locator,
NodeLocator,
} from './locators/locators.js';
import type {Realm} from './Realm.js';
/**
* @public
*/
export interface WaitForOptions {
/**
* Maximum wait time in milliseconds. Pass 0 to disable the timeout.
*
* The default value can be changed by using the
* {@link Page.setDefaultTimeout} or {@link Page.setDefaultNavigationTimeout}
* methods.
*
* @defaultValue `30000`
*/
timeout?: number;
/**
* When to consider waiting succeeds. Given an array of event strings, waiting
* is considered to be successful after all events have been fired.
*
* @defaultValue `'load'`
*/
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
/**
* @public
*/
export interface GoToOptions extends WaitForOptions {
/**
* If provided, it will take preference over the referer header value set by
* {@link Page.setExtraHTTPHeaders | page.setExtraHTTPHeaders()}.
*/
referer?: string;
/**
* If provided, it will take preference over the referer-policy header value
* set by {@link Page.setExtraHTTPHeaders | page.setExtraHTTPHeaders()}.
*/
referrerPolicy?: string;
}
/**
* @public
@ -127,6 +173,44 @@ export interface FrameAddStyleTagOptions {
content?: string;
}
/**
* @public
*/
export interface FrameEvents extends Record<EventType, unknown> {
/** @internal */
[FrameEvent.FrameNavigated]: Protocol.Page.NavigationType;
/** @internal */
[FrameEvent.FrameSwapped]: undefined;
/** @internal */
[FrameEvent.LifecycleEvent]: undefined;
/** @internal */
[FrameEvent.FrameNavigatedWithinDocument]: undefined;
/** @internal */
[FrameEvent.FrameDetached]: Frame;
/** @internal */
[FrameEvent.FrameSwappedByActivation]: undefined;
}
/**
* We use symbols to prevent external parties listening to these events.
* They are internal to Puppeteer.
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace FrameEvent {
export const FrameNavigated = Symbol('Frame.FrameNavigated');
export const FrameSwapped = Symbol('Frame.FrameSwapped');
export const LifecycleEvent = Symbol('Frame.LifecycleEvent');
export const FrameNavigatedWithinDocument = Symbol(
'Frame.FrameNavigatedWithinDocument'
);
export const FrameDetached = Symbol('Frame.FrameDetached');
export const FrameSwappedByActivation = Symbol(
'Frame.FrameSwappedByActivation'
);
}
/**
* @internal
*/
@ -181,13 +265,13 @@ export const throwIfDetached = throwIfDisposed<Frame>(frame => {
* Frame lifecycles are controlled by three events that are all dispatched on
* the parent {@link Frame.page | page}:
*
* - {@link PageEmittedEvents.FrameAttached}
* - {@link PageEmittedEvents.FrameNavigated}
* - {@link PageEmittedEvents.FrameDetached}
* - {@link PageEvent.FrameAttached}
* - {@link PageEvent.FrameNavigated}
* - {@link PageEvent.FrameDetached}
*
* @public
*/
export abstract class Frame extends EventEmitter {
export abstract class Frame extends EventEmitter<FrameEvents> {
/**
* @internal
*/
@ -228,12 +312,10 @@ export abstract class Frame extends EventEmitter {
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
* `false`.
*/
isOOPFrame(): boolean {
throw new Error('Not implemented');
}
abstract isOOPFrame(): boolean;
/**
* Navigates a frame to the given url.
* Navigates the frame to the given `url`.
*
* @remarks
* Navigation to `about:blank` or navigation to the same URL with a different
@ -247,20 +329,17 @@ export abstract class Frame extends EventEmitter {
*
* :::
*
* @param url - the URL to navigate the frame to. This should include the
* scheme, e.g. `https://`.
* @param options - navigation options. `waitUntil` is useful to define when
* the navigation should be considered successful - see the docs for
* {@link PuppeteerLifeCycleEvent} for more details.
*
* @param url - URL to navigate the frame 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 This method will throw an error if:
* @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 timeout is exceeded during navigation.
* - the remote server does not respond or is unreachable.
* - the main resource failed to load.
*
@ -298,14 +377,12 @@ export abstract class Frame extends EventEmitter {
* ]);
* ```
*
* @param options - options to configure when the navigation is consided
* finished.
* @returns a promise that resolves when the frame navigates to a new URL.
* @param options - Options to configure waiting behavior.
* @returns A promise which resolves to the main resource response.
*/
abstract waitForNavigation(options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<HTTPResponse | null>;
abstract waitForNavigation(
options?: WaitForOptions
): Promise<HTTPResponse | null>;
/**
* @internal
@ -527,7 +604,7 @@ export abstract class Frame extends EventEmitter {
*
* @example
*
* ```js
* ```ts
* const divsCounts = await frame.$$eval('div', divs => divs.length);
* ```
*
@ -1118,26 +1195,10 @@ export abstract class Frame extends EventEmitter {
* await devicePrompt.waitForDevice(({name}) => name.includes('My Device'))
* );
* ```
*
* @internal
*/
waitForDevicePrompt(
abstract waitForDevicePrompt(
options?: WaitTimeoutOptions
): Promise<DeviceRequestPrompt>;
/**
* @internal
*/
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
throw new Error('Not implemented');
}
/**
* @internal
*/
exposeFunction<Args extends unknown[], Ret>(
name: string,
fn: (...args: Args) => Awaitable<Ret>
): Promise<void>;
exposeFunction(): Promise<void> {
throw new Error('Not implemented');
}
}

View File

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js';
import {Frame} from './Frame.js';
import {HTTPResponse} from './HTTPResponse.js';
import type {CDPSession} from './CDPSession.js';
import type {Frame} from './Frame.js';
import type {HTTPResponse} from './HTTPResponse.js';
/**
* @public
@ -101,7 +100,7 @@ export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0;
*
* @public
*/
export class HTTPRequest {
export abstract class HTTPRequest {
/**
* @internal
*/
@ -132,9 +131,7 @@ export class HTTPRequest {
*
* @experimental
*/
get client(): CDPSession {
throw new Error('Not implemented');
}
abstract get client(): CDPSession;
/**
* @internal
@ -144,33 +141,25 @@ export class HTTPRequest {
/**
* The URL of the request
*/
url(): string {
throw new Error('Not implemented');
}
abstract url(): string;
/**
* The `ContinueRequestOverrides` that will be used
* if the interception is allowed to continue (ie, `abort()` and
* `respond()` aren't called).
*/
continueRequestOverrides(): ContinueRequestOverrides {
throw new Error('Not implemented');
}
abstract continueRequestOverrides(): ContinueRequestOverrides;
/**
* The `ResponseForRequest` that gets used if the
* interception is allowed to respond (ie, `abort()` is not called).
*/
responseForRequest(): Partial<ResponseForRequest> | null {
throw new Error('Not implemented');
}
abstract responseForRequest(): Partial<ResponseForRequest> | null;
/**
* The most recent reason for aborting the request
*/
abortErrorReason(): Protocol.Network.ErrorReason | null {
throw new Error('Not implemented');
}
abstract abortErrorReason(): Protocol.Network.ErrorReason | null;
/**
* An InterceptResolutionState object describing the current resolution
@ -183,17 +172,13 @@ export class HTTPRequest {
* InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
* `disabled`, `none`, or `already-handled`.
*/
interceptResolutionState(): InterceptResolutionState {
throw new Error('Not implemented');
}
abstract interceptResolutionState(): InterceptResolutionState;
/**
* Is `true` if the intercept resolution has already been handled,
* `false` otherwise.
*/
isInterceptResolutionHandled(): boolean {
throw new Error('Not implemented');
}
abstract isInterceptResolutionHandled(): boolean;
/**
* Adds an async request handler to the processing queue.
@ -201,80 +186,59 @@ export class HTTPRequest {
* but they are guaranteed to resolve before the request interception
* is finalized.
*/
enqueueInterceptAction(
abstract enqueueInterceptAction(
pendingHandler: () => void | PromiseLike<unknown>
): void;
enqueueInterceptAction(): void {
throw new Error('Not implemented');
}
/**
* Awaits pending interception handlers and then decides how to fulfill
* the request interception.
*/
async finalizeInterceptions(): Promise<void> {
throw new Error('Not implemented');
}
abstract finalizeInterceptions(): Promise<void>;
/**
* Contains the request's resource type as it was perceived by the rendering
* engine.
*/
resourceType(): ResourceType {
throw new Error('Not implemented');
}
abstract resourceType(): ResourceType;
/**
* The method used (`GET`, `POST`, etc.)
*/
method(): string {
throw new Error('Not implemented');
}
abstract method(): string;
/**
* The request's post body, if any.
*/
postData(): string | undefined {
throw new Error('Not implemented');
}
abstract postData(): string | undefined;
/**
* An object with HTTP headers associated with the request. All
* header names are lower-case.
*/
headers(): Record<string, string> {
throw new Error('Not implemented');
}
abstract headers(): Record<string, string>;
/**
* A matching `HTTPResponse` object, or null if the response has not
* been received yet.
*/
response(): HTTPResponse | null {
throw new Error('Not implemented');
}
abstract response(): HTTPResponse | null;
/**
* The frame that initiated the request, or null if navigating to
* error pages.
*/
frame(): Frame | null {
throw new Error('Not implemented');
}
abstract frame(): Frame | null;
/**
* True if the request is the driver of the current frame's navigation.
*/
isNavigationRequest(): boolean {
throw new Error('Not implemented');
}
abstract isNavigationRequest(): boolean;
/**
* The initiator of the request.
*/
initiator(): Protocol.Network.Initiator | undefined {
throw new Error('Not implemented');
}
abstract initiator(): Protocol.Network.Initiator | undefined;
/**
* A `redirectChain` is a chain of requests initiated to fetch a resource.
@ -303,9 +267,7 @@ export class HTTPRequest {
* @returns the chain of requests - if a server responds with at least a
* single redirect, this chain will contain all requests that were redirected.
*/
redirectChain(): HTTPRequest[] {
throw new Error('Not implemented');
}
abstract redirectChain(): HTTPRequest[];
/**
* Access information about the request's failure.
@ -327,9 +289,7 @@ export class HTTPRequest {
* message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be
* failure text if the request fails.
*/
failure(): {errorText: string} | null {
throw new Error('Not implemented');
}
abstract failure(): {errorText: string} | null;
/**
* Continues request with optional request overrides.
@ -360,13 +320,10 @@ export class HTTPRequest {
* cooperative handling rules. Otherwise, intercept is resolved
* immediately.
*/
async continue(
abstract continue(
overrides?: ContinueRequestOverrides,
priority?: number
): Promise<void>;
async continue(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Fulfills a request with the given response.
@ -400,13 +357,10 @@ export class HTTPRequest {
* cooperative handling rules. Otherwise, intercept is resolved
* immediately.
*/
async respond(
abstract respond(
response: Partial<ResponseForRequest>,
priority?: number
): Promise<void>;
async respond(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Aborts a request.
@ -421,10 +375,7 @@ export class HTTPRequest {
* cooperative handling rules. Otherwise, intercept is resolved
* immediately.
*/
async abort(errorCode?: ErrorCode, priority?: number): Promise<void>;
async abort(): Promise<void> {
throw new Error('Not implemented');
}
abstract abort(errorCode?: ErrorCode, priority?: number): Promise<void>;
}
/**
@ -500,7 +451,7 @@ export function headersArray(
* List taken from {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml}
* with extra 306 and 418 codes.
*/
export const STATUS_TEXTS: Record<string, string | undefined> = {
export const STATUS_TEXTS: Record<string, string> = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',

View File

@ -14,12 +14,12 @@
* limitations under the License.
*/
import Protocol from 'devtools-protocol';
import type Protocol from 'devtools-protocol';
import {SecurityDetails} from '../common/SecurityDetails.js';
import type {SecurityDetails} from '../common/SecurityDetails.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
import type {Frame} from './Frame.js';
import type {HTTPRequest} from './HTTPRequest.js';
/**
* @public
@ -35,33 +35,22 @@ export interface RemoteAddress {
*
* @public
*/
export class HTTPResponse {
export abstract class HTTPResponse {
/**
* @internal
*/
constructor() {}
/**
* @internal
*/
_resolveBody(_err: Error | null): void {
throw new Error('Not implemented');
}
/**
* The IP address and port number used to connect to the remote
* server.
*/
remoteAddress(): RemoteAddress {
throw new Error('Not implemented');
}
abstract remoteAddress(): RemoteAddress;
/**
* The URL of the response.
*/
url(): string {
throw new Error('Not implemented');
}
abstract url(): string;
/**
* True if the response was successful (status in the range 200-299).
@ -75,47 +64,35 @@ export class HTTPResponse {
/**
* The status code of the response (e.g., 200 for a success).
*/
status(): number {
throw new Error('Not implemented');
}
abstract status(): number;
/**
* The status text of the response (e.g. usually an "OK" for a
* success).
*/
statusText(): string {
throw new Error('Not implemented');
}
abstract statusText(): string;
/**
* An object with HTTP headers associated with the response. All
* header names are lower-case.
*/
headers(): Record<string, string> {
throw new Error('Not implemented');
}
abstract headers(): Record<string, string>;
/**
* {@link SecurityDetails} if the response was received over the
* secure connection, or `null` otherwise.
*/
securityDetails(): SecurityDetails | null {
throw new Error('Not implemented');
}
abstract securityDetails(): SecurityDetails | null;
/**
* Timing information related to the response.
*/
timing(): Protocol.Network.ResourceTiming | null {
throw new Error('Not implemented');
}
abstract timing(): Protocol.Network.ResourceTiming | null;
/**
* Promise which resolves to a buffer with response body.
*/
buffer(): Promise<Buffer> {
throw new Error('Not implemented');
}
abstract buffer(): Promise<Buffer>;
/**
* Promise which resolves to a text representation of response body.
@ -141,30 +118,22 @@ export class HTTPResponse {
/**
* A matching {@link HTTPRequest} object.
*/
request(): HTTPRequest {
throw new Error('Not implemented');
}
abstract request(): HTTPRequest;
/**
* True if the response was served from either the browser's disk
* cache or memory cache.
*/
fromCache(): boolean {
throw new Error('Not implemented');
}
abstract fromCache(): boolean;
/**
* True if the response was served by a service worker.
*/
fromServiceWorker(): boolean {
throw new Error('Not implemented');
}
abstract fromServiceWorker(): boolean;
/**
* A {@link Frame} that initiated this response, or `null` if
* navigating to error pages.
*/
frame(): Frame | null {
throw new Error('Not implemented');
}
abstract frame(): Frame | null;
}

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {KeyInput} from '../common/USKeyboardLayout.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import {Point} from './ElementHandle.js';
import type {Point} from './ElementHandle.js';
/**
* @public
@ -87,7 +87,7 @@ export type KeyPressOptions = KeyDownOptions & KeyboardTypeOptions;
*
* @public
*/
export class Keyboard {
export abstract class Keyboard {
/**
* @internal
*/
@ -120,10 +120,10 @@ export class Keyboard {
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/
async down(key: KeyInput, options?: Readonly<KeyDownOptions>): Promise<void>;
async down(): Promise<void> {
throw new Error('Not implemented');
}
abstract down(
key: KeyInput,
options?: Readonly<KeyDownOptions>
): Promise<void>;
/**
* Dispatches a `keyup` event.
@ -132,10 +132,7 @@ export class Keyboard {
* See {@link KeyInput | KeyInput}
* for a list of all key names.
*/
async up(key: KeyInput): Promise<void>;
async up(): Promise<void> {
throw new Error('Not implemented');
}
abstract up(key: KeyInput): Promise<void>;
/**
* Dispatches a `keypress` and `input` event.
@ -153,10 +150,7 @@ export class Keyboard {
*
* @param char - Character to send into the page.
*/
async sendCharacter(char: string): Promise<void>;
async sendCharacter(): Promise<void> {
throw new Error('Not implemented');
}
abstract sendCharacter(char: string): Promise<void>;
/**
* Sends a `keydown`, `keypress`/`input`,
@ -181,13 +175,10 @@ export class Keyboard {
* if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
* Defaults to 0.
*/
async type(
abstract type(
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
async type(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Shortcut for {@link Keyboard.down}
@ -211,13 +202,10 @@ export class Keyboard {
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/
async press(
abstract press(
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void>;
async press(): Promise<void> {
throw new Error('Not implemented');
}
}
/**
@ -367,7 +355,7 @@ export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton];
*
* @public
*/
export class Mouse {
export abstract class Mouse {
/**
* @internal
*/
@ -377,9 +365,7 @@ export class Mouse {
* Resets the mouse to the default state: No buttons pressed; position at
* (0,0).
*/
async reset(): Promise<void> {
throw new Error('Not implemented');
}
abstract reset(): Promise<void>;
/**
* Moves the mouse to the given coordinate.
@ -388,34 +374,25 @@ export class Mouse {
* @param y - Vertical position of the mouse.
* @param options - Options to configure behavior.
*/
async move(
abstract move(
x: number,
y: number,
options?: Readonly<MouseMoveOptions>
): Promise<void>;
async move(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Presses the mouse.
*
* @param options - Options to configure behavior.
*/
async down(options?: Readonly<MouseOptions>): Promise<void>;
async down(): Promise<void> {
throw new Error('Not implemented');
}
abstract down(options?: Readonly<MouseOptions>): Promise<void>;
/**
* Releases the mouse.
*
* @param options - Options to configure behavior.
*/
async up(options?: Readonly<MouseOptions>): Promise<void>;
async up(): Promise<void> {
throw new Error('Not implemented');
}
abstract up(options?: Readonly<MouseOptions>): Promise<void>;
/**
* Shortcut for `mouse.move`, `mouse.down` and `mouse.up`.
@ -424,14 +401,11 @@ export class Mouse {
* @param y - Vertical position of the mouse.
* @param options - Options to configure behavior.
*/
async click(
abstract click(
x: number,
y: number,
options?: Readonly<MouseClickOptions>
): Promise<void>;
async click(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Dispatches a `mousewheel` event.
@ -455,50 +429,41 @@ export class Mouse {
* await page.mouse.wheel({deltaY: -100});
* ```
*/
async wheel(options?: Readonly<MouseWheelOptions>): Promise<void>;
async wheel(): Promise<void> {
throw new Error('Not implemented');
}
abstract wheel(options?: Readonly<MouseWheelOptions>): Promise<void>;
/**
* Dispatches a `drag` event.
* @param start - starting point for drag
* @param target - point to drag to
*/
async drag(start: Point, target: Point): Promise<Protocol.Input.DragData>;
async drag(): Promise<Protocol.Input.DragData> {
throw new Error('Not implemented');
}
abstract drag(start: Point, target: Point): Promise<Protocol.Input.DragData>;
/**
* Dispatches a `dragenter` event.
* @param target - point for emitting `dragenter` event
* @param data - drag data containing items and operations mask
*/
async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void>;
async dragEnter(): Promise<void> {
throw new Error('Not implemented');
}
abstract dragEnter(
target: Point,
data: Protocol.Input.DragData
): Promise<void>;
/**
* Dispatches a `dragover` event.
* @param target - point for emitting `dragover` event
* @param data - drag data containing items and operations mask
*/
async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void>;
async dragOver(): Promise<void> {
throw new Error('Not implemented');
}
abstract dragOver(
target: Point,
data: Protocol.Input.DragData
): Promise<void>;
/**
* Performs a dragenter, dragover, and drop in sequence.
* @param target - point to drop on
* @param data - drag data containing items and operations mask
*/
async drop(target: Point, data: Protocol.Input.DragData): Promise<void>;
async drop(): Promise<void> {
throw new Error('Not implemented');
}
abstract drop(target: Point, data: Protocol.Input.DragData): Promise<void>;
/**
* Performs a drag, dragenter, dragover, and drop in sequence.
@ -508,21 +473,18 @@ export class Mouse {
* if specified, is the time to wait between `dragover` and `drop` in milliseconds.
* Defaults to 0.
*/
async dragAndDrop(
abstract dragAndDrop(
start: Point,
target: Point,
options?: {delay?: number}
): Promise<void>;
async dragAndDrop(): Promise<void> {
throw new Error('Not implemented');
}
}
/**
* The Touchscreen class exposes touchscreen events.
* @public
*/
export class Touchscreen {
export abstract class Touchscreen {
/**
* @internal
*/
@ -533,9 +495,9 @@ export class Touchscreen {
* @param x - Horizontal position of the tap.
* @param y - Vertical position of the tap.
*/
async tap(x: number, y: number): Promise<void>;
async tap(): Promise<void> {
throw new Error('Not implemented');
async tap(x: number, y: number): Promise<void> {
await this.touchStart(x, y);
await this.touchEnd();
}
/**
@ -543,10 +505,7 @@ export class Touchscreen {
* @param x - Horizontal position of the tap.
* @param y - Vertical position of the tap.
*/
async touchStart(x: number, y: number): Promise<void>;
async touchStart(): Promise<void> {
throw new Error('Not implemented');
}
abstract touchStart(x: number, y: number): Promise<void>;
/**
* Dispatches a `touchMove` event.
@ -560,16 +519,10 @@ export class Touchscreen {
* {@link https://developer.chrome.com/blog/a-more-compatible-smoother-touch/#chromes-new-model-the-throttled-async-touchmove-model | throttles}
* touch move events.
*/
async touchMove(x: number, y: number): Promise<void>;
async touchMove(): Promise<void> {
throw new Error('Not implemented');
}
abstract touchMove(x: number, y: number): Promise<void>;
/**
* Dispatches a `touchend` event.
*/
async touchEnd(): Promise<void>;
async touchEnd(): Promise<void> {
throw new Error('Not implemented');
}
abstract touchEnd(): Promise<void>;
}

View File

@ -14,20 +14,15 @@
* limitations under the License.
*/
import Protocol from 'devtools-protocol';
import type Protocol from 'devtools-protocol';
import {Symbol} from '../../third_party/disposablestack/disposablestack.js';
import {
EvaluateFuncWith,
HandleFor,
HandleOr,
Moveable,
} from '../common/types.js';
import type {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {moveable} from '../util/decorators.js';
import {moveable, throwIfDisposed} from '../util/decorators.js';
import {disposeSymbol, asyncDisposeSymbol} from '../util/disposable.js';
import {ElementHandle} from './ElementHandle.js';
import {Realm} from './Realm.js';
import type {ElementHandle} from './ElementHandle.js';
import type {Realm} from './Realm.js';
/**
* Represents a reference to a JavaScript object. Instances can be created using
@ -51,9 +46,7 @@ import {Realm} from './Realm.js';
* @public
*/
@moveable
export abstract class JSHandle<T = unknown>
implements Disposable, AsyncDisposable, Moveable
{
export abstract class JSHandle<T = unknown> {
declare move: () => this;
/**
@ -74,9 +67,7 @@ export abstract class JSHandle<T = unknown>
/**
* @internal
*/
get disposed(): boolean {
throw new Error('Not implemented');
}
abstract get disposed(): boolean;
/**
* Evaluates the given function with the current handle as its first argument.
@ -124,6 +115,7 @@ export abstract class JSHandle<T = unknown>
/**
* @internal
*/
@throwIfDisposed()
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
@ -150,6 +142,7 @@ export abstract class JSHandle<T = unknown>
* children; // holds elementHandles to all children of document.body
* ```
*/
@throwIfDisposed()
async getProperties(): Promise<Map<string, JSHandle>> {
const propertyNames = await this.evaluate(object => {
const enumerableProperties = [];
@ -217,11 +210,13 @@ export abstract class JSHandle<T = unknown>
*/
abstract remoteObject(): Protocol.Runtime.RemoteObject;
[Symbol.dispose](): void {
/** @internal */
[disposeSymbol](): void {
return void this.dispose().catch(debugError);
}
[Symbol.asyncDispose](): Promise<void> {
/** @internal */
[asyncDisposeSymbol](): Promise<void> {
return this.dispose();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,13 +14,18 @@
* limitations under the License.
*/
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {EvaluateFunc, HandleFor, InnerLazyParams} from '../common/types.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {
EvaluateFunc,
HandleFor,
InnerLazyParams,
} from '../common/types.js';
import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {disposeSymbol} from '../util/disposable.js';
import {ElementHandle} from './ElementHandle.js';
import {Environment} from './Environment.js';
import {JSHandle} from './JSHandle.js';
import type {ElementHandle} from './ElementHandle.js';
import type {Environment} from './Environment.js';
import type {JSHandle} from './JSHandle.js';
/**
* @internal
@ -92,12 +97,15 @@ export abstract class Realm implements Disposable {
return await waitTask.result;
}
abstract adoptBackendNode(backendNodeId?: number): Promise<JSHandle<Node>>;
get disposed(): boolean {
return this.#disposed;
}
#disposed = false;
[Symbol.dispose](): void {
/** @internal */
[disposeSymbol](): void {
this.#disposed = true;
this.taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')

View File

@ -16,9 +16,10 @@
import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js';
import {Page} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js';
import {WebWorker} from '../common/WebWorker.js';
import type {Page} from '../api/Page.js';
import type {WebWorker} from '../cdp/WebWorker.js';
import type {CDPSession} from './CDPSession.js';
/**
* @public
@ -44,7 +45,7 @@ export enum TargetType {
* worker.
* @public
*/
export class Target {
export abstract class Target {
/**
* @internal
*/
@ -65,16 +66,12 @@ export class Target {
return null;
}
url(): string {
throw new Error('not implemented');
}
abstract url(): string;
/**
* Creates a Chrome Devtools Protocol session attached to the target.
*/
createCDPSession(): Promise<CDPSession> {
throw new Error('not implemented');
}
abstract createCDPSession(): Promise<CDPSession>;
/**
* Identifies what kind of target this is.
@ -83,28 +80,20 @@ export class Target {
*
* See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages.
*/
type(): TargetType {
throw new Error('not implemented');
}
abstract type(): TargetType;
/**
* Get the browser the target belongs to.
*/
browser(): Browser {
throw new Error('not implemented');
}
abstract browser(): Browser;
/**
* Get the browser context the target belongs to.
*/
browserContext(): BrowserContext {
throw new Error('not implemented');
}
abstract browserContext(): BrowserContext;
/**
* Get the target that opened this target. Top-level targets return `null`.
*/
opener(): Target | undefined {
throw new Error('not implemented');
}
abstract opener(): Target | undefined;
}

View File

@ -28,3 +28,4 @@ export * from './locators/locators.js';
export * from './Page.js';
export * from './Realm.js';
export * from './Target.js';
export * from './CDPSession.js';

View File

@ -1,97 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor} from '../../common/common.js';
import {Locator, VisibilityOption} from './locators.js';
/**
* @internal
*/
export abstract class DelegatedLocator<T, U> extends Locator<U> {
#delegate: Locator<T>;
constructor(delegate: Locator<T>) {
super();
this.#delegate = delegate;
this.copyOptions(this.#delegate);
}
protected get delegate(): Locator<T> {
return this.#delegate;
}
override setTimeout(timeout: number): DelegatedLocator<T, U> {
const locator = super.setTimeout(timeout) as DelegatedLocator<T, U>;
locator.#delegate = this.#delegate.setTimeout(timeout);
return locator;
}
override setVisibility<ValueType extends Node, NodeType extends Node>(
this: DelegatedLocator<ValueType, NodeType>,
visibility: VisibilityOption
): DelegatedLocator<ValueType, NodeType> {
const locator = super.setVisibility<NodeType>(
visibility
) as DelegatedLocator<ValueType, NodeType>;
locator.#delegate = locator.#delegate.setVisibility<ValueType>(visibility);
return locator;
}
override setWaitForEnabled<ValueType extends Node, NodeType extends Node>(
this: DelegatedLocator<ValueType, NodeType>,
value: boolean
): DelegatedLocator<ValueType, NodeType> {
const locator = super.setWaitForEnabled<NodeType>(
value
) as DelegatedLocator<ValueType, NodeType>;
locator.#delegate = this.#delegate.setWaitForEnabled(value);
return locator;
}
override setEnsureElementIsInTheViewport<
ValueType extends Element,
ElementType extends Element,
>(
this: DelegatedLocator<ValueType, ElementType>,
value: boolean
): DelegatedLocator<ValueType, ElementType> {
const locator = super.setEnsureElementIsInTheViewport<ElementType>(
value
) as DelegatedLocator<ValueType, ElementType>;
locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
return locator;
}
override setWaitForStableBoundingBox<
ValueType extends Element,
ElementType extends Element,
>(
this: DelegatedLocator<ValueType, ElementType>,
value: boolean
): DelegatedLocator<ValueType, ElementType> {
const locator = super.setWaitForStableBoundingBox<ElementType>(
value
) as DelegatedLocator<ValueType, ElementType>;
locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
return locator;
}
abstract override _clone(): DelegatedLocator<T, U>;
abstract override _wait(): Observable<HandleFor<U>>;
}

View File

@ -1,83 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Observable,
filter,
from,
map,
mergeMap,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/common.js';
import {DelegatedLocator} from './DelegatedLocator.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @public
*/
export type Predicate<From, To extends From = From> =
| ((value: From) => value is To)
| ((value: From) => Awaitable<boolean>);
/**
* @internal
*/
export type HandlePredicate<From, To extends From = From> =
| ((value: HandleFor<From>, signal?: AbortSignal) => value is HandleFor<To>)
| ((value: HandleFor<From>, signal?: AbortSignal) => Awaitable<boolean>);
/**
* @internal
*/
export class FilteredLocator<From, To extends From> extends DelegatedLocator<
From,
To
> {
#predicate: HandlePredicate<From, To>;
constructor(base: Locator<From>, predicate: HandlePredicate<From, To>) {
super(base);
this.#predicate = predicate;
}
override _clone(): FilteredLocator<From, To> {
return new FilteredLocator(
this.delegate.clone(),
this.#predicate
).copyOptions(this);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from(
Promise.resolve(this.#predicate(handle, options?.signal))
).pipe(
filter(value => {
return value;
}),
map(() => {
// SAFETY: It passed the predicate, so this is correct.
return handle as HandleFor<To>;
})
);
}),
throwIfEmpty()
);
}
}

View File

@ -1,69 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Observable,
defer,
from,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/types.js';
import {Frame} from '../Frame.js';
import {Page} from '../Page.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @internal
*/
export class FunctionLocator<T> extends Locator<T> {
static create<Ret>(
pageOrFrame: Page | Frame,
func: () => Awaitable<Ret>
): Locator<Ret> {
return new FunctionLocator<Ret>(pageOrFrame, func).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
#pageOrFrame: Page | Frame;
#func: () => Awaitable<T>;
private constructor(pageOrFrame: Page | Frame, func: () => Awaitable<T>) {
super();
this.#pageOrFrame = pageOrFrame;
this.#func = func;
}
override _clone(): FunctionLocator<T> {
return new FunctionLocator(this.#pageOrFrame, this.#func);
}
_wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
const signal = options?.signal;
return defer(() => {
return from(
this.#pageOrFrame.waitForFunction(this.#func, {
timeout: this.timeout,
signal,
})
);
}).pipe(throwIfEmpty());
}
}

View File

@ -1,773 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EMPTY,
Observable,
OperatorFunction,
catchError,
defaultIfEmpty,
defer,
filter,
first,
firstValueFrom,
from,
fromEvent,
identity,
ignoreElements,
map,
merge,
mergeMap,
noop,
pipe,
raceWith,
retry,
tap,
} from '../../../third_party/rxjs/rxjs.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {HandleFor} from '../../common/types.js';
import {debugError, timeout} from '../../common/util.js';
import {BoundingBox, ClickOptions, ElementHandle} from '../ElementHandle.js';
import {
Action,
AwaitedLocator,
FilteredLocator,
HandleMapper,
MappedLocator,
Mapper,
Predicate,
RaceLocator,
} from './locators.js';
/**
* For observables coming from promises, a delay is needed, otherwise RxJS will
* never yield in a permanent failure for a promise.
*
* We also don't want RxJS to do promise operations to often, so we bump the
* delay up to 100ms.
*
* @internal
*/
export const RETRY_DELAY = 100;
/**
* @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 {
signal?: AbortSignal;
}
/**
* @public
*/
export type LocatorClickOptions = ClickOptions & ActionOptions;
/**
* @public
*/
export interface LocatorScrollOptions extends ActionOptions {
scrollTop?: number;
scrollLeft?: number;
}
/**
* All the events that a locator instance may emit.
*
* @public
*/
export enum LocatorEmittedEvents {
/**
* Emitted every time before the locator performs an action on the located element(s).
*/
Action = 'action',
}
/**
* @public
*/
export interface LocatorEventObject {
[LocatorEmittedEvents.Action]: never;
}
/**
* Locators describe a strategy of locating objects and performing an action on
* them. If the action fails because the object is not ready for the action, the
* whole operation is retried. Various preconditions for a successful action are
* checked automatically.
*
* @public
*/
export abstract class Locator<T> extends EventEmitter {
/**
* Creates a race between multiple locators but ensures that only a single one
* acts.
*
* @public
*/
static race<Locators extends readonly unknown[] | []>(
locators: Locators
): Locator<AwaitedLocator<Locators[number]>> {
return RaceLocator.create(locators);
}
/**
* Used for nominally typing {@link Locator}.
*/
declare _?: T;
/**
* @internal
*/
protected visibility: VisibilityOption = null;
/**
* @internal
*/
protected _timeout = 30_000;
#ensureElementIsInTheViewport = true;
#waitForEnabled = true;
#waitForStableBoundingBox = true;
/**
* @internal
*/
protected operators = {
conditions: (
conditions: Array<Action<T, never>>,
signal?: AbortSignal
): OperatorFunction<HandleFor<T>, HandleFor<T>> => {
return mergeMap((handle: HandleFor<T>) => {
return merge(
...conditions.map(condition => {
return condition(handle, signal);
})
).pipe(defaultIfEmpty(handle));
});
},
retryAndRaceWithSignalAndTimer: <T>(
signal?: AbortSignal
): OperatorFunction<T, T> => {
const candidates = [];
if (signal) {
candidates.push(
fromEvent(signal, 'abort').pipe(
map(() => {
throw signal.reason;
})
)
);
}
candidates.push(timeout(this._timeout));
return pipe(
retry({delay: RETRY_DELAY}),
raceWith<T, never[]>(...candidates)
);
},
};
// Determines when the locator will timeout for actions.
get timeout(): number {
return this._timeout;
}
override on<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.on(eventName, handler);
}
override once<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.once(eventName, handler);
}
override off<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.off(eventName, handler);
}
setTimeout(timeout: number): Locator<T> {
const locator = this._clone();
locator._timeout = timeout;
return locator;
}
setVisibility<NodeType extends Node>(
this: Locator<NodeType>,
visibility: VisibilityOption
): Locator<NodeType> {
const locator = this._clone();
locator.visibility = visibility;
return locator;
}
setWaitForEnabled<NodeType extends Node>(
this: Locator<NodeType>,
value: boolean
): Locator<NodeType> {
const locator = this._clone();
locator.#waitForEnabled = value;
return locator;
}
setEnsureElementIsInTheViewport<ElementType extends Element>(
this: Locator<ElementType>,
value: boolean
): Locator<ElementType> {
const locator = this._clone();
locator.#ensureElementIsInTheViewport = value;
return locator;
}
setWaitForStableBoundingBox<ElementType extends Element>(
this: Locator<ElementType>,
value: boolean
): Locator<ElementType> {
const locator = this._clone();
locator.#waitForStableBoundingBox = value;
return locator;
}
/**
* @internal
*/
copyOptions<T>(locator: Locator<T>): this {
this._timeout = locator._timeout;
this.visibility = locator.visibility;
this.#waitForEnabled = locator.#waitForEnabled;
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
this.#waitForStableBoundingBox = locator.#waitForStableBoundingBox;
return this;
}
/**
* If the element has a "disabled" property, wait for the element to be
* enabled.
*/
#waitForEnabledIfNeeded = <ElementType extends Node>(
handle: HandleFor<ElementType>,
signal?: AbortSignal
): Observable<never> => {
if (!this.#waitForEnabled) {
return EMPTY;
}
return from(
handle.frame.waitForFunction(
element => {
if (!(element instanceof HTMLElement)) {
return true;
}
const isNativeFormControl = [
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
'OPTION',
'OPTGROUP',
].includes(element.nodeName);
return !isNativeFormControl || !element.hasAttribute('disabled');
},
{
timeout: this._timeout,
signal,
},
handle
)
).pipe(ignoreElements());
};
/**
* Compares the bounding box of the element for two consecutive animation
* frames and waits till they are the same.
*/
#waitForStableBoundingBoxIfNeeded = <ElementType extends Element>(
handle: HandleFor<ElementType>
): Observable<never> => {
if (!this.#waitForStableBoundingBox) {
return EMPTY;
}
return defer(() => {
// Note we don't use waitForFunction because that relies on RAF.
return from(
handle.evaluate(element => {
return new Promise<[BoundingBox, BoundingBox]>(resolve => {
window.requestAnimationFrame(() => {
const rect1 = element.getBoundingClientRect();
window.requestAnimationFrame(() => {
const rect2 = element.getBoundingClientRect();
resolve([
{
x: rect1.x,
y: rect1.y,
width: rect1.width,
height: rect1.height,
},
{
x: rect2.x,
y: rect2.y,
width: rect2.width,
height: rect2.height,
},
]);
});
});
});
})
);
}).pipe(
first(([rect1, rect2]) => {
return (
rect1.x === rect2.x &&
rect1.y === rect2.y &&
rect1.width === rect2.width &&
rect1.height === rect2.height
);
}),
retry({delay: RETRY_DELAY}),
ignoreElements()
);
};
/**
* Checks if the element is in the viewport and auto-scrolls it if it is not.
*/
#ensureElementIsInTheViewportIfNeeded = <ElementType extends Element>(
handle: HandleFor<ElementType>
): Observable<never> => {
if (!this.#ensureElementIsInTheViewport) {
return EMPTY;
}
return from(handle.isIntersectingViewport({threshold: 0})).pipe(
filter(isIntersectingViewport => {
return !isIntersectingViewport;
}),
mergeMap(() => {
return from(handle.scrollIntoView());
}),
mergeMap(() => {
return defer(() => {
return from(handle.isIntersectingViewport({threshold: 0}));
}).pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
})
);
};
#click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
this.#waitForEnabledIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(handle.click(options)).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#fill<ElementType extends Element>(
this: Locator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
this.#waitForEnabledIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(
(handle as unknown as ElementHandle<HTMLElement>).evaluate(el => {
if (el instanceof HTMLSelectElement) {
return 'select';
}
if (el instanceof HTMLTextAreaElement) {
return 'typeable-input';
}
if (el instanceof HTMLInputElement) {
if (
new Set([
'textarea',
'text',
'url',
'tel',
'search',
'password',
'number',
'email',
]).has(el.type)
) {
return 'typeable-input';
} else {
return 'other-input';
}
}
if (el.isContentEditable) {
return 'contenteditable';
}
return 'unknown';
})
)
.pipe(
mergeMap(inputType => {
switch (inputType) {
case 'select':
return from(handle.select(value).then(noop));
case 'contenteditable':
case 'typeable-input':
return from(
(
handle as unknown as ElementHandle<HTMLInputElement>
).evaluate((input, newValue) => {
const currentValue = input.isContentEditable
? input.innerText
: input.value;
// Clear the input if the current value does not match the filled
// out value.
if (
newValue.length <= currentValue.length ||
!newValue.startsWith(input.value)
) {
if (input.isContentEditable) {
input.innerText = '';
} else {
input.value = '';
}
return newValue;
}
const originalValue = input.isContentEditable
? input.innerText
: input.value;
// If the value is partially filled out, only type the rest. Move
// cursor to the end of the common prefix.
if (input.isContentEditable) {
input.innerText = '';
input.innerText = originalValue;
} else {
input.value = '';
input.value = originalValue;
}
return newValue.substring(originalValue.length);
}, value)
).pipe(
mergeMap(textToType => {
return from(handle.type(textToType));
})
);
case 'other-input':
return from(handle.focus()).pipe(
mergeMap(() => {
return from(
handle.evaluate((input, value) => {
(input as HTMLInputElement).value = value;
input.dispatchEvent(
new Event('input', {bubbles: true})
);
input.dispatchEvent(
new Event('change', {bubbles: true})
);
}, value)
);
})
);
case 'unknown':
throw new Error(`Element cannot be filled out.`);
}
})
)
.pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#hover<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<ActionOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(handle.hover()).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#scroll<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(
handle.evaluate(
(el, scrollTop, scrollLeft) => {
if (scrollTop !== undefined) {
el.scrollTop = scrollTop;
}
if (scrollLeft !== undefined) {
el.scrollLeft = scrollLeft;
}
},
options?.scrollTop,
options?.scrollLeft
)
).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
/**
* @internal
*/
abstract _clone(): Locator<T>;
/**
* @internal
*/
abstract _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>>;
/**
* Clones the locator.
*/
clone(): Locator<T> {
return this._clone();
}
/**
* Waits for the locator to get a handle from the page.
*
* @public
*/
async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
return await firstValueFrom(
this._wait(options).pipe(
this.operators.retryAndRaceWithSignalAndTimer(options?.signal)
)
);
}
/**
* Waits for the locator to get the serialized value from the page.
*
* Note this requires the value to be JSON-serializable.
*
* @public
*/
async wait(options?: Readonly<ActionOptions>): Promise<T> {
using handle = await this.waitHandle(options);
return await handle.jsonValue();
}
/**
* Maps the locator using the provided mapper.
*
* @public
*/
map<To>(mapper: Mapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), handle => {
// SAFETY: TypeScript cannot deduce the type.
return (handle as any).evaluateHandle(mapper);
});
}
/**
* Creates an expectation that is evaluated against located values.
*
* If the expectations do not match, then the locator will retry.
*
* @public
*/
filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
return new FilteredLocator(this._clone(), async (handle, signal) => {
await (handle as ElementHandle<Node>).frame.waitForFunction(
predicate,
{signal, timeout: this._timeout},
handle
);
return true;
});
}
/**
* Creates an expectation that is evaluated against located handles.
*
* If the expectations do not match, then the locator will retry.
*
* @internal
*/
filterHandle<S extends T>(
predicate: Predicate<HandleFor<T>, HandleFor<S>>
): Locator<S> {
return new FilteredLocator(this._clone(), predicate);
}
/**
* Maps the locator using the provided mapper.
*
* @internal
*/
mapHandle<To>(mapper: HandleMapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), mapper);
}
click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Promise<void> {
return firstValueFrom(this.#click(options));
}
/**
* 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.
*/
fill<ElementType extends Element>(
this: Locator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void> {
return firstValueFrom(this.#fill(value, options));
}
hover<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<ActionOptions>
): Promise<void> {
return firstValueFrom(this.#hover(options));
}
scroll<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Promise<void> {
return firstValueFrom(this.#scroll(options));
}
}

View File

@ -1,59 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable, from, mergeMap} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/common.js';
import {ActionOptions, DelegatedLocator, Locator} from './locators.js';
/**
* @public
*/
export type Mapper<From, To> = (value: From) => Awaitable<To>;
/**
* @internal
*/
export type HandleMapper<From, To> = (
value: HandleFor<From>,
signal?: AbortSignal
) => Awaitable<HandleFor<To>>;
/**
* @internal
*/
export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
#mapper: HandleMapper<From, To>;
constructor(base: Locator<From>, mapper: HandleMapper<From, To>) {
super(base);
this.#mapper = mapper;
}
override _clone(): MappedLocator<From, To> {
return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions(
this
);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from(Promise.resolve(this.#mapper(handle, options?.signal)));
})
);
}
}

View File

@ -1,117 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EMPTY,
Observable,
defer,
filter,
first,
from,
identity,
ignoreElements,
retry,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor, NodeFor} from '../../common/types.js';
import {Frame} from '../Frame.js';
import {Page} from '../Page.js';
import {ActionOptions, Locator, RETRY_DELAY} from './locators.js';
/**
* @internal
*/
export type Action<T, U> = (
element: HandleFor<T>,
signal?: AbortSignal
) => Observable<U>;
/**
* @internal
*/
export class NodeLocator<T extends Node> extends Locator<T> {
static create<Selector extends string>(
pageOrFrame: Page | Frame,
selector: Selector
): Locator<NodeFor<Selector>> {
return new NodeLocator<NodeFor<Selector>>(pageOrFrame, selector).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
#pageOrFrame: Page | Frame;
#selector: string;
private constructor(pageOrFrame: Page | Frame, selector: string) {
super();
this.#pageOrFrame = pageOrFrame;
this.#selector = selector;
}
/**
* Waits for the element to become visible or hidden. visibility === 'visible'
* means that the element has a computed style, the visibility property other
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
* 'hidden' means the opposite of that.
*/
#waitForVisibilityIfNeeded = (handle: HandleFor<T>): Observable<never> => {
if (!this.visibility) {
return EMPTY;
}
return (() => {
switch (this.visibility) {
case 'hidden':
return defer(() => {
return from(handle.isHidden());
});
case 'visible':
return defer(() => {
return from(handle.isVisible());
});
}
})().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
};
override _clone(): NodeLocator<T> {
return new NodeLocator<T>(this.#pageOrFrame, this.#selector).copyOptions(
this
);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
const signal = options?.signal;
return defer(() => {
return from(
this.#pageOrFrame.waitForSelector(this.#selector, {
visible: false,
timeout: this._timeout,
signal,
}) as Promise<HandleFor<T> | null>
);
}).pipe(
filter((value): value is NonNullable<typeof value> => {
return value !== null;
}),
throwIfEmpty(),
this.operators.conditions([this.#waitForVisibilityIfNeeded], signal)
);
}
}

View File

@ -1,71 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable, race} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor} from '../../puppeteer-core.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @public
*/
export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;
function checkLocatorArray<T extends readonly unknown[] | []>(
locators: T
): ReadonlyArray<Locator<AwaitedLocator<T[number]>>> {
for (const locator of locators) {
if (!(locator instanceof Locator)) {
throw new Error('Unknown locator for race candidate');
}
}
return locators as ReadonlyArray<Locator<AwaitedLocator<T[number]>>>;
}
/**
* @internal
*/
export class RaceLocator<T> extends Locator<T> {
static create<T extends readonly unknown[]>(
locators: T
): Locator<AwaitedLocator<T[number]>> {
const array = checkLocatorArray(locators);
return new RaceLocator(array);
}
#locators: ReadonlyArray<Locator<T>>;
constructor(locators: ReadonlyArray<Locator<T>>) {
super();
this.#locators = locators;
}
override _clone(): RaceLocator<T> {
return new RaceLocator<T>(
this.#locators.map(locator => {
return locator.clone();
})
).copyOptions(this);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
return race(
...this.#locators.map(locator => {
return locator._wait(options);
})
);
}
}

View File

@ -14,28 +14,30 @@
* limitations under the License.
*/
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js';
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {CDPSession, Connection as CDPPPtrConnection} from '../Connection.js';
import {TargetCloseError} from '../Errors.js';
import {EventEmitter, Handler} from '../EventEmitter.js';
import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {debug} from '../common/Debug.js';
import {TargetCloseError} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {Connection as BidiPPtrConnection} from './Connection.js';
import {BidiConnection} from './Connection.js';
type CdpEvents = {
[Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
debug(`bidi:${prefix}`)(args);
};
/**
* @internal
*/
export async function connectBidiOverCDP(
cdp: CDPPPtrConnection
): Promise<BidiPPtrConnection> {
export async function connectBidiOverCdp(
cdp: CdpConnection
): Promise<BidiConnection> {
const transportBiDi = new NoOpTransport();
const cdpConnectionAdapter = new CDPConnectionAdapter(cdp);
const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
const pptrTransport = {
send(message: string): void {
// Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
@ -53,11 +55,15 @@ export async function connectBidiOverCDP(
// Forwards a BiDi event sent by BidiServer to Puppeteer.
pptrTransport.onmessage(JSON.stringify(message));
});
const pptrBiDiConnection = new BidiPPtrConnection(cdp.url(), pptrTransport);
const pptrBiDiConnection = new BidiConnection(cdp.url(), pptrTransport);
const bidiServer = await BidiMapper.BidiServer.createAndStart(
transportBiDi,
cdpConnectionAdapter,
''
// TODO: most likely need a little bit of refactoring
cdpConnectionAdapter.browserClient(),
'',
undefined,
bidiServerLogger
);
return pptrBiDiConnection;
}
@ -66,24 +72,24 @@ export async function connectBidiOverCDP(
* Manages CDPSessions for BidiServer.
* @internal
*/
class CDPConnectionAdapter {
#cdp: CDPPPtrConnection;
class CdpConnectionAdapter {
#cdp: CdpConnection;
#adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
#browser: CDPClientAdapter<CDPPPtrConnection>;
#browser: CDPClientAdapter<CdpConnection>;
constructor(cdp: CDPPPtrConnection) {
constructor(cdp: CdpConnection) {
this.#cdp = cdp;
this.#browser = new CDPClientAdapter(cdp);
}
browserClient(): CDPClientAdapter<CDPPPtrConnection> {
browserClient(): CDPClientAdapter<CdpConnection> {
return this.#browser;
}
getCdpClient(id: string) {
const session = this.#cdp.session(id);
if (!session) {
throw new Error('Unknown CDP session with id' + id);
throw new Error(`Unknown CDP session with id ${id}`);
}
if (!this.#adapters.has(session)) {
const adapter = new CDPClientAdapter(session, id, this.#browser);
@ -107,8 +113,8 @@ class CDPConnectionAdapter {
*
* @internal
*/
class CDPClientAdapter<T extends EventEmitter & Pick<CDPPPtrConnection, 'send'>>
extends BidiMapper.EventEmitter<CdpEvents>
class CDPClientAdapter<T extends CDPSession | CdpConnection>
extends BidiMapper.EventEmitter<CDPEvents>
implements BidiMapper.CdpClient
{
#closed = false;
@ -132,9 +138,9 @@ class CDPClientAdapter<T extends EventEmitter & Pick<CDPPPtrConnection, 'send'>>
return this.#browserClient!;
}
#forwardMessage = <T extends keyof CdpEvents>(
#forwardMessage = <T extends keyof CDPEvents>(
method: T,
event: CdpEvents[T]
event: CDPEvents[T]
) => {
this.emit(method, event);
};

View File

@ -14,40 +14,51 @@
* limitations under the License.
*/
import {ChildProcess} from 'child_process';
import type {ChildProcess} from 'child_process';
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {
Browser,
BrowserCloseCallback,
BrowserContextEmittedEvents,
BrowserContextOptions,
BrowserEmittedEvents,
} from '../../api/Browser.js';
import {Page} from '../../api/Page.js';
import {Target} from '../../api/Target.js';
import {Handler} from '../EventEmitter.js';
import {Viewport} from '../PuppeteerViewport.js';
BrowserEvent,
type BrowserCloseCallback,
type BrowserContextOptions,
} from '../api/Browser.js';
import {BrowserContextEvent} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {BidiBrowserContext} from './BrowserContext.js';
import {
BrowsingContext,
BrowsingContextEmittedEvents,
} from './BrowsingContext.js';
import {Connection} from './Connection.js';
import {BrowsingContext, BrowsingContextEvent} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import {
BiDiBrowserTarget,
BiDiBrowsingContextTarget,
BiDiPageTarget,
BidiTarget,
type BidiTarget,
} from './Target.js';
import {debugError} from './utils.js';
/**
* @internal
*/
export interface BidiBrowserOptions {
process?: ChildProcess;
closeCallback?: BrowserCloseCallback;
connection: BidiConnection;
defaultViewport: Viewport | null;
ignoreHTTPSErrors?: boolean;
}
/**
* @internal
*/
export class BidiBrowser extends Browser {
readonly protocol = 'webDriverBiDi';
// TODO: Update generator to include fully module
static readonly subscribeModules: string[] = [
'browsingContext',
@ -65,9 +76,10 @@ export class BidiBrowser extends Browser {
// TODO: subscribe to all CDP events in the future.
'cdp.Network.requestWillBeSent',
'cdp.Debugger.scriptParsed',
'cdp.Page.screencastFrame',
];
static async create(opts: Options): Promise<BidiBrowser> {
static async create(opts: BidiBrowserOptions): Promise<BidiBrowser> {
let browserName = '';
let browserVersion = '';
@ -108,7 +120,7 @@ export class BidiBrowser extends Browser {
#browserVersion = '';
#process?: ChildProcess;
#closeCallback?: BrowserCloseCallback;
#connection: Connection;
#connection: BidiConnection;
#defaultViewport: Viewport | null;
#defaultContext: BidiBrowserContext;
#targets = new Map<string, BidiTarget>();
@ -127,7 +139,7 @@ export class BidiBrowser extends Browser {
]);
constructor(
opts: Options & {
opts: BidiBrowserOptions & {
browserName: string;
browserVersion: string;
}
@ -142,7 +154,7 @@ export class BidiBrowser extends Browser {
this.#process?.once('close', () => {
this.#connection.dispose();
this.emit(BrowserEmittedEvents.Disconnected);
this.emit(BrowserEvent.Disconnected, undefined);
});
this.#defaultContext = new BidiBrowserContext(this, {
defaultViewport: this.#defaultViewport,
@ -156,20 +168,22 @@ export class BidiBrowser extends Browser {
}
}
override userAgent(): never {
throw new UnsupportedOperation();
}
#onContextDomLoaded(event: Bidi.BrowsingContext.Info) {
const target = this.#targets.get(event.context);
if (target) {
this.emit(BrowserEmittedEvents.TargetChanged, target);
this.emit(BrowserEvent.TargetChanged, target);
}
}
#onContextNavigation(event: Bidi.BrowsingContext.NavigationInfo) {
const target = this.#targets.get(event.context);
if (target) {
this.emit(BrowserEmittedEvents.TargetChanged, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetChanged, target);
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
}
}
@ -192,14 +206,12 @@ export class BidiBrowser extends Browser {
: new BiDiBrowsingContextTarget(browserContext, context);
this.#targets.set(event.context, target);
this.emit(BrowserEmittedEvents.TargetCreated, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetCreated, target);
this.emit(BrowserEvent.TargetCreated, target);
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
if (context.parent) {
const topLevel = this.#connection.getTopLevelContext(context.parent);
topLevel.emit(BrowsingContextEmittedEvents.Created, context);
topLevel.emit(BrowsingContextEvent.Created, context);
}
}
@ -215,20 +227,18 @@ export class BidiBrowser extends Browser {
) {
const context = this.#connection.getBrowsingContext(event.context);
const topLevelContext = this.#connection.getTopLevelContext(event.context);
topLevelContext.emit(BrowsingContextEmittedEvents.Destroyed, context);
topLevelContext.emit(BrowsingContextEvent.Destroyed, context);
const target = this.#targets.get(event.context);
const page = await target?.page();
await page?.close().catch(debugError);
this.#targets.delete(event.context);
if (target) {
this.emit(BrowserEmittedEvents.TargetDestroyed, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetDestroyed, target);
this.emit(BrowserEvent.TargetDestroyed, target);
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
}
}
get connection(): Connection {
get connection(): BidiConnection {
return this.#connection;
}
@ -243,13 +253,12 @@ export class BidiBrowser extends Browser {
if (this.#connection.closed) {
return;
}
// TODO: implement browser.close.
// await this.#connection.send('browser.close', {});
await this.#connection.send('browser.close', {});
this.#connection.dispose();
await this.#closeCallback?.call(null);
}
override isConnected(): boolean {
override get connected(): boolean {
return !this.#connection.closed;
}
@ -273,10 +282,6 @@ export class BidiBrowser extends Browser {
return `${this.#browserName}/${this.#browserVersion}`;
}
/**
* Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BidiBrowserContext}.
*/
override browserContexts(): BidiBrowserContext[] {
// TODO: implement incognito context https://github.com/w3c/webdriver-bidi/issues/289.
return this.#contexts;
@ -294,9 +299,6 @@ export class BidiBrowser extends Browser {
}
}
/**
* Returns the default browser context. The default browser context cannot be closed.
*/
override defaultBrowserContext(): BidiBrowserContext {
return this.#defaultContext;
}
@ -320,12 +322,8 @@ export class BidiBrowser extends Browser {
override target(): Target {
return this.#browserTarget;
}
}
interface Options {
process?: ChildProcess;
closeCallback?: BrowserCloseCallback;
connection: Connection;
defaultViewport: Viewport | null;
ignoreHTTPSErrors?: boolean;
override disconnect(): void {
this;
}
}

View File

@ -16,16 +16,21 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {BrowserContext} from '../../api/BrowserContext.js';
import {Page} from '../../api/Page.js';
import {Target} from '../../api/Target.js';
import {Viewport} from '../PuppeteerViewport.js';
import type {WaitForTargetOptions} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {Viewport} from '../common/Viewport.js';
import {BidiBrowser} from './Browser.js';
import {Connection} from './Connection.js';
import {BidiPage} from './Page.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiConnection} from './Connection.js';
import type {BidiPage} from './Page.js';
interface BrowserContextOptions {
/**
* @internal
*/
export interface BidiBrowserContextOptions {
defaultViewport: Viewport | null;
isDefault: boolean;
}
@ -35,11 +40,11 @@ interface BrowserContextOptions {
*/
export class BidiBrowserContext extends BrowserContext {
#browser: BidiBrowser;
#connection: Connection;
#connection: BidiConnection;
#defaultViewport: Viewport | null;
#isDefault = false;
constructor(browser: BidiBrowser, options: BrowserContextOptions) {
constructor(browser: BidiBrowser, options: BidiBrowserContextOptions) {
super();
this.#browser = browser;
this.#connection = this.#browser.connection;
@ -55,14 +60,14 @@ export class BidiBrowserContext extends BrowserContext {
override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {}
options: WaitForTargetOptions = {}
): Promise<Target> {
return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target);
}, options);
}
get connection(): Connection {
get connection(): BidiConnection {
return this.#connection;
}
@ -120,4 +125,12 @@ export class BidiBrowserContext extends BrowserContext {
override isIncognito(): boolean {
return !this.#isDefault;
}
override overridePermissions(): never {
throw new UnsupportedOperation();
}
override clearPermissionOverrides(): never {
throw new UnsupportedOperation();
}
}

View File

@ -0,0 +1,187 @@
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';
import {CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {EventType} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import type {BidiConnection} from './Connection.js';
import {BidiRealm} from './Realm.js';
/**
* @internal
*/
export const cdpSessions = new Map<string, CdpSessionWrapper>();
/**
* @internal
*/
export class CdpSessionWrapper extends CDPSession {
#context: BrowsingContext;
#sessionId = Deferred.create<string>();
#detached = false;
constructor(context: BrowsingContext, sessionId?: string) {
super();
this.#context = context;
if (!this.#context.supportsCdp()) {
return;
}
if (sessionId) {
this.#sessionId.resolve(sessionId);
cdpSessions.set(sessionId, this);
} else {
context.connection
.send('cdp.getSession', {
context: context.id,
})
.then(session => {
this.#sessionId.resolve(session.result.session!);
cdpSessions.set(session.result.session!, this);
})
.catch(err => {
this.#sessionId.reject(err);
});
}
}
override connection(): CdpConnection | undefined {
return undefined;
}
override async send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#context.supportsCdp()) {
throw new UnsupportedOperation(
'CDP support is required for this feature. The current browser does not support CDP.'
);
}
if (this.#detached) {
throw new TargetCloseError(
`Protocol error (${method}): Session closed. Most likely the page has been closed.`
);
}
const session = await this.#sessionId.valueOrThrow();
const {result} = await this.#context.connection.send('cdp.sendCommand', {
method: method,
params: paramArgs[0],
session,
});
return result.result;
}
override async detach(): Promise<void> {
cdpSessions.delete(this.id());
if (!this.#detached && this.#context.supportsCdp()) {
await this.#context.cdpSession.send('Target.detachFromTarget', {
sessionId: this.id(),
});
}
this.#detached = true;
}
override id(): string {
const val = this.#sessionId.value();
return val instanceof Error || val === undefined ? '' : val;
}
}
/**
* Internal events that the BrowsingContext class emits.
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace BrowsingContextEvent {
/**
* Emitted on the top-level context, when a descendant context is created.
*/
export const Created = Symbol('BrowsingContext.created');
/**
* Emitted on the top-level context, when a descendant context or the
* top-level context itself is destroyed.
*/
export const Destroyed = Symbol('BrowsingContext.destroyed');
}
/**
* @internal
*/
export interface BrowsingContextEvents extends Record<EventType, unknown> {
[BrowsingContextEvent.Created]: BrowsingContext;
[BrowsingContextEvent.Destroyed]: BrowsingContext;
}
/**
* @internal
*/
export class BrowsingContext extends BidiRealm {
#id: string;
#url: string;
#cdpSession: CDPSession;
#parent?: string | null;
#browserName = '';
constructor(
connection: BidiConnection,
info: Bidi.BrowsingContext.Info,
browserName: string
) {
super(connection);
this.#id = info.context;
this.#url = info.url;
this.#parent = info.parent;
this.#browserName = browserName;
this.#cdpSession = new CdpSessionWrapper(this, undefined);
this.on('browsingContext.domContentLoaded', this.#updateUrl.bind(this));
this.on('browsingContext.fragmentNavigated', this.#updateUrl.bind(this));
this.on('browsingContext.load', this.#updateUrl.bind(this));
}
supportsCdp(): boolean {
return !this.#browserName.toLowerCase().includes('firefox');
}
#updateUrl(info: Bidi.BrowsingContext.NavigationInfo) {
this.#url = info.url;
}
createRealmForSandbox(): BidiRealm {
return new BidiRealm(this.connection);
}
get url(): string {
return this.#url;
}
get id(): string {
return this.#id;
}
get parent(): string | undefined | null {
return this.#parent;
}
get cdpSession(): CDPSession {
return this.#cdpSession;
}
async sendCdpCommand<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
return await this.#cdpSession.send(method, ...paramArgs);
}
dispose(): void {
this.removeAllListeners();
this.connection.unregisterBrowsingContexts(this.#id);
void this.#cdpSession.detach().catch(debugError);
}
}

View File

@ -18,9 +18,9 @@ import {describe, it} from 'node:test';
import expect from 'expect';
import {ConnectionTransport} from '../ConnectionTransport.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {Connection} from './Connection.js';
import {BidiConnection} from './Connection.js';
describe('WebDriver BiDi Connection', () => {
class TestConnectionTransport implements ConnectionTransport {
@ -38,7 +38,7 @@ describe('WebDriver BiDi Connection', () => {
it('should work', async () => {
const transport = new TestConnectionTransport();
const connection = new Connection('ws://127.0.0.1', transport);
const connection = new BidiConnection('ws://127.0.0.1', transport);
const responsePromise = connection.send('session.new', {
capabilities: {},
});
@ -48,6 +48,7 @@ describe('WebDriver BiDi Connection', () => {
const id = JSON.parse(transport.sent[0]!).id;
const rawResponse = {
id,
type: 'success',
result: {ready: false, message: 'already connected'},
};
(transport as ConnectionTransport).onmessage?.(JSON.stringify(rawResponse));

View File

@ -14,15 +14,15 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {CallbackRegistry} from '../Connection.js';
import {ConnectionTransport} from '../ConnectionTransport.js';
import {debug} from '../Debug.js';
import {EventEmitter} from '../EventEmitter.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import {debug} from '../common/Debug.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {BrowsingContext, cdpSessions} from './BrowsingContext.js';
import {debugError} from './utils.js';
import {type BrowsingContext, cdpSessions} from './BrowsingContext.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
@ -30,7 +30,7 @@ const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
/**
* @internal
*/
interface Commands {
export interface Commands {
'script.evaluate': {
params: Bidi.Script.EvaluateParameters;
returnType: Bidi.Script.EvaluateResult;
@ -52,6 +52,11 @@ interface Commands {
returnType: Bidi.EmptyResult;
};
'browser.close': {
params: Bidi.EmptyParams;
returnType: Bidi.EmptyResult;
};
'browsingContext.activate': {
params: Bidi.BrowsingContext.ActivateParameters;
returnType: Bidi.EmptyResult;
@ -74,7 +79,7 @@ interface Commands {
};
'browsingContext.reload': {
params: Bidi.BrowsingContext.ReloadParameters;
returnType: Bidi.EmptyResult;
returnType: Bidi.BrowsingContext.NavigateResult;
};
'browsingContext.print': {
params: Bidi.BrowsingContext.PrintParameters;
@ -88,6 +93,10 @@ interface Commands {
params: Bidi.BrowsingContext.HandleUserPromptParameters;
returnType: Bidi.EmptyResult;
};
'browsingContext.setViewport': {
params: Bidi.BrowsingContext.SetViewportParameters;
returnType: Bidi.EmptyResult;
};
'input.performActions': {
params: Bidi.Input.PerformActionsParameters;
@ -127,7 +136,17 @@ interface Commands {
/**
* @internal
*/
export class Connection extends EventEmitter {
export type BidiEvents = {
[K in Bidi.ChromiumBidi.Event['method']]: Extract<
Bidi.ChromiumBidi.Event,
{method: K}
>['params'];
};
/**
* @internal
*/
export class BidiConnection extends EventEmitter<BidiEvents> {
#url: string;
#transport: ConnectionTransport;
#delay: number;
@ -185,37 +204,47 @@ export class Connection extends EventEmitter {
});
}
debugProtocolReceive(message);
const object = JSON.parse(message) as Bidi.ChromiumBidi.Message;
if ('id' in object && object.id) {
if ('error' in object) {
this.#callbacks.reject(
object.id,
createProtocolError(object as Bidi.ErrorResponse),
object.message
);
} else {
this.#callbacks.resolve(object.id, object);
}
} else {
if ('error' in object || 'id' in object || 'launched' in object) {
debugError(object);
} else {
this.#maybeEmitOnContext(object);
this.emit(object.method, object.params);
const object: Bidi.ChromiumBidi.Message = JSON.parse(message);
if ('type' in object) {
switch (object.type) {
case 'success':
this.#callbacks.resolve(object.id, object);
return;
case 'error':
if (object.id === null) {
break;
}
this.#callbacks.reject(
object.id,
createProtocolError(object),
object.message
);
return;
case 'event':
this.#maybeEmitOnContext(object);
// SAFETY: We know the method and parameter still match here.
this.emit(
object.method,
object.params as BidiEvents[keyof BidiEvents]
);
return;
}
}
debugError(object);
}
#maybeEmitOnContext(event: Bidi.ChromiumBidi.Event) {
let context: BrowsingContext | undefined;
// Context specific events
if ('context' in event.params && event.params.context) {
if ('context' in event.params && event.params.context !== null) {
context = this.#browsingContexts.get(event.params.context);
// `log.entryAdded` specific context
} else if ('source' in event.params && event.params.source.context) {
} else if (
'source' in event.params &&
event.params.source.context !== undefined
) {
context = this.#browsingContexts.get(event.params.source.context);
} else if (isCDPEvent(event)) {
} else if (isCdpEvent(event)) {
cdpSessions
.get(event.params.session)
?.emit(event.params.event, event.params.params);
@ -259,8 +288,10 @@ export class Connection extends EventEmitter {
return;
}
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
// Both may still be invoked and produce errors
this.#transport.onmessage = () => {};
this.#transport.onclose = () => {};
this.#callbacks.clear();
}
@ -281,6 +312,6 @@ function createProtocolError(object: Bidi.ErrorResponse): string {
return message;
}
function isCDPEvent(event: Bidi.ChromiumBidi.Event): event is Bidi.Cdp.Event {
function isCdpEvent(event: Bidi.ChromiumBidi.Event): event is Bidi.Cdp.Event {
return event.method.startsWith('cdp.');
}

View File

@ -0,0 +1,106 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debugError} from '../common/util.js';
/**
* @internal
*/
export class BidiDeserializer {
static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
switch (value) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
return value;
}
}
static deserializeLocalValue(result: Bidi.Script.RemoteValue): unknown {
switch (result.type) {
case 'array':
return result.value?.map(value => {
return BidiDeserializer.deserializeLocalValue(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
return acc.add(BidiDeserializer.deserializeLocalValue(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
const {key, value} = BidiDeserializer.deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
case 'map':
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
const {key, value} = BidiDeserializer.deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
return {};
case 'regexp':
return new RegExp(result.value.pattern, result.value.flags);
case 'date':
return new Date(result.value);
case 'undefined':
return undefined;
case 'null':
return null;
case 'number':
return BidiDeserializer.deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
return Boolean(result.value);
case 'string':
return result.value;
}
debugError(`Deserialization of type ${result.type} not supported.`);
return undefined;
}
static deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
: BidiDeserializer.deserializeLocalValue(serializedKey);
const value = BidiDeserializer.deserializeLocalValue(serializedValue);
return {key, value};
}
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result) {
debugError('Service did not produce a result.');
return undefined;
}
return BidiDeserializer.deserializeLocalValue(result);
}
}

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Dialog} from '../../api/Dialog.js';
import {Dialog} from '../api/Dialog.js';
import {BrowsingContext} from './BrowsingContext.js';
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal

View File

@ -14,14 +14,16 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {AutofillData, ElementHandle} from '../../api/ElementHandle.js';
import {type AutofillData, ElementHandle} from '../api/ElementHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {throwIfDisposed} from '../util/decorators.js';
import {BidiFrame} from './Frame.js';
import type {BidiFrame} from './Frame.js';
import {BidiJSHandle} from './JSHandle.js';
import {Realm} from './Realm.js';
import {Sandbox} from './Sandbox.js';
import type {BidiRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
/**
* @internal
@ -43,7 +45,7 @@ export class BidiElementHandle<
return this.realm.environment;
}
context(): Realm {
context(): BidiRealm {
return this.handle.context();
}
@ -55,6 +57,7 @@ export class BidiElementHandle<
return this.handle.remoteValue();
}
@throwIfDisposed()
override async autofill(data: AutofillData): Promise<void> {
const client = this.frame.client;
const nodeInfo = await client.send('DOM.describeNode', {
@ -72,6 +75,7 @@ export class BidiElementHandle<
override async contentFrame(
this: BidiElementHandle<HTMLIFrameElement>
): Promise<BidiFrame>;
@throwIfDisposed()
@ElementHandle.bindIsolatedHandle
override async contentFrame(): Promise<BidiFrame | null> {
using handle = (await this.evaluateHandle(element => {
@ -86,4 +90,8 @@ export class BidiElementHandle<
}
return null;
}
override uploadFile(this: ElementHandle<HTMLInputElement>): never {
throw new UnsupportedOperation();
}
}

View File

@ -13,13 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Viewport} from '../common/Viewport.js';
import {BrowsingContext} from './BrowsingContext.js';
interface Viewport {
width: number;
height: number;
}
import type {BrowsingContext} from './BrowsingContext.js';
/**
* @internal
@ -32,12 +28,18 @@ export class EmulationManager {
}
async emulateViewport(viewport: Viewport): Promise<void> {
await this.#browsingContext.connection.send(
'browsingContext.setViewport' as any,
{
context: this.#browsingContext.id,
viewport,
}
);
await this.#browsingContext.connection.send('browsingContext.setViewport', {
context: this.#browsingContext.id,
viewport:
viewport.width && viewport.height
? {
width: viewport.width,
height: viewport.height,
}
: null,
devicePixelRatio: viewport.deviceScaleFactor
? viewport.deviceScaleFactor
: null,
});
}
}

View File

@ -16,15 +16,16 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {assert} from '../../util/assert.js';
import {Deferred} from '../../util/Deferred.js';
import {interpolateFunction, stringifyFunction} from '../../util/Function.js';
import {Awaitable, FlattenHandle} from '../types.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import {Connection} from './Connection.js';
import {BidiFrame} from './Frame.js';
import type {BidiConnection} from './Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiFrame} from './Frame.js';
import {BidiSerializer} from './Serializer.js';
import {debugError} from './utils.js';
type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
type SendResolveChannel<Ret> = (
@ -39,6 +40,9 @@ interface RemotePromiseCallbacks {
reject: Deferred<Bidi.Script.RemoteValue>;
}
/**
* @internal
*/
export class ExposeableFunction<Args extends unknown[], Ret> {
readonly #frame;
@ -51,6 +55,8 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
Map<number, RemotePromiseCallbacks>
>();
#preloadScriptId?: Bidi.Script.PreloadScript;
constructor(
frame: BidiFrame,
name: string,
@ -61,15 +67,9 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
this.#apply = apply;
this.#channels = {
args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
this.name
}_args`,
resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
this.name
}_resolve`,
reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
this.name
}_reject`,
args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_args`,
resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_resolve`,
reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_reject`,
};
}
@ -117,17 +117,26 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
)
);
await connection.send('script.addPreloadScript', {
const {result} = await connection.send('script.addPreloadScript', {
functionDeclaration,
arguments: channelArguments,
contexts: [this.#frame.page().mainFrame()._id],
});
this.#preloadScriptId = result.script;
await connection.send('script.callFunction', {
functionDeclaration,
arguments: channelArguments,
awaitPromise: false,
target: this.#frame.mainRealm().realm.target,
});
await Promise.all(
this.#frame
.page()
.frames()
.map(async frame => {
return await connection.send('script.callFunction', {
functionDeclaration,
arguments: channelArguments,
awaitPromise: false,
target: frame.mainRealm().realm.target,
});
})
);
}
#handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
@ -139,7 +148,7 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
const args = remoteValue.value?.[1];
assert(args);
try {
const result = await this.#apply(...BidiSerializer.deserialize(args));
const result = await this.#apply(...BidiDeserializer.deserialize(args));
await connection.send('script.callFunction', {
functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
resolve(result);
@ -200,7 +209,7 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
};
get #connection(): Connection {
get #connection(): BidiConnection {
return this.#frame.context().connection;
}
@ -273,4 +282,16 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
return {callbacks, remoteValue: data};
}
[Symbol.dispose](): void {
void this[Symbol.asyncDispose]().catch(debugError);
}
async [Symbol.asyncDispose](): Promise<void> {
if (this.#preloadScriptId) {
await this.#connection.send('script.removePreloadScript', {
script: this.#preloadScriptId,
});
}
}
}

View File

@ -0,0 +1,273 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {
type Observable,
from,
fromEvent,
merge,
map,
forkJoin,
first,
firstValueFrom,
raceWith,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import {
Frame,
type GoToOptions,
type WaitForOptions,
throwIfDetached,
} from '../api/Frame.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {Awaitable} from '../common/types.js';
import {UTILITY_WORLD_NAME, setPageContent, timeout} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {ExposeableFunction} from './ExposedFunction.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {
getBiDiLifecycleEvent,
getBiDiReadinessState,
rewriteNavigationError,
} from './lifecycle.js';
import type {BidiPage} from './Page.js';
import {
MAIN_SANDBOX,
PUPPETEER_SANDBOX,
Sandbox,
type SandboxChart,
} from './Sandbox.js';
/**
* Puppeteer's Frame class could be viewed as a BiDi BrowsingContext implementation
* @internal
*/
export class BidiFrame extends Frame {
#page: BidiPage;
#context: BrowsingContext;
#timeoutSettings: TimeoutSettings;
#abortDeferred = Deferred.create<never>();
#disposed = false;
sandboxes: SandboxChart;
override _id: string;
constructor(
page: BidiPage,
context: BrowsingContext,
timeoutSettings: TimeoutSettings,
parentId?: string | null
) {
super();
this.#page = page;
this.#context = context;
this.#timeoutSettings = timeoutSettings;
this._id = this.#context.id;
this._parentId = parentId ?? undefined;
this.sandboxes = {
[MAIN_SANDBOX]: new Sandbox(undefined, this, context, timeoutSettings),
[PUPPETEER_SANDBOX]: new Sandbox(
UTILITY_WORLD_NAME,
this,
context.createRealmForSandbox(),
timeoutSettings
),
};
}
override get client(): CDPSession {
return this.context().cdpSession;
}
override mainRealm(): Sandbox {
return this.sandboxes[MAIN_SANDBOX];
}
override isolatedRealm(): Sandbox {
return this.sandboxes[PUPPETEER_SANDBOX];
}
override page(): BidiPage {
return this.#page;
}
override isOOPFrame(): never {
throw new UnsupportedOperation();
}
override url(): string {
return this.#context.url;
}
override parentFrame(): BidiFrame | null {
return this.#page.frame(this._parentId ?? '');
}
override childFrames(): BidiFrame[] {
return this.#page.childFrames(this.#context.id);
}
@throwIfDetached
override async goto(
url: string,
options: GoToOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const response = await firstValueFrom(
this.#page
._waitWithNetworkIdle(
this.#context.connection.send('browsingContext.navigate', {
context: this.#context.id,
url,
wait: readiness,
}),
networkIdle
)
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())))
.pipe(rewriteNavigationError(url, ms))
);
return this.#page.getNavigationResponse(response?.result.navigation);
}
@throwIfDetached
override async setContent(
html: string,
options: WaitForOptions = {}
): Promise<void> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [waitEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
await firstValueFrom(
this.#page
._waitWithNetworkIdle(
forkJoin([
fromEvent(this.#context, waitEvent).pipe(first()),
from(setPageContent(this, html)),
]).pipe(
map(() => {
return null;
})
),
networkIdle
)
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())))
.pipe(rewriteNavigationError('setContent', ms))
);
}
context(): BrowsingContext {
return this.#context;
}
@throwIfDetached
override async waitForNavigation(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this.#timeoutSettings.navigationTimeout(),
} = options;
const [waitUntilEvent, networkIdle] = getBiDiLifecycleEvent(waitUntil);
const navigatedObservable = merge(
forkJoin([
fromEvent(
this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted
).pipe(first()),
fromEvent(this.#context, waitUntilEvent).pipe(
first()
) as Observable<Bidi.BrowsingContext.NavigationInfo>,
]),
fromEvent(
this.#context,
Bidi.ChromiumBidi.BrowsingContext.EventNames.FragmentNavigated
) as Observable<Bidi.BrowsingContext.NavigationInfo>
).pipe(
map(result => {
if (Array.isArray(result)) {
return {result: result[1]};
}
return {result};
})
);
const response = await firstValueFrom(
this.#page
._waitWithNetworkIdle(navigatedObservable, networkIdle)
.pipe(raceWith(timeout(ms), from(this.#abortDeferred.valueOrThrow())))
);
return this.#page.getNavigationResponse(response?.result.navigation);
}
override waitForDevicePrompt(): never {
throw new UnsupportedOperation();
}
override get detached(): boolean {
return this.#disposed;
}
[disposeSymbol](): void {
if (this.#disposed) {
return;
}
this.#disposed = true;
this.#abortDeferred.reject(new Error('Frame detached'));
this.#context.dispose();
this.sandboxes[MAIN_SANDBOX][disposeSymbol]();
this.sandboxes[PUPPETEER_SANDBOX][disposeSymbol]();
}
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
async exposeFunction<Args extends unknown[], Ret>(
name: string,
apply: (...args: Args) => Awaitable<Ret>
): Promise<void> {
if (this.#exposedFunctions.has(name)) {
throw new Error(
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`
);
}
const exposeable = new ExposeableFunction(this, name, apply);
this.#exposedFunctions.set(name, exposeable);
try {
await exposeable.expose();
} catch (error) {
this.#exposedFunctions.delete(name);
throw error;
}
}
}

View File

@ -13,22 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Frame} from '../../api/Frame.js';
import {
HTTPRequest as BaseHTTPRequest,
ResourceType,
} from '../../api/HTTPRequest.js';
import type {Frame} from '../api/Frame.js';
import type {
ContinueRequestOverrides,
ResponseForRequest,
} from '../api/HTTPRequest.js';
import {HTTPRequest, type ResourceType} from '../api/HTTPRequest.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {HTTPResponse} from './HTTPResponse.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
/**
* @internal
*/
export class HTTPRequest extends BaseHTTPRequest {
override _response: HTTPResponse | null = null;
override _redirectChain: HTTPRequest[];
export class BidiHTTPRequest extends HTTPRequest {
override _response: BidiHTTPResponse | null = null;
override _redirectChain: BidiHTTPRequest[];
_navigationId: string | null;
#url: string;
@ -43,7 +45,7 @@ export class HTTPRequest extends BaseHTTPRequest {
constructor(
event: Bidi.Network.BeforeRequestSentParameters,
frame: Frame | null,
redirectChain: HTTPRequest[] = []
redirectChain: BidiHTTPRequest[] = []
) {
super();
@ -67,6 +69,10 @@ export class HTTPRequest extends BaseHTTPRequest {
}
}
override get client(): never {
throw new UnsupportedOperation();
}
override url(): string {
return this.#url;
}
@ -87,7 +93,7 @@ export class HTTPRequest extends BaseHTTPRequest {
return this.#headers;
}
override response(): HTTPResponse | null {
override response(): BidiHTTPResponse | null {
return this._response;
}
@ -99,7 +105,7 @@ export class HTTPRequest extends BaseHTTPRequest {
return this.#initiator;
}
override redirectChain(): HTTPRequest[] {
override redirectChain(): BidiHTTPRequest[] {
return this._redirectChain.slice();
}
@ -113,4 +119,47 @@ export class HTTPRequest extends BaseHTTPRequest {
override frame(): Frame | null {
return this.#frame;
}
override continueRequestOverrides(): never {
throw new UnsupportedOperation();
}
override continue(_overrides: ContinueRequestOverrides = {}): never {
throw new UnsupportedOperation();
}
override responseForRequest(): never {
throw new UnsupportedOperation();
}
override abortErrorReason(): never {
throw new UnsupportedOperation();
}
override interceptResolutionState(): never {
throw new UnsupportedOperation();
}
override isInterceptResolutionHandled(): never {
throw new UnsupportedOperation();
}
override finalizeInterceptions(): never {
throw new UnsupportedOperation();
}
override abort(): never {
throw new UnsupportedOperation();
}
override respond(
_response: Partial<ResponseForRequest>,
_priority?: number
): never {
throw new UnsupportedOperation();
}
override failure(): never {
throw new UnsupportedOperation();
}
}

View File

@ -13,22 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import Protocol from 'devtools-protocol';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import {Frame} from '../../api/Frame.js';
import type {Frame} from '../api/Frame.js';
import {
HTTPResponse as BaseHTTPResponse,
RemoteAddress,
} from '../../api/HTTPResponse.js';
HTTPResponse as HTTPResponse,
type RemoteAddress,
} from '../api/HTTPResponse.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {HTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
/**
* @internal
*/
export class HTTPResponse extends BaseHTTPResponse {
#request: HTTPRequest;
export class BidiHTTPResponse extends HTTPResponse {
#request: BidiHTTPRequest;
#remoteAddress: RemoteAddress;
#status: number;
#statusText: string;
@ -38,7 +39,7 @@ export class HTTPResponse extends BaseHTTPResponse {
#timings: Record<string, string> | null;
constructor(
request: HTTPRequest,
request: BidiHTTPRequest,
{response}: Bidi.Network.ResponseCompletedParameters
) {
super();
@ -86,7 +87,7 @@ export class HTTPResponse extends BaseHTTPResponse {
return this.#headers;
}
override request(): HTTPRequest {
override request(): BidiHTTPRequest {
return this.#request;
}
@ -105,4 +106,12 @@ export class HTTPResponse extends BaseHTTPResponse {
override fromServiceWorker(): boolean {
return false;
}
override securityDetails(): never {
throw new UnsupportedOperation();
}
override buffer(): never {
throw new UnsupportedOperation();
}
}

View File

@ -16,23 +16,25 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Point} from '../../api/ElementHandle.js';
import type {Point} from '../api/ElementHandle.js';
import {
Keyboard as BaseKeyboard,
Mouse as BaseMouse,
Touchscreen as BaseTouchscreen,
KeyDownOptions,
KeyPressOptions,
KeyboardTypeOptions,
Keyboard,
Mouse,
MouseButton,
MouseClickOptions,
MouseMoveOptions,
MouseOptions,
MouseWheelOptions,
} from '../../api/Input.js';
import {KeyInput} from '../USKeyboardLayout.js';
Touchscreen,
type KeyDownOptions,
type KeyPressOptions,
type KeyboardTypeOptions,
type MouseClickOptions,
type MouseMoveOptions,
type MouseOptions,
type MouseWheelOptions,
} from '../api/Input.js';
import {UnsupportedOperation} from '../common/Errors.js';
import type {KeyInput} from '../common/USKeyboardLayout.js';
import {BrowsingContext} from './BrowsingContext.js';
import type {BrowsingContext} from './BrowsingContext.js';
import type {BidiPage} from './Page.js';
const enum InputId {
Mouse = '__puppeteer_mouse',
@ -284,20 +286,20 @@ const getBidiKeyValue = (key: KeyInput) => {
/**
* @internal
*/
export class Keyboard extends BaseKeyboard {
#context: BrowsingContext;
export class BidiKeyboard extends Keyboard {
#page: BidiPage;
constructor(context: BrowsingContext) {
constructor(page: BidiPage) {
super();
this.#context = context;
this.#page = page;
}
override async down(
key: KeyInput,
_options?: Readonly<KeyDownOptions>
): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
@ -314,8 +316,8 @@ export class Keyboard extends BaseKeyboard {
}
override async up(key: KeyInput): Promise<void> {
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
@ -352,8 +354,8 @@ export class Keyboard extends BaseKeyboard {
type: ActionType.KeyUp,
value: getBidiKeyValue(key),
});
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
@ -404,8 +406,8 @@ export class Keyboard extends BaseKeyboard {
);
}
}
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
await this.#page.connection.send('input.performActions', {
context: this.#page.mainFrame()._id,
actions: [
{
type: SourceActionsType.Key,
@ -415,26 +417,37 @@ export class Keyboard extends BaseKeyboard {
],
});
}
override async sendCharacter(char: string): Promise<void> {
// Measures the number of code points rather than UTF-16 code units.
if ([...char].length > 1) {
throw new Error('Cannot send more than 1 character.');
}
const frame = await this.#page.focusedFrame();
await frame.isolatedRealm().evaluate(async char => {
document.execCommand('insertText', false, char);
}, char);
}
}
/**
* @internal
*/
interface BidiMouseClickOptions extends MouseClickOptions {
export interface BidiMouseClickOptions extends MouseClickOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
interface BidiMouseMoveOptions extends MouseMoveOptions {
export interface BidiMouseMoveOptions extends MouseMoveOptions {
origin?: Bidi.Input.Origin;
}
/**
* @internal
*/
interface BidiTouchMoveOptions {
export interface BidiTouchMoveOptions {
origin?: Bidi.Input.Origin;
}
@ -456,9 +469,9 @@ const getBidiButton = (button: MouseButton) => {
/**
* @internal
*/
export class Mouse extends BaseMouse {
export class BidiMouse extends Mouse {
#context: BrowsingContext;
#lastMovePoint?: Point;
#lastMovePoint: Point = {x: 0, y: 0};
constructor(context: BrowsingContext) {
super();
@ -466,7 +479,7 @@ export class Mouse extends BaseMouse {
}
override async reset(): Promise<void> {
this.#lastMovePoint = undefined;
this.#lastMovePoint = {x: 0, y: 0};
await this.#context.connection.send('input.releaseActions', {
context: this.#context.id,
});
@ -477,25 +490,35 @@ export class Mouse extends BaseMouse {
y: number,
options: Readonly<BidiMouseMoveOptions> = {}
): Promise<void> {
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = {
const from = this.#lastMovePoint;
const to = {
x: Math.round(x),
y: Math.round(y),
};
const actions: Bidi.Input.PointerSourceAction[] = [];
const steps = options.steps ?? 0;
for (let i = 0; i < steps; ++i) {
actions.push({
type: ActionType.PointerMove,
x: from.x + (to.x - from.x) * (i / steps),
y: from.y + (to.y - from.y) * (i / steps),
origin: options.origin,
});
}
actions.push({
type: ActionType.PointerMove,
...to,
origin: options.origin,
});
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = to;
await this.#context.connection.send('input.performActions', {
context: this.#context.id,
actions: [
{
type: SourceActionsType.Pointer,
id: InputId.Mouse,
actions: [
{
type: ActionType.PointerMove,
...this.#lastMovePoint,
duration: (options.steps ?? 0) * 50,
origin: options.origin,
},
],
actions,
},
],
});
@ -605,12 +628,32 @@ export class Mouse extends BaseMouse {
],
});
}
override drag(): never {
throw new UnsupportedOperation();
}
override dragOver(): never {
throw new UnsupportedOperation();
}
override dragEnter(): never {
throw new UnsupportedOperation();
}
override drop(): never {
throw new UnsupportedOperation();
}
override dragAndDrop(): never {
throw new UnsupportedOperation();
}
}
/**
* @internal
*/
export class Touchscreen extends BaseTouchscreen {
export class BidiTouchscreen extends Touchscreen {
#context: BrowsingContext;
constructor(context: BrowsingContext) {
@ -618,15 +661,6 @@ export class Touchscreen extends BaseTouchscreen {
this.#context = context;
}
override async tap(
x: number,
y: number,
options: BidiTouchMoveOptions = {}
): Promise<void> {
await this.touchStart(x, y, options);
await this.touchEnd();
}
override async touchStart(
x: number,
y: number,

View File

@ -14,17 +14,20 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import Protocol from 'devtools-protocol';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {ElementHandle} from '../../api/ElementHandle.js';
import {JSHandle} from '../../api/JSHandle.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {Realm} from './Realm.js';
import {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {releaseReference} from './utils.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
import type {Sandbox} from './Sandbox.js';
import {releaseReference} from './util.js';
/**
* @internal
*/
export class BidiJSHandle<T = unknown> extends JSHandle<T> {
#disposed = false;
readonly #sandbox: Sandbox;
@ -36,7 +39,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
this.#remoteValue = remoteValue;
}
context(): Realm {
context(): BidiRealm {
return this.realm.environment.context();
}
@ -88,7 +91,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
override toString(): string {
if (this.isPrimitiveValue) {
return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue);
return 'JSHandle:' + BidiDeserializer.deserialize(this.#remoteValue);
}
return 'JSHandle@' + this.#remoteValue.type;
@ -102,7 +105,7 @@ export class BidiJSHandle<T = unknown> extends JSHandle<T> {
return this.#remoteValue;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
throw new Error('Not available in WebDriver BiDi');
override remoteObject(): never {
throw new UnsupportedOperation('Not available in WebDriver BiDi');
}
}

View File

@ -14,42 +14,66 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {EventEmitter, Handler} from '../EventEmitter.js';
import {NetworkManagerEmittedEvents} from '../NetworkManager.js';
import {EventEmitter, EventSubscription} from '../common/EventEmitter.js';
import {
NetworkManagerEvent,
type NetworkManagerEvents,
} from '../common/NetworkManagerEvents.js';
import {DisposableStack} from '../util/disposable.js';
import {Connection} from './Connection.js';
import {BidiFrame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {BidiPage} from './Page.js';
import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';
import {BidiHTTPRequest} from './HTTPRequest.js';
import {BidiHTTPResponse} from './HTTPResponse.js';
import type {BidiPage} from './Page.js';
/**
* @internal
*/
export class NetworkManager extends EventEmitter {
#connection: Connection;
export class BidiNetworkManager extends EventEmitter<NetworkManagerEvents> {
#connection: BidiConnection;
#page: BidiPage;
#subscribedEvents = new Map<string, Handler<any>>([
['network.beforeRequestSent', this.#onBeforeRequestSent.bind(this)],
['network.responseStarted', this.#onResponseStarted.bind(this)],
['network.responseCompleted', this.#onResponseCompleted.bind(this)],
['network.fetchError', this.#onFetchError.bind(this)],
]) as Map<Bidi.Event['method'], Handler>;
#subscriptions = new DisposableStack();
#requestMap = new Map<string, HTTPRequest>();
#navigationMap = new Map<string, HTTPResponse>();
#requestMap = new Map<string, BidiHTTPRequest>();
#navigationMap = new Map<string, BidiHTTPResponse>();
constructor(connection: Connection, page: BidiPage) {
constructor(connection: BidiConnection, page: BidiPage) {
super();
this.#connection = connection;
this.#page = page;
// TODO: Subscribe to the Frame individually
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.on(event, subscriber);
}
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.beforeRequestSent',
this.#onBeforeRequestSent.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.responseStarted',
this.#onResponseStarted.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.responseCompleted',
this.#onResponseCompleted.bind(this)
)
);
this.#subscriptions.use(
new EventSubscription(
this.#connection,
'network.fetchError',
this.#onFetchError.bind(this)
)
);
}
#onBeforeRequestSent(event: Bidi.Network.BeforeRequestSentParameters): void {
@ -58,37 +82,34 @@ export class NetworkManager extends EventEmitter {
return;
}
const request = this.#requestMap.get(event.request.request);
let upsertRequest: HTTPRequest;
let upsertRequest: BidiHTTPRequest;
if (request) {
const requestChain = request._redirectChain;
upsertRequest = new HTTPRequest(event, frame, requestChain);
request._redirectChain.push(request);
upsertRequest = new BidiHTTPRequest(event, frame, request._redirectChain);
} else {
upsertRequest = new HTTPRequest(event, frame, []);
upsertRequest = new BidiHTTPRequest(event, frame, []);
}
this.#requestMap.set(event.request.request, upsertRequest);
this.emit(NetworkManagerEmittedEvents.Request, upsertRequest);
this.emit(NetworkManagerEvent.Request, upsertRequest);
}
#onResponseStarted(_event: any) {}
#onResponseStarted(_event: Bidi.Network.ResponseStartedParameters) {}
#onResponseCompleted(event: Bidi.Network.ResponseCompletedParameters): void {
const request = this.#requestMap.get(event.request.request);
if (!request) {
return;
}
const response = new HTTPResponse(request, event);
const response = new BidiHTTPResponse(request, event);
request._response = response;
if (event.navigation) {
this.#navigationMap.set(event.navigation, response);
}
if (response.fromCache()) {
this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request);
this.emit(NetworkManagerEvent.RequestServedFromCache, request);
}
this.emit(NetworkManagerEmittedEvents.Response, response);
this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
this.#requestMap.delete(event.request.request);
this.emit(NetworkManagerEvent.Response, response);
this.emit(NetworkManagerEvent.RequestFinished, request);
}
#onFetchError(event: Bidi.Network.FetchErrorParameters) {
@ -97,11 +118,11 @@ export class NetworkManager extends EventEmitter {
return;
}
request._failureText = event.errorText;
this.emit(NetworkManagerEmittedEvents.RequestFailed, request);
this.emit(NetworkManagerEvent.RequestFailed, request);
this.#requestMap.delete(event.request.request);
}
getNavigationResponse(navigationId: string | null): HTTPResponse | null {
getNavigationResponse(navigationId?: string | null): BidiHTTPResponse | null {
if (!navigationId) {
return null;
}
@ -139,9 +160,6 @@ export class NetworkManager extends EventEmitter {
this.removeAllListeners();
this.#requestMap.clear();
this.#navigationMap.clear();
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.off(event, subscriber);
}
this.#subscriptions.dispose();
}
}

View File

@ -0,0 +1,934 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Readable} from 'stream';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type Protocol from 'devtools-protocol';
import type {Observable, ObservableInput} from '../../third_party/rxjs/rxjs.js';
import {
first,
firstValueFrom,
forkJoin,
from,
map,
raceWith,
} from '../../third_party/rxjs/rxjs.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {WaitForOptions} from '../api/Frame.js';
import {
Page,
PageEvent,
type GeolocationOptions,
type MediaFeature,
type NewDocumentScriptEvaluation,
type ScreenshotOptions,
} from '../api/Page.js';
import {Accessibility} from '../cdp/Accessibility.js';
import {Coverage} from '../cdp/Coverage.js';
import {EmulationManager as CdpEmulationManager} from '../cdp/EmulationManager.js';
import {FrameTree} from '../cdp/FrameTree.js';
import {Tracing} from '../cdp/Tracing.js';
import {
ConsoleMessage,
type ConsoleMessageLocation,
} from '../common/ConsoleMessage.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import type {Handler} from '../common/EventEmitter.js';
import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
import type {PDFOptions} from '../common/PDFOptions.js';
import type {Awaitable} from '../common/types.js';
import {
debugError,
evaluationString,
NETWORK_IDLE_TIME,
timeout,
validateDialogType,
waitForHTTP,
} from '../common/util.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {disposeSymbol} from '../util/disposable.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
import {
BrowsingContextEvent,
CdpSessionWrapper,
type BrowsingContext,
} from './BrowsingContext.js';
import type {BidiConnection} from './Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiDialog} from './Dialog.js';
import {BidiElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js';
import {BidiFrame} from './Frame.js';
import type {BidiHTTPRequest} from './HTTPRequest.js';
import type {BidiHTTPResponse} from './HTTPResponse.js';
import {BidiKeyboard, BidiMouse, BidiTouchscreen} from './Input.js';
import type {BidiJSHandle} from './JSHandle.js';
import type {BiDiNetworkIdle} from './lifecycle.js';
import {getBiDiReadinessState, rewriteNavigationError} from './lifecycle.js';
import {BidiNetworkManager} from './NetworkManager.js';
import {createBidiHandle} from './Realm.js';
/**
* @internal
*/
export class BidiPage extends Page {
#accessibility: Accessibility;
#connection: BidiConnection;
#frameTree = new FrameTree<BidiFrame>();
#networkManager: BidiNetworkManager;
#viewport: Viewport | null = null;
#closedDeferred = Deferred.create<never, TargetCloseError>();
#subscribedEvents = new Map<Bidi.Event['method'], Handler<any>>([
['log.entryAdded', this.#onLogEntryAdded.bind(this)],
['browsingContext.load', this.#onFrameLoaded.bind(this)],
[
'browsingContext.fragmentNavigated',
this.#onFrameFragmentNavigated.bind(this),
],
[
'browsingContext.domContentLoaded',
this.#onFrameDOMContentLoaded.bind(this),
],
['browsingContext.userPromptOpened', this.#onDialog.bind(this)],
]);
readonly #networkManagerEvents = [
[
NetworkManagerEvent.Request,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.Request, request);
},
],
[
NetworkManagerEvent.RequestServedFromCache,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestServedFromCache, request);
},
],
[
NetworkManagerEvent.RequestFailed,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestFailed, request);
},
],
[
NetworkManagerEvent.RequestFinished,
(request: BidiHTTPRequest) => {
this.emit(PageEvent.RequestFinished, request);
},
],
[
NetworkManagerEvent.Response,
(response: BidiHTTPResponse) => {
this.emit(PageEvent.Response, response);
},
],
] as const;
readonly #browsingContextEvents = new Map<symbol, Handler<any>>([
[BrowsingContextEvent.Created, this.#onContextCreated.bind(this)],
[BrowsingContextEvent.Destroyed, this.#onContextDestroyed.bind(this)],
]);
#tracing: Tracing;
#coverage: Coverage;
#cdpEmulationManager: CdpEmulationManager;
#emulationManager: EmulationManager;
#mouse: BidiMouse;
#touchscreen: BidiTouchscreen;
#keyboard: BidiKeyboard;
#browsingContext: BrowsingContext;
#browserContext: BidiBrowserContext;
_client(): CDPSession {
return this.mainFrame().context().cdpSession;
}
constructor(
browsingContext: BrowsingContext,
browserContext: BidiBrowserContext
) {
super();
this.#browsingContext = browsingContext;
this.#browserContext = browserContext;
this.#connection = browsingContext.connection;
for (const [event, subscriber] of this.#browsingContextEvents) {
this.#browsingContext.on(event, subscriber);
}
this.#networkManager = new BidiNetworkManager(this.#connection, this);
for (const [event, subscriber] of this.#subscribedEvents) {
this.#connection.on(event, subscriber);
}
for (const [event, subscriber] of this.#networkManagerEvents) {
// TODO: remove any
this.#networkManager.on(event, subscriber as any);
}
const frame = new BidiFrame(
this,
this.#browsingContext,
this._timeoutSettings,
this.#browsingContext.parent
);
this.#frameTree.addFrame(frame);
this.emit(PageEvent.FrameAttached, frame);
// TODO: https://github.com/w3c/webdriver-bidi/issues/443
this.#accessibility = new Accessibility(
this.mainFrame().context().cdpSession
);
this.#tracing = new Tracing(this.mainFrame().context().cdpSession);
this.#coverage = new Coverage(this.mainFrame().context().cdpSession);
this.#cdpEmulationManager = new CdpEmulationManager(
this.mainFrame().context().cdpSession
);
this.#emulationManager = new EmulationManager(browsingContext);
this.#mouse = new BidiMouse(this.mainFrame().context());
this.#touchscreen = new BidiTouchscreen(this.mainFrame().context());
this.#keyboard = new BidiKeyboard(this);
}
/**
* @internal
*/
get connection(): BidiConnection {
return this.#connection;
}
override async setUserAgent(
userAgent: string,
userAgentMetadata?: Protocol.Emulation.UserAgentMetadata | undefined
): Promise<void> {
// TODO: handle CDP-specific cases such as mprach.
await this._client().send('Network.setUserAgentOverride', {
userAgent: userAgent,
userAgentMetadata: userAgentMetadata,
});
}
override async setBypassCSP(enabled: boolean): Promise<void> {
// TODO: handle CDP-specific cases such as mprach.
await this._client().send('Page.setBypassCSP', {enabled});
}
override async queryObjects<Prototype>(
prototypeHandle: BidiJSHandle<Prototype>
): Promise<BidiJSHandle<Prototype[]>> {
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
assert(
prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await this.mainFrame().client.send(
'Runtime.queryObjects',
{
prototypeObjectId: prototypeHandle.id,
}
);
return createBidiHandle(this.mainFrame().mainRealm(), {
type: 'array',
handle: response.objects.objectId,
}) as BidiJSHandle<Prototype[]>;
}
_setBrowserContext(browserContext: BidiBrowserContext): void {
this.#browserContext = browserContext;
}
override get accessibility(): Accessibility {
return this.#accessibility;
}
override get tracing(): Tracing {
return this.#tracing;
}
override get coverage(): Coverage {
return this.#coverage;
}
override get mouse(): BidiMouse {
return this.#mouse;
}
override get touchscreen(): BidiTouchscreen {
return this.#touchscreen;
}
override get keyboard(): BidiKeyboard {
return this.#keyboard;
}
override browser(): BidiBrowser {
return this.browserContext().browser();
}
override browserContext(): BidiBrowserContext {
return this.#browserContext;
}
override mainFrame(): BidiFrame {
const mainFrame = this.#frameTree.getMainFrame();
assert(mainFrame, 'Requesting main frame too early!');
return mainFrame;
}
/**
* @internal
*/
async focusedFrame(): Promise<BidiFrame> {
using frame = await this.mainFrame()
.isolatedRealm()
.evaluateHandle(() => {
let frame: HTMLIFrameElement | undefined;
let win: Window | null = window;
while (win?.document.activeElement instanceof HTMLIFrameElement) {
frame = win.document.activeElement;
win = frame.contentWindow;
}
return frame;
});
if (!(frame instanceof BidiElementHandle)) {
return this.mainFrame();
}
return await frame.contentFrame();
}
override frames(): BidiFrame[] {
return Array.from(this.#frameTree.frames());
}
frame(frameId?: string): BidiFrame | null {
return this.#frameTree.getById(frameId ?? '') || null;
}
childFrames(frameId: string): BidiFrame[] {
return this.#frameTree.childFrames(frameId);
}
#onFrameLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame && this.mainFrame() === frame) {
this.emit(PageEvent.Load, undefined);
}
}
#onFrameFragmentNavigated(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame) {
this.emit(PageEvent.FrameNavigated, frame);
}
}
#onFrameDOMContentLoaded(info: Bidi.BrowsingContext.NavigationInfo): void {
const frame = this.frame(info.context);
if (frame) {
frame._hasStartedLoading = true;
if (this.mainFrame() === frame) {
this.emit(PageEvent.DOMContentLoaded, undefined);
}
this.emit(PageEvent.FrameNavigated, frame);
}
}
#onContextCreated(context: BrowsingContext): void {
if (
!this.frame(context.id) &&
(this.frame(context.parent ?? '') || !this.#frameTree.getMainFrame())
) {
const frame = new BidiFrame(
this,
context,
this._timeoutSettings,
context.parent
);
this.#frameTree.addFrame(frame);
if (frame !== this.mainFrame()) {
this.emit(PageEvent.FrameAttached, frame);
}
}
}
#onContextDestroyed(context: BrowsingContext): void {
const frame = this.frame(context.id);
if (frame) {
if (frame === this.mainFrame()) {
this.emit(PageEvent.Close, undefined);
}
this.#removeFramesRecursively(frame);
}
}
#removeFramesRecursively(frame: BidiFrame): void {
for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child);
}
frame[disposeSymbol]();
this.#networkManager.clearMapAfterFrameDispose(frame);
this.#frameTree.removeFrame(frame);
this.emit(PageEvent.FrameDetached, frame);
}
#onLogEntryAdded(event: Bidi.Log.Entry): void {
const frame = this.frame(event.source.context);
if (!frame) {
return;
}
if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => {
return createBidiHandle(frame.mainRealm(), arg);
});
const text = args
.reduce((value, arg) => {
const parsedValue = arg.isPrimitiveValue
? BidiDeserializer.deserialize(arg.remoteValue())
: arg.toString();
return `${value} ${parsedValue}`;
}, '')
.slice(1);
this.emit(
PageEvent.Console,
new ConsoleMessage(
event.method as any,
text,
args,
getStackTraceLocations(event.stackTrace)
)
);
} else if (isJavaScriptLogEntry(event)) {
const error = new Error(event.text ?? '');
const messageHeight = error.message.split('\n').length;
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
const stackLines = [];
if (event.stackTrace) {
for (const frame of event.stackTrace.callFrames) {
// Note we need to add `1` because the values are 0-indexed.
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber + 1
}:${frame.columnNumber + 1})`
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
this.emit(PageEvent.PageError, error);
} else {
debugError(
`Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"`
);
}
}
#onDialog(event: Bidi.BrowsingContext.UserPromptOpenedParameters): void {
const frame = this.frame(event.context);
if (!frame) {
return;
}
const type = validateDialogType(event.type);
const dialog = new BidiDialog(
frame.context(),
type,
event.message,
event.defaultValue
);
this.emit(PageEvent.Dialog, dialog);
}
getNavigationResponse(id?: string | null): BidiHTTPResponse | null {
return this.#networkManager.getNavigationResponse(id);
}
override isClosed(): boolean {
return this.#closedDeferred.finished();
}
override async close(): Promise<void> {
if (this.#closedDeferred.finished()) {
return;
}
this.#closedDeferred.reject(new TargetCloseError('Page closed!'));
this.#networkManager.dispose();
await this.#connection.send('browsingContext.close', {
context: this.mainFrame()._id,
});
this.emit(PageEvent.Close, undefined);
this.removeAllListeners();
}
override async reload(
options: WaitForOptions = {}
): Promise<BidiHTTPResponse | null> {
const {
waitUntil = 'load',
timeout: ms = this._timeoutSettings.navigationTimeout(),
} = options;
const [readiness, networkIdle] = getBiDiReadinessState(waitUntil);
const response = await firstValueFrom(
this._waitWithNetworkIdle(
this.#connection.send('browsingContext.reload', {
context: this.mainFrame()._id,
wait: readiness,
}),
networkIdle
)
.pipe(raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow())))
.pipe(rewriteNavigationError(this.url(), ms))
);
return this.getNavigationResponse(response?.result.navigation);
}
override setDefaultNavigationTimeout(timeout: number): void {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
override setDefaultTimeout(timeout: number): void {
this._timeoutSettings.setDefaultTimeout(timeout);
}
override getDefaultTimeout(): number {
return this._timeoutSettings.timeout();
}
override isJavaScriptEnabled(): boolean {
return this.#cdpEmulationManager.javascriptEnabled;
}
override async setGeolocation(options: GeolocationOptions): Promise<void> {
return await this.#cdpEmulationManager.setGeolocation(options);
}
override async setJavaScriptEnabled(enabled: boolean): Promise<void> {
return await this.#cdpEmulationManager.setJavaScriptEnabled(enabled);
}
override async emulateMediaType(type?: string): Promise<void> {
return await this.#cdpEmulationManager.emulateMediaType(type);
}
override async emulateCPUThrottling(factor: number | null): Promise<void> {
return await this.#cdpEmulationManager.emulateCPUThrottling(factor);
}
override async emulateMediaFeatures(
features?: MediaFeature[]
): Promise<void> {
return await this.#cdpEmulationManager.emulateMediaFeatures(features);
}
override async emulateTimezone(timezoneId?: string): Promise<void> {
return await this.#cdpEmulationManager.emulateTimezone(timezoneId);
}
override async emulateIdleState(overrides?: {
isUserActive: boolean;
isScreenUnlocked: boolean;
}): Promise<void> {
return await this.#cdpEmulationManager.emulateIdleState(overrides);
}
override async emulateVisionDeficiency(
type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type']
): Promise<void> {
return await this.#cdpEmulationManager.emulateVisionDeficiency(type);
}
override async setViewport(viewport: Viewport): Promise<void> {
if (!this.#browsingContext.supportsCdp()) {
await this.#emulationManager.emulateViewport(viewport);
this.#viewport = viewport;
return;
}
const needsReload =
await this.#cdpEmulationManager.emulateViewport(viewport);
this.#viewport = viewport;
if (needsReload) {
await this.reload();
}
}
override viewport(): Viewport | null {
return this.#viewport;
}
override async pdf(options: PDFOptions = {}): Promise<Buffer> {
const {path = undefined} = options;
const {
printBackground: background,
margin,
landscape,
width,
height,
pageRanges: ranges,
scale,
preferCSSPageSize,
timeout: ms,
} = this._getPDFOptions(options, 'cm');
const pageRanges = ranges ? ranges.split(', ') : [];
const {result} = await firstValueFrom(
from(
this.#connection.send('browsingContext.print', {
context: this.mainFrame()._id,
background,
margin,
orientation: landscape ? 'landscape' : 'portrait',
page: {
width,
height,
},
pageRanges,
scale,
shrinkToFit: !preferCSSPageSize,
})
).pipe(raceWith(timeout(ms)))
);
const buffer = Buffer.from(result.data, 'base64');
await this._maybeWriteBufferToFile(path, buffer);
return buffer;
}
override async createPDFStream(
options?: PDFOptions | undefined
): Promise<Readable> {
const buffer = await this.pdf(options);
try {
const {Readable} = await import('stream');
return Readable.from(buffer);
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Can only pass a file path in a Node-like environment.'
);
}
throw error;
}
}
override async _screenshot(
options: Readonly<ScreenshotOptions>
): Promise<string> {
const {clip, type, captureBeyondViewport, allowViewportExpansion, quality} =
options;
if (captureBeyondViewport && !allowViewportExpansion) {
throw new UnsupportedOperation(
`BiDi does not support 'captureBeyondViewport'. Use 'allowViewportExpansion'.`
);
}
if (options.omitBackground !== undefined && options.omitBackground) {
throw new UnsupportedOperation(`BiDi does not support 'omitBackground'.`);
}
if (options.optimizeForSpeed !== undefined && options.optimizeForSpeed) {
throw new UnsupportedOperation(
`BiDi does not support 'optimizeForSpeed'.`
);
}
if (options.fromSurface !== undefined && !options.fromSurface) {
throw new UnsupportedOperation(`BiDi does not support 'fromSurface'.`);
}
if (clip !== undefined && clip.scale !== undefined && clip.scale !== 1) {
throw new UnsupportedOperation(
`BiDi does not support 'scale' in 'clip'.`
);
}
const {
result: {data},
} = await this.#connection.send('browsingContext.captureScreenshot', {
context: this.mainFrame()._id,
format: {
type: `image/${type}`,
quality: quality ? quality / 100 : undefined,
},
clip: clip && {
type: 'box',
...clip,
},
});
return data;
}
override async waitForRequest(
urlOrPredicate:
| string
| ((req: BidiHTTPRequest) => boolean | Promise<boolean>),
options: {timeout?: number} = {}
): Promise<BidiHTTPRequest> {
const {timeout = this._timeoutSettings.timeout()} = options;
return await waitForHTTP(
this.#networkManager,
NetworkManagerEvent.Request,
urlOrPredicate,
timeout,
this.#closedDeferred
);
}
override async waitForResponse(
urlOrPredicate:
| string
| ((res: BidiHTTPResponse) => boolean | Promise<boolean>),
options: {timeout?: number} = {}
): Promise<BidiHTTPResponse> {
const {timeout = this._timeoutSettings.timeout()} = options;
return await waitForHTTP(
this.#networkManager,
NetworkManagerEvent.Response,
urlOrPredicate,
timeout,
this.#closedDeferred
);
}
override async waitForNetworkIdle(
options: {idleTime?: number; timeout?: number} = {}
): Promise<void> {
const {
idleTime = NETWORK_IDLE_TIME,
timeout: ms = this._timeoutSettings.timeout(),
} = options;
await firstValueFrom(
this._waitForNetworkIdle(this.#networkManager, idleTime).pipe(
raceWith(timeout(ms), from(this.#closedDeferred.valueOrThrow()))
)
);
}
/** @internal */
_waitWithNetworkIdle(
observableInput: ObservableInput<{
result: Bidi.BrowsingContext.NavigateResult;
} | null>,
networkIdle: BiDiNetworkIdle
): Observable<{
result: Bidi.BrowsingContext.NavigateResult;
} | null> {
const delay = networkIdle
? this._waitForNetworkIdle(
this.#networkManager,
NETWORK_IDLE_TIME,
networkIdle === 'networkidle0' ? 0 : 2
)
: from(Promise.resolve());
return forkJoin([
from(observableInput).pipe(first()),
delay.pipe(first()),
]).pipe(
map(([response]) => {
return response;
})
);
}
override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.mainFrame()
.context()
.cdpSession.send('Target.attachToTarget', {
targetId: this.mainFrame()._id,
flatten: true,
});
return new CdpSessionWrapper(this.mainFrame().context(), sessionId);
}
override async bringToFront(): Promise<void> {
await this.#connection.send('browsingContext.activate', {
context: this.mainFrame()._id,
});
}
override async evaluateOnNewDocument<
Params extends unknown[],
Func extends (...args: Params) => unknown = (...args: Params) => unknown,
>(
pageFunction: Func | string,
...args: Params
): Promise<NewDocumentScriptEvaluation> {
const expression = evaluationExpression(pageFunction, ...args);
const {result} = await this.#connection.send('script.addPreloadScript', {
functionDeclaration: expression,
contexts: [this.mainFrame()._id],
});
return {identifier: result.script};
}
override async removeScriptToEvaluateOnNewDocument(
id: string
): Promise<void> {
await this.#connection.send('script.removePreloadScript', {
script: id,
});
}
override async exposeFunction<Args extends unknown[], Ret>(
name: string,
pptrFunction:
| ((...args: Args) => Awaitable<Ret>)
| {default: (...args: Args) => Awaitable<Ret>}
): Promise<void> {
return await this.mainFrame().exposeFunction(
name,
'default' in pptrFunction ? pptrFunction.default : pptrFunction
);
}
override isDragInterceptionEnabled(): boolean {
return false;
}
override async setCacheEnabled(enabled?: boolean): Promise<void> {
// TODO: handle CDP-specific cases such as mprach.
await this._client().send('Network.setCacheDisabled', {
cacheDisabled: !enabled,
});
}
override isServiceWorkerBypassed(): never {
throw new UnsupportedOperation();
}
override target(): never {
throw new UnsupportedOperation();
}
override waitForFileChooser(): never {
throw new UnsupportedOperation();
}
override workers(): never {
throw new UnsupportedOperation();
}
override setRequestInterception(): never {
throw new UnsupportedOperation();
}
override setDragInterception(): never {
throw new UnsupportedOperation();
}
override setBypassServiceWorker(): never {
throw new UnsupportedOperation();
}
override setOfflineMode(): never {
throw new UnsupportedOperation();
}
override emulateNetworkConditions(): never {
throw new UnsupportedOperation();
}
override cookies(): never {
throw new UnsupportedOperation();
}
override setCookie(): never {
throw new UnsupportedOperation();
}
override deleteCookie(): never {
throw new UnsupportedOperation();
}
override removeExposedFunction(): never {
// TODO: Quick win?
throw new UnsupportedOperation();
}
override authenticate(): never {
throw new UnsupportedOperation();
}
override setExtraHTTPHeaders(): never {
throw new UnsupportedOperation();
}
override metrics(): never {
throw new UnsupportedOperation();
}
override goBack(): never {
throw new UnsupportedOperation();
}
override goForward(): never {
throw new UnsupportedOperation();
}
override waitForDevicePrompt(): never {
throw new UnsupportedOperation();
}
}
function isConsoleLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.ConsoleLogEntry {
return event.type === 'console';
}
function isJavaScriptLogEntry(
event: Bidi.Log.Entry
): event is Bidi.Log.JavascriptLogEntry {
return event.type === 'javascript';
}
function getStackTraceLocations(
stackTrace?: Bidi.Script.StackTrace
): ConsoleMessageLocation[] {
const stackTraceLocations: ConsoleMessageLocation[] = [];
if (stackTrace) {
for (const callFrame of stackTrace.callFrames) {
stackTraceLocations.push({
url: callFrame.url,
lineNumber: callFrame.lineNumber,
columnNumber: callFrame.columnNumber,
});
}
}
return stackTraceLocations;
}
function evaluationExpression(fun: Function | string, ...args: unknown[]) {
return `() => {${evaluationString(fun, ...args)}}`;
}

View File

@ -1,36 +1,37 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import PuppeteerUtil from '../../injected/injected.js';
import {stringifyFunction} from '../../util/Function.js';
import {EventEmitter} from '../EventEmitter.js';
import {scriptInjector} from '../ScriptInjector.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {EventEmitter, type EventType} from '../common/EventEmitter.js';
import {scriptInjector} from '../common/ScriptInjector.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {
PuppeteerURL,
SOURCE_URL_REGEX,
getSourcePuppeteerURLIfAvailable,
getSourceUrlComment,
isString,
} from '../util.js';
} from '../common/util.js';
import type PuppeteerUtil from '../injected/injected.js';
import {disposeSymbol} from '../util/disposable.js';
import {stringifyFunction} from '../util/Function.js';
import {Connection} from './Connection.js';
import type {BidiConnection} from './Connection.js';
import {BidiDeserializer} from './Deserializer.js';
import {BidiElementHandle} from './ElementHandle.js';
import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
import type {Sandbox} from './Sandbox.js';
import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js';
import {createEvaluationError} from './util.js';
export const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export const getSourceUrlComment = (url: string): string => {
return `//# sourceURL=${url}`;
};
export class Realm extends EventEmitter {
readonly connection: Connection;
/**
* @internal
*/
export class BidiRealm extends EventEmitter<Record<EventType, any>> {
readonly connection: BidiConnection;
#id!: string;
#sandbox!: Sandbox;
constructor(connection: Connection) {
constructor(connection: BidiConnection) {
super();
this.connection = connection;
}
@ -195,11 +196,11 @@ export class Realm extends EventEmitter {
}
return returnByValue
? BidiSerializer.deserialize(result.result)
? BidiDeserializer.deserialize(result.result)
: createBidiHandle(sandbox, result.result);
}
[Symbol.dispose](): void {
[disposeSymbol](): void {
this.connection.off(
Bidi.ChromiumBidi.Script.EventNames.RealmCreated,
this.handleRealmCreated

View File

@ -14,15 +14,16 @@
* limitations under the License.
*/
import {JSHandle} from '../../api/JSHandle.js';
import {Realm} from '../../api/Realm.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js';
import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../common/util.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BidiFrame} from './Frame.js';
import {Realm as BidiRealm} from './Realm.js';
import type {BrowsingContext} from './BrowsingContext.js';
import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
import type {BidiRealm as BidiRealm} from './Realm.js';
/**
* A unique key for {@link SandboxChart} to denote the default world.
* Realms are automatically created in the default sandbox.
@ -117,4 +118,16 @@ export class Sandbox extends Realm {
await handle.dispose();
return transferredHandle as unknown as T;
}
override async adoptBackendNode(
backendNodeId?: number
): Promise<JSHandle<Node>> {
const {object} = await this.environment.client.send('DOM.resolveNode', {
backendNodeId: backendNodeId,
});
return new BidiElementHandle(this, {
handle: object.objectId,
type: 'node',
});
}
}

View File

@ -14,14 +14,14 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {LazyArg} from '../LazyArg.js';
import {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
import {LazyArg} from '../common/LazyArg.js';
import {isDate, isPlainObject, isRegExp} from '../common/util.js';
import {BidiElementHandle} from './ElementHandle.js';
import {BidiJSHandle} from './JSHandle.js';
import {Sandbox} from './Sandbox.js';
import type {Sandbox} from './Sandbox.js';
/**
* @internal
@ -157,8 +157,7 @@ export class BidiSerializer {
if (objectHandle) {
if (
objectHandle.realm.environment.context() !==
sandbox.environment.context() &&
!('sharedId' in objectHandle.remoteValue())
sandbox.environment.context()
) {
throw new Error(
'JSHandles can be evaluated only in the context they were created!'
@ -172,108 +171,4 @@ export class BidiSerializer {
return BidiSerializer.serializeRemoteValue(arg);
}
static deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
switch (value) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
return value;
}
}
static deserializeLocalValue(result: Bidi.Script.RemoteValue): unknown {
switch (result.type) {
case 'array':
if (result.value) {
return result.value.map(value => {
return BidiSerializer.deserializeLocalValue(value);
});
}
break;
case 'set':
if (result.value) {
return result.value.reduce((acc: Set<unknown>, value) => {
return acc.add(BidiSerializer.deserializeLocalValue(value));
}, new Set());
}
break;
case 'object':
if (result.value) {
return result.value.reduce((acc: Record<any, unknown>, tuple) => {
const {key, value} = BidiSerializer.deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
}
break;
case 'map':
if (result.value) {
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
const {key, value} = BidiSerializer.deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
}
break;
case 'promise':
return {};
case 'regexp':
return new RegExp(result.value.pattern, result.value.flags);
case 'date':
return new Date(result.value);
case 'undefined':
return undefined;
case 'null':
return null;
case 'number':
return BidiSerializer.deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
return Boolean(result.value);
case 'string':
return result.value;
}
throw new UnserializableError(
`Deserialization of type ${result.type} not supported.`
);
}
static deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
: BidiSerializer.deserializeLocalValue(serializedKey);
const value = BidiSerializer.deserializeLocalValue(serializedValue);
return {key, value};
}
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result) {
debugError('Service did not produce a result.');
return undefined;
}
try {
return BidiSerializer.deserializeLocalValue(result);
} catch (error) {
if (error instanceof UnserializableError) {
debugError(error.message);
return undefined;
}
throw error;
}
}
}

View File

@ -14,16 +14,19 @@
* limitations under the License.
*/
import {Target, TargetType} from '../../api/Target.js';
import {CDPSession} from '../Connection.js';
import type {WebWorker} from '../WebWorker.js';
import type {CDPSession} from '../api/CDPSession.js';
import {Target, TargetType} from '../api/Target.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {BidiBrowser} from './Browser.js';
import {BidiBrowserContext} from './BrowserContext.js';
import {BrowsingContext, CDPSessionWrapper} from './BrowsingContext.js';
import type {BidiBrowser} from './Browser.js';
import type {BidiBrowserContext} from './BrowserContext.js';
import {type BrowsingContext, CdpSessionWrapper} from './BrowsingContext.js';
import {BidiPage} from './Page.js';
export class BidiTarget extends Target {
/**
* @internal
*/
export abstract class BidiTarget extends Target {
protected _browserContext: BidiBrowserContext;
constructor(browserContext: BidiBrowserContext) {
@ -31,7 +34,11 @@ export class BidiTarget extends Target {
this._browserContext = browserContext;
}
override async worker(): Promise<WebWorker | null> {
_setBrowserContext(browserContext: BidiBrowserContext): void {
this._browserContext = browserContext;
}
override async worker(): Promise<null> {
return null;
}
@ -43,12 +50,12 @@ export class BidiTarget extends Target {
return this._browserContext;
}
override opener(): Target | undefined {
throw new Error('Not implemented');
override opener(): never {
throw new UnsupportedOperation();
}
_setBrowserContext(browserContext: BidiBrowserContext): void {
this._browserContext = browserContext;
override createCDPSession(): Promise<CDPSession> {
throw new UnsupportedOperation();
}
}
@ -92,7 +99,7 @@ export class BiDiBrowsingContextTarget extends BidiTarget {
flatten: true,
}
);
return new CDPSessionWrapper(this._browsingContext, sessionId);
return new CdpSessionWrapper(this._browsingContext, sessionId);
}
override type(): TargetType {
@ -115,7 +122,7 @@ export class BiDiPageTarget extends BiDiBrowsingContextTarget {
this.#page = new BidiPage(browsingContext, browserContext);
}
override async page(): Promise<BidiPage | null> {
override async page(): Promise<BidiPage> {
return this.#page;
}

View File

@ -0,0 +1,32 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './BidiOverCdp.js';
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './BrowsingContext.js';
export * from './Connection.js';
export * from './ElementHandle.js';
export * from './Frame.js';
export * from './HTTPRequest.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
export * from './NetworkManager.js';
export * from './Page.js';
export * from './Realm.js';
export * from './Sandbox.js';
export * from './Target.js';

View File

@ -0,0 +1,129 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {
ObservableInput,
ObservedValueOf,
OperatorFunction,
} from '../../third_party/rxjs/rxjs.js';
import {catchError} from '../../third_party/rxjs/rxjs.js';
import type {PuppeteerLifeCycleEvent} from '../cdp/LifecycleWatcher.js';
import {ProtocolError, TimeoutError} from '../common/Errors.js';
/**
* @internal
*/
export type BiDiNetworkIdle = Extract<
PuppeteerLifeCycleEvent,
'networkidle0' | 'networkidle2'
> | null;
/**
* @internal
*/
export function getBiDiLifeCycles(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'>,
BiDiNetworkIdle,
] {
if (Array.isArray(event)) {
const pageLifeCycle = event.some(lifeCycle => {
return lifeCycle !== 'domcontentloaded';
})
? 'load'
: 'domcontentloaded';
const networkLifeCycle = event.reduce((acc, lifeCycle) => {
if (lifeCycle === 'networkidle0') {
return lifeCycle;
} else if (acc !== 'networkidle0' && lifeCycle === 'networkidle2') {
return lifeCycle;
}
return acc;
}, null as BiDiNetworkIdle);
return [pageLifeCycle, networkLifeCycle];
}
if (event === 'networkidle0' || event === 'networkidle2') {
return ['load', event];
}
return [event, null];
}
/**
* @internal
*/
export const lifeCycleToReadinessState = new Map<
PuppeteerLifeCycleEvent,
Bidi.BrowsingContext.ReadinessState
>([
['load', Bidi.BrowsingContext.ReadinessState.Complete],
['domcontentloaded', Bidi.BrowsingContext.ReadinessState.Interactive],
]);
export function getBiDiReadinessState(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [Bidi.BrowsingContext.ReadinessState, BiDiNetworkIdle] {
const lifeCycles = getBiDiLifeCycles(event);
const readiness = lifeCycleToReadinessState.get(lifeCycles[0])!;
return [readiness, lifeCycles[1]];
}
/**
* @internal
*/
export const lifeCycleToSubscribedEvent = new Map<
PuppeteerLifeCycleEvent,
'browsingContext.load' | 'browsingContext.domContentLoaded'
>([
['load', 'browsingContext.load'],
['domcontentloaded', 'browsingContext.domContentLoaded'],
]);
/**
* @internal
*/
export function getBiDiLifecycleEvent(
event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]
): [
'browsingContext.load' | 'browsingContext.domContentLoaded',
BiDiNetworkIdle,
] {
const lifeCycles = getBiDiLifeCycles(event);
const bidiEvent = lifeCycleToSubscribedEvent.get(lifeCycles[0])!;
return [bidiEvent, lifeCycles[1]];
}
/**
* @internal
*/
export function rewriteNavigationError<T, R extends ObservableInput<T>>(
message: string,
ms: number
): OperatorFunction<T, T | ObservedValueOf<R>> {
return catchError<T, R>(error => {
if (error instanceof ProtocolError) {
error.message += ` at ${message}`;
} else if (error instanceof TimeoutError) {
error.message = `Navigation timeout of ${ms} ms exceeded`;
}
throw error;
});
}

View File

@ -14,23 +14,18 @@
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debug} from '../Debug.js';
import {PuppeteerURL} from '../util.js';
import {PuppeteerURL, debugError} from '../common/util.js';
import {Realm} from './Realm.js';
import {BidiSerializer} from './Serializer.js';
import {BidiDeserializer} from './Deserializer.js';
import type {BidiRealm} from './Realm.js';
/**
* @internal
*/
export const debugError = debug('puppeteer:error');
/**
* @internal
*/
export async function releaseReference(
client: Realm,
client: BidiRealm,
remoteReference: Bidi.Script.RemoteReference
): Promise<void> {
if (!remoteReference.handle) {
@ -55,7 +50,7 @@ export function createEvaluationError(
details: Bidi.Script.ExceptionDetails
): unknown {
if (details.exception.type !== 'error') {
return BidiSerializer.deserialize(details.exception);
return BidiDeserializer.deserialize(details.exception);
}
const [name = '', ...parts] = details.text.split(': ');
const message = parts.join(': ');

View File

@ -14,11 +14,10 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import {CDPSession} from './Connection.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
/**
* Represents a Node and the properties of it that are relevant to Accessibility.
@ -304,7 +303,12 @@ class AXNode {
#isTextOnlyObject(): boolean {
const role = this.#role;
return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox';
return (
role === 'LineBreak' ||
role === 'text' ||
role === 'InlineTextBox' ||
role === 'StaticText'
);
}
#hasFocusableChild(): boolean {
@ -354,6 +358,7 @@ class AXNode {
case 'doc-cover':
case 'graphics-symbol':
case 'img':
case 'image':
case 'Meter':
case 'scrollbar':
case 'slider':

View File

@ -14,16 +14,16 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import type {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import {QueryHandler, type QuerySelector} from '../common/QueryHandler.js';
import type {AwaitableIterable} from '../common/types.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {QueryHandler, QuerySelector} from './QueryHandler.js';
import {AwaitableIterable} from './types.js';
const NON_ELEMENT_NODE_ROLES = new Set(['StaticText', 'InlineTextBox']);
const queryAXTree = async (
client: CDPSession,
@ -37,7 +37,7 @@ const queryAXTree = async (
role,
});
return nodes.filter((node: Protocol.Accessibility.AXNode) => {
return !node.role || node.role.value !== 'StaticText';
return !node.role || !NON_ELEMENT_NODE_ROLES.has(node.role.value);
});
};
@ -46,11 +46,10 @@ interface ARIASelector {
role?: string;
}
const KNOWN_ATTRIBUTES = Object.freeze(['name', 'role']);
const isKnownAttribute = (
attribute: string
): attribute is keyof ARIASelector => {
return KNOWN_ATTRIBUTES.includes(attribute);
return ['name', 'role'].includes(attribute);
};
const normalizeValue = (value: string): string => {
@ -64,7 +63,7 @@ const normalizeValue = (value: string): string => {
* The following examples showcase how the syntax works wrt. querying:
*
* - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'.
* - '[role="img"]' queries for elements with role 'img' and any name.
* - '[role="image"]' queries for elements with role 'image' and any name.
* - 'label' queries for elements with name 'label' and any role.
* - '[name=""][role="button"]' queries for elements with no name and role 'button'.
*/
@ -114,9 +113,9 @@ export class ARIAQueryHandler extends QueryHandler {
role
);
yield* AsyncIterableUtil.map(results, node => {
return (element.realm as IsolatedWorld).adoptBackendNode(
node.backendDOMNodeId
) as Promise<ElementHandle<Node>>;
return element.realm.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
});
}

View File

@ -1,8 +1,9 @@
import {JSHandle} from '../api/JSHandle.js';
import {debugError} from '../common/util.js';
import {DisposableStack} from '../util/disposable.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {ExecutionContext} from './ExecutionContext.js';
import {debugError} from './util.js';
import type {ExecutionContext} from './ExecutionContext.js';
/**
* @internal

View File

@ -14,46 +14,47 @@
* limitations under the License.
*/
import {ChildProcess} from 'child_process';
import type {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {
Browser as BrowserBase,
BrowserCloseCallback,
TargetFilterCallback,
IsPageTargetCallback,
BrowserEmittedEvents,
BrowserContextEmittedEvents,
BrowserContextOptions,
BrowserEvent,
WEB_PERMISSION_TO_PROTOCOL_PERMISSION,
Permission,
type BrowserCloseCallback,
type BrowserContextOptions,
type IsPageTargetCallback,
type Permission,
type TargetFilterCallback,
type WaitForTargetOptions,
} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
import {Page} from '../api/Page.js';
import {Target} from '../api/Target.js';
import {USE_TAB_TARGET} from '../environment.js';
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
import type {Page} from '../api/Page.js';
import type {Target} from '../api/Target.js';
import type {Viewport} from '../common/Viewport.js';
import {assert} from '../util/assert.js';
import {ChromeTargetManager} from './ChromeTargetManager.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import type {Connection} from './Connection.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {Viewport} from './PuppeteerViewport.js';
import {
DevToolsTarget,
InitializationStatus,
OtherTarget,
PageTarget,
CDPTarget,
WorkerTarget,
DevToolsTarget,
type CdpTarget,
} from './Target.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {TargetManagerEvent, type TargetManager} from './TargetManager.js';
/**
* @internal
*/
export class CDPBrowser extends BrowserBase {
export class CdpBrowser extends BrowserBase {
readonly protocol = 'cdp';
static async _create(
product: 'firefox' | 'chrome' | undefined,
connection: Connection,
@ -64,10 +65,9 @@ export class CDPBrowser extends BrowserBase {
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
waitForInitiallyDiscoveredTargets = true,
useTabTarget = USE_TAB_TARGET
): Promise<CDPBrowser> {
const browser = new CDPBrowser(
waitForInitiallyDiscoveredTargets = true
): Promise<CdpBrowser> {
const browser = new CdpBrowser(
product,
connection,
contextIds,
@ -77,8 +77,7 @@ export class CDPBrowser extends BrowserBase {
closeCallback,
targetFilterCallback,
isPageTargetCallback,
waitForInitiallyDiscoveredTargets,
useTabTarget
waitForInitiallyDiscoveredTargets
);
await browser._attach();
return browser;
@ -90,15 +89,10 @@ export class CDPBrowser extends BrowserBase {
#closeCallback: BrowserCloseCallback;
#targetFilterCallback: TargetFilterCallback;
#isPageTargetCallback!: IsPageTargetCallback;
#defaultContext: CDPBrowserContext;
#contexts = new Map<string, CDPBrowserContext>();
#screenshotTaskQueue: TaskQueue;
#defaultContext: CdpBrowserContext;
#contexts = new Map<string, CdpBrowserContext>();
#targetManager: TargetManager;
override get _targets(): Map<string, CDPTarget> {
return this.#targetManager.getAvailableTargets();
}
constructor(
product: 'chrome' | 'firefox' | undefined,
connection: Connection,
@ -109,15 +103,13 @@ export class CDPBrowser extends BrowserBase {
closeCallback?: BrowserCloseCallback,
targetFilterCallback?: TargetFilterCallback,
isPageTargetCallback?: IsPageTargetCallback,
waitForInitiallyDiscoveredTargets = true,
useTabTarget = USE_TAB_TARGET
waitForInitiallyDiscoveredTargets = true
) {
super();
product = product || 'chrome';
this.#ignoreHTTPSErrors = ignoreHTTPSErrors;
this.#defaultViewport = defaultViewport;
this.#process = process;
this.#screenshotTaskQueue = new TaskQueue();
this.#connection = connection;
this.#closeCallback = closeCallback || function (): void {};
this.#targetFilterCallback =
@ -137,74 +129,63 @@ export class CDPBrowser extends BrowserBase {
connection,
this.#createTarget,
this.#targetFilterCallback,
waitForInitiallyDiscoveredTargets,
useTabTarget
waitForInitiallyDiscoveredTargets
);
}
this.#defaultContext = new CDPBrowserContext(this.#connection, this);
this.#defaultContext = new CdpBrowserContext(this.#connection, this);
for (const contextId of contextIds) {
this.#contexts.set(
contextId,
new CDPBrowserContext(this.#connection, this, contextId)
new CdpBrowserContext(this.#connection, this, contextId)
);
}
}
#emitDisconnected = () => {
this.emit(BrowserEmittedEvents.Disconnected);
this.emit(BrowserEvent.Disconnected, undefined);
};
override async _attach(): Promise<void> {
this.#connection.on(
ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected
);
async _attach(): Promise<void> {
this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected);
this.#targetManager.on(
TargetManagerEmittedEvents.TargetAvailable,
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget
);
this.#targetManager.on(
TargetManagerEmittedEvents.TargetGone,
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget
);
this.#targetManager.on(
TargetManagerEmittedEvents.TargetChanged,
TargetManagerEvent.TargetChanged,
this.#onTargetChanged
);
this.#targetManager.on(
TargetManagerEmittedEvents.TargetDiscovered,
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered
);
await this.#targetManager.initialize();
}
override _detach(): void {
this.#connection.off(
ConnectionEmittedEvents.Disconnected,
this.#emitDisconnected
);
_detach(): void {
this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected);
this.#targetManager.off(
TargetManagerEmittedEvents.TargetAvailable,
TargetManagerEvent.TargetAvailable,
this.#onAttachedToTarget
);
this.#targetManager.off(
TargetManagerEmittedEvents.TargetGone,
TargetManagerEvent.TargetGone,
this.#onDetachedFromTarget
);
this.#targetManager.off(
TargetManagerEmittedEvents.TargetChanged,
TargetManagerEvent.TargetChanged,
this.#onTargetChanged
);
this.#targetManager.off(
TargetManagerEmittedEvents.TargetDiscovered,
TargetManagerEvent.TargetDiscovered,
this.#onTargetDiscovered
);
}
/**
* The spawned browser process. Returns `null` if the browser instance was created with
* {@link Puppeteer.connect}.
*/
override process(): ChildProcess | null {
return this.#process ?? null;
}
@ -225,31 +206,13 @@ export class CDPBrowser extends BrowserBase {
});
}
override _getIsPageTargetCallback(): IsPageTargetCallback | undefined {
_getIsPageTargetCallback(): IsPageTargetCallback | undefined {
return this.#isPageTargetCallback;
}
/**
* Creates a new incognito browser context. This won't share cookies/cache with other
* browser contexts.
*
* @example
*
* ```ts
* (async () => {
* const browser = await puppeteer.launch();
* // Create a new incognito browser context.
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page in a pristine context.
* const page = await context.newPage();
* // Do stuff
* await page.goto('https://example.com');
* })();
* ```
*/
override async createIncognitoBrowserContext(
options: BrowserContextOptions = {}
): Promise<CDPBrowserContext> {
): Promise<CdpBrowserContext> {
const {proxyServer, proxyBypassList} = options;
const {browserContextId} = await this.#connection.send(
@ -259,7 +222,7 @@ export class CDPBrowser extends BrowserBase {
proxyBypassList: proxyBypassList && proxyBypassList.join(','),
}
);
const context = new CDPBrowserContext(
const context = new CdpBrowserContext(
this.#connection,
this,
browserContextId
@ -268,22 +231,15 @@ export class CDPBrowser extends BrowserBase {
return context;
}
/**
* Returns an array of all open browser contexts. In a newly created browser, this will
* return a single instance of {@link BrowserContext}.
*/
override browserContexts(): CDPBrowserContext[] {
override browserContexts(): CdpBrowserContext[] {
return [this.#defaultContext, ...Array.from(this.#contexts.values())];
}
/**
* Returns the default browser context. The default browser context cannot be closed.
*/
override defaultBrowserContext(): CDPBrowserContext {
override defaultBrowserContext(): CdpBrowserContext {
return this.#defaultContext;
}
override async _disposeContext(contextId?: string): Promise<void> {
async _disposeContext(contextId?: string): Promise<void> {
if (!contextId) {
return;
}
@ -310,7 +266,7 @@ export class CDPBrowser extends BrowserBase {
const createSession = (isAutoAttachEmulated: boolean) => {
return this.#connection._createSession(targetInfo, isAutoAttachEmulated);
};
const targetForFilter = new OtherTarget(
const otherTarget = new OtherTarget(
targetInfo,
session,
context,
@ -325,11 +281,10 @@ export class CDPBrowser extends BrowserBase {
this.#targetManager,
createSession,
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null,
this.#screenshotTaskQueue
this.#defaultViewport ?? null
);
}
if (this.#isPageTargetCallback(targetForFilter)) {
if (this.#isPageTargetCallback(otherTarget)) {
return new PageTarget(
targetInfo,
session,
@ -337,8 +292,7 @@ export class CDPBrowser extends BrowserBase {
this.#targetManager,
createSession,
this.#ignoreHTTPSErrors,
this.#defaultViewport ?? null,
this.#screenshotTaskQueue
this.#defaultViewport ?? null
);
}
if (
@ -353,89 +307,58 @@ export class CDPBrowser extends BrowserBase {
createSession
);
}
return new OtherTarget(
targetInfo,
session,
context,
this.#targetManager,
createSession
);
return otherTarget;
};
#onAttachedToTarget = async (target: CDPTarget) => {
#onAttachedToTarget = async (target: CdpTarget) => {
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
InitializationStatus.SUCCESS
) {
this.emit(BrowserEmittedEvents.TargetCreated, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetCreated, target);
this.emit(BrowserEvent.TargetCreated, target);
target.browserContext().emit(BrowserContextEvent.TargetCreated, target);
}
};
#onDetachedFromTarget = async (target: CDPTarget): Promise<void> => {
#onDetachedFromTarget = async (target: CdpTarget): Promise<void> => {
target._initializedDeferred.resolve(InitializationStatus.ABORTED);
target._isClosedDeferred.resolve();
if (
target._isTargetExposed() &&
(await target._initializedDeferred.valueOrThrow()) ===
InitializationStatus.SUCCESS
InitializationStatus.SUCCESS
) {
this.emit(BrowserEmittedEvents.TargetDestroyed, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetDestroyed, target);
this.emit(BrowserEvent.TargetDestroyed, target);
target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target);
}
};
#onTargetChanged = ({target}: {target: CDPTarget}): void => {
this.emit(BrowserEmittedEvents.TargetChanged, target);
target
.browserContext()
.emit(BrowserContextEmittedEvents.TargetChanged, target);
#onTargetChanged = ({target}: {target: CdpTarget}): void => {
this.emit(BrowserEvent.TargetChanged, target);
target.browserContext().emit(BrowserContextEvent.TargetChanged, target);
};
#onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => {
this.emit('targetdiscovered', targetInfo);
this.emit(BrowserEvent.TargetDiscovered, targetInfo);
};
/**
* The browser websocket endpoint which can be used as an argument to
* {@link Puppeteer.connect}.
*
* @returns The Browser websocket url.
*
* @remarks
*
* The format is `ws://${host}:${port}/devtools/browser/<id>`.
*
* You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`.
* Learn more about the
* {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and
* the {@link
* https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
* | browser endpoint}.
*/
override wsEndpoint(): string {
return this.#connection.url();
}
/**
* Promise which resolves to a new {@link Page} object. The Page is created in
* a default browser context.
*/
override async newPage(): Promise<Page> {
return await this.#defaultContext.newPage();
}
override async _createPageInContext(contextId?: string): Promise<Page> {
async _createPageInContext(contextId?: string): Promise<Page> {
const {targetId} = await this.#connection.send('Target.createTarget', {
url: 'about:blank',
browserContextId: contextId || undefined,
});
const target = (await this.waitForTarget(t => {
return (t as CDPTarget)._targetId === targetId;
})) as CDPTarget;
return (t as CdpTarget)._targetId === targetId;
})) as CdpTarget;
if (!target) {
throw new Error(`Missing target for page (id = ${targetId})`);
}
@ -454,24 +377,18 @@ export class CDPBrowser extends BrowserBase {
return page;
}
/**
* All active targets inside the Browser. In case of multiple browser contexts, returns
* an array with all the targets in all browser contexts.
*/
override targets(): CDPTarget[] {
override targets(): CdpTarget[] {
return Array.from(
this.#targetManager.getAvailableTargets().values()
).filter(target => {
return (
target._isTargetExposed() &&
target._initializedDeferred.value() === InitializationStatus.SUCCESS
);
});
}
/**
* The target associated with the browser.
*/
override target(): CDPTarget {
override target(): CdpTarget {
const browserTarget = this.targets().find(target => {
return target.type() === 'browser';
});
@ -486,10 +403,6 @@ export class CDPBrowser extends BrowserBase {
return version.product;
}
/**
* The browser's original user agent. Pages can override the browser user agent with
* {@link Page.setUserAgent}.
*/
override async userAgent(): Promise<string> {
const version = await this.#getVersion();
return version.userAgent;
@ -506,10 +419,7 @@ export class CDPBrowser extends BrowserBase {
this._detach();
}
/**
* Indicates that the browser is connected.
*/
override isConnected(): boolean {
override get connected(): boolean {
return !this.#connection._closed;
}
@ -521,12 +431,12 @@ export class CDPBrowser extends BrowserBase {
/**
* @internal
*/
export class CDPBrowserContext extends BrowserContext {
export class CdpBrowserContext extends BrowserContext {
#connection: Connection;
#browser: CDPBrowser;
#browser: CdpBrowser;
#id?: string;
constructor(connection: Connection, browser: CDPBrowser, contextId?: string) {
constructor(connection: Connection, browser: CdpBrowser, contextId?: string) {
super();
this.#connection = connection;
this.#browser = browser;
@ -537,51 +447,21 @@ export class CDPBrowserContext extends BrowserContext {
return this.#id;
}
/**
* An array of all active targets inside the browser context.
*/
override targets(): CDPTarget[] {
override targets(): CdpTarget[] {
return this.#browser.targets().filter(target => {
return target.browserContext() === this;
});
}
/**
* This searches for a target in this specific browser context.
*
* @example
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timeout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
override waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options: {timeout?: number} = {}
options: WaitForTargetOptions = {}
): Promise<Target> {
return this.#browser.waitForTarget(target => {
return target.browserContext() === this && predicate(target);
}, options);
}
/**
* An array of all pages inside the browser context.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
*/
override async pages(): Promise<Page[]> {
const pages = await Promise.all(
this.targets()
@ -601,31 +481,10 @@ export class CDPBrowserContext extends BrowserContext {
});
}
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
*
* @remarks
* The default browser context cannot be closed.
*/
override isIncognito(): boolean {
return !!this.#id;
}
/**
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
*/
override async overridePermissions(
origin: string,
permissions: Permission[]
@ -645,45 +504,20 @@ export class CDPBrowserContext extends BrowserContext {
});
}
/**
* Clears all permission overrides for the browser context.
*
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
override async clearPermissionOverrides(): Promise<void> {
await this.#connection.send('Browser.resetPermissions', {
browserContextId: this.#id || undefined,
});
}
/**
* Creates a new page in the browser context.
*/
override newPage(): Promise<Page> {
return this.#browser._createPageInContext(this.#id);
}
/**
* The browser this browser context belongs to.
*/
override browser(): CDPBrowser {
override browser(): CdpBrowser {
return this.#browser;
}
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
*
* @remarks
* Only incognito browser contexts can be closed.
*/
override async close(): Promise<void> {
assert(this.#id, 'Non-incognito profiles cannot be closed!');
await this.#browser._disposeContext(this.#id);

View File

@ -14,121 +14,50 @@
* limitations under the License.
*/
import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
import type {BidiBrowser} from '../bidi/Browser.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import type {
BrowserConnectOptions,
ConnectOptions,
} from '../common/ConnectOptions.js';
import {UnsupportedOperation} from '../common/Errors.js';
import {getFetch} from '../common/fetch.js';
import {debugError} from '../common/util.js';
import {isNode} from '../environment.js';
import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPBrowser} from './Browser.js';
import {CdpBrowser} from './Browser.js';
import {Connection} from './Connection.js';
import {ConnectionTransport} from './ConnectionTransport.js';
import {getFetch} from './fetch.js';
import type {ConnectOptions} from './Puppeteer.js';
import {Viewport} from './PuppeteerViewport.js';
import {debugError} from './util.js';
/**
* Generic browser options that can be passed when launching any browser or when
* connecting to an existing browser instance.
* @public
*/
export interface BrowserConnectOptions {
/**
* Whether to ignore HTTPS errors during navigation.
* @defaultValue `false`
*/
ignoreHTTPSErrors?: boolean;
/**
* Sets the viewport for each page.
*/
defaultViewport?: Viewport | null;
/**
* Slows down Puppeteer operations by the specified amount of milliseconds to
* aid debugging.
*/
slowMo?: number;
/**
* Callback to decide if Puppeteer should connect to a given target or not.
*/
targetFilter?: TargetFilterCallback;
/**
* @internal
*/
_isPageTarget?: IsPageTargetCallback;
/**
* @defaultValue 'cdp'
* @internal
*/
protocol?: 'cdp' | 'webDriverBiDi';
/**
* Timeout setting for individual protocol (CDP) calls.
*
* @defaultValue `180_000`
*/
protocolTimeout?: number;
}
const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
const getWebSocketTransportClass = async () => {
return isNode
? (await import('./NodeWebSocketTransport.js')).NodeWebSocketTransport
: (await import('./BrowserWebSocketTransport.js'))
? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport
: (await import('../common/BrowserWebSocketTransport.js'))
.BrowserWebSocketTransport;
};
/**
* Users should never call this directly; it's called when calling
* `puppeteer.connect`.
* `puppeteer.connect` with `protocol: 'cdp'`.
*
* @internal
*/
export async function _connectToCDPBrowser(
export async function _connectToCdpBrowser(
options: BrowserConnectOptions & ConnectOptions
): Promise<CDPBrowser> {
): Promise<CdpBrowser> {
const {
browserWSEndpoint,
browserURL,
ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600},
transport,
headers = {},
slowMo = 0,
defaultViewport = DEFAULT_VIEWPORT,
targetFilter,
_isPageTarget: isPageTarget,
protocolTimeout,
} = options;
assert(
Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) ===
1,
'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
);
const connection = await getCdpConnection(options);
let connection!: Connection;
if (transport) {
connection = new Connection('', transport, slowMo, protocolTimeout);
} else if (browserWSEndpoint) {
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(browserWSEndpoint, headers);
connection = new Connection(
browserWSEndpoint,
connectionTransport,
slowMo,
protocolTimeout
);
} else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(connectionURL);
connection = new Connection(
connectionURL,
connectionTransport,
slowMo,
protocolTimeout
);
}
const version = await connection.send('Browser.getVersion');
const product = version.product.toLowerCase().includes('firefox')
? 'firefox'
: 'chrome';
@ -136,7 +65,7 @@ export async function _connectToCDPBrowser(
const {browserContextIds} = await connection.send(
'Target.getBrowserContexts'
);
const browser = await CDPBrowser._create(
const browser = await CdpBrowser._create(
product || 'chrome',
connection,
browserContextIds,
@ -152,6 +81,42 @@ export async function _connectToCDPBrowser(
return browser;
}
/**
* Users should never call this directly; it's called when calling
* `puppeteer.connect` with `protocol: 'webDriverBiDi'`.
*
* @internal
*/
export async function _connectToBiDiOverCdpBrowser(
options: BrowserConnectOptions & ConnectOptions
): Promise<BidiBrowser> {
const {ignoreHTTPSErrors = false, defaultViewport = DEFAULT_VIEWPORT} =
options;
const connection = await getCdpConnection(options);
const version = await connection.send('Browser.getVersion');
if (version.product.toLowerCase().includes('firefox')) {
throw new UnsupportedOperation(
'Firefox is not supported in BiDi over CDP mode.'
);
}
// TODO: use other options too.
const BiDi = await import(/* webpackIgnore: true */ '../bidi/bidi.js');
const bidiConnection = await BiDi.connectBidiOverCdp(connection);
const bidiBrowser = await BiDi.BidiBrowser.create({
connection: bidiConnection,
closeCallback: () => {
return connection.send('Browser.close').catch(debugError);
},
process: undefined,
defaultViewport: defaultViewport,
ignoreHTTPSErrors: ignoreHTTPSErrors,
});
return bidiBrowser;
}
async function getWSEndpoint(browserURL: string): Promise<string> {
const endpointURL = new URL('/json/version', browserURL);
@ -174,3 +139,51 @@ async function getWSEndpoint(browserURL: string): Promise<string> {
throw error;
}
}
/**
* Returns a CDP connection for the given options.
*/
async function getCdpConnection(
options: BrowserConnectOptions & ConnectOptions
): Promise<Connection> {
const {
browserWSEndpoint,
browserURL,
transport,
headers = {},
slowMo = 0,
protocolTimeout,
} = options;
assert(
Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) ===
1,
'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
);
if (transport) {
return new Connection('', transport, slowMo, protocolTimeout);
} else if (browserWSEndpoint) {
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(browserWSEndpoint, headers);
return new Connection(
browserWSEndpoint,
connectionTransport,
slowMo,
protocolTimeout
);
} else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL);
const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport =
await WebSocketClass.create(connectionURL);
return new Connection(
connectionURL,
connectionTransport,
slowMo,
protocolTimeout
);
}
throw new Error('Invalid connection options');
}

View File

@ -0,0 +1,169 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {
type CDPEvents,
CDPSession,
CDPSessionEvent,
} from '../api/CDPSession.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
import {TargetCloseError} from '../common/Errors.js';
import {assert} from '../util/assert.js';
import {createProtocolErrorMessage} from '../util/ErrorLike.js';
import type {Connection} from './Connection.js';
import type {CdpTarget} from './Target.js';
/**
* @internal
*/
export class CdpCDPSession extends CDPSession {
#sessionId: string;
#targetType: string;
#callbacks = new CallbackRegistry();
#connection?: Connection;
#parentSessionId?: string;
#target?: CdpTarget;
/**
* @internal
*/
constructor(
connection: Connection,
targetType: string,
sessionId: string,
parentSessionId: string | undefined
) {
super();
this.#connection = connection;
this.#targetType = targetType;
this.#sessionId = sessionId;
this.#parentSessionId = parentSessionId;
}
/**
* Sets the {@link CdpTarget} associated with the session instance.
*
* @internal
*/
_setTarget(target: CdpTarget): void {
this.#target = target;
}
/**
* Gets the {@link CdpTarget} associated with the session instance.
*
* @internal
*/
_target(): CdpTarget {
assert(this.#target, 'Target must exist');
return this.#target;
}
override connection(): Connection | undefined {
return this.#connection;
}
override parentSession(): CDPSession | undefined {
if (!this.#parentSessionId) {
// To make it work in Firefox that does not have parent (tab) sessions.
return this;
}
const parent = this.#connection?.session(this.#parentSessionId);
return parent ?? undefined;
}
override send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#connection) {
return Promise.reject(
new TargetCloseError(
`Protocol error (${method}): Session closed. Most likely the ${this.#targetType} has been closed.`
)
);
}
// See the comment in Connection#send explaining why we do this.
const params = paramArgs.length ? paramArgs[0] : undefined;
return this.#connection._rawSend(
this.#callbacks,
method,
params,
this.#sessionId
);
}
/**
* @internal
*/
_onMessage(object: {
id?: number;
method: keyof CDPEvents;
params: CDPEvents[keyof CDPEvents];
error: {message: string; data: any; code: number};
result?: any;
}): void {
if (object.id) {
if (object.error) {
this.#callbacks.reject(
object.id,
createProtocolErrorMessage(object),
object.error.message
);
} else {
this.#callbacks.resolve(object.id, object.result);
}
} else {
assert(!object.id);
this.emit(object.method, object.params);
}
}
/**
* Detaches the cdpSession from the target. Once detached, the cdpSession object
* won't emit any events and can't be used to send messages.
*/
override async detach(): Promise<void> {
if (!this.#connection) {
throw new Error(
`Session already detached. Most likely the ${this.#targetType} has been closed.`
);
}
await this.#connection.send('Target.detachFromTarget', {
sessionId: this.#sessionId,
});
}
/**
* @internal
*/
_onClosed(): void {
this.#callbacks.clear();
this.#connection = undefined;
this.emit(CDPSessionEvent.Disconnected, undefined);
}
/**
* Returns the session's id.
*/
override id(): string {
return this.#sessionId;
}
}

View File

@ -14,29 +14,27 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import {TargetFilterCallback} from '../api/Browser.js';
import {TargetType} from '../api/Target.js';
import type {TargetFilterCallback} from '../api/Browser.js';
import {CDPSession, CDPSessionEvent} from '../api/CDPSession.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {debugError} from '../common/util.js';
import {assert} from '../util/assert.js';
import {Deferred} from '../util/Deferred.js';
import {CDPSession, CDPSessionEmittedEvents, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {InitializationStatus, CDPTarget} from './Target.js';
import type {CdpCDPSession} from './CDPSession.js';
import type {Connection} from './Connection.js';
import {CdpTarget, InitializationStatus} from './Target.js';
import {
TargetFactory,
TargetManager,
TargetManagerEmittedEvents,
type TargetFactory,
type TargetManager,
TargetManagerEvent,
type TargetManagerEvents,
} from './TargetManager.js';
import {debugError} from './util.js';
function isTargetExposed(target: CDPTarget): boolean {
return target.type() !== TargetType.TAB && !target._subtype();
}
function isPageTargetBecomingPrimary(
target: CDPTarget,
target: CdpTarget,
newTargetInfo: Protocol.Target.TargetInfo
): boolean {
return Boolean(target._subtype()) && !newTargetInfo.subtype;
@ -49,7 +47,10 @@ function isPageTargetBecomingPrimary(
*
* @internal
*/
export class ChromeTargetManager extends EventEmitter implements TargetManager {
export class ChromeTargetManager
extends EventEmitter<TargetManagerEvents>
implements TargetManager
{
#connection: Connection;
/**
* Keeps track of the following events: 'Target.targetCreated',
@ -66,11 +67,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
* A target is added to this map once ChromeTargetManager has created
* a Target and attached at least once to it.
*/
#attachedTargetsByTargetId = new Map<string, CDPTarget>();
#attachedTargetsByTargetId = new Map<string, CdpTarget>();
/**
* Tracks which sessions attach to which target.
*/
#attachedTargetsBySessionId = new Map<string, CDPTarget>();
#attachedTargetsBySessionId = new Map<string, CdpTarget>();
/**
* If a target was filtered out by `targetFilterCallback`, we still receive
* events about it from CDP, but we don't forward them to the rest of Puppeteer.
@ -81,7 +82,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
#attachedToTargetListenersBySession = new WeakMap<
CDPSession | Connection,
(event: Protocol.Target.AttachedToTargetEvent) => Promise<void>
(event: Protocol.Target.AttachedToTargetEvent) => void
>();
#detachedFromTargetListenersBySession = new WeakMap<
CDPSession | Connection,
@ -92,22 +93,15 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
#targetsIdsForInit = new Set<string>();
#waitForInitiallyDiscoveredTargets = true;
// TODO: remove the flag once the testing/rollout is done.
#tabMode: boolean;
#discoveryFilter: Protocol.Target.FilterEntry[];
#discoveryFilter: Protocol.Target.FilterEntry[] = [{}];
constructor(
connection: Connection,
targetFactory: TargetFactory,
targetFilterCallback?: TargetFilterCallback,
waitForInitiallyDiscoveredTargets = true,
useTabTarget = false
waitForInitiallyDiscoveredTargets = true
) {
super();
this.#tabMode = useTabTarget;
this.#discoveryFilter = this.#tabMode
? [{}]
: [{type: 'tab', exclude: true}, {}];
this.#connection = connection;
this.#targetFilterCallback = targetFilterCallback;
this.#targetFactory = targetFactory;
@ -116,16 +110,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
this.#connection.on('Target.targetCreated', this.#onTargetCreated);
this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed);
this.#connection.on('Target.targetInfoChanged', this.#onTargetInfoChanged);
this.#connection.on('sessiondetached', this.#onSessionDetached);
this.#connection.on(
CDPSessionEvent.SessionDetached,
this.#onSessionDetached
);
this.#setupAttachmentListeners(this.#connection);
this.#connection
.send('Target.setDiscoverTargets', {
discover: true,
filter: this.#discoveryFilter,
})
.then(this.#storeExistingTargetsForInit)
.catch(debugError);
}
#storeExistingTargetsForInit = () => {
@ -136,7 +125,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
targetId,
targetInfo,
] of this.#discoveredTargetsByTargetId.entries()) {
const targetForFilter = new CDPTarget(
const targetForFilter = new CdpTarget(
targetInfo,
undefined,
undefined,
@ -154,19 +143,24 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
};
async initialize(): Promise<void> {
await this.#connection.send('Target.setDiscoverTargets', {
discover: true,
filter: this.#discoveryFilter,
});
this.#storeExistingTargetsForInit();
await this.#connection.send('Target.setAutoAttach', {
waitForDebuggerOnStart: true,
flatten: true,
autoAttach: true,
filter: this.#tabMode
? [
{
type: 'page',
exclude: true,
},
...this.#discoveryFilter,
]
: this.#discoveryFilter,
filter: [
{
type: 'page',
exclude: true,
},
...this.#discoveryFilter,
],
});
this.#finishInitializationIfReady();
await this.#initializeDeferred.valueOrThrow();
@ -176,24 +170,21 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
this.#connection.off('Target.targetCreated', this.#onTargetCreated);
this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed);
this.#connection.off('Target.targetInfoChanged', this.#onTargetInfoChanged);
this.#connection.off('sessiondetached', this.#onSessionDetached);
this.#connection.off(
CDPSessionEvent.SessionDetached,
this.#onSessionDetached
);
this.#removeAttachmentListeners(this.#connection);
}
getAvailableTargets(): Map<string, CDPTarget> {
const result = new Map<string, CDPTarget>();
for (const [id, target] of this.#attachedTargetsByTargetId.entries()) {
if (isTargetExposed(target)) {
result.set(id, target);
}
}
return result;
getAvailableTargets(): ReadonlyMap<string, CdpTarget> {
return this.#attachedTargetsByTargetId;
}
#setupAttachmentListeners(session: CDPSession | Connection): void {
const listener = (event: Protocol.Target.AttachedToTargetEvent) => {
return this.#onAttachedToTarget(session, event);
void this.#onAttachedToTarget(session, event);
};
assert(!this.#attachedToTargetListenersBySession.has(session));
this.#attachedToTargetListenersBySession.set(session, listener);
@ -210,11 +201,9 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
}
#removeAttachmentListeners(session: CDPSession | Connection): void {
if (this.#attachedToTargetListenersBySession.has(session)) {
session.off(
'Target.attachedToTarget',
this.#attachedToTargetListenersBySession.get(session)!
);
const listener = this.#attachedToTargetListenersBySession.get(session);
if (listener) {
session.off('Target.attachedToTarget', listener);
this.#attachedToTargetListenersBySession.delete(session);
}
@ -237,7 +226,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
event.targetInfo
);
this.emit(TargetManagerEmittedEvents.TargetDiscovered, event.targetInfo);
this.emit(TargetManagerEvent.TargetDiscovered, event.targetInfo);
// The connection is already attached to the browser target implicitly,
// therefore, no new CDPSession is created and we have special handling
@ -263,8 +252,10 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
// Special case for service workers: report TargetGone event when
// the worker is destroyed.
const target = this.#attachedTargetsByTargetId.get(event.targetId);
this.emit(TargetManagerEmittedEvents.TargetGone, target);
this.#attachedTargetsByTargetId.delete(event.targetId);
if (target) {
this.emit(TargetManagerEvent.TargetGone, target);
this.#attachedTargetsByTargetId.delete(event.targetId);
}
}
};
@ -293,22 +284,19 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
target._initializedDeferred.value() === InitializationStatus.SUCCESS;
if (isPageTargetBecomingPrimary(target, event.targetInfo)) {
const target = this.#attachedTargetsByTargetId.get(
event.targetInfo.targetId
);
const session = target?._session();
assert(
session,
'Target that is being activated is missing a CDPSession.'
);
session.parentSession()?.emit(CDPSessionEmittedEvents.Swapped, session);
session.parentSession()?.emit(CDPSessionEvent.Swapped, session);
}
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) {
this.emit(TargetManagerEmittedEvents.TargetChanged, {
target: target,
this.emit(TargetManagerEvent.TargetChanged, {
target,
wasInitialized,
previousURL,
});
@ -347,10 +335,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
// `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we
// should determine if a target is auto-attached or not with the help of
// CDP.
if (
targetInfo.type === 'service_worker' &&
this.#connection.isAutoAttached(targetInfo.targetId)
) {
if (targetInfo.type === 'service_worker') {
this.#finishInitializationIfReady(targetInfo.targetId);
await silentDetach();
if (this.#attachedTargetsByTargetId.has(targetInfo.targetId)) {
@ -359,7 +344,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
const target = this.#targetFactory(targetInfo);
target._initialize();
this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
this.emit(TargetManagerEvent.TargetAvailable, target);
return;
}
@ -382,27 +367,29 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
return;
}
if (!isExistingTarget) {
target._initialize();
}
this.#setupAttachmentListeners(session);
if (isExistingTarget) {
(session as CdpCDPSession)._setTarget(target);
this.#attachedTargetsBySessionId.set(
session.id(),
this.#attachedTargetsByTargetId.get(targetInfo.targetId)!
);
} else {
target._initialize();
this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
this.#attachedTargetsBySessionId.set(session.id(), target);
}
parentSession.emit(CDPSessionEmittedEvents.Ready, session);
if (parentSession instanceof CDPSession) {
parentSession.emit(CDPSessionEvent.Ready, session);
} else {
parentSession.emit(CDPSessionEvent.Ready, session);
}
this.#targetsIdsForInit.delete(target._targetId);
if (!isExistingTarget && isTargetExposed(target)) {
this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
if (!isExistingTarget) {
this.emit(TargetManagerEvent.TargetAvailable, target);
}
this.#finishInitializationIfReady();
@ -439,8 +426,6 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
}
this.#attachedTargetsByTargetId.delete(target._targetId);
if (isTargetExposed(target)) {
this.emit(TargetManagerEmittedEvents.TargetGone, target);
}
this.emit(TargetManagerEvent.TargetGone, target);
};
}

View File

@ -0,0 +1,269 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Protocol} from 'devtools-protocol';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {
CDPSessionEvent,
type CDPSession,
type CDPSessionEvents,
} from '../api/CDPSession.js';
import {CallbackRegistry} from '../common/CallbackRegistry.js';
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 {createProtocolErrorMessage} from '../util/ErrorLike.js';
import {CdpCDPSession} from './CDPSession.js';
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
/**
* @public
*/
export type {ConnectionTransport, ProtocolMapping};
/**
* @public
*/
export class Connection extends EventEmitter<CDPSessionEvents> {
#url: string;
#transport: ConnectionTransport;
#delay: number;
#timeout: number;
#sessions = new Map<string, CdpCDPSession>();
#closed = false;
#manuallyAttached = new Set<string>();
#callbacks = new CallbackRegistry();
constructor(
url: string,
transport: ConnectionTransport,
delay = 0,
timeout?: number
) {
super();
this.#url = url;
this.#delay = delay;
this.#timeout = timeout ?? 180_000;
this.#transport = transport;
this.#transport.onmessage = this.onMessage.bind(this);
this.#transport.onclose = this.#onClose.bind(this);
}
static fromSession(session: CDPSession): Connection | undefined {
return session.connection();
}
get timeout(): number {
return this.#timeout;
}
/**
* @internal
*/
get _closed(): boolean {
return this.#closed;
}
/**
* @internal
*/
get _sessions(): Map<string, CDPSession> {
return this.#sessions;
}
/**
* @param sessionId - The session id
* @returns The current CDP session if it exists
*/
session(sessionId: string): CDPSession | null {
return this.#sessions.get(sessionId) || null;
}
url(): string {
return this.#url;
}
send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
// There is only ever 1 param arg passed, but the Protocol defines it as an
// array of 0 or 1 items See this comment:
// https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285
// which explains why the protocol defines the params this way for better
// type-inference.
// So now we check if there are any params or not and deal with them accordingly.
const params = paramArgs.length ? paramArgs[0] : undefined;
return this._rawSend(this.#callbacks, method, params);
}
/**
* @internal
*/
_rawSend<T extends keyof ProtocolMapping.Commands>(
callbacks: CallbackRegistry,
method: T,
params: ProtocolMapping.Commands[T]['paramsType'][0],
sessionId?: string
): Promise<ProtocolMapping.Commands[T]['returnType']> {
return callbacks.create(method, this.#timeout, id => {
const stringifiedMessage = JSON.stringify({
method,
params,
id,
sessionId,
});
debugProtocolSend(stringifiedMessage);
this.#transport.send(stringifiedMessage);
}) as Promise<ProtocolMapping.Commands[T]['returnType']>;
}
/**
* @internal
*/
async closeBrowser(): Promise<void> {
await this.send('Browser.close');
}
/**
* @internal
*/
protected async onMessage(message: string): Promise<void> {
if (this.#delay) {
await new Promise(r => {
return setTimeout(r, this.#delay);
});
}
debugProtocolReceive(message);
const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new CdpCDPSession(
this,
object.params.targetInfo.type,
sessionId,
object.sessionId
);
this.#sessions.set(sessionId, session);
this.emit(CDPSessionEvent.SessionAttached, session);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit(CDPSessionEvent.SessionAttached, session);
}
} else if (object.method === 'Target.detachedFromTarget') {
const session = this.#sessions.get(object.params.sessionId);
if (session) {
session._onClosed();
this.#sessions.delete(object.params.sessionId);
this.emit(CDPSessionEvent.SessionDetached, session);
const parentSession = this.#sessions.get(object.sessionId);
if (parentSession) {
parentSession.emit(CDPSessionEvent.SessionDetached, session);
}
}
}
if (object.sessionId) {
const session = this.#sessions.get(object.sessionId);
if (session) {
session._onMessage(object);
}
} else if (object.id) {
if (object.error) {
this.#callbacks.reject(
object.id,
createProtocolErrorMessage(object),
object.error.message
);
} else {
this.#callbacks.resolve(object.id, object.result);
}
} else {
this.emit(object.method, object.params);
}
}
#onClose(): void {
if (this.#closed) {
return;
}
this.#closed = true;
this.#transport.onmessage = undefined;
this.#transport.onclose = undefined;
this.#callbacks.clear();
for (const session of this.#sessions.values()) {
session._onClosed();
}
this.#sessions.clear();
this.emit(CDPSessionEvent.Disconnected, undefined);
}
dispose(): void {
this.#onClose();
this.#transport.close();
}
/**
* @internal
*/
isAutoAttached(targetId: string): boolean {
return !this.#manuallyAttached.has(targetId);
}
/**
* @internal
*/
async _createSession(
targetInfo: Protocol.Target.TargetInfo,
isAutoAttachEmulated = true
): Promise<CDPSession> {
if (!isAutoAttachEmulated) {
this.#manuallyAttached.add(targetInfo.targetId);
}
const {sessionId} = await this.send('Target.attachToTarget', {
targetId: targetInfo.targetId,
flatten: true,
});
this.#manuallyAttached.delete(targetInfo.targetId);
const session = this.#sessions.get(sessionId);
if (!session) {
throw new Error('CDPSession creation failed.');
}
return session;
}
/**
* @param targetInfo - The target info
* @returns The CDP session that is created
*/
async createSession(
targetInfo: Protocol.Target.TargetInfo
): Promise<CDPSession> {
return await this._createSession(targetInfo, false);
}
}
/**
* @internal
*/
export function isTargetClosedError(error: Error): boolean {
return error instanceof TargetCloseError;
}

View File

@ -14,23 +14,13 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Protocol} from 'devtools-protocol';
import type {CDPSession} from '../api/CDPSession.js';
import {EventSubscription} from '../common/EventEmitter.js';
import {debugError, PuppeteerURL} from '../common/util.js';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {
addEventListener,
debugError,
PuppeteerEventListener,
PuppeteerURL,
removeEventListeners,
} from './util.js';
/**
* @internal
*/
export {PuppeteerEventListener};
import {DisposableStack} from '../util/disposable.js';
/**
* The CoverageEntry class represents one entry of the coverage report.
@ -211,7 +201,7 @@ export class JSCoverage {
#enabled = false;
#scriptURLs = new Map<string, string>();
#scriptSources = new Map<string, string>();
#eventListeners: PuppeteerEventListener[] = [];
#subscriptions?: DisposableStack;
#resetOnNavigation = false;
#reportAnonymousScripts = false;
#includeRawScriptCoverage = false;
@ -248,18 +238,21 @@ export class JSCoverage {
this.#enabled = true;
this.#scriptURLs.clear();
this.#scriptSources.clear();
this.#eventListeners = [
addEventListener(
this.#subscriptions = new DisposableStack();
this.#subscriptions.use(
new EventSubscription(
this.#client,
'Debugger.scriptParsed',
this.#onScriptParsed.bind(this)
),
addEventListener(
)
);
this.#subscriptions.use(
new EventSubscription(
this.#client,
'Runtime.executionContextsCleared',
this.#onExecutionContextsCleared.bind(this)
),
];
)
);
await Promise.all([
this.#client.send('Profiler.enable'),
this.#client.send('Profiler.startPreciseCoverage', {
@ -313,7 +306,7 @@ export class JSCoverage {
this.#client.send('Debugger.disable'),
]);
removeEventListeners(this.#eventListeners);
this.#subscriptions?.dispose();
const coverage = [];
const profileResponse = result[0];
@ -350,7 +343,7 @@ export class CSSCoverage {
#enabled = false;
#stylesheetURLs = new Map<string, string>();
#stylesheetSources = new Map<string, string>();
#eventListeners: PuppeteerEventListener[] = [];
#eventListeners?: DisposableStack;
#resetOnNavigation = false;
constructor(client: CDPSession) {
@ -371,18 +364,21 @@ export class CSSCoverage {
this.#enabled = true;
this.#stylesheetURLs.clear();
this.#stylesheetSources.clear();
this.#eventListeners = [
addEventListener(
this.#eventListeners = new DisposableStack();
this.#eventListeners.use(
new EventSubscription(
this.#client,
'CSS.styleSheetAdded',
this.#onStyleSheet.bind(this)
),
addEventListener(
)
);
this.#eventListeners.use(
new EventSubscription(
this.#client,
'Runtime.executionContextsCleared',
this.#onExecutionContextsCleared.bind(this)
),
];
)
);
await Promise.all([
this.#client.send('DOM.enable'),
this.#client.send('CSS.enable'),
@ -426,7 +422,7 @@ export class CSSCoverage {
this.#client.send('CSS.disable'),
this.#client.send('DOM.disable'),
]);
removeEventListeners(this.#eventListeners);
this.#eventListeners?.dispose();
// aggregate by styleSheetId
const styleSheetIdToCoverage = new Map();

Some files were not shown because too many files have changed in this diff Show More