Bug 1817934 - [puppeteer] Vendor Puppeteer v19.7.2. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D170799
This commit is contained in:
Henrik Skupin 2023-02-25 10:14:26 +00:00
parent 13840898bb
commit 33c84b1801
213 changed files with 7845 additions and 35529 deletions

View file

@ -117,10 +117,14 @@ _OPT\.OBJ/
# Ignore node_module directories and npm artifacts
^remote/test/puppeteer/.*\.tsbuildinfo
^remote/test/puppeteer/.*/lib/
^remote/test/puppeteer/.*/node_modules/
^remote/test/puppeteer/.*/\.wireit/
^remote/test/puppeteer/\.devcontainer/
^remote/test/puppeteer/\.github
^remote/test/puppeteer/\.husky
^remote/test/puppeteer/\.wireit/
^remote/test/puppeteer/coverage/
^remote/test/puppeteer/.devcontainer/
^remote/test/puppeteer/docker/
^remote/test/puppeteer/docs/puppeteer-core\.api\.json
^remote/test/puppeteer/docs/puppeteer\.api\.json

View file

@ -22,6 +22,8 @@ generated/
# IDE Artifacts
.vscode
!.vscode/extensions.json
!.vscode/*.template.json
.devcontainer
# Misc

View file

@ -110,6 +110,14 @@ module.exports = {
],
'import/extensions': ['error', 'ignorePackages'],
'import/order': [
'error',
{
'newlines-between': 'always',
alphabetize: {order: 'asc', caseInsensitive: true},
},
],
'no-restricted-syntax': [
'error',
// Don't allow underscored declarations on camelCased variables/properties.

View file

@ -23,6 +23,7 @@ generated/
# IDE Artifacts
.vscode/*
!.vscode/extensions.json
!.vscode/*.template.json
.devcontainer
# Misc

View file

@ -1,6 +1,6 @@
{
"packages/puppeteer": "19.6.0",
"packages/puppeteer-core": "19.6.0",
"packages/puppeteer": "19.7.2",
"packages/puppeteer-core": "19.7.2",
"packages/testserver": "0.6.0",
"packages/ng-schematics": "0.1.0"
}

View file

@ -44,9 +44,17 @@ npm i puppeteer
```
When you install Puppeteer, it automatically downloads a recent version of
Chromium (~170MB macOS, ~282MB Linux, ~280MB Windows) that is
[guaranteed to work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy)
with Puppeteer. For a version of Puppeteer without installation, see
Chromium (~170MB macOS, ~282MB Linux, ~280MB Windows) that is [guaranteed to
work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy)
with Puppeteer. The browser is downloaded to the `$HOME/.cache/puppeteer` folder
by default (starting with Puppeteer v19.0.0).
If you deploy a project using Puppeteer to a hosting provider, such as Render or
Heroku, you might need to reconfigure the location of the cache to be within
your project folder (see an example below) because not all hosting providers
include `$HOME/.cache` into the project's deployment.
For a version of Puppeteer without the browser installation, see
[`puppeteer-core`](#puppeteer-core).
#### Configuration
@ -149,7 +157,7 @@ import puppeteer from 'puppeteer';
await page.waitForSelector(searchResultSelector);
await page.click(searchResultSelector);
// Localte the full title with a unique string
// Locate the full title with a unique string
const textSelector = await page.waitForSelector(
'text/Customize and automate'
);

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,6 @@ origin:
description: Headless Chrome Node API
license: Apache-2.0
name: puppeteer
release: e13e9647fc0d917da94af8851a09ed318fb0e07c
url: /Users/alexandraborovova/Projects/puppeteer
release: puppeteer-v19.7.2
url: https://github.com/puppeteer/puppeteer.git
schema: 1

View file

@ -19,7 +19,7 @@
"@microsoft/api-extractor": "7.33.7",
"@microsoft/api-extractor-model": "7.25.3",
"@pptr/testserver": "file:packages/testserver",
"@rollup/plugin-commonjs": "24.0.0",
"@rollup/plugin-commonjs": "24.0.1",
"@rollup/plugin-node-resolve": "15.0.1",
"@types/debug": "4.1.7",
"@types/diff": "5.0.2",
@ -40,7 +40,6 @@
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"c8": "7.12.0",
"chromium-bidi": "0.4.3",
"commonmark": "0.30.0",
"cross-env": "7.0.3",
"diff": "5.1.0",
@ -70,8 +69,7 @@
"pngjs": "6.0.0",
"prettier": "2.8.1",
"puppeteer": "file:packages/puppeteer",
"rollup": "2.79.1",
"rollup-plugin-dts": "4.2.2",
"rollup": "3.12.1",
"semver": "7.3.8",
"sinon": "15.0.1",
"source-map-support": "0.5.21",
@ -103,11 +101,11 @@
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.1501.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.2.tgz",
"integrity": "sha512-AfORVGLN0FBIUXO3FkfGOKu+Gz6oJjF8Bu8cPn27duiI0wszxGNY3fATKwbSg7JcKx1oQS/G7RjyC5OiTA6a0Q==",
"version": "0.1501.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz",
"integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==",
"dependencies": {
"@angular-devkit/core": "15.1.2",
"@angular-devkit/core": "15.1.6",
"rxjs": "6.6.7"
},
"engines": {
@ -116,10 +114,10 @@
"yarn": ">= 1.13.0"
}
},
"node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.2.tgz",
"integrity": "sha512-wkLZYvTZt30Ge6Z83Gxsr6mO1TIHCu3SImdE0zwW63EdU9o1NYkU74z1D9VUZ9Up7uHi1cHs/dssbxUuZ4eWOA==",
"node_modules/@angular-devkit/core": {
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz",
"integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==",
"dependencies": {
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
@ -141,7 +139,7 @@
}
}
},
"node_modules/@angular-devkit/architect/node_modules/ajv": {
"node_modules/@angular-devkit/core/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
@ -156,7 +154,7 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@angular-devkit/architect/node_modules/source-map": {
"node_modules/@angular-devkit/core/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
@ -1386,14 +1384,18 @@
"resolved": "test",
"link": true
},
"node_modules/@puppeteer/browsers": {
"resolved": "packages/browsers",
"link": true
},
"node_modules/@puppeteer/ng-schematics": {
"resolved": "packages/ng-schematics",
"link": true
},
"node_modules/@rollup/plugin-commonjs": {
"version": "24.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz",
"integrity": "sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==",
"version": "24.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz",
"integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
@ -1421,18 +1423,6 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"node_modules/@rollup/plugin-commonjs/node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz",
@ -2673,13 +2663,14 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/chromium-bidi": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.3.tgz",
"integrity": "sha512-A40H1rdpJqkTdnGhnYDzMhtDdIbkXNFj2wgIfivMXL7LyHFDmBtv1hdyycDhnxtYunbPLDZtTs/n+ZT5j7Vnew==",
"dev": true,
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.4.tgz",
"integrity": "sha512-4BX5cSaponuvVT1+SbLYTOAgDoVtX/Khoc9UsbFJ/AsPVUeFAM3RiIDFI6XFhLYMi9WmVJqh1ZH+dRpNKkKwiQ==",
"dependencies": {
"mitt": "3.0.0"
},
"peerDependencies": {
"devtools-protocol": "*",
"mitt": "*"
"devtools-protocol": "*"
}
},
"node_modules/cli-cursor": {
@ -2717,7 +2708,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@ -3036,9 +3026,9 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1082910",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1082910.tgz",
"integrity": "sha512-RqoZ2GmqaNxyx+99L/RemY5CkwG9D0WEfOKxekwCRXOGrDCep62ngezEJUVMq6rISYQ+085fJnWDQqGHlxVNww=="
"version": "0.0.1094867",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz",
"integrity": "sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ=="
},
"node_modules/diff": {
"version": "5.1.0",
@ -5617,9 +5607,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@ -5851,12 +5841,12 @@
}
},
"node_modules/magic-string": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
"integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dependencies": {
"sourcemap-codec": "^1.4.8"
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
@ -6049,8 +6039,7 @@
"node_modules/mitt": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz",
"integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==",
"dev": true
"integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
@ -7363,55 +7352,21 @@
}
},
"node_modules/rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
"node": ">=14.18.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-dts": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-4.2.2.tgz",
"integrity": "sha512-A3g6Rogyko/PXeKoUlkjxkP++8UDVpgA7C+Tdl77Xj4fgEaIjPSnxRmR53EzvoYy97VMVwLAOcWJudaVAuxneQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.26.1"
},
"engines": {
"node": ">=v12.22.11"
},
"funding": {
"url": "https://github.com/sponsors/Swatinem"
},
"optionalDependencies": {
"@babel/code-frame": "^7.16.7"
},
"peerDependencies": {
"rollup": "^2.55",
"typescript": "^4.1"
}
},
"node_modules/rollup-plugin-dts/node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"dev": true,
"optional": true,
"dependencies": {
"@babel/highlight": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -8040,9 +7995,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@ -8192,7 +8147,7 @@
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -8469,10 +8424,9 @@
}
},
"node_modules/yargs": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz",
"integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@ -8480,7 +8434,7 @@
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.0.0"
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
@ -8543,7 +8497,6 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
@ -8606,14 +8559,62 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
"packages/browsers": {
"name": "@puppeteer/browsers",
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
"debug": "4.3.4",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"yargs": "17.7.0"
},
"bin": {
"browsers": "lib/cjs/browsers.js"
},
"devDependencies": {
"@types/node": "^14.15.0",
"@types/yargs": "17.0.22"
},
"engines": {
"node": ">=14.1.0"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/browsers/node_modules/@types/node": {
"version": "14.18.36",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz",
"integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==",
"dev": true
},
"packages/browsers/node_modules/@types/yargs": {
"version": "17.0.22",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
"integrity": "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
}
},
"packages/ng-schematics": {
"name": "@puppeteer/ng-schematics",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"@angular-devkit/architect": "^0.1501.2",
"@angular-devkit/core": "^15.1.2",
"@angular-devkit/schematics": "^15.1.2"
"@angular-devkit/architect": "^0.1501.6",
"@angular-devkit/core": "^15.1.6",
"@angular-devkit/schematics": "^15.1.6"
},
"devDependencies": {
"@schematics/angular": "^14.2.8",
@ -8623,37 +8624,12 @@
"node": ">=14.1.0"
}
},
"packages/ng-schematics/node_modules/@angular-devkit/core": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.2.tgz",
"integrity": "sha512-wkLZYvTZt30Ge6Z83Gxsr6mO1TIHCu3SImdE0zwW63EdU9o1NYkU74z1D9VUZ9Up7uHi1cHs/dssbxUuZ4eWOA==",
"dependencies": {
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"jsonc-parser": "3.2.0",
"rxjs": "6.6.7",
"source-map": "0.7.4"
},
"engines": {
"node": "^14.20.0 || ^16.13.0 || >=18.10.0",
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
"yarn": ">= 1.13.0"
},
"peerDependencies": {
"chokidar": "^3.5.2"
},
"peerDependenciesMeta": {
"chokidar": {
"optional": true
}
}
},
"packages/ng-schematics/node_modules/@angular-devkit/schematics": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.2.tgz",
"integrity": "sha512-HjJPm+4SS5TdAHHvdpXLv25wsvwVOn5RYs0A9MazTndlm80ct3PKeYUgakNDRFjRj8uORNlJMKmQIIhUSDjFsw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz",
"integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==",
"dependencies": {
"@angular-devkit/core": "15.1.2",
"@angular-devkit/core": "15.1.6",
"jsonc-parser": "3.2.0",
"magic-string": "0.27.0",
"ora": "5.4.1",
@ -8671,21 +8647,6 @@
"integrity": "sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==",
"dev": true
},
"packages/ng-schematics/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"packages/ng-schematics/node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
@ -8697,16 +8658,8 @@
"node": ">=12"
}
},
"packages/ng-schematics/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">= 8"
}
},
"packages/puppeteer": {
"version": "19.6.0",
"version": "19.7.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -8714,19 +8667,20 @@
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"puppeteer-core": "19.6.0"
"puppeteer-core": "19.7.2"
},
"engines": {
"node": ">=14.1.0"
}
},
"packages/puppeteer-core": {
"version": "19.6.0",
"version": "19.7.2",
"license": "Apache-2.0",
"dependencies": {
"chromium-bidi": "0.4.4",
"cross-fetch": "3.1.5",
"debug": "4.3.4",
"devtools-protocol": "0.0.1082910",
"devtools-protocol": "0.0.1094867",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"proxy-from-env": "1.1.0",
@ -8737,6 +8691,14 @@
},
"engines": {
"node": ">=14.1.0"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"packages/puppeteer/node_modules/argparse": {
@ -8982,26 +8944,26 @@
}
},
"@angular-devkit/architect": {
"version": "0.1501.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.2.tgz",
"integrity": "sha512-AfORVGLN0FBIUXO3FkfGOKu+Gz6oJjF8Bu8cPn27duiI0wszxGNY3fATKwbSg7JcKx1oQS/G7RjyC5OiTA6a0Q==",
"version": "0.1501.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz",
"integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==",
"requires": {
"@angular-devkit/core": "15.1.2",
"@angular-devkit/core": "15.1.6",
"rxjs": "6.6.7"
}
},
"dependencies": {
"@angular-devkit/core": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.2.tgz",
"integrity": "sha512-wkLZYvTZt30Ge6Z83Gxsr6mO1TIHCu3SImdE0zwW63EdU9o1NYkU74z1D9VUZ9Up7uHi1cHs/dssbxUuZ4eWOA==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz",
"integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==",
"requires": {
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"jsonc-parser": "3.2.0",
"rxjs": "6.6.7",
"source-map": "0.7.4"
}
},
"dependencies": {
"ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@ -10007,34 +9969,54 @@
}
}
},
"@puppeteer/browsers": {
"version": "file:packages/browsers",
"requires": {
"@types/node": "^14.15.0",
"@types/yargs": "17.0.22",
"debug": "4.3.4",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"yargs": "17.7.0"
},
"dependencies": {
"@types/node": {
"version": "14.18.36",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz",
"integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==",
"dev": true
},
"@types/yargs": {
"version": "17.0.22",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
"integrity": "sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
}
}
},
"@puppeteer/ng-schematics": {
"version": "file:packages/ng-schematics",
"requires": {
"@angular-devkit/architect": "^0.1501.2",
"@angular-devkit/core": "^15.1.2",
"@angular-devkit/schematics": "^15.1.2",
"@angular-devkit/architect": "^0.1501.6",
"@angular-devkit/core": "^15.1.6",
"@angular-devkit/schematics": "^15.1.6",
"@schematics/angular": "^14.2.8",
"@types/node": "^14.15.0"
},
"dependencies": {
"@angular-devkit/core": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.2.tgz",
"integrity": "sha512-wkLZYvTZt30Ge6Z83Gxsr6mO1TIHCu3SImdE0zwW63EdU9o1NYkU74z1D9VUZ9Up7uHi1cHs/dssbxUuZ4eWOA==",
"requires": {
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"jsonc-parser": "3.2.0",
"rxjs": "6.6.7",
"source-map": "0.7.4"
}
},
"@angular-devkit/schematics": {
"version": "15.1.2",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.2.tgz",
"integrity": "sha512-HjJPm+4SS5TdAHHvdpXLv25wsvwVOn5RYs0A9MazTndlm80ct3PKeYUgakNDRFjRj8uORNlJMKmQIIhUSDjFsw==",
"version": "15.1.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz",
"integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==",
"requires": {
"@angular-devkit/core": "15.1.2",
"@angular-devkit/core": "15.1.6",
"jsonc-parser": "3.2.0",
"magic-string": "0.27.0",
"ora": "5.4.1",
@ -10047,17 +10029,6 @@
"integrity": "sha512-qelS/Ra6sacc4loe/3MSjXNL1dNQ/GjxNHVzuChwMfmk7HuycRLVQN2qNY3XahK+fZc5E2szqQSKUyAF0E+2bg==",
"dev": true
},
"ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
@ -10065,18 +10036,13 @@
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.13"
}
},
"source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
}
}
},
"@rollup/plugin-commonjs": {
"version": "24.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz",
"integrity": "sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==",
"version": "24.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz",
"integrity": "sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^5.0.1",
@ -10092,15 +10058,6 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.13"
}
}
}
},
@ -11056,11 +11013,12 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"chromium-bidi": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.3.tgz",
"integrity": "sha512-A40H1rdpJqkTdnGhnYDzMhtDdIbkXNFj2wgIfivMXL7LyHFDmBtv1hdyycDhnxtYunbPLDZtTs/n+ZT5j7Vnew==",
"dev": true,
"requires": {}
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.4.tgz",
"integrity": "sha512-4BX5cSaponuvVT1+SbLYTOAgDoVtX/Khoc9UsbFJ/AsPVUeFAM3RiIDFI6XFhLYMi9WmVJqh1ZH+dRpNKkKwiQ==",
"requires": {
"mitt": "3.0.0"
}
},
"cli-cursor": {
"version": "3.1.0",
@ -11085,7 +11043,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@ -11320,9 +11277,9 @@
}
},
"devtools-protocol": {
"version": "0.0.1082910",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1082910.tgz",
"integrity": "sha512-RqoZ2GmqaNxyx+99L/RemY5CkwG9D0WEfOKxekwCRXOGrDCep62ngezEJUVMq6rISYQ+085fJnWDQqGHlxVNww=="
"version": "0.0.1094867",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1094867.tgz",
"integrity": "sha512-pmMDBKiRVjh0uKK6CT1WqZmM3hBVSgD+N2MrgyV1uNizAZMw4tx6i/RTc+/uCsKSCmg0xXx7arCP/OFcIwTsiQ=="
},
"diff": {
"version": "5.1.0",
@ -13126,9 +13083,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonc-parser": {
@ -13318,12 +13275,12 @@
}
},
"magic-string": {
"version": "0.26.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
"integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.8"
"@jridgewell/sourcemap-codec": "^1.4.13"
}
},
"make-dir": {
@ -13460,8 +13417,7 @@
"mitt": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz",
"integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==",
"dev": true
"integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
},
"mkdirp-classic": {
"version": "0.5.3",
@ -14130,7 +14086,7 @@
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"puppeteer-core": "19.6.0"
"puppeteer-core": "19.7.2"
},
"dependencies": {
"argparse": {
@ -14162,9 +14118,10 @@
"puppeteer-core": {
"version": "file:packages/puppeteer-core",
"requires": {
"chromium-bidi": "0.4.4",
"cross-fetch": "3.1.5",
"debug": "4.3.4",
"devtools-protocol": "0.0.1082910",
"devtools-protocol": "0.0.1094867",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"proxy-from-env": "1.1.0",
@ -14482,36 +14439,14 @@
}
},
"rollup": {
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz",
"integrity": "sha512-t9elERrz2i4UU9z7AwISj3CQcXP39cWxgRWLdf4Tm6aKm1eYrqHIgjzXBgb67GNY1sZckTFFi0oMozh3/S++Ig==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"rollup-plugin-dts": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-4.2.2.tgz",
"integrity": "sha512-A3g6Rogyko/PXeKoUlkjxkP++8UDVpgA7C+Tdl77Xj4fgEaIjPSnxRmR53EzvoYy97VMVwLAOcWJudaVAuxneQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.16.7",
"magic-string": "^0.26.1"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"dev": true,
"optional": true,
"requires": {
"@babel/highlight": "^7.18.6"
}
}
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -14981,9 +14916,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@ -15090,7 +15025,7 @@
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true
"devOptional": true
},
"unbox-primitive": {
"version": "1.0.2",
@ -15300,10 +15235,9 @@
"dev": true
},
"yargs": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz",
"integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==",
"dev": true,
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.0.tgz",
"integrity": "sha512-dwqOPg5trmrre9+v8SUo2q/hAwyKoVfu8OC1xPHKJGNdxAvPl4sKxL4vBnh3bQz/ZvvGAFeA5H3ou2kcOY8sQQ==",
"requires": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@ -15311,14 +15245,13 @@
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.0.0"
"yargs-parser": "^21.1.1"
},
"dependencies": {
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
}
}
},

View file

@ -7,13 +7,14 @@
},
"scripts": {
"bisect": "tsx tools/bisect.ts",
"build": "npm run build --workspaces --if-present",
"build": "wireit",
"build:docs": "wireit",
"check:pinned-deps": "tsx tools/ensure-pinned-deps",
"check": "npm run check --workspaces --if-present && run-p check:*",
"clean": "npm run clean --workspaces --if-present && rimraf **/.wireit",
"clean": "rimraf **/.wireit && npm run clean --workspaces --if-present",
"commitlint": "commitlint --from=HEAD~1",
"debug": "mocha --inspect-brk",
"docs": "run-s build generate:markdown",
"docs": "run-s build:docs generate:markdown",
"format:eslint": "eslint --ext js --ext ts --fix .",
"format:prettier": "prettier --write .",
"format": "run-s format:*",
@ -25,17 +26,93 @@
"prepare": "husky install",
"test-install": "npm run test --workspace @puppeteer-test/installation",
"test-types": "tsd -t packages/puppeteer",
"test:chrome:headful": "npm test -- --test-suite chrome-headful",
"test:chrome:headless-chrome": "npm test -- --test-suite chrome-new-headless",
"test:chrome:headless": "npm test -- --test-suite chrome-headless",
"test:chrome:bidi": "npm test -- --test-suite chrome-bidi",
"test:chrome": "run-s test:chrome:*",
"test:firefox:bidi": "npm test -- --test-suite firefox-bidi",
"test:firefox:headful": "npm test -- --test-suite firefox-headful",
"test:firefox:headless": "npm test -- --test-suite firefox-headless",
"test:firefox": "run-s test:firefox:*",
"test:chrome:headful": "wireit",
"test:chrome:new-headless": "wireit",
"test:chrome:headless": "wireit",
"test:chrome:bidi": "wireit",
"test:chrome": "wireit",
"test:firefox:bidi": "wireit",
"test:firefox:headful": "wireit",
"test:firefox:headless": "wireit",
"test:firefox": "wireit",
"test": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node tools/mochaRunner/lib/main.js"
},
"wireit": {
"build": {
"dependencies": [
"./packages/browsers:build",
"./packages/ng-schematics:build",
"./packages/puppeteer-core:build",
"./packages/puppeteer:build",
"./packages/testserver:build",
"./test:build",
"./test/installation:build"
]
},
"build:docs": {
"dependencies": [
"./packages/puppeteer:build:docs",
"./packages/puppeteer-core:build:docs"
]
},
"test:chrome:headful": {
"command": "npm test -- --test-suite chrome-headful",
"dependencies": [
"./test:build"
]
},
"test:chrome:headless": {
"command": "npm test -- --test-suite chrome-headless",
"dependencies": [
"./test:build"
]
},
"test:chrome:new-headless": {
"command": "npm test -- --test-suite chrome-new-headless",
"dependencies": [
"./test:build"
]
},
"test:chrome:bidi": {
"command": "npm test -- --test-suite chrome-bidi",
"dependencies": [
"./test:build"
]
},
"test:firefox:headful": {
"command": "npm test -- --test-suite firefox-headful",
"dependencies": [
"./test:build"
]
},
"test:firefox:headless": {
"command": "npm test -- --test-suite firefox-headless",
"dependencies": [
"./test:build"
]
},
"test:firefox:bidi": {
"command": "npm test -- --test-suite firefox-bidi",
"dependencies": [
"./test:build"
]
},
"test:chrome": {
"dependencies": [
"test:chrome:headful",
"test:chrome:headless",
"test:chrome:new-headless",
"test:chrome:bidi"
]
},
"test:firefox": {
"dependencies": [
"test:firefox:headful",
"test:firefox:headless",
"test:firefox:bidi"
]
}
},
"devDependencies": {
"@actions/core": "1.10.0",
"@commitlint/cli": "17.3.0",
@ -44,7 +121,7 @@
"@microsoft/api-extractor": "7.33.7",
"@microsoft/api-extractor-model": "7.25.3",
"@pptr/testserver": "file:packages/testserver",
"@rollup/plugin-commonjs": "24.0.0",
"@rollup/plugin-commonjs": "24.0.1",
"@rollup/plugin-node-resolve": "15.0.1",
"@types/debug": "4.1.7",
"@types/diff": "5.0.2",
@ -65,7 +142,6 @@
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"c8": "7.12.0",
"chromium-bidi": "0.4.3",
"commonmark": "0.30.0",
"cross-env": "7.0.3",
"diff": "5.1.0",
@ -95,8 +171,7 @@
"pngjs": "6.0.0",
"prettier": "2.8.1",
"puppeteer": "file:packages/puppeteer",
"rollup": "2.79.1",
"rollup-plugin-dts": "4.2.2",
"rollup": "3.12.1",
"semver": "7.3.8",
"sinon": "15.0.1",
"source-map-support": "0.5.21",

View file

@ -0,0 +1,6 @@
module.exports = {
logLevel: 'debug',
spec: 'test/build/**/*.spec.js',
exit: !!process.env.CI,
reporter: 'spec',
};

View file

@ -0,0 +1,3 @@
# @puppeteer/browsers
TODO

View file

@ -0,0 +1,87 @@
{
"name": "@puppeteer/browsers",
"version": "0.0.1",
"description": "Download and launch browsers",
"scripts": {
"build": "wireit",
"build:test": "wireit",
"clean": "tsc --build --clean && rimraf lib",
"test": "wireit"
},
"bin": {
"@puppeteer/browsers": "lib/cjs/browsers.js"
},
"wireit": {
"build": {
"command": "tsc -b",
"files": [
"src/**/*.ts",
"tsconfig.json"
],
"output": [
"lib/**"
]
},
"build:test": {
"command": "tsc -b test/src/tsconfig.json",
"files": [
"test/**/*.ts",
"test/src/tsconfig.json"
],
"output": [
"test/build/**"
],
"dependencies": [
"build"
]
},
"test": {
"command": "mocha",
"files": [
".mocharc.cjs"
],
"dependencies": [
"build:test"
]
}
},
"keywords": [
"puppeteer",
"browsers"
],
"repository": {
"type": "git",
"url": "https://github.com/puppeteer/puppeteer/tree/main/packages/browsers"
},
"author": "The Chromium Authors",
"license": "Apache-2.0",
"engines": {
"node": ">=14.1.0"
},
"files": [
"lib",
"!*.tsbuildinfo"
],
"dependencies": {
"debug": "4.3.4",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"yargs": "17.7.0"
},
"devDependencies": {
"@types/node": "^14.15.0",
"@types/yargs": "17.0.22"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}

View file

@ -0,0 +1,176 @@
/**
* 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 ProgressBar from 'progress';
import yargs from 'yargs';
import {hideBin} from 'yargs/helpers';
import {Browser, BrowserPlatform} from './browsers/types.js';
import {fetch} from './fetch.js';
import {computeExecutablePath, launch} from './launcher.js';
type InstallArgs = {
browser: {
name: Browser;
revision: string;
};
path?: string;
platform?: BrowserPlatform;
};
type LaunchArgs = {
browser: {
name: Browser;
revision: string;
};
path?: string;
platform?: BrowserPlatform;
detached: boolean;
};
export class CLI {
#cachePath;
constructor(cachePath = process.cwd()) {
this.#cachePath = cachePath;
}
async run(argv: string[]): Promise<void> {
await yargs(hideBin(argv))
.command(
'install <browser>',
'Download and install the specified browser',
yargs => {
yargs.positional('browser', {
description: 'The browser version',
type: 'string',
coerce: (opt): InstallArgs['browser'] => {
return {
name: this.#parseBrowser(opt),
revision: this.#parseRevision(opt),
};
},
});
},
async argv => {
const args = argv as unknown as InstallArgs;
await fetch({
browser: args.browser.name,
revision: args.browser.revision,
platform: args.platform,
cacheDir: args.path ?? this.#cachePath,
downloadProgressCallback: this.#makeProgressCallback(
args.browser.name,
args.browser.revision
),
});
}
)
.option('path', {
type: 'string',
desc: 'Path where the browsers will be downloaded to and installed from',
default: process.cwd(),
})
.option('platform', {
type: 'string',
desc: 'Platform that the binary needs to be compatible with.',
choices: Object.values(BrowserPlatform),
defaultDescription: 'Auto-detected by default.',
})
.command(
'launch <browser>',
'Launch the specified browser',
yargs => {
yargs.positional('browser', {
description: 'The browser version',
type: 'string',
coerce: (opt): LaunchArgs['browser'] => {
return {
name: this.#parseBrowser(opt),
revision: this.#parseRevision(opt),
};
},
});
},
async argv => {
const args = argv as unknown as LaunchArgs;
const executablePath = computeExecutablePath({
browser: args.browser.name,
revision: args.browser.revision,
cacheDir: args.path ?? this.#cachePath,
platform: args.platform,
});
launch({
executablePath,
detached: args.detached,
});
}
)
.option('path', {
type: 'string',
desc: 'Path where the browsers will be downloaded to and installed from',
default: process.cwd(),
})
.option('detached', {
type: 'boolean',
desc: 'Whether to detach the child process.',
default: false,
})
.option('platform', {
type: 'string',
desc: 'Platform that the binary needs to be compatible with.',
choices: Object.values(BrowserPlatform),
defaultDescription: 'Auto-detected by default.',
})
.parse();
}
#parseBrowser(version: string): Browser {
return version.split('@').shift() as Browser;
}
#parseRevision(version: string): string {
return version.split('@').pop() ?? 'latest';
}
#toMegabytes(bytes: number) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}
#makeProgressCallback(browser: Browser, revision: string) {
let progressBar: ProgressBar;
let lastDownloadedBytes = 0;
return (downloadedBytes: number, totalBytes: number) => {
if (!progressBar) {
progressBar = new ProgressBar(
`Downloading ${browser} r${revision} - ${this.#toMegabytes(
totalBytes
)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
};
}
}

View file

@ -0,0 +1,53 @@
/**
* 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 path from 'path';
import {Browser, BrowserPlatform} from './browsers/types.js';
/**
* The cache used by Puppeteer relies on the following structure:
*
* - rootDir
* -- <browser1> | browserRoot(browser1)
* ---- <platform>-<revision> | installationDir()
* ------ the browser-platform-revision
* ------ specific structure.
* -- <browser2> | browserRoot(browser2)
* ---- <platform>-<revision> | installationDir()
* ------ the browser-platform-revision
* ------ specific structure.
* @internal
*/
export class CacheStructure {
#rootDir: string;
constructor(rootDir: string) {
this.#rootDir = rootDir;
}
browserRoot(browser: Browser): string {
return path.join(this.#rootDir, browser);
}
installationDir(
browser: Browser,
platform: BrowserPlatform,
revision: string
): string {
return path.join(this.browserRoot(browser), `${platform}-${revision}`);
}
}

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node
/**
* 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,5 +16,6 @@
* limitations under the License.
*/
export * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js';
export * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {CLI} from './CLI.js';
new CLI().run(process.argv);

View file

@ -0,0 +1,31 @@
/**
* 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 chrome from './chrome.js';
import * as firefox from './firefox.js';
import {Browser, BrowserPlatform} from './types.js';
export const downloadUrls = {
[Browser.CHROME]: chrome.resolveDownloadUrl,
[Browser.FIREFOX]: firefox.resolveDownloadUrl,
};
export const executablePathByBrowser = {
[Browser.CHROME]: chrome.relativeExecutablePath,
[Browser.FIREFOX]: firefox.relativeExecutablePath,
};
export {Browser, BrowserPlatform};

View file

@ -0,0 +1,81 @@
/**
* 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 path from 'path';
import {BrowserPlatform} from './types.js';
function archive(platform: BrowserPlatform, revision: string): string {
switch (platform) {
case BrowserPlatform.LINUX:
return 'chrome-linux';
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return 'chrome-mac';
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
}
}
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX:
return 'Linux_x64';
case BrowserPlatform.MAC_ARM:
return 'Mac_Arm';
case BrowserPlatform.MAC:
return 'Mac';
case BrowserPlatform.WIN32:
return 'Win';
case BrowserPlatform.WIN64:
return 'Win_x64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
revision: string,
baseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots'
): string {
return `${baseUrl}/${folder(platform)}/${revision}/${archive(
platform,
revision
)}.zip`;
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_revision: string
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join(
'chrome-mac',
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
);
case BrowserPlatform.LINUX:
return path.join('chrome-linux', 'chrome');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('chrome-win', 'chrome.exe');
}
}

View file

@ -0,0 +1,56 @@
/**
* 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 path from 'path';
import {BrowserPlatform} from './types.js';
function archive(platform: BrowserPlatform, revision: string): string {
switch (platform) {
case BrowserPlatform.LINUX:
return `firefox-${revision}.en-US.${platform}-x86_64.tar.bz2`;
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return `firefox-${revision}.en-US.mac.dmg`;
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return `firefox-${revision}.en-US.${platform}.zip`;
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
revision: string,
baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central'
): string {
return `${baseUrl}/${archive(platform, revision)}`;
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_revision: string
): string {
switch (platform) {
case BrowserPlatform.MAC_ARM:
case BrowserPlatform.MAC:
return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox');
case BrowserPlatform.LINUX:
return path.join('firefox', 'firefox');
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join('firefox', 'firefox.exe');
}
}

View file

@ -0,0 +1,43 @@
/**
* 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 chrome from './chrome.js';
import * as firefox from './firefox.js';
/**
* Supported browsers.
*/
export enum Browser {
CHROME = 'chrome',
FIREFOX = 'firefox',
}
/**
* Platform names used to identify a OS platfrom x architecture combination in the way
* that is relevant for the browser download.
*/
export enum BrowserPlatform {
LINUX = 'linux',
MAC = 'mac',
MAC_ARM = 'mac_arm',
WIN32 = 'win32',
WIN64 = 'win64',
}
export const downloadUrls = {
[Browser.CHROME]: chrome.resolveDownloadUrl,
[Browser.FIREFOX]: firefox.resolveDownloadUrl,
};

View file

@ -0,0 +1,19 @@
/**
* 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 debug from 'debug';
export {debug};

View file

@ -0,0 +1,58 @@
/**
* 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 os from 'os';
import {BrowserPlatform} from './browsers/browsers.js';
export function detectBrowserPlatform(): BrowserPlatform | undefined {
const platform = os.platform();
switch (platform) {
case 'darwin':
return os.arch() === 'arm64'
? BrowserPlatform.MAC_ARM
: BrowserPlatform.MAC;
case 'linux':
return BrowserPlatform.LINUX;
case 'win32':
return os.arch() === 'x64' ||
// Windows 11 for ARM supports x64 emulation
(os.arch() === 'arm64' && isWindows11(os.release()))
? BrowserPlatform.WIN64
: BrowserPlatform.WIN32;
default:
return undefined;
}
}
/**
* Windows 11 is identified by the version 10.0.22000 or greater
* @internal
*/
function isWindows11(version: string): boolean {
const parts = version.split('.');
if (parts.length > 2) {
const major = parseInt(parts[0] as string, 10);
const minor = parseInt(parts[1] as string, 10);
const patch = parseInt(parts[2] as string, 10);
return (
major > 10 ||
(major === 10 && minor > 0) ||
(major === 10 && minor === 0 && patch >= 22000)
);
}
return false;
}

View file

@ -0,0 +1,140 @@
/**
* 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 assert from 'assert';
import {existsSync} from 'fs';
import {mkdir, unlink} from 'fs/promises';
import os from 'os';
import path from 'path';
import {Browser, BrowserPlatform, downloadUrls} from './browsers/browsers.js';
import {CacheStructure} from './CacheStructure.js';
import {debug} from './debug.js';
import {detectBrowserPlatform} from './detectPlatform.js';
import {unpackArchive} from './fileUtil.js';
import {downloadFile, headHttpRequest} from './httpUtil.js';
const debugFetch = debug('puppeteer:browsers:fetcher');
/**
* @public
*/
export interface Options {
/**
* Determines the path to download browsers to.
*/
cacheDir: string;
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue Auto-detected.
*/
platform?: BrowserPlatform;
/**
* Determines which browser to fetch.
*/
browser: Browser;
/**
* Determines which revision to dowloand. Revision should uniquely identify
* binaries and they are used for caching.
*/
revision: string;
/**
* Provides information about the progress of the download.
*/
downloadProgressCallback?: (
downloadedBytes: number,
totalBytes: number
) => void;
}
export type InstalledBrowser = {
path: string;
browser: Browser;
revision: string;
platform: BrowserPlatform;
};
export async function fetch(options: Options): Promise<InstalledBrowser> {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
);
}
const url = getDownloadUrl(
options.browser,
options.platform,
options.revision
);
const fileName = url.toString().split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const structure = new CacheStructure(options.cacheDir);
const browserRoot = structure.browserRoot(options.browser);
const archivePath = path.join(browserRoot, fileName);
if (!existsSync(browserRoot)) {
await mkdir(browserRoot, {recursive: true});
}
const outputPath = structure.installationDir(
options.browser,
options.platform,
options.revision
);
if (existsSync(outputPath)) {
return {
path: outputPath,
browser: options.browser,
platform: options.platform,
revision: options.revision,
};
}
try {
debugFetch(`Downloading binary from ${url}`);
await downloadFile(url, archivePath, options.downloadProgressCallback);
debugFetch(`Installing ${archivePath} to ${outputPath}`);
await unpackArchive(archivePath, outputPath);
} finally {
if (existsSync(archivePath)) {
await unlink(archivePath);
}
}
return {
path: outputPath,
browser: options.browser,
platform: options.platform,
revision: options.revision,
};
}
export async function canFetch(options: Options): Promise<boolean> {
options.platform ??= detectBrowserPlatform();
if (!options.platform) {
throw new Error(
`Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`
);
}
return await headHttpRequest(
getDownloadUrl(options.browser, options.platform, options.revision)
);
}
function getDownloadUrl(
browser: Browser,
platform: BrowserPlatform,
revision: string
): URL {
return new URL(downloadUrls[browser](platform, revision));
}

View file

@ -0,0 +1,89 @@
/**
* 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 {exec as execChildProcess} from 'child_process';
import {createReadStream} from 'fs';
import {mkdir, readdir} from 'fs/promises';
import * as path from 'path';
import {promisify} from 'util';
import extractZip from 'extract-zip';
import tar from 'tar-fs';
import bzip from 'unbzip2-stream';
const exec = promisify(execChildProcess);
/**
* @internal
*/
export async function unpackArchive(
archivePath: string,
folderPath: string
): Promise<void> {
if (archivePath.endsWith('.zip')) {
await extractZip(archivePath, {dir: folderPath});
} else if (archivePath.endsWith('.tar.bz2')) {
await extractTar(archivePath, folderPath);
} else if (archivePath.endsWith('.dmg')) {
await mkdir(folderPath);
await installDMG(archivePath, folderPath);
} else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}
}
/**
* @internal
*/
function extractTar(tarPath: string, folderPath: string): Promise<void> {
return new Promise((fulfill, reject) => {
const tarStream = tar.extract(folderPath);
tarStream.on('error', reject);
tarStream.on('finish', fulfill);
const readStream = createReadStream(tarPath);
readStream.pipe(bzip()).pipe(tarStream);
});
}
/**
* @internal
*/
async function installDMG(dmgPath: string, folderPath: string): Promise<void> {
const {stdout} = await exec(
`hdiutil attach -nobrowse -noautoopen "${dmgPath}"`
);
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes) {
throw new Error(`Could not find volume path in ${stdout}`);
}
const mountPath = volumes[0]!;
try {
const fileNames = await readdir(mountPath);
const appName = fileNames.find(item => {
return typeof item === 'string' && item.endsWith('.app');
});
if (!appName) {
throw new Error(`Cannot find app in ${mountPath}`);
}
const mountedPath = path.join(mountPath!, appName);
await exec(`cp -R "${mountedPath}" "${folderPath}"`);
} finally {
await exec(`hdiutil detach "${mountPath}" -quiet`);
}
}

