Bug 1797744 - [puppeteer] Sync vendored puppeteer to v19.6.0. r=webdriver-reviewers,jdescottes,whimboo

Differential Revision: https://phabricator.services.mozilla.com/D167407
This commit is contained in:
Alexandra Borovova 2023-01-27 16:50:45 +00:00
parent 51f82934e3
commit 7e919228b0
319 changed files with 48062 additions and 8097 deletions

View file

@ -113,18 +113,19 @@ _OPT\.OBJ/
^remote/test/puppeteer/.*\.tsbuildinfo ^remote/test/puppeteer/.*\.tsbuildinfo
^remote/test/puppeteer/\.github ^remote/test/puppeteer/\.github
^remote/test/puppeteer/\.husky ^remote/test/puppeteer/\.husky
^remote/test/puppeteer/\.local-chromium/
^remote/test/puppeteer/\.local-firefox/
^remote/test/puppeteer/coverage/ ^remote/test/puppeteer/coverage/
^remote/test/puppeteer/.devcontainer/
^remote/test/puppeteer/docker/ ^remote/test/puppeteer/docker/
^remote/test/puppeteer/docs/puppeteer-core\.api\.json
^remote/test/puppeteer/docs/puppeteer\.api\.json ^remote/test/puppeteer/docs/puppeteer\.api\.json
^remote/test/puppeteer/experimental/ ^remote/test/puppeteer/experimental/
^remote/test/puppeteer/lib/ ^remote/test/puppeteer/lib/
^remote/test/puppeteer/node_modules/ ^remote/test/puppeteer/node_modules/
^remote/test/puppeteer/package-lock\.json ^remote/test/puppeteer/package-lock\.json
^remote/test/puppeteer/puppeteer.*\.tgz ^remote/test/puppeteer/packages/ng-schematics/test/build
^remote/test/puppeteer/src/generated ^remote/test/puppeteer/src/generated
^remote/test/puppeteer/test/build ^remote/test/puppeteer/test/build
^remote/test/puppeteer/test/installation/puppeteer.*\.tgz
^remote/test/puppeteer/test/output-firefox ^remote/test/puppeteer/test/output-firefox
^remote/test/puppeteer/test/output-chromium ^remote/test/puppeteer/test/output-chromium
^remote/test/puppeteer/testserver/lib/ ^remote/test/puppeteer/testserver/lib/

14
remote/.gitignore vendored
View file

@ -1,20 +1,20 @@
test/puppeteer/**/.wireit
test/puppeteer/**/*.tsbuildinfo test/puppeteer/**/*.tsbuildinfo
test/puppeteer/**/lib
test/puppeteer/.github test/puppeteer/.github
test/puppeteer/.husky test/puppeteer/.husky
test/puppeteer/.local-chromium/
test/puppeteer/.local-firefox/
test/puppeteer/coverage/ test/puppeteer/coverage/
test/puppeteer/.devcontainer/
test/puppeteer/docker/ test/puppeteer/docker/
test/puppeteer/docs/puppeteer-core.api.json
test/puppeteer/docs/puppeteer.api.json test/puppeteer/docs/puppeteer.api.json
test/puppeteer/experimental/ test/puppeteer/experimental/
test/puppeteer/lib/
test/puppeteer/node_modules/ test/puppeteer/node_modules/
test/puppeteer/package-lock.json test/puppeteer/package-lock.json
test/puppeteer/puppeteer*.tgz test/puppeteer/packages/ng-schematics/test/build
test/puppeteer/test/installation/puppeteer*.tgz
test/puppeteer/src/generated test/puppeteer/src/generated
test/puppeteer/test/build test/puppeteer/test/**/build
test/puppeteer/test/output-firefox test/puppeteer/test/output-firefox
test/puppeteer/test/output-chromium test/puppeteer/test/output-chromium
test/puppeteer/testserver/lib/
test/puppeteer/utils/mochaRunner/lib/
test/puppeteer/website test/puppeteer/website

View file

@ -11,7 +11,7 @@ process interspersed with some tips.
1. Clone the Puppeteer git repository and checkout the release tag you want 1. Clone the Puppeteer git repository and checkout the release tag you want
to vendor into mozilla-central. to vendor into mozilla-central.
% git checkout tags/v10.0 -b sync-v10.0 % git checkout tags/puppeteer-v10.0 -b sync-v10.0
2. Apply any recent changes in `remote/test/puppeteer` to the Puppeteer branch 2. Apply any recent changes in `remote/test/puppeteer` to the Puppeteer branch
created above. created above.

View file

@ -299,7 +299,20 @@ class MochaOutputHandler(object):
None, None,
) )
if expected_item_for_file is None: if expected_item_for_file is None:
expected = ["PASS"] # if there is no expectation data for the file,
# try to find data for all tests.
expected_item_for_all_tests = next(
(
expectation
for expectation in list(self.expected)
if expectation["testIdPattern"] == ""
),
None,
)
if expected_item_for_all_tests is None:
expected = ["PASS"]
else:
expected = expected_item_for_all_tests["expectations"]
else: else:
expected = expected_item_for_file["expectations"] expected = expected_item_for_file["expectations"]
else: else:
@ -392,9 +405,6 @@ class PuppeteerRunner(MozbuildObject):
before invoking npm. Overrides default preferences. before invoking npm. Overrides default preferences.
`enable_webrender`: `enable_webrender`:
Boolean to indicate whether to enable WebRender compositor in Gecko. Boolean to indicate whether to enable WebRender compositor in Gecko.
`subset`
Indicates only a subset of tests are being run, so we should
skip the check for missing results
""" """
setup() setup()
@ -426,18 +436,26 @@ class PuppeteerRunner(MozbuildObject):
"--no-coverage", "--no-coverage",
] ]
env["HEADLESS"] = str(params.get("headless", False)) env["HEADLESS"] = str(params.get("headless", False))
test_command = "test:" + product
if product == "firefox": if product == "firefox":
env["BINARY"] = binary env["BINARY"] = binary
env["PUPPETEER_PRODUCT"] = "firefox" env["PUPPETEER_PRODUCT"] = "firefox"
env["MOZ_WEBRENDER"] = "%d" % params.get("enable_webrender", False) env["MOZ_WEBRENDER"] = "%d" % params.get("enable_webrender", False)
test_command = "test:firefox"
elif env["HEADLESS"] == "False":
test_command = "test:chrome:headful"
else: else:
test_command = "test:chrome:headless" env["PUPPETEER_CACHE_DIR"] = os.path.join(
self.topobjdir,
"_tests",
"remote",
"test",
"puppeteer",
".cache",
)
if env["HEADLESS"] == "True":
test_command = test_command + ":headless"
else:
test_command = test_command + ":headful"
command = ["run", test_command, "--"] + mocha_options command = ["run", test_command, "--"] + mocha_options
@ -464,14 +482,16 @@ class PuppeteerRunner(MozbuildObject):
expected_data = json.load(f) expected_data = json.load(f)
else: else:
expected_data = [] expected_data = []
# Filter expectation data for the selected browser,
# headless or headful mode, and the operating system.
expected_platform = platform.uname().system.lower() expected_platform = platform.uname().system.lower()
if expected_platform == "windows": if expected_platform == "windows":
expected_platform = "win32" expected_platform = "win32"
# Filter expectation data for the selected browser,
# headless or headful mode, and the operating system.
expectations = filter( expectations = filter(
lambda el: product in el["parameters"] lambda el: product in el["parameters"]
and "webDriverBiDi" not in el["parameters"]
and ( and (
(env["HEADLESS"] == "False" and "headless" not in el["parameters"]) (env["HEADLESS"] == "False" and "headless" not in el["parameters"])
or "headful" not in el["parameters"] or "headful" not in el["parameters"]
@ -559,13 +579,6 @@ def create_parser_puppeteer():
"debug level messages with -v, trace messages with -vv," "debug level messages with -v, trace messages with -vv,"
"and to not truncate long trace messages with -vvv", "and to not truncate long trace messages with -vvv",
) )
p.add_argument(
"--subset",
action="store_true",
default=False,
help="Indicate that only a subset of the tests are running, "
"so checks for missing tests should be skipped",
)
p.add_argument("tests", nargs="*") p.add_argument("tests", nargs="*")
mozlog.commandline.add_logging_group(p) mozlog.commandline.add_logging_group(p)
return p return p
@ -597,7 +610,6 @@ def puppeteer_test(
verbosity=0, verbosity=0,
tests=None, tests=None,
product="firefox", product="firefox",
subset=False,
**kwargs, **kwargs,
): ):
@ -656,7 +668,6 @@ def puppeteer_test(
"extra_prefs": prefs, "extra_prefs": prefs,
"product": product, "product": product,
"extra_launcher_options": options, "extra_launcher_options": options,
"subset": subset,
} }
puppeteer = command_context._spawn(PuppeteerRunner) puppeteer = command_context._spawn(PuppeteerRunner)
try: try:
@ -674,25 +685,31 @@ def install_puppeteer(command_context, product, ci):
env = {"HUSKY": "0"} env = {"HUSKY": "0"}
puppeteer_dir = os.path.join("remote", "test", "puppeteer") puppeteer_dir = os.path.join("remote", "test", "puppeteer")
puppeteer_dir_full_path = os.path.join(command_context.topsrcdir, puppeteer_dir)
puppeteer_test_dir = os.path.join(puppeteer_dir, "test")
if product != "chrome": if product == "chrome":
env["PUPPETEER_CACHE_DIR"] = os.path.join(
command_context.topobjdir, "_tests", puppeteer_dir, ".cache"
)
else:
env["PUPPETEER_SKIP_DOWNLOAD"] = "1" env["PUPPETEER_SKIP_DOWNLOAD"] = "1"
if not ci: if not ci:
npm( npm(
"run", "run",
"clean", "clean",
cwd=os.path.join(command_context.topsrcdir, puppeteer_dir), cwd=puppeteer_dir_full_path,
env=env, env=env,
exit_on_fail=False, exit_on_fail=False,
) )
command = "ci" if ci else "install" command = "ci" if ci else "install"
npm(command, cwd=os.path.join(command_context.topsrcdir, puppeteer_dir), env=env) npm(command, cwd=puppeteer_dir_full_path, env=env)
npm( npm(
"run", "run",
"build:dev", "build",
cwd=os.path.join(command_context.topsrcdir, puppeteer_dir), cwd=os.path.join(command_context.topsrcdir, puppeteer_test_dir),
env=env, env=env,
) )

View file

@ -8,17 +8,17 @@ lib/
# Generated files # Generated files
**/*.tsbuildinfo **/*.tsbuildinfo
puppeteer.api.json *.api.json
puppeteer*.tgz *.tgz
yarn.lock yarn.lock
.docusaurus/ .docusaurus/
.cache-loader .cache-loader
.local-chromium/
.local-firefox/
test/output-*/ test/output-*/
.dev_profile* .dev_profile*
coverage/ coverage/
src/generated generated/
.eslintcache
/.cache/
# IDE Artifacts # IDE Artifacts
.vscode .vscode
@ -35,8 +35,10 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# Wireit
.wireit
## [END] Keep in sync with .gitignore ## [END] Keep in sync with .gitignore
# ESLint ignores. # ESLint ignores.
assets/ assets/
vendor/ third_party/

View file

@ -103,7 +103,7 @@ module.exports = {
{ {
name: 'mitt', name: 'mitt',
message: message:
'Import Mitt from the vendored location: vendor/mitt/src/index.js', 'Import `mitt` from the vendored location: third_party/mitt/index.js',
}, },
], ],
}, },

View file

@ -8,20 +8,21 @@ lib/
# Generated files # Generated files
**/*.tsbuildinfo **/*.tsbuildinfo
puppeteer.api.json *.api.json
puppeteer*.tgz *.tgz
yarn.lock yarn.lock
.docusaurus/ .docusaurus/
.cache-loader .cache-loader
.local-chromium/
.local-firefox/
test/output-*/ test/output-*/
.dev_profile* .dev_profile*
coverage/ coverage/
src/generated generated/
.eslintcache
/.cache/
# IDE Artifacts # IDE Artifacts
.vscode .vscode/*
!.vscode/extensions.json
.devcontainer .devcontainer
# Misc # Misc
@ -34,14 +35,14 @@ src/generated
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# Wireit
.wireit
## [END] Keep in sync with .gitignore ## [END] Keep in sync with .gitignore
# Prettier-only ignores. # Prettier-only ignores.
assets/
CHANGELOG.md CHANGELOG.md
package-lock.json package-lock.json
package.json
test/assets/ test/assets/
vendor/ docs/api
docs/
versioned_*/ versioned_*/

View file

@ -1 +1,7 @@
module.exports = require('gts/.prettierrc.json'); /**
* @type {import('prettier').Config}
*/
module.exports = {
...require('gts/.prettierrc.json'),
// proseWrap: 'always', // Uncomment this while working on Markdown documents. MAKE SURE TO COMMENT THIS BEFORE RUNNING CHECKS/FORMATS OR EVERYTHING WILL BE MODIFIED.
};

View file

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

View file

@ -1,32 +1,35 @@
# Puppeteer # Puppeteer
<!-- [START badges] --> [![Build status](https://github.com/puppeteer/puppeteer/workflows/CI/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI)
[![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer)
[![Build status](https://github.com/puppeteer/puppeteer/workflows/CI/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer)
<!-- [END badges] -->
<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/> <img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/>
###### [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting) #### [Guides](https://pptr.dev/category/guides) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting)
> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. > Puppeteer is a Node.js library which provides a high-level API to control
> Chrome/Chromium over the
> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/).
> Puppeteer runs in
> [headless](https://developers.google.com/web/updates/2017/04/headless-chrome)
> mode by default, but can be configured to run in full (non-headless)
> Chrome/Chromium.
<!-- [START usecases] --> #### What can I do?
##### What can I do? Most things that you can do manually in the browser can be done using Puppeteer!
Here are a few examples to get you started:
Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started:
- Generate screenshots and PDFs of pages. - Generate screenshots and PDFs of pages.
- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). - Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e.
"SSR" (Server-Side Rendering)).
- Automate form submission, UI testing, keyboard input, etc. - Automate form submission, UI testing, keyboard input, etc.
- Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. - Create an automated testing environment using the latest JavaScript and
- Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues. browser features.
- Capture a
[timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference)
of your site to help diagnose performance issues.
- Test Chrome Extensions. - Test Chrome Extensions.
<!-- [END usecases] -->
<!-- [START getstarted] -->
## Getting Started ## Getting Started
@ -36,240 +39,138 @@ To use Puppeteer in your project, run:
```bash ```bash
npm i puppeteer npm i puppeteer
# or "yarn add puppeteer" # or `yarn add puppeteer`
# or `pnpm i puppeteer`
``` ```
When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API (customizable through [Environment Variables](#environment-variables)). For a version of Puppeteer purely for connection, see [`puppeteer-core`](#puppeteer-core). 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
[`puppeteer-core`](#puppeteer-core).
#### Environment Variables #### Configuration
Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. Puppeteer uses several defaults that can be customized through configuration
If Puppeteer doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config). files.
- `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run the browser. For example, to change the default cache directory Puppeteer uses to install
- `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. browsers, you can add a `.puppeteerrc.cjs` (or `puppeteer.config.cjs`) at the
- `PUPPETEER_TMP_DIR` - defines the directory to be used by Puppeteer for creating temporary files. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir). root of your application with the contents
- `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`.
- `PUPPETEER_DOWNLOAD_PATH` - overwrite the path for the downloads folder. Defaults to `<root>/.local-chromium`, where `<root>` is Puppeteer's package root.
- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) on how executable path is inferred.
- `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch).
- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This can also be used during installation to fetch the recommended browser binary. Setting `product` programmatically in [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) supersedes this environment variable. The product is exposed in [`puppeteer.product`](https://pptr.dev/api/puppeteer.product)
- `PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM` — specify Puppeteer download Chromium for Apple M1. On Apple M1 devices Puppeteer by default downloads the version for Intel's processor which runs via Rosetta. It works without any problems, however, with this option, you should get more efficient resource usage (CPU and RAM) that could lead to a faster execution time.
:::danger ```js
const {join} = require('path');
Puppeteer is only [guaranteed to work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. /**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
```
::: After adding the configuration file, you will need to remove and reinstall
`puppeteer` for it to take effect.
:::caution See the [configuration guide](https://pptr.dev/guides/configuration) for more
information.
`PUPPETEER_*` env variables are not accounted for in [`puppeteer-core`](#puppeteer-core). #### `puppeteer-core`
:::
#### puppeteer-core
Every release since v1.7.0 we publish two packages: Every release since v1.7.0 we publish two packages:
- [`puppeteer`](https://www.npmjs.com/package/puppeteer) - [`puppeteer`](https://www.npmjs.com/package/puppeteer)
- [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) - [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core)
`puppeteer` is a _product_ for browser automation. When installed, it downloads a version of `puppeteer` is a _product_ for browser automation. When installed, it downloads
Chromium, which it then drives using `puppeteer-core`. Being an end-user product, `puppeteer` supports a bunch of convenient `PUPPETEER_*` env variables to tweak its behavior. a version of Chromium, which it then drives using `puppeteer-core`. Being an
end-user product, `puppeteer` automates several workflows using reasonable
defaults [that can be customized](https://pptr.dev/guides/configuration).
`puppeteer-core` is a _library_ to help drive anything that supports DevTools protocol. `puppeteer-core` doesn't download Chromium when installed. Being a library, `puppeteer-core` is fully driven `puppeteer-core` is a _library_ to help drive anything that supports DevTools
through its programmatic interface and disregards all the `PUPPETEER_*` env variables. protocol. Being a library, `puppeteer-core` is fully driven through its
programmatic interface implying no defaults are assumed and `puppeteer-core`
will not download Chromium when installed.
To sum up, the only differences between `puppeteer-core` and `puppeteer` are: You should use `puppeteer-core` if you are
[connecting to a remote browser](https://pptr.dev/api/puppeteer.puppeteer.connect)
or [managing browsers yourself](https://pptr.dev/api/puppeteer.browserfetcher).
If you are managing browsers yourself, you will need to call
[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with
an an explicit
[`executablePath`](https://pptr.dev/api/puppeteer.launchoptions.executablepath)
(or [`channel`](https://pptr.dev/api/puppeteer.launchoptions.channel) if it's
installed in a standard location).
- `puppeteer-core` doesn't automatically download Chromium when installed. When using `puppeteer-core`, remember to change the import:
- `puppeteer-core` ignores all `PUPPETEER_*` env variables.
In most cases, you'll be fine using the `puppeteer` package.
However, you should use `puppeteer-core` if:
- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `puppeteer-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space.
- you're bundling Puppeteer to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary.
- you're building a set of tools where `puppeteer-core` is one of the ingredients and you want to postpone `install.js` script execution until Chromium is about to be used.
When using `puppeteer-core`, remember to change the _include_ line:
```ts ```ts
const puppeteer = require('puppeteer-core'); import puppeteer from 'puppeteer-core';
``` ```
You will then need to call [`puppeteer.connect`](https://pptr.dev/api/puppeteer.puppeteer.connect) or [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with an explicit `executablePath` or `channel` option.
### Usage ### Usage
Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. Puppeteer follows the latest
[maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of
Node.
Puppeteer will be familiar to people using other browser testing frameworks. You create an instance Puppeteer will be familiar to people using other browser testing frameworks. You
of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://pptr.dev/api). [launch](https://pptr.dev/api/puppeteer.puppeteernode.launch)/[connect](https://pptr.dev/api/puppeteer.puppeteernode.connect)
a [browser](https://pptr.dev/api/puppeteer.browser),
[create](https://pptr.dev/api/puppeteer.browser.newpage) some
[pages](https://pptr.dev/api/puppeteer.page), and then manipulate them with
[Puppeteer's API](https://pptr.dev/api).
**Example** - navigating to https://example.com and saving a screenshot as _example.png_: For more in-depth usage, check our [guides](https://pptr.dev/category/guides)
and [examples](https://github.com/puppeteer/puppeteer/tree/main/examples).
Save file as **example.js** #### Example
The following example searches [developer.chrome.com](https://developer.chrome.com/) for blog posts with text "automate beyond recorder", click on the first result and print the full title of the blog post.
```ts ```ts
const puppeteer = require('puppeteer'); import puppeteer from 'puppeteer';
(async () => { (async () => {
const browser = await puppeteer.launch(); const browser = await puppeteer.launch();
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});
await browser.close(); await page.goto('https://developer.chrome.com/');
})();
```
Execute script on the command line // Set screen size
await page.setViewport({width: 1080, height: 1024});
```bash // Type into search box
node example.js await page.type('.search-box__input', 'automate beyond recorder');
```
Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://pptr.dev/api/puppeteer.page.setviewport). // Wait and click on first result
const searchResultSelector = '.search-box__link';
await page.waitForSelector(searchResultSelector);
await page.click(searchResultSelector);
**Example** - create a PDF. // Localte the full title with a unique string
const textSelector = await page.waitForSelector(
Save file as **hn.js** 'text/Customize and automate'
```ts
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {
waitUntil: 'networkidle2',
});
await page.pdf({path: 'hn.pdf', format: 'a4'});
await browser.close();
})();
```
Execute script on the command line
```bash
node hn.js
```
See [`Page.pdf`](https://pptr.dev/api/puppeteer.page.pdf) for more information about creating pdfs.
**Example** - evaluate script in the context of the page
Save file as **get-dimensions.js**
```ts
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Get the "viewport" of the page, as reported by the page.
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio,
};
});
console.log('Dimensions:', dimensions);
await browser.close();
})();
```
Execute script on the command line
```bash
node get-dimensions.js
```
See [`Page.evaluate`](https://pptr.dev/api/puppeteer.page.evaluate) and related methods like [`Page.evaluateOnNewDocument`](https://pptr.dev/api/puppeteer.page.evaluateOnNewDocument) and [`Page.exposeFunction`](https://pptr.dev/api/puppeteer.page.exposeFunction).
<!-- [END getstarted] -->
### Running in Docker
Puppeteer offers a Docker image that includes Chromium along with the required dependencies and a pre-installed Puppeteer version. The image is available via the [GitHub Container Registry](https://github.com/puppeteer/puppeteer/pkgs/container/puppeteer). The latest image is tagged as `latest` and other tags match Puppeteer versions. For example,
```sh
docker pull ghcr.io/puppeteer/puppeteer:latest # pulls the latest
docker pull ghcr.io/puppeteer/puppeteer:16.1.0 # pulls the image that contains Puppeteer v16.1.0
```
The image is meant for running the browser in the sandbox mode and therefore, running the image requires the `SYS_ADMIN` capability. For example,
```sh
docker run -i --init --cap-add=SYS_ADMIN --rm ghcr.io/puppeteer/puppeteer:latest node -e "`cat docker/test/smoke-test.js`"
```
Replace the path to [`smoke-test.js`](https://raw.githubusercontent.com/puppeteer/puppeteer/main/docker/test/smoke-test.js) with a path to your script.
The script can import or require the `puppeteer` module because it's pre-installed inside the image.
Currently, the image includes the LTS version of Node.js. If you need to build an image based on a different base image, you can use our [`Dockerfile`](https://github.com/puppeteer/puppeteer/blob/main/docker/Dockerfile) as the starting point.
### Working with Chrome Extensions
Puppeteer can be used for testing Chrome Extensions.
:::caution
Extensions in Chrome / Chromium currently only work in non-headless mode and experimental Chrome headless mode.
:::
The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of an extension whose source is located in `./my-extension`:
```ts
const puppeteer = require('puppeteer');
(async () => {
const pathToExtension = require('path').join(__dirname, 'my-extension');
const browser = await puppeteer.launch({
headless: 'chrome',
args: [
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
],
});
const backgroundPageTarget = await browser.waitForTarget(
target => target.type() === 'background_page'
); );
const backgroundPage = await backgroundPageTarget.page(); const fullTitle = await textSelector.evaluate(el => el.textContent);
// Test the background page as you would any other page.
// Print the full title
console.log('The title of this blog post is "%s".', fullTitle);
await browser.close(); await browser.close();
})(); })();
``` ```
:::note ### Default runtime settings
Chrome Manifest V3 extensions have a background ServiceWorker of type 'service_worker', instead of a page of type 'background_page'.
:::
:::note
It is not yet possible to test extension popups or content scripts.
:::
<!-- [START runtimesettings] -->
## Default runtime settings
**1. Uses Headless mode** **1. Uses Headless mode**
Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions.headless) when launching a browser: Puppeteer launches Chromium in
[headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome).
To launch a full version of Chromium, set the
[`headless`](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions.headless)
option when launching a browser:
```ts ```ts
const browser = await puppeteer.launch({headless: false}); // default is true const browser = await puppeteer.launch({headless: false}); // default is true
@ -277,139 +178,51 @@ const browser = await puppeteer.launch({headless: false}); // default is true
**2. Runs a bundled version of Chromium** **2. Runs a bundled version of Chromium**
By default, Puppeteer downloads and uses a specific version of Chromium so its API By default, Puppeteer downloads and uses a specific version of Chromium so its
is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium, API is guaranteed to work out of the box. To use Puppeteer with a different
pass in the executable's path when creating a `Browser` instance: version of Chrome or Chromium, pass in the executable's path when creating a
`Browser` instance:
```ts ```ts
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
``` ```
You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) for more information. You can also use Puppeteer with Firefox Nightly (experimental support). See
[`Puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) for
more information.
See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. See
[`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/)
for a description of the differences between Chromium and Chrome.
[`This article`](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/chromium_browser_vs_google_chrome.md)
describes some differences for Linux users.
**3. Creates a fresh user profile** **3. Creates a fresh user profile**
Puppeteer creates its own browser user profile which it **cleans up on every run**. Puppeteer creates its own browser user profile which it **cleans up on every
run**.
<!-- [END runtimesettings] --> #### Using Docker
See our [Docker guide](https://pptr.dev/guides/docker).
#### Using Chrome Extensions
See our [Chrome extensions guide](https://pptr.dev/guides/chrome-extensions).
## Resources ## Resources
- [API Documentation](https://pptr.dev/api) - [API Documentation](https://pptr.dev/api)
- [Guides](https://pptr.dev/category/guides)
- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples) - [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples)
- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) - [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer)
<!-- [START debugging] -->
## Debugging tips
1. Turn off headless mode - sometimes it's useful to see what the browser is
displaying. Instead of launching in headless mode, launch a full version of
the browser using `headless: false`:
```ts
const browser = await puppeteer.launch({headless: false});
```
2. Slow it down - the `slowMo` option slows down Puppeteer operations by the
specified amount of milliseconds. It's another way to help see what's going on.
```ts
const browser = await puppeteer.launch({
headless: false,
slowMo: 250, // slow down by 250ms
});
```
3. Capture console output - You can listen for the `console` event.
This is also handy when debugging code in `page.evaluate()`:
```ts
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.evaluate(() => console.log(`url is ${location.href}`));
```
4. Use debugger in application code browser
There are two execution context: node.js that is running test code, and the browser
running application code being tested. This lets you debug code in the
application code browser; ie code inside `evaluate()`.
- Use `{devtools: true}` when launching Puppeteer:
```ts
const browser = await puppeteer.launch({devtools: true});
```
- Change default test timeout:
jest: `jest.setTimeout(100000);`
jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;`
mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442))
- Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement:
```ts
await page.evaluate(() => {
debugger;
});
```
The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode.
5. Use debugger in node.js
This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser.
Note that you won't be able to run `await page.click()` in
DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if
you want to try something out, you have to add it to your test file.
- Add `debugger;` to your test, eg:
```ts
debugger;
await page.click('a[target=_blank]');
```
- Set `headless` to `false`
- Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests`
- In Chrome open `chrome://inspect/#devices` and click `inspect`
- In the newly opened test browser, type `F8` to resume test execution
- Now your `debugger` will be hit and you can debug in the test browser
6. Enable verbose logging - internal DevTools protocol traffic
will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace.
# Basic verbose logging
env DEBUG="puppeteer:*" node script.js
# Protocol traffic can be rather noisy. This example filters out all Network domain messages
env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'
7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb)
- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!)
- add a `debugger` to your Puppeteer (node) code
- add `ndb` (or `npx ndb`) before your test command. For example:
`ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`)
- debug your test inside chromium like a boss!
<!-- [END debugging] -->
## Contributing ## Contributing
Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of Puppeteer development. Check out our [contributing guide](https://pptr.dev/contributing) to get an
overview of Puppeteer development.
## FAQ ## FAQ
Our [FAQ](https://pptr.dev/faq) has migrated to [our site](https://pptr.dev/faq). Our [FAQ](https://pptr.dev/faq) has migrated to
[our site](https://pptr.dev/faq).

View file

@ -0,0 +1,7 @@
# Security Policy
The Puppeteer project takes security very seriously. Please use Chromium's process to report security issues.
## Reporting a Vulnerability
See https://www.chromium.org/Home/chromium-security/reporting-security-bugs/

View file

@ -13,11 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
// See https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-rules.md
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/config-conventional'],
rules: { rules: {
'body-max-line-length': [0, 'always', 100], // Override. The subject may be the name of a class.
'footer-max-line-length': [0, 'always', 100], 'subject-case': [0],
'subject-case': [0, 'never'], // Override. Most UIs wrap the body.
'body-max-line-length': [0],
// Override. Most UIs wrap the footer.
'footer-max-line-length': [0],
}, },
}; };

View file

@ -1,16 +0,0 @@
# Compatibility layer
This directory provides an additional compatibility layer between ES modules and CommonJS.
## Why?
Both `./cjs/compat.ts` and `./esm/compat.ts` are written as ES modules, but `./cjs/compat.ts` can additionally use NodeJS CommonJS globals such as `__dirname` and `require` while these are disabled in ES module mode. For more information, see [Differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs).
## Adding exports
In order to add exports, two things need to be done:
- The exports must be declared in `src/compat.ts`.
- The exports must be realized in `./cjs/compat.ts` and `./esm/compat.ts`.
In the event `compat.ts` becomes too large, you can place declarations in another file. Just make sure `./cjs`, `./esm`, and `src` have the same structure.

View file

@ -1,19 +0,0 @@
import {dirname} from 'path';
/**
* @internal
*/
let puppeteerDirname: string;
try {
// In some environments, like esbuild, this will throw an error.
// We suppress the error since the bundled binary is not expected
// to be used or installed in this case and, therefore, the
// root directory does not have to be known.
puppeteerDirname = dirname(require.resolve('./compat'));
} catch (error) {
// Fallback to __dirname.
puppeteerDirname = __dirname;
}
export {puppeteerDirname};

View file

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

View file

@ -1,22 +0,0 @@
import {createRequire} from 'module';
import {dirname} from 'path';
import {fileURLToPath} from 'url';
const require = createRequire(import.meta.url);
/**
* @internal
*/
let puppeteerDirname: string;
try {
// In some environments, like esbuild, this will throw an error.
// We suppress the error since the bundled binary is not expected
// to be used or installed in this case and, therefore, the
// root directory does not have to be known.
puppeteerDirname = dirname(require.resolve('./compat'));
} catch (error) {
puppeteerDirname = dirname(fileURLToPath(import.meta.url));
}
export {puppeteerDirname};

View file

@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "../../lib/esm/puppeteer",
"module": "esnext"
}
}

File diff suppressed because it is too large Load diff

View file

@ -39,3 +39,4 @@ More complex and use case driven examples can be found at [github.com/GoogleChro
## Services ## Services
- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps. - [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps.
- [Doppio](https://doppio.sh) - SaaS API to create screenshots or PDFs from HTML/CSS/JS

View file

@ -34,7 +34,7 @@ const firefoxOptions = {
await page.goto('https://news.ycombinator.com/'); await page.goto('https://news.ycombinator.com/');
// Extract articles from the page. // Extract articles from the page.
const resultsSelector = '.titlelink'; const resultsSelector = '.titleline > a';
const links = await page.evaluate(resultsSelector => { const links = await page.evaluate(resultsSelector => {
const anchors = Array.from(document.querySelectorAll(resultsSelector)); const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map(anchor => { return anchors.map(anchor => {

View file

@ -1,89 +0,0 @@
/**
* 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.
*/
/**
* This file is part of public API.
*
* By default, the `puppeteer` package runs this script during the installation
* process unless one of the env flags is provided.
* `puppeteer-core` package doesn't include this step at all. However, it's
* still possible to install a supported browser using this script when
* necessary.
*/
const compileTypeScriptIfRequired = require('./typescript-if-required.js');
async function download() {
await compileTypeScriptIfRequired();
// need to ensure TS is compiled before loading the installer
const {
downloadBrowser,
logPolitely,
} = require('./lib/cjs/puppeteer/node/install.js');
if (process.env.PUPPETEER_SKIP_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.'
);
return;
}
if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.'
);
return;
}
if (
process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.'
);
return;
}
if (
process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD ||
process.env.npm_package_config_puppeteer_skip_chromium_download
) {
logPolitely(
'**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.'
);
return;
}
downloadBrowser();
}
download();

View file

@ -5,6 +5,6 @@ origin:
description: Headless Chrome Node API description: Headless Chrome Node API
license: Apache-2.0 license: Apache-2.0
name: puppeteer name: puppeteer
release: 7d6927209e5d557891bd618ddb01d54bc3566307 release: e13e9647fc0d917da94af8851a09ed318fb0e07c
url: /Users/alexandraborovova/Projects/puppeteer url: /Users/alexandraborovova/Projects/puppeteer
schema: 1 schema: 1

File diff suppressed because it is too large Load diff

View file

@ -1,145 +1,115 @@
{ {
"name": "puppeteer", "name": "puppeteer-repo",
"version": "18.0.0", "private": true,
"description": "A high-level API to control headless Chrome over the DevTools Protocol", "repository": {
"keywords": [ "type": "git",
"puppeteer", "url": "https://github.com/puppeteer/puppeteer"
"chrome",
"headless",
"automation"
],
"type": "commonjs",
"main": "./lib/cjs/puppeteer/puppeteer.js",
"exports": {
".": {
"types": "./lib/types.d.ts",
"import": "./lib/esm/puppeteer/puppeteer.js",
"require": "./lib/cjs/puppeteer/puppeteer.js"
},
"./*": {
"import": "./*",
"require": "./*"
}
},
"types": "lib/types.d.ts",
"repository": "github:puppeteer/puppeteer",
"engines": {
"node": ">=14.1.0"
}, },
"scripts": { "scripts": {
"test": "cross-env MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node utils/mochaRunner/lib/main.js", "bisect": "tsx tools/bisect.ts",
"test:types": "tsd", "build": "npm run build --workspaces --if-present",
"test:install": "scripts/test-install.sh", "check:pinned-deps": "tsx tools/ensure-pinned-deps",
"test:firefox": "npm run test -- --test-suite firefox-headless", "check": "npm run check --workspaces --if-present && run-p check:*",
"test:chrome": "run-s test:chrome:*", "clean": "npm run clean --workspaces --if-present && rimraf **/.wireit",
"test:chrome:headless": "npm run test -- --test-suite chrome-headless",
"test:chrome:headless-chrome": "npm run test -- --test-suite chrome-new-headless",
"test:chrome:headful": "npm run test -- --test-suite chrome-headful",
"prepublishOnly": "npm run build",
"prepare": "node typescript-if-required.js && husky install",
"lint": "run-s lint:prettier lint:eslint",
"lint:prettier": "prettier --check .",
"lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"install": "node install.js",
"generate:sources": "tsx utils/generate_sources.ts",
"generate:artifacts": "tsx utils/generate_artifacts.ts",
"generate:markdown": "tsx utils/generate_docs.ts",
"format": "run-s format:*",
"format:prettier": "prettier --write .",
"format:eslint": "eslint --ext js --ext ts --fix .",
"docs": "run-s build generate:markdown",
"debug": "npm run build:dev && mocha --inspect-brk",
"commitlint": "commitlint --from=HEAD~1", "commitlint": "commitlint --from=HEAD~1",
"clean": "rimraf lib && rimraf test/build", "debug": "mocha --inspect-brk",
"check": "run-p check:*", "docs": "run-s build generate:markdown",
"check:protocol-revision": "tsx scripts/ensure-correct-devtools-protocol-package", "format:eslint": "eslint --ext js --ext ts --fix .",
"check:pinned-deps": "tsx scripts/ensure-pinned-deps", "format:prettier": "prettier --write .",
"build": "npm run build:prod", "format": "run-s format:*",
"build:dev": "run-s generate:sources build:tsc:dev generate:artifacts", "generate:markdown": "tsx tools/generate_docs.ts",
"build:prod": "run-s generate:sources build:tsc:prod generate:artifacts", "lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"build:tsc:dev": "tsc -b test", "lint:prettier": "prettier --check .",
"build:tsc:prod": "tsc -b tsconfig.lib.json" "lint": "run-s lint:prettier lint:eslint",
}, "postinstall": "npm run postinstall --workspaces --if-present",
"files": [ "prepare": "husky install",
"lib", "test-install": "npm run test --workspace @puppeteer-test/installation",
"install.js", "test-types": "tsd -t packages/puppeteer",
"typescript-if-required.js", "test:chrome:headful": "npm test -- --test-suite chrome-headful",
"!**/*.tsbuildinfo" "test:chrome:headless-chrome": "npm test -- --test-suite chrome-new-headless",
], "test:chrome:headless": "npm test -- --test-suite chrome-headless",
"author": "The Chromium Authors", "test:chrome:bidi": "npm test -- --test-suite chrome-bidi",
"license": "Apache-2.0", "test:chrome": "run-s test:chrome:*",
"dependencies": { "test:firefox:bidi": "npm test -- --test-suite firefox-bidi",
"cross-fetch": "3.1.5", "test:firefox:headful": "npm test -- --test-suite firefox-headful",
"debug": "4.3.4", "test:firefox:headless": "npm test -- --test-suite firefox-headless",
"devtools-protocol": "0.0.1036444", "test:firefox": "run-s test:firefox:*",
"extract-zip": "2.0.1", "test": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node tools/mochaRunner/lib/main.js"
"https-proxy-agent": "5.0.1",
"progress": "2.0.3",
"proxy-from-env": "1.1.0",
"rimraf": "3.0.2",
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"ws": "8.8.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.0.3", "@actions/core": "1.10.0",
"@commitlint/config-conventional": "17.0.3", "@commitlint/cli": "17.3.0",
"@microsoft/api-documenter": "7.19.4", "@commitlint/config-conventional": "17.3.0",
"@microsoft/api-extractor": "7.29.2", "@microsoft/api-documenter": "7.19.26",
"@microsoft/api-extractor-model": "7.23.0", "@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-node-resolve": "15.0.1",
"@types/debug": "4.1.7", "@types/debug": "4.1.7",
"@types/diff": "5.0.2", "@types/diff": "5.0.2",
"@types/glob": "7.2.0", "@types/glob": "8.0.0",
"@types/mime": "3.0.1", "@types/mime": "3.0.1",
"@types/mocha": "9.1.1", "@types/mocha": "10.0.1",
"@types/node": "18.7.1", "@types/node": "18.11.17",
"@types/pixelmatch": "5.2.4", "@types/pixelmatch": "5.2.4",
"@types/pngjs": "6.0.1", "@types/pngjs": "6.0.1",
"@types/progress": "2.0.5", "@types/progress": "2.0.5",
"@types/proxy-from-env": "1.0.1", "@types/proxy-from-env": "1.0.1",
"@types/rimraf": "3.0.2", "@types/rimraf": "3.0.2",
"@types/semver": "7.3.11", "@types/semver": "7.3.13",
"@types/sinon": "10.0.13", "@types/sinon": "10.0.13",
"@types/tar-fs": "2.0.1", "@types/tar-fs": "2.0.1",
"@types/unbzip2-stream": "1.4.0", "@types/unbzip2-stream": "1.4.0",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.33.0", "@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.33.0", "@typescript-eslint/parser": "5.46.1",
"c8": "7.12.0", "c8": "7.12.0",
"chromium-bidi": "0.4.3",
"commonmark": "0.30.0", "commonmark": "0.30.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"diff": "5.1.0", "diff": "5.1.0",
"esbuild": "0.15.5", "esbuild": "0.16.9",
"eslint": "8.21.0", "eslint": "8.30.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-formatter-codeframe": "7.32.1", "eslint-formatter-codeframe": "7.32.1",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-local": "1.0.0", "eslint-plugin-local": "1.0.0",
"eslint-plugin-mocha": "10.1.0", "eslint-plugin-mocha": "10.1.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-tsdoc": "0.2.16", "eslint-plugin-tsdoc": "0.2.17",
"eslint-plugin-unused-imports": "2.0.0", "eslint-plugin-unused-imports": "2.0.0",
"esprima": "4.0.1", "esprima": "4.0.1",
"expect": "25.2.7", "expect": "25.2.7",
"glob": "8.0.3", "glob": "8.1.0",
"gts": "4.0.0", "gts": "4.0.0",
"husky": "8.0.1", "husky": "8.0.2",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
"mime": "3.0.0", "mime": "3.0.0",
"minimist": "1.2.6", "minimist": "1.2.7",
"mocha": "10.0.0", "mitt": "3.0.0",
"mocha": "10.2.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"pixelmatch": "5.3.0", "pixelmatch": "5.3.0",
"pngjs": "6.0.0", "pngjs": "6.0.0",
"prettier": "2.7.1", "prettier": "2.8.1",
"semver": "7.3.7", "puppeteer": "file:packages/puppeteer",
"sinon": "14.0.0", "rollup": "2.79.1",
"rollup-plugin-dts": "4.2.2",
"semver": "7.3.8",
"sinon": "15.0.1",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"text-diff": "1.0.1", "text-diff": "1.0.1",
"tsd": "0.22.0", "tsd": "0.25.0",
"tsx": "3.8.2", "tsx": "3.12.1",
"typescript": "4.7.4", "typescript": "4.9.4",
"zod": "3.18.0" "wireit": "0.9.2",
} "zod": "3.20.2"
},
"workspaces": [
"packages/*",
"test",
"test/installation"
]
} }

View file

@ -0,0 +1,2 @@
# Ignore File that will be copied to Angular
/files/

View file

@ -0,0 +1,22 @@
# Outputs
src/**/*.js
src/**/*.js.map
src/**/*.d.ts
# Keep files that serve as template
!src/**/files/**/*
# IDEs
.idea/
jsconfig.json
.vscode/
# Misc
node_modules/
npm-debug.log*
yarn-error.log*
# Mac OSX Finder files.
**/.DS_Store
.DS_Store

View file

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

View file

@ -0,0 +1,8 @@
# Changelog
## 0.1.0 (2022-11-23)
### Features
* **ng-schematics:** Release @puppeteer/ng-schematics ([#9244](https://github.com/puppeteer/puppeteer/issues/9244)) ([be33929](https://github.com/puppeteer/puppeteer/commit/be33929770e473992ad49029e6d038d36591e108))

View file

@ -0,0 +1,51 @@
# Puppeteer Angular Schematic
Adds Puppeteer-based e2e tests to your Angular project.
## Usage
Run the command below in an Angular CLI app directory and follow the prompts.
_Note this will add the schematic as a dependency to your project._
```bash
ng add @puppeteer/ng-schematics
```
Or you can use the same command followed by the [options](#options) below.
Currently, this schematic supports the following test frameworks:
- **Jasmine** [https://jasmine.github.io/]
- **Jest** [https://jestjs.io/]
- **Mocha** [https://mochajs.org/]
- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html]
With the schematics installed you can run E2E tests:
```bash
ng e2e
```
> Note: Command spawns it's own server on the same port `ng serve` does.
## Options
When adding schematics to your project you can to provide following options:
| Option | Description | Value | Required |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |
| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` |
| `--exportConfig` | When true, creates an empty [Puppeteer configuration](https://pptr.dev/guides/configuration) file. (`.puppeteerrc.cjs`) | `boolean` | `true` |
| `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
## Contributing
Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of what you need to develop in the Puppeteer repo.
### Unit Testing
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:
```bash
npm run test
```

View file

@ -0,0 +1,70 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs/promises');
const path = require('path');
/**
*
* @param {String} directory
* @param {String[]} files
*/
async function findSchemaFiles(directory, files = []) {
const items = await fs.readdir(directory);
const promises = [];
// Match any listing that has no *.* format
// Ignore files folder
const regEx = /^.*\.[^\s]*$/;
items.forEach(item => {
if (!item.match(regEx)) {
promises.push(findSchemaFiles(`${directory}/${item}`, files));
} else if (item.endsWith('.json') || directory.includes('files')) {
files.push(`${directory}/${item}`);
}
});
await Promise.all(promises);
return files;
}
async function copySchemaFiles() {
const srcDir = './src';
const outputDir = './lib';
const files = await findSchemaFiles(srcDir);
const moves = files.map(file => {
const to = file.replace(srcDir, outputDir);
return {from: file, to};
});
// Because fs.cp is Experimental (recursive support)
// We need to create directories first and copy the files
await Promise.all(
moves.map(({to}) => {
const dir = path.dirname(to);
return fs.mkdir(dir, {recursive: true});
})
);
await Promise.all(
moves.map(({from, to}) => {
return fs.copyFile(from, to);
})
);
}
copySchemaFiles();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,86 @@
{
"name": "@puppeteer/ng-schematics",
"version": "0.1.0",
"description": "Puppeteer Angular schematics",
"scripts": {
"dev": "npm run build --watch",
"dev:test": "npm run test --watch",
"copy": "wireit",
"build": "wireit",
"clean": "tsc --build --clean && rimraf lib",
"clean:test": "rimraf test/build",
"test": "wireit"
},
"wireit": {
"copy": {
"clean": "if-file-deleted",
"command": "node copySchemaFiles.js",
"files": [
"src/**/files/**",
"src/**/*.json"
],
"output": [
"lib/**/files/**",
"lib/**/*.json"
],
"dependencies": [
"clean"
]
},
"build": {
"command": "tsc -b",
"files": [
"src/**/*.ts",
"!src/**/files",
"!src/**/*.json"
],
"output": [
"lib/**",
"!lib/**/files",
"!lib/**/*.json"
],
"dependencies": [
"copy"
]
},
"test": {
"command": "mocha",
"dependencies": [
"clean:test",
"build"
]
}
},
"keywords": [
"angular",
"puppeteer",
"schematics"
],
"repository": {
"type": "git",
"url": "https://github.com/puppeteer/puppeteer/tree/main/packages/ng-schematics"
},
"author": "The Chromium Authors",
"license": "Apache-2.0",
"engines": {
"node": ">=14.1.0"
},
"dependencies": {
"@angular-devkit/architect": "^0.1501.2",
"@angular-devkit/core": "^15.1.2",
"@angular-devkit/schematics": "^15.1.2"
},
"devDependencies": {
"@types/node": "^14.15.0",
"@schematics/angular": "^14.2.8"
},
"files": [
"lib",
"!*.tsbuildinfo"
],
"ng-add": {
"save": "devDependencies"
},
"schematics": "./lib/schematics/collection.json",
"builders": "./lib/builders/builders.json"
}

View file

@ -0,0 +1,10 @@
{
"$schema": "../../../../node_modules/@angular-devkit/architect/src/builders-schema.json",
"builders": {
"puppeteer": {
"implementation": "./puppeteer",
"schema": "./puppeteer/schema.json",
"description": "Run e2e test with Puppeteer"
}
}
}

View file

@ -0,0 +1,137 @@
import {
createBuilder,
BuilderContext,
BuilderOutput,
targetFromTargetString,
BuilderRun,
} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {spawn} from 'child_process';
import {PuppeteerBuilderOptions} from './types.js';
const terminalStyles = {
blue: '\u001b[34m',
green: '\u001b[32m',
bold: '\u001b[1m',
reverse: '\u001b[7m',
clear: '\u001b[0m',
};
function getError(executable: string, args: string[]) {
return (
`Puppeteer E2E tests failed!` +
'\n' +
`Error running '${executable}' with arguments '${args.join(' ')}'.` +
`\n` +
'Please look at the output above to determine the issue!'
);
}
function getExecutable(command: string[]) {
const executable = command.shift()!;
const error = getError(executable, command);
if (executable === 'node') {
return {
executable: executable,
args: command,
error,
};
}
return {
executable: `./node_modules/.bin/${executable}`,
args: command,
error,
};
}
async function executeCommand(context: BuilderContext, command: string[]) {
await new Promise((resolve, reject) => {
context.logger.debug(`Trying to execute command - ${command.join(' ')}.`);
const {executable, args, error} = getExecutable(command);
const child = spawn(executable, args, {
cwd: context.workspaceRoot,
stdio: 'inherit',
});
child.on('error', message => {
console.log(message);
reject(error);
});
child.on('exit', code => {
if (code === 0) {
resolve(true);
} else {
reject(error);
}
});
});
}
function message(
message: string,
context: BuilderContext,
type: 'info' | 'success' = 'info'
): void {
const color = type === 'info' ? terminalStyles.blue : terminalStyles.green;
context.logger.info(
`${terminalStyles.bold}${terminalStyles.reverse}${color}${message}${terminalStyles.clear}`
);
}
async function startServer(
options: PuppeteerBuilderOptions,
context: BuilderContext
): Promise<BuilderRun> {
context.logger.debug('Trying to start server.');
const target = targetFromTargetString(options.devServerTarget);
const defaultServerOptions = await context.getTargetOptions(target);
const overrides = {
watch: false,
host: defaultServerOptions['host'],
port: defaultServerOptions['port'],
} as JsonObject;
message('Spawning test server...\n', context);
const server = await context.scheduleTarget(target, overrides);
const result = await server.result;
if (!result.success) {
throw new Error('Failed to spawn server! Stopping tests...');
}
return server;
}
async function executeE2ETest(
options: PuppeteerBuilderOptions,
context: BuilderContext
): Promise<BuilderOutput> {
let server: BuilderRun | null = null;
try {
server = await startServer(options, context);
message('\nRunning tests...\n', context);
for (const command of options.commands) {
await executeCommand(context, command);
}
message('\nTest ran successfully!', context, 'success');
return {success: true};
} catch (error) {
if (error instanceof Error) {
return {success: false, error: error.message};
}
return {success: false, error: error as any};
} finally {
if (server) {
await server.stop();
}
}
}
export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest) as any;

View file

@ -0,0 +1,22 @@
{
"title": "Puppeteer",
"description": "Options for Puppeteer Angular Schematics",
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "array",
"item": {
"type": "string"
}
},
"description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')."
},
"devServerTarget": {
"type": "string",
"description": "Angular target that spawns the server."
}
},
"additionalProperties": true
}

View file

@ -0,0 +1,24 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 {JsonObject} from '@angular-devkit/core';
type Command = [string, ...string[]];
export interface PuppeteerBuilderOptions extends JsonObject {
commands: Command[];
devServerTarget: string;
}

View file

@ -0,0 +1,10 @@
{
"$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"ng-add": {
"description": "Add Puppeteer to an Angular project",
"factory": "./ng-add/index#ngAdd",
"schema": "./ng-add/schema.json"
}
}
}

View file

@ -0,0 +1,4 @@
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {};

View file

@ -0,0 +1,59 @@
import * as puppeteer from 'puppeteer';
<% if(testingFramework == 'node') { %>
import {
describe,
it,
before,
beforeEach,
after,
afterEach,
} from 'node:test';
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
import * as assert from 'assert';
<% } %>
describe('App test', function () {
let browser: puppeteer.Browser;
let page: puppeteer.Page;
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
beforeAll(async () => {
browser = await puppeteer.launch();
});
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
before(async () => {
browser = await puppeteer.launch();
});
<% } %>
beforeEach(async () => {
page = await browser.newPage();
await page.goto('<%= baseUrl %>');
});
afterEach(async () => {
await page.close();
});
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
afterAll(async () => {
await browser.close();
});
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
after(async () => {
await browser.close();
});
<% } %>
it('is running', async function () {
const element = await page.waitForSelector(
'text/<%= project %> app is running!'
);
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
expect(element).not.toBeNull();
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
assert.ok(element);
<% } %>
});
});

View file

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
<% if(testingFramework == 'jest') { %>
"esModuleInterop": true,
<% } %><% if(testingFramework == 'node') { %>
"module": "CommonJS",
"rootDir": "tests/",
"outDir": "test/",
<% } %>
"types": ["<%= testingFramework %>"]
},
"include": ["tests/**/*.e2e.ts"]
}

View file

@ -0,0 +1,4 @@
require('@babel/register')({
extensions: ['.js', '.ts'],
presets: ['@babel/preset-env', '@babel/preset-typescript'],
});

View file

@ -0,0 +1,9 @@
{
"spec_dir": "e2e",
"spec_files": ["**/*[eE]2[eE].ts"],
"helpers": ["helpers/babel.js", "helpers/**/*.{js|ts}"],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

View file

@ -0,0 +1,11 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testMatch: ['<rootDir>/tests/**/?(*.)+(e2e).[tj]s?(x)'],
preset: 'ts-jest',
testEnvironment: 'node',
};

View file

@ -0,0 +1,4 @@
module.exports = {
file: ['e2e/babel.js'],
spec: './e2e/tests/**/*.e2e.ts',
};

View file

@ -0,0 +1,4 @@
require('@babel/register')({
extensions: ['.js', '.ts'],
presets: ['@babel/preset-env', '@babel/preset-typescript'],
});

View file

@ -0,0 +1,3 @@
# Compiled e2e tests output Node auto resolves files in folders named 'test'
test/

View file

@ -0,0 +1,128 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 {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 {
addBaseFiles,
addFrameworkFiles,
getNgCommandName,
} from '../utils/files.js';
import {
addPackageJsonDependencies,
addPackageJsonScripts,
getDependenciesFromOptions,
getPackageLatestNpmVersion,
DependencyType,
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.
export function ngAdd(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
return chain([
addDependencies(options),
addPuppeteerFiles(options),
addOtherFiles(options),
updateScripts(options),
updateAngularConfig(options),
])(tree, context);
};
}
function addDependencies(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding dependencies to "package.json"');
const dependencies = getDependenciesFromOptions(options);
return of(...dependencies).pipe(
concatMap((packageName: string) => {
return getPackageLatestNpmVersion(packageName);
}),
scan((array, nodePackage) => {
array.push(nodePackage);
return array;
}, [] as NodePackage[]),
map(packages => {
context.logger.debug('Updating dependencies...');
addPackageJsonDependencies(tree, packages, DependencyType.Dev);
context.addTask(new NodePackageInstallTask());
return tree;
})
);
};
}
function updateScripts(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext): Tree => {
context.logger.debug('Updating "package.json" scripts');
const angularJson = getAngularConfig(tree);
const projects = Object.keys(angularJson['projects']);
if (projects.length === 1) {
const name = getNgCommandName(options);
const prefix = options.isDefaultTester ? '' : `run ${projects[0]}:`;
return addPackageJsonScripts(tree, [
{
name,
script: `ng ${prefix}${name}`,
},
]);
}
return tree;
};
}
function addPuppeteerFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer base files.');
const {projects} = getAngularConfig(tree);
return addBaseFiles(tree, context, {
projects,
options,
});
};
}
function addOtherFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer additional files.');
const {projects} = getAngularConfig(tree);
return addFrameworkFiles(tree, context, {
projects,
options,
});
};
}
function updateAngularConfig(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext): Tree => {
context.logger.debug('Updating "angular.json".');
return updateAngularJsonScripts(tree, options);
};
}

View file

@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "Puppeteer",
"title": "Puppeteer Install Schema",
"type": "object",
"properties": {
"isDefaultTester": {
"description": "",
"type": "boolean",
"default": true,
"x-prompt": "Use Puppeteer as default `ng e2e` command?"
},
"exportConfig": {
"description": "",
"type": "boolean",
"default": false,
"x-prompt": "Export default Puppeteer config file?"
},
"testingFramework": {
"description": "",
"type": "string",
"enum": ["jasmine", "jest", "mocha", "node"],
"default": "jasmine",
"x-prompt": {
"message": "With what Testing Library do you wish to integrate?",
"type": "list",
"items": [
{
"value": "jasmine",
"label": "Use Jasmine [https://jasmine.github.io/]"
},
{
"value": "jest",
"label": "Use Jest [https://jestjs.io/]"
},
{
"value": "mocha",
"label": "Use Mocha [https://mochajs.org/]"
},
{
"value": "node",
"label": "Use Node Test Runner (Experimental: Node v18) [https://nodejs.org/api/test.html]"
}
]
}
}
},
"required": []
}

View file

@ -0,0 +1,160 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 {getSystemPath, normalize, strings} from '@angular-devkit/core';
import {
SchematicContext,
Tree,
apply,
applyTemplates,
chain,
filter,
mergeWith,
move,
url,
} from '@angular-devkit/schematics';
import {relative, resolve} from 'path';
import {SchematicsOptions, TestingFramework} from './types.js';
export interface FilesOptions {
projects: any;
options: SchematicsOptions;
applyPath: string;
relativeToWorkspacePath: string;
movePath?: string;
filterPredicate?: (path: string) => boolean;
}
const PUPPETEER_CONFIG_TEMPLATE = '.puppeteerrc.cjs.template';
export function addFiles(
tree: Tree,
context: SchematicContext,
{
projects,
options,
applyPath,
movePath,
relativeToWorkspacePath,
filterPredicate,
}: FilesOptions
): any {
return chain(
Object.keys(projects).map(name => {
const project = projects[name];
const projectPath = resolve(getSystemPath(normalize(project.root)));
const workspacePath = resolve(getSystemPath(normalize('')));
const relativeToWorkspace = relative(
`${projectPath}${relativeToWorkspacePath}`,
workspacePath
);
const baseUrl = getProjectBaseUrl(project);
return mergeWith(
apply(url(applyPath), [
filter(
filterPredicate ??
(() => {
return true;
})
),
move(movePath ? `${project.root}${movePath}` : project.root),
applyTemplates({
...options,
...strings,
root: project.root ? `${project.root}/` : project.root,
baseUrl,
project: name,
relativeToWorkspace,
}),
])
);
})
)(tree, context);
}
function getProjectBaseUrl(project: any): string {
let options = {protocol: 'http', port: 4200, host: 'localhost'};
if (project.architect?.serve?.options) {
const projectOptions = project.architect?.serve?.options;
options = {...options, ...projectOptions};
options.protocol = projectOptions.ssl ? 'https' : 'http';
}
return `${options.protocol}://${options.host}:${options.port}`;
}
export function addBaseFiles(
tree: Tree,
context: SchematicContext,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const options: FilesOptions = {
...filesOptions,
applyPath: './files/base',
relativeToWorkspacePath: `/`,
filterPredicate: path => {
return path.includes(PUPPETEER_CONFIG_TEMPLATE) &&
!filesOptions.options.exportConfig
? false
: true;
},
};
return addFiles(tree, context, options);
}
export function addFrameworkFiles(
tree: Tree,
context: SchematicContext,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const testingFramework = filesOptions.options.testingFramework;
const options: FilesOptions = {
...filesOptions,
applyPath: `./files/${testingFramework}`,
relativeToWorkspacePath: `/`,
};
return addFiles(tree, context, options);
}
export function getScriptFromOptions(options: SchematicsOptions): string[][] {
switch (options.testingFramework) {
case TestingFramework.Jasmine:
return [[`jasmine`, '--config=./e2e/support/jasmine.json']];
case TestingFramework.Jest:
return [[`jest`, '-c', 'e2e/jest.config.js']];
case TestingFramework.Mocha:
return [[`mocha`, '--config=./e2e/.mocharc.js']];
case TestingFramework.Node:
return [
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
];
}
}
export function getNgCommandName(options: SchematicsOptions): string {
if (options.isDefaultTester) {
return 'e2e';
}
return 'puppeteer';
}

