зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
3037fef913
Коммит
1cd37ca581
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,7 @@ node_modules
|
|||
# Production
|
||||
build/
|
||||
lib/
|
||||
bin/
|
||||
|
||||
# Generated files
|
||||
**/*.tsbuildinfo
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ node_modules
|
|||
# Production
|
||||
build/
|
||||
lib/
|
||||
bin/
|
||||
|
||||
# Generated files
|
||||
**/*.tsbuildinfo
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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']);
|
||||
},
|
||||
});
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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": [
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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.')
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>>;
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
|
@ -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.');
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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)}}`;
|
||||
}
|
|
@ -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
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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(': ');
|
|
@ -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':
|
|
@ -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>
|
||||
>;
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
|
@ -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');
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче