Bug 1791532 - [puppeteer] Sync puppeteer v17.1.2 r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D160331
This commit is contained in:
Alexandra Borovova 2022-10-27 07:22:47 +00:00
parent 51aa9fcd1f
commit 428f90d6b8
99 changed files with 4253 additions and 2404 deletions

View file

@ -108,20 +108,23 @@ _OPT\.OBJ/
^devtools/.*/node_modules/ ^devtools/.*/node_modules/
# Ignore node_module directories and npm artifacts # Ignore node_module directories and npm artifacts
^remote/test/puppeteer/.github ^remote/test/puppeteer/.*\.tsbuildinfo
^remote/test/puppeteer/.husky ^remote/test/puppeteer/\.github
^remote/test/puppeteer/.local-chromium/ ^remote/test/puppeteer/\.husky
^remote/test/puppeteer/.local-firefox/ ^remote/test/puppeteer/\.local-chromium/
^remote/test/puppeteer/\.local-firefox/
^remote/test/puppeteer/coverage/ ^remote/test/puppeteer/coverage/
^remote/test/puppeteer/docker/ ^remote/test/puppeteer/docker/
^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/src/generated
^remote/test/puppeteer/test/build ^remote/test/puppeteer/test/build
^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/utils/testserver/tsconfig.tsbuildinfo
^remote/test/puppeteer/website ^remote/test/puppeteer/website
# git checkout of libstagefright # git checkout of libstagefright

5
remote/.gitignore vendored
View file

@ -1,15 +1,18 @@
test/puppeteer/**/*.tsbuildinfo
test/puppeteer/.github test/puppeteer/.github
test/puppeteer/.husky test/puppeteer/.husky
test/puppeteer/.local-chromium/ test/puppeteer/.local-chromium/
test/puppeteer/.local-firefox/ test/puppeteer/.local-firefox/
test/puppeteer/coverage/ test/puppeteer/coverage/
test/puppeteer/docker/ test/puppeteer/docker/
test/puppeteer/docs/puppeteer.api.json
test/puppeteer/experimental/ test/puppeteer/experimental/
test/puppeteer/lib/ 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/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/utils/testserver/tsconfig.tsbuildinfo
test/puppeteer/website test/puppeteer/website

View file

@ -710,6 +710,12 @@ def install_puppeteer(command_context, product, ci):
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=os.path.join(command_context.topsrcdir, puppeteer_dir), env=env)
npm(
"run",
"build:dev",
cwd=os.path.join(command_context.topsrcdir, puppeteer_dir),
env=env,
)
def exit(code, error=None): def exit(code, error=None):

View file

@ -698,6 +698,9 @@
"ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails (ignorehttpserrors.spec.js)": [ "ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails (ignorehttpserrors.spec.js)": [
"PASS", "FAIL" "PASS", "FAIL"
], ],
"InjectedUtil tests should work (injected.spec.js)": [
"PASS"
],
"input tests input should upload the file (input.spec.js)": [ "input tests input should upload the file (input.spec.js)": [
"SKIP" "SKIP"
], ],
@ -924,7 +927,7 @@
"PASS" "PASS"
], ],
"Launcher specs Puppeteer Puppeteer.launch userDataDir option should restore cookies (launcher.spec.js)": [ "Launcher specs Puppeteer Puppeteer.launch userDataDir option should restore cookies (launcher.spec.js)": [
"SKIP" "PASS"
], ],
"Launcher specs Puppeteer Puppeteer.launch should return the default arguments (launcher.spec.js)": [ "Launcher specs Puppeteer Puppeteer.launch should return the default arguments (launcher.spec.js)": [
"PASS" "PASS"
@ -1097,9 +1100,6 @@
"navigation Page.goto should fail when navigating to bad SSL after redirects (navigation.spec.js)": [ "navigation Page.goto should fail when navigating to bad SSL after redirects (navigation.spec.js)": [
"PASS" "PASS"
], ],
"navigation Page.goto should throw if networkidle is passed as an option (navigation.spec.js)": [
"PASS"
],
"navigation Page.goto should fail when main resources failed to load (navigation.spec.js)": [ "navigation Page.goto should fail when main resources failed to load (navigation.spec.js)": [
"PASS" "PASS"
], ],
@ -2176,6 +2176,9 @@
"Screenshots Page.screenshot should clip rect (screenshot.spec.js)": [ "Screenshots Page.screenshot should clip rect (screenshot.spec.js)": [
"FAIL" "FAIL"
], ],
"Screenshots Page.screenshot should use scale for clip (screenshot.spec.js)": [
"FAIL"
],
"Screenshots Page.screenshot should get screenshot bigger than the viewport (screenshot.spec.js)": [ "Screenshots Page.screenshot should get screenshot bigger than the viewport (screenshot.spec.js)": [
"PASS" "PASS"
], ],

View file

@ -7,7 +7,7 @@ build/
lib/ lib/
# Generated files # Generated files
tsconfig.tsbuildinfo **/*.tsbuildinfo
puppeteer.api.json puppeteer.api.json
puppeteer*.tgz puppeteer*.tgz
yarn.lock yarn.lock
@ -18,9 +18,11 @@ yarn.lock
test/output-*/ test/output-*/
.dev_profile* .dev_profile*
coverage/ coverage/
src/generated
# IDE Artifacts # IDE Artifacts
.vscode .vscode
.devcontainer
# Misc # Misc
.DS_Store .DS_Store
@ -32,6 +34,7 @@ coverage/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
## [END] Keep in sync with .gitignore ## [END] Keep in sync with .gitignore
# ESLint ignores. # ESLint ignores.

View file

@ -7,7 +7,7 @@ build/
lib/ lib/
# Generated files # Generated files
tsconfig.tsbuildinfo **/*.tsbuildinfo
puppeteer.api.json puppeteer.api.json
puppeteer*.tgz puppeteer*.tgz
yarn.lock yarn.lock
@ -18,9 +18,11 @@ yarn.lock
test/output-*/ test/output-*/
.dev_profile* .dev_profile*
coverage/ coverage/
src/generated
# IDE Artifacts # IDE Artifacts
.vscode .vscode
.devcontainer
# Misc # Misc
.DS_Store .DS_Store

View file

@ -1,3 +1,3 @@
{ {
".": "16.1.1" ".": "17.1.2"
} }

View file

@ -2,6 +2,65 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07)
### Bug Fixes
* add missing code coverage ranges that span only a single character ([#8911](https://github.com/puppeteer/puppeteer/issues/8911)) ([0c577b9](https://github.com/puppeteer/puppeteer/commit/0c577b9bf8855dc0ccb6098cd43a25c528f6d7f5))
* add Page.getDefaultTimeout getter ([#8903](https://github.com/puppeteer/puppeteer/issues/8903)) ([3240095](https://github.com/puppeteer/puppeteer/commit/32400954c50cbddc48468ad118c3f8a47653b9d3)), closes [#8901](https://github.com/puppeteer/puppeteer/issues/8901)
* don't detect project root for puppeteer-core ([#8907](https://github.com/puppeteer/puppeteer/issues/8907)) ([b4f5ea1](https://github.com/puppeteer/puppeteer/commit/b4f5ea1167a60c870194c70d22f5372ada5b7c4c)), closes [#8896](https://github.com/puppeteer/puppeteer/issues/8896)
* support scale for screenshot clips ([#8908](https://github.com/puppeteer/puppeteer/issues/8908)) ([260e428](https://github.com/puppeteer/puppeteer/commit/260e4282275ab1d05c86e5643e2a02c01f269a9c)), closes [#5329](https://github.com/puppeteer/puppeteer/issues/5329)
* work around a race in waitForFileChooser ([#8905](https://github.com/puppeteer/puppeteer/issues/8905)) ([053d960](https://github.com/puppeteer/puppeteer/commit/053d960fb593e514e7914d7da9af436afc39a12f)), closes [#6040](https://github.com/puppeteer/puppeteer/issues/6040)
## [17.1.1](https://github.com/puppeteer/puppeteer/compare/v17.1.0...v17.1.1) (2022-09-05)
### Bug Fixes
* restore deferred promise debugging ([#8895](https://github.com/puppeteer/puppeteer/issues/8895)) ([7b42250](https://github.com/puppeteer/puppeteer/commit/7b42250c7bb91ac873307acda493726ffc4c54a8))
## [17.1.0](https://github.com/puppeteer/puppeteer/compare/v17.0.0...v17.1.0) (2022-09-02)
### Features
* **chromium:** roll to Chromium 106.0.5249.0 (r1036745) ([#8869](https://github.com/puppeteer/puppeteer/issues/8869)) ([6e9a47a](https://github.com/puppeteer/puppeteer/commit/6e9a47a6faa06d241dec0bcf7bcdf49370517008))
### Bug Fixes
* allow getting a frame from an elementhandle ([#8875](https://github.com/puppeteer/puppeteer/issues/8875)) ([3732757](https://github.com/puppeteer/puppeteer/commit/3732757450b4363041ccbacc3b236289a156abb0))
* typos in documentation ([#8858](https://github.com/puppeteer/puppeteer/issues/8858)) ([8d95a9b](https://github.com/puppeteer/puppeteer/commit/8d95a9bc920b98820aa655ad4eb2d8fd9b2b893a))
* use the timeout setting in waitForFileChooser ([#8856](https://github.com/puppeteer/puppeteer/issues/8856)) ([f477b46](https://github.com/puppeteer/puppeteer/commit/f477b46f212da9206102da695697760eea539f05))
## [17.0.0](https://github.com/puppeteer/puppeteer/compare/v16.2.0...v17.0.0) (2022-08-26)
### ⚠ BREAKING CHANGES
* remove `root` from `WaitForSelectorOptions` (#8848)
* internalize execution context (#8844)
### Bug Fixes
* allow multiple navigations to happen in LifecycleWatcher ([#8826](https://github.com/puppeteer/puppeteer/issues/8826)) ([341b669](https://github.com/puppeteer/puppeteer/commit/341b669a5e45ecbb9ffb0f28c45b520660f27ad2)), closes [#8811](https://github.com/puppeteer/puppeteer/issues/8811)
* internalize execution context ([#8844](https://github.com/puppeteer/puppeteer/issues/8844)) ([2f33237](https://github.com/puppeteer/puppeteer/commit/2f33237d0443de77d58dca4454b0c9a1d2b57d03))
* remove `root` from `WaitForSelectorOptions` ([#8848](https://github.com/puppeteer/puppeteer/issues/8848)) ([1155c8e](https://github.com/puppeteer/puppeteer/commit/1155c8eac85b176c3334cc3d98adfe7d943dfbe6))
* remove deferred promise timeouts ([#8835](https://github.com/puppeteer/puppeteer/issues/8835)) ([202ffce](https://github.com/puppeteer/puppeteer/commit/202ffce0aa4f34dba35fbb8e7d740af16efee35f)), closes [#8832](https://github.com/puppeteer/puppeteer/issues/8832)
## [16.2.0](https://github.com/puppeteer/puppeteer/compare/v16.1.1...v16.2.0) (2022-08-18)
### Features
* add Khmer (Cambodian) language support ([#8809](https://github.com/puppeteer/puppeteer/issues/8809)) ([34f8737](https://github.com/puppeteer/puppeteer/commit/34f873721804d57a5faf3eab8ef50340c69ed180))
### Bug Fixes
* handle service workers in extensions ([#8807](https://github.com/puppeteer/puppeteer/issues/8807)) ([2a0eefb](https://github.com/puppeteer/puppeteer/commit/2a0eefb99f0ae00dacc9e768a253308c0d18a4c3)), closes [#8800](https://github.com/puppeteer/puppeteer/issues/8800)
## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16) ## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16)

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: a4938d7edc53fbb1d217914981155ce3bbcc149f release: 0d2d99efeca73fba255fb10b28b5d3f50c2e20e4
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,6 +1,6 @@
{ {
"name": "puppeteer", "name": "puppeteer",
"version": "16.1.1", "version": "17.1.2",
"description": "A high-level API to control headless Chrome over the DevTools Protocol", "description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [ "keywords": [
"puppeteer", "puppeteer",
@ -30,35 +30,35 @@
"test": "c8 --check-coverage --lines 93 run-s test:chrome:* test:firefox", "test": "c8 --check-coverage --lines 93 run-s test:chrome:* test:firefox",
"test:types": "tsd", "test:types": "tsd",
"test:install": "scripts/test-install.sh", "test:install": "scripts/test-install.sh",
"test:firefox": "cross-env PUPPETEER_PRODUCT=firefox mocha", "test:firefox": "cross-env PUPPETEER_PRODUCT=firefox MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
"test:chrome": "run-s test:chrome:*", "test:chrome": "run-s test:chrome:*",
"test:chrome:headless": "cross-env HEADLESS=true mocha", "test:chrome:headless": "cross-env HEADLESS=true PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
"test:chrome:headless-chrome": "cross-env HEADLESS=chrome mocha", "test:chrome:headless-chrome": "cross-env HEADLESS=chrome PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
"test:chrome:headful": "cross-env HEADLESS=false mocha", "test:chrome:headful": "cross-env HEADLESS=false PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"prepare": "node typescript-if-required.js && husky install", "prepare": "node typescript-if-required.js && husky install",
"lint": "run-s lint:prettier lint:eslint", "lint": "run-s lint:prettier lint:eslint",
"lint:prettier": "prettier --check .", "lint:prettier": "prettier --check .",
"lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", "lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
"install": "node install.js", "install": "node install.js",
"generate:types": "node utils/export_all.js && api-extractor run --local --verbose && eslint --ext ts --no-ignore --no-eslintrc -c .eslintrc.types.cjs --fix lib/types.d.ts", "generate:sources": "tsx utils/generate_sources.ts",
"generate:markdown": "ts-node -O '{\"module\":\"commonjs\"}' utils/generate_docs.ts && prettier --ignore-path none --write docs", "generate:artifacts": "tsx utils/generate_artifacts.ts",
"generate:esm-package-json": "echo '{\"type\": \"module\"}' > lib/esm/package.json", "generate:markdown": "tsx utils/generate_docs.ts",
"format": "run-s format:*", "format": "run-s format:*",
"format:prettier": "prettier --write .", "format:prettier": "prettier --write .",
"format:eslint": "eslint --ext js --ext ts --fix .", "format:eslint": "eslint --ext js --ext ts --fix .",
"docs": "run-s build generate:markdown", "docs": "run-s build generate:markdown",
"debug": "npm run build && mocha --inspect-brk", "debug": "npm run build:dev && mocha --inspect-brk",
"commitlint": "commitlint --from=HEAD~1", "commitlint": "commitlint --from=HEAD~1",
"clean": "rimraf lib && rimraf test/build", "clean": "rimraf lib && rimraf test/build",
"check": "run-p check:*", "check": "run-p check:*",
"check:protocol-revision": "ts-node -s scripts/ensure-correct-devtools-protocol-package", "check:protocol-revision": "tsx scripts/ensure-correct-devtools-protocol-package",
"check:pinned-deps": "ts-node -s scripts/ensure-pinned-deps", "check:pinned-deps": "tsx scripts/ensure-pinned-deps",
"build": "run-s build:tsc generate:types generate:esm-package-json", "build": "npm run build:prod",
"build:tsc": "tsc --version && run-p build:tsc:*", "build:dev": "run-s generate:sources build:tsc:dev generate:artifacts",
"build:tsc:esm": "tsc -b src/tsconfig.esm.json", "build:prod": "run-s generate:sources build:tsc:prod generate:artifacts",
"build:tsc:cjs": "tsc -b src/tsconfig.cjs.json", "build:tsc:dev": "tsc -b test",
"build:tsc:test": "tsc -b test" "build:tsc:prod": "tsc -b tsconfig.lib.json"
}, },
"files": [ "files": [
"lib", "lib",
@ -71,10 +71,9 @@
"dependencies": { "dependencies": {
"cross-fetch": "3.1.5", "cross-fetch": "3.1.5",
"debug": "4.3.4", "debug": "4.3.4",
"devtools-protocol": "0.0.1019158", "devtools-protocol": "0.0.1036444",
"extract-zip": "2.0.1", "extract-zip": "2.0.1",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
"pkg-dir": "4.2.0",
"progress": "2.0.3", "progress": "2.0.3",
"proxy-from-env": "1.1.0", "proxy-from-env": "1.1.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
@ -90,6 +89,7 @@
"@microsoft/api-extractor-model": "7.23.0", "@microsoft/api-extractor-model": "7.23.0",
"@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/mime": "3.0.1", "@types/mime": "3.0.1",
"@types/mocha": "9.1.1", "@types/mocha": "9.1.1",
"@types/node": "18.7.1", "@types/node": "18.7.1",
@ -109,6 +109,7 @@
"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",
"eslint": "8.21.0", "eslint": "8.21.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",
@ -120,6 +121,7 @@
"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",
"gts": "4.0.0", "gts": "4.0.0",
"husky": "8.0.1", "husky": "8.0.1",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
@ -136,6 +138,7 @@
"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.22.0",
"tsx": "3.8.2",
"typescript": "4.7.4" "typescript": "4.7.4"
} }
} }

View file

@ -15,15 +15,11 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {
IsolatedWorld,
PageBinding,
WaitForSelectorOptions,
} from './IsolatedWorld.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {JSHandle} from './JSHandle.js'; import {Frame} from './Frame.js';
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js';
import {InternalQueryHandler} from './QueryHandler.js'; import {InternalQueryHandler} from './QueryHandler.js';
async function queryAXTree( async function queryAXTree(
@ -90,52 +86,86 @@ function parseAriaSelector(selector: string): ARIAQueryOption {
return queryOptions; return queryOptions;
} }
const queryOne = async ( const queryOneId = async (element: ElementHandle<Node>, selector: string) => {
element: ElementHandle<Node>,
selector: string
): Promise<ElementHandle<Node> | null> => {
const exeCtx = element.executionContext();
const {name, role} = parseAriaSelector(selector); const {name, role} = parseAriaSelector(selector);
const res = await queryAXTree(exeCtx._client, element, name, role); const res = await queryAXTree(element.client, element, name, role);
if (!res[0] || !res[0].backendDOMNodeId) { if (!res[0] || !res[0].backendDOMNodeId) {
return null; return null;
} }
return (await exeCtx._world!.adoptBackendNode( return res[0].backendDOMNodeId;
res[0].backendDOMNodeId };
const queryOne: InternalQueryHandler['queryOne'] = async (
element,
selector
) => {
const id = await queryOneId(element, selector);
if (!id) {
return null;
}
return (await element.frame.worlds[MAIN_WORLD].adoptBackendNode(
id
)) as ElementHandle<Node>; )) as ElementHandle<Node>;
}; };
const waitFor = async ( const waitFor: InternalQueryHandler['waitFor'] = async (
isolatedWorld: IsolatedWorld, elementOrFrame,
selector: string, selector,
options: WaitForSelectorOptions options
): Promise<ElementHandle<Element> | null> => { ) => {
let frame: Frame;
let element: ElementHandle<Node> | undefined;
if (elementOrFrame instanceof Frame) {
frame = elementOrFrame;
} else {
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
}
const binding: PageBinding = { const binding: PageBinding = {
name: 'ariaQuerySelector', name: 'ariaQuerySelector',
pptrFunction: async (selector: string) => { pptrFunction: async (selector: string) => {
const root = options.root || (await isolatedWorld.document()); const id = await queryOneId(
const element = await queryOne(root, selector); element || (await frame.worlds[PUPPETEER_WORLD].document()),
return element; selector
);
if (!id) {
return null;
}
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
id
)) as ElementHandle<Node>;
}, },
}; };
return (await isolatedWorld._waitForSelectorInPage( const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
(_: Element, selector: string) => { (_: Element, selector: string) => {
return ( return (
globalThis as unknown as { globalThis as unknown as {
ariaQuerySelector(selector: string): void; ariaQuerySelector(selector: string): Node | null;
} }
).ariaQuerySelector(selector); ).ariaQuerySelector(selector);
}, },
element,
selector, selector,
options, options,
binding binding
)) as ElementHandle<Element> | null; );
if (element) {
await element.dispose();
}
if (!result) {
return null;
}
if (!(result instanceof ElementHandle)) {
await result.dispose();
return null;
}
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
}; };
const queryAll = async ( const queryAll: InternalQueryHandler['queryAll'] = async (
element: ElementHandle<Node>, element,
selector: string selector
): Promise<Array<ElementHandle<Node>>> => { ) => {
const exeCtx = element.executionContext(); const exeCtx = element.executionContext();
const {name, role} = parseAriaSelector(selector); const {name, role} = parseAriaSelector(selector);
const res = await queryAXTree(exeCtx._client, element, name, role); const res = await queryAXTree(exeCtx._client, element, name, role);
@ -149,18 +179,6 @@ const queryAll = async (
); );
}; };
const queryAllArray = async (
element: ElementHandle<Node>,
selector: string
): Promise<JSHandle<Node[]>> => {
const elementHandles = await queryAll(element, selector);
const exeCtx = element.executionContext();
const jsHandle = exeCtx.evaluateHandle((...elements) => {
return elements;
}, ...elementHandles);
return jsHandle;
};
/** /**
* @internal * @internal
*/ */
@ -168,5 +186,4 @@ export const ariaHandler: InternalQueryHandler = {
queryOne, queryOne,
waitFor, waitFor,
queryAll, queryAll,
queryAllArray,
}; };

View file

@ -16,7 +16,7 @@
import {ChildProcess} from 'child_process'; import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {waitWithTimeout} from './util.js'; import {waitWithTimeout} from './util.js';

View file

@ -14,9 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import {debugError, isErrorLike} from './util.js'; import {debugError} from './util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import { import {
Browser, Browser,
IsPageTargetCallback, IsPageTargetCallback,

View file

@ -15,7 +15,7 @@
*/ */
import Protocol from 'devtools-protocol'; import Protocol from 'devtools-protocol';
import {assert} from './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';
import {Target} from './Target.js'; import {Target} from './Target.js';
@ -317,11 +317,12 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
) { ) {
this.#finishInitializationIfReady(targetInfo.targetId); this.#finishInitializationIfReady(targetInfo.targetId);
await silentDetach(); await silentDetach();
if (parentSession instanceof CDPSession) { if (this.#attachedTargetsByTargetId.has(targetInfo.targetId)) {
return;
}
const target = this.#targetFactory(targetInfo); const target = this.#targetFactory(targetInfo);
this.#attachedTargetsByTargetId.set(targetInfo.targetId, target); this.#attachedTargetsByTargetId.set(targetInfo.targetId, target);
this.emit(TargetManagerEmittedEvents.TargetAvailable, target); this.emit(TargetManagerEmittedEvents.TargetAvailable, target);
}
return; return;
} }

View file

@ -13,7 +13,7 @@
* 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.
*/ */
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {debug} from './Debug.js'; import {debug} from './Debug.js';
const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); const debugProtocolSend = debug('puppeteer:protocol:SEND ►');
const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀'); const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀');

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {addEventListener, debugError, PuppeteerEventListener} from './util.js'; import {addEventListener, debugError, PuppeteerEventListener} from './util.js';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
@ -484,6 +484,6 @@ function convertToDisjointRanges(
} }
// Filter out empty ranges. // Filter out empty ranges.
return results.filter(range => { return results.filter(range => {
return range.end - range.start > 1; return range.end - range.start > 0;
}); });
} }

View file

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

View file

@ -1,13 +1,9 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.js'; import {Frame} from './Frame.js';
import { import {FrameManager} from './FrameManager.js';
MAIN_WORLD, import {WaitForSelectorOptions} from './IsolatedWorld.js';
PUPPETEER_WORLD,
WaitForSelectorOptions,
} from './IsolatedWorld.js';
import { import {
BoundingBox, BoundingBox,
BoxModel, BoxModel,
@ -19,7 +15,7 @@ import {
} from './JSHandle.js'; } from './JSHandle.js';
import {Page, ScreenshotOptions} from './Page.js'; import {Page, ScreenshotOptions} from './Page.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js'; import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {EvaluateFunc, NodeFor} from './types.js'; import {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';
@ -71,24 +67,29 @@ export class ElementHandle<
ElementType extends Node = Element ElementType extends Node = Element
> extends JSHandle<ElementType> { > extends JSHandle<ElementType> {
#frame: Frame; #frame: Frame;
#page: Page;
#frameManager: FrameManager;
/** /**
* @internal * @internal
*/ */
constructor( constructor(
context: ExecutionContext, context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject, remoteObject: Protocol.Runtime.RemoteObject,
frame: Frame, frame: Frame
page: Page,
frameManager: FrameManager
) { ) {
super(context, client, remoteObject); super(context, remoteObject);
this.#frame = frame; this.#frame = frame;
this.#page = page; }
this.#frameManager = frameManager;
get #frameManager(): FrameManager {
return this.#frame._frameManager;
}
get #page(): Page {
return this.#frame.page();
}
get frame(): Frame {
return this.#frame;
} }
/** /**
@ -228,13 +229,24 @@ export class ElementHandle<
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
const {updatedSelector, queryHandler} = const {updatedSelector, queryHandler} =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray); assert(
const arrayHandle = (await queryHandler.queryAllArray( queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector'
);
const handles = (await queryHandler.queryAll(
this, this,
updatedSelector updatedSelector
)) as JSHandle<Array<NodeFor<Selector>>>; )) as Array<HandleFor<NodeFor<Selector>>>;
const result = await arrayHandle.evaluate(pageFunction, ...args); const elements = await this.evaluateHandle((_, ...elements) => {
await arrayHandle.dispose(); return elements;
}, ...handles);
const [result] = await Promise.all([
elements.evaluate(pageFunction, ...args),
...handles.map(handle => {
return handle.dispose();
}),
]);
await elements.dispose();
return result; return result;
} }
@ -291,27 +303,16 @@ export class ElementHandle<
*/ */
async waitForSelector<Selector extends string>( async waitForSelector<Selector extends string>(
selector: Selector, selector: Selector,
options: Exclude<WaitForSelectorOptions, 'root'> = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
const frame = this.executionContext().frame(); const {updatedSelector, queryHandler} =
assert(frame); getQueryHandlerAndSelector(selector);
const adoptedRoot = await frame.worlds[PUPPETEER_WORLD].adoptHandle(this); assert(queryHandler.waitFor, 'Query handler does not support waiting');
const handle = await frame.worlds[PUPPETEER_WORLD].waitForSelector( return (await queryHandler.waitFor(
selector, this,
{ updatedSelector,
...options, options
root: adoptedRoot, )) as ElementHandle<NodeFor<Selector>> | null;
}
);
await adoptedRoot.dispose();
if (!handle) {
return null;
}
const result = (await frame.worlds[MAIN_WORLD].adoptHandle(
handle
)) as ElementHandle<NodeFor<Selector>>;
await handle.dispose();
return result;
} }
/** /**

View file

@ -15,9 +15,7 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {Frame} from './FrameManager.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {EvaluateFunc, HandleFor} from './types.js'; import {EvaluateFunc, HandleFor} from './types.js';
@ -35,8 +33,6 @@ export const EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
/** /**
* @deprecated Do not use directly.
*
* Represents a context for JavaScript execution. * Represents a context for JavaScript execution.
* *
* @example * @example
@ -55,6 +51,8 @@ const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
* @remarks * @remarks
* Besides pages, execution contexts can be found in * Besides pages, execution contexts can be found in
* {@link WebWorker | workers}. * {@link WebWorker | workers}.
*
* @internal
*/ */
export class ExecutionContext { export class ExecutionContext {
/** /**
@ -88,18 +86,6 @@ export class ExecutionContext {
this._contextName = contextPayload.name; this._contextName = contextPayload.name;
} }
/**
* @returns The frame associated with this execution context.
*
* @remarks
* Not every execution context is associated with a frame. For example,
* {@link WebWorker | workers} have execution contexts that are not associated
* with frames.
*/
frame(): Frame | null {
return this._world ? this._world.frame() : null;
}
/** /**
* Evaluates the given function. * Evaluates the given function.
* *
@ -355,8 +341,10 @@ export class ExecutionContext {
} }
return {value: arg}; return {value: arg};
} }
}
}
function rewriteError(error: Error): Protocol.Runtime.EvaluateResponse { const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => {
if (error.message.includes('Object reference chain is too long')) { if (error.message.includes('Object reference chain is too long')) {
return {result: {type: 'undefined'}}; return {result: {type: 'undefined'}};
} }
@ -373,43 +361,4 @@ export class ExecutionContext {
); );
} }
throw error; throw error;
} };
}
/**
* Iterates through the JavaScript heap and finds all the objects with the
* given prototype.
*
* @example
*
* ```ts
* // Create a Map object
* await page.evaluate(() => (window.map = new Map()));
* // Get a handle to the Map object prototype
* const mapPrototype = await page.evaluateHandle(() => Map.prototype);
* // Query all map instances into an array
* const mapInstances = await page.queryObjects(mapPrototype);
* // Count amount of map objects in heap
* const count = await page.evaluate(maps => maps.length, mapInstances);
* await mapInstances.dispose();
* await mapPrototype.dispose();
* ```
*
* @param prototypeHandle - a handle to the object prototype
* @returns A handle to an array of objects with the given prototype.
*/
async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<HandleFor<Prototype[]>> {
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert(
remoteObject.objectId,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: remoteObject.objectId,
});
return createJSHandle(this, response.objects) as HandleFor<Prototype[]>;
}
}

View file

@ -15,7 +15,7 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
/** /**

View file

@ -15,7 +15,7 @@
*/ */
import Protocol from 'devtools-protocol'; import Protocol from 'devtools-protocol';
import {assert} from './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';
import {TargetFilterCallback} from './Browser.js'; import {TargetFilterCallback} from './Browser.js';

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -16,7 +16,7 @@
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {Frame} from './FrameManager.js'; import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {SecurityDetails} from './SecurityDetails.js'; import {SecurityDetails} from './SecurityDetails.js';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';

View file

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

View file

@ -15,24 +15,23 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {source as injectedSource} from '../generated/injected.js';
import {assert} from '../util/assert.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {TimeoutError} from './Errors.js'; import {TimeoutError} from './Errors.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.js'; import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js'; import {MouseButton} from './Input.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {getQueryHandlerAndSelector} from './QueryHandler.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import { import {
createDeferredPromise,
createJSHandle, createJSHandle,
debugError, debugError,
DeferredPromise,
importFS,
isNumber, isNumber,
isString, isString,
makePredicateString, makePredicateString,
@ -77,10 +76,6 @@ export interface WaitForSelectorOptions {
* @defaultValue `30000` (30 seconds) * @defaultValue `30000` (30 seconds)
*/ */
timeout?: number; timeout?: number;
/**
* @deprecated Do not use. Use the {@link ElementHandle.waitForSelector}
*/
root?: ElementHandle<Node>;
} }
/** /**
@ -118,12 +113,10 @@ export interface IsolatedWorldChart {
* @internal * @internal
*/ */
export class IsolatedWorld { export class IsolatedWorld {
#frameManager: FrameManager;
#client: CDPSession;
#frame: Frame; #frame: Frame;
#timeoutSettings: TimeoutSettings; #injected: boolean;
#documentPromise: Promise<ElementHandle<Document>> | null = null; #document?: ElementHandle<Document>;
#contextPromise: DeferredPromise<ExecutionContext> = createDeferredPromise(); #context = createDeferredPromise<ExecutionContext>();
#detached = false; #detached = false;
// Set of bindings that have been registered in the current context. // Set of bindings that have been registered in the current context.
@ -145,44 +138,48 @@ export class IsolatedWorld {
return `${name}_${contextId}`; return `${name}_${contextId}`;
}; };
constructor( constructor(frame: Frame, injected = false) {
client: CDPSession,
frameManager: FrameManager,
frame: Frame,
timeoutSettings: TimeoutSettings
) {
// Keep own reference to client because it might differ from the FrameManager's // Keep own reference to client because it might differ from the FrameManager's
// client for OOP iframes. // client for OOP iframes.
this.#client = client;
this.#frameManager = frameManager;
this.#frame = frame; this.#frame = frame;
this.#timeoutSettings = timeoutSettings; this.#injected = injected;
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled); this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
} }
get #client(): CDPSession {
return this.#frame._client();
}
get #frameManager(): FrameManager {
return this.#frame._frameManager;
}
get #timeoutSettings(): TimeoutSettings {
return this.#frameManager.timeoutSettings;
}
frame(): Frame { frame(): Frame {
return this.#frame; return this.#frame;
} }
clearContext(): void { clearContext(): void {
this.#documentPromise = null; this.#document = undefined;
this.#contextPromise = createDeferredPromise(); this.#context = createDeferredPromise();
} }
setContext(context: ExecutionContext): void { setContext(context: ExecutionContext): void {
assert( if (this.#injected) {
this.#contextPromise, context.evaluate(injectedSource).catch(debugError);
`ExecutionContext ${context._contextId} has already been set.` }
);
this.#ctxBindings.clear(); this.#ctxBindings.clear();
this.#contextPromise.resolve(context); this.#context.resolve(context);
for (const waitTask of this._waitTasks) { for (const waitTask of this._waitTasks) {
waitTask.rerun(); waitTask.rerun();
} }
} }
hasContext(): boolean { hasContext(): boolean {
return this.#contextPromise.resolved(); return this.#context.resolved();
} }
_detach(): void { _detach(): void {
@ -201,10 +198,10 @@ export class IsolatedWorld {
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)` `Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`
); );
} }
if (this.#contextPromise === null) { if (this.#context === null) {
throw new Error(`Execution content promise is missing`); throw new Error(`Execution content promise is missing`);
} }
return this.#contextPromise; return this.#context;
} }
async evaluateHandle< async evaluateHandle<
@ -244,15 +241,14 @@ export class IsolatedWorld {
} }
async document(): Promise<ElementHandle<Document>> { async document(): Promise<ElementHandle<Document>> {
if (this.#documentPromise) { if (this.#document) {
return this.#documentPromise; return this.#document;
} }
this.#documentPromise = this.executionContext().then(async context => { const context = await this.executionContext();
return await context.evaluateHandle(() => { this.#document = await context.evaluateHandle(() => {
return document; return document;
}); });
}); return this.#document;
return this.#documentPromise;
} }
async $x(expression: string): Promise<Array<ElementHandle<Node>>> { async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
@ -290,20 +286,6 @@ export class IsolatedWorld {
return document.$$eval(selector, pageFunction, ...args); return document.$$eval(selector, pageFunction, ...args);
} }
async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const {updatedSelector, queryHandler} =
getQueryHandlerAndSelector(selector);
assert(queryHandler.waitFor, 'Query handler does not support waiting');
return (await queryHandler.waitFor(
this,
updatedSelector,
options
)) as ElementHandle<NodeFor<Selector>> | null;
}
async content(): Promise<string> { async content(): Promise<string> {
return await this.evaluate(() => { return await this.evaluate(() => {
let retVal = ''; let retVal = '';
@ -351,191 +333,6 @@ export class IsolatedWorld {
} }
} }
/**
* Adds a script tag into the current context.
*
* @remarks
* You can pass a URL, filepath or string of contents. Note that when running Puppeteer
* in a browser environment you cannot pass a filepath and should use either
* `url` or `content`.
*/
async addScriptTag(options: {
url?: string;
path?: string;
content?: string;
id?: string;
type?: string;
}): Promise<ElementHandle<HTMLScriptElement>> {
const {
url = null,
path = null,
content = null,
id = '',
type = '',
} = options;
if (url !== null) {
try {
const context = await this.executionContext();
return await context.evaluateHandle(addScriptUrl, url, id, type);
} catch (error) {
throw new Error(`Loading script from ${url} failed`);
}
}
if (path !== null) {
let fs;
try {
fs = (await import('fs')).promises;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Can only pass a filepath to addScriptTag in a Node-like environment.'
);
}
throw error;
}
let contents = await fs.readFile(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
const context = await this.executionContext();
return await context.evaluateHandle(addScriptContent, contents, id, type);
}
if (content !== null) {
const context = await this.executionContext();
return await context.evaluateHandle(addScriptContent, content, id, type);
}
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addScriptUrl(url: string, id: string, type: string) {
const script = document.createElement('script');
script.src = url;
if (id) {
script.id = id;
}
if (type) {
script.type = type;
}
const promise = new Promise((res, rej) => {
script.onload = res;
script.onerror = rej;
});
document.head.appendChild(script);
await promise;
return script;
}
function addScriptContent(
content: string,
id: string,
type = 'text/javascript'
) {
const script = document.createElement('script');
script.type = type;
script.text = content;
if (id) {
script.id = id;
}
let error = null;
script.onerror = e => {
return (error = e);
};
document.head.appendChild(script);
if (error) {
throw error;
}
return script;
}
}
/**
* Adds a style tag into the current context.
*
* @remarks
* You can pass a URL, filepath or string of contents. Note that when running Puppeteer
* in a browser environment you cannot pass a filepath and should use either
* `url` or `content`.
*/
async addStyleTag(options: {
url?: string;
path?: string;
content?: string;
}): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
const {url = null, path = null, content = null} = options;
if (url !== null) {
try {
const context = await this.executionContext();
return (await context.evaluateHandle(
addStyleUrl,
url
)) as ElementHandle<HTMLLinkElement>;
} catch (error) {
throw new Error(`Loading style from ${url} failed`);
}
}
if (path !== null) {
let fs: typeof import('fs').promises;
try {
fs = (await importFS()).promises;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(
'Cannot pass a filepath to addStyleTag in the browser environment.'
);
}
throw error;
}
let contents = await fs.readFile(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
const context = await this.executionContext();
return (await context.evaluateHandle(
addStyleContent,
contents
)) as ElementHandle<HTMLStyleElement>;
}
if (content !== null) {
const context = await this.executionContext();
return (await context.evaluateHandle(
addStyleContent,
content
)) as ElementHandle<HTMLStyleElement>;
}
throw new Error(
'Provide an object with a `url`, `path` or `content` property'
);
async function addStyleUrl(url: string): Promise<HTMLElement> {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
const promise = new Promise((res, rej) => {
link.onload = res;
link.onerror = rej;
});
document.head.appendChild(link);
await promise;
return link;
}
async function addStyleContent(content: string): Promise<HTMLElement> {
const style = document.createElement('style');
style.appendChild(document.createTextNode(content));
const promise = new Promise((res, rej) => {
style.onload = res;
style.onerror = rej;
});
document.head.appendChild(style);
await promise;
return style;
}
}
async click( async click(
selector: string, selector: string,
options: {delay?: number; button?: MouseButton; clickCount?: number} options: {delay?: number; button?: MouseButton; clickCount?: number}
@ -703,10 +500,11 @@ export class IsolatedWorld {
async _waitForSelectorInPage( async _waitForSelectorInPage(
queryOne: Function, queryOne: Function,
root: ElementHandle<Node> | undefined,
selector: string, selector: string,
options: WaitForSelectorOptions, options: WaitForSelectorOptions,
binding?: PageBinding binding?: PageBinding
): Promise<ElementHandle<Node> | null> { ): Promise<JSHandle<unknown> | null> {
const { const {
visible: waitForVisible = false, visible: waitForVisible = false,
hidden: waitForHidden = false, hidden: waitForHidden = false,
@ -722,9 +520,7 @@ export class IsolatedWorld {
waitForVisible: boolean, waitForVisible: boolean,
waitForHidden: boolean waitForHidden: boolean
): Promise<Node | null | boolean> { ): Promise<Node | null | boolean> {
const node = predicateQueryHandler const node = (await predicateQueryHandler(root, selector)) as Element;
? ((await predicateQueryHandler(root, selector)) as Element)
: root.querySelector(selector);
return checkWaitForOptions(node, waitForVisible, waitForHidden); return checkWaitForOptions(node, waitForVisible, waitForHidden);
} }
const waitTaskOptions: WaitTaskOptions = { const waitTaskOptions: WaitTaskOptions = {
@ -736,16 +532,10 @@ export class IsolatedWorld {
timeout, timeout,
args: [selector, waitForVisible, waitForHidden], args: [selector, waitForVisible, waitForHidden],
binding, binding,
root: options.root, root,
}; };
const waitTask = new WaitTask(waitTaskOptions); const waitTask = new WaitTask(waitTaskOptions);
const jsHandle = await waitTask.promise; return waitTask.promise;
const elementHandle = jsHandle.asElement();
if (!elementHandle) {
await jsHandle.dispose();
return null;
}
return elementHandle;
} }
waitForFunction( waitForFunction(
@ -796,6 +586,12 @@ export class IsolatedWorld {
}); });
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
} }
async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
const result = await this.adoptHandle(handle);
await handle.dispose();
return result;
}
} }
/** /**

View file

@ -15,7 +15,7 @@
*/ */
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import type {ElementHandle} from './ElementHandle.js'; import type {ElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
@ -78,7 +78,6 @@ export class JSHandle<T = unknown> {
*/ */
[__JSHandleSymbol]?: T; [__JSHandleSymbol]?: T;
#client: CDPSession;
#disposed = false; #disposed = false;
#context: ExecutionContext; #context: ExecutionContext;
#remoteObject: Protocol.Runtime.RemoteObject; #remoteObject: Protocol.Runtime.RemoteObject;
@ -87,7 +86,7 @@ export class JSHandle<T = unknown> {
* @internal * @internal
*/ */
get client(): CDPSession { get client(): CDPSession {
return this.#client; return this.#context._client;
} }
/** /**
@ -102,16 +101,14 @@ export class JSHandle<T = unknown> {
*/ */
constructor( constructor(
context: ExecutionContext, context: ExecutionContext,
client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
) { ) {
this.#context = context; this.#context = context;
this.#client = client;
this.#remoteObject = remoteObject; this.#remoteObject = remoteObject;
} }
/** /**
* @returns The execution context the handle belongs to. * @internal
*/ */
executionContext(): ExecutionContext { executionContext(): ExecutionContext {
return this.#context; return this.#context;
@ -196,7 +193,7 @@ export class JSHandle<T = unknown> {
assert(this.#remoteObject.objectId); assert(this.#remoteObject.objectId);
// We use Runtime.getProperties rather than iterative building because the // We use Runtime.getProperties rather than iterative building because the
// iterative approach might create a distorted snapshot. // iterative approach might create a distorted snapshot.
const response = await this.#client.send('Runtime.getProperties', { const response = await this.client.send('Runtime.getProperties', {
objectId: this.#remoteObject.objectId, objectId: this.#remoteObject.objectId,
ownProperties: true, ownProperties: true,
}); });
@ -247,7 +244,7 @@ export class JSHandle<T = unknown> {
return; return;
} }
this.#disposed = true; this.#disposed = true;
await releaseObject(this.#client, this.#remoteObject); await releaseObject(this.client, this.#remoteObject);
} }
/** /**
@ -279,11 +276,11 @@ export class JSHandle<T = unknown> {
*/ */
export interface Offset { export interface Offset {
/** /**
* x-offset for the clickable point relative to the top-left corder of the border box. * x-offset for the clickable point relative to the top-left corner of the border box.
*/ */
x: number; x: number;
/** /**
* y-offset for the clickable point relative to the top-left corder of the border box. * y-offset for the clickable point relative to the top-left corner of the border box.
*/ */
y: number; y: number;
} }
@ -307,7 +304,7 @@ export interface ClickOptions {
*/ */
clickCount?: number; clickCount?: number;
/** /**
* Offset for the clickable point relative to the top-left corder of the border box. * Offset for the clickable point relative to the top-left corner of the border box.
*/ */
offset?: Offset; offset?: Offset;
} }

View file

@ -14,20 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import { import {
addEventListener, addEventListener,
PuppeteerEventListener, PuppeteerEventListener,
removeEventListeners, removeEventListeners,
} from './util.js';
import {
DeferredPromise, DeferredPromise,
createDeferredPromise, createDeferredPromise,
} from './util.js'; } from '../util/DeferredPromise.js';
import {TimeoutError} from './Errors.js'; import {TimeoutError} from './Errors.js';
import { import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
FrameManager, import {Frame} from './Frame.js';
Frame,
FrameManagerEmittedEvents,
} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {NetworkManagerEmittedEvents} from './NetworkManager.js'; import {NetworkManagerEmittedEvents} from './NetworkManager.js';
@ -180,9 +179,10 @@ export class LifecycleWatcher {
return; return;
} }
this.#navigationRequest = request; this.#navigationRequest = request;
this.#navigationResponseReceived?.reject( // Resolve previous navigation response in case there are multiple
new Error('New navigation request was received') // navigation requests reported by the backend. This generally should not
); // happen by it looks like it's possible.
this.#navigationResponseReceived?.resolve();
this.#navigationResponseReceived = createDeferredPromise(); this.#navigationResponseReceived = createDeferredPromise();
if (request.response() !== null) { if (request.response() !== null) {
this.#navigationResponseReceived?.resolve(); this.#navigationResponseReceived?.resolve();

View file

@ -16,18 +16,15 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {Frame} from './FrameManager.js'; import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js'; import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js';
import { import {debugError, isString} from './util.js';
debugError, import {DeferredPromise} from '../util/DeferredPromise.js';
isString, import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js';
createDeferredPromiseWithTimer,
DeferredPromise,
} from './util.js';
/** /**
* @public * @public
@ -145,9 +142,8 @@ export class NetworkManager extends EventEmitter {
if (this.#deferredInitPromise) { if (this.#deferredInitPromise) {
return this.#deferredInitPromise; return this.#deferredInitPromise;
} }
this.#deferredInitPromise = createDeferredPromiseWithTimer<void>( this.#deferredInitPromise = createDebuggableDeferredPromise(
'NetworkManager initialization timed out', 'NetworkManager initialization timed out'
30000
); );
const init = Promise.all([ const init = Promise.all([
this.#ignoreHTTPSErrors this.#ignoreHTTPSErrors

View file

@ -16,26 +16,32 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import type {Readable} from 'stream'; import type {Readable} from 'stream';
import {assert} from '../util/assert.js';
import {
createDeferredPromise,
DeferredPromise,
} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {Accessibility} from './Accessibility.js'; import {Accessibility} from './Accessibility.js';
import {assert} from './assert.js';
import {Browser, BrowserContext} from './Browser.js'; import {Browser, BrowserContext} from './Browser.js';
import {CDPSession, CDPSessionEmittedEvents} from './Connection.js'; import {CDPSession, CDPSessionEmittedEvents} from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js'; import {Coverage} from './Coverage.js';
import {Dialog} from './Dialog.js'; import {Dialog} from './Dialog.js';
import {MAIN_WORLD, WaitForSelectorOptions} from './IsolatedWorld.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {EventEmitter, Handler} from './EventEmitter.js'; import {EventEmitter, Handler} from './EventEmitter.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
import { import {
Frame, Frame,
FrameManager, FrameAddScriptTagOptions,
FrameManagerEmittedEvents, FrameAddStyleTagOptions,
} from './FrameManager.js'; } from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js'; import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js'; import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js';
import {MAIN_WORLD, WaitForSelectorOptions} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import { import {
@ -56,10 +62,9 @@ import {
debugError, debugError,
evaluationString, evaluationString,
getExceptionMessage, getExceptionMessage,
importFS,
getReadableAsBuffer, getReadableAsBuffer,
getReadableFromProtocolStream, getReadableFromProtocolStream,
isErrorLike, importFS,
isNumber, isNumber,
isString, isString,
pageBindingDeliverErrorString, pageBindingDeliverErrorString,
@ -70,8 +75,6 @@ import {
valueFromRemoteObject, valueFromRemoteObject,
waitForEvent, waitForEvent,
waitWithTimeout, waitWithTimeout,
createDeferredPromiseWithTimer,
DeferredPromise,
} from './util.js'; } from './util.js';
import {WebWorker} from './WebWorker.js'; import {WebWorker} from './WebWorker.js';
@ -160,6 +163,10 @@ export interface ScreenshotClip {
y: number; y: number;
width: number; width: number;
height: number; height: number;
/**
* @defaultValue 1
*/
scale?: number;
} }
/** /**
@ -760,21 +767,25 @@ export class Page extends EventEmitter {
* await fileChooser.accept(['/tmp/myfile.pdf']); * await fileChooser.accept(['/tmp/myfile.pdf']);
* ``` * ```
*/ */
async waitForFileChooser( waitForFileChooser(options: WaitTimeoutOptions = {}): Promise<FileChooser> {
options: WaitTimeoutOptions = {} const needsEnable = this.#fileChooserPromises.size === 0;
): Promise<FileChooser> { const {timeout = this.#timeoutSettings.timeout()} = options;
if (!this.#fileChooserPromises.size) { const promise = createDeferredPromise<FileChooser>({
await this.#client.send('Page.setInterceptFileChooserDialog', { message: `Waiting for \`FileChooser\` failed: ${timeout}ms exceeded`,
timeout,
});
this.#fileChooserPromises.add(promise);
let enablePromise: Promise<void> | undefined;
if (needsEnable) {
enablePromise = this.#client.send('Page.setInterceptFileChooserDialog', {
enabled: true, enabled: true,
}); });
} }
return Promise.all([promise, enablePromise])
const {timeout = this.#timeoutSettings.timeout()} = options; .then(([result]) => {
const promise = createDeferredPromiseWithTimer<FileChooser>( return result;
`Waiting for \`FileChooser\` failed: ${timeout}ms exceeded` })
); .catch(error => {
this.#fileChooserPromises.add(promise);
return promise.catch(error => {
this.#fileChooserPromises.delete(promise); this.#fileChooserPromises.delete(promise);
throw error; throw error;
}); });
@ -1038,6 +1049,13 @@ export class Page extends EventEmitter {
this.#timeoutSettings.setDefaultTimeout(timeout); this.#timeoutSettings.setDefaultTimeout(timeout);
} }
/**
* @returns Maximum time in milliseconds.
*/
getDefaultTimeout(): number {
return this.#timeoutSettings.timeout();
}
/** /**
* Runs `document.querySelector` within the page. If no element matches the * Runs `document.querySelector` within the page. If no element matches the
* selector, the return value resolves to `null`. * selector, the return value resolves to `null`.
@ -1137,11 +1155,6 @@ export class Page extends EventEmitter {
* This method iterates the JavaScript heap and finds all objects with the * This method iterates the JavaScript heap and finds all objects with the
* given prototype. * given prototype.
* *
* @remarks
* Shortcut for
* {@link ExecutionContext.queryObjects |
* page.mainFrame().executionContext().queryObjects(prototypeHandle)}.
*
* @example * @example
* *
* ```ts * ```ts
@ -1165,7 +1178,16 @@ export class Page extends EventEmitter {
prototypeHandle: JSHandle<Prototype> prototypeHandle: JSHandle<Prototype>
): Promise<JSHandle<Prototype[]>> { ): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext(); const context = await this.mainFrame().executionContext();
return context.queryObjects(prototypeHandle); assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert(
remoteObject.objectId,
'Prototype JSHandle must not be referencing primitive value'
);
const response = await context._client.send('Runtime.queryObjects', {
prototypeObjectId: remoteObject.objectId,
});
return createJSHandle(context, response.objects) as HandleFor<Prototype[]>;
} }
/** /**
@ -1408,30 +1430,35 @@ export class Page extends EventEmitter {
* Shortcut for * Shortcut for
* {@link Frame.addScriptTag | page.mainFrame().addScriptTag(options)}. * {@link Frame.addScriptTag | page.mainFrame().addScriptTag(options)}.
* *
* @returns Promise which resolves to the added tag when the script's onload * @param options - Options for the script.
* fires or when the script content was injected into frame. * @returns An {@link ElementHandle | element handle} to the injected
* `<script>` element.
*/ */
async addScriptTag(options: { async addScriptTag(
url?: string; options: FrameAddScriptTagOptions
path?: string; ): Promise<ElementHandle<HTMLScriptElement>> {
content?: string;
type?: string;
id?: string;
}): Promise<ElementHandle<HTMLScriptElement>> {
return this.mainFrame().addScriptTag(options); return this.mainFrame().addScriptTag(options);
} }
/** /**
* Adds a `<link rel="stylesheet">` tag into the page with the desired URL or a * Adds a `<link rel="stylesheet">` tag into the page with the desired URL or
* `<style type="text/css">` tag with the content. * a `<style type="text/css">` tag with the content.
* @returns Promise which resolves to the added tag when the stylesheet's *
* onload fires or when the CSS content was injected into frame. * Shortcut for
* {@link Frame.addStyleTag | page.mainFrame().addStyleTag(options)}.
*
* @returns An {@link ElementHandle | element handle} to the injected `<link>`
* or `<style>` element.
*/ */
async addStyleTag(options: { async addStyleTag(
url?: string; options: Omit<FrameAddStyleTagOptions, 'url'>
path?: string; ): Promise<ElementHandle<HTMLStyleElement>>;
content?: string; async addStyleTag(
}): Promise<ElementHandle<Node>> { options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLLinkElement>>;
async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
return this.mainFrame().addStyleTag(options); return this.mainFrame().addStyleTag(options);
} }
@ -2770,7 +2797,7 @@ export class Page extends EventEmitter {
* }); * });
* *
* // In your puppeteer script, assuming the preload.js file is * // In your puppeteer script, assuming the preload.js file is
* in same folder of our script * // in same folder of our script.
* const preloadFile = fs.readFileSync('./preload.js', 'utf8'); * const preloadFile = fs.readFileSync('./preload.js', 'utf8');
* await page.evaluateOnNewDocument(preloadFile); * await page.evaluateOnNewDocument(preloadFile);
* ``` * ```
@ -2989,7 +3016,12 @@ export class Page extends EventEmitter {
const result = await this.#client.send('Page.captureScreenshot', { const result = await this.#client.send('Page.captureScreenshot', {
format, format,
quality: options.quality, quality: options.quality,
clip, clip: clip
? {
...clip,
scale: clip.scale === undefined ? 1 : clip.scale,
}
: undefined,
captureBeyondViewport, captureBeyondViewport,
fromSurface, fromSurface,
}); });
@ -3021,14 +3053,12 @@ export class Page extends EventEmitter {
} }
return buffer; return buffer;
function processClip( function processClip(clip: ScreenshotClip): ScreenshotClip {
clip: ScreenshotClip
): ScreenshotClip & {scale: number} {
const x = Math.round(clip.x); const x = Math.round(clip.x);
const y = Math.round(clip.y); const y = Math.round(clip.y);
const width = Math.round(clip.width + clip.x - x); const width = Math.round(clip.width + clip.x - x);
const height = Math.round(clip.height + clip.y - y); const height = Math.round(clip.height + clip.y - y);
return {x, y, width, height, scale: 1}; return {x, y, width, height, scale: clip.scale};
} }
} }
@ -3393,7 +3423,7 @@ export class Page extends EventEmitter {
*/ */
async waitForSelector<Selector extends string>( async waitForSelector<Selector extends string>(
selector: Selector, selector: Selector,
options: Exclude<WaitForSelectorOptions, 'root'> = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> { ): Promise<ElementHandle<NodeFor<Selector>> | null> {
return await this.mainFrame().waitForSelector(selector, options); return await this.mainFrame().waitForSelector(selector, options);
} }

View file

@ -15,9 +15,13 @@
*/ */
import {ariaHandler} from './AriaQueryHandler.js'; import {ariaHandler} from './AriaQueryHandler.js';
import {IsolatedWorld, WaitForSelectorOptions} from './IsolatedWorld.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {JSHandle} from './JSHandle.js'; import {Frame} from './Frame.js';
import {
MAIN_WORLD,
PUPPETEER_WORLD,
WaitForSelectorOptions,
} from './IsolatedWorld.js';
/** /**
* @public * @public
@ -55,25 +59,13 @@ export interface InternalQueryHandler {
element: ElementHandle<Node>, element: ElementHandle<Node>,
selector: string selector: string
) => Promise<Array<ElementHandle<Node>>>; ) => Promise<Array<ElementHandle<Node>>>;
/**
* Queries for multiple nodes given a selector and {@link ElementHandle}.
* Unlike {@link queryAll}, this returns a handle to a node array.
*
* Akin to {@link Window.prototype.querySelectorAll}.
*/
queryAllArray?: (
element: ElementHandle<Node>,
selector: string
) => Promise<JSHandle<Node[]>>;
/** /**
* Waits until a single node appears for a given selector and * Waits until a single node appears for a given selector and
* {@link ElementHandle}. * {@link ElementHandle}.
*
* Akin to {@link Window.prototype.querySelectorAll}.
*/ */
waitFor?: ( waitFor?: (
isolatedWorld: IsolatedWorld, elementOrFrame: ElementHandle<Node> | Frame,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
) => Promise<ElementHandle<Node> | null>; ) => Promise<ElementHandle<Node> | null>;
@ -95,12 +87,34 @@ function internalizeCustomQueryHandler(
await jsHandle.dispose(); await jsHandle.dispose();
return null; return null;
}; };
internalHandler.waitFor = ( internalHandler.waitFor = async (elementOrFrame, selector, options) => {
domWorld: IsolatedWorld, let frame: Frame;
selector: string, let element: ElementHandle<Node> | undefined;
options: WaitForSelectorOptions if (elementOrFrame instanceof Frame) {
) => { frame = elementOrFrame;
return domWorld._waitForSelectorInPage(queryOne, selector, options); } else {
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(
elementOrFrame
);
}
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
queryOne,
element,
selector,
options
);
if (element) {
await element.dispose();
}
if (!result) {
return null;
}
if (!(result instanceof ElementHandle)) {
await result.dispose();
return null;
}
return frame.worlds[MAIN_WORLD].transferHandle(result);
}; };
} }
@ -119,16 +133,6 @@ function internalizeCustomQueryHandler(
} }
return result; return result;
}; };
internalHandler.queryAllArray = async (element, selector) => {
const resultHandle = (await element.evaluateHandle(
queryAll,
selector
)) as JSHandle<Element[] | NodeListOf<Element>>;
const arrayHandle = await resultHandle.evaluateHandle(res => {
return Array.from(res);
});
return arrayHandle;
};
} }
return internalHandler; return internalHandler;