View file

@ -0,0 +1,38 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 {SchematicsException, Tree} from '@angular-devkit/schematics';
export function getJsonFileAsObject(
tree: Tree,
path: string
): Record<string, any> {
try {
const buffer = tree.read(path) as Buffer;
const content = buffer.toString();
return JSON.parse(content);
} catch {
throw new SchematicsException(`Unable to retrieve file at ${path}.`);
}
}
export function getObjectAsJson(object: Record<string, any>): string {
return JSON.stringify(object, null, 2);
}
export function getAngularConfig(tree: Tree): Record<string, any> {
return getJsonFileAsObject(tree, './angular.json');
}

View file

@ -0,0 +1,202 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 {Tree} from '@angular-devkit/schematics';
import {get} from 'https';
import {SchematicsOptions, TestingFramework} from './types.js';
import {
getAngularConfig,
getJsonFileAsObject,
getObjectAsJson,
} from './json.js';
import {getNgCommandName, getScriptFromOptions} from './files.js';
export interface NodePackage {
name: string;
version: string;
}
export interface NodeScripts {
name: string;
script: string;
}
export enum DependencyType {
Default = 'dependencies',
Dev = 'devDependencies',
Peer = 'peerDependencies',
Optional = 'optionalDependencies',
}
export function getPackageLatestNpmVersion(name: string): Promise<NodePackage> {
return new Promise(resolve => {
let version = 'latest';
return get(`https://registry.npmjs.org/${name}`, res => {
let data = '';
res.on('data', (chunk: any) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
version = response?.['dist-tags']?.latest ?? version;
} catch {
} finally {
resolve({
name,
version,
});
}
});
}).on('error', () => {
resolve({
name,
version,
});
});
});
}
function updateJsonValues(
json: Record<string, any>,
target: string,
updates: Array<{name: string; value: any}>,
overwrite = false
) {
updates.forEach(({name, value}) => {
if (!json[target][name] || overwrite) {
json[target] = {
...json[target],
[name]: value,
};
}
});
}
export function addPackageJsonDependencies(
tree: Tree,
packages: NodePackage[],
type: DependencyType,
overwrite?: boolean,
fileLocation = './package.json'
): Tree {
const packageJson = getJsonFileAsObject(tree, fileLocation);
updateJsonValues(
packageJson,
type,
packages.map(({name, version}) => {
return {name, value: version};
}),
overwrite
);
tree.overwrite(fileLocation, getObjectAsJson(packageJson));
return tree;
}
export function getDependenciesFromOptions(
options: SchematicsOptions
): string[] {
const dependencies = ['puppeteer'];
const babelPackages = [
'@babel/core',
'@babel/register',
'@babel/preset-env',
'@babel/preset-typescript',
];
switch (options.testingFramework) {
case TestingFramework.Jasmine:
dependencies.push('jasmine', ...babelPackages);
break;
case TestingFramework.Jest:
dependencies.push('jest', '@types/jest', 'ts-jest');
break;
case TestingFramework.Mocha:
dependencies.push('mocha', '@types/mocha', ...babelPackages);
break;
case TestingFramework.Node:
dependencies.push('@types/node');
break;
}
return dependencies;
}
export function addPackageJsonScripts(
tree: Tree,
scripts: NodeScripts[],
overwrite?: boolean,
fileLocation = './package.json'
): Tree {
const packageJson = getJsonFileAsObject(tree, fileLocation);
updateJsonValues(
packageJson,
'scripts',
scripts.map(({name, script}) => {
return {name, value: script};
}),
overwrite
);
tree.overwrite(fileLocation, getObjectAsJson(packageJson));
return tree;
}
export function updateAngularJsonScripts(
tree: Tree,
options: SchematicsOptions,
overwrite = true
): Tree {
const angularJson = getAngularConfig(tree);
const commands = getScriptFromOptions(options);
const name = getNgCommandName(options);
Object.keys(angularJson['projects']).forEach(project => {
const e2eScript = [
{
name,
value: {
builder: '@puppeteer/ng-schematics:puppeteer',
options: {
commands,
devServerTarget: `${project}:serve`,
},
configurations: {
production: {
devServerTarget: `${project}:serve:production`,
},
},
},
},
];
updateJsonValues(
angularJson['projects'][project],
'architect',
e2eScript,
overwrite
);
});
tree.overwrite('./angular.json', getObjectAsJson(angularJson));
return tree;
}