View file

@ -0,0 +1,139 @@
/**
* 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 {createWriteStream} from 'fs';
import * as http from 'http';
import * as https from 'https';
import {URL} from 'url';
import createHttpsProxyAgent from 'https-proxy-agent';
import {getProxyForUrl} from 'proxy-from-env';
export function headHttpRequest(url: URL): Promise<boolean> {
return new Promise(resolve => {
const request = httpRequest(
url,
'HEAD',
response => {
resolve(response.statusCode === 200);
},
false
);
request.on('error', () => {
resolve(false);
});
});
}
export function httpRequest(
url: URL,
method: string,
response: (x: http.IncomingMessage) => void,
keepAlive = true
): http.ClientRequest {
const options: http.RequestOptions = {
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
path: url.pathname,
method,
headers: keepAlive ? {Connection: 'keep-alive'} : undefined,
};
const proxyURL = getProxyForUrl(url.toString());
if (proxyURL) {
const proxy = new URL(proxyURL);
if (proxy.protocol === 'http:') {
options.path = url.href;
options.hostname = proxy.hostname;
options.protocol = proxy.protocol;
options.port = proxy.port;
} else {
options.agent = createHttpsProxyAgent({
host: proxy.host,
path: proxy.pathname,
port: proxy.port,
secureProxy: proxy.protocol === 'https:',
headers: options.headers,
});
}
}
const requestCallback = (res: http.IncomingMessage): void => {
if (
res.statusCode &&
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
) {
httpRequest(new URL(res.headers.location), method, response);
} else {
response(res);
}
};
const request =
options.protocol === 'https:'
? https.request(options, requestCallback)
: http.request(options, requestCallback);
request.end();
return request;
}
/**
* @internal
*/
export function downloadFile(
url: URL,
destinationPath: string,
progressCallback?: (downloadedBytes: number, totalBytes: number) => void
): Promise<void> {
return new Promise<void>((resolve, reject) => {
let downloadedBytes = 0;
let totalBytes = 0;
function onData(chunk: string): void {
downloadedBytes += chunk.length;
progressCallback!(downloadedBytes, totalBytes);
}
const request = httpRequest(url, 'GET', response => {
if (response.statusCode !== 200) {
const error = new Error(
`Download failed: server returned code ${response.statusCode}. URL: ${url}`
);
// consume response data to free up memory
response.resume();
reject(error);
return;
}
const file = createWriteStream(destinationPath);
file.on('finish', () => {
return resolve();
});
file.on('error', error => {
return reject(error);
});
response.pipe(file);
totalBytes = parseInt(response.headers['content-length']!, 10);
if (progressCallback) {
response.on('data', onData);
}
});
request.on('error', error => {
return reject(error);
});
});
}

View file