View file

@ -13,12 +13,9 @@
* 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.
*/ */
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import { import {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js';
getReadableAsBuffer, import {isErrorLike} from '../util/ErrorLike.js';
getReadableFromProtocolStream,
isErrorLike,
} from './util.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
/** /**

View file

@ -21,6 +21,7 @@ import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
/** /**
* @internal * @internal
@ -38,8 +39,6 @@ export type ExceptionThrownCallback = (
details: Protocol.Runtime.ExceptionDetails details: Protocol.Runtime.ExceptionDetails
) => void; ) => void;
type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
/** /**
* This class represents a * This class represents a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}. * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}.
@ -67,10 +66,10 @@ type JSHandleFactory = (obj: Protocol.Runtime.RemoteObject) => JSHandle;
* @public * @public
*/ */
export class WebWorker extends EventEmitter { export class WebWorker extends EventEmitter {
#executionContext = createDeferredPromise<ExecutionContext>();
#client: CDPSession; #client: CDPSession;
#url: string; #url: string;
#executionContextPromise: Promise<ExecutionContext>;
#executionContextCallback!: (value: ExecutionContext) => void;
/** /**
* @internal * @internal
@ -84,32 +83,34 @@ export class WebWorker extends EventEmitter {
super(); super();
this.#client = client; this.#client = client;
this.#url = url; this.#url = url;
this.#executionContextPromise = new Promise<ExecutionContext>(x => {
return (this.#executionContextCallback = x);
});
let jsHandleFactory: JSHandleFactory;
this.#client.once('Runtime.executionContextCreated', async event => { this.#client.once('Runtime.executionContextCreated', async event => {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type const context = new ExecutionContext(client, event.context);
jsHandleFactory = remoteObject => { this.#executionContext.resolve(context);
return new JSHandle(executionContext, client, remoteObject);
};
const executionContext = new ExecutionContext(client, event.context);
this.#executionContextCallback(executionContext);
}); });
this.#client.on('Runtime.consoleAPICalled', async event => {
// This might fail if the target is closed before we receive all execution contexts. const context = await this.#executionContext;
this.#client.send('Runtime.enable').catch(debugError);
this.#client.on('Runtime.consoleAPICalled', event => {
return consoleAPICalled( return consoleAPICalled(
event.type, event.type,
event.args.map(jsHandleFactory), event.args.map((object: Protocol.Runtime.RemoteObject) => {
return new JSHandle(context, object);
}),
event.stackTrace event.stackTrace
); );
}); });
this.#client.on('Runtime.exceptionThrown', exception => { this.#client.on('Runtime.exceptionThrown', exception => {
return exceptionThrown(exception.exceptionDetails); return exceptionThrown(exception.exceptionDetails);
}); });
// This might fail if the target is closed before we receive all execution contexts.
this.#client.send('Runtime.enable').catch(debugError);
}
/**
* @internal
*/
async executionContext(): Promise<ExecutionContext> {
return this.#executionContext;
} }
/** /**
@ -119,14 +120,6 @@ export class WebWorker extends EventEmitter {
return this.#url; return this.#url;
} }
/**
* Returns the ExecutionContext the WebWorker runs in
* @returns The ExecutionContext the web worker runs in.
*/
async executionContext(): Promise<ExecutionContext> {
return this.#executionContextPromise;
}
/** /**
* If the function passed to the `worker.evaluate` returns a Promise, then * If the function passed to the `worker.evaluate` returns a Promise, then
* `worker.evaluate` would wait for the promise to resolve and return its * `worker.evaluate` would wait for the promise to resolve and return its
@ -148,10 +141,8 @@ export class WebWorker extends EventEmitter {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
return (await this.#executionContextPromise).evaluate( const context = await this.#executionContext;
pageFunction, return context.evaluate(pageFunction, ...args);
...args
);
} }
/** /**
@ -173,9 +164,7 @@ export class WebWorker extends EventEmitter {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return (await this.#executionContextPromise).evaluateHandle( const context = await this.#executionContext;
pageFunction, return context.evaluateHandle(pageFunction, ...args);
...args
);
} }
} }

View file

@ -17,7 +17,8 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import type {Readable} from 'stream'; import type {Readable} from 'stream';
import {isNode} from '../environment.js'; import {isNode} from '../environment.js';
import {assert} from './assert.js'; import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {debug} from './Debug.js'; import {debug} from './Debug.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
@ -216,19 +217,10 @@ export function createJSHandle(
context: ExecutionContext, context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
): JSHandle | ElementHandle<Node> { ): JSHandle | ElementHandle<Node> {
const frame = context.frame(); if (remoteObject.subtype === 'node' && context._world) {
if (remoteObject.subtype === 'node' && frame) { return new ElementHandle(context, remoteObject, context._world.frame());
const frameManager = frame._frameManager;
return new ElementHandle(
context,
context._client,
remoteObject,
frame,
frameManager.page(),
frameManager
);
} }
return new JSHandle(context, context._client, remoteObject); return new JSHandle(context, remoteObject);
} }
/** /**
@ -341,7 +333,7 @@ export function pageBindingDeliverErrorValueString(
*/ */
export function makePredicateString( export function makePredicateString(
predicate: Function, predicate: Function,
predicateQueryHandler?: Function predicateQueryHandler: Function
): string { ): string {
function checkWaitForOptions( function checkWaitForOptions(
node: Node | null, node: Node | null,
@ -371,12 +363,10 @@ export function makePredicateString(
return !!(rect.top || rect.bottom || rect.width || rect.height); return !!(rect.top || rect.bottom || rect.width || rect.height);
} }
} }
const predicateQueryHandlerDef = predicateQueryHandler
? `const predicateQueryHandler = ${predicateQueryHandler};`
: '';
return ` return `
(() => { (() => {
${predicateQueryHandlerDef} const predicateQueryHandler = ${predicateQueryHandler};
const checkWaitForOptions = ${checkWaitForOptions}; const checkWaitForOptions = ${checkWaitForOptions};
return (${predicate})(...args) return (${predicate})(...args)
})() `; })() `;
@ -496,104 +486,3 @@ export async function getReadableFromProtocolStream(
}, },
}); });
} }
/**
* @internal
*/
export interface ErrorLike extends Error {
name: string;
message: string;
}
/**
* @internal
*/
export function isErrorLike(obj: unknown): obj is ErrorLike {
return (
typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj
);
}
/**
* @internal
*/
export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
return (
isErrorLike(obj) &&
('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
);
}
/**
* @internal
*/
export interface DeferredPromise<T> extends Promise<T> {
resolved: () => boolean;
resolve: (_: T) => void;
reject: (_: Error) => void;
}
/**
* Creates an returns a promise along with the resolve/reject functions.
*
* If the promise has not been resolved/rejected withing the `timeout` period,
* the promise gets rejected with a timeout error.
*
* @internal
*/
export function createDeferredPromiseWithTimer<T>(
timeoutMessage: string,
timeout = 5000
): DeferredPromise<T> {
let isResolved = false;
let resolver = (_: T): void => {};
let rejector = (_: Error) => {};
const taskPromise = new Promise<T>((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
const timeoutId = setTimeout(() => {
rejector(new TimeoutError(timeoutMessage));
}, timeout);
return Object.assign(taskPromise, {
resolved: () => {
return isResolved;
},
resolve: (value: T) => {
clearTimeout(timeoutId);
isResolved = true;
resolver(value);
},
reject: (err: Error) => {
clearTimeout(timeoutId);
rejector(err);
},
});
}
/**
* Creates an returns a promise along with the resolve/reject functions.
*
* @internal
*/
export function createDeferredPromise<T>(): DeferredPromise<T> {
let isResolved = false;
let resolver = (_: T): void => {};
let rejector = (_: Error) => {};
const taskPromise = new Promise<T>((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
return Object.assign(taskPromise, {
resolved: () => {
return isResolved;
},
resolve: (value: T) => {
isResolved = true;
resolver(value);
},
reject: (err: Error) => {
rejector(err);
},
});
}

View file

@ -18,3 +18,12 @@
* @internal * @internal
*/ */
export const isNode = !!(typeof process !== 'undefined' && process.version); export const isNode = !!(typeof process !== 'undefined' && process.version);
/**
* @internal
*/
export const DEFERRED_PROMISE_DEBUG_TIMEOUT =
typeof process !== 'undefined' &&
typeof process.env['PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT'] !== 'undefined'
? Number(process.env['PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT'])
: -1;

View file

@ -1,3 +0,0 @@
# Generated Artifacts
**Do not edit manually edit any TypeScript files in this folder** All TS files are generated from their respectively named template file (ext. `tmpl`) in the `templates` directory. Edit them there is needed.

View file

@ -1,4 +1,4 @@
/** /**
* @internal * @internal
*/ */
export const packageVersion = '16.1.1'; export const packageVersion = '17.1.2';

View file

@ -14,18 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import {sync} from 'pkg-dir';
import {Product} from './common/Product.js'; import {Product} from './common/Product.js';
import {rootDirname} from './constants.js'; import {rootDirname} from './constants.js';
import {PuppeteerNode} from './node/Puppeteer.js'; import {PuppeteerNode} from './node/Puppeteer.js';
import {PUPPETEER_REVISIONS} from './revisions.js'; import {PUPPETEER_REVISIONS} from './revisions.js';
import {getPackageDirectory} from './util/getPackageDirectory.js';
/** /**
* @internal * @internal
*/ */
export const initializePuppeteer = (packageName: string): PuppeteerNode => { export const initializePuppeteer = (packageName: string): PuppeteerNode => {
const isPuppeteerCore = packageName === 'puppeteer-core'; const isPuppeteerCore = packageName === 'puppeteer-core';
const puppeteerRootDirectory = sync(rootDirname);
let preferredRevision = PUPPETEER_REVISIONS.chromium; let preferredRevision = PUPPETEER_REVISIONS.chromium;
// puppeteer-core ignores environment variables // puppeteer-core ignores environment variables
const productName = !isPuppeteerCore const productName = !isPuppeteerCore
@ -39,7 +38,7 @@ export const initializePuppeteer = (packageName: string): PuppeteerNode => {
} }
return new PuppeteerNode({ return new PuppeteerNode({
projectRoot: puppeteerRootDirectory, projectRoot: isPuppeteerCore ? undefined : getPackageDirectory(rootDirname),
preferredRevision, preferredRevision,
isPuppeteerCore, isPuppeteerCore,
productName, productName,

View file

@ -0,0 +1,156 @@
import {
createDeferredPromise,
DeferredPromise,
} from '../util/DeferredPromise.js';
import {assert} from '../util/assert.js';
interface Poller<T> {
start(): Promise<T>;
stop(): Promise<void>;
result(): Promise<T>;
}
export class MutationPoller<T> implements Poller<T> {
#fn: () => Promise<T>;
#root: Node;
#observer?: MutationObserver;
#promise?: DeferredPromise<T>;
constructor(fn: () => Promise<T>, root: Node) {
this.#fn = fn;
this.#root = root;
}
async start(): Promise<T> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
}
this.#observer = new MutationObserver(async () => {
const result = await this.#fn();
if (!result) {
return;
}
promise.resolve(result);
await this.stop();
});
this.#observer.observe(this.#root, {
childList: true,
subtree: true,
attributes: true,
});
return this.#promise;
}
async stop(): Promise<void> {
assert(this.#promise, 'Polling never started.');
if (!this.#promise.finished()) {
this.#promise.reject(new Error('Polling stopped'));
}
if (this.#observer) {
this.#observer.disconnect();
}
}
result(): Promise<T> {
assert(this.#promise, 'Polling never started.');
return this.#promise;
}
}
export class RAFPoller<T> implements Poller<T> {
#fn: () => Promise<T>;
#promise?: DeferredPromise<T>;
constructor(fn: () => Promise<T>) {
this.#fn = fn;
}
async start(): Promise<T> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
}
const poll = async () => {
if (promise.finished()) {
return;
}
const result = await this.#fn();
if (!result) {
window.requestAnimationFrame(poll);
return;
}
promise.resolve(result);
await this.stop();
};
window.requestAnimationFrame(poll);
return this.#promise;
}
async stop(): Promise<void> {
assert(this.#promise, 'Polling never started.');
if (!this.#promise.finished()) {
this.#promise.reject(new Error('Polling stopped'));
}
}
result(): Promise<T> {
assert(this.#promise, 'Polling never started.');
return this.#promise;
}
}
export class IntervalPoller<T> implements Poller<T> {
#fn: () => Promise<T>;
#ms: number;
#interval?: NodeJS.Timer;
#promise?: DeferredPromise<T>;
constructor(fn: () => Promise<T>, ms: number) {
this.#fn = fn;
this.#ms = ms;
}
async start(): Promise<T> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
}
this.#interval = setInterval(async () => {
const result = await this.#fn();
if (!result) {
return;
}
promise.resolve(result);
await this.stop();
}, this.#ms);
return this.#promise;
}
async stop(): Promise<void> {
assert(this.#promise, 'Polling never started.');
if (!this.#promise.finished()) {
this.#promise.reject(new Error('Polling stopped'));
}
if (this.#interval) {
clearInterval(this.#interval);
}
}
result(): Promise<T> {
assert(this.#promise, 'Polling never started.');
return this.#promise;
}
}

View file

@ -0,0 +1,5 @@
# Injected
This folder contains code that is injected into every Puppeteer execution context. Each file is transpiled using esbuild into a script in `src/generated` which is then imported into server code.
See `utils/generate_injected.ts` for more information.

View file

@ -0,0 +1,14 @@
import {createDeferredPromise} from '../util/DeferredPromise.js';
import * as Poller from './Poller.js';
import * as util from './util.js';
Object.assign(
self,
Object.freeze({
InjectedUtil: {
...Poller,
...util,
createDeferredPromise,
},
})
);

View file

@ -0,0 +1,18 @@
const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
/**
* Creates a function from a string.
*/
export const createFunction = (
functionValue: string
): ((...args: unknown[]) => unknown) => {
let fn = createdFunctions.get(functionValue);
if (fn) {
return fn;
}
fn = new Function(`return ${functionValue}`)() as (
...args: unknown[]
) => unknown;
createdFunctions.set(functionValue, fn);
return fn;
};

View file

@ -33,7 +33,7 @@ import createHttpsProxyAgent, {
HttpsProxyAgentOptions, HttpsProxyAgentOptions,
} from 'https-proxy-agent'; } from 'https-proxy-agent';
import {getProxyForUrl} from 'proxy-from-env'; import {getProxyForUrl} from 'proxy-from-env';
import {assert} from '../common/assert.js'; import {assert} from '../util/assert.js';
import tar from 'tar-fs'; import tar from 'tar-fs';
import bzip from 'unbzip2-stream'; import bzip from 'unbzip2-stream';

View file

@ -20,18 +20,17 @@ import * as path from 'path';
import * as readline from 'readline'; import * as readline from 'readline';
import removeFolder from 'rimraf'; import removeFolder from 'rimraf';
import {promisify} from 'util'; import {promisify} from 'util';
import {assert} from '../common/assert.js'; import {assert} from '../util/assert.js';
import {Connection} from '../common/Connection.js'; import {Connection} from '../common/Connection.js';
import {debug} from '../common/Debug.js'; import {debug} from '../common/Debug.js';
import {TimeoutError} from '../common/Errors.js'; import {TimeoutError} from '../common/Errors.js';
import { import {
debugError, debugError,
addEventListener, addEventListener,
isErrnoException,
isErrorLike,
PuppeteerEventListener, PuppeteerEventListener,
removeEventListeners, removeEventListeners,
} from '../common/util.js'; } from '../common/util.js';
import {isErrnoException, isErrorLike} from '../util/ErrorLike.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {NodeWebSocketTransport as WebSocketTransport} from '../node/NodeWebSocketTransport.js'; import {NodeWebSocketTransport as WebSocketTransport} from '../node/NodeWebSocketTransport.js';
import {LaunchOptions} from './LaunchOptions.js'; import {LaunchOptions} from './LaunchOptions.js';

View file

@ -1,6 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {assert} from '../common/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../common/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserRunner} from './BrowserRunner.js'; import {BrowserRunner} from './BrowserRunner.js';

View file

@ -1,7 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {assert} from '../common/assert.js'; import {assert} from '../util/assert.js';
import {Browser} from '../common/Browser.js'; import {Browser} from '../common/Browser.js';
import {Product} from '../common/Product.js'; import {Product} from '../common/Product.js';
import {BrowserFetcher} from './BrowserFetcher.js'; import {BrowserFetcher} from './BrowserFetcher.js';

View file

@ -16,27 +16,12 @@
import NodeWebSocket from 'ws'; import NodeWebSocket from 'ws';
import {ConnectionTransport} from '../common/ConnectionTransport.js'; import {ConnectionTransport} from '../common/ConnectionTransport.js';
import {packageVersion} from '../generated/version.js'; import {packageVersion} from '../generated/version.js';
import {promises as dns} from 'dns';
import {URL} from 'url';
/** /**
* @internal * @internal
*/ */
export class NodeWebSocketTransport implements ConnectionTransport { export class NodeWebSocketTransport implements ConnectionTransport {
static async create(urlString: string): Promise<NodeWebSocketTransport> { static create(url: string): Promise<NodeWebSocketTransport> {
// TODO(jrandolf): Starting in Node 17, IPv6 is favoured over IPv4 due to a change
// in a default option:
// - https://github.com/nodejs/node/issues/40537,
// Due to this, for Firefox, we must parse and resolve the `localhost` hostname
// manually with the previous behavior according to:
// - https://nodejs.org/api/dns.html#dnslookuphostname-options-callback
// because of https://bugzilla.mozilla.org/show_bug.cgi?id=1769994.
const url = new URL(urlString);
if (url.hostname === 'localhost') {
const {address} = await dns.lookup(url.hostname, {verbatim: false});
url.hostname = address;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ws = new NodeWebSocket(url, [], { const ws = new NodeWebSocket(url, [], {
followRedirects: true, followRedirects: true,

View file

@ -13,7 +13,7 @@
* 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.
*/ */
import {assert} from '../common/assert.js'; import {assert} from '../util/assert.js';
import {ConnectionTransport} from '../common/ConnectionTransport.js'; import {ConnectionTransport} from '../common/ConnectionTransport.js';
import { import {
addEventListener, addEventListener,

View file

@ -18,6 +18,6 @@
* @internal * @internal
*/ */
export const PUPPETEER_REVISIONS = Object.freeze({ export const PUPPETEER_REVISIONS = Object.freeze({
chromium: '1022525', chromium: '1036745',
firefox: 'latest', firefox: 'latest',
}); });

View file

@ -0,0 +1,3 @@
# Templated Artifacts
These files are generated as TypeScript files in the `src/generated` folder.

View file

@ -0,0 +1,10 @@
import {createDeferredPromise} from '../util/DeferredPromise.js';
declare global {
const InjectedUtil: {
createDeferredPromise: typeof createDeferredPromise;
};
}
/** @internal */
export const source = SOURCE_CODE;

View file

@ -8,5 +8,6 @@
"references": [ "references": [
{"path": "../vendor/tsconfig.cjs.json"}, {"path": "../vendor/tsconfig.cjs.json"},
{"path": "../compat/cjs/tsconfig.json"} {"path": "../compat/cjs/tsconfig.json"}
] ],
"exclude": ["injected/injected.ts"]
} }

View file

@ -8,5 +8,6 @@
"references": [ "references": [
{"path": "../vendor/tsconfig.esm.json"}, {"path": "../vendor/tsconfig.esm.json"},
{"path": "../compat/esm/tsconfig.json"} {"path": "../compat/esm/tsconfig.json"}
] ],
"exclude": ["injected/injected.ts"]
} }

View file

@ -1,13 +1,5 @@
// AUTOGENERATED - Use `utils/export_all.js` to regenerate. // AUTOGENERATED - Use `npm run generate:sources` to regenerate.
export * from './compat.d.js';
export * from './constants.js';
export * from './environment.js';
export * from './initializePuppeteer.js';
export * from './puppeteer.js';
export * from './revisions.js';
// Exports from `common`
export * from './common/Accessibility.js'; export * from './common/Accessibility.js';
export * from './common/AriaQueryHandler.js'; export * from './common/AriaQueryHandler.js';
export * from './common/Browser.js'; export * from './common/Browser.js';
@ -26,8 +18,10 @@ export * from './common/EmulationManager.js';
export * from './common/Errors.js'; export * from './common/Errors.js';
export * from './common/EventEmitter.js'; export * from './common/EventEmitter.js';
export * from './common/ExecutionContext.js'; export * from './common/ExecutionContext.js';
export * from './common/fetch.js';
export * from './common/FileChooser.js'; export * from './common/FileChooser.js';
export * from './common/FirefoxTargetManager.js'; export * from './common/FirefoxTargetManager.js';
export * from './common/Frame.js';
export * from './common/FrameManager.js'; export * from './common/FrameManager.js';
export * from './common/HTTPRequest.js'; export * from './common/HTTPRequest.js';
export * from './common/HTTPResponse.js'; export * from './common/HTTPResponse.js';
@ -38,8 +32,8 @@ export * from './common/LifecycleWatcher.js';
export * from './common/NetworkConditions.js'; export * from './common/NetworkConditions.js';
export * from './common/NetworkEventManager.js'; export * from './common/NetworkEventManager.js';
export * from './common/NetworkManager.js'; export * from './common/NetworkManager.js';
export * from './common/PDFOptions.js';
export * from './common/Page.js'; export * from './common/Page.js';
export * from './common/PDFOptions.js';
export * from './common/Product.js'; export * from './common/Product.js';
export * from './common/Puppeteer.js'; export * from './common/Puppeteer.js';
export * from './common/PuppeteerViewport.js'; export * from './common/PuppeteerViewport.js';
@ -50,25 +44,31 @@ export * from './common/TargetManager.js';
export * from './common/TaskQueue.js'; export * from './common/TaskQueue.js';
export * from './common/TimeoutSettings.js'; export * from './common/TimeoutSettings.js';
export * from './common/Tracing.js'; export * from './common/Tracing.js';
export * from './common/USKeyboardLayout.js';
export * from './common/WebWorker.js';
export * from './common/assert.js';
export * from './common/fetch.js';
export * from './common/types.js'; export * from './common/types.js';
export * from './common/USKeyboardLayout.js';
export * from './common/util.js'; export * from './common/util.js';
export * from './common/WebWorker.js';
// Exports from `node` export * from './compat.d.js';
export * from './constants.js';
export * from './environment.js';
export * from './generated/injected.js';
export * from './generated/version.js';
export * from './initializePuppeteer.js';
export * from './node/BrowserFetcher.js'; export * from './node/BrowserFetcher.js';
export * from './node/BrowserRunner.js'; export * from './node/BrowserRunner.js';
export * from './node/ChromeLauncher.js'; export * from './node/ChromeLauncher.js';
export * from './node/FirefoxLauncher.js'; export * from './node/FirefoxLauncher.js';
export * from './node/install.js';
export * from './node/LaunchOptions.js'; export * from './node/LaunchOptions.js';
export * from './node/NodeWebSocketTransport.js'; export * from './node/NodeWebSocketTransport.js';
export * from './node/PipeTransport.js'; export * from './node/PipeTransport.js';
export * from './node/ProductLauncher.js'; export * from './node/ProductLauncher.js';
export * from './node/Puppeteer.js'; export * from './node/Puppeteer.js';
export * from './node/install.js';
export * from './node/util.js'; export * from './node/util.js';
export * from './puppeteer.js';
// Exports from `generated` export * from './revisions.js';
export * from './generated/version.js'; export * from './util/assert.js';
export * from './util/DebuggableDeferredPromise.js';
export * from './util/DeferredPromise.js';
export * from './util/ErrorLike.js';
export * from './util/getPackageDirectory.js';

View file

@ -0,0 +1,20 @@
import {DEFERRED_PROMISE_DEBUG_TIMEOUT} from '../environment.js';
import {DeferredPromise, createDeferredPromise} from './DeferredPromise.js';
/**
* Creates and returns a deferred promise using DEFERRED_PROMISE_DEBUG_TIMEOUT
* if it's specified or a normal deferred promise otherwise.
*
* @internal
*/
export function createDebuggableDeferredPromise<T>(
message: string
): DeferredPromise<T> {
if (DEFERRED_PROMISE_DEBUG_TIMEOUT > 0) {
return createDeferredPromise({
message,
timeout: DEFERRED_PROMISE_DEBUG_TIMEOUT,
});
}
return createDeferredPromise();
}

View file

@ -0,0 +1,68 @@
import {TimeoutError} from '../common/Errors.js';
/**
* @internal
*/
export interface DeferredPromise<T> extends Promise<T> {
finished: () => boolean;
resolved: () => boolean;
resolve: (_: T) => void;
reject: (_: Error) => void;
}
/**
* @internal
*/
export interface DeferredPromiseOptions {
message: string;
timeout: number;
}
/**
* Creates and returns a promise along with the resolve/reject functions.
*
* If the promise has not been resolved/rejected within the `timeout` period,
* the promise gets rejected with a timeout error. `timeout` has to be greater than 0 or
* it is ignored.
*
* @internal
*/
export function createDeferredPromise<T>(
opts?: DeferredPromiseOptions
): DeferredPromise<T> {
let isResolved = false;
let isRejected = false;
let resolver = (_: T): void => {};
let rejector = (_: Error) => {};
const taskPromise = new Promise<T>((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
const timeoutId =
opts && opts.timeout > 0
? setTimeout(() => {
isRejected = true;
rejector(new TimeoutError(opts.message));
}, opts.timeout)
: undefined;
return Object.assign(taskPromise, {
resolved: () => {
return isResolved;
},
finished: () => {
return isResolved || isRejected;
},
resolve: (value: T) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
isResolved = true;
resolver(value);
},
reject: (err: Error) => {
clearTimeout(timeoutId);
isRejected = true;
rejector(err);
},
});
}

View file

@ -0,0 +1,27 @@
/**
* @internal
*/
export interface ErrorLike extends Error {
name: string;
message: string;
}
/**
* @internal
*/
export function isErrorLike(obj: unknown): obj is ErrorLike {
return (
typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj
);
}
/**
* @internal
*/
export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException {
return (
isErrorLike(obj) &&
('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj)
);
}

View file

@ -0,0 +1,18 @@
import {existsSync} from 'fs';
import {dirname, join, parse} from 'path';
/**
* @internal
*/
export const getPackageDirectory = (from: string): string => {
let found = existsSync(join(from, 'package.json'));
const root = parse(from).root;
while (!found) {
if (from === root) {
throw new Error('Cannot find package directory');
}
from = dirname(from);
found = existsSync(join(from, 'package.json'));
}
return from;
};

View file

@ -6,6 +6,7 @@ function foo() {
console.log(2); console.log(2);
let x = 1 > 2 ? 'foo' : 'bar'; let x = 1 > 2 ? 'foo' : 'bar';
let y = 1 < 2 ? 'foo' : 'bar'; let y = 1 < 2 ? 'foo' : 'bar';
let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};
let z = () => {}; let z = () => {};
let q = () => {}; let q = () => {};
q(); q();

View file

@ -0,0 +1 @@
// empty

View file

@ -0,0 +1,9 @@
{
"name": "Simple extension",
"version": "0.1",
"background": {
"service_worker": "background.js"
},
"permissions": ["background", "activeTab"],
"manifest_version": 3
}

View file

@ -16,13 +16,21 @@
}, },
{ {
"start": 148, "start": 148,
"end": 160 "end": 168
}, },
{ {
"start": 168, "start": 203,
"end": 207 "end": 204
},
{
"start": 238,
"end": 251
},
{
"start": 259,
"end": 298
} }
], ],
"text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n"
} }
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -22,7 +22,7 @@ import {
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeChromeOnly, describeChromeOnly,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
describeChromeOnly('Target.createCDPSession', function () { describeChromeOnly('Target.createCDPSession', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();

View file

@ -23,7 +23,7 @@ import {
} from '../../lib/cjs/puppeteer/common/NetworkManager.js'; } from '../../lib/cjs/puppeteer/common/NetworkManager.js';
import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js'; import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js';
import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js';
import {Frame} from '../../lib/cjs/puppeteer/common/FrameManager.js'; import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js';
import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js';
class MockCDPSession extends EventEmitter { class MockCDPSession extends EventEmitter {

View file

@ -334,7 +334,7 @@ describeChromeOnly('AriaQueryHandler', () => {
await otherFrame!.evaluate(addElement, 'button'); await otherFrame!.evaluate(addElement, 'button');
await page.evaluate(addElement, 'button'); await page.evaluate(addElement, 'button');
const elementHandle = await watchdog; const elementHandle = await watchdog;
expect(elementHandle!.executionContext().frame()).toBe(page.mainFrame()); expect(elementHandle!.frame).toBe(page.mainFrame());
}); });
it('should run in specified frame', async () => { it('should run in specified frame', async () => {
@ -350,7 +350,7 @@ describeChromeOnly('AriaQueryHandler', () => {
await frame1!.evaluate(addElement, 'button'); await frame1!.evaluate(addElement, 'button');
await frame2!.evaluate(addElement, 'button'); await frame2!.evaluate(addElement, 'button');
const elementHandle = await waitForSelectorPromise; const elementHandle = await waitForSelectorPromise;
expect(elementHandle!.executionContext().frame()).toBe(frame2); expect(elementHandle!.frame).toBe(frame2);
}); });
it('should throw when frame is detached', async () => { it('should throw when frame is detached', async () => {
@ -687,7 +687,7 @@ describeChromeOnly('AriaQueryHandler', () => {
const {page} = getTestState(); const {page} = getTestState();
const found = await page.$$('aria/title'); const found = await page.$$('aria/title');
const ids = await getIds(found); const ids = await getIds(found);
expect(ids).toEqual(['shown', 'hidden']); expect(ids).toEqual(['shown']);
}); });
}); });
}); });

View file

@ -104,9 +104,11 @@ describe('Coverage specs', function () {
const coverage = await page.coverage.stopJSCoverage(); const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(1); expect(coverage.length).toBe(1);
const entry = coverage[0]!; const entry = coverage[0]!;
expect(entry.ranges.length).toBe(1); expect(entry.ranges.length).toBe(2);
const range = entry.ranges[0]!; const range1 = entry.ranges[0]!;
expect(entry.text.substring(range.start, range.end)).toBe( expect(entry.text.substring(range1.start, range1.end)).toBe('\n');
const range2 = entry.ranges[1]!;
expect(entry.text.substring(range2.start, range2.end)).toBe(
`console.log('used!');` `console.log('used!');`
); );
}); });

View file

@ -16,7 +16,7 @@
import expect from 'expect'; import expect from 'expect';
import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.js'; import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.js';
import {Frame} from '../../lib/cjs/puppeteer/common/FrameManager.js'; import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js';
import { import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
@ -41,8 +41,8 @@ describe('Frame specs', function () {
expect(context1).toBeTruthy(); expect(context1).toBeTruthy();
expect(context2).toBeTruthy(); expect(context2).toBeTruthy();
expect(context1 !== context2).toBeTruthy(); expect(context1 !== context2).toBeTruthy();
expect(context1.frame()).toBe(frame1); expect(context1._world?.frame()).toBe(frame1);
expect(context2.frame()).toBe(frame2); expect(context2._world?.frame()).toBe(frame2);
await Promise.all([ await Promise.all([
context1.evaluate(() => { context1.evaluate(() => {

View file

@ -36,6 +36,13 @@ const mkdtempAsync = promisify(fs.mkdtemp);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const extensionPath = path.join(__dirname, '../assets', 'simple-extension'); const extensionPath = path.join(__dirname, '../assets', 'simple-extension');
const serviceWorkerExtensionPath = path.join(
__dirname,
'..',
'assets',
'serviceworkers',
'extension'
);
describeChromeOnly('headful tests', function () { describeChromeOnly('headful tests', function () {
/* These tests fire up an actual browser so let's /* These tests fire up an actual browser so let's
@ -120,7 +127,7 @@ describeChromeOnly('headful tests', function () {
); );
const page = await browserWithExtension.newPage(); const page = await browserWithExtension.newPage();
const backgroundPageTarget = await browserWithExtension.waitForTarget( const backgroundPageTarget = await browserWithExtension.waitForTarget(
(target: {type: () => string}) => { target => {
return target.type() === 'background_page'; return target.type() === 'background_page';
} }
); );
@ -128,6 +135,26 @@ describeChromeOnly('headful tests', function () {
await browserWithExtension.close(); await browserWithExtension.close();
expect(backgroundPageTarget).toBeTruthy(); expect(backgroundPageTarget).toBeTruthy();
}); });
it('service_worker target type should be available', async () => {
const {puppeteer, defaultBrowserOptions} = getTestState();
const browserWithExtension = await launchBrowser(puppeteer, {
...defaultBrowserOptions,
headless: false,
args: [
`--disable-extensions-except=${serviceWorkerExtensionPath}`,
`--load-extension=${serviceWorkerExtensionPath}`,
],
});
const page = await browserWithExtension.newPage();
const serviceWorkerTarget = await browserWithExtension.waitForTarget(
target => {
return target.type() === 'service_worker';
}
);
await page.close();
await browserWithExtension.close();
expect(serviceWorkerTarget).toBeTruthy();
});
it('target.page() should return a background_page', async function () { it('target.page() should return a background_page', async function () {
const {puppeteer} = getTestState(); const {puppeteer} = getTestState();
const browserWithExtension = await launchBrowser( const browserWithExtension = await launchBrowser(
@ -135,7 +162,7 @@ describeChromeOnly('headful tests', function () {
extensionOptions extensionOptions
); );
const backgroundPageTarget = await browserWithExtension.waitForTarget( const backgroundPageTarget = await browserWithExtension.waitForTarget(
(target: {type: () => string}) => { target => {
return target.type() === 'background_page'; return target.type() === 'background_page';
} }
); );

View file

@ -0,0 +1,40 @@
/**
* 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 expect from 'expect';
import '../../lib/cjs/puppeteer/generated/injected.js';
import {PUPPETEER_WORLD} from '../../lib/cjs/puppeteer/common/IsolatedWorld.js';
import {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} from './mocha-utils.js';
describe('InjectedUtil tests', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async () => {
const {page} = getTestState();
const handle = await page
.mainFrame()
.worlds[PUPPETEER_WORLD].evaluate(() => {
return typeof InjectedUtil === 'object';
});
expect(handle).toBeTruthy();
});
});

View file

@ -375,9 +375,7 @@ describe('Launcher specs', function () {
// This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778
await rmAsync(userDataDir).catch(() => {}); await rmAsync(userDataDir).catch(() => {});
}); });
// This mysteriously fails on Windows on AppVeyor. See it('userDataDir option should restore cookies', async () => {
// https://github.com/puppeteer/puppeteer/issues/4111
xit('userDataDir option should restore cookies', async () => {
const {server, puppeteer, defaultBrowserOptions} = getTestState(); const {server, puppeteer, defaultBrowserOptions} = getTestState();
const userDataDir = await mkdtempAsync(TMP_FOLDER); const userDataDir = await mkdtempAsync(TMP_FOLDER);

View file

@ -26,7 +26,7 @@ import {
BrowserContext, BrowserContext,
} from '../../lib/cjs/puppeteer/common/Browser.js'; } from '../../lib/cjs/puppeteer/common/Browser.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
PuppeteerLaunchOptions, PuppeteerLaunchOptions,
PuppeteerNode, PuppeteerNode,

View file

@ -75,7 +75,7 @@ describe('navigation', function () {
let error!: Error; let error!: Error;
await page await page
.goto(server.PREFIX + '/frames/one-frame.html') .goto(server.PREFIX + '/frames/one-frame.html')
.catch((error_) => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).toBeUndefined(); expect(error).toBeUndefined();
@ -88,7 +88,7 @@ describe('navigation', function () {
res.end(); res.end();
}); });
let error!: Error; let error!: Error;
await page.goto(server.EMPTY_PAGE).catch((error_) => { await page.goto(server.EMPTY_PAGE).catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).not.toBe(null); expect(error).not.toBe(null);
@ -122,27 +122,33 @@ describe('navigation', function () {
const response = await page.goto(server.PREFIX + '/grid.html'); const response = await page.goto(server.PREFIX + '/grid.html');
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
}); });
it('should navigate to empty page with networkidle0', async () => { it(
'should navigate to empty page with networkidle0',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, { const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle0', waitUntil: 'networkidle0',
}); });
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
}); }
it('should navigate to empty page with networkidle2', async () => { );
it(
'should navigate to empty page with networkidle2',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const response = await page.goto(server.EMPTY_PAGE, { const response = await page.goto(server.EMPTY_PAGE, {
waitUntil: 'networkidle2', waitUntil: 'networkidle2',
}); });
expect(response!.status()).toBe(200); expect(response!.status()).toBe(200);
}); }
);
it('should fail when navigating to bad url', async () => { it('should fail when navigating to bad url', async () => {
const {page, isChrome} = getTestState(); const {page, isChrome} = getTestState();
let error!: Error; let error!: Error;
await page.goto('asdfasdf').catch((error_) => { await page.goto('asdfasdf').catch(error_ => {
return (error = error_); return (error = error_);
}); });
if (isChrome) { if (isChrome) {
@ -184,7 +190,7 @@ describe('navigation', function () {
}); });
let error!: Error; let error!: Error;
await page.goto(httpsServer.EMPTY_PAGE).catch((error_) => { await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => {
return (error = error_); return (error = error_);
}); });
if (isChrome) { if (isChrome) {
@ -203,9 +209,7 @@ describe('navigation', function () {
server.setRedirect('/redirect/1.html', '/redirect/2.html'); server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html'); server.setRedirect('/redirect/2.html', '/empty.html');
let error!: Error; let error!: Error;
await page await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => {
.goto(httpsServer.PREFIX + '/redirect/1.html')
.catch((error_) => {
return (error = error_); return (error = error_);
}); });
if (isChrome) { if (isChrome) {
@ -214,27 +218,13 @@ describe('navigation', function () {
expect(error.message).toContain('SSL_ERROR_UNKNOWN'); expect(error.message).toContain('SSL_ERROR_UNKNOWN');
} }
}); });
it('should throw if networkidle is passed as an option', async () => {
const { page, server } = getTestState();
let error!: Error;
await page
// @ts-expect-error purposefully passing an old option
.goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' })
.catch((error_) => {
return (error = error_);
});
expect(error.message).toContain(
'"networkidle" option is no longer supported'
);
});
it('should fail when main resources failed to load', async () => { it('should fail when main resources failed to load', async () => {
const {page, isChrome} = getTestState(); const {page, isChrome} = getTestState();
let error!: Error; let error!: Error;
await page await page
.goto('http://localhost:44123/non-existing-url') .goto('http://localhost:44123/non-existing-url')
.catch((error_) => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
if (isChrome) { if (isChrome) {
@ -251,7 +241,7 @@ describe('navigation', function () {
let error!: Error; let error!: Error;
await page await page
.goto(server.PREFIX + '/empty.html', {timeout: 1}) .goto(server.PREFIX + '/empty.html', {timeout: 1})
.catch((error_) => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
@ -264,7 +254,7 @@ describe('navigation', function () {
server.setRoute('/empty.html', () => {}); server.setRoute('/empty.html', () => {});
let error!: Error; let error!: Error;
page.setDefaultNavigationTimeout(1); page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch((error_) => { await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
@ -277,7 +267,7 @@ describe('navigation', function () {
server.setRoute('/empty.html', () => {}); server.setRoute('/empty.html', () => {});
let error!: Error; let error!: Error;
page.setDefaultTimeout(1); page.setDefaultTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch((error_) => { await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
@ -291,7 +281,7 @@ describe('navigation', function () {
let error!: Error; let error!: Error;
page.setDefaultTimeout(0); page.setDefaultTimeout(0);
page.setDefaultNavigationTimeout(1); page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch((error_) => { await page.goto(server.PREFIX + '/empty.html').catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); expect(error.message).toContain('Navigation timeout of 1 ms exceeded');
@ -307,7 +297,7 @@ describe('navigation', function () {
}); });
await page await page
.goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}) .goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']})
.catch((error_) => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).toBeUndefined(); expect(error).toBeUndefined();
@ -342,7 +332,9 @@ describe('navigation', function () {
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
expect(response.url()).toBe(server.EMPTY_PAGE); expect(response.url()).toBe(server.EMPTY_PAGE);
}); });
it('should wait for network idle to succeed navigation', async () => { it(
'should wait for network idle to succeed navigation',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
let responses: ServerResponse[] = []; let responses: ServerResponse[] = [];
@ -370,9 +362,12 @@ describe('navigation', function () {
// Navigate to a page which loads immediately and then does a bunch of // Navigate to a page which loads immediately and then does a bunch of
// requests via javascript's fetch method. // requests via javascript's fetch method.
const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', { const navigationPromise = page.goto(
server.PREFIX + '/networkidle.html',
{
waitUntil: 'networkidle0', waitUntil: 'networkidle0',
}); }
);
// Track when the navigation gets completed. // Track when the navigation gets completed.
let navigationFinished = false; let navigationFinished = false;
navigationPromise.then(() => { navigationPromise.then(() => {
@ -380,7 +375,7 @@ describe('navigation', function () {
}); });
// Wait for the page's 'load' event. // Wait for the page's 'load' event.
await new Promise((fulfill) => { await new Promise(fulfill => {
return page.once('load', fulfill); return page.once('load', fulfill);
}); });
expect(navigationFinished).toBe(false); expect(navigationFinished).toBe(false);
@ -414,12 +409,13 @@ describe('navigation', function () {
const response = (await navigationPromise)!; const response = (await navigationPromise)!;
// Expect navigation to succeed. // Expect navigation to succeed.
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
}); }
);
it('should not leak listeners during navigation', async () => { it('should not leak listeners during navigation', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
let warning = null; let warning = null;
const warningHandler: NodeJS.WarningListener = (w) => { const warningHandler: NodeJS.WarningListener = w => {
return (warning = w); return (warning = w);
}; };
process.on('warning', warningHandler); process.on('warning', warningHandler);
@ -433,7 +429,7 @@ describe('navigation', function () {
const {page} = getTestState(); const {page} = getTestState();
let warning = null; let warning = null;
const warningHandler: NodeJS.WarningListener = (w) => { const warningHandler: NodeJS.WarningListener = w => {
return (warning = w); return (warning = w);
}; };
process.on('warning', warningHandler); process.on('warning', warningHandler);
@ -449,7 +445,7 @@ describe('navigation', function () {
const {context, server} = getTestState(); const {context, server} = getTestState();
let warning = null; let warning = null;
const warningHandler: NodeJS.WarningListener = (w) => { const warningHandler: NodeJS.WarningListener = w => {
return (warning = w); return (warning = w);
}; };
process.on('warning', warningHandler); process.on('warning', warningHandler);
@ -463,11 +459,13 @@ describe('navigation', function () {
process.removeListener('warning', warningHandler); process.removeListener('warning', warningHandler);
expect(warning).toBe(null); expect(warning).toBe(null);
}); });
it('should navigate to dataURL and fire dataURL requests', async () => { it(
'should navigate to dataURL and fire dataURL requests',
async () => {
const {page} = getTestState(); const {page} = getTestState();
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
page.on('request', (request) => { page.on('request', request => {
return !utils.isFavicon(request) && requests.push(request); return !utils.isFavicon(request) && requests.push(request);
}); });
const dataURL = 'data:text/html,<div>yo</div>'; const dataURL = 'data:text/html,<div>yo</div>';
@ -475,12 +473,15 @@ describe('navigation', function () {
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(requests.length).toBe(1); expect(requests.length).toBe(1);
expect(requests[0]!.url()).toBe(dataURL); expect(requests[0]!.url()).toBe(dataURL);
}); }
it('should navigate to URL with hash and fire requests without hash', async () => { );
it(
'should navigate to URL with hash and fire requests without hash',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
const requests: HTTPRequest[] = []; const requests: HTTPRequest[] = [];
page.on('request', (request) => { page.on('request', request => {
return !utils.isFavicon(request) && requests.push(request); return !utils.isFavicon(request) && requests.push(request);
}); });
const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!; const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!;
@ -488,7 +489,8 @@ describe('navigation', function () {
expect(response.url()).toBe(server.EMPTY_PAGE); expect(response.url()).toBe(server.EMPTY_PAGE);
expect(requests.length).toBe(1); expect(requests.length).toBe(1);
expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); expect(requests[0]!.url()).toBe(server.EMPTY_PAGE);
}); }
);
it('should work with self requesting page', async () => { it('should work with self requesting page', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -612,7 +614,9 @@ describe('navigation', function () {
expect(response).toBe(null); expect(response).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/replaced.html'); expect(page.url()).toBe(server.PREFIX + '/replaced.html');
}); });
it('should work with DOM history.back()/history.forward()', async () => { it(
'should work with DOM history.back()/history.forward()',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -639,8 +643,11 @@ describe('navigation', function () {
]); ]);
expect(forwardResponse).toBe(null); expect(forwardResponse).toBe(null);
expect(page.url()).toBe(server.PREFIX + '/second.html'); expect(page.url()).toBe(server.PREFIX + '/second.html');
}); }
it('should work when subframe issues window.stop()', async () => { );
it(
'should work when subframe issues window.stop()',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
server.setRoute('/frames/style.css', () => {}); server.setRoute('/frames/style.css', () => {});
@ -648,8 +655,8 @@ describe('navigation', function () {
server.PREFIX + '/frames/one-frame.html' server.PREFIX + '/frames/one-frame.html'
); );
const frame = await utils.waitEvent(page, 'frameattached'); const frame = await utils.waitEvent(page, 'frameattached');
await new Promise<void>((fulfill) => { await new Promise<void>(fulfill => {
page.on('framenavigated', (f) => { page.on('framenavigated', f => {
if (f === frame) { if (f === frame) {
fulfill(); fulfill();
} }
@ -661,7 +668,8 @@ describe('navigation', function () {
}), }),
navigationPromise, navigationPromise,
]); ]);
}); }
);
}); });
describe('Page.goBack', function () { describe('Page.goBack', function () {
@ -722,12 +730,12 @@ describe('navigation', function () {
const navigationPromise = page const navigationPromise = page
.frames()[1]! .frames()[1]!
.goto(server.EMPTY_PAGE) .goto(server.EMPTY_PAGE)
.catch((error_) => { .catch(error_ => {
return error_; return error_;
}); });
await server.waitForRequest('/empty.html'); await server.waitForRequest('/empty.html');
await page.$eval('iframe', (frame) => { await page.$eval('iframe', frame => {
return frame.remove(); return frame.remove();
}); });
const error = await navigationPromise; const error = await navigationPromise;
@ -791,7 +799,7 @@ describe('navigation', function () {
server.setRoute('/empty.html', () => {}); server.setRoute('/empty.html', () => {});
let error!: Error; let error!: Error;
const navigationPromise = frame.waitForNavigation().catch((error_) => { const navigationPromise = frame.waitForNavigation().catch(error_ => {
return (error = error_); return (error = error_);
}); });
await Promise.all([ await Promise.all([
@ -800,7 +808,7 @@ describe('navigation', function () {
return ((window as any).location = '/empty.html'); return ((window as any).location = '/empty.html');
}), }),
]); ]);
await page.$eval('iframe', (frame) => { await page.$eval('iframe', frame => {
return frame.remove(); return frame.remove();
}); });
await navigationPromise; await navigationPromise;

View file

@ -469,7 +469,6 @@ describe('network', function () {
}); });
(await page.goto(server.EMPTY_PAGE))!; (await page.goto(server.EMPTY_PAGE))!;
expect(responses.length).toBe(1); expect(responses.length).toBe(1);
console.log('timing',responses[0]!.timing())
expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0); expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0);
}); });
}); });
@ -854,10 +853,13 @@ describe('network', function () {
res.end(); res.end();
}); });
await page.goto(httpsServer.PREFIX + '/setcookie.html'); await page.goto(httpsServer.PREFIX + '/setcookie.html');
const response = await new Promise<HTTPResponse>(resolve => {
page.on('response', resolve);
const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
const response = await new Promise<HTTPResponse>(resolve => {
page.on('response', response => {
if (response.url() === url) {
resolve(response);
}
});
page.evaluate(src => { page.evaluate(src => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', src); xhr.open('GET', src);

View file

@ -433,9 +433,7 @@ describe('Page', function () {
}) })
).toEqual(['prompt', 'denied', 'granted', 'prompt']); ).toEqual(['prompt', 'denied', 'granted', 'prompt']);
}); });
it( it('should isolate permissions between browser contexts', async () => {
'should isolate permissions between browser contexts',
async () => {
const {page, server, context, browser} = getTestState(); const {page, server, context, browser} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -457,8 +455,7 @@ describe('Page', function () {
expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
await otherContext.close(); await otherContext.close();
} });
);
it('should grant persistent-storage', async () => { it('should grant persistent-storage', async () => {
const {page, server, context} = getTestState(); const {page, server, context} = getTestState();
@ -1630,7 +1627,7 @@ describe('Page', function () {
error = error_ as Error; error = error_ as Error;
} }
expect(error.message).toBe( expect(error.message).toBe(
'Provide an object with a `url`, `path` or `content` property' 'Exactly one of `url`, `path`, or `content` must be specified.'
); );
}); });
@ -1692,7 +1689,7 @@ describe('Page', function () {
}); });
it('should throw an error if loading from url fail', async () => { it('should throw an error if loading from url fail', async () => {
const {page, server} = getTestState(); const {page, server, isFirefox} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
let error!: Error; let error!: Error;
@ -1701,7 +1698,11 @@ describe('Page', function () {
} catch (error_) { } catch (error_) {
error = error_ as Error; error = error_ as Error;
} }
expect(error.message).toBe('Loading script from /nonexistfile.js failed'); if (isFirefox) {
expect(error.message).toBeTruthy();
} else {
expect(error.message).toContain('Could not load script');
}
}); });
it('should work with a path', async () => { it('should work with a path', async () => {
@ -1796,7 +1797,7 @@ describe('Page', function () {
error = error_ as Error; error = error_ as Error;
} }
expect(error.message).toBe( expect(error.message).toBe(
'Provide an object with a `url`, `path` or `content` property' 'Exactly one of `url`, `path`, or `content` must be specified.'
); );
}); });
@ -1814,7 +1815,7 @@ describe('Page', function () {
}); });
it('should throw an error if loading from url fail', async () => { it('should throw an error if loading from url fail', async () => {
const {page, server} = getTestState(); const {page, server, isFirefox} = getTestState();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
let error!: Error; let error!: Error;
@ -1823,7 +1824,11 @@ describe('Page', function () {
} catch (error_) { } catch (error_) {
error = error_ as Error; error = error_ as Error;
} }
expect(error.message).toBe('Loading style from /nonexistfile.js failed'); if (isFirefox) {
expect(error.message).toBeTruthy();
} else {
expect(error.message).toContain('Could not load style');
}
}); });
it('should work with a path', async () => { it('should work with a path', async () => {
@ -1849,7 +1854,7 @@ describe('Page', function () {
path: path.join(__dirname, '../assets/injectedstyle.css'), path: path.join(__dirname, '../assets/injectedstyle.css'),
}); });
const styleHandle = (await page.$('style'))!; const styleHandle = (await page.$('style'))!;
const styleContent = await page.evaluate((style: HTMLStyleElement) => { const styleContent = await page.evaluate(style => {
return style.innerHTML; return style.innerHTML;
}, styleHandle); }, styleHandle);
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
@ -1870,9 +1875,7 @@ describe('Page', function () {
).toBe('rgb(0, 128, 0)'); ).toBe('rgb(0, 128, 0)');
}); });
it( it('should throw when added with content to the CSP page', async () => {
'should throw when added with content to the CSP page',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
@ -1883,8 +1886,7 @@ describe('Page', function () {
return (error = error_); return (error = error_);
}); });
expect(error).toBeTruthy(); expect(error).toBeTruthy();
} });
);
it('should throw when added with URL to the CSP page', async () => { it('should throw when added with URL to the CSP page', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -1953,9 +1955,7 @@ describe('Page', function () {
]); ]);
expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
}); });
it( it('should stay disabled when toggling request interception on/off', async () => {
'should stay disabled when toggling request interception on/off',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.setCacheEnabled(false); await page.setCacheEnabled(false);
@ -1968,8 +1968,7 @@ describe('Page', function () {
page.reload(), page.reload(),
]); ]);
expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined);
} });
);
}); });
describe('printing to PDF', function () { describe('printing to PDF', function () {
@ -2214,9 +2213,7 @@ describe('Page', function () {
expect(error.message).toContain('Values must be strings'); expect(error.message).toContain('Values must be strings');
}); });
// @see https://github.com/puppeteer/puppeteer/issues/3327 // @see https://github.com/puppeteer/puppeteer/issues/3327
it( it('should work when re-defining top-level Event class', async () => {
'should work when re-defining top-level Event class',
async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/select.html'); await page.goto(server.PREFIX + '/input/select.html');
@ -2235,8 +2232,7 @@ describe('Page', function () {
return (globalThis as any).result.onChange; return (globalThis as any).result.onChange;
}) })
).toEqual(['blue']); ).toEqual(['blue']);
} });
);
}); });
describe('Page.Events.Close', function () { describe('Page.Events.Close', function () {

View file

@ -18,8 +18,8 @@ import expect from 'expect';
import http from 'http'; import http from 'http';
import os from 'os'; import os from 'os';
import { import {
describeFailsFirefox,
getTestState, getTestState,
describeFailsFirefox,
itFailsWindows, itFailsWindows,
} from './mocha-utils.js'; } from './mocha-utils.js';
import type {Server, IncomingMessage, ServerResponse} from 'http'; import type {Server, IncomingMessage, ServerResponse} from 'http';
@ -125,7 +125,7 @@ describeFailsFirefox('request proxy', () => {
const response = (await page.goto(emptyPageUrl))!; const response = (await page.goto(emptyPageUrl))!;
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
console.log('test',{proxiedRequestUrls, emptyPageUrl})
expect(proxiedRequestUrls).toEqual([emptyPageUrl]); expect(proxiedRequestUrls).toEqual([emptyPageUrl]);
}); });

View file

@ -614,10 +614,11 @@ describe('request interception', function () {
return request.continue(); return request.continue();
}); });
await page.goto(server.PREFIX + '/cached/one-style-font.html'); const responsePromise = page.waitForResponse(r => {
await page.waitForResponse(r => {
return r.url().endsWith('/one-style.woff'); return r.url().endsWith('/one-style.woff');
}); });
await page.goto(server.PREFIX + '/cached/one-style-font.html');
await responsePromise;
}); });
}); });

View file

@ -51,6 +51,22 @@ describe('Screenshots', function () {
}); });
expect(screenshot).toBeGolden('screenshot-clip-rect.png'); expect(screenshot).toBeGolden('screenshot-clip-rect.png');
}); });
it('should use scale for clip', async () => {
const {page, server} = getTestState();
await page.setViewport({width: 500, height: 500});
await page.goto(server.PREFIX + '/grid.html');
const screenshot = await page.screenshot({
clip: {
x: 50,
y: 100,
width: 150,
height: 100,
scale: 2,
},
});
expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png');
});
it( it(
'should get screenshot bigger than the viewport', 'should get screenshot bigger than the viewport',
async () => { async () => {

View file

@ -99,13 +99,10 @@ describe('Target', function () {
return window.open(url); return window.open(url);
}, server.CROSS_PROCESS_PREFIX + '/empty.html'), }, server.CROSS_PROCESS_PREFIX + '/empty.html'),
]); ]);
expect(otherPage!.url()).toEqual( expect(otherPage!.url()).toEqual(
server.CROSS_PROCESS_PREFIX + '/empty.html' server.CROSS_PROCESS_PREFIX + '/empty.html'
); );
expect(page).not.toEqual(otherPage); expect(page).not.toEqual(otherPage);
await otherPage!.close();
}); });
it( it(
'should report when a new page is created and closed', 'should report when a new page is created and closed',

View file

@ -16,7 +16,7 @@
import expect from 'expect'; import expect from 'expect';
import path from 'path'; import path from 'path';
import {Frame} from '../../lib/cjs/puppeteer/common/FrameManager.js'; import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js';
import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js';
import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js';
import {compare} from './golden-utils.js'; import {compare} from './golden-utils.js';

View file

@ -15,7 +15,7 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
@ -476,7 +476,7 @@ describe('waittask specs', function () {
await otherFrame.evaluate(addElement, 'div'); await otherFrame.evaluate(addElement, 'div');
await page.evaluate(addElement, 'div'); await page.evaluate(addElement, 'div');
const eHandle = await watchdog; const eHandle = await watchdog;
expect(eHandle?.executionContext().frame()).toBe(page.mainFrame()); expect(eHandle?.frame).toBe(page.mainFrame());
} }
); );
@ -491,7 +491,7 @@ describe('waittask specs', function () {
await frame1.evaluate(addElement, 'div'); await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div');
const eHandle = await waitForSelectorPromise; const eHandle = await waitForSelectorPromise;
expect(eHandle?.executionContext().frame()).toBe(frame2); expect(eHandle?.frame).toBe(frame2);
}); });
it('should throw when frame is detached', async () => { it('should throw when frame is detached', async () => {
@ -748,7 +748,7 @@ describe('waittask specs', function () {
await frame1.evaluate(addElement, 'div'); await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div');
const eHandle = await waitForXPathPromise; const eHandle = await waitForXPathPromise;
expect(eHandle?.executionContext().frame()).toBe(frame2); expect(eHandle?.frame).toBe(frame2);
}); });
it('should throw when frame is detached', async () => { it('should throw when frame is detached', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();

View file

@ -9,7 +9,7 @@
}, },
"include": ["src"], "include": ["src"],
"references": [ "references": [
{"path": "../src/tsconfig.cjs.json"}, {"path": "../tsconfig.lib.json"},
{"path": "../utils/testserver/tsconfig.json"} {"path": "../utils/testserver/tsconfig.json"}
] ]
} }

View file

@ -0,0 +1,16 @@
/**
* This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation.
* For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`.
* See the details in CONTRIBUTING.md that describes our TypeScript setup.
*/
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true
},
"references": [
{"path": "src/tsconfig.esm.json"},
{"path": "src/tsconfig.cjs.json"}
],
"exclude": ["**/*"]
}

View file

@ -45,7 +45,7 @@ const fileExists = async filePath => {
* place. * place.
*/ */
async function compileTypeScript() { async function compileTypeScript() {
return exec('npm run build:tsc').catch(error => { return exec('npm run build').catch(error => {
console.error('Error running TypeScript', error); console.error('Error running TypeScript', error);
process.exit(1); process.exit(1);
}); });

View file

@ -1,30 +0,0 @@
const {readdirSync, writeFileSync} = require('fs');
const {join, basename} = require('path');
const EXCLUDE_FILES = ['puppeteer-core.ts'];
let typesTs = '// AUTOGENERATED - Use `utils/export_all.js` to regenerate.\n';
typesTs += `\n`;
for (const file of readdirSync(join(__dirname, `../src`)).filter(filename => {
return (
filename.endsWith('ts') &&
!filename.startsWith('types') &&
!EXCLUDE_FILES.includes(filename)
);
})) {
typesTs += `export * from './${basename(file, '.ts')}.js';\n`;
}
for (const folder of ['common', 'node', 'generated']) {
typesTs += `\n// Exports from \`${folder}\`\n`;
for (const file of readdirSync(join(__dirname, `../src/${folder}`)).filter(
filename => {
return filename.endsWith('ts') && !EXCLUDE_FILES.includes(filename);
}
)) {
typesTs += `export * from './${folder}/${basename(file, '.ts')}.js';\n`;
}
}
writeFileSync(join(__dirname, '../src/types.ts'), typesTs);

View file

@ -0,0 +1,28 @@
#!/usr/bin/env node
import {writeFile} from 'fs/promises';
import {job} from './internal/job.js';
import {spawnAndLog} from './internal/util.js';
(async () => {
job('', async ({outputs}) => {
await writeFile(outputs[0]!, '{"type": "module"}');
})
.outputs(['lib/esm/package.json'])
.build();
job('', async ({outputs}) => {
spawnAndLog('api-extractor', 'run', '--local');
spawnAndLog(
'eslint',
'--ext=ts',
'--no-ignore',
'--no-eslintrc',
'-c=.eslintrc.types.cjs',
'--fix',
outputs[0]!
);
})
.inputs(['lib/esm/puppeteer/types.d.ts'])
.outputs(['lib/types.d.ts', 'docs/puppeteer.api.json'])
.build();
})();

View file

@ -14,15 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import {readFileSync, writeFileSync} from 'fs'; import {readFile, rm, writeFile} from 'fs/promises';
import {join} from 'path';
import {chdir} from 'process';
import semver from 'semver'; import semver from 'semver';
import {versionsPerRelease} from '../versions.js'; import {generateDocs} from './internal/custom_markdown_action.js';
import versionsArchived from '../website/versionsArchived.json'; import {job} from './internal/job.js';
import {spawnAndLog} from './internal/util.js';
// eslint-disable-next-line import/extensions
import {generateDocs} from './internal/custom_markdown_action';
function getOffsetAndLimit( function getOffsetAndLimit(
sectionName: string, sectionName: string,
@ -49,25 +45,25 @@ function spliceIntoSection(
return lines.join('\n'); return lines.join('\n');
} }
// Change to root directory (async () => {
chdir(join(__dirname, '..')); const job1 = job('', async ({inputs, outputs}) => {
const content = await readFile(inputs[0]!, 'utf-8');
// README
{
const content = readFileSync('README.md', 'utf-8');
const sectionContent = ` const sectionContent = `
--- ---
sidebar_position: 1 sidebar_position: 1
--- ---
`; `;
writeFileSync('docs/index.md', sectionContent + content); await writeFile(outputs[0]!, sectionContent + content);
} })
.inputs(['README.md'])
.outputs(['docs/index.md'])
.build();
// Chrome Versions // Chrome Versions
{ const job2 = job('', async ({inputs, outputs}) => {
const filename = 'docs/chromium-support.md'; let content = await readFile(inputs[2]!, {encoding: 'utf8'});
let content = readFileSync(filename, {encoding: 'utf8'}); const {versionsPerRelease} = await import(inputs[0]!);
const versionsArchived = JSON.parse(await readFile(inputs[1]!, 'utf8'));
// Generate versions // Generate versions
const buffer: string[] = []; const buffer: string[] = [];
@ -97,8 +93,25 @@ chdir(join(__dirname, '..'));
} }
content = spliceIntoSection('version', content, buffer.join('\n')); content = spliceIntoSection('version', content, buffer.join('\n'));
writeFileSync(filename, content); await writeFile(outputs[0]!, content);
} })
.inputs([
'versions.js',
'website/versionsArchived.json',
'docs/chromium-support.md',
])
.outputs(['docs/chromium-support.md'])
.build();
await Promise.all([job1, job2]);
// Generate documentation // Generate documentation
generateDocs('docs/puppeteer.api.json', 'docs/api'); job('', async ({inputs, outputs}) => {
await rm(outputs[0]!, {recursive: true, force: true});
generateDocs(inputs[0]!, outputs[0]!);
spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs');
})
.inputs(['docs/puppeteer.api.json'])
.outputs(['docs/api'])
.build();
})();

View file

@ -0,0 +1,106 @@
#!/usr/bin/env node
import {createHash} from 'crypto';
import esbuild from 'esbuild';
import {mkdir, mkdtemp, readFile, rm, writeFile} from 'fs/promises';
import {sync as glob} from 'glob';
import path from 'path';
import {job} from './internal/job.js';
const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util'];
(async () => {
await job('', async ({outputs}) => {
await Promise.all(
outputs.map(outputs => {
return mkdir(outputs, {recursive: true});
})
);
})
.outputs(['src/generated'])
.build();
await job('', async ({name, inputs, outputs}) => {
const input = inputs.find(input => {
return input.endsWith('injected.ts');
})!;
const template = await readFile(
inputs.find(input => {
return input.includes('injected.ts.tmpl');
})!,
'utf8'
);
const tmp = await mkdtemp(name);
await esbuild.build({
entryPoints: [input],
bundle: true,
outdir: tmp,
format: 'cjs',
platform: 'browser',
target: 'ES2019',
});
const baseName = path.basename(input);
const content = await readFile(
path.join(tmp, baseName.replace('.ts', '.js')),
'utf-8'
);
const scriptContent = template.replace(
'SOURCE_CODE',
JSON.stringify(content)
);
await writeFile(outputs[0]!, scriptContent);
await rm(tmp, {recursive: true, force: true});
})
.inputs(['src/templates/injected.ts.tmpl', 'src/injected/**/*.ts'])
.outputs(['src/generated/injected.ts'])
.build();
const sources = glob(
`src/{@(${INCLUDED_FOLDERS.join('|')})/*.ts,!(types|puppeteer-core).ts}`
);
await job('', async ({outputs}) => {
let types =
'// AUTOGENERATED - Use `npm run generate:sources` to regenerate.\n\n';
for (const input of sources.map(source => {
return `.${source.slice(3)}`;
})) {
types += `export * from '${input.replace('.ts', '.js')}';\n`;
}
await writeFile(outputs[0]!, types);
})
.value(
sources
.reduce((hmac, value) => {
return hmac.update(value);
}, createHash('sha256'))
.digest('hex')
)
.outputs(['src/types.ts'])
.build();
if (process.env['PUBLISH']) {
job('', async ({inputs}) => {
const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version;
await writeFile(
inputs[1]!,
(
await readFile(inputs[1]!, {
encoding: 'utf-8',
})
).replace("'NEXT'", `'v${version}'`)
);
})
.inputs(['package.json', 'versions.js'])
.build();
}
job('', async ({inputs, outputs}) => {
const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version;
await writeFile(
outputs[0]!,
(await readFile(inputs[1]!, 'utf8')).replace('PACKAGE_VERSION', version)
);
})
.inputs(['package.json', 'src/templates/version.ts.tmpl'])
.outputs(['src/generated/version.ts'])
.build();
})();

View file

@ -1,18 +0,0 @@
const {writeFileSync, readFileSync} = require('fs');
const {join} = require('path');
const version = require('../package.json').version;
writeFileSync(
join(__dirname, '../src/generated/version.ts'),
readFileSync(join(__dirname, '../src/templates/version.ts.tmpl'), {
encoding: 'utf-8',
}).replace('PACKAGE_VERSION', version)
);
writeFileSync(
join(__dirname, '../versions.js'),
readFileSync(join(__dirname, '../versions.js'), {
encoding: 'utf-8',
}).replace('NEXT', `v${version}`)
);

View file

@ -15,9 +15,7 @@
*/ */
import {ApiModel} from '@microsoft/api-extractor-model'; import {ApiModel} from '@microsoft/api-extractor-model';
import {MarkdownDocumenter} from './custom_markdown_documenter.js';
// eslint-disable-next-line import/extensions
import {MarkdownDocumenter} from './custom_markdown_documenter';
export const generateDocs = (jsonPath: string, outputDir: string): void => { export const generateDocs = (jsonPath: string, outputDir: string): void => {
const apiModel = new ApiModel(); const apiModel = new ApiModel();

View file

@ -0,0 +1,161 @@
import {createHash} from 'crypto';
import {existsSync, Stats} from 'fs';
import {mkdir, readFile, stat, writeFile} from 'fs/promises';
import {glob} from 'glob';
import {tmpdir} from 'os';
import {dirname, join, resolve} from 'path';
import {chdir} from 'process';
const packageRoot = resolve(join(__dirname, '..', '..'));
chdir(packageRoot);
interface JobContext {
name: string;
inputs: string[];
outputs: string[];
}
class JobBuilder {
#inputs: string[] = [];
#outputs: string[] = [];
#callback: (ctx: JobContext) => Promise<void>;
#name: string;
#value = '';
#force = false;
constructor(name: string, callback: (ctx: JobContext) => Promise<void>) {
this.#name = name;
this.#callback = callback;
}
get jobHash(): string {
return createHash('sha256').update(this.#name).digest('hex');
}
force() {
this.#force = true;
return this;
}
value(value: string) {
this.#value = value;
return this;
}
inputs(inputs: string[]): JobBuilder {
this.#inputs = inputs.flatMap(value => {
if (glob.hasMagic(value)) {
return glob.sync(value).map(value => {
// Glob doesn't support `\` on Windows, so we join here.
return join(packageRoot, value);
});
}
return join(packageRoot, value);
});
return this;
}
outputs(outputs: string[]): JobBuilder {
if (!this.#name) {
this.#name = outputs.join(' and ');
}
this.#outputs = outputs.map(value => {
return join(packageRoot, value);
});
return this;
}
async build(): Promise<void> {
console.log(`Running job ${this.#name}...`);
// For debugging.
if (this.#force) {
return this.#run();
}
// In case we deleted an output file on purpose.
if (!this.getOutputStats()) {
return this.#run();
}
// Run if the job has a value, but it changes.
if (this.#value) {
if (!(await this.isValueDifferent())) {
return;
}
return this.#run();
}
// Always run when there is no output.
if (!this.#outputs.length) {
return this.#run();
}
// Make-like comparator.
if (!(await this.areInputsNewer())) {
return;
}
return this.#run();
}
async isValueDifferent(): Promise<boolean> {
const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`);
await mkdir(dirname(file), {recursive: true});
if (!existsSync(file)) {
await writeFile(file, this.#value);
return true;
}
return this.#value !== (await readFile(file, 'utf8'));
}
#outputStats?: Stats[];
async getOutputStats(): Promise<Stats[] | undefined> {
if (this.#outputStats) {
return this.#outputStats;
}
try {
this.#outputStats = await Promise.all(
this.#outputs.map(output => {
return stat(output);
})
);
} catch {}
return this.#outputStats;
}
async areInputsNewer(): Promise<boolean> {
const inputStats = await Promise.all(
this.#inputs.map(input => {
return stat(input);
})
);
const outputStats = await this.getOutputStats();
if (
outputStats &&
outputStats.reduce(reduceMinTime, Infinity) >
inputStats.reduce(reduceMaxTime, 0)
) {
return false;
}
return true;
}
#run(): Promise<void> {
return this.#callback({
name: this.#name,
inputs: this.#inputs,
outputs: this.#outputs,
});
}
}
export const job = (
name: string,
callback: (ctx: JobContext) => Promise<void>
): JobBuilder => {
return new JobBuilder(name, callback);
};
const reduceMaxTime = (time: number, stat: Stats) => {
return time < stat.mtimeMs ? stat.mtimeMs : time;
};
const reduceMinTime = (time: number, stat: Stats) => {
return time > stat.mtimeMs ? stat.mtimeMs : time;
};

View file

@ -0,0 +1,14 @@
import {spawnSync} from 'child_process';
export const spawnAndLog = (...args: string[]): void => {
const {stdout, stderr} = spawnSync(args[0]!, args.slice(1), {
encoding: 'utf-8',
shell: true,
});
if (stdout) {
console.log(stdout);
}
if (stderr) {
console.error(stderr);
}
};

View file

@ -0,0 +1,12 @@
/**
* This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation.
* For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`.
* See the details in CONTRIBUTING.md that describes our TypeScript setup.
*/
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"module": "CommonJS"
}
}

View file

@ -16,7 +16,8 @@
const versionsPerRelease = new Map([ const versionsPerRelease = new Map([
// This is a mapping from Chromium version => Puppeteer version. // This is a mapping from Chromium version => Puppeteer version.
// In Chromium roll patches, use 'v16.1.1' for the Puppeteer version. // In Chromium roll patches, use `NEXT` for the Puppeteer version.
['106.0.5249.0', 'v17.1.0'],
['105.0.5173.0', 'v15.5.0'], ['105.0.5173.0', 'v15.5.0'],
['104.0.5109.0', 'v15.1.0'], ['104.0.5109.0', 'v15.1.0'],
['103.0.5059.0', 'v14.2.0'], ['103.0.5059.0', 'v14.2.0'],