View file

@ -0,0 +1,28 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum TestingFramework {
Jasmine = 'jasmine',
Jest = 'jest',
Mocha = 'mocha',
Node = 'node',
}
export interface SchematicsOptions {
isDefaultTester: boolean;
exportConfig: boolean;
testingFramework: TestingFramework;
}

View file

@ -0,0 +1,211 @@
import expect from 'expect';
import sinon from 'sinon';
import https from 'https';
import {join} from 'path';
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing/schematic-test-runner';
import {JsonObject} from '@angular-devkit/core';
const WORKSPACE_OPTIONS = {
name: 'workspace',
newProjectRoot: 'projects',
version: '14.0.0',
};
const APPLICATION_OPTIONS = {
name: 'sandbox',
};
function getProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`;
}
function getAngularJsonScripts(
tree: UnitTestTree,
isDefault = true
): {
builder: string;
configurations: Record<string, any>;
options: Record<string, any>;
} {
const angularJson = tree.readJson('angular.json') as any;
const e2eScript = isDefault ? 'e2e' : 'puppeteer';
return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][
e2eScript
];
}
function getPackageJson(tree: UnitTestTree): {
scripts: Record<string, string>;
devDependencies: string[];
} {
const packageJson = tree.readJson('package.json') as JsonObject;
return {
scripts: packageJson['scripts'] as any,
devDependencies: Object.keys(
packageJson['devDependencies'] as Record<string, string>
),
};
}
async function buildTestingTree(userOptions?: Record<string, any>) {
const runner = new SchematicTestRunner(
'schematics',
join(__dirname, '../../lib/schematics/collection.json')
);
const options = {
isDefaultTester: true,
exportConfig: false,
testingFramework: 'jasmine',
...userOptions,
};
let workingTree: UnitTestTree;
// Build workspace
workingTree = await runner
.runExternalSchematicAsync(
'@schematics/angular',
'workspace',
WORKSPACE_OPTIONS
)
.toPromise();
// Build dummy application
workingTree = await runner
.runExternalSchematicAsync(
'@schematics/angular',
'application',
APPLICATION_OPTIONS,
workingTree
)
.toPromise();
return await runner
.runSchematicAsync('ng-add', options, workingTree)
.toPromise();
}
describe('@puppeteer/ng-schematics: ng-add', () => {
// Stop outgoing Request for version fetching
before(() => {
const httpsGetStub = sinon.stub(https, 'get');
httpsGetStub.returns({
on: (_: any, callback: () => void) => {
callback();
},
} as any);
});
after(() => {
sinon.restore();
});
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree();
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/tsconfig.json'));
expect(tree.files).toContain(getProjectFile('e2e/tests/app.e2e.ts'));
expect(devDependencies).toContain('puppeteer');
expect(scripts['e2e']).toBe('ng e2e');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(configurations).toEqual({
production: {
devServerTarget: 'sandbox:serve:production',
},
});
});
it('should update create proper "ng" command for non default tester', async () => {
const tree = await buildTestingTree({
isDefaultTester: false,
});
const {scripts} = getPackageJson(tree);
const {builder} = getAngularJsonScripts(tree, false);
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
});
it('should create Puppeteer config', async () => {
const {files} = await buildTestingTree({
exportConfig: true,
});
expect(files).toContain(getProjectFile('.puppeteerrc.cjs'));
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree({
exportConfig: false,
});
expect(files).not.toContain(getProjectFile('.puppeteerrc.cjs'));
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree({
testingFramework: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json'));
expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js'));
expect(devDependencies).toContain('jasmine');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[`jasmine`, '--config=./e2e/support/jasmine.json'],
]);
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree({
testingFramework: 'jest',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/jest.config.js'));
expect(devDependencies).toContain('jest');
expect(devDependencies).toContain('@types/jest');
expect(devDependencies).toContain('ts-jest');
expect(options['commands']).toEqual([[`jest`, '-c', 'e2e/jest.config.js']]);
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree({
testingFramework: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js'));
expect(tree.files).toContain(getProjectFile('e2e/babel.js'));
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[`mocha`, '--config=./e2e/.mocharc.js'],
]);
});
it('should create Node files"', async () => {
const tree = await buildTestingTree({
testingFramework: 'node',
});
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.gitignore'));
expect(options['commands']).toEqual([
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
]);
});
});

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "tsconfig",
"lib": ["ES2018"],
"module": "CommonJS",
"noEmitOnError": true,
"rootDir": "src/",
"outDir": "lib/",
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
"types": ["node"],
"target": "ES6"
},
"include": ["src/**/*"],
"exclude": ["src/**/files/**/*"],
"references": [{"path": "./tsconfig.spec.json"}]
}

View file

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "test/src/",
"outDir": "test/build/",
"types": ["node", "mocha"]
},
"include": ["test/src/**/*"],
"exclude": ["test/build/**/*"]
}

View file

@ -0,0 +1 @@
README.md

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/types.d.ts", "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer-core.d.ts",
"bundledPackages": [], "bundledPackages": [],
"apiReport": { "apiReport": {
@ -9,7 +9,7 @@
"docModel": { "docModel": {
"enabled": true, "enabled": true,
"apiJsonFilePath": "<projectFolder>/docs/<unscopedPackageName>.api.json" "apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json"
}, },
"dtsRollup": { "dtsRollup": {

View file

@ -0,0 +1,169 @@
{
"name": "puppeteer-core",
"version": "19.6.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [
"puppeteer",
"chrome",
"headless",
"automation"
],
"type": "commonjs",
"main": "./lib/cjs/puppeteer/puppeteer-core.js",
"types": "./lib/types.d.ts",
"exports": {
".": {
"types": "./lib/types.d.ts",
"import": "./lib/esm/puppeteer/puppeteer-core.js",
"require": "./lib/cjs/puppeteer/puppeteer-core.js"
},
"./internal/*": {
"import": "./lib/esm/puppeteer/*",
"require": "./lib/cjs/puppeteer/*"
},
"./*": {
"import": "./*",
"require": "./*"
}
},
"repository": {
"type": "git",
"url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core"
},
"engines": {
"node": ">=14.1.0"
},
"scripts": {
"build:third_party": "wireit",
"build:tsc": "wireit",
"build:types": "wireit",
"build": "wireit",
"check": "tsx tools/ensure-correct-devtools-protocol-package",
"format:types": "wireit",
"generate:package-json": "wireit",
"generate:sources": "wireit",
"prepack": "wireit",
"clean": "tsc -b --clean && rimraf lib src/generated",
"clean:third_party": "wireit"
},
"wireit": {
"prepack": {
"command": "cp ../../README.md README.md",
"files": [
"../../README.md"
],
"output": [
"README.md"
]
},
"build": {
"dependencies": [
"build:third_party",
"format:types",
"generate:package-json"
]
},
"generate:sources": {
"command": "tsx tools/generate_sources.ts",
"files": [
"tools/generate_sources.ts",
"src/templates/**"
],
"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/**"
]
},
"generate:package-json": {
"command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json",
"clean": "if-file-deleted",
"dependencies": [
"build:tsc"
],
"output": [
"lib/esm/package.json"
]
},
"build:types": {
"command": "api-extractor run --local",
"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",
"clean": "if-file-deleted",
"dependencies": [
"clean:third_party",
"generate:sources"
],
"files": [
"src/**",
"compat/**",
"third_party/**",
"**/tsconfig.*.json"
],
"output": [
"lib/esm/**",
"lib/cjs/**"
]
}
},
"files": [
"lib",
"!*.tsbuildinfo"
],
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"cross-fetch": "3.1.5",
"debug": "4.3.4",
"devtools-protocol": "0.0.1082910",
"extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1",
"proxy-from-env": "1.1.0",
"rimraf": "3.0.2",
"tar-fs": "2.1.1",
"unbzip2-stream": "1.4.3",
"ws": "8.11.0"
}
}

View file

@ -0,0 +1,41 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import glob from 'glob';
import dts from 'rollup-plugin-dts';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
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`)) {
configs.push({
input: jsFile,
output: {file: jsFile, 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

@ -19,8 +19,9 @@
import {ChildProcess} from 'child_process'; import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {EventEmitter} from '../common/EventEmitter.js'; import {EventEmitter} from '../common/EventEmitter.js';
import type {Page} from '../common/Page.js'; // TODO: move to ./api import type {Page} from './Page.js'; // TODO: move to ./api
import type {Target} from '../common/Target.js'; // TODO: move to ./api import type {Target} from '../common/Target.js'; // TODO: move to ./api
import type {BrowserContext} from './BrowserContext.js';
/** /**
* BrowserContext options. * BrowserContext options.
@ -181,7 +182,7 @@ export const enum BrowserEmittedEvents {
* An example of using a {@link Browser} to create a {@link Page}: * An example of using a {@link Browser} to create a {@link Page}:
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -195,7 +196,7 @@ export const enum BrowserEmittedEvents {
* An example of disconnecting from and reconnecting to a {@link Browser}: * An example of disconnecting from and reconnecting to a {@link Browser}:
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -466,163 +467,3 @@ export const enum BrowserContextEmittedEvents {
*/ */
TargetDestroyed = 'targetdestroyed', TargetDestroyed = 'targetdestroyed',
} }
/**
* BrowserContexts provide a way to operate multiple independent browser
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/
export class BrowserContext extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* An array of all active targets inside the browser context.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* This searches for a target in this specific browser context.
*
* @example
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number}
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all pages inside the browser context.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
*
* @remarks
* The default browser context cannot be closed.
*/
isIncognito(): boolean {
throw new Error('Not implemented');
}
/**
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
*/
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
overridePermissions(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Clears all permission overrides for the browser context.
*
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
clearPermissionOverrides(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Creates a new page in the browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* The browser this browser context belongs to.
*/
browser(): Browser {
throw new Error('Not implemented');
}
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
*
* @remarks
* Only incognito browser contexts can be closed.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
}

View file

@ -0,0 +1,185 @@
/**
* 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 {EventEmitter} from '../common/EventEmitter.js';
import {Page} from './Page.js';
import {Target} from '../common/Target.js';
import type {Permission, Browser} from './Browser.js';
/**
* BrowserContexts provide a way to operate multiple independent browser
* sessions. When a browser is launched, it has a single BrowserContext used by
* default. The method {@link Browser.newPage | Browser.newPage} creates a page
* in the default browser context.
*
* @remarks
*
* The Browser class extends from Puppeteer's {@link EventEmitter} class and
* will emit various events which are documented in the
* {@link BrowserContextEmittedEvents} enum.
*
* If a page opens another page, e.g. with a `window.open` call, the popup will
* belong to the parent page's browser context.
*
* Puppeteer allows creation of "incognito" browser contexts with
* {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext}
* method. "Incognito" browser contexts don't write any browsing data to disk.
*
* @example
*
* ```ts
* // Create a new incognito browser context
* const context = await browser.createIncognitoBrowserContext();
* // Create a new page inside context.
* const page = await context.newPage();
* // ... do stuff with page ...
* await page.goto('https://example.com');
* // Dispose context once it's no longer needed.
* await context.close();
* ```
*
* @public
*/
export class BrowserContext extends EventEmitter {
/**
* @internal
*/
constructor() {
super();
}
/**
* An array of all active targets inside the browser context.
*/
targets(): Target[] {
throw new Error('Not implemented');
}
/**
* This searches for a target in this specific browser context.
*
* @example
* An example of finding a target for a page opened via `window.open`:
*
* ```ts
* await page.evaluate(() => window.open('https://www.example.com/'));
* const newWindowTarget = await browserContext.waitForTarget(
* target => target.url() === 'https://www.example.com/'
* );
* ```
*
* @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timeout,
* which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found
* that matches the `predicate` function.
*/
waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: {timeout?: number}
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
}
/**
* An array of all pages inside the browser context.
*
* @returns Promise which resolves to an array of all open pages.
* Non visible pages, such as `"background_page"`, will not be listed here.
* You can find them using {@link Target.page | the target page}.
*/
pages(): Promise<Page[]> {
throw new Error('Not implemented');
}
/**
* Returns whether BrowserContext is incognito.
* The default browser context is the only non-incognito browser context.
*
* @remarks
* The default browser context cannot be closed.
*/
isIncognito(): boolean {
throw new Error('Not implemented');
}
/**
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* await context.overridePermissions('https://html5demos.com', [
* 'geolocation',
* ]);
* ```
*
* @param origin - The origin to grant permissions to, e.g. "https://example.com".
* @param permissions - An array of permissions to grant.
* All permissions that are not listed here will be automatically denied.
*/
overridePermissions(origin: string, permissions: Permission[]): Promise<void>;
overridePermissions(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Clears all permission overrides for the browser context.
*
* @example
*
* ```ts
* const context = browser.defaultBrowserContext();
* context.overridePermissions('https://example.com', ['clipboard-read']);
* // do stuff ..
* context.clearPermissionOverrides();
* ```
*/
clearPermissionOverrides(): Promise<void> {
throw new Error('Not implemented');
}
/**
* Creates a new page in the browser context.
*/
newPage(): Promise<Page> {
throw new Error('Not implemented');
}
/**
* The browser this browser context belongs to.
*/
browser(): Browser {
throw new Error('Not implemented');
}
/**
* Closes the browser context. All the targets that belong to the browser context
* will be closed.
*
* @remarks
* Only incognito browser contexts can be closed.
*/
close(): Promise<void> {
throw new Error('Not implemented');
}
get id(): string | undefined {
return undefined;
}
}

View file

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

View file

@ -564,7 +564,10 @@ class AXNode {
} }
for (const node of nodeById.values()) { for (const node of nodeById.values()) {
for (const childId of node.payload.childIds || []) { for (const childId of node.payload.childIds || []) {
node.children.push(nodeById.get(childId)!); const child = nodeById.get(childId);
if (child) {
node.children.push(child);
}
} }
} }
return nodeById.values().next().value; return nodeById.values().next().value;

View file

@ -15,12 +15,14 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {Frame} from './Frame.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js'; import type {ElementHandle} from './ElementHandle.js';
import {PuppeteerQueryHandler} from './QueryHandler.js'; import type {PuppeteerQueryHandler} from './QueryHandler.js';
import type {Frame} from './Frame.js';
async function queryAXTree( async function queryAXTree(
client: CDPSession, client: CDPSession,
@ -115,7 +117,7 @@ const waitFor: PuppeteerQueryHandler['waitFor'] = async (
) => { ) => {
let frame: Frame; let frame: Frame;
let element: ElementHandle<Node> | undefined; let element: ElementHandle<Node> | undefined;
if (elementOrFrame instanceof Frame) { if ('isOOPFrame' in elementOrFrame) {
frame = elementOrFrame; frame = elementOrFrame;
} else { } else {
frame = elementOrFrame.frame; frame = elementOrFrame.frame;
@ -146,16 +148,18 @@ const waitFor: PuppeteerQueryHandler['waitFor'] = async (
element, element,
selector, selector,
options, options,
new Set([ariaQuerySelector]) new Map([['ariaQuerySelector', ariaQuerySelector]])
); );
if (element) { if (element) {
await element.dispose(); await element.dispose();
} }
if (!(result instanceof ElementHandle)) {
const handle = result?.asElement();
if (!handle) {
await result?.dispose(); await result?.dispose();
return null; return null;
} }
return result.frame.worlds[MAIN_WORLD].transferHandle(result); return handle.frame.worlds[MAIN_WORLD].transferHandle(handle);
}; };
const queryAll: PuppeteerQueryHandler['queryAll'] = async ( const queryAll: PuppeteerQueryHandler['queryAll'] = async (

View file

@ -19,7 +19,7 @@ import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {waitWithTimeout} from './util.js'; import {waitWithTimeout} from './util.js';
import {Page} from './Page.js'; import {Page} from '../api/Page.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {TaskQueue} from './TaskQueue.js'; import {TaskQueue} from './TaskQueue.js';
@ -28,7 +28,6 @@ import {ChromeTargetManager} from './ChromeTargetManager.js';
import {FirefoxTargetManager} from './FirefoxTargetManager.js'; import {FirefoxTargetManager} from './FirefoxTargetManager.js';
import { import {
Browser as BrowserBase, Browser as BrowserBase,
BrowserContext,
BrowserCloseCallback, BrowserCloseCallback,
TargetFilterCallback, TargetFilterCallback,
IsPageTargetCallback, IsPageTargetCallback,
@ -39,6 +38,7 @@ import {
WaitForTargetOptions, WaitForTargetOptions,
Permission, Permission,
} from '../api/Browser.js'; } from '../api/Browser.js';
import {BrowserContext} from '../api/BrowserContext.js';
/** /**
* @internal * @internal
@ -596,6 +596,10 @@ export class CDPBrowserContext extends BrowserContext {
this.#id = contextId; this.#id = contextId;
} }
override get id(): string | undefined {
return this.#id;
}
/** /**
* An array of all active targets inside the browser context. * An array of all active targets inside the browser context.
*/ */
@ -619,7 +623,7 @@ export class CDPBrowserContext extends BrowserContext {
* ``` * ```
* *
* @param predicate - A function to be run for every target * @param predicate - A function to be run for every target
* @param options - An object of options. Accepts a timout, * @param options - An object of options. Accepts a timeout,
* which is the maximum wait time in milliseconds. * which is the maximum wait time in milliseconds.
* Pass `0` to disable the timeout. Defaults to 30 seconds. * Pass `0` to disable the timeout. Defaults to 30 seconds.
* @returns Promise which resolves to the first target found * @returns Promise which resolves to the first target found

View file

@ -24,6 +24,8 @@ import {Connection} from './Connection.js';
import {ConnectionTransport} from './ConnectionTransport.js'; import {ConnectionTransport} from './ConnectionTransport.js';
import {getFetch} from './fetch.js'; import {getFetch} from './fetch.js';
import {Viewport} from './PuppeteerViewport.js'; import {Viewport} from './PuppeteerViewport.js';
import type {ConnectOptions} from './Puppeteer.js';
/** /**
* Generic browser options that can be passed when launching any browser or when * Generic browser options that can be passed when launching any browser or when
* connecting to an existing browser instance. * connecting to an existing browser instance.
@ -61,7 +63,7 @@ export interface BrowserConnectOptions {
const getWebSocketTransportClass = async () => { const getWebSocketTransportClass = async () => {
return isNode return isNode
? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport ? (await import('./NodeWebSocketTransport.js')).NodeWebSocketTransport
: (await import('./BrowserWebSocketTransport.js')) : (await import('./BrowserWebSocketTransport.js'))
.BrowserWebSocketTransport; .BrowserWebSocketTransport;
}; };
@ -73,11 +75,7 @@ const getWebSocketTransportClass = async () => {
* @internal * @internal
*/ */
export async function _connectToCDPBrowser( export async function _connectToCDPBrowser(
options: BrowserConnectOptions & { options: BrowserConnectOptions & ConnectOptions
browserWSEndpoint?: string;
browserURL?: string;
transport?: ConnectionTransport;
}
): Promise<CDPBrowser> { ): Promise<CDPBrowser> {
const { const {
browserWSEndpoint, browserWSEndpoint,
@ -85,6 +83,7 @@ export async function _connectToCDPBrowser(
ignoreHTTPSErrors = false, ignoreHTTPSErrors = false,
defaultViewport = {width: 800, height: 600}, defaultViewport = {width: 800, height: 600},
transport, transport,
headers = {},
slowMo = 0, slowMo = 0,
targetFilter, targetFilter,
_isPageTarget: isPageTarget, _isPageTarget: isPageTarget,
@ -102,7 +101,7 @@ export async function _connectToCDPBrowser(
} else if (browserWSEndpoint) { } else if (browserWSEndpoint) {
const WebSocketClass = await getWebSocketTransportClass(); const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport = const connectionTransport: ConnectionTransport =
await WebSocketClass.create(browserWSEndpoint); await WebSocketClass.create(browserWSEndpoint, headers);
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
} else if (browserURL) { } else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL); const connectionURL = await getWSEndpoint(browserURL);

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import Protocol from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection} from './Connection.js'; import {CDPSession, Connection} from './Connection.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
@ -231,13 +231,6 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
const target = this.#targetFactory(event.targetInfo, undefined); const target = this.#targetFactory(event.targetInfo, undefined);
this.#attachedTargetsByTargetId.set(event.targetInfo.targetId, target); this.#attachedTargetsByTargetId.set(event.targetInfo.targetId, target);
} }
if (event.targetInfo.type === 'shared_worker') {
// Special case (https://crbug.com/1338156): currently, shared_workers
// don't get auto-attached. This should be removed once the auto-attach
// works.
await this.#connection._createSession(event.targetInfo, true);
}
}; };
#onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => { #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => {
@ -360,7 +353,7 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
// present in #attachedTargetsBySessionId. // present in #attachedTargetsBySessionId.
assert(this.#attachedTargetsBySessionId.has(parentSession.id())); assert(this.#attachedTargetsBySessionId.has(parentSession.id()));
} }
await interceptor( interceptor(
target, target,
parentSession instanceof Connection parentSession instanceof Connection
? null ? null

View file

@ -0,0 +1,135 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Product} from './Product.js';
/**
* Defines experiment options for Puppeteer.
*
* See individual properties for more information.
*
* @public
*/
export interface ExperimentsConfiguration {
/**
* Require Puppeteer to download Chromium for Apple M1.
*
* On Apple M1 devices Puppeteer by default downloads the version for
* Intel's processor which runs via Rosetta. It works without any problems,
* however, with this option, you should get more efficient resource usage
* (CPU and RAM) that could lead to a faster execution time.
*
* Can be overridden by `PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM`.
*
* @defaultValue `false`
*/
macArmChromiumEnabled?: boolean;
}
/**
* Defines options to configure Puppeteer's behavior during installation and
* runtime.
*
* See individual properties for more information.
*
* @public
*/
export interface Configuration {
/**
* Specifies a certain version of the browser you'd like Puppeteer to use.
*
* Can be overridden by `PUPPETEER_BROWSER_REVISION`.
*
* See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path
* is inferred.
*
* @defaultValue A compatible-revision of the browser.
*/
browserRevision?: string;
/**
* Defines the directory to be used by Puppeteer for caching.
*
* Can be overridden by `PUPPETEER_CACHE_DIR`.
*
* @defaultValue `path.join(os.homedir(), '.cache', 'puppeteer')`
*/
cacheDirectory?: string;
/**
* Specifies the URL prefix that is used to download Chromium.
*
* Can be overridden by `PUPPETEER_DOWNLOAD_HOST`.
*
* @remarks
* This must include the protocol and may even need a path prefix.
*
* @defaultValue Either https://storage.googleapis.com or
* https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central,
* depending on the product.
*/
downloadHost?: string;
/**
* Specifies the path for the downloads folder.
*
* Can be overridden by `PUPPETEER_DOWNLOAD_PATH`.
*
* @defaultValue `<cache>/<product>` where `<cache>` is Puppeteer's cache
* directory and `<product>` is the name of the browser.
*/
downloadPath?: string;
/**
* Specifies an executable path to be used in
* {@link PuppeteerNode.launch | puppeteer.launch}.
*
* Can be overridden by `PUPPETEER_EXECUTABLE_PATH`.
*
* @defaultValue Auto-computed.
*/
executablePath?: string;
/**
* Specifies which browser you'd like Puppeteer to use.
*
* Can be overridden by `PUPPETEER_PRODUCT`.
*
* @defaultValue `'chrome'`
*/
defaultProduct?: Product;
/**
* Defines the directory to be used by Puppeteer for creating temporary files.
*
* Can be overridden by `PUPPETEER_TMP_DIR`.
*
* @defaultValue `os.tmpdir()`
*/
temporaryDirectory?: string;
/**
* Tells Puppeteer to not download during installation.
*
* Can be overridden by `PUPPETEER_SKIP_DOWNLOAD`.
*/
skipDownload?: boolean;
/**
* Tells Puppeteer to log at the given level.
*
* At the moment, any option silences logging.
*
* @defaultValue `undefined`
*/
logLevel?: 'silent' | 'error' | 'warn';
/**
* Defines experimental options for Puppeteer.
*/
experiments?: ExperimentsConfiguration;
}

View file

@ -74,6 +74,12 @@ export interface JSCoverageOptions {
* Whether the result includes raw V8 script coverage entries. * Whether the result includes raw V8 script coverage entries.
*/ */
includeRawScriptCoverage?: boolean; includeRawScriptCoverage?: boolean;
/**
* Whether to collect coverage information at the block level.
* If true, coverage will be collected at the block level (this is the default).
* If false, coverage will be collected at the function level.
*/
useBlockCoverage?: boolean;
} }
/** /**
@ -135,7 +141,8 @@ export class Coverage {
/** /**
* @param options - Set of configurable options for coverage defaults to * @param options - Set of configurable options for coverage defaults to
* `resetOnNavigation : true, reportAnonymousScripts : false` * `resetOnNavigation : true, reportAnonymousScripts : false,`
* `includeRawScriptCoverage : false, useBlockCoverage : true`
* @returns Promise that resolves when coverage is started. * @returns Promise that resolves when coverage is started.
* *
* @remarks * @remarks
@ -204,6 +211,7 @@ export class JSCoverage {
resetOnNavigation?: boolean; resetOnNavigation?: boolean;
reportAnonymousScripts?: boolean; reportAnonymousScripts?: boolean;
includeRawScriptCoverage?: boolean; includeRawScriptCoverage?: boolean;
useBlockCoverage?: boolean;
} = {} } = {}
): Promise<void> { ): Promise<void> {
assert(!this.#enabled, 'JSCoverage is already enabled'); assert(!this.#enabled, 'JSCoverage is already enabled');
@ -211,6 +219,7 @@ export class JSCoverage {
resetOnNavigation = true, resetOnNavigation = true,
reportAnonymousScripts = false, reportAnonymousScripts = false,
includeRawScriptCoverage = false, includeRawScriptCoverage = false,
useBlockCoverage = true,
} = options; } = options;
this.#resetOnNavigation = resetOnNavigation; this.#resetOnNavigation = resetOnNavigation;
this.#reportAnonymousScripts = reportAnonymousScripts; this.#reportAnonymousScripts = reportAnonymousScripts;
@ -234,7 +243,7 @@ export class JSCoverage {
this.#client.send('Profiler.enable'), this.#client.send('Profiler.enable'),
this.#client.send('Profiler.startPreciseCoverage', { this.#client.send('Profiler.startPreciseCoverage', {
callCount: this.#includeRawScriptCoverage, callCount: this.#includeRawScriptCoverage,
detailed: true, detailed: useBlockCoverage,
}), }),
this.#client.send('Debugger.enable'), this.#client.send('Debugger.enable'),
this.#client.send('Debugger.setSkipAllPauses', {skip: true}), this.#client.send('Debugger.setSkipAllPauses', {skip: true}),

View file

@ -76,6 +76,9 @@ export async function importDebug(): Promise<typeof import('debug')> {
export const debug = (prefix: string): ((...args: unknown[]) => void) => { export const debug = (prefix: string): ((...args: unknown[]) => void) => {
if (isNode) { if (isNode) {
return async (...logArgs: unknown[]) => { return async (...logArgs: unknown[]) => {
if (captureLogs) {
capturedLogs.push(prefix + logArgs);
}
(await importDebug())(prefix)(logArgs); (await importDebug())(prefix)(logArgs);
}; };
} }
@ -107,3 +110,27 @@ export const debug = (prefix: string): ((...args: unknown[]) => void) => {
console.log(`${prefix}:`, ...logArgs); console.log(`${prefix}:`, ...logArgs);
}; };
}; };
/**
* @internal
*/
let capturedLogs: string[] = [];
/**
* @internal
*/
let captureLogs = false;
/**
* @internal
*/
export function setLogCapture(value: boolean): void {
capturedLogs = [];
captureLogs = value;
}
/**
* @internal
*/
export function getCapturedLogs(): string[] {
return capturedLogs;
}

View file

@ -14,23 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import {Viewport} from './PuppeteerViewport.js';
/** /**
* @public * @public
*/ */
export interface Device { export interface Device {
name: string;
userAgent: string; userAgent: string;
viewport: { viewport: Viewport;
width: number;
height: number;
deviceScaleFactor: number;
isMobile: boolean;
hasTouch: boolean;
isLandscape: boolean;
};
} }
const deviceArray: Device[] = [ const knownDevices = [
{ {
name: 'Blackberry PlayBook', name: 'Blackberry PlayBook',
userAgent: userAgent:
@ -1526,23 +1520,25 @@ const deviceArray: Device[] = [
isLandscape: true, isLandscape: true,
}, },
}, },
]; ] as const;
const knownDevicesByName = {} as Record<
typeof knownDevices[number]['name'],
Device
>;
for (const device of knownDevices) {
knownDevicesByName[device.name] = device;
}
/** /**
* @public * A list of devices to be used with {@link Page.emulate}.
*/
export type DevicesMap = {
[name: string]: Device;
};
/**
* A list of devices to be used with `page.emulate(options)`. Actual list of devices can be found in {@link https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts | src/common/DeviceDescriptors.ts}.
* *
* @example * @example
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import {KnownDevices} from 'puppeteer';
* const iPhone = puppeteer.devices['iPhone 6']; * const iPhone = KnownDevices['iPhone 6'];
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -1556,10 +1552,11 @@ export type DevicesMap = {
* *
* @public * @public
*/ */
const devices: DevicesMap = {}; export const KnownDevices = Object.freeze(knownDevicesByName);
for (const device of deviceArray) { /**
devices[device.name] = device; * @deprecated Import {@link KnownDevices}
} *
* @public
export {devices}; */
export const devices = KnownDevices;

View file

@ -26,7 +26,7 @@ import {Protocol} from 'devtools-protocol';
* @example * @example
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();

View file

@ -29,11 +29,12 @@ import {
Point, Point,
PressOptions, PressOptions,
} from './JSHandle.js'; } from './JSHandle.js';
import {Page, ScreenshotOptions} from './Page.js'; import {Page, ScreenshotOptions} from '../api/Page.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js'; import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {ElementFor, EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {KeyInput} from './USKeyboardLayout.js'; import {KeyInput} from './USKeyboardLayout.js';
import {debugError, isString} from './util.js'; import {debugError, isString} from './util.js';
import {CDPPage} from './Page.js';
const applyOffsetsToQuad = ( const applyOffsetsToQuad = (
quad: Point[], quad: Point[],
@ -52,7 +53,7 @@ const applyOffsetsToQuad = (
* ElementHandles can be created with the {@link Page.$} method. * ElementHandles can be created with the {@link Page.$} method.
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -236,8 +237,8 @@ export class ElementHandle<
Selector extends string, Selector extends string,
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc< Func extends EvaluateFunc<
[Array<NodeFor<Selector>>, ...Params] [HandleFor<Array<NodeFor<Selector>>>, ...Params]
> = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]> > = EvaluateFunc<[HandleFor<Array<NodeFor<Selector>>>, ...Params]>
>( >(
selector: Selector, selector: Selector,
pageFunction: Func | string, pageFunction: Func | string,
@ -253,9 +254,9 @@ export class ElementHandle<
this, this,
updatedSelector updatedSelector
)) as Array<HandleFor<NodeFor<Selector>>>; )) as Array<HandleFor<NodeFor<Selector>>>;
const elements = await this.evaluateHandle((_, ...elements) => { const elements = (await this.evaluateHandle((_, ...elements) => {
return elements; return elements;
}, ...handles); }, ...handles)) as JSHandle<Array<NodeFor<Selector>>>;
const [result] = await Promise.all([ const [result] = await Promise.all([
elements.evaluate(pageFunction, ...args), elements.evaluate(pageFunction, ...args),
...handles.map(handle => { ...handles.map(handle => {
@ -269,7 +270,12 @@ export class ElementHandle<
/** /**
* @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix. * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix.
* *
* Example: `await elementHandle.$$('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the elementHandle. * 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. * 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} * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/ */
@ -290,7 +296,7 @@ export class ElementHandle<
* @example * @example
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -335,6 +341,10 @@ export class ElementHandle<
* @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath` * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath`
* prefix. * 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 * 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 * method the `xpath` already exists, the method will return immediately. If
* the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
@ -343,10 +353,10 @@ export class ElementHandle<
* If `xpath` starts with `//` instead of `.//`, the dot will be appended * If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically. * automatically.
* *
* This method works across navigation * This method works across navigation.
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
* const page = await browser.newPage(); * const page = await browser.newPage();
@ -402,6 +412,37 @@ export class ElementHandle<
return this.waitForSelector(`xpath/${xpath}`, options); 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<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap
>(tagName: K): Promise<HandleFor<ElementFor<K>>> {
const isMatchingTagName = await this.evaluate((node, tagName) => {
return node.nodeName === tagName.toUpperCase();
}, tagName);
if (!isMatchingTagName) {
throw new Error(`Element is not a(n) \`${tagName}\` element`);
}
return this as unknown as HandleFor<ElementFor<K>>;
}
override asElement(): ElementHandle<ElementType> | null { override asElement(): ElementHandle<ElementType> | null {
return this; return this;
} }
@ -510,7 +551,7 @@ export class ElementHandle<
objectId: this.remoteObject().objectId, objectId: this.remoteObject().objectId,
}) })
.catch(debugError), .catch(debugError),
this.#page._client().send('Page.getLayoutMetrics'), (this.#page as CDPPage)._client().send('Page.getLayoutMetrics'),
]); ]);
if (!result || !result.quads.length) { if (!result || !result.quads.length) {
throw new Error('Node is either not clickable or not an HTMLElement'); throw new Error('Node is either not clickable or not an HTMLElement');

View file

@ -15,9 +15,14 @@
*/ */
/** /**
* @deprecated Do not use.
*
* @public * @public
*/ */
export class CustomError extends Error { export class CustomError extends Error {
/**
* @internal
*/
constructor(message?: string) { constructor(message?: string) {
super(message); super(message);
this.name = this.constructor.name; this.name = this.constructor.name;
@ -43,11 +48,39 @@ export class TimeoutError extends CustomError {}
* @public * @public
*/ */
export class ProtocolError extends CustomError { export class ProtocolError extends CustomError {
public code?: number; #code?: number;
public originalMessage = ''; #originalMessage = '';
/**
* @internal
*/
set code(code: number | undefined) {
this.#code = code;
}
/**
* @public
*/
get code(): number | undefined {
return this.#code;
}
/**
* @internal
*/
set originalMessage(originalMessage: string) {
this.#originalMessage = originalMessage;
}
/**
* @public
*/
get originalMessage(): string {
return this.#originalMessage;
}
} }
/** /**
* @deprecated Do not use.
*
* @public * @public
*/ */
export interface PuppeteerErrors { export interface PuppeteerErrors {
@ -56,6 +89,8 @@ export interface PuppeteerErrors {
} }
/** /**
* @deprecated Import error classes directly.
*
* Puppeteer methods might throw errors if they are unable to fulfill a request. * Puppeteer methods might throw errors if they are unable to fulfill a request.
* For example, `page.waitForSelector(selector[, options])` might fail if the * For example, `page.waitForSelector(selector[, options])` might fail if the
* selector doesn't match any nodes during the given timeframe. * selector doesn't match any nodes during the given timeframe.
@ -70,7 +105,7 @@ export interface PuppeteerErrors {
* try { * try {
* await page.waitForSelector('.foo'); * await page.waitForSelector('.foo');
* } catch (e) { * } catch (e) {
* if (e instanceof puppeteer.errors.TimeoutError) { * if (e instanceof TimeoutError) {
* // Do something if this is a timeout. * // Do something if this is a timeout.
* } * }
* } * }

View file

@ -14,16 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
import mitt, { import mitt, {Emitter, EventHandlerMap} from '../../third_party/mitt/index.js';
Emitter,
EventType,
Handler,
} from '../../vendor/mitt/src/index.js';
/** /**
* @public * @public
*/ */
export {EventType, Handler}; export type EventType = string | symbol;
/**
* @public
*/
export type Handler<T = unknown> = (event: T) => void;
/** /**
* @public * @public
@ -57,8 +57,8 @@ export interface CommonEventEmitter {
* @public * @public
*/ */
export class EventEmitter implements CommonEventEmitter { export class EventEmitter implements CommonEventEmitter {
private emitter: Emitter; private emitter: Emitter<Record<string | symbol, any>>;
private eventsMap = new Map<EventType, Handler[]>(); private eventsMap: EventHandlerMap<Record<string | symbol, any>> = new Map();
/** /**
* @internal * @internal
@ -73,7 +73,7 @@ export class EventEmitter implements CommonEventEmitter {
* @param handler - the function to be called when the event occurs. * @param handler - the function to be called when the event occurs.
* @returns `this` to enable you to chain method calls. * @returns `this` to enable you to chain method calls.
*/ */
on(event: EventType, handler: Handler): EventEmitter { on(event: EventType, handler: Handler<any>): EventEmitter {
this.emitter.on(event, handler); this.emitter.on(event, handler);
return this; return this;
} }
@ -84,7 +84,7 @@ export class EventEmitter implements CommonEventEmitter {
* @param handler - the function that should be removed. * @param handler - the function that should be removed.
* @returns `this` to enable you to chain method calls. * @returns `this` to enable you to chain method calls.
*/ */
off(event: EventType, handler: Handler): EventEmitter { off(event: EventType, handler: Handler<any>): EventEmitter {
this.emitter.off(event, handler); this.emitter.off(event, handler);
return this; return this;
} }
@ -93,7 +93,7 @@ export class EventEmitter implements CommonEventEmitter {
* Remove an event listener. * Remove an event listener.
* @deprecated please use {@link EventEmitter.off} instead. * @deprecated please use {@link EventEmitter.off} instead.
*/ */
removeListener(event: EventType, handler: Handler): EventEmitter { removeListener(event: EventType, handler: Handler<any>): EventEmitter {
this.off(event, handler); this.off(event, handler);
return this; return this;
} }
@ -102,7 +102,7 @@ export class EventEmitter implements CommonEventEmitter {
* Add an event listener. * Add an event listener.
* @deprecated please use {@link EventEmitter.on} instead. * @deprecated please use {@link EventEmitter.on} instead.
*/ */
addListener(event: EventType, handler: Handler): EventEmitter { addListener(event: EventType, handler: Handler<any>): EventEmitter {
this.on(event, handler); this.on(event, handler);
return this; return this;
} }
@ -125,8 +125,8 @@ export class EventEmitter implements CommonEventEmitter {
* @param handler - the handler function to run when the event occurs * @param handler - the handler function to run when the event occurs
* @returns `this` to enable you to chain method calls. * @returns `this` to enable you to chain method calls.
*/ */
once(event: EventType, handler: Handler): EventEmitter { once(event: EventType, handler: Handler<any>): EventEmitter {
const onceHandler: Handler = eventData => { const onceHandler: Handler<any> = eventData => {
handler(eventData); handler(eventData);
this.off(event, onceHandler); this.off(event, onceHandler);
}; };

View file

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

View file

@ -26,12 +26,11 @@ import {MouseButton} from './Input.js';
import { import {
IsolatedWorld, IsolatedWorld,
IsolatedWorldChart, IsolatedWorldChart,
MAIN_WORLD,
PUPPETEER_WORLD,
WaitForSelectorOptions, WaitForSelectorOptions,
} from './IsolatedWorld.js'; } from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {Page} from './Page.js'; import {Page} from '../api/Page.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js'; import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {importFS} from './util.js'; import {importFS} from './util.js';
@ -128,7 +127,7 @@ export interface FrameAddStyleTagOptions {
* An example of dumping frame tree: * An example of dumping frame tree:
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -290,12 +289,16 @@ export class Frame {
url: string, url: string,
options: { options: {
referer?: string; referer?: string;
referrerPolicy?: string;
timeout?: number; timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
} = {} } = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
const { const {
referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'], referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'],
referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[
'referer-policy'
],
waitUntil = ['load'], waitUntil = ['load'],
timeout = this._frameManager.timeoutSettings.navigationTimeout(), timeout = this._frameManager.timeoutSettings.navigationTimeout(),
} = options; } = options;
@ -308,7 +311,13 @@ export class Frame {
timeout timeout
); );
let error = await Promise.race([ let error = await Promise.race([
navigate(this.#client, url, referer, this._id), navigate(
this.#client,
url,
referer,
referrerPolicy as Protocol.Page.ReferrerPolicy,
this._id
),
watcher.timeoutOrTerminationPromise(), watcher.timeoutOrTerminationPromise(),
]); ]);
if (!error) { if (!error) {
@ -333,6 +342,7 @@ export class Frame {
client: CDPSession, client: CDPSession,
url: string, url: string,
referrer: string | undefined, referrer: string | undefined,
referrerPolicy: Protocol.Page.ReferrerPolicy | undefined,
frameId: string frameId: string
): Promise<Error | null> { ): Promise<Error | null> {
try { try {
@ -340,6 +350,7 @@ export class Frame {
url, url,
referrer, referrer,
frameId, frameId,
referrerPolicy,
}); });
ensureNewDocumentNavigation = !!response.loaderId; ensureNewDocumentNavigation = !!response.loaderId;
return response.errorText return response.errorText
@ -551,7 +562,11 @@ export class Frame {
/** /**
* @deprecated Use {@link Frame.$$} with the `xpath` prefix. * @deprecated Use {@link Frame.$$} with the `xpath` prefix.
* *
* Example: `await frame.$$('xpath/' + xpathExpression)`
*
* This method evaluates the given XPath expression and returns the results. * This method evaluates the given XPath expression and returns the results.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
* @param expression - the XPath expression to evaluate. * @param expression - the XPath expression to evaluate.
*/ */
async $x(expression: string): Promise<Array<ElementHandle<Node>>> { async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
@ -566,7 +581,7 @@ export class Frame {
* @example * @example
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* const browser = await puppeteer.launch(); * const browser = await puppeteer.launch();
@ -610,6 +625,12 @@ export class Frame {
/** /**
* @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix. * @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix.
* *
* Example: `await frame.waitForSelector('xpath/' + xpathExpression)`
*
* The method evaluates the XPath expression relative to the Frame.
* If `xpath` starts with `//` instead of `.//`, the dot will be appended
* automatically.
*
* Wait for the `xpath` to appear in page. If at the moment of calling the * Wait for the `xpath` to appear in page. If at the moment of calling the
* method the `xpath` already exists, the method will return immediately. If * method the `xpath` already exists, the method will return immediately. If
* the xpath doesn't appear after the `timeout` milliseconds of waiting, the * the xpath doesn't appear after the `timeout` milliseconds of waiting, the
@ -620,7 +641,7 @@ export class Frame {
* an XPath. * an XPath.
* *
* @param xpath - the XPath expression to wait for. * @param xpath - the XPath expression to wait for.
* @param options - options to configure the visiblity of the element and how * @param options - options to configure the visibility of the element and how
* long to wait before timing out. * long to wait before timing out.
*/ */
async waitForXPath( async waitForXPath(
@ -638,7 +659,7 @@ export class Frame {
* The `waitForFunction` can be used to observe viewport size change: * The `waitForFunction` can be used to observe viewport size change:
* *
* ```ts * ```ts
* const puppeteer = require('puppeteer'); * import puppeteer from 'puppeteer';
* *
* (async () => { * (async () => {
* . const browser = await puppeteer.launch(); * . const browser = await puppeteer.launch();
@ -1020,7 +1041,7 @@ export class Frame {
} }
/** /**
* @deprecated Use `new Promise(r => setTimeout(r, milliseconds));`. * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`.
* *
* Causes your script to wait for the given number of milliseconds. * Causes your script to wait for the given number of milliseconds.
* *

View file

@ -22,9 +22,10 @@ import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {FrameTree} from './FrameTree.js'; import {FrameTree} from './FrameTree.js';
import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {NetworkManager} from './NetworkManager.js'; import {NetworkManager} from './NetworkManager.js';
import {Page} from './Page.js'; import {Page} from '../api/Page.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
@ -67,6 +68,13 @@ export class FrameManager extends EventEmitter {
*/ */
_frameTree = new FrameTree(); _frameTree = new FrameTree();
/**
* Set of frame IDs stored to indicate if a frame has received a
* frameNavigated event so that frame tree responses could be ignored as the
* frameNavigated event usually contains the latest information.
*/
#frameNavigatedReceived = new Set<string>();
get timeoutSettings(): TimeoutSettings { get timeoutSettings(): TimeoutSettings {
return this.#timeoutSettings; return this.#timeoutSettings;
} }
@ -98,6 +106,7 @@ export class FrameManager extends EventEmitter {
this.#onFrameAttached(session, event.frameId, event.parentFrameId); this.#onFrameAttached(session, event.frameId, event.parentFrameId);
}); });
session.on('Page.frameNavigated', event => { session.on('Page.frameNavigated', event => {
this.#frameNavigatedReceived.add(event.frame.id);
this.#onFrameNavigated(event.frame); this.#onFrameNavigated(event.frame);
}); });
session.on('Page.navigatedWithinDocument', event => { session.on('Page.navigatedWithinDocument', event => {
@ -202,15 +211,6 @@ export class FrameManager extends EventEmitter {
this.initialize(target._session()); this.initialize(target._session());
} }
onDetachedFromTarget(target: Target): void {
const frame = this.frame(target._targetId);
if (frame && frame.isOOPFrame()) {
// When an OOP iframe is removed from the page, it
// will only get a Target.detachedFromTarget event.
this.#removeFramesRecursively(frame);
}
}
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.frame(event.frameId); const frame = this.frame(event.frameId);
if (!frame) { if (!frame) {
@ -248,7 +248,12 @@ export class FrameManager extends EventEmitter {
frameTree.frame.parentId frameTree.frame.parentId
); );
} }
this.#onFrameNavigated(frameTree.frame); if (!this.#frameNavigatedReceived.has(frameTree.frame.id)) {
this.#onFrameNavigated(frameTree.frame);
} else {
this.#frameNavigatedReceived.delete(frameTree.frame.id);
}
if (!frameTree.childFrames) { if (!frameTree.childFrames) {
return; return;
} }
@ -383,8 +388,7 @@ export class FrameManager extends EventEmitter {
if (frame._client() !== session) { if (frame._client() !== session) {
return; return;
} }
if (contextPayload.auxData && contextPayload.auxData['isDefault']) {
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame.worlds[MAIN_WORLD]; world = frame.worlds[MAIN_WORLD];
} else if ( } else if (
contextPayload.name === UTILITY_WORLD_NAME && contextPayload.name === UTILITY_WORLD_NAME &&

View file

@ -35,7 +35,7 @@ type KeyDescription = Required<
* {@link Keyboard.up}, and {@link Keyboard.sendCharacter} * {@link Keyboard.up}, and {@link Keyboard.sendCharacter}
* to manually fire events as if they were generated from a real keyboard. * to manually fire events as if they were generated from a real keyboard.
* *
* On MacOS, keyboard shortcuts like `⌘ A` -\> Select All do not work. * On macOS, keyboard shortcuts like `⌘ A` -\> Select All do not work.
* See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}. * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}.
* *
* @example * @example
@ -104,11 +104,16 @@ export class Keyboard {
* See {@link KeyInput} for a list of all key names. * See {@link KeyInput} for a list of all key names.
* *
* @param options - An object of options. Accepts text which, if specified, * @param options - An object of options. Accepts text which, if specified,
* generates an input event with this text. * generates an input event with this text. Accepts commands which, if specified,
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/ */
async down( async down(
key: KeyInput, key: KeyInput,
options: {text?: string} = {text: undefined} options: {text?: string; commands?: string[]} = {
text: undefined,
commands: [],
}
): Promise<void> { ): Promise<void> {
const description = this.#keyDescriptionForString(key); const description = this.#keyDescriptionForString(key);
@ -128,6 +133,7 @@ export class Keyboard {
autoRepeat, autoRepeat,
location: description.location, location: description.location,
isKeypad: description.location === 3, isKeypad: description.location === 3,
commands: options.commands,
}); });
} }
@ -304,11 +310,13 @@ export class Keyboard {
* @param options - An object of options. Accepts text which, if specified, * @param options - An object of options. Accepts text which, if specified,
* generates an input event with this text. Accepts delay which, * generates an input event with this text. Accepts delay which,
* if specified, is the time to wait between `keydown` and `keyup` in milliseconds. * if specified, is the time to wait between `keydown` and `keyup` in milliseconds.
* Defaults to 0. * Defaults to 0. Accepts commands which, if specified,
* is the commands of keyboard shortcuts,
* see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names.
*/ */
async press( async press(
key: KeyInput, key: KeyInput,
options: {delay?: number; text?: string} = {} options: {delay?: number; text?: string; commands?: string[]} = {}
): Promise<void> { ): Promise<void> {
const {delay = null} = options; const {delay = null} = options;
await this.down(key, options); await this.down(key, options);
@ -615,7 +623,7 @@ export class Mouse {
/** /**
* Performs a drag, dragenter, dragover, and drop in sequence. * Performs a drag, dragenter, dragover, and drop in sequence.
* @param target - point to drag from * @param start - point to drag from
* @param target - point to drop on * @param target - point to drop on
* @param options - An object of options. Accepts delay which, * @param options - An object of options. Accepts delay which,
* if specified, is the time to wait between `dragover` and `drop` in milliseconds. * if specified, is the time to wait between `dragover` and `drop` in milliseconds.

View file

@ -16,12 +16,10 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {source as injectedSource} from '../generated/injected.js'; import {source as injectedSource} from '../generated/injected.js';
import type PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {createDeferredPromise} from '../util/DeferredPromise.js'; import {createDeferredPromise} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js'; import {FrameManager} from './FrameManager.js';
@ -30,9 +28,13 @@ import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js'; import {LazyArg} from './LazyArg.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, HandleFor, InnerLazyParams, NodeFor} from './types.js';
import {createJSHandle, debugError, pageBindingInitString} from './util.js'; import {createJSHandle, debugError, pageBindingInitString} from './util.js';
import {TaskManager, WaitTask} from './WaitTask.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 * @public
@ -70,20 +72,6 @@ export interface PageBinding {
pptrFunction: Function; pptrFunction: Function;
} }
/**
* A unique key for {@link IsolatedWorldChart} to denote the default world.
* Execution contexts are automatically created in the default world.
*
* @internal
*/
export const MAIN_WORLD = Symbol('mainWorld');
/**
* A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
* This world contains all puppeteer-internal bindings/code.
*
* @internal
*/
export const PUPPETEER_WORLD = Symbol('puppeteerWorld');
/** /**
* @internal * @internal
*/ */
@ -486,7 +474,7 @@ export class IsolatedWorld {
); );
} catch (error) { } catch (error) {
// The WaitTask may already have been resolved by timing out, or the // The WaitTask may already have been resolved by timing out, or the
// exection context may have been destroyed. // execution context may have been destroyed.
// In both caes, the promises above are rejected with a protocol error. // In both caes, the promises above are rejected with a protocol error.
// We can safely ignores these, as the WaitTask is re-installed in // We can safely ignores these, as the WaitTask is re-installed in
// the next execution context if needed. // the next execution context if needed.
@ -502,7 +490,7 @@ export class IsolatedWorld {
root: ElementHandle<Node> | undefined, root: ElementHandle<Node> | undefined,
selector: string, selector: string,
options: WaitForSelectorOptions, options: WaitForSelectorOptions,
bindings = new Set<(...args: never[]) => unknown>() bindings = new Map<string, (...args: never[]) => unknown>()
): Promise<JSHandle<unknown> | null> { ): Promise<JSHandle<unknown> | null> {
const { const {
visible: waitForVisible = false, visible: waitForVisible = false,
@ -559,14 +547,16 @@ export class IsolatedWorld {
waitForFunction< waitForFunction<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>
>( >(
pageFunction: Func | string, pageFunction: Func | string,
options: { options: {
polling?: 'raf' | 'mutation' | number; polling?: 'raf' | 'mutation' | number;
timeout?: number; timeout?: number;
root?: ElementHandle<Node>; root?: ElementHandle<Node>;
bindings?: Set<(...args: never[]) => unknown>; bindings?: Map<string, (...args: never[]) => unknown>;
} = {}, } = {},
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {

View file

@ -0,0 +1,30 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A unique key for {@link IsolatedWorldChart} to denote the default world.
* Execution contexts are automatically created in the default world.
*
* @internal
*/
export const MAIN_WORLD = Symbol('mainWorld');
/**
* A unique key for {@link IsolatedWorldChart} to denote the puppeteer world.
* This world contains all puppeteer-internal bindings/code.
*
* @internal
*/
export const PUPPETEER_WORLD = Symbol('puppeteerWorld');

View file

@ -167,7 +167,7 @@ export class JSHandle<T = unknown> {
propertyName: HandleOr<K> propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> { ): Promise<HandleFor<T[K]>> {
return this.evaluateHandle((object, propertyName) => { return this.evaluateHandle((object, propertyName) => {
return object[propertyName]; return object[propertyName as K];
}, propertyName); }, propertyName);
} }

View file

@ -168,6 +168,11 @@ export class LifecycleWatcher {
NetworkManagerEmittedEvents.Response, NetworkManagerEmittedEvents.Response,
this.#onResponse.bind(this) this.#onResponse.bind(this)
), ),
addEventListener(
this.#frameManager.networkManager,
NetworkManagerEmittedEvents.RequestFailed,
this.#onRequestFailed.bind(this)
),
]; ];
this.#timeoutPromise = this.#createTimeoutPromise(); this.#timeoutPromise = this.#createTimeoutPromise();
@ -189,6 +194,13 @@ export class LifecycleWatcher {
} }
} }
#onRequestFailed(request: HTTPRequest): void {
if (this.#navigationRequest?._requestId !== request._requestId) {
return;
}
this.#navigationResponseReceived?.resolve();
}
#onResponse(response: HTTPResponse): void { #onResponse(response: HTTPResponse): void {
if (this.#navigationRequest?._requestId !== response.request()._requestId) { if (this.#navigationRequest?._requestId !== response.request()._requestId) {
return; return;

View file

@ -21,7 +21,10 @@ import {packageVersion} from '../generated/version.js';
* @internal * @internal
*/ */
export class NodeWebSocketTransport implements ConnectionTransport { export class NodeWebSocketTransport implements ConnectionTransport {
static create(url: string): Promise<NodeWebSocketTransport> { static create(
url: string,
headers?: Record<string, string>
): Promise<NodeWebSocketTransport> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ws = new NodeWebSocket(url, [], { const ws = new NodeWebSocket(url, [], {
followRedirects: true, followRedirects: true,
@ -29,6 +32,7 @@ export class NodeWebSocketTransport implements ConnectionTransport {
maxPayload: 256 * 1024 * 1024, // 256Mb maxPayload: 256 * 1024 * 1024, // 256Mb
headers: { headers: {
'User-Agent': `Puppeteer ${packageVersion}`, 'User-Agent': `Puppeteer ${packageVersion}`,
...headers,
}, },
}); });

View file

@ -167,7 +167,7 @@ export interface PDFOptions {
*/ */
omitBackground?: boolean; omitBackground?: boolean;
/** /**
* Timeout in milliseconds * Timeout in milliseconds. Pass `0` to disable timeout.
* @defaultValue 30000 * @defaultValue 30000
*/ */
timeout?: number; timeout?: number;

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