@ -0,0 +1,295 @@
/**
* 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 childProcess from 'child_process';
import os from 'os';
import path from 'path';
import {
Browser,
BrowserPlatform,
executablePathByBrowser,
} from './browsers/browsers.js';
import {CacheStructure} from './CacheStructure.js';
import {debug} from './debug.js';
import {detectBrowserPlatform} from './detectPlatform.js';
const debugLaunch = debug('puppeteer:browsers:launcher');
/**
* @public
*/
export interface Options {
/**
* Root path to the storage directory.
*/
cacheDir: string;
/**
* Determines which platform the browser will be suited for.
*
* @defaultValue Auto-detected.
*/
platform?: BrowserPlatform;
/**
* Determines which browser to fetch.
*/
browser: Browser;
/**
* Determines which revision to dowloand. Revision should uniquely identify
* binaries and they are used for caching.
*/
revision: string;
}
export function computeExecutablePath(options: Options): 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 CacheStructure(options.cacheDir).installationDir(
options.browser,
options.platform,
options.revision
);
return path.join(
installationDir,
executablePathByBrowser[options.browser](options.platform, options.revision)
);
}
type LaunchOptions = {
executablePath: string;
pipe?: boolean;
dumpio?: boolean;
args?: string[];
env?: Record<string, string>;
handleSIGINT?: boolean;
handleSIGTERM?: boolean;
handleSIGHUP?: boolean;
detached?: boolean;
};
export function launch(opts: LaunchOptions): Process {
return new Process(opts);
}
class Process {
#executablePath;
#args: string[];
#browserProcess: childProcess.ChildProcess;
#exited = false;
#browserProcessExiting: Promise<void>;
constructor(opts: LaunchOptions) {
this.#executablePath = opts.executablePath;
this.#args = opts.args ?? [];
opts.pipe ??= false;
opts.dumpio ??= false;
opts.handleSIGINT ??= true;
opts.handleSIGTERM ??= true;
opts.handleSIGHUP ??= true;
opts.detached ??= true;
const stdio = this.#configureStdio({
pipe: opts.pipe,
dumpio: opts.dumpio,
});
debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`);
this.#browserProcess = childProcess.spawn(
this.#executablePath,
this.#args,
{
// On non-windows platforms, `detached: true` makes child process a
// leader of a new process group, making it possible to kill child
// process tree with `.kill(-pid)` command. @see
// https://nodejs.org/api/child_process.html#child_process_options_detached
detached: opts.detached,
env: opts.env,
stdio,
}
);
if (opts.dumpio) {
this.#browserProcess.stderr?.pipe(process.stderr);
this.#browserProcess.stdout?.pipe(process.stdout);
}
process.on('exit', this.#onDriverProcessExit);
if (opts.handleSIGINT) {
process.on('SIGINT', this.#onDriverProcessSignal);
}
if (opts.handleSIGTERM) {
process.on('SIGTERM', this.#onDriverProcessSignal);
}
if (opts.handleSIGHUP) {
process.on('SIGHUP', this.#onDriverProcessSignal);
}
this.#browserProcessExiting = new Promise(resolve => {
this.#browserProcess.once('exit', () => {
this.#exited = true;
this.#clearListeners();
resolve();
});
});
}
#configureStdio(opts: {
pipe: boolean;
dumpio: boolean;
}): Array<'ignore' | 'pipe'> {
if (opts.pipe) {
if (opts.dumpio) {
return ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
} else {
return ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
}
} else {
if (opts.dumpio) {
return ['pipe', 'pipe', 'pipe'];
} else {
return ['pipe', 'ignore', 'pipe'];
}
}
}
#clearListeners(): void {
process.off('exit', this.#onDriverProcessExit);
process.off('SIGINT', this.#onDriverProcessSignal);
process.off('SIGTERM', this.#onDriverProcessSignal);
process.off('SIGHUP', this.#onDriverProcessSignal);
}
#onDriverProcessExit = (_code: number) => {
this.kill();
};
#onDriverProcessSignal = (signal: string): void => {
switch (signal) {
case 'SIGINT':
this.kill();
process.exit(130);
case 'SIGTERM':
case 'SIGUP':
this.kill();
break;
}
};
close(): Promise<void> {
if (this.#exited) {
return Promise.resolve();
}
this.kill();
return this.#browserProcessExiting;
}
kill(): void {
// If the process failed to launch (for example if the browser executable path
// is invalid), then the process does not get a pid assigned. A call to
// `proc.kill` would error, as the `pid` to-be-killed can not be found.
if (
this.#browserProcess &&
this.#browserProcess.pid &&
pidExists(this.#browserProcess.pid)
) {
try {
if (process.platform === 'win32') {
childProcess.exec(
`taskkill /pid ${this.#browserProcess.pid} /T /F`,
error => {
if (error) {
// taskkill can fail to kill the process e.g. due to missing permissions.
// Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies.
this.#browserProcess.kill();
}
}
);
} else {
// on linux the process group can be killed with the group id prefixed with
// a minus sign. The process group id is the group leader's pid.
const processGroupId = -this.#browserProcess.pid;
try {
process.kill(processGroupId, 'SIGKILL');
} catch (error) {
// Killing the process group can fail due e.g. to missing permissions.
// Let's kill the process via Node API. This delays killing of all child
// processes of `this.proc` until the main Node.js process dies.
this.#browserProcess.kill('SIGKILL');
}
}
} catch (error) {
throw new Error(
`${PROCESS_ERROR_EXPLANATION}\nError cause: ${
isErrorLike(error) ? error.stack : error
}`
);
}
}
this.#clearListeners();
}
}
const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary.
This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser.
Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed.
If you think this is a bug, please report it on the Puppeteer issue tracker.`;
/**
* @internal
*/
function pidExists(pid: number): boolean {
try {
return process.kill(pid, 0);
} catch (error) {
if (isErrnoException(error)) {
if (error.code && error.code === 'ESRCH') {
return false;
}
}
throw error;
}
}
/**
* @internal
*/
export interface ErrorLike extends Error {
name: string;
message: string;
}
/**
* @internal
*/
export function isErrorLike(obj: unknown): obj is ErrorLike {
return (
typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj
);
}
/**
* @internal
*/
export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
return (
isErrorLike(obj) &&
('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
);
}

View file

@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "../lib/cjs"
}
}

View file

@ -0,0 +1,6 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib/esm"
}
}

View file

@ -0,0 +1,72 @@
/**
* 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 assert from 'assert';
import path from 'path';
import {BrowserPlatform} from '../../lib/cjs/browsers/browsers.js';
import {
resolveDownloadUrl,
relativeExecutablePath,
} from '../../lib/cjs/browsers/chrome.js';
describe('Chrome', () => {
it('should resolve download URLs', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, '1083080'),
'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, '1083080'),
'https://storage.googleapis.com/chromium-browser-snapshots/Mac/1083080/chrome-mac.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, '1083080'),
'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1083080/chrome-mac.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, '1083080'),
'https://storage.googleapis.com/chromium-browser-snapshots/Win/1083080/chrome-win.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, '1083080'),
'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/1083080/chrome-win.zip'
);
});
it('should resolve executable paths', () => {
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
path.join('chrome-linux', 'chrome')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
path.join('chrome-win', 'chrome.exe')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
path.join('chrome-win', 'chrome.exe')
);
});
});

View file

@ -0,0 +1,75 @@
/**
* 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 assert from 'assert';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {CLI} from '../../lib/cjs/CLI.js';
describe('CLI', function () {
this.timeout(60000);
let tmpDir = '/tmp/puppeteer-browsers-test';
const testChromeRevision = '1083080';
const testFirefoxRevision = '111.0a1';
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true});
});
it('should download Chromium binaries', async () => {
await new CLI(tmpDir).run([
'npx',
'@puppeteer/browsers',
'install',
`chrome@${testChromeRevision}`,
`--path=${tmpDir}`,
'--platform=linux',
]);
assert.ok(
fs.existsSync(
path.join(
tmpDir,
'chrome',
`linux-${testChromeRevision}`,
'chrome-linux'
)
)
);
});
it('should download Firefox binaries', async () => {
await new CLI(tmpDir).run([
'npx',
'@puppeteer/browsers',
'install',
`firefox@${testFirefoxRevision}`,
`--path=${tmpDir}`,
'--platform=linux',
]);
assert.ok(
fs.existsSync(
path.join(tmpDir, 'firefox', `linux-${testFirefoxRevision}`, 'firefox')
)
);
});
});

View file

@ -0,0 +1,228 @@
/**
* 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 assert from 'assert';
import fs from 'fs';
import http from 'http';
import https from 'https';
import os from 'os';
import path from 'path';
import {Browser, BrowserPlatform} from '../../lib/cjs/browsers/browsers.js';
import {fetch, canFetch} from '../../lib/cjs/fetch.js';
/**
* Tests in this spec use real download URLs and unpack live browser archives
* so it requires the network access.
*/
describe('fetch', () => {
let tmpDir = '/tmp/puppeteer-browsers-test';
const testChromeRevision = '1083080';
const testFirefoxRevision = '111.0a1';
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true});
});
it('should check if a revision can be downloaded', async () => {
assert.ok(
await canFetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: testChromeRevision,
})
);
});
it('should report if a revision is not downloadable', async () => {
assert.strictEqual(
await canFetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: 'unknown',
}),
false
);
});
it('should download a revision that is a zip archive', async function () {
this.timeout(60000);
const expectedOutputPath = path.join(
tmpDir,
'chrome',
`${BrowserPlatform.LINUX}-${testChromeRevision}`
);
assert.strictEqual(fs.existsSync(expectedOutputPath), false);
let browser = await fetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: testChromeRevision,
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
// Second iteration should be no-op.
browser = await fetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: testChromeRevision,
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
});
it('should download a revision that is a bzip2 archive', async function () {
this.timeout(60000);
const expectedOutputPath = path.join(
tmpDir,
'firefox',
`${BrowserPlatform.LINUX}-${testFirefoxRevision}`
);
assert.strictEqual(fs.existsSync(expectedOutputPath), false);
const browser = await fetch({
cacheDir: tmpDir,
browser: Browser.FIREFOX,
platform: BrowserPlatform.LINUX,
revision: testFirefoxRevision,
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
});
// Fetch relies on the `hdiutil` utility on MacOS.
// The utility is not available on other platforms.
(os.platform() === 'darwin' ? it : it.skip)(
'should download a revision that is a dmg archive',
async function () {
this.timeout(120000);
const expectedOutputPath = path.join(
tmpDir,
'firefox',
`${BrowserPlatform.MAC}-${testFirefoxRevision}`
);
assert.strictEqual(fs.existsSync(expectedOutputPath), false);
const browser = await fetch({
cacheDir: tmpDir,
browser: Browser.FIREFOX,
platform: BrowserPlatform.MAC,
revision: testFirefoxRevision,
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
}
);
describe('with proxy', () => {
const proxyUrl = new URL(`http://localhost:54321`);
let proxyServer: http.Server;
let proxiedRequestUrls: string[] = [];
beforeEach(() => {
proxiedRequestUrls = [];
proxyServer = http
.createServer(
(
originalRequest: http.IncomingMessage,
originalResponse: http.ServerResponse
) => {
const url = originalRequest.url as string;
const proxyRequest = (
url.startsWith('http:') ? http : https
).request(
url,
{
method: originalRequest.method,
rejectUnauthorized: false,
},
proxyResponse => {
originalResponse.writeHead(
proxyResponse.statusCode as number,
proxyResponse.headers
);
proxyResponse.pipe(originalResponse, {end: true});
}
);
originalRequest.pipe(proxyRequest, {end: true});
proxiedRequestUrls.push(url);
}
)
.listen({
port: proxyUrl.port,
hostname: proxyUrl.hostname,
});
process.env['HTTPS_PROXY'] = proxyUrl.toString();
process.env['HTTP_PROXY'] = proxyUrl.toString();
});
afterEach(async () => {
await new Promise((resolve, reject) => {
proxyServer.close(error => {
if (error) {
reject(error);
} else {
resolve(undefined);
}
});
});
delete process.env['HTTP_PROXY'];
delete process.env['HTTPS_PROXY'];
});
it('can send canFetch requests via a proxy', async () => {
assert.strictEqual(
await canFetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: testChromeRevision,
}),
true
);
assert.deepStrictEqual(proxiedRequestUrls, [
'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip',
]);
});
it('can fetch via a proxy', async function () {
this.timeout(60000);
const expectedOutputPath = path.join(
tmpDir,
'chrome',
`${BrowserPlatform.LINUX}-${testChromeRevision}`
);
assert.strictEqual(fs.existsSync(expectedOutputPath), false);
const browser = await fetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: testChromeRevision,
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
assert.deepStrictEqual(proxiedRequestUrls, [
'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip',
]);
});
});
});

View file

@ -0,0 +1,72 @@
/**
* 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 assert from 'assert';
import path from 'path';
import {BrowserPlatform} from '../../lib/cjs/browsers/browsers.js';
import {
relativeExecutablePath,
resolveDownloadUrl,
} from '../../lib/cjs/browsers/firefox.js';
describe('Firefox', () => {
it('should resolve download URLs', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win32.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, '111.0a1'),
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win64.zip'
);
});
it('should resolve executable paths', () => {
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'),
path.join('firefox', 'firefox')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC, '111.0a1'),
path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC_ARM, '111.0a1'),
path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN32, '111.0a1'),
path.join('firefox', 'firefox.exe')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN64, '111.0a1'),
path.join('firefox', 'firefox.exe')
);
});
});

View file

@ -0,0 +1,124 @@
/**
* 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 assert from 'assert';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {Browser, BrowserPlatform} from '../../lib/cjs/browsers/browsers.js';
import {fetch} from '../../lib/cjs/fetch.js';
import {computeExecutablePath, launch} from '../../lib/cjs/launcher.js';
describe('launcher', () => {
it('should compute executable path for Chrome', () => {
assert.strictEqual(
computeExecutablePath({
browser: Browser.CHROME,
platform: BrowserPlatform.LINUX,
revision: '123',
cacheDir: 'cache',
}),
path.join('cache', 'chrome', 'linux-123', 'chrome-linux', 'chrome')
);
});
it('should compute executable path for Firefox', () => {
assert.strictEqual(
computeExecutablePath({
browser: Browser.FIREFOX,
platform: BrowserPlatform.LINUX,
revision: '123',
cacheDir: 'cache',
}),
path.join('cache', 'firefox', 'linux-123', 'firefox', 'firefox')
);
});
describe('Chrome', function () {
this.timeout(60000);
let tmpDir = '/tmp/puppeteer-browsers-test';
const testChromeRevision = '1083080';
beforeEach(async () => {
tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'puppeteer-browsers-test')
);
await fetch({
cacheDir: tmpDir,
browser: Browser.CHROME,
revision: testChromeRevision,
});
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true});
});
it('should launch a Chrome browser', async () => {
const executablePath = computeExecutablePath({
cacheDir: tmpDir,
browser: Browser.CHROME,
revision: testChromeRevision,
});
const process = launch({
executablePath,
args: [
'--use-mock-keychain',
'--disable-features=DialMediaRouteProvider',
`--user-data-dir=${path.join(tmpDir, 'profile')}`,
],
});
await process.close();
});
});
describe('Firefox', function () {
this.timeout(60000);
let tmpDir = '/tmp/puppeteer-browsers-test';
const testFirefoxRevision = '111.0a1';
beforeEach(async () => {
tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'puppeteer-browsers-test')
);
await fetch({
cacheDir: tmpDir,
browser: Browser.FIREFOX,
revision: testFirefoxRevision,
});
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true});
});
it('should launch a Firefox browser', async () => {
const executablePath = computeExecutablePath({
cacheDir: tmpDir,
browser: Browser.FIREFOX,
revision: testFirefoxRevision,
});
const process = launch({
executablePath,
args: [`--user-data-dir=${path.join(tmpDir, 'profile')}`],
});
await process.close();
});
});
});

View file

@ -0,0 +1,8 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "../build"
},
"references": [{"path": "../../tsconfig.json"}]
}

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"references": [
{"path": "src/tsconfig.esm.json"},
{"path": "src/tsconfig.cjs.json"}
]
}

View file

@ -3,18 +3,16 @@
"version": "0.1.0",
"description": "Puppeteer Angular schematics",
"scripts": {
"dev": "npm run build --watch",
"dev:test": "npm run test --watch",
"copy": "wireit",
"build:tsc": "wireit",
"build": "wireit",
"clean": "tsc --build --clean && rimraf lib",
"clean:test": "rimraf test/build",
"clean": "tsc -b --clean && rimraf lib && rimraf test/build",
"dev:test": "npm run test --watch",
"dev": "npm run build --watch",
"test": "wireit"
},
"wireit": {
"copy": {
"clean": "if-file-deleted",
"command": "node copySchemaFiles.js",
"build": {
"command": "node tools/copySchemaFiles.js",
"files": [
"src/**/files/**",
"src/**/*.json"
@ -24,29 +22,25 @@
"lib/**/*.json"
],
"dependencies": [
"clean"
"build:tsc"
]
},
"build": {
"build:tsc": {
"command": "tsc -b",
"clean": "if-file-deleted",
"files": [
"src/**/*.ts",
"!src/**/files",
"!src/**/*.json"
"**/tsconfig.*.json",
"**/tsconfig.json",
"src/**/*.ts"
],
"output": [
"lib/**",
"!lib/**/files",
"!lib/**/*.json"
],
"dependencies": [
"copy"
"lib/**/*.{ts,js}",
"lib/**/*.{ts,js}.map"
]
},
"test": {
"command": "mocha",
"dependencies": [
"clean:test",
"build"
]
}
@ -66,9 +60,9 @@
"node": ">=14.1.0"
},
"dependencies": {
"@angular-devkit/architect": "^0.1501.2",
"@angular-devkit/core": "^15.1.2",
"@angular-devkit/schematics": "^15.1.2"
"@angular-devkit/architect": "^0.1501.6",
"@angular-devkit/core": "^15.1.6",
"@angular-devkit/schematics": "^15.1.6"
},
"devDependencies": {
"@types/node": "^14.15.0",

View file

@ -1,3 +1,5 @@
import {spawn} from 'child_process';
import {
createBuilder,
BuilderContext,
@ -6,7 +8,6 @@ import {
BuilderRun,
} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {spawn} from 'child_process';
import {PuppeteerBuilderOptions} from './types.js';

View file

@ -16,14 +16,15 @@
import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {concatMap, map, scan} from 'rxjs/operators';
import {of} from 'rxjs';
import {concatMap, map, scan} from 'rxjs/operators';
import {
addBaseFiles,
addFrameworkFiles,
getNgCommandName,
} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js';
import {
addPackageJsonDependencies,
addPackageJsonScripts,
@ -33,9 +34,7 @@ import {
type NodePackage,
updateAngularJsonScripts,
} from '../utils/packages.js';
import {type SchematicsOptions} from '../utils/types.js';
import {getAngularConfig} from '../utils/json.js';
// You don't have to export the function as default. You can also have more than one rule
// factory per file.

View file

@ -14,6 +14,8 @@
* limitations under the License.
*/
import {relative, resolve} from 'path';
import {getSystemPath, normalize, strings} from '@angular-devkit/core';
import {
SchematicContext,
@ -26,7 +28,7 @@ import {
move,
url,
} from '@angular-devkit/schematics';
import {relative, resolve} from 'path';
import {SchematicsOptions, TestingFramework} from './types.js';
export interface FilesOptions {

View file

@ -14,15 +14,17 @@
* limitations under the License.
*/
import {Tree} from '@angular-devkit/schematics';
import {get} from 'https';
import {SchematicsOptions, TestingFramework} from './types.js';
import {Tree} from '@angular-devkit/schematics';
import {getNgCommandName, getScriptFromOptions} from './files.js';
import {
getAngularConfig,
getJsonFileAsObject,
getObjectAsJson,
} from './json.js';
import {getNgCommandName, getScriptFromOptions} from './files.js';
import {SchematicsOptions, TestingFramework} from './types.js';
export interface NodePackage {
name: string;
version: string;

View file

@ -1,12 +1,13 @@
import expect from 'expect';
import sinon from 'sinon';
import https from 'https';
import {join} from 'path';
import {JsonObject} from '@angular-devkit/core';
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing/schematic-test-runner';
import {JsonObject} from '@angular-devkit/core';
import expect from 'expect';
import sinon from 'sinon';
const WORKSPACE_OPTIONS = {
name: 'workspace',

View file

@ -15,7 +15,9 @@
*/
const fs = require('fs/promises');
const {join} = require('path');
const path = require('path');
/**
*
* @param {String} directory
@ -42,8 +44,8 @@ async function findSchemaFiles(directory, files = []) {
}
async function copySchemaFiles() {
const srcDir = './src';
const outputDir = './lib';
const srcDir = join(__dirname, '..', 'src');
const outputDir = join(__dirname, '..', 'lib');
const files = await findSchemaFiles(srcDir);
const moves = files.map(file => {

View file

@ -2,6 +2,59 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [19.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.1...puppeteer-core-v19.7.2) (2023-02-20)
### Bug Fixes
* bump chromium-bidi to a version that does not declare mitt as a peer dependency ([#9701](https://github.com/puppeteer/puppeteer/issues/9701)) ([82916c1](https://github.com/puppeteer/puppeteer/commit/82916c102b2c399093ba9019e272207b5ce81849))
## [19.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.0...puppeteer-core-v19.7.1) (2023-02-15)
### Bug Fixes
* fix circularity on JSHandle interface ([#9661](https://github.com/puppeteer/puppeteer/issues/9661)) ([eb13863](https://github.com/puppeteer/puppeteer/commit/eb138635d661d3cdaf2940959fece5aca482178a))
* make chromium-bidi an opt peer dep ([#9667](https://github.com/puppeteer/puppeteer/issues/9667)) ([c6054ac](https://github.com/puppeteer/puppeteer/commit/c6054ac1a56c08ee7bf01321878699b7b4ab4e0b))
## [19.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.3...puppeteer-core-v19.7.0) (2023-02-13)
### Features
* add touchstart, touchmove and touchend methods ([#9622](https://github.com/puppeteer/puppeteer/issues/9622)) ([c8bb11a](https://github.com/puppeteer/puppeteer/commit/c8bb11adfcf1537032730a91baa3c36a6e324926))
* **chromium:** roll to Chromium 111.0.5556.0 (r1095492) ([#9656](https://github.com/puppeteer/puppeteer/issues/9656)) ([df59d01](https://github.com/puppeteer/puppeteer/commit/df59d010c20644da06eb4c4e28a11c4eea164aba))
### Bug Fixes
* `page.goto` error throwing on 40x/50x responses with an empty body ([#9523](https://github.com/puppeteer/puppeteer/issues/9523)) ([#9577](https://github.com/puppeteer/puppeteer/issues/9577)) ([ddb0cc1](https://github.com/puppeteer/puppeteer/commit/ddb0cc174d2a14c0948dcdaf9bae78620937c667))
## [19.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.2...puppeteer-core-v19.6.3) (2023-02-01)
### Bug Fixes
* ignore not found contexts for console messages ([#9595](https://github.com/puppeteer/puppeteer/issues/9595)) ([390685b](https://github.com/puppeteer/puppeteer/commit/390685bbe52c22b686fc0e3119b4ac7b1073c581))
* restore WaitTask terminate condition ([#9612](https://github.com/puppeteer/puppeteer/issues/9612)) ([e16cbc6](https://github.com/puppeteer/puppeteer/commit/e16cbc6626cffd40d0caa30801620e7293455006))
## [19.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.1...puppeteer-core-v19.6.2) (2023-01-27)
### Bug Fixes
* atomically get Puppeteer utilities ([#9597](https://github.com/puppeteer/puppeteer/issues/9597)) ([050a7b0](https://github.com/puppeteer/puppeteer/commit/050a7b062415ebaf10bcb71c405143eacc4e5d4b))
## [19.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.0...puppeteer-core-v19.6.1) (2023-01-26)
### Bug Fixes
* don't clean up previous browser versions ([#9568](https://github.com/puppeteer/puppeteer/issues/9568)) ([344bc2a](https://github.com/puppeteer/puppeteer/commit/344bc2af62e4068fe2cb8162d4b6c8242aac843b)), closes [#9533](https://github.com/puppeteer/puppeteer/issues/9533)
* mimic rejection for PuppeteerUtil on early call ([#9589](https://github.com/puppeteer/puppeteer/issues/9589)) ([1980de9](https://github.com/puppeteer/puppeteer/commit/1980de91a161523c7098a79919b20e6d8d2e5d81))
* **revert:** use LazyArg for puppeteer utilities ([#9590](https://github.com/puppeteer/puppeteer/issues/9590)) ([6edd996](https://github.com/puppeteer/puppeteer/commit/6edd99676827de2c83f7a858e4f903b1c34e7d35))
* use LazyArg for puppeteer utilities ([#9575](https://github.com/puppeteer/puppeteer/issues/9575)) ([496658f](https://github.com/puppeteer/puppeteer/commit/496658f02945b53096483f36cb3d64556cff045e))
## [19.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.2...puppeteer-core-v19.6.0) (2023-01-23)

View file

@ -0,0 +1,15 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer-core.d.ts",
"extends": "./api-extractor.json",
"dtsRollup": {
"enabled": false
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json"
}
}

View file

@ -8,8 +8,7 @@
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json"
"enabled": false
},
"dtsRollup": {

View file

@ -1,6 +1,6 @@
{
"name": "puppeteer-core",
"version": "19.6.0",
"version": "19.7.2",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [
"puppeteer",
@ -34,17 +34,15 @@
"node": ">=14.1.0"
},
"scripts": {
"build:third_party": "wireit",
"build:docs": "wireit",
"build:tsc": "wireit",
"build:types": "wireit",
"build": "wireit",
"check": "tsx tools/ensure-correct-devtools-protocol-package",
"format:types": "wireit",
"clean": "tsc -b --clean && rimraf lib src/generated",
"generate:package-json": "wireit",
"generate:sources": "wireit",
"prepack": "wireit",
"clean": "tsc -b --clean && rimraf lib src/generated",
"clean:third_party": "wireit"
"prepack": "wireit"
},
"wireit": {
"prepack": {
@ -58,93 +56,71 @@
},
"build": {
"dependencies": [
"build:third_party",
"format:types",
"generate:package-json"
"build:tsc",
"build:types"
]
},
"generate:sources": {
"command": "tsx tools/generate_sources.ts",
"clean": "if-file-deleted",
"files": [
"tools/generate_sources.ts",
"src/templates/**"
"../../versions.js",
"src/{injected,templates}/**",
"tools/generate_sources.ts"
],
"output": [
"src/generated/**"
]
},
"clean:third_party": {
"command": "rimraf lib/esm/third_party lib/cjs/third_party"
},
"build:third_party": {
"command": "rollup --config rollup.third_party.config.js",
"dependencies": [
"build:tsc"
],
"clean": false,
"files": [
"lib/esm/third_party/**",
"lib/cjs/third_party/**"
],
"output": [
"lib/esm/third_party/**",
"lib/cjs/third_party/**"
"src/generated/*.ts"
]
},
"generate:package-json": {
"command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json",
"clean": "if-file-deleted",
"dependencies": [
"build:tsc"
"files": [
"../../tools/generate_module_package_json.ts"
],
"output": [
"lib/esm/package.json"
]
},
"build:types": {
"command": "api-extractor run --local",
"build:docs": {
"command": "api-extractor run --local --config \"./api-extractor.docs.json\"",
"files": [
"api-extractor.docs.json",
"lib/esm/puppeteer/puppeteer-core.d.ts",
"tsconfig.json"
],
"dependencies": [
"build:tsc"
],
"files": [
"tsconfig.json",
"api-extractor.json",
"lib/esm/puppeteer/types.d.ts"
],
"output": [
"lib/types.d.ts"
]
},
"format:types": {
"command": "eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts",
"dependencies": [
"build:types"
],
"clean": false,
"files": [
"lib/types.d.ts",
"../../.eslintrc.types.cjs"
],
"output": [
"lib/types.d.ts"
]
},
"build:tsc": {
"command": "tsc -b",
"command": "tsc -b && rollup --config rollup.third_party.config.mjs",
"clean": "if-file-deleted",
"dependencies": [
"clean:third_party",
"generate:package-json",
"generate:sources"
],
"files": [
"src/**",
"compat/**",
"third_party/**",
"**/tsconfig.*.json"
"{compat,src,third_party}/**",
"rollup.third_party.config.mjs"
],
"output": [
"lib/esm/**",
"lib/cjs/**"
"lib/{cjs,esm}/**",
"!lib/esm/package.json"
]
},
"build:types": {
"command": "api-extractor run --local && eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts",
"files": [
"../../.eslintrc.types.cjs",
"api-extractor.json",
"lib/esm/puppeteer/types.d.ts",
"tsconfig.json"
],
"output": [
"lib/types.d.ts"
],
"dependencies": [
"build:tsc"
]
}
},
@ -155,9 +131,10 @@
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"chromium-bidi": "0.4.4",
"cross-fetch": "3.1.5",
"debug": "4.3.4",
"devtools-protocol": "0.0.1082910",
"devtools-protocol": "0.0.1094867",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"proxy-from-env": "1.1.0",
@ -165,5 +142,13 @@
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"ws": "8.11.0"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}

View file

@ -13,29 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import glob from 'glob';
import dts from 'rollup-plugin-dts';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import glob from 'glob';
export default ['cjs', 'esm'].flatMap(outputType => {
const configs = [];
// Note we don't use path.join here. We cannot since `glob` does not support
// the backslash path separator.
const thirdPartyPath = `lib/${outputType}/third_party`;
for (const jsFile of glob.sync(`${thirdPartyPath}/**/*.js`)) {
for (const file of glob.sync(`lib/${outputType}/third_party/**/*.js`)) {
configs.push({
input: jsFile,
output: {file: jsFile, format: outputType},
input: file,
output: {
file,
format: outputType,
},
plugins: [commonjs(), nodeResolve()],
});
}
for (const typesFile of glob.sync(`${thirdPartyPath}/**/*.d.ts`)) {
configs.push({
input: typesFile,
output: {file: typesFile, format: outputType},
plugins: [dts({respectExternal: true})],
});
}
return configs;
});

View file

@ -17,11 +17,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Page} from './Page.js'; // TODO: move to ./api
import type {Target} from '../common/Target.js'; // TODO: move to ./api
import type {BrowserContext} from './BrowserContext.js';
import type {Page} from './Page.js'; // TODO: move to ./api
/**
* BrowserContext options.

View file

@ -15,9 +15,10 @@
*/
import {EventEmitter} from '../common/EventEmitter.js';
import {Page} from './Page.js';
import {Target} from '../common/Target.js';
import type {Permission, Browser} from './Browser.js';
import {Page} from './Page.js';
/**
* BrowserContexts provide a way to operate multiple independent browser

View file

@ -0,0 +1,728 @@
/**
* 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 {Protocol} from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {Frame} from '../common/Frame.js';
import {MouseButton} from '../common/Input.js';
import {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import {
ElementFor,
EvaluateFuncWith,
HandleFor,
NodeFor,
} from '../common/types.js';
import {KeyInput} from '../common/USKeyboardLayout.js';
import {JSHandle} from './JSHandle.js';
import {ScreenshotOptions} from './Page.js';
/**
* @public
*/
export interface BoxModel {
content: Point[];
padding: Point[];
border: Point[];
margin: Point[];
width: number;
height: number;
}
/**
* @public
*/
export interface BoundingBox extends Point {
/**
* the width of the element in pixels.
*/
width: number;
/**
* the height of the element in pixels.
*/
height: number;
}
/**
* @public
*/
export interface Offset {
/**
* x-offset for the clickable point relative to the top-left corner of the border box.
*/
x: number;
/**
* y-offset for the clickable point relative to the top-left corner of the border box.
*/
y: number;
}
/**
* @public
*/
export interface ClickOptions {
/**
* Time to wait between `mousedown` and `mouseup` in milliseconds.
*
* @defaultValue 0
*/
delay?: number;
/**
* @defaultValue 'left'
*/
button?: MouseButton;
/**
* @defaultValue 1
*/
clickCount?: number;
/**
* Offset for the clickable point relative to the top-left corner of the border box.
*/
offset?: Offset;
}
/**
* @public
*/
export interface PressOptions {
/**
* Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
*/
delay?: number;
/**
* If specified, generates an input event with this text.
*/
text?: string;
}
/**
* @public
*/
export interface Point {
x: number;
y: number;
}
/**
* ElementHandle represents an in-page DOM element.
*
* @remarks
* ElementHandles can be created with the {@link Page.$} method.
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* const hrefElement = await page.$('a');
* await hrefElement.click();
* // ...
* })();
* ```
*
* ElementHandle prevents the DOM element from being garbage-collected unless the
* handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed
* when their origin frame gets navigated.
*
* ElementHandle instances can be used as arguments in {@link Page.$eval} and
* {@link Page.evaluate} methods.
*
* If you're using TypeScript, ElementHandle takes a generic argument that
* denotes the type of element the handle is holding within. For example, if you
* have a handle to a `<select>` element, you can type it as
* `ElementHandle<HTMLSelectElement>` and you get some nicer type checks.
*
* @public
*/
export class ElementHandle<
ElementType extends Node = Element
> extends JSHandle<ElementType> {
/**
* @internal
*/
constructor() {
super();
}
/**
* @internal
*/
override executionContext(): ExecutionContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
override get client(): CDPSession {
throw new Error('Not implemented');
}
get frame(): Frame {
throw new Error('Not implemented');
}
/**
* Queries the current element for an element matching the given selector.
*
* @param selector - The selector to query for.
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async $<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
}
/**
* Queries the current element for all elements matching the given selector.
*
* @param selector - The selector to query for.
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>>;
async $$<Selector extends string>(): Promise<
Array<ElementHandle<NodeFor<Selector>>>
> {
throw new Error('Not implemented');
}
/**
* Runs the given function on the first element matching the given selector in
* the current element.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```ts
* const tweetHandle = await page.$('.tweet');
* expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe(
* '100'
* );
* expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe(
* '10'
* );
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in this element's page's
* context. The first element matching the selector will be passed in as the
* first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $eval(): Promise<unknown> {
throw new Error('Not implemented');
}
/**
* Runs the given function on an array of elements matching the given selector
* in the current element.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
* HTML:
*
* ```html
* <div class="feed">
* <div class="tweet">Hello!</div>
* <div class="tweet">Hi!</div>
* </div>
* ```
*
* JavaScript:
*
* ```js
* const feedHandle = await page.$('.feed');
* expect(
* await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))
* ).toEqual(['Hello!', 'Hi!']);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the element's page's
* context. An array of elements matching the given selector will be passed to
* the function as its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $$eval(): Promise<unknown> {
throw new Error('Not implemented');
}
/**
* @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
*
* Example: `await elementHandle.$$('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the elementHandle.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* 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}
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>>;
async $x(): Promise<Array<ElementHandle<Node>>> {
throw new Error('Not implemented');
}
/**
* Wait for an element matching the given selector to appear in the current
* element.
*
* Unlike {@link Frame.waitForSelector}, this method does not work across
* navigations or if the element is detached from DOM.
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .mainFrame()
* .waitForSelector('img')
* .then(() => console.log('First URL with image: ' + currentURL));
*
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param selector - The selector to query and wait for.
* @param options - Options for customizing waiting behavior.
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
async waitForSelector<Selector extends string>(
selector: Selector,
options?: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async waitForSelector<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
}
/**
* @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath`
* prefix.
*
* Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the elementHandle.
*
* Wait for the `xpath` within the element. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
* function will throw.
*
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* This method works across navigation.
*
* ```ts
* import puppeteer from 'puppeteer';
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .waitForXPath('//img')
* .then(() => console.log('First URL with image: ' + currentURL));
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param xpath - A
* {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
* element to wait for
* @param options - Optional waiting parameters
* @returns Promise which resolves when element specified by xpath string is
* added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
* not found in DOM, otherwise resolves to `ElementHandle`.
* @remarks
* The optional Argument `options` have properties:
*
* - `visible`: A boolean to wait for element to be present in DOM and to be
* visible, i.e. to not have `display: none` or `visibility: hidden` CSS
* properties. Defaults to `false`.
*
* - `hidden`: A boolean wait for element to not be found in the DOM or to be
* hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
* Defaults to `false`.
*
* - `timeout`: A number which is maximum time to wait for in milliseconds.
* Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The
* default value can be changed by using the {@link Page.setDefaultTimeout}
* method.
*/
async waitForXPath(
xpath: string,
options?: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
}
): Promise<ElementHandle<Node> | null>;
async waitForXPath(): Promise<ElementHandle<Node> | null> {
throw new Error('Not implemented');
}
/**
* Converts the current handle to the given element type.
*
* @example
*
* ```ts
* const element: ElementHandle<Element> = await page.$(
* '.class-name-of-anchor'
* );
* // DO NOT DISPOSE `element`, this will be always be the same handle.
* const anchor: ElementHandle<HTMLAnchorElement> = await element.toElement(
* 'a'
* );
* ```
*
* @param tagName - The tag name of the desired element type.
* @throws An error if the handle does not match. **The handle will not be
* automatically disposed.**
*/
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(tagName: K): Promise<HandleFor<ElementFor<K>>>;
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(): Promise<HandleFor<ElementFor<K>>> {
throw new Error('Not implemented');
}
override asElement(): ElementHandle<ElementType> | null {
return this;
}
/**
* Resolves to the content frame for element handles referencing
* iframe nodes, or null otherwise
*/
async contentFrame(): Promise<Frame | null> {
throw new Error('Not implemented');
}
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
async clickablePoint(offset?: Offset): Promise<Point>;
async clickablePoint(): Promise<Point> {
throw new Error('Not implemented');
}
/**
* This method scrolls element into view if needed, and then
* uses {@link Page.mouse} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
async hover(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method scrolls element into view if needed, and then
* uses {@link Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
async click(
this: ElementHandle<Element>,
options?: ClickOptions
): Promise<void>;
async click(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method creates and captures a dragevent from the element.
*/
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');
}
/**
* This method creates a `dragenter` event on the element.
*/
async dragEnter(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragEnter(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method creates a `dragover` event on the element.
*/
async dragOver(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async dragOver(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method triggers a drop on the element.
*/
async drop(
this: ElementHandle<Element>,
data?: Protocol.Input.DragData
): Promise<void>;
async drop(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method triggers a dragenter, dragover, and drop on the element.
*/
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');
}
/**
* Triggers a `change` and `input` event once all the provided options have been
* selected. If there's no `<select>` element matching `selector`, the method
* throws an error.
*
* @example
*
* ```ts
* handle.select('blue'); // single selection
* handle.select('red', 'green', 'blue'); // multiple selections
* ```
*
* @param values - Values of options to select. If the `<select>` has the
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
*/
async select(...values: string[]): Promise<string[]>;
async select(): Promise<string[]> {
throw new Error('Not implemented');
}
/**
* This method expects `elementHandle` to point to an
* {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}.
*
* @param filePaths - Sets the value of the file input to these paths.
* If a path is relative, then it is resolved against the
* {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
* Note for locals script connecting to remote chrome environments,
* paths must be absolute.
*/
async uploadFile(
this: ElementHandle<HTMLInputElement>,
...filePaths: 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.
*/
async tap(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
async touchStart(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
async touchMove(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
async touchEnd(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
async focus(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and
* `keyup` event for each character in the text.
*
* To press a special key, like `Control` or `ArrowDown`,
* use {@link ElementHandle.press}.
*
* @example
*
* ```ts
* await elementHandle.type('Hello'); // Types instantly
* await elementHandle.type('World', {delay: 100}); // Types slower, like a user
* ```
*
* @example
* An example of typing into a text field and then submitting the form:
*
* ```ts
* const elementHandle = await page.$('input');
* await elementHandle.type('some text');
* await elementHandle.press('Enter');
* ```
*/
async type(text: string, options?: {delay: number}): Promise<void>;
async type(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
*
* @remarks
* If `key` is a single character and no modifier keys besides `Shift`
* are being held down, a `keypress`/`input` event will also be generated.
* The `text` option can be specified to force an input event to be generated.
*
* **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift`
* will type the text in upper case.
*
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*/
async press(key: KeyInput, options?: PressOptions): Promise<void>;
async press(): Promise<void> {
throw new Error('Not implemented');
}
/**
* This method returns the bounding box of the element (relative to the main frame),
* or `null` if the element is not visible.
*/
async boundingBox(): Promise<BoundingBox | null> {
throw new Error('Not implemented');
}
/**
* This method returns boxes of the element, or `null` if the element is not visible.
*
* @remarks
*
* Boxes are represented as an array of points;
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/
async boxModel(): Promise<BoxModel | null> {
throw new Error('Not implemented');
}
/**
* This method scrolls element into view if needed, and then uses
* {@link Page.screenshot} to take a screenshot of the element.
* If the element is detached from DOM, the method throws an error.
*/
async screenshot(
this: ElementHandle<Element>,
options?: ScreenshotOptions
): Promise<string | Buffer>;
async screenshot(this: ElementHandle<Element>): Promise<string | Buffer> {
throw new Error('Not implemented');
}
/**
* Resolves to true if the element is visible in the current viewport.
*/
async isIntersectingViewport(
this: ElementHandle<Element>,
options?: {
threshold?: number;
}
): Promise<boolean>;
async isIntersectingViewport(): Promise<boolean> {
throw new Error('Not implemented');
}
}

View file

@ -0,0 +1,197 @@
/**
* 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 Protocol from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
import {ElementHandle} from './ElementHandle.js';
declare const __JSHandleSymbol: unique symbol;
/**
* Represents a reference to a JavaScript object. Instances can be created using
* {@link Page.evaluateHandle}.
*
* Handles prevent the referenced JavaScript object from being garbage-collected
* unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
* are auto-disposed when their associated frame is navigated away or the parent
* context gets destroyed.
*
* Handles can be used as arguments for any evaluation function such as
* {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
* They are resolved to their referenced object.
*
* @example
*
* ```ts
* const windowHandle = await page.evaluateHandle(() => window);
* ```
*
* @public
*/
export class JSHandle<T = unknown> {
/**
* Used for nominally typing {@link JSHandle}.
*/
[__JSHandleSymbol]?: T;
/**
* @internal
*/
constructor() {}
/**
* @internal
*/
get disposed(): boolean {
throw new Error('Not implemented');
}
/**
* @internal
*/
executionContext(): ExecutionContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
get client(): CDPSession {
throw new Error('Not implemented');
}
/**
* Evaluates the given function with the current handle as its first argument.
*/
async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async evaluate(): Promise<unknown> {
throw new Error('Not implemented');
}
/**
* Evaluates the given function with the current handle as its first argument.
*
*/
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async evaluateHandle(): Promise<HandleFor<unknown>> {
throw new Error('Not implemented');
}
/**
* Fetches a single property from the referenced object.
*/
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty<K extends keyof T>(): Promise<HandleFor<T[K]>> {
throw new Error('Not implemented');
}
/**
* Gets a map of handles representing the properties of the current handle.
*
* @example
*
* ```ts
* const listHandle = await page.evaluateHandle(() => document.body.children);
* const properties = await listHandle.getProperties();
* const children = [];
* for (const property of properties.values()) {
* const element = property.asElement();
* if (element) {
* children.push(element);
* }
* }
* children; // holds elementHandles to all children of document.body
* ```
*/
async getProperties(): Promise<Map<string, JSHandle>> {
throw new Error('Not implemented');
}
/**
* @returns A vanilla object representing the serializable portions of the
* referenced object.
* @throws Throws if the object cannot be serialized due to circularity.
*
* @remarks
* If the object has a `toJSON` function, it **will not** be called.
*/
async jsonValue(): Promise<T> {
throw new Error('Not implemented');
}
/**
* @returns Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
asElement(): ElementHandle<Node> | null {
throw new Error('Not implemented');
}
/**
* Releases the object referenced by the handle for garbage collection.
*/
async dispose(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Returns a string representation of the JSHandle.
*
* @remarks
* Useful during debugging.
*/
toString(): string {
throw new Error('Not implemented');
}
/**
* @internal
*/
get id(): string | undefined {
throw new Error('Not implemented');
}
/**
* Provides access to the
* [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject)
* backing this handle.
*/
remoteObject(): Protocol.Runtime.RemoteObject {
throw new Error('Not implemented');
}
}

View file

@ -14,14 +14,15 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Readable} from 'stream';
import {Protocol} from 'devtools-protocol';
import type {Accessibility} from '../common/Accessibility.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js';
import type {Dialog} from '../common/Dialog.js';
import type {ElementHandle} from '../common/ElementHandle.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js';
import type {
@ -39,17 +40,24 @@ import type {
Touchscreen,
} from '../common/Input.js';
import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import type {JSHandle} from '../common/JSHandle.js';
import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import type {Credentials, NetworkConditions} from '../common/NetworkManager.js';
import type {PDFOptions} from '../common/PDFOptions.js';
import type {Viewport} from '../common/PuppeteerViewport.js';
import type {Target} from '../common/Target.js';
import type {Tracing} from '../common/Tracing.js';
import type {EvaluateFunc, HandleFor, NodeFor} from '../common/types.js';
import type {
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
NodeFor,
} from '../common/types.js';
import type {WebWorker} from '../common/WebWorker.js';
import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js';
import type {ElementHandle} from './ElementHandle.js';
import type {JSHandle} from './JSHandle.js';
/**
* @public
@ -956,21 +964,16 @@ export class Page extends EventEmitter {
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
>(): Promise<Awaited<ReturnType<Func>>> {
async $eval(): Promise<unknown> {
throw new Error('Not implemented');
}
@ -1039,21 +1042,16 @@ export class Page extends EventEmitter {
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]>
>(): Promise<Awaited<ReturnType<Func>>> {
async $$eval(): Promise<unknown> {
throw new Error('Not implemented');
}
@ -2465,7 +2463,7 @@ export class Page extends EventEmitter {
* @param options - Optional waiting parameters
* @returns Promise which resolves when element specified by xpath string is
* added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
* not found in DOM.
* not found in DOM, otherwise resolves to `ElementHandle`.
* @remarks
* The optional Argument `options` have properties:
*
@ -2483,11 +2481,7 @@ export class Page extends EventEmitter {
*/
waitForXPath(
xpath: string,
options?: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
}
options?: WaitForSelectorOptions
): Promise<ElementHandle<Node> | null>;
waitForXPath(): Promise<ElementHandle<Node> | null> {
throw new Error('Not implemented');

View file

@ -17,3 +17,5 @@
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './Page.js';
export * from './JSHandle.js';
export * from './ElementHandle.js';

View file

@ -15,8 +15,10 @@
*/
import {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
/**
* Represents a Node and the properties of it that are relevant to Accessibility.
@ -186,7 +188,7 @@ export class Accessibility {
let backendNodeId: number | undefined;
if (root) {
const {node} = await this.#client.send('DOM.describeNode', {
objectId: root.remoteObject().objectId,
objectId: root.id,
});
backendNodeId = node.backendNodeId;
}

View file

@ -16,46 +16,42 @@
import {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {QueryHandler, QuerySelector} from './QueryHandler.js';
import {AwaitableIterable} from './types.js';
import type {ElementHandle} from './ElementHandle.js';
import type {PuppeteerQueryHandler} from './QueryHandler.js';
import type {Frame} from './Frame.js';
async function queryAXTree(
const queryAXTree = async (
client: CDPSession,
element: ElementHandle<Node>,
accessibleName?: string,
role?: string
): Promise<Protocol.Accessibility.AXNode[]> {
): Promise<Protocol.Accessibility.AXNode[]> => {
const {nodes} = await client.send('Accessibility.queryAXTree', {
objectId: element.remoteObject().objectId,
objectId: element.id,
accessibleName,
role,
});
const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter(
(node: Protocol.Accessibility.AXNode) => {
return nodes.filter((node: Protocol.Accessibility.AXNode) => {
return !node.role || node.role.value !== 'StaticText';
}
);
return filteredNodes;
}
});
};
type ARIASelector = {name?: string; role?: string};
const KNOWN_ATTRIBUTES = Object.freeze(['name', 'role']);
const isKnownAttribute = (
attribute: string
): attribute is keyof ARIASelector => {
return KNOWN_ATTRIBUTES.includes(attribute);
};
const normalizeValue = (value: string): string => {
return value.replace(/ +/g, ' ').trim();
};
const knownAttributes = new Set(['name', 'role']);
const attributeRegexp =
/\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g;
type ARIAQueryOption = {name?: string; role?: string};
function isKnownAttribute(
attribute: string
): attribute is keyof ARIAQueryOption {
return knownAttributes.has(attribute);
}
/**
* The selectors consist of an accessible name to query for and optionally
@ -68,11 +64,13 @@ function isKnownAttribute(
* - 'label' queries for elements with name 'label' and any role.
* - '[name=""][role="button"]' queries for elements with no name and role 'button'.
*/
function parseAriaSelector(selector: string): ARIAQueryOption {
const queryOptions: ARIAQueryOption = {};
const ATTRIBUTE_REGEXP =
/\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g;
const parseARIASelector = (selector: string): ARIASelector => {
const queryOptions: ARIASelector = {};
const defaultName = selector.replace(
attributeRegexp,
(_, attribute: string, _quote: string, value: string) => {
ATTRIBUTE_REGEXP,
(_, attribute, __, value) => {
attribute = attribute.trim();
assert(
isKnownAttribute(attribute),
@ -86,104 +84,41 @@ function parseAriaSelector(selector: string): ARIAQueryOption {
queryOptions.name = normalizeValue(defaultName);
}
return queryOptions;
}
const queryOneId = async (element: ElementHandle<Node>, selector: string) => {
const {name, role} = parseAriaSelector(selector);
const res = await queryAXTree(element.client, element, name, role);
if (!res[0] || !res[0].backendDOMNodeId) {
return null;
}
return res[0].backendDOMNodeId;
};
const queryOne: PuppeteerQueryHandler['queryOne'] = async (
element,
selector
) => {
const id = await queryOneId(element, selector);
if (!id) {
return null;
}
return (await element.frame.worlds[MAIN_WORLD].adoptBackendNode(
id
)) as ElementHandle<Node>;
};
const waitFor: PuppeteerQueryHandler['waitFor'] = async (
elementOrFrame,
selector,
options
) => {
let frame: Frame;
let element: ElementHandle<Node> | undefined;
if ('isOOPFrame' in elementOrFrame) {
frame = elementOrFrame;
} else {
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
}
const ariaQuerySelector = async (selector: string) => {
const id = await queryOneId(
element || (await frame.worlds[PUPPETEER_WORLD].document()),
selector
);
if (!id) {
return null;
}
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
id
)) as ElementHandle<Node>;
};
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
(_: Element, selector: string) => {
return (
globalThis as unknown as {
ariaQuerySelector(selector: string): Node | null;
}
).ariaQuerySelector(selector);
},
element,
selector,
options,
new Map([['ariaQuerySelector', ariaQuerySelector]])
);
if (element) {
await element.dispose();
}
const handle = result?.asElement();
if (!handle) {
await result?.dispose();
return null;
}
return handle.frame.worlds[MAIN_WORLD].transferHandle(handle);
};
const queryAll: PuppeteerQueryHandler['queryAll'] = async (
element,
selector
) => {
const exeCtx = element.executionContext();
const {name, role} = parseAriaSelector(selector);
const res = await queryAXTree(exeCtx._client, element, name, role);
const world = exeCtx._world!;
return Promise.all(
res.map(axNode => {
return world.adoptBackendNode(axNode.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
})
);
};
/**
* @internal
*/
export const ariaHandler: PuppeteerQueryHandler = {
queryOne,
waitFor,
queryAll,
};
export class ARIAQueryHandler extends QueryHandler {
static override querySelector: QuerySelector = async (
node,
selector,
{ariaQuerySelector}
) => {
return ariaQuerySelector(node, selector);
};
static override async *queryAll(
element: ElementHandle<Node>,
selector: string
): AwaitableIterable<ElementHandle<Node>> {
const context = element.executionContext();
const {name, role} = parseARIASelector(selector);
const results = await queryAXTree(context._client, element, name, role);
const world = context._world!;
yield* AsyncIterableUtil.map(results, node => {
return world.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
});
}
static override queryOne = async (
element: ElementHandle<Node>,
selector: string
): Promise<ElementHandle<Node> | null> => {
return (
(await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null
);
};
}

View file

@ -0,0 +1,123 @@
import {JSHandle} from '../api/JSHandle.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {ExecutionContext} from './ExecutionContext.js';
import {debugError} from './util.js';
/**
* @internal
*/
export class Binding {
#name: string;
#fn: (...args: unknown[]) => unknown;
constructor(name: string, fn: (...args: unknown[]) => unknown) {
this.#name = name;
this.#fn = fn;
}
get name(): string {
return this.#name;
}
/**
* @param context - Context to run the binding in; the context should have
* the binding added to it beforehand.
* @param id - ID of the call. This should come from the CDP
* `onBindingCalled` response.
* @param args - Plain arguments from CDP.
*/
async run(
context: ExecutionContext,
id: number,
args: unknown[],
isTrivial: boolean
): Promise<void> {
const garbage = [];
try {
if (!isTrivial) {
// Getting non-trivial arguments.
const handles = await context.evaluateHandle(
(name, seq) => {
// @ts-expect-error Code is evaluated in a different context.
return globalThis[name].args.get(seq);
},
this.#name,
id
);
try {
const properties = await handles.getProperties();
for (const [index, handle] of properties) {
// This is not straight-forward since some arguments can stringify, but
// aren't plain objects so add subtypes when the use-case arises.
if (index in args) {
switch (handle.remoteObject().subtype) {
case 'node':
args[+index] = handle;
break;
default:
garbage.push(handle.dispose());
}
} else {
garbage.push(handle.dispose());
}
}
} finally {
await handles.dispose();
}
}
await context.evaluate(
(name, seq, result) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).resolve(result);
callbacks.delete(seq);
},
this.#name,
id,
await this.#fn(...args)
);
for (const arg of args) {
if (arg instanceof JSHandle) {
garbage.push(arg.dispose());
}
}
} catch (error) {
if (isErrorLike(error)) {
await context
.evaluate(
(name, seq, message, stack) => {
const error = new Error(message);
error.stack = stack;
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).reject(error);
callbacks.delete(seq);
},
this.#name,
id,
error.message,
error.stack
)
.catch(debugError);
} else {
await context
.evaluate(
(name, seq, error) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = globalThis[name].callbacks;
callbacks.get(seq).reject(error);
callbacks.delete(seq);
},
this.#name,
id,
error
)
.catch(debugError);
}
} finally {
await Promise.all(garbage);
}
}
}

View file

@ -15,17 +15,9 @@
*/
import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {waitWithTimeout} from './util.js';
import {Page} from '../api/Page.js';
import {Viewport} from './PuppeteerViewport.js';
import {Target} from './Target.js';
import {TaskQueue} from './TaskQueue.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {ChromeTargetManager} from './ChromeTargetManager.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {
Browser as BrowserBase,
BrowserCloseCallback,
@ -39,6 +31,17 @@ import {
Permission,
} from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
import {Page} from '../api/Page.js';
import {assert} from '../util/assert.js';
import {ChromeTargetManager} from './ChromeTargetManager.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import {Viewport} from './PuppeteerViewport.js';
import {Target} from './Target.js';
import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {waitWithTimeout} from './util.js';
/**
* @internal

View file

@ -14,18 +14,18 @@
* limitations under the License.
*/
import {debugError} from './util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
import {isNode} from '../environment.js';
import {assert} from '../util/assert.js';
import {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPBrowser} from './Browser.js';
import {Connection} from './Connection.js';
import {ConnectionTransport} from './ConnectionTransport.js';
import {getFetch} from './fetch.js';
import {Viewport} from './PuppeteerViewport.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.

View file

@ -15,18 +15,20 @@
*/
import {Protocol} from 'devtools-protocol';
import {TargetFilterCallback} from '../api/Browser.js';
import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {Target} from './Target.js';
import {debugError} from './util.js';
import {TargetFilterCallback} from '../api/Browser.js';
import {
TargetInterceptor,
TargetFactory,
TargetManager,
TargetManagerEmittedEvents,
} from './TargetManager.js';
import {debugError} from './util.js';
/**
* ChromeTargetManager uses the CDP's auto-attach mechanism to intercept

View file

@ -13,16 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/order */
import {assert} from '../util/assert.js';
import {debug} from './Debug.js';
const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');
import {Protocol} from 'devtools-protocol';
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {ConnectionTransport} from './ConnectionTransport.js';
import {EventEmitter} from './EventEmitter.js';
import {ProtocolError} from './Errors.js';
import {EventEmitter} from './EventEmitter.js';
/**
* @public

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import {JSHandle} from './JSHandle.js';
import {JSHandle} from '../api/JSHandle.js';
/**
* @public

View file

@ -14,12 +14,13 @@
* limitations under the License.
*/
import {assert} from '../util/assert.js';
import {addEventListener, debugError, PuppeteerEventListener} from './util.js';
import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {EVALUATION_SCRIPT_URL} from './ExecutionContext.js';
import {addEventListener, debugError, PuppeteerEventListener} from './util.js';
import {removeEventListeners} from './util.js';
/**

View file

@ -0,0 +1,227 @@
/**
* 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 PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import {QueryHandler, QuerySelector, QuerySelectorAll} from './QueryHandler.js';
import {scriptInjector} from './ScriptInjector.js';
/**
* @public
*/
export interface CustomQueryHandler {
/**
* @returns A {@link Node} matching the given `selector` from {@link node}.
*/
queryOne?: (node: Node, selector: string) => Node | null;
/**
* @returns Some {@link Node}s matching the given `selector` from {@link node}.
*/
queryAll?: (node: Node, selector: string) => Iterable<Node>;
}
/**
* The registry of {@link CustomQueryHandler | custom query handlers}.
*
* @example
*
* ```ts
* Puppeteer.customQueryHandlers.register('lit', { });
* const aHandle = await page.$('lit/…');
* ```
*
* @internal
*/
export class CustomQueryHandlerRegistry {
#handlers = new Map<
string,
[registerScript: string, Handler: typeof QueryHandler]
>();
/**
* @internal
*/
get(name: string): typeof QueryHandler | undefined {
const handler = this.#handlers.get(name);
return handler ? handler[1] : undefined;
}
/**
* Registers a {@link CustomQueryHandler | custom query handler}.
*
* @remarks
* After registration, the handler can be used everywhere where a selector is
* expected by prepending the selection string with `<name>/`. The name is
* only allowed to consist of lower- and upper case latin letters.
*
* @example
*
* ```ts
* Puppeteer.customQueryHandlers.register('lit', { });
* const aHandle = await page.$('lit/…');
* ```
*
* @param name - Name to register under.
* @param queryHandler - {@link CustomQueryHandler | Custom query handler} to
* register.
*
* @internal
*/
register(name: string, handler: CustomQueryHandler): void {
if (this.#handlers.has(name)) {
throw new Error(`Cannot register over existing handler: ${name}`);
}
assert(
!this.#handlers.has(name),
`Cannot register over existing handler: ${name}`
);
assert(
/^[a-zA-Z]+$/.test(name),
`Custom query handler names may only contain [a-zA-Z]`
);
assert(
handler.queryAll || handler.queryOne,
`At least one query method must be implemented.`
);
const Handler = class extends QueryHandler {
static override querySelectorAll: QuerySelectorAll = interpolateFunction(
(node, selector, PuppeteerUtil) => {
return PuppeteerUtil.customQuerySelectors
.get(PLACEHOLDER('name'))!
.querySelectorAll(node, selector);
},
{name: JSON.stringify(name)}
);
static override querySelector: QuerySelector = interpolateFunction(
(node, selector, PuppeteerUtil) => {
return PuppeteerUtil.customQuerySelectors
.get(PLACEHOLDER('name'))!
.querySelector(node, selector);
},
{name: JSON.stringify(name)}
);
};
const registerScript = interpolateFunction(
(PuppeteerUtil: PuppeteerUtil) => {
PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), {
queryAll: PLACEHOLDER('queryAll'),
queryOne: PLACEHOLDER('queryOne'),
});
},
{
name: JSON.stringify(name),
queryAll: handler.queryAll
? stringifyFunction(handler.queryAll)
: String(undefined),
queryOne: handler.queryOne
? stringifyFunction(handler.queryOne)
: String(undefined),
}
).toString();
this.#handlers.set(name, [registerScript, Handler]);
scriptInjector.append(registerScript);
}
/**
* Unregisters the {@link CustomQueryHandler | custom query handler} for the
* given name.
*
* @throws `Error` if there is no handler under the given name.
*
* @internal
*/
unregister(name: string): void {
const handler = this.#handlers.get(name);
if (!handler) {
throw new Error(`Cannot unregister unknown handler: ${name}`);
}
scriptInjector.pop(handler[0]);
this.#handlers.delete(name);
}
/**
* Gets the names of all {@link CustomQueryHandler | custom query handlers}.
*
* @internal
*/
names(): string[] {
return [...this.#handlers.keys()];
}
/**
* Unregisters all custom query handlers.
*
* @internal
*/
clear(): void {
for (const [registerScript] of this.#handlers) {
scriptInjector.pop(registerScript);
}
this.#handlers.clear();
}
}
/**
* @internal
*/
export const customQueryHandlers = new CustomQueryHandlerRegistry();
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.registerCustomQueryHandler}
*
* @public
*/
export function registerCustomQueryHandler(
name: string,
handler: CustomQueryHandler
): void {
customQueryHandlers.register(name, handler);
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.unregisterCustomQueryHandler}
*
* @public
*/
export function unregisterCustomQueryHandler(name: string): void {
customQueryHandlers.unregister(name);
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.customQueryHandlerNames}
*
* @public
*/
export function customQueryHandlerNames(): string[] {
return customQueryHandlers.names();
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.clearCustomQueryHandlers}
*
* @public
*/
export function clearCustomQueryHandlers(): void {
customQueryHandlers.clear();
}

View file

@ -14,10 +14,12 @@
* limitations under the License.
*/
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
/**
* Dialog instances are dispatched by the {@link Page} via the `dialog` event.
*

View file

@ -15,26 +15,38 @@
*/
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {
BoundingBox,
BoxModel,
ClickOptions,
JSHandle,
ElementHandle,
Offset,
Point,
PressOptions,
} from './JSHandle.js';
} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {Page, ScreenshotOptions} from '../api/Page.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {ElementFor, EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {getQueryHandlerAndSelector} from './GetQueryHandler.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js';
import {CDPPage} from './Page.js';
import {
ElementFor,
EvaluateFuncWith,
HandleFor,
HandleOr,
NodeFor,
} from './types.js';
import {KeyInput} from './USKeyboardLayout.js';
import {debugError, isString} from './util.js';
import {CDPPage} from './Page.js';
const applyOffsetsToQuad = (
quad: Point[],
@ -47,56 +59,76 @@ const applyOffsetsToQuad = (
};
/**
* ElementHandle represents an in-page DOM element.
* The CDPElementHandle extends ElementHandle now to keep compatibility
* with `instanceof` because of that we need to have methods for
* CDPJSHandle to in this implementation as well.
*
* @remarks
* ElementHandles can be created with the {@link Page.$} method.
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* await page.goto('https://example.com');
* const hrefElement = await page.$('a');
* await hrefElement.click();
* // ...
* })();
* ```
*
* ElementHandle prevents the DOM element from being garbage-collected unless the
* handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed
* when their origin frame gets navigated.
*
* ElementHandle instances can be used as arguments in {@link Page.$eval} and
* {@link Page.evaluate} methods.
*
* If you're using TypeScript, ElementHandle takes a generic argument that
* denotes the type of element the handle is holding within. For example, if you
* have a handle to a `<select>` element, you can type it as
* `ElementHandle<HTMLSelectElement>` and you get some nicer type checks.
*
* @public
*/
export class ElementHandle<
ElementType extends Node = Element
> extends JSHandle<ElementType> {
#frame: Frame;
/**
* @internal
*/
export class CDPElementHandle<
ElementType extends Node = Element
> extends ElementHandle<ElementType> {
#frame: Frame;
#jsHandle: CDPJSHandle<ElementType>;
constructor(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject,
frame: Frame
) {
super(context, remoteObject);
super();
this.#jsHandle = new CDPJSHandle(context, remoteObject);
this.#frame = frame;
}
/**
* @internal
*/
override executionContext(): ExecutionContext {
return this.#jsHandle.executionContext();
}
/**
* @internal
*/
override get client(): CDPSession {
return this.#jsHandle.client;
}
override get id(): string | undefined {
return this.#jsHandle.id;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#jsHandle.remoteObject();
}
override async evaluate<
Params extends unknown[],
Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
ElementType,
Params
>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return this.executionContext().evaluate(pageFunction, this, ...args);
}
override evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
ElementType,
Params
>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.executionContext().evaluateHandle(pageFunction, this, ...args);
}
get #frameManager(): FrameManager {
return this.#frame._frameManager;
}
@ -105,85 +137,72 @@ export class ElementHandle<
return this.#frame.page();
}
get frame(): Frame {
override get frame(): Frame {
return this.#frame;
}
/**
* Queries the current element for an element matching the given selector.
*
* @param selector - The selector to query for.
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
async $<Selector extends string>(
override get disposed(): boolean {
return this.#jsHandle.disposed;
}
override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K>
): Promise<HandleFor<ElementType[K]>>;
override async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K>
): Promise<HandleFor<ElementType[K]>> {
return this.#jsHandle.getProperty(propertyName);
}
override async getProperties(): Promise<Map<string, JSHandle>> {
return this.#jsHandle.getProperties();
}
override asElement(): CDPElementHandle<ElementType> | null {
return this;
}
override async jsonValue(): Promise<ElementType> {
return this.#jsHandle.jsonValue();
}
override toString(): string {
return this.#jsHandle.toString();
}
override async dispose(): Promise<void> {
return await this.#jsHandle.dispose();
}
override async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, queryHandler} =
): Promise<CDPElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryOne,
'Cannot handle queries for a single element with the given selector'
);
return (await queryHandler.queryOne(
return (await QueryHandler.queryOne(
this,
updatedSelector
)) as ElementHandle<NodeFor<Selector>> | null;
)) as CDPElementHandle<NodeFor<Selector>> | null;
}
/**
* Queries the current element for all elements matching the given selector.
*
* @param selector - The selector to query for.
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
async $$<Selector extends string>(
override async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
const {updatedSelector, queryHandler} =
): Promise<Array<CDPElementHandle<NodeFor<Selector>>>> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector'
);
return (await queryHandler.queryAll(this, updatedSelector)) as Array<
ElementHandle<NodeFor<Selector>>
>;
return AsyncIterableUtil.collect(
QueryHandler.queryAll(this, updatedSelector)
) as Promise<Array<CDPElementHandle<NodeFor<Selector>>>>;
}
/**
* Runs the given function on the first element matching the given selector in
* the current element.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
*
* ```ts
* const tweetHandle = await page.$('.tweet');
* expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe(
* '100'
* );
* expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe(
* '10'
* );
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in this element's page's
* context. The first element matching the selector will be passed in as the
* first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $eval<
override async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
@ -200,238 +219,69 @@ export class ElementHandle<
return result;
}
/**
* Runs the given function on an array of elements matching the given selector
* in the current element.
*
* If the given function returns a promise, then this method will wait till
* the promise resolves.
*
* @example
* HTML:
*
* ```html
* <div class="feed">
* <div class="tweet">Hello!</div>
* <div class="tweet">Hi!</div>
* </div>
* ```
*
* JavaScript:
*
* ```js
* const feedHandle = await page.$('.feed');
* expect(
* await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))
* ).toEqual(['Hello!', 'Hi!']);
* ```
*
* @param selector - The selector to query for.
* @param pageFunction - The function to be evaluated in the element's page's
* context. An array of elements matching the given selector will be passed to
* the function as its first argument.
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
async $$eval<
override async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[HandleFor<Array<NodeFor<Selector>>>, ...Params]
> = EvaluateFunc<[HandleFor<Array<NodeFor<Selector>>>, ...Params]>
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
const {updatedSelector, queryHandler} =
getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector'
);
const handles = (await queryHandler.queryAll(
this,
updatedSelector
)) as Array<HandleFor<NodeFor<Selector>>>;
const elements = (await this.evaluateHandle((_, ...elements) => {
const results = await this.$$(selector);
const elements = await this.evaluateHandle((_, ...elements) => {
return elements;
}, ...handles)) as JSHandle<Array<NodeFor<Selector>>>;
}, ...results);
const [result] = await Promise.all([
elements.evaluate(pageFunction, ...args),
...handles.map(handle => {
return handle.dispose();
...results.map(results => {
return results.dispose();
}),
]);
await elements.dispose();
return result;
}
/**
* @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
*
* Example: `await elementHandle.$$('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the elementHandle.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* 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}
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
override async $x(
expression: string
): Promise<Array<CDPElementHandle<Node>>> {
if (expression.startsWith('//')) {
expression = `.${expression}`;
}
return this.$$(`xpath/${expression}`);
}
/**
* Wait for an element matching the given selector to appear in the current
* element.
*
* Unlike {@link Frame.waitForSelector}, this method does not work across
* navigations or if the element is detached from DOM.
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .mainFrame()
* .waitForSelector('img')
* .then(() => console.log('First URL with image: ' + currentURL));
*
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param selector - The selector to query and wait for.
* @param options - Options for customizing waiting behavior.
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
async waitForSelector<Selector extends string>(
override async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, queryHandler} =
): Promise<CDPElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
assert(queryHandler.waitFor, 'Query handler does not support waiting');
return (await queryHandler.waitFor(
return (await QueryHandler.waitFor(
this,
updatedSelector,
options
)) as ElementHandle<NodeFor<Selector>> | null;
)) as CDPElementHandle<NodeFor<Selector>> | null;
}
/**
* @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath`
* prefix.
*
* Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the elementHandle.
*
* Wait for the `xpath` within the element. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If
* the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
* function will throw.
*
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* This method works across navigation.
*
* ```ts
* import puppeteer from 'puppeteer';
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* let currentURL;
* page
* .waitForXPath('//img')
* .then(() => console.log('First URL with image: ' + currentURL));
* for (currentURL of [
* 'https://example.com',
* 'https://google.com',
* 'https://bbc.com',
* ]) {
* await page.goto(currentURL);
* }
* await browser.close();
* })();
* ```
*
* @param xpath - A
* {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
* element to wait for
* @param options - Optional waiting parameters
* @returns Promise which resolves when element specified by xpath string is
* added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
* not found in DOM.
* @remarks
* The optional Argument `options` have properties:
*
* - `visible`: A boolean to wait for element to be present in DOM and to be
* visible, i.e. to not have `display: none` or `visibility: hidden` CSS
* properties. Defaults to `false`.
*
* - `hidden`: A boolean wait for element to not be found in the DOM or to be
* hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
* Defaults to `false`.
*
* - `timeout`: A number which is maximum time to wait for in milliseconds.
* Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The
* default value can be changed by using the {@link Page.setDefaultTimeout}
* method.
*/
async waitForXPath(
override async waitForXPath(
xpath: string,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
): Promise<ElementHandle<Node> | null> {
): Promise<CDPElementHandle<Node> | null> {
if (xpath.startsWith('//')) {
xpath = `.${xpath}`;
}
return this.waitForSelector(`xpath/${xpath}`, options);
}
/**
* Converts the current handle to the given element type.
*
* @example
*
* ```ts
* const element: ElementHandle<Element> = await page.$(
* '.class-name-of-anchor'
* );
* // DO NOT DISPOSE `element`, this will be always be the same handle.
* const anchor: ElementHandle<HTMLAnchorElement> = await element.toElement(
* 'a'
* );
* ```
*
* @param tagName - The tag name of the desired element type.
* @throws An error if the handle does not match. **The handle will not be
* automatically disposed.**
*/
async toElement<
override async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(tagName: K): Promise<HandleFor<ElementFor<K>>> {
const isMatchingTagName = await this.evaluate((node, tagName) => {
@ -443,15 +293,7 @@ export class ElementHandle<
return this as unknown as HandleFor<ElementFor<K>>;
}
override asElement(): ElementHandle<ElementType> | null {
return this;
}
/**
* Resolves to the content frame for element handles referencing
* iframe nodes, or null otherwise
*/
async contentFrame(): Promise<Frame | null> {
override async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.remoteObject().objectId,
});
@ -461,7 +303,9 @@ export class ElementHandle<
return this.#frameManager.frame(nodeInfo.node.frameId);
}
async #scrollIntoViewIfNeeded(this: ElementHandle<Element>): Promise<void> {
async #scrollIntoViewIfNeeded(
this: CDPElementHandle<Element>
): Promise<void> {
const error = await this.evaluate(
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
@ -541,10 +385,7 @@ export class ElementHandle<
return {offsetX, offsetY};
}
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
async clickablePoint(offset?: Offset): Promise<Point> {
override async clickablePoint(offset?: Offset): Promise<Point> {
const [result, layoutMetrics] = await Promise.all([
this.client
.send('DOM.getContentQuads', {
@ -615,7 +456,7 @@ export class ElementHandle<
#getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this.remoteObject().objectId,
objectId: this.id,
};
return this.client.send('DOM.getBoxModel', params).catch(error => {
return debugError(error);
@ -649,7 +490,7 @@ export class ElementHandle<
* uses {@link Page.mouse} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
async hover(this: ElementHandle<Element>): Promise<void> {
override async hover(this: CDPElementHandle<Element>): Promise<void> {
await this.#scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.mouse.move(x, y);
@ -660,8 +501,8 @@ export class ElementHandle<
* uses {@link Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
async click(
this: ElementHandle<Element>,
override async click(
this: CDPElementHandle<Element>,
options: ClickOptions = {}
): Promise<void> {
await this.#scrollIntoViewIfNeeded();
@ -672,8 +513,8 @@ export class ElementHandle<
/**
* This method creates and captures a dragevent from the element.
*/
async drag(
this: ElementHandle<Element>,
override async drag(
this: CDPElementHandle<Element>,
target: Point
): Promise<Protocol.Input.DragData> {
assert(
@ -685,11 +526,8 @@ export class ElementHandle<
return await this.#page.mouse.drag(start, target);
}
/**
* This method creates a `dragenter` event on the element.
*/
async dragEnter(
this: ElementHandle<Element>,
override async dragEnter(
this: CDPElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.#scrollIntoViewIfNeeded();
@ -697,11 +535,8 @@ export class ElementHandle<
await this.#page.mouse.dragEnter(target, data);
}
/**
* This method creates a `dragover` event on the element.
*/
async dragOver(
this: ElementHandle<Element>,
override async dragOver(
this: CDPElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.#scrollIntoViewIfNeeded();
@ -709,11 +544,8 @@ export class ElementHandle<
await this.#page.mouse.dragOver(target, data);
}
/**
* This method triggers a drop on the element.
*/
async drop(
this: ElementHandle<Element>,
override async drop(
this: CDPElementHandle<Element>,
data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1}
): Promise<void> {
await this.#scrollIntoViewIfNeeded();
@ -721,12 +553,9 @@ export class ElementHandle<
await this.#page.mouse.drop(destination, data);
}
/**
* This method triggers a dragenter, dragover, and drop on the element.
*/
async dragAndDrop(
this: ElementHandle<Element>,
target: ElementHandle<Node>,
override async dragAndDrop(
this: CDPElementHandle<Element>,
target: CDPElementHandle<Node>,
options?: {delay: number}
): Promise<void> {
await this.#scrollIntoViewIfNeeded();
@ -735,23 +564,7 @@ export class ElementHandle<
await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options);
}
/**
* Triggers a `change` and `input` event once all the provided options have been
* selected. If there's no `<select>` element matching `selector`, the method
* throws an error.
*
* @example
*
* ```ts
* handle.select('blue'); // single selection
* handle.select('red', 'green', 'blue'); // multiple selections
* ```
*
* @param values - Values of options to select. If the `<select>` has the
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
*/
async select(...values: string[]): Promise<string[]> {
override async select(...values: string[]): Promise<string[]> {
for (const value of values) {
assert(
isString(value),
@ -795,18 +608,8 @@ export class ElementHandle<
}, values);
}
/**
* This method expects `elementHandle` to point to an
* {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}.
*
* @param filePaths - Sets the value of the file input to these paths.
* If a path is relative, then it is resolved against the
* {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}.
* Note for locals script connecting to remote chrome environments,
* paths must be absolute.
*/
async uploadFile(
this: ElementHandle<HTMLInputElement>,
override async uploadFile(
this: CDPElementHandle<HTMLInputElement>,
...filePaths: string[]
): Promise<void> {
const isMultiple = await this.evaluate(element => {
@ -837,7 +640,9 @@ export class ElementHandle<
}
});
const {objectId} = this.remoteObject();
const {node} = await this.client.send('DOM.describeNode', {objectId});
const {node} = await this.client.send('DOM.describeNode', {
objectId,
});
const {backendNodeId} = node;
/* The zero-length array is a special case, it seems that
@ -861,21 +666,31 @@ export class ElementHandle<
}
}
/**
* 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.
*/
async tap(this: ElementHandle<Element>): Promise<void> {
override async tap(this: CDPElementHandle<Element>): Promise<void> {
await this.#scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.tap(x, y);
await this.#page.touchscreen.touchStart(x, y);
await this.#page.touchscreen.touchEnd();
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
async focus(): Promise<void> {
override async touchStart(this: CDPElementHandle<Element>): Promise<void> {
await this.#scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.touchStart(x, y);
}
override async touchMove(this: CDPElementHandle<Element>): Promise<void> {
await this.#scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.touchMove(x, y);
}
override async touchEnd(this: CDPElementHandle<Element>): Promise<void> {
await this.#scrollIntoViewIfNeeded();
await this.#page.touchscreen.touchEnd();
}
override async focus(): Promise<void> {
await this.evaluate(element => {
if (!(element instanceof HTMLElement)) {
throw new Error('Cannot focus non-HTMLElement');
@ -884,58 +699,17 @@ export class ElementHandle<
});
}
/**
* Focuses the element, and then sends a `keydown`, `keypress`/`input`, and
* `keyup` event for each character in the text.
*
* To press a special key, like `Control` or `ArrowDown`,
* use {@link ElementHandle.press}.
*
* @example
*
* ```ts
* await elementHandle.type('Hello'); // Types instantly
* await elementHandle.type('World', {delay: 100}); // Types slower, like a user
* ```
*
* @example
* An example of typing into a text field and then submitting the form:
*
* ```ts
* const elementHandle = await page.$('input');
* await elementHandle.type('some text');
* await elementHandle.press('Enter');
* ```
*/
async type(text: string, options?: {delay: number}): Promise<void> {
override async type(text: string, options?: {delay: number}): Promise<void> {
await this.focus();
await this.#page.keyboard.type(text, options);
}
/**
* Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
*
* @remarks
* If `key` is a single character and no modifier keys besides `Shift`
* are being held down, a `keypress`/`input` event will also be generated.
* The `text` option can be specified to force an input event to be generated.
*
* **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift`
* will type the text in upper case.
*
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*/
async press(key: KeyInput, options?: PressOptions): Promise<void> {
override async press(key: KeyInput, options?: PressOptions): Promise<void> {
await this.focus();
await this.#page.keyboard.press(key, options);
}
/**
* This method returns the bounding box of the element (relative to the main frame),
* or `null` if the element is not visible.
*/
async boundingBox(): Promise<BoundingBox | null> {
override async boundingBox(): Promise<BoundingBox | null> {
const result = await this.#getBoxModel();
if (!result) {
@ -952,15 +726,7 @@ export class ElementHandle<
return {x: x + offsetX, y: y + offsetY, width, height};
}
/**
* This method returns boxes of the element, or `null` if the element is not visible.
*
* @remarks
*
* Boxes are represented as an array of points;
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/
async boxModel(): Promise<BoxModel | null> {
override async boxModel(): Promise<BoxModel | null> {
const result = await this.#getBoxModel();
if (!result) {
@ -996,13 +762,8 @@ export class ElementHandle<
};
}
/**
* This method scrolls element into view if needed, and then uses
* {@link Page.screenshot} to take a screenshot of the element.
* If the element is detached from DOM, the method throws an error.
*/
async screenshot(
this: ElementHandle<Element>,
override async screenshot(
this: CDPElementHandle<Element>,
options: ScreenshotOptions = {}
): Promise<string | Buffer> {
let needsViewportReset = false;
@ -1059,11 +820,8 @@ export class ElementHandle<
return imageData;
}
/**
* Resolves to true if the element is visible in the current viewport.
*/
async isIntersectingViewport(
this: ElementHandle<Element>,
override async isIntersectingViewport(
this: CDPElementHandle<Element>,
options?: {
threshold?: number;
}

View file

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js';
import {Viewport} from './PuppeteerViewport.js';
import {Protocol} from 'devtools-protocol';
/**
* @internal

View file

@ -15,10 +15,21 @@
*/
import {Protocol} from 'devtools-protocol';
import type {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import type PuppeteerUtil from '../injected/injected.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {stringifyFunction} from '../util/Function.js';
import {ARIAQueryHandler} from './AriaQueryHandler.js';
import {Binding} from './Binding.js';
import {CDPSession} from './Connection.js';
import {CDPElementHandle} from './ElementHandle.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {CDPJSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {scriptInjector} from './ScriptInjector.js';
import {EvaluateFunc, HandleFor} from './types.js';
import {
createJSHandle,
@ -71,7 +82,7 @@ export class ExecutionContext {
/**
* @internal
*/
_contextName: string;
_contextName?: string;
/**
* @internal
@ -84,8 +95,56 @@ export class ExecutionContext {
this._client = client;
this._world = world;
this._contextId = contextPayload.id;
if (contextPayload.name) {
this._contextName = contextPayload.name;
}
}
#puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>;
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
scriptInjector.inject(script => {
if (this.#puppeteerUtil) {
this.#puppeteerUtil.then(handle => {
handle.dispose();
});
}
this.#puppeteerUtil = Promise.all([
this.#installGlobalBinding(
new Binding(
'__ariaQuerySelector',
ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown
)
),
this.#installGlobalBinding(
new Binding('__ariaQuerySelectorAll', (async (
element: ElementHandle<Node>,
selector: string
): Promise<JSHandle<Node[]>> => {
const results = ARIAQueryHandler.queryAll(element, selector);
return element.executionContext().evaluateHandle((...elements) => {
return elements;
}, ...(await AsyncIterableUtil.collect(results)));
}) as (...args: unknown[]) => unknown)
),
]).then(() => {
return this.evaluateHandle(script) as Promise<JSHandle<PuppeteerUtil>>;
});
}, !this.#puppeteerUtil);
return this.#puppeteerUtil as Promise<JSHandle<PuppeteerUtil>>;
}
async #installGlobalBinding(binding: Binding) {
try {
if (this._world) {
this._world._bindings.set(binding.name, binding);
await this._world._addBindingToContext(this, binding.name);
}
} catch {
// If the binding cannot be added, then either the browser doesn't support
// bindings (e.g. Firefox) or the context is broken. Either breakage is
// okay, so we ignore the error.
}
}
/**
* Evaluates the given function.
@ -250,29 +309,10 @@ export class ExecutionContext {
: createJSHandle(this, remoteObject);
}
let functionText = pageFunction.toString();
try {
new Function('(' + functionText + ')');
} catch (error) {
// This means we might have a function shorthand. Try another
// time prefixing 'function '.
if (functionText.startsWith('async ')) {
functionText =
'async function ' + functionText.substring('async '.length);
} else {
functionText = 'function ' + functionText;
}
try {
new Function('(' + functionText + ')');
} catch (error) {
// We tried hard to serialize, but there's a weird beast here.
throw new Error('Passed function is not well-serializable!');
}
}
let callFunctionOnPromise;
try {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n',
functionDeclaration: `${stringifyFunction(pageFunction)}\n${suffix}\n`,
executionContextId: this._contextId,
arguments: await Promise.all(args.map(convertArgument.bind(this))),
returnByValue,
@ -304,7 +344,7 @@ export class ExecutionContext {
arg: unknown
): Promise<Protocol.Runtime.CallArgument> {
if (arg instanceof LazyArg) {
arg = await arg.get();
arg = await arg.get(this);
}
if (typeof arg === 'bigint') {
// eslint-disable-line valid-typeof
@ -322,7 +362,10 @@ export class ExecutionContext {
if (Object.is(arg, NaN)) {
return {unserializableValue: 'NaN'};
}
const objectHandle = arg && arg instanceof JSHandle ? arg : null;
const objectHandle =
arg && (arg instanceof CDPJSHandle || arg instanceof CDPElementHandle)
? arg
: null;
if (objectHandle) {
if (objectHandle.executionContext() !== this) {
throw new Error(

View file

@ -15,8 +15,9 @@
*/
import {Protocol} from 'devtools-protocol';
import {ElementHandle} from '../api/ElementHandle.js';
import {assert} from '../util/assert.js';
import {ElementHandle} from './ElementHandle.js';
/**
* File choosers let you react to the page requesting for a file.

View file

@ -15,17 +15,19 @@
*/
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js';
import {Target} from './Target.js';
import {TargetFilterCallback} from '../api/Browser.js';
import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {Target} from './Target.js';
import {
TargetFactory,
TargetInterceptor,
TargetManagerEmittedEvents,
TargetManager,
} from './TargetManager.js';
import {EventEmitter} from './EventEmitter.js';
/**
* FirefoxTargetManager implements target management using

View file

@ -15,12 +15,15 @@
*/
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {ElementHandle} from '../api/ElementHandle.js';
import {Page} from '../api/Page.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js';
import {FrameManager} from './FrameManager.js';
import {getQueryHandlerAndSelector} from './GetQueryHandler.js';
import {HTTPResponse} from './HTTPResponse.js';
import {MouseButton} from './Input.js';
import {
@ -29,10 +32,9 @@ import {
WaitForSelectorOptions,
} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {LazyArg} from './LazyArg.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {Page} from '../api/Page.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
import {importFS} from './util.js';
/**
@ -353,6 +355,9 @@ export class Frame {
referrerPolicy,
});
ensureNewDocumentNavigation = !!response.loaderId;
if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') {
return null;
}
return response.errorText
? new Error(`${response.errorText} at ${url}`)
: null;
@ -514,9 +519,10 @@ export class Frame {
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
@ -548,9 +554,10 @@ export class Frame {
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
@ -612,10 +619,9 @@ export class Frame {
selector: Selector,
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, queryHandler} =
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
assert(queryHandler.waitFor, 'Query handler does not support waiting');
return (await queryHandler.waitFor(
return (await QueryHandler.waitFor(
this,
updatedSelector,
options
@ -839,7 +845,9 @@ export class Frame {
await promise;
return script;
},
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
{...options, type, content}
)
);
@ -923,7 +931,9 @@ export class Frame {
await promise;
return element;
},
await this.worlds[PUPPETEER_WORLD].puppeteerUtil,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
options
)
);

View file

@ -15,8 +15,11 @@
*/
import {Protocol} from 'devtools-protocol';
import {Page} from '../api/Page.js';
import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession, isTargetClosedError} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
@ -25,7 +28,6 @@ import {FrameTree} from './FrameTree.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {NetworkManager} from './NetworkManager.js';
import {Page} from '../api/Page.js';
import {Target} from './Target.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {debugError} from './util.js';
@ -174,12 +176,18 @@ export class FrameManager extends EventEmitter {
contextId: number,
session: CDPSession = this.#client
): ExecutionContext {
const key = `${session.id()}:${contextId}`;
const context = this.#contextIdToContext.get(key);
const context = this.getExecutionContextById(contextId, session);
assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId);
return context;
}
getExecutionContextById(
contextId: number,
session: CDPSession = this.#client
): ExecutionContext | undefined {
return this.#contextIdToContext.get(`${session.id()}:${contextId}`);
}
page(): Page {
return this.#page;
}

View file

@ -18,6 +18,7 @@ import {
createDeferredPromise,
DeferredPromise,
} from '../util/DeferredPromise.js';
import type {Frame} from './Frame.js';
/**

View file

@ -0,0 +1,70 @@
/**
* 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 {ARIAQueryHandler} from './AriaQueryHandler.js';
import {customQueryHandlers} from './CustomQueryHandler.js';
import {PierceQueryHandler} from './PierceQueryHandler.js';
import {PQueryHandler} from './PQueryHandler.js';
import type {QueryHandler} from './QueryHandler.js';
import {TextQueryHandler} from './TextQueryHandler.js';
import {XPathQueryHandler} from './XPathQueryHandler.js';
export const BUILTIN_QUERY_HANDLERS = Object.freeze({
aria: ARIAQueryHandler,
pierce: PierceQueryHandler,
xpath: XPathQueryHandler,
text: TextQueryHandler,
});
const QUERY_SEPARATORS = ['=', '/'];
/**
* @internal
*/
export function getQueryHandlerByName(
name: string
): typeof QueryHandler | undefined {
if (name in BUILTIN_QUERY_HANDLERS) {
return BUILTIN_QUERY_HANDLERS[name as 'aria'];
}
return customQueryHandlers.get(name);
}
/**
* @internal
*/
export function getQueryHandlerAndSelector(selector: string): {
updatedSelector: string;
QueryHandler: typeof QueryHandler;
} {
for (const handlerMap of [
customQueryHandlers.names().map(name => {
return [name, customQueryHandlers.get(name)!] as const;
}),
Object.entries(BUILTIN_QUERY_HANDLERS),
]) {
for (const [name, QueryHandler] of handlerMap) {
for (const separator of QUERY_SEPARATORS) {
const prefix = `${name}${separator}`;
if (selector.startsWith(prefix)) {
selector = selector.slice(prefix.length);
return {updatedSelector: selector, QueryHandler};
}
}
}
}
return {updatedSelector: selector, QueryHandler: PQueryHandler};
}

View file

@ -15,12 +15,14 @@
*/
import {Protocol} from 'devtools-protocol';
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {assert} from '../util/assert.js';
import {ProtocolError} from './Errors.js';
import {EventEmitter} from './EventEmitter.js';
import {Frame} from './Frame.js';
import {debugError, isString} from './util.js';
import {HTTPResponse} from './HTTPResponse.js';
import {debugError, isString} from './util.js';
/**
* @public

View file

@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {ProtocolError} from './Errors.js';
import {EventEmitter} from './EventEmitter.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
import {SecurityDetails} from './SecurityDetails.js';
import {Protocol} from 'devtools-protocol';
import {ProtocolError} from './Errors.js';
/**
* @public

View file

@ -0,0 +1,81 @@
/**
* 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 {JSHandle} from '../api/JSHandle.js';
import {AwaitableIterable, HandleFor} from './types.js';
const DEFAULT_BATCH_SIZE = 20;
/**
* This will transpose an iterator JSHandle into a fast, Puppeteer-side iterator
* of JSHandles.
*
* @param size - The number of elements to transpose. This should be something
* reasonable.
*/
async function* fastTransposeIteratorHandle<T>(
iterator: JSHandle<AwaitableIterator<T>>,
size = DEFAULT_BATCH_SIZE
) {
const array = await iterator.evaluateHandle(async (iterator, size) => {
const results = [];
while (results.length < size) {
const result = await iterator.next();
if (result.done) {
break;
}
results.push(result.value);
}
return results;
}, size);
const properties = (await array.getProperties()) as Map<string, HandleFor<T>>;
await array.dispose();
yield* properties.values();
return properties.size === 0;
}
/**
* This will transpose an iterator JSHandle in batches based on the default size
* of {@link fastTransposeIteratorHandle}.
*/
async function* transposeIteratorHandle<T>(
iterator: JSHandle<AwaitableIterator<T>>
) {
try {
while (!(yield* fastTransposeIteratorHandle(iterator))) {}
} finally {
await iterator.dispose();
}
}
type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>;
/**
* @internal
*/
export async function* transposeIterableHandle<T>(
handle: JSHandle<AwaitableIterable<T>>
): AsyncIterableIterator<HandleFor<T>> {
yield* transposeIteratorHandle(
await handle.evaluateHandle(iterable => {
return (async function* () {
yield* iterable;
})();
})
);
}

View file

@ -14,11 +14,13 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import {Point} from '../api/ElementHandle.js';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {_keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout.js';
import {Protocol} from 'devtools-protocol';
import {Point} from './JSHandle.js';
type KeyDescription = Required<
Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'>
@ -670,12 +672,40 @@ export class Touchscreen {
* @param y - Vertical position of the tap.
*/
async tap(x: number, y: number): Promise<void> {
await this.touchStart(x, y);
await this.touchEnd();
}
/**
* Dispatches a `touchstart` event.
* @param x - Horizontal position of the tap.
* @param y - Vertical position of the tap.
*/
async touchStart(x: number, y: number): Promise<void> {
const touchPoints = [{x: Math.round(x), y: Math.round(y)}];
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints,
modifiers: this.#keyboard._modifiers,
});
}
/**
* Dispatches a `touchMove` event.
* @param x - Horizontal position of the move.
* @param y - Vertical position of the move.
*/
async touchMove(x: number, y: number): Promise<void> {
const movePoints = [{x: Math.round(x), y: Math.round(y)}];
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: movePoints,
modifiers: this.#keyboard._modifiers,
});
}
/**
* Dispatches a `touchend` event.
*/
async touchEnd(): Promise<void> {
await this.#client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [],

View file

@ -15,26 +15,31 @@
*/
import {Protocol} from 'devtools-protocol';
import {source as injectedSource} from '../generated/injected.js';
import type {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {assert} from '../util/assert.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {Binding} from './Binding.js';
import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor, InnerLazyParams, NodeFor} from './types.js';
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
import {
BindingPayload,
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from './types.js';
import {addPageBinding, createJSHandle, debugError} from './util.js';
import {TaskManager, WaitTask} from './WaitTask.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import type PuppeteerUtil from '../injected/injected.js';
import type {ElementHandle} from './ElementHandle.js';
/**
* @public
@ -91,29 +96,20 @@ export class IsolatedWorld {
#detached = false;
// Set of bindings that have been registered in the current context.
#ctxBindings = new Set<string>();
#contextBindings = new Set<string>();
// Contains mapping from functions that should be bound to Puppeteer functions.
#boundFunctions = new Map<string, Function>();
#bindings = new Map<string, Binding>();
#taskManager = new TaskManager();
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
return this.#puppeteerUtil;
}
get taskManager(): TaskManager {
return this.#taskManager;
}
get _boundFunctions(): Map<string, Function> {
return this.#boundFunctions;
get _bindings(): Map<string, Binding> {
return this.#bindings;
}
static #bindingIdentifier = (name: string, contextId: number) => {
return `${name}_${contextId}`;
};
constructor(frame: Frame) {
// Keep own reference to client because it might differ from the FrameManager's
// client for OOP iframes.
@ -139,31 +135,13 @@ export class IsolatedWorld {
clearContext(): void {
this.#document = undefined;
this.#puppeteerUtil = createDeferredPromise();
this.#context = createDeferredPromise();
}
setContext(context: ExecutionContext): void {
this.#injectPuppeteerUtil(context);
this.#ctxBindings.clear();
this.#contextBindings.clear();
this.#context.resolve(context);
}
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
try {
this.#puppeteerUtil.resolve(
(await context.evaluateHandle(
`(() => {
const module = {};
${injectedSource}
return module.exports.default;
})()`
)) as JSHandle<PuppeteerUtil>
);
this.#taskManager.rerunAll();
} catch (error: unknown) {
debugError(error);
}
}
hasContext(): boolean {
@ -245,9 +223,10 @@ export class IsolatedWorld {
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
@ -260,9 +239,10 @@ export class IsolatedWorld {
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
@ -371,35 +351,25 @@ export class IsolatedWorld {
// If multiple waitFor are set up asynchronously, we need to wait for the
// first one to set up the binding in the page before running the others.
#settingUpBinding: Promise<void> | null = null;
#mutex = new Mutex();
async _addBindingToContext(
context: ExecutionContext,
name: string
): Promise<void> {
// Previous operation added the binding so we are done.
if (
this.#ctxBindings.has(
IsolatedWorld.#bindingIdentifier(name, context._contextId)
)
) {
if (this.#contextBindings.has(name)) {
return;
}
// Wait for other operation to finish
if (this.#settingUpBinding) {
await this.#settingUpBinding;
return this._addBindingToContext(context, name);
}
const bind = async (name: string) => {
const expression = pageBindingInitString('internal', name);
await this.#mutex.acquire();
try {
// TODO: In theory, it would be enough to call this just once
await context._client.send('Runtime.addBinding', {
name,
executionContextName: context._contextName,
});
await context.evaluate(expression);
await context.evaluate(addPageBinding, 'internal', name);
this.#contextBindings.add(name);
} catch (error) {
// We could have tried to evaluate in a context which was already
// destroyed. This happens, for example, if the page is navigated while
@ -416,26 +386,15 @@ export class IsolatedWorld {
}
debugError(error);
return;
} finally {
this.#mutex.release();
}
this.#ctxBindings.add(
IsolatedWorld.#bindingIdentifier(name, context._contextId)
);
};
this.#settingUpBinding = bind(name);
await this.#settingUpBinding;
this.#settingUpBinding = null;
}
#onBindingCalled = async (
event: Protocol.Runtime.BindingCalledEvent
): Promise<void> => {
let payload: {type: string; name: string; seq: number; args: unknown[]};
if (!this.hasContext()) {
return;
}
const context = await this.executionContext();
let payload: BindingPayload;
try {
payload = JSON.parse(event.payload);
} catch {
@ -443,108 +402,23 @@ export class IsolatedWorld {
// called before our wrapper was initialized.
return;
}
const {type, name, seq, args} = payload;
if (
type !== 'internal' ||
!this.#ctxBindings.has(
IsolatedWorld.#bindingIdentifier(name, context._contextId)
)
) {
const {type, name, seq, args, isTrivial} = payload;
if (type !== 'internal') {
return;
}
if (context._contextId !== event.executionContextId) {
if (!this.#contextBindings.has(name)) {
return;
}
try {
const fn = this._boundFunctions.get(name);
if (!fn) {
throw new Error(`Bound function $name is not found`);
}
const result = await fn(...args);
await context.evaluate(
(name: string, seq: number, result: unknown) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = self[name].callbacks;
callbacks.get(seq).resolve(result);
callbacks.delete(seq);
},
name,
seq,
result
);
} catch (error) {
// The WaitTask may already have been resolved by timing out, or the
// execution context may have been destroyed.
// In both caes, the promises above are rejected with a protocol error.
// We can safely ignores these, as the WaitTask is re-installed in
// the next execution context if needed.
if ((error as Error).message.includes('Protocol error')) {
const context = await this.#context;
if (event.executionContextId !== context._contextId) {
return;
}
debugError(error);
}
const binding = this._bindings.get(name);
await binding?.run(context, seq, args, isTrivial);
};
async _waitForSelectorInPage(
queryOne: Function,
root: ElementHandle<Node> | undefined,
selector: string,
options: WaitForSelectorOptions,
bindings = new Map<string, (...args: never[]) => unknown>()
): Promise<JSHandle<unknown> | null> {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this.#timeoutSettings.timeout(),
} = options;
try {
const handle = await this.waitForFunction(
async (PuppeteerUtil, query, selector, root, visible) => {
if (!PuppeteerUtil) {
return;
}
const node = (await PuppeteerUtil.createFunction(query)(
root || document,
selector,
PuppeteerUtil
)) as Node | null;
return PuppeteerUtil.checkVisibility(node, visible);
},
{
bindings,
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
root,
timeout,
},
new LazyArg(async () => {
try {
// In case CDP fails.
return await this.puppeteerUtil;
} catch {
return undefined;
}
}),
queryOne.toString(),
selector,
root,
waitForVisible ? true : waitForHidden ? false : undefined
);
const elementHandle = handle.asElement();
if (!elementHandle) {
await handle.dispose();
return null;
}
return elementHandle;
} catch (error) {
if (!isErrorLike(error)) {
throw error;
}
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
throw error;
}
}
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
@ -556,14 +430,12 @@ export class IsolatedWorld {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
bindings?: Map<string, (...args: never[]) => unknown>;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.#timeoutSettings.timeout(),
bindings,
root,
} = options;
if (typeof polling === 'number' && polling < 0) {
@ -572,7 +444,6 @@ export class IsolatedWorld {
const waitTask = new WaitTask(
this,
{
bindings,
polling,
root,
timeout,
@ -603,20 +474,57 @@ export class IsolatedWorld {
}
async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
const executionContext = await this.executionContext();
const context = await this.executionContext();
assert(
handle.executionContext() !== executionContext,
handle.executionContext() !== context,
'Cannot adopt handle that already belongs to this execution context'
);
const nodeInfo = await this.#client.send('DOM.describeNode', {
objectId: handle.remoteObject().objectId,
objectId: handle.id,
});
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
}
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
const result = await this.adoptHandle(handle);
const context = await this.executionContext();
if (handle.executionContext() === context) {
return handle;
}
const info = await this.#client.send('DOM.describeNode', {
objectId: handle.remoteObject().objectId,
});
const newHandle = (await this.adoptBackendNode(
info.node.backendNodeId
)) as T;
await handle.dispose();
return result;
return newHandle;
}
}
class Mutex {
#locked = false;
#acquirers: Array<() => void> = [];
// This is FIFO.
acquire(): Promise<void> {
if (!this.#locked) {
this.#locked = true;
return Promise.resolve();
}
let resolve!: () => void;
const promise = new Promise<void>(res => {
resolve = res;
});
this.#acquirers.push(resolve);
return promise;
}
release(): void {
const resolve = this.#acquirers.shift();
if (!resolve) {
this.#locked = false;
return;
}
resolve();
}
}

View file

@ -15,64 +15,22 @@
*/
import {Protocol} from 'devtools-protocol';
import {JSHandle} from '../api/JSHandle.js';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import type {ElementHandle} from './ElementHandle.js';
import type {CDPElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js';
import {MouseButton} from './Input.js';
import {EvaluateFunc, HandleFor, HandleOr} from './types.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from './types.js';
import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js';
declare const __JSHandleSymbol: unique symbol;
/**
* @public
* @internal
*/
export interface BoxModel {
content: Point[];
padding: Point[];
border: Point[];
margin: Point[];
width: number;
height: number;
}
/**
* @public
*/
export interface BoundingBox extends Point {
/**
* the width of the element in pixels.
*/
width: number;
/**
* the height of the element in pixels.
*/
height: number;
}
/**
* Represents a reference to a JavaScript object. Instances can be created using
* {@link Page.evaluateHandle}.
*
* Handles prevent the referenced JavaScript object from being garbage-collected
* unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles
* are auto-disposed when their associated frame is navigated away or the parent
* context gets destroyed.
*
* Handles can be used as arguments for any evaluation function such as
* {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}.
* They are resolved to their referenced object.
*
* @example
*
* ```ts
* const windowHandle = await page.evaluateHandle(() => window);
* ```
*
* @public
*/
export class JSHandle<T = unknown> {
export class CDPJSHandle<T = unknown> extends JSHandle<T> {
/**
* Used for nominally typing {@link JSHandle}.
*/
@ -82,73 +40,50 @@ export class JSHandle<T = unknown> {
#context: ExecutionContext;
#remoteObject: Protocol.Runtime.RemoteObject;
/**
* @internal
*/
get client(): CDPSession {
return this.#context._client;
}
/**
* @internal
*/
get disposed(): boolean {
override get disposed(): boolean {
return this.#disposed;
}
/**
* @internal
*/
constructor(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
) {
super();
this.#context = context;
this.#remoteObject = remoteObject;
}
/**
* @internal
*/
executionContext(): ExecutionContext {
override executionContext(): ExecutionContext {
return this.#context;
}
override get client(): CDPSession {
return this.#context._client;
}
/**
* Evaluates the given function with the current handle as its first argument.
*
* @see {@link ExecutionContext.evaluate} for more details.
*/
async evaluate<
override async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc<
[this, ...Params]
>
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): // @ts-expect-error Circularity here is okay because we only need the return
// type which doesn't use `this`.
Promise<Awaited<ReturnType<Func>>> {
): Promise<Awaited<ReturnType<Func>>> {
return await this.executionContext().evaluate(pageFunction, this, ...args);
}
/**
* Evaluates the given function with the current handle as its first argument.
*
* @see {@link ExecutionContext.evaluateHandle} for more details.
*/
async evaluateHandle<
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc<
[this, ...Params]
>
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): // @ts-expect-error Circularity here is okay because we only need the return
// type which doesn't use `this`.
Promise<HandleFor<Awaited<ReturnType<Func>>>> {
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.executionContext().evaluateHandle(
pageFunction,
this,
@ -156,14 +91,11 @@ export class JSHandle<T = unknown> {
);
}
/**
* Fetches a single property from the referenced object.
*/
async getProperty<K extends keyof T>(
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
async getProperty<K extends keyof T>(
override async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return this.evaluateHandle((object, propertyName) => {
@ -171,25 +103,7 @@ export class JSHandle<T = unknown> {
}, propertyName);
}
/**
* Gets a map of handles representing the properties of the current handle.
*
* @example
*
* ```ts
* const listHandle = await page.evaluateHandle(() => document.body.children);
* const properties = await listHandle.getProperties();
* const children = [];
* for (const property of properties.values()) {
* const element = property.asElement();
* if (element) {
* children.push(element);
* }
* }
* children; // holds elementHandles to all children of document.body
* ```
*/
async getProperties(): Promise<Map<string, JSHandle>> {
override async getProperties(): Promise<Map<string, JSHandle>> {
assert(this.#remoteObject.objectId);
// We use Runtime.getProperties rather than iterative building because the
// iterative approach might create a distorted snapshot.
@ -207,15 +121,7 @@ export class JSHandle<T = unknown> {
return result;
}
/**
* @returns A vanilla object representing the serializable portions of the
* referenced object.
* @throws Throws if the object cannot be serialized due to circularity.
*
* @remarks
* If the object has a `toJSON` function, it **will not** be called.
*/
async jsonValue(): Promise<T> {
override async jsonValue(): Promise<T> {
if (!this.#remoteObject.objectId) {
return valueFromRemoteObject(this.#remoteObject);
}
@ -232,14 +138,11 @@ export class JSHandle<T = unknown> {
* @returns Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
asElement(): ElementHandle<Node> | null {
override asElement(): CDPElementHandle<Node> | null {
return null;
}
/**
* Releases the object referenced by the handle for garbage collection.
*/
async dispose(): Promise<void> {
override async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
@ -247,13 +150,7 @@ export class JSHandle<T = unknown> {
await releaseObject(this.client, this.#remoteObject);
}
/**
* Returns a string representation of the JSHandle.
*
* @remarks
* Useful during debugging.
*/
toString(): string {
override toString(): string {
if (!this.#remoteObject.objectId) {
return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject);
}
@ -261,72 +158,11 @@ export class JSHandle<T = unknown> {
return 'JSHandle@' + type;
}
/**
* Provides access to the
* [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject)
* backing this handle.
*/
remoteObject(): Protocol.Runtime.RemoteObject {
override get id(): string | undefined {
return this.#remoteObject.objectId;
}
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject;
}
}
/**
* @public
*/
export interface Offset {
/**
* x-offset for the clickable point relative to the top-left corner of the border box.
*/
x: number;
/**
* y-offset for the clickable point relative to the top-left corner of the border box.
*/
y: number;
}
/**
* @public
*/
export interface ClickOptions {
/**
* Time to wait between `mousedown` and `mouseup` in milliseconds.
*
* @defaultValue 0
*/
delay?: number;
/**
* @defaultValue 'left'
*/
button?: MouseButton;
/**
* @defaultValue 1
*/
clickCount?: number;
/**
* Offset for the clickable point relative to the top-left corner of the border box.
*/
offset?: Offset;
}
/**
* @public
*/
export interface PressOptions {
/**
* Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0.
*/
delay?: number;
/**
* If specified, generates an input event with this text.
*/
text?: string;
}
/**
* @public
*/
export interface Point {
x: number;
y: number;
}

View file

@ -14,16 +14,26 @@
* limitations under the License.
*/
import {ExecutionContext} from './ExecutionContext.js';
/**
* @internal
*/
export class LazyArg<T> {
#get: () => Promise<T>;
constructor(get: () => Promise<T>) {
static create = <T>(
get: (context: ExecutionContext) => Promise<T> | T
): T => {
// We don't want to introduce LazyArg to the type system, otherwise we would
// have to make it public.
return new LazyArg(get) as unknown as T;
};
#get: (context: ExecutionContext) => Promise<T> | T;
private constructor(get: (context: ExecutionContext) => Promise<T> | T) {
this.#get = get;
}
get(): Promise<T> {
return this.#get();
async get(context: ExecutionContext): Promise<T> {
return this.#get(context);
}
}

View file

@ -15,22 +15,23 @@
*/
import {assert} from '../util/assert.js';
import {
DeferredPromise,
createDeferredPromise,
} from '../util/DeferredPromise.js';
import {CDPSessionEmittedEvents} from './Connection.js';
import {TimeoutError} from './Errors.js';
import {Frame} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {NetworkManagerEmittedEvents} from './NetworkManager.js';
import {
addEventListener,
PuppeteerEventListener,
removeEventListeners,
} from './util.js';
import {
DeferredPromise,
createDeferredPromise,
} from '../util/DeferredPromise.js';
import {TimeoutError} from './Errors.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {NetworkManagerEmittedEvents} from './NetworkManager.js';
import {CDPSessionEmittedEvents} from './Connection.js';
/**
* @public
*/

View file

@ -15,6 +15,7 @@
*/
import {Protocol} from 'devtools-protocol';
import {HTTPRequest} from './HTTPRequest.js';
/**

View file

@ -15,16 +15,18 @@
*/
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
import {DeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
import {debugError, isString} from './util.js';
import {DeferredPromise} from '../util/DeferredPromise.js';
import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
import {CDPSession} from './Connection.js';
/**
* @public

View file

@ -14,6 +14,7 @@
* limitations under the License.
*/
import NodeWebSocket from 'ws';
import {ConnectionTransport} from '../common/ConnectionTransport.js';
import {packageVersion} from '../generated/version.js';

View file

@ -0,0 +1,37 @@
/**
* 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 {QueryHandler, QuerySelector, QuerySelectorAll} from './QueryHandler.js';
/**
* @internal
*/
export class PQueryHandler extends QueryHandler {
static override querySelectorAll: QuerySelectorAll = (
element,
selector,
{pQuerySelectorAll}
) => {
return pQuerySelectorAll(element, selector);
};
static override querySelector: QuerySelector = (
element,
selector,
{pQuerySelector}
) => {
return pQuerySelector(element, selector);
};
}

View file

@ -14,10 +14,14 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import type {Readable} from 'stream';
import {Protocol} from 'devtools-protocol';
import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js';
import {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import {
GeolocationOptions,
MediaFeature,
@ -35,7 +39,9 @@ import {
DeferredPromise,
} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {Accessibility} from './Accessibility.js';
import {Binding} from './Binding.js';
import {
CDPSession,
CDPSessionEmittedEvents,
@ -44,7 +50,6 @@ import {
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js';
import {Dialog} from './Dialog.js';
import {ElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js';
import {FileChooser} from './FileChooser.js';
import {
@ -59,7 +64,6 @@ import {HTTPResponse} from './HTTPResponse.js';
import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {MAIN_WORLD} from './IsolatedWorlds.js';
import {JSHandle} from './JSHandle.js';
import {
Credentials,
NetworkConditions,
@ -72,7 +76,13 @@ import {TargetManagerEmittedEvents} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {Tracing} from './Tracing.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {
BindingPayload,
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
NodeFor,
} from './types.js';
import {
createJSHandle,
debugError,
@ -83,9 +93,6 @@ import {
importFS,
isNumber,
isString,
pageBindingDeliverErrorString,
pageBindingDeliverErrorValueString,
pageBindingDeliverResultString,
pageBindingInitString,
releaseObject,
valueFromRemoteObject,
@ -140,7 +147,7 @@ export class CDPPage extends Page {
#frameManager: FrameManager;
#emulationManager: EmulationManager;
#tracing: Tracing;
#pageBindings = new Map<string, Function>();
#bindings = new Map<string, Binding>();
#coverage: Coverage;
#javascriptEnabled = true;
#viewport: Viewport | null;
@ -521,13 +528,12 @@ export class CDPPage extends Page {
): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext();
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert(
remoteObject.objectId,
prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await context._client.send('Runtime.queryObjects', {
prototypeObjectId: remoteObject.objectId,
prototypeObjectId: prototypeHandle.id,
});
return createJSHandle(context, response.objects) as HandleFor<Prototype[]>;
}
@ -535,9 +541,10 @@ export class CDPPage extends Page {
override async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[ElementHandle<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>
>(
selector: Selector,
pageFunction: Func | string,
@ -549,9 +556,10 @@ export class CDPPage extends Page {
override async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]>
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>
>(
selector: Selector,
pageFunction: Func | string,
@ -646,23 +654,29 @@ export class CDPPage extends Page {
name: string,
pptrFunction: Function | {default: Function}
): Promise<void> {
if (this.#pageBindings.has(name)) {
if (this.#bindings.has(name)) {
throw new Error(
`Failed to add page binding with name ${name}: window['${name}'] already exists!`
);
}
let exposedFunction: Function;
let binding: Binding;
switch (typeof pptrFunction) {
case 'function':
exposedFunction = pptrFunction;
binding = new Binding(
name,
pptrFunction as (...args: unknown[]) => unknown
);
break;
default:
exposedFunction = pptrFunction.default;
binding = new Binding(
name,
pptrFunction.default as (...args: unknown[]) => unknown
);
break;
}
this.#pageBindings.set(name, exposedFunction);
this.#bindings.set(name, binding);
const expression = pageBindingInitString('exposedFun', name);
await this.#client.send('Runtime.addBinding', {name: name});
@ -747,10 +761,20 @@ export class CDPPage extends Page {
// @see https://github.com/puppeteer/puppeteer/issues/3865
return;
}
const context = this.#frameManager.executionContextById(
const context = this.#frameManager.getExecutionContextById(
event.executionContextId,
this.#client
);
if (!context) {
debugError(
new Error(
`ExecutionContext not found for a console message: ${JSON.stringify(
event
)}`
)
);
return;
}
const values = event.args.map(arg => {
return createJSHandle(context, arg);
});
@ -760,7 +784,7 @@ export class CDPPage extends Page {
async #onBindingCalled(
event: Protocol.Runtime.BindingCalledEvent
): Promise<void> {
let payload: {type: string; name: string; seq: number; args: unknown[]};
let payload: BindingPayload;
try {
payload = JSON.parse(event.payload);
} catch {
@ -768,34 +792,21 @@ export class CDPPage extends Page {
// called before our wrapper was initialized.
return;
}
const {type, name, seq, args} = payload;
if (type !== 'exposedFun' || !this.#pageBindings.has(name)) {
const {type, name, seq, args, isTrivial} = payload;
if (type !== 'exposedFun') {
return;
}
let expression = null;
try {
const pageBinding = this.#pageBindings.get(name);
assert(pageBinding);
const result = await pageBinding(...args);
expression = pageBindingDeliverResultString(name, seq, result);
} catch (error) {
if (isErrorLike(error)) {
expression = pageBindingDeliverErrorString(
name,
seq,
error.message,
error.stack
);
} else {
expression = pageBindingDeliverErrorValueString(name, seq, error);
}
}
const context = this.#frameManager.executionContextById(
event.executionContextId,
this.#client
.send('Runtime.evaluate', {
expression,
contextId: event.executionContextId,
})
.catch(debugError);
);
if (!context) {
return;
}
const binding = this.#bindings.get(name);
await binding?.run(context, seq, args, isTrivial);
}
#addConsoleMessage(
@ -811,7 +822,7 @@ export class CDPPage extends Page {
}
const textTokens = [];
for (const arg of args) {
const remoteObject = arg.remoteObject();
const remoteObject = arg.remoteObject() as Protocol.Runtime.RemoteObject;
if (remoteObject.objectId) {
textTokens.push(arg.toString());
} else {
@ -1621,11 +1632,7 @@ export class CDPPage extends Page {
override waitForXPath(
xpath: string,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
options: WaitForSelectorOptions = {}
): Promise<ElementHandle<Node> | null> {
return this.mainFrame().waitForXPath(xpath, options);
}

View file

@ -0,0 +1,39 @@
/**
* 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 PuppeteerUtil from '../injected/injected.js';
import {QueryHandler} from './QueryHandler.js';
/**
* @internal
*/
export class PierceQueryHandler extends QueryHandler {
static override querySelector = (
element: Node,
selector: string,
{pierceQuerySelector}: PuppeteerUtil
): Node | null => {
return pierceQuerySelector(element, selector);
};
static override querySelectorAll = (
element: Node,
selector: string,
{pierceQuerySelectorAll}: PuppeteerUtil
): Iterable<Node> => {
return pierceQuerySelectorAll(element, selector);
};
}

View file

@ -13,19 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Browser} from '../api/Browser.js';
import {
BrowserConnectOptions,
_connectToCDPBrowser,
} from './BrowserConnector.js';
import {ConnectionTransport} from './ConnectionTransport.js';
import {
clearCustomQueryHandlers,
CustomQueryHandler,
customQueryHandlerNames,
registerCustomQueryHandler,
unregisterCustomQueryHandler,
} from './QueryHandler.js';
import {CustomQueryHandler, customQueryHandlers} from './CustomQueryHandler.js';
/**
* Settings that are common to the Puppeteer class, regardless of environment.
@ -57,9 +53,18 @@ export interface ConnectOptions extends BrowserConnectOptions {
* instance of {@link PuppeteerNode} when you import or require `puppeteer`.
* That class extends `Puppeteer`, so has all the methods documented below as
* well as all that are defined on {@link PuppeteerNode}.
*
* @public
*/
export class Puppeteer {
/**
* Operations for {@link CustomQueryHandler | custom query handlers}. See
* {@link CustomQueryHandlerRegistry}.
*
* @internal
*/
static customQueryHandlers = customQueryHandlers;
/**
* Registers a {@link CustomQueryHandler | custom query handler}.
*
@ -86,28 +91,28 @@ export class Puppeteer {
name: string,
queryHandler: CustomQueryHandler
): void {
return registerCustomQueryHandler(name, queryHandler);
return this.customQueryHandlers.register(name, queryHandler);
}
/**
* Unregisters a custom query handler for a given name.
*/
static unregisterCustomQueryHandler(name: string): void {
return unregisterCustomQueryHandler(name);
return this.customQueryHandlers.unregister(name);
}
/**
* Gets the names of all custom query handlers.
*/
static customQueryHandlerNames(): string[] {
return customQueryHandlerNames();
return this.customQueryHandlers.names();
}
/**
* Unregisters all custom query handlers.
*/
static clearCustomQueryHandlers(): void {
return clearCustomQueryHandlers();
return this.customQueryHandlers.clear();
}
/**

View file

@ -1,5 +1,5 @@
/**
* Copyright 2020 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,305 +14,202 @@
* limitations under the License.
*/
import PuppeteerUtil from '../injected/injected.js';
import {ariaHandler} from './AriaQueryHandler.js';
import {ElementHandle} from './ElementHandle.js';
import {Frame} from './Frame.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {ElementHandle} from '../api/ElementHandle.js';
import type PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import type {Frame} from './Frame.js';
import {transposeIterableHandle} from './HandleIterator.js';
import type {WaitForSelectorOptions} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
/**
* @public
*/
export interface CustomQueryHandler {
/**
* @returns A {@link Node} matching the given `selector` from {@link node}.
*/
queryOne?: (node: Node, selector: string) => Node | null;
/**
* @returns Some {@link Node}s matching the given `selector` from {@link node}.
*/
queryAll?: (node: Node, selector: string) => Node[];
}
import {LazyArg} from './LazyArg.js';
import type {Awaitable, AwaitableIterable} from './types.js';
/**
* @internal
*/
export interface InternalQueryHandler {
/**
* @returns A {@link Node} matching the given `selector` from {@link node}.
*/
queryOne?: (
export type QuerySelectorAll = (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil
) => Node | null;
/**
* @returns Some {@link Node}s matching the given `selector` from {@link node}.
*/
queryAll?: (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil
) => Node[];
}
) => AwaitableIterable<Node>;
/**
* @internal
*/
export interface PuppeteerQueryHandler {
/**
* Queries for a single node given a selector and {@link ElementHandle}.
*
* Akin to {@link Window.prototype.querySelector}.
export type QuerySelector = (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil
) => Awaitable<Node | null>;
/**
* @internal
*/
queryOne?: (
element: ElementHandle<Node>,
selector: string
) => Promise<ElementHandle<Node> | null>;
export class QueryHandler {
// Either one of these may be implemented, but at least one must be.
static querySelectorAll?: QuerySelectorAll;
static querySelector?: QuerySelector;
static get _querySelector(): QuerySelector {
if (this.querySelector) {
return this.querySelector;
}
if (!this.querySelectorAll) {
throw new Error('Cannot create default `querySelector`.');
}
return (this.querySelector = interpolateFunction(
async (node, selector, PuppeteerUtil) => {
const querySelectorAll: QuerySelectorAll =
PLACEHOLDER('querySelectorAll');
const results = querySelectorAll(node, selector, PuppeteerUtil);
for await (const result of results) {
return result;
}
return null;
},
{
querySelectorAll: stringifyFunction(this.querySelectorAll),
}
));
}
static get _querySelectorAll(): QuerySelectorAll {
if (this.querySelectorAll) {
return this.querySelectorAll;
}
if (!this.querySelector) {
throw new Error('Cannot create default `querySelectorAll`.');
}
return (this.querySelectorAll = interpolateFunction(
async function* (node, selector, PuppeteerUtil) {
const querySelector: QuerySelector = PLACEHOLDER('querySelector');
const result = await querySelector(node, selector, PuppeteerUtil);
if (result) {
yield result;
}
},
{
querySelector: stringifyFunction(this.querySelector),
}
));
}
/**
* Queries for multiple nodes given a selector and {@link ElementHandle}.
*
* Akin to {@link Window.prototype.querySelectorAll}.
* Akin to {@link Document.prototype.querySelectorAll}.
*/
queryAll?: (
static async *queryAll(
element: ElementHandle<Node>,
selector: string
) => Promise<Array<ElementHandle<Node>>>;
): AwaitableIterable<ElementHandle<Node>> {
const world = element.executionContext()._world;
assert(world);
const handle = await element.evaluateHandle(
this._querySelectorAll,
selector,
LazyArg.create(context => {
return context.puppeteerUtil;
})
);
yield* transposeIterableHandle(handle);
}
/**
* Waits until a single node appears for a given selector and
* {@link ElementHandle}.
* Queries for a single node given a selector and {@link ElementHandle}.
*
* Akin to {@link Document.prototype.querySelector}.
*/
waitFor?: (
elementOrFrame: ElementHandle<Node> | Frame,
selector: string,
options: WaitForSelectorOptions
) => Promise<ElementHandle<Node> | null>;
}
function createPuppeteerQueryHandler(
handler: InternalQueryHandler
): PuppeteerQueryHandler {
const internalHandler: PuppeteerQueryHandler = {};
if (handler.queryOne) {
const queryOne = handler.queryOne;
internalHandler.queryOne = async (element, selector) => {
const jsHandle = await element.evaluateHandle(
queryOne,
static async queryOne(
element: ElementHandle<Node>,
selector: string
): Promise<ElementHandle<Node> | null> {
const world = element.executionContext()._world;
assert(world);
const result = await element.evaluateHandle(
this._querySelector,
selector,
await element.executionContext()._world!.puppeteerUtil
LazyArg.create(context => {
return context.puppeteerUtil;
})
);
const elementHandle = jsHandle.asElement();
if (elementHandle) {
return elementHandle;
}
await jsHandle.dispose();
return null;
};
internalHandler.waitFor = async (elementOrFrame, selector, options) => {
let frame: Frame;
let element: ElementHandle<Node> | undefined;
if (elementOrFrame instanceof Frame) {
frame = elementOrFrame;
} else {
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(
elementOrFrame
);
}
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
queryOne,
element,
selector,
options
);
if (element) {
await element.dispose();
}
if (!result) {
return null;
}
if (!(result instanceof ElementHandle)) {
await result.dispose();
return null;
}
return frame.worlds[MAIN_WORLD].transferHandle(result);
};
}
if (handler.queryAll) {
const queryAll = handler.queryAll;
internalHandler.queryAll = async (element, selector) => {
const jsHandle = await element.evaluateHandle(
queryAll,
selector,
await element.executionContext()._world!.puppeteerUtil
);
const properties = await jsHandle.getProperties();
await jsHandle.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
if (elementHandle) {
result.push(elementHandle);
}
}
return result;
};
}
return internalHandler;
}
/**
* Waits until a single node appears for a given selector and
* {@link ElementHandle}.
*
* This will always query the handle in the Puppeteer world and migrate the
* result to the main world.
*/
static async waitFor(
elementOrFrame: ElementHandle<Node> | Frame,
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle<Node> | null> {
let frame: Frame;
let element: ElementHandle<Node> | undefined;
if (!(elementOrFrame instanceof ElementHandle)) {
frame = elementOrFrame;
} else {
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
}
const defaultHandler = createPuppeteerQueryHandler({
queryOne: (element, selector) => {
if (!('querySelector' in element)) {
throw new Error(
`Could not invoke \`querySelector\` on node of type ${element.nodeName}.`
const {visible = false, hidden = false, timeout} = options;
try {
const handle = await frame.worlds[PUPPETEER_WORLD].waitForFunction(
async (PuppeteerUtil, query, selector, root, visible) => {
const querySelector = PuppeteerUtil.createFunction(
query
) as QuerySelector;
const node = await querySelector(
root ?? document,
selector,
PuppeteerUtil
);
}
return (
element as unknown as {querySelector(selector: string): Element}
).querySelector(selector);
return PuppeteerUtil.checkVisibility(node, visible);
},
queryAll: (element, selector) => {
if (!('querySelectorAll' in element)) {
throw new Error(
`Could not invoke \`querySelectorAll\` on node of type ${element.nodeName}.`
{
polling: visible || hidden ? 'raf' : 'mutation',
root: element,
timeout,
},
LazyArg.create(context => {
return context.puppeteerUtil;
}),
stringifyFunction(this._querySelector),
selector,
element,
visible ? true : hidden ? false : undefined
);
if (!(handle instanceof ElementHandle)) {
await handle.dispose();
return null;
}
return frame.worlds[MAIN_WORLD].transferHandle(handle);
} catch (error) {
if (!isErrorLike(error)) {
throw error;
}
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
throw error;
} finally {
if (element) {
await element.dispose();
}
}
return [
...(
element as unknown as {
querySelectorAll(selector: string): NodeList;
}
).querySelectorAll(selector),
];
},
});
const pierceHandler = createPuppeteerQueryHandler({
queryOne: (element, selector, {pierceQuerySelector}) => {
return pierceQuerySelector(element, selector);
},
queryAll: (element, selector, {pierceQuerySelectorAll}) => {
return pierceQuerySelectorAll(element, selector);
},
});
const xpathHandler = createPuppeteerQueryHandler({
queryOne: (element, selector, {xpathQuerySelector}) => {
return xpathQuerySelector(element, selector);
},
queryAll: (element, selector, {xpathQuerySelectorAll}) => {
return xpathQuerySelectorAll(element, selector);
},
});
const textQueryHandler = createPuppeteerQueryHandler({
queryOne: (element, selector, {textQuerySelector}) => {
return textQuerySelector(element, selector);
},
queryAll: (element, selector, {textQuerySelectorAll}) => {
return textQuerySelectorAll(element, selector);
},
});
interface RegisteredQueryHandler {
handler: PuppeteerQueryHandler;
transformSelector?: (selector: string) => string;
}
const INTERNAL_QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>([
['aria', {handler: ariaHandler}],
['pierce', {handler: pierceHandler}],
['xpath', {handler: xpathHandler}],
['text', {handler: textQueryHandler}],
]);
const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>();
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.registerCustomQueryHandler}
*
* @public
*/
export function registerCustomQueryHandler(
name: string,
handler: CustomQueryHandler
): void {
if (INTERNAL_QUERY_HANDLERS.has(name)) {
throw new Error(`A query handler named "${name}" already exists`);
}
if (QUERY_HANDLERS.has(name)) {
throw new Error(`A custom query handler named "${name}" already exists`);
}
const isValidName = /^[a-zA-Z]+$/.test(name);
if (!isValidName) {
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
}
QUERY_HANDLERS.set(name, {handler: createPuppeteerQueryHandler(handler)});
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.unregisterCustomQueryHandler}
*
* @public
*/
export function unregisterCustomQueryHandler(name: string): void {
QUERY_HANDLERS.delete(name);
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.customQueryHandlerNames}
*
* @public
*/
export function customQueryHandlerNames(): string[] {
return [...QUERY_HANDLERS.keys()];
}
/**
* @deprecated Import {@link Puppeteer} and use the static method
* {@link Puppeteer.clearCustomQueryHandlers}
*
* @public
*/
export function clearCustomQueryHandlers(): void {
QUERY_HANDLERS.clear();
}
const CUSTOM_QUERY_SEPARATORS = ['=', '/'];
/**
* @internal
*/
export function getQueryHandlerAndSelector(selector: string): {
updatedSelector: string;
queryHandler: PuppeteerQueryHandler;
} {
for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) {
for (const [
name,
{handler: queryHandler, transformSelector},
] of handlerMap) {
for (const separator of CUSTOM_QUERY_SEPARATORS) {
const prefix = `${name}${separator}`;
if (selector.startsWith(prefix)) {
selector = selector.slice(prefix.length);
if (transformSelector) {
selector = transformSelector(selector);
}
return {updatedSelector: selector, queryHandler};
}
}
}
}
return {updatedSelector: selector, queryHandler: defaultHandler};
}

View file

@ -0,0 +1,49 @@
import {source as injectedSource} from '../generated/injected.js';
class ScriptInjector {
#updated = false;
#amendments = new Set<string>();
// Appends a statement of the form `(PuppeteerUtil) => {...}`.
append(statement: string): void {
this.#update(() => {
this.#amendments.add(statement);
});
}
pop(statement: string): void {
this.#update(() => {
this.#amendments.delete(statement);
});
}
inject(inject: (script: string) => void, force = false) {
if (this.#updated || force) {
inject(this.#get());
}
this.#updated = false;
}
#update(callback: () => void): void {
callback();
this.#updated = true;
}
#get(): string {
return `(() => {
const module = {};
${injectedSource}
${[...this.#amendments]
.map(statement => {
return `(${statement})(module.exports.default);`;
})
.join('')}
return module.exports.default;
})()`;
}
}
/**
* @internal
*/
export const scriptInjector = new ScriptInjector();

View file

@ -14,16 +14,18 @@
* limitations under the License.
*/
import {Page, PageEmittedEvents} from '../api/Page.js';
import {WebWorker} from './WebWorker.js';
import {CDPSession} from './Connection.js';
import {Protocol} from 'devtools-protocol';
import type {Browser, IsPageTargetCallback} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js';
import {Viewport} from './PuppeteerViewport.js';
import {Protocol} from 'devtools-protocol';
import {TaskQueue} from './TaskQueue.js';
import {TargetManager} from './TargetManager.js';
import {Page, PageEmittedEvents} from '../api/Page.js';
import {CDPSession} from './Connection.js';
import {CDPPage} from './Page.js';
import {Viewport} from './PuppeteerViewport.js';
import {TargetManager} from './TargetManager.js';
import {TaskQueue} from './TaskQueue.js';
import {WebWorker} from './WebWorker.js';
/**
* Target represents a

View file

@ -15,6 +15,7 @@
*/
import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js';
import {EventEmitter} from './EventEmitter.js';
import {Target} from './Target.js';

View file

@ -0,0 +1,30 @@
/**
* 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 {QueryHandler, QuerySelectorAll} from './QueryHandler.js';
/**
* @internal
*/
export class TextQueryHandler extends QueryHandler {
static override querySelectorAll: QuerySelectorAll = (
element,
selector,
{textQuerySelectorAll}
) => {
return textQuerySelectorAll(element, selector);
};
}

View file

@ -14,9 +14,10 @@
* limitations under the License.
*/
import {assert} from '../util/assert.js';
import {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js';
import {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js';
/**
* @public

View file

@ -14,19 +14,21 @@
* limitations under the License.
*/
import {ElementHandle} from '../api/ElementHandle.js';
import {JSHandle} from '../api/JSHandle.js';
import type {Poller} from '../injected/Poller.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {ElementHandle} from './ElementHandle.js';
import {stringifyFunction} from '../util/Function.js';
import {TimeoutError} from './Errors.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {HandleFor} from './types.js';
/**
* @internal
*/
export interface WaitTaskOptions {
bindings?: Map<string, (...args: never[]) => unknown>;
polling: 'raf' | 'mutation' | number;
root?: ElementHandle<Node>;
timeout: number;
@ -37,7 +39,6 @@ export interface WaitTaskOptions {
*/
export class WaitTask<T = unknown> {
#world: IsolatedWorld;
#bindings: Map<string, (...args: never[]) => unknown>;
#polling: 'raf' | 'mutation' | number;
#root?: ElementHandle<Node>;
@ -57,7 +58,6 @@ export class WaitTask<T = unknown> {
...args: unknown[]
) {
this.#world = world;
this.#bindings = options.bindings ?? new Map();
this.#polling = options.polling;
this.#root = options.root;
@ -66,7 +66,7 @@ export class WaitTask<T = unknown> {
this.#fn = `() => {return (${fn});}`;
break;
default:
this.#fn = fn.toString();
this.#fn = stringifyFunction(fn);
break;
}
this.#args = args;
@ -81,12 +81,6 @@ export class WaitTask<T = unknown> {
}, options.timeout);
}
if (this.#bindings.size !== 0) {
for (const [name, fn] of this.#bindings) {
this.#world._boundFunctions.set(name, fn);
}
}
this.rerun();
}
@ -96,15 +90,6 @@ export class WaitTask<T = unknown> {
async rerun(): Promise<void> {
try {
if (this.#bindings.size !== 0) {
const context = await this.#world.executionContext();
await Promise.all(
[...this.#bindings].map(async ([name]) => {
return await this.#world._addBindingToContext(context, name);
})
);
}
switch (this.#polling) {
case 'raf':
this.#poller = await this.#world.evaluateHandle(
@ -114,7 +99,9 @@ export class WaitTask<T = unknown> {
return fun(...args) as Promise<T>;
});
},
await this.#world.puppeteerUtil,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
this.#fn,
...this.#args
);
@ -127,7 +114,9 @@ export class WaitTask<T = unknown> {
return fun(...args) as Promise<T>;
}, root || document);
},
await this.#world.puppeteerUtil,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
this.#root,
this.#fn,
...this.#args
@ -141,7 +130,9 @@ export class WaitTask<T = unknown> {
return fun(...args) as Promise<T>;
}, ms);
},
await this.#world.puppeteerUtil,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
this.#polling,
this.#fn,
...this.#args

View file

@ -14,21 +14,23 @@
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js';
import {ConsoleMessageType} from './ConsoleMessage.js';
import {EvaluateFunc, HandleFor} from './types.js';
import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js';
import {JSHandle} from './JSHandle.js';
import {CDPJSHandle} from './JSHandle.js';
import {EvaluateFunc, HandleFor} from './types.js';
import {debugError} from './util.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
/**
* @internal
*/
export type ConsoleAPICalledCallback = (
eventType: ConsoleMessageType,
handles: JSHandle[],
handles: CDPJSHandle[],
trace: Protocol.Runtime.StackTrace
) => void;
@ -93,7 +95,7 @@ export class WebWorker extends EventEmitter {
return consoleAPICalled(
event.type,
event.args.map((object: Protocol.Runtime.RemoteObject) => {
return new JSHandle(context, object);
return new CDPJSHandle(context, object);
}),
event.stackTrace
);

View file

@ -0,0 +1,30 @@
/**
* 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 {QueryHandler, QuerySelectorAll} from './QueryHandler.js';
/**
* @internal
*/
export class XPathQueryHandler extends QueryHandler {
static override querySelectorAll: QuerySelectorAll = (
element,
selector,
{xpathQuerySelectorAll}
) => {
return xpathQuerySelectorAll(element, selector);
};
}

View file

@ -1,9 +1,12 @@
import {CDPSession, Connection as CDPPPtrConnection} from '../Connection.js';
import {Connection as BidiPPtrConnection} from './Connection.js';
import {Bidi, BidiMapper} from '../../../third_party/chromium-bidi/index.js';
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js';
import * 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 {Handler} from '../EventEmitter.js';
import {Connection as BidiPPtrConnection} from './Connection.js';
type CdpEvents = {
[Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0];
};
@ -99,9 +102,9 @@ class CDPClientAdapter<
this.#client.on('*', this.#forwardMessage as Handler<any>);
}
#forwardMessage = (
method: keyof ProtocolMapping.Events,
event: ProtocolMapping.Events[keyof ProtocolMapping.Events]
#forwardMessage = <T extends keyof CdpEvents>(
method: T,
event: CdpEvents[T]
) => {
this.emit(method, event);
};

View file

@ -14,15 +14,17 @@
* limitations under the License.
*/
import {ChildProcess} from 'child_process';
import {
Browser as BrowserBase,
BrowserCloseCallback,
BrowserContextOptions,
} from '../../api/Browser.js';
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Connection} from './Connection.js';
import {ChildProcess} from 'child_process';
import {BrowserContext} from './BrowserContext.js';
import {Connection} from './Connection.js';
/**
* @internal
@ -34,7 +36,7 @@ export class Browser extends BrowserBase {
static async create(opts: Options): Promise<Browser> {
// TODO: await until the connection is established.
try {
(await opts.connection.send('session.new', {})) as {sessionId: string};
await opts.connection.send('session.new', {});
} catch {}
return new Browser(opts);
}

View file

@ -16,6 +16,7 @@
import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js';
import {Page as PageBase} from '../../api/Page.js';
import {Connection} from './Connection.js';
import {Page} from './Page.js';
@ -31,10 +32,10 @@ export class BrowserContext extends BrowserContextBase {
}
override async newPage(): Promise<PageBase> {
const result = (await this.#connection.send('browsingContext.create', {
const response = await this.#connection.send('browsingContext.create', {
type: 'tab',
})) as {context: string};
return new Page(this.#connection, result.context);
});
return new Page(this.#connection, response.result.context);
}
override async close(): Promise<void> {}

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