diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000..c6c8b3621938a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000..a6257f0e56673 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,17 @@ +test/assets/modernizr.js +third_party/* +utils/browser/puppeteer-web.js +utils/doclint/check_public_api/test/ +node6/* +node6-test/* +experimental/ +lib/ +/index.d.ts +# We ignore this file because it uses ES imports which we don't yet use +# in the Puppeteer src, so it trips up the ESLint-TypeScript parser. +utils/doclint/generate_types/test/test.ts +vendor/ +web-test-runner.config.mjs +test-ts-types/ +website/ +docs-dist/ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000000..ac342c269b11d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,190 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: ['mocha', '@typescript-eslint', 'unicorn', 'import'], + + extends: ['plugin:prettier/recommended'], + + rules: { + // Error if files are not formatted with Prettier correctly. + 'prettier/prettier': 2, + // syntax preferences + quotes: [ + 2, + 'single', + { + avoidEscape: true, + allowTemplateLiterals: true, + }, + ], + 'spaced-comment': [ + 2, + 'always', + { + markers: ['*'], + }, + ], + eqeqeq: [2], + 'accessor-pairs': [ + 2, + { + getWithoutSet: false, + setWithoutGet: false, + }, + ], + 'new-parens': 2, + 'func-call-spacing': 2, + 'prefer-const': 2, + + 'max-len': [ + 2, + { + /* this setting doesn't impact things as we use Prettier to format + * our code and hence dictate the line length. + * Prettier aims for 80 but sometimes makes the decision to go just + * over 80 chars as it decides that's better than wrapping. ESLint's + * rule defaults to 80 but therefore conflicts with Prettier. So we + * set it to something far higher than Prettier would allow to avoid + * it causing issues and conflicting with Prettier. + */ + code: 200, + comments: 90, + ignoreTemplateLiterals: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreRegExpLiterals: true, + }, + ], + // anti-patterns + 'no-var': 2, + 'no-with': 2, + 'no-multi-str': 2, + 'no-caller': 2, + 'no-implied-eval': 2, + 'no-labels': 2, + 'no-new-object': 2, + 'no-octal-escape': 2, + 'no-self-compare': 2, + 'no-shadow-restricted-names': 2, + 'no-cond-assign': 2, + 'no-debugger': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-unreachable': 2, + 'no-unsafe-negation': 2, + radix: 2, + 'valid-typeof': 2, + 'no-unused-vars': [ + 2, + { + args: 'none', + vars: 'local', + varsIgnorePattern: + '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', + }, + ], + 'no-implicit-globals': [2], + + // es2015 features + 'require-yield': 2, + 'template-curly-spacing': [2, 'never'], + + // ensure we don't have any it.only or describe.only in prod + 'mocha/no-exclusive-tests': 'error', + + // enforce the variable in a catch block is named error + 'unicorn/catch-error-name': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import Mitt from the vendored location: vendor/mitt/src/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + }, + overrides: [ + { + files: ['*.ts'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + 'func-call-spacing': 0, + '@typescript-eslint/func-call-spacing': 2, + semi: 0, + '@typescript-eslint/semi': 2, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-use-before-define': 0, + // We have to use any on some types so the warning isn't valuable. + '@typescript-eslint/no-explicit-any': 0, + // We don't require explicit return types on basic functions or + // dummy functions in tests, for example + '@typescript-eslint/explicit-function-return-type': 0, + // We know it's bad and use it very sparingly but it's needed :( + '@typescript-eslint/ban-ts-ignore': 0, + // We allow non-null assertions if the value was asserted using `assert` API. + '@typescript-eslint/no-non-null-assertion': 0, + /** + * This is the default options (as per + * https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/docs/rules/ban-types.md), + * + * Unfortunately there's no way to + */ + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + /* + * Puppeteer's API accepts generic functions in many places so it's + * not a useful linting rule to ban the `Function` type. This turns off + * the banning of the `Function` type which is a default rule. + */ + Function: false, + }, + }, + ], + '@typescript-eslint/array-type': [ + 2, + { + default: 'array-simple', + }, + ], + // By default this is a warning but we want it to error. + '@typescript-eslint/explicit-module-boundary-types': 2, + }, + }, + { + files: ['test-browser/**/*.js'], + parserOptions: { + sourceType: 'module', + }, + env: { + es6: true, + browser: true, + es2020: true, + }, + }, + ], +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000..222aec2e3a784 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Declare files that will always have LF line endings on checkout. +*.txt eol=lf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000000..368fde0d06aaa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,58 @@ +name: Bug report +description: File a bug report +title: '[Bug]: ' +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: summary + attributes: + label: Bug description + description: What did you do? What did you expect to happen? What actually happened instead? + value: | + Steps to reproduce the problem: + + 1. … + validations: + required: true + - type: input + id: puppeteer-version + attributes: + label: Puppeteer version + description: What version of Puppeteer are you running? + validations: + required: true + - type: input + id: node-version + attributes: + label: Node.js version + description: What version of Node.js are you running? + validations: + required: true + - type: input + id: npm-version + attributes: + label: npm version + description: What version of npm are you running? + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: What operating system are you seeing the problem on? + multiple: true + options: + - Linux + - macOS + - Windows + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. No need for backticks — this automatically gets formatted into code. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..e494371d2d842 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: General Puppeteer questions + url: https://stackoverflow.com/questions/tagged/puppeteer + about: For general technical questions or “how to” guidance, please search StackOverflow for questions tagged “puppeteer” or create a new post. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..5410d486554ff --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ + + +**What kind of change does this PR introduce?** + + + +**Did you add tests for your changes?** + +**If relevant, did you update the documentation?** + +**Summary** + + + + +**Does this PR introduce a breaking change?** + + + +**Other information** diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000..6737890096f01 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: daily + open-pull-requests-limit: 3 + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: daily + open-pull-requests-limit: 2 diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 0000000000000..60a1e744b4bc3 --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1,4 @@ +releaseType: node +primaryBranch: main +handleGHRelease: true +manifest: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000..c538ffb2de507 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,261 @@ +name: CI + +# Declare default permissions as read only. +permissions: read-all + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + linux: + # https://github.com/actions/virtual-environments#available-environments + runs-on: ubuntu-latest + strategy: + matrix: + # Include all major maintenance + active LTS + current Node.js versions. + # https://github.com/nodejs/Release#release-schedule + node: [14, 16] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies + run: | + sudo apt-get install xvfb + # Ensure both a Chromium and a Firefox binary are available. + PUPPETEER_PRODUCT=firefox npm install + npm install + ls .local-chromium .local-firefox + + - name: Build + run: | + npm run build + + - name: Run code checks + run: | + npm run ensure-pinned-deps + npm run lint + # Skipping as it's flakey and we are not currently using the new documentation site in the wild yet. + # See https://github.com/puppeteer/puppeteer/issues/7710 for more info + # npm run generate-docs + npm run ensure-correct-devtools-protocol-revision + npm run test-types-file + + - name: Run commit lint + run: | + npm run commitlint + if: github.event_name != 'pull_request' + + - name: Run unit tests + uses: nick-invision/retry@v2 + env: + CHROMIUM: true + with: + max_attempts: 3 + command: xvfb-run --auto-servernum npm run unit + timeout_minutes: 10 + + - name: Run unit tests with coverage + env: + CHROMIUM: true + run: | + xvfb-run --auto-servernum npm run unit-with-coverage + xvfb-run --auto-servernum npm run assert-unit-coverage + + - name: Run unit tests on Firefox + uses: nick-invision/retry@v2 + env: + FIREFOX: true + MOZ_WEBRENDER: 0 + with: + max_attempts: 3 + timeout_minutes: 10 + command: xvfb-run --auto-servernum npm run funit + + - name: Run browser tests + run: | + npm run test-browser + + - name: Test bundling and installation + env: + CHROMIUM: true + run: | + # Note: this modifies package.json to test puppeteer-core. + npm run test-install + # Undo those changes. + git checkout --force + + macos: + # https://github.com/actions/virtual-environments#available-environments + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + # Test only the oldest maintenance LTS Node.js version. + # https://github.com/nodejs/Release#release-schedule + node-version: 14 + + - name: Install dependencies + run: | + # Test platform-specific browser binary fetching for both + # Chromium and Firefox. + PUPPETEER_PRODUCT=firefox npm install + npm install + ls .local-chromium .local-firefox + + - name: Build + run: | + npm run build + + - name: Run unit tests + env: + CHROMIUM: true + run: | + npm run unit + + - name: Run unit tests on Firefox + uses: nick-invision/retry@v2 + with: + max_attempts: 3 + timeout_minutes: 10 + command: npm run funit + + windows: + # https://github.com/actions/virtual-environments#available-environments + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + # Test only the oldest maintenance LTS Node.js version. + # https://github.com/nodejs/Release#release-schedule + node-version: 14 + + - name: Install dependencies + run: | + # Test platform-specific browser binary fetching for both + # Chromium and Firefox. + $env:PUPPETEER_PRODUCT='firefox' + npm install + Remove-Item Env:\PUPPETEER_PRODUCT + npm install + Get-ChildItem -Path .local-chromium,.local-firefox + + - name: Build + run: | + npm run build + + - name: Run unit tests + env: + CHROMIUM: true + run: | + npm run unit + + - name: Run unit tests on Firefox + uses: nick-invision/retry@v2 + continue-on-error: true + env: + FIREFOX: true + MOZ_WEBRENDER: 0 + with: + max_attempts: 3 + timeout_minutes: 10 + command: npm run funit + + linux-headful: + # https://github.com/actions/virtual-environments#available-environments + runs-on: ubuntu-latest + strategy: + matrix: + node: [16] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: | + sudo apt-get install xvfb + # Ensure both a Chromium and a Firefox binary are available. + PUPPETEER_PRODUCT=firefox npm install + npm install + ls .local-chromium .local-firefox + + - name: Build + run: | + npm run build + - name: Run unit tests in headful mode + uses: nick-invision/retry@v2 + continue-on-error: true + env: + CHROMIUM: true + HEADLESS: false + with: + max_attempts: 1 + command: xvfb-run --auto-servernum npm run unit + timeout_minutes: 10 + + chrome-headless: + runs-on: ${{ matrix.os }} + strategy: + matrix: + # https://github.com/actions/virtual-environments#available-environments + os: [ubuntu-latest, macos-latest, windows-latest] + node: [16] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: | + npm install + ls .local-chromium + - name: Build + run: | + npm run build + - name: Run unit tests + uses: nick-invision/retry@v2 + continue-on-error: true + env: + CHROMIUM: true + with: + max_attempts: 1 + command: npm run chrome-headless-unit + timeout_minutes: 30 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000000000..ae92e0911b75b --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,28 @@ +name: Pre-release + +on: + push: + branches: + - release-please-* + +jobs: + pre-release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install dependencies + run: npm install + - name: Build + run: | + node utils/generate_version_file.js + IS_PRE_RELEASE=1 npm run doc + - name: Commit and push + run: | + git config --global user.email "55107282+release-please[bot]@users.noreply.github.com" + git config --global user.name "release-please[bot]" + git add -A + git commit --amend --no-edit + git push -f diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000000..ead3564e267aa --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish + +on: + push: + tags: + - v* + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install dependencies + run: npm install + - name: Build puppeteer + run: npm run build + - name: Generate release documentation + run: IS_RELEASE=1 npm run doc + - name: Publish puppeteer + env: + NPM_TOKEN: ${{secrets.NPM_TOKEN_PUPPETEER}} + run: | + npm config set registry 'https://wombat-dressing-room.appspot.com/' + npm config set '//wombat-dressing-room.appspot.com/:_authToken' '${NPM_TOKEN}' + echo "Publishing puppeteer" + npm publish + # DEPRECATED_RANGE=$(node utils/get_deprecated_version_range.js) + # echo "Deprecating old puppeteer versions: $DEPRECATED_RANGE" + # npm deprecate puppeteer@$DEPRECATED_RANGE "Version no longer supported. Upgrade to @latest" + - name: Publish puppeteer-core + env: + NPM_TOKEN: ${{secrets.NPM_TOKEN_PUPPETEER_CORE}} + run: | + utils/prepare_puppeteer_core.js + npm config set registry 'https://wombat-dressing-room.appspot.com/' + npm config set '//wombat-dressing-room.appspot.com/:_authToken' '${NPM_TOKEN}' + npm publish diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml new file mode 100644 index 0000000000000..684fbe7cc5f95 --- /dev/null +++ b/.github/workflows/scorecards-analysis.yml @@ -0,0 +1,52 @@ +name: Scorecards supply-chain security + +on: + # Only the default branch is supported. + branch_protection_rule: + schedule: + - cron: '23 8 * * 6' + push: + branches: [main] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + + steps: + - name: 'Checkout code' + uses: actions/checkout@v3 # v2.4.0 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # v1.0.2 + with: + results_file: results.sarif + results_format: sarif + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # Upload the results as artifacts (optional). + - name: 'Upload artifact' + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v2.3.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub’s code scanning dashboard. + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@2f58583a1b24a7d3c7034f6bf9fa506d23b1183b # v1.0.26 + with: + sarif_file: results.sarif diff --git a/.github/workflows/tot-ci.yml b/.github/workflows/tot-ci.yml new file mode 100644 index 0000000000000..f467b32405863 --- /dev/null +++ b/.github/workflows/tot-ci.yml @@ -0,0 +1,75 @@ +name: ToT CI + +# Checks Puppeteer against the latest ToT build of Chromium. +# Declare default permissions as read only. +permissions: read-all + +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # Supposed to be every day at 8 am (UTC). + - cron: '0 8 * * *' + +jobs: + linux: + # https://github.com/actions/virtual-environments#available-environments + runs-on: ubuntu-latest + strategy: + matrix: + node: [16] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Set up Node.js + uses: actions/setup-node@v3.2.0 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies and build + run: | + sudo apt-get install xvfb + # Ensure both a Chromium and a Firefox binary are available. + PUPPETEER_PRODUCT=firefox npm install + npm install + ls .local-chromium .local-firefox + REV=$(node utils/check_availability.js -p linux) + echo "Installing revision $REV" + cat src/revisions.ts | sed "s/[0-9]\{6,\}/$REV/" > src/revisions.ts.replaced + mv src/revisions.ts.replaced src/revisions.ts + npm run build + npm install + + - name: Run unit tests in headless + uses: nick-invision/retry@v2 + env: + CHROMIUM: true + HEADLESS: true + with: + max_attempts: 3 + command: xvfb-run --auto-servernum npm run unit + timeout_minutes: 10 + + - name: Run unit tests in headful + uses: nick-invision/retry@v2 + continue-on-error: true + env: + CHROMIUM: true + HEADLESS: false + with: + max_attempts: 3 + command: xvfb-run --auto-servernum npm run unit + timeout_minutes: 10 + + - name: Run unit tests in chrome headless + uses: nick-invision/retry@v2 + continue-on-error: true + env: + CHROMIUM: true + with: + max_attempts: 3 + command: xvfb-run --auto-servernum npm run chrome-headless-unit + timeout_minutes: 10 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000..ae3dfc539b1e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +/node_modules/ +test-ts-types/**/node_modules +test-ts-types/**/dist/ +/test/output-chromium +/test/output-firefox +/test/test-user-data-dir* +/.local-chromium/ +/.local-firefox/ +/.dev_profile* +.DS_Store +*.swp +*.pyc +.vscode +package-lock.json +yarn.lock +/node6 +/utils/browser/puppeteer-web.js +/lib +test/coverage.json +temp/ +new-docs/ +puppeteer*.tgz +docs-api-json/ +docs-dist/ +website/docs +docs/api.html diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000000000..0bd658f49625b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" diff --git a/.husky/pre-comit b/.husky/pre-comit new file mode 100755 index 0000000000000..a8c5b0709784f --- /dev/null +++ b/.husky/pre-comit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run eslint diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000000000..71fac926e75e0 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run tsc && npm run eslint && npm run doc && npm run prettier && npm run ensure-pinned-deps diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000..94a06c2180166 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +access=public diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000..91dde1356f1fb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +node_modules/ +lib/ +third_party/ +vendor/ + +package-lock.json +yarn.lock +package.json +docs-api-json/ +docs-dist/ +website/ +experimental/ +CHANGELOG.md +test/assets/ +/.local-chromium/ +/.local-firefox/ +test-ts-types diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000000000..220aa850f53a1 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "14.2.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000..025ba0edc4236 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,576 @@ +# Changelog + +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. + +## [14.2.0](https://github.com/puppeteer/puppeteer/compare/v14.1.2...v14.2.0) (2022-05-31) + + +### Features + +* **chromium:** roll to Chromium 103.0.5059.0 (r1002410) ([#8410](https://github.com/puppeteer/puppeteer/issues/8410)) ([54efc2c](https://github.com/puppeteer/puppeteer/commit/54efc2c949be1d6ef22f4d2630620e33d14d2597)) + + +### Bug Fixes + +* multiple same request event listener ([#8404](https://github.com/puppeteer/puppeteer/issues/8404)) ([9211015](https://github.com/puppeteer/puppeteer/commit/92110151d9a33f26abc07bc805f4f2f3943697a0)) +* process documentation during publishing ([#8433](https://github.com/puppeteer/puppeteer/issues/8433)) ([d111d19](https://github.com/puppeteer/puppeteer/commit/d111d19f788d88d984dcf4ad7542f59acd2f4c1e)) + +## [14.1.2](https://github.com/puppeteer/puppeteer/compare/v14.1.1...v14.1.2) (2022-05-30) + + +### Bug Fixes + +* do not use loaderId for lifecycle events ([#8395](https://github.com/puppeteer/puppeteer/issues/8395)) ([c96c915](https://github.com/puppeteer/puppeteer/commit/c96c915b535dcf414038677bd3d3ed6b980a4901)) +* fix release-please bot ([#8400](https://github.com/puppeteer/puppeteer/issues/8400)) ([5c235c7](https://github.com/puppeteer/puppeteer/commit/5c235c701fc55380f09d09ac2cf63f2c94b60e3d)) +* use strict TS in Input.ts ([#8392](https://github.com/puppeteer/puppeteer/issues/8392)) ([af92a24](https://github.com/puppeteer/puppeteer/commit/af92a24ba9fc8efea1ba41f96d87515cf760da65)) + +### [14.1.1](https://github.com/puppeteer/puppeteer/compare/v14.1.0...v14.1.1) (2022-05-19) + + +### Bug Fixes + +* kill browser process when 'taskkill' fails on Windows ([#8352](https://github.com/puppeteer/puppeteer/issues/8352)) ([dccfadb](https://github.com/puppeteer/puppeteer/commit/dccfadb90e8947cae3f33d7a209b6f5752f97b46)) +* only check loading iframe in lifecycling ([#8348](https://github.com/puppeteer/puppeteer/issues/8348)) ([7438030](https://github.com/puppeteer/puppeteer/commit/74380303ac6cc6e2d84948a10920d56e665ccebe)) +* recompile before funit and unit commands ([#8363](https://github.com/puppeteer/puppeteer/issues/8363)) ([8735b78](https://github.com/puppeteer/puppeteer/commit/8735b784ba7838c1002b521a7f9f23bb27263d03)), closes [#8362](https://github.com/puppeteer/puppeteer/issues/8362) + +## [14.1.0](https://github.com/puppeteer/puppeteer/compare/v14.0.0...v14.1.0) (2022-05-13) + + +### Features + +* add waitForXPath to ElementHandle ([#8329](https://github.com/puppeteer/puppeteer/issues/8329)) ([7eaadaf](https://github.com/puppeteer/puppeteer/commit/7eaadafe197279a7d1753e7274d2e24dfc11abdf)) +* allow handling other targets as pages internally ([#8336](https://github.com/puppeteer/puppeteer/issues/8336)) ([3b66a2c](https://github.com/puppeteer/puppeteer/commit/3b66a2c47ee36785a6a72c9afedd768fab3d040a)) + + +### Bug Fixes + +* disable AvoidUnnecessaryBeforeUnloadCheckSync to fix navigations ([#8330](https://github.com/puppeteer/puppeteer/issues/8330)) ([4854ad5](https://github.com/puppeteer/puppeteer/commit/4854ad5b15c9bdf93c06dcb758393e7cbacd7469)) +* If currentNode and root are the same, do not include them in the result ([#8332](https://github.com/puppeteer/puppeteer/issues/8332)) ([a61144d](https://github.com/puppeteer/puppeteer/commit/a61144d43780b5c32197427d7682b9b6c433f2bb)) + +## [14.0.0](https://github.com/puppeteer/puppeteer/compare/v13.7.0...v14.0.0) (2022-05-09) + + +### ⚠ BREAKING CHANGES + +* strict mode fixes for HTTPRequest/Response classes (#8297) +* Node 12 is no longer supported. + +### Features + +* add support for Apple Silicon chromium builds ([#7546](https://github.com/puppeteer/puppeteer/issues/7546)) ([baa017d](https://github.com/puppeteer/puppeteer/commit/baa017db92b1fecf2e3584d5b3161371ae60f55b)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622) +* **chromium:** roll to Chromium 102.0.5002.0 (r991974) ([#8319](https://github.com/puppeteer/puppeteer/issues/8319)) ([be4c930](https://github.com/puppeteer/puppeteer/commit/be4c930c60164f681a966d0f8cb745f6c263fe2b)) +* support ES modules ([#8306](https://github.com/puppeteer/puppeteer/issues/8306)) ([6841bd6](https://github.com/puppeteer/puppeteer/commit/6841bd68d85e3b3952c5e7ce454ac4d23f84262d)) + + +### Bug Fixes + +* apparent typo SUPPORTER_PLATFORMS ([#8294](https://github.com/puppeteer/puppeteer/issues/8294)) ([e09287f](https://github.com/puppeteer/puppeteer/commit/e09287f4e9a1ff3c637dd165d65f221394970e2c)) +* make sure inner OOPIFs can be attached to ([#8304](https://github.com/puppeteer/puppeteer/issues/8304)) ([5539598](https://github.com/puppeteer/puppeteer/commit/553959884f4edb4deab760fa8ca38fc1c85c05c5)) +* strict mode fixes for HTTPRequest/Response classes ([#8297](https://github.com/puppeteer/puppeteer/issues/8297)) ([2804ae8](https://github.com/puppeteer/puppeteer/commit/2804ae8cdbc4c90bf942510bce656275a2d409e1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769) +* tests failing in headful ([#8273](https://github.com/puppeteer/puppeteer/issues/8273)) ([e841d7f](https://github.com/puppeteer/puppeteer/commit/e841d7f9f3f407c02dbc48e107b545b91db104e6)) + + +* drop Node 12 support ([#8299](https://github.com/puppeteer/puppeteer/issues/8299)) ([274bd6b](https://github.com/puppeteer/puppeteer/commit/274bd6b3b98c305ed014909d8053e4c54187971b)) + +## [13.7.0](https://github.com/puppeteer/puppeteer/compare/v13.6.0...v13.7.0) (2022-04-28) + + +### Features + +* add `back` and `forward` mouse buttons ([#8284](https://github.com/puppeteer/puppeteer/issues/8284)) ([7a51bff](https://github.com/puppeteer/puppeteer/commit/7a51bff47f6436fc29d0df7eb74f12f69102ca5b)) +* support chrome headless mode ([#8260](https://github.com/puppeteer/puppeteer/issues/8260)) ([1308d9a](https://github.com/puppeteer/puppeteer/commit/1308d9aa6a5920b20da02dca8db03c63e43c8b84)) + + +### Bug Fixes + +* doc typo ([#8263](https://github.com/puppeteer/puppeteer/issues/8263)) ([952a2ae](https://github.com/puppeteer/puppeteer/commit/952a2ae0bc4f059f8e8b4d1de809d0a486a74551)) +* use different test names for browser specific tests in launcher.spec.ts ([#8250](https://github.com/puppeteer/puppeteer/issues/8250)) ([c6cf1a9](https://github.com/puppeteer/puppeteer/commit/c6cf1a9f27621c8a619cfbdc9d0821541768ac94)) + +## [13.6.0](https://github.com/puppeteer/puppeteer/compare/v13.5.2...v13.6.0) (2022-04-19) + + +### Features + +* **chromium:** roll to Chromium 101.0.4950.0 (r982053) ([#8213](https://github.com/puppeteer/puppeteer/issues/8213)) ([ec74bd8](https://github.com/puppeteer/puppeteer/commit/ec74bd811d9b7fbaf600068e86f13a63d7b0bc6f)) +* respond multiple headers with same key ([#8183](https://github.com/puppeteer/puppeteer/issues/8183)) ([c1dcd85](https://github.com/puppeteer/puppeteer/commit/c1dcd857e3bc17769f02474a41bbedee01f471dc)) + + +### Bug Fixes + +* also kill Firefox when temporary profile is used ([#8233](https://github.com/puppeteer/puppeteer/issues/8233)) ([b6504d7](https://github.com/puppeteer/puppeteer/commit/b6504d7186336a2fc0b41c3878c843b7409ba5fb)) +* consider existing frames when waiting for a frame ([#8200](https://github.com/puppeteer/puppeteer/issues/8200)) ([0955225](https://github.com/puppeteer/puppeteer/commit/0955225b51421663288523a3dfb63103b51775b4)) +* disable bfcache in the launcher ([#8196](https://github.com/puppeteer/puppeteer/issues/8196)) ([9ac7318](https://github.com/puppeteer/puppeteer/commit/9ac7318506ac858b3465e9b4ede8ad75fbbcee11)), closes [#8182](https://github.com/puppeteer/puppeteer/issues/8182) +* enable page.spec event handler test for firefox ([#8214](https://github.com/puppeteer/puppeteer/issues/8214)) ([2b45027](https://github.com/puppeteer/puppeteer/commit/2b45027d256f85f21a0c824183696b237e00ad33)) +* forget queuedEventGroup when emitting response in responseReceivedExtraInfo ([#8234](https://github.com/puppeteer/puppeteer/issues/8234)) ([#8239](https://github.com/puppeteer/puppeteer/issues/8239)) ([91a8e73](https://github.com/puppeteer/puppeteer/commit/91a8e73b1196e4128b1e7c25e08080f2faaf3cf7)) +* forget request will be sent from the _requestWillBeSentMap list. ([#8226](https://github.com/puppeteer/puppeteer/issues/8226)) ([4b786c9](https://github.com/puppeteer/puppeteer/commit/4b786c904cbfe3f059322292f3b788b8a5ebd9bf)) +* ignore favicon requests in page.spec event handler tests ([#8208](https://github.com/puppeteer/puppeteer/issues/8208)) ([04e5c88](https://github.com/puppeteer/puppeteer/commit/04e5c889973432c6163a8539cdec23c0e8726bff)) +* **network.spec.ts:** typo in the word should ([#8223](https://github.com/puppeteer/puppeteer/issues/8223)) ([e93faad](https://github.com/puppeteer/puppeteer/commit/e93faadc21b7fcb1e03b69c451c28b769f9cde51)) + +### [13.5.2](https://github.com/puppeteer/puppeteer/compare/v13.5.1...v13.5.2) (2022-03-31) + + +### Bug Fixes + +* chromium downloading hung at 99% ([#8169](https://github.com/puppeteer/puppeteer/issues/8169)) ([8f13470](https://github.com/puppeteer/puppeteer/commit/8f13470af06045857f32496f03e77b14f3ecff98)) +* get extra headers from Fetch.requestPaused event ([#8162](https://github.com/puppeteer/puppeteer/issues/8162)) ([37ede68](https://github.com/puppeteer/puppeteer/commit/37ede6877017a8dc6c946a3dff4ec6d79c3ebc59)) + +### [13.5.1](https://github.com/puppeteer/puppeteer/compare/v13.5.0...v13.5.1) (2022-03-09) + + +### Bug Fixes + +* waitForNavigation in OOPIFs ([#8117](https://github.com/puppeteer/puppeteer/issues/8117)) ([34775e5](https://github.com/puppeteer/puppeteer/commit/34775e58316be49d8bc5a13209a1f570bc66b448)) + +## [13.5.0](https://github.com/puppeteer/puppeteer/compare/v13.4.1...v13.5.0) (2022-03-07) + + +### Features + +* **chromium:** roll to Chromium 100.0.4889.0 (r970485) ([#8108](https://github.com/puppeteer/puppeteer/issues/8108)) ([d12f427](https://github.com/puppeteer/puppeteer/commit/d12f42754f7013b5ec0a2198cf2d9cf945d3cb38)) + + +### Bug Fixes + +* Inherit browser-level proxy settings from incognito context ([#7770](https://github.com/puppeteer/puppeteer/issues/7770)) ([3feca32](https://github.com/puppeteer/puppeteer/commit/3feca325a9472ee36f7e866ebe375c7f083e0e36)) +* **page:** page.createIsolatedWorld error catching has been added ([#7848](https://github.com/puppeteer/puppeteer/issues/7848)) ([309e8b8](https://github.com/puppeteer/puppeteer/commit/309e8b80da0519327bc37b44a3ebb6f2e2d357a7)) +* **tests:** ensure all tests honour BINARY envvar ([#8092](https://github.com/puppeteer/puppeteer/issues/8092)) ([3b8b9ad](https://github.com/puppeteer/puppeteer/commit/3b8b9adde5d18892af96329b6f9303979f9c04f5)) + +### [13.4.1](https://github.com/puppeteer/puppeteer/compare/v13.4.0...v13.4.1) (2022-03-01) + + +### Bug Fixes + +* regression in --user-data-dir handling ([#8060](https://github.com/puppeteer/puppeteer/issues/8060)) ([85decdc](https://github.com/puppeteer/puppeteer/commit/85decdc28d7d2128e6d2946a72f4d99dd5dbb48a)) + +## [13.4.0](https://github.com/puppeteer/puppeteer/compare/v13.3.2...v13.4.0) (2022-02-22) + + +### Features + +* add support for async waitForTarget ([#7885](https://github.com/puppeteer/puppeteer/issues/7885)) ([dbf0639](https://github.com/puppeteer/puppeteer/commit/dbf0639822d0b2736993de52c0bfe1dbf4e58f25)) +* export `Frame._client` through getter ([#8041](https://github.com/puppeteer/puppeteer/issues/8041)) ([e9278fc](https://github.com/puppeteer/puppeteer/commit/e9278fcfcffe2558de63ce7542483445bcb6e74f)) +* **HTTPResponse:** expose timing information ([#8025](https://github.com/puppeteer/puppeteer/issues/8025)) ([30b3d49](https://github.com/puppeteer/puppeteer/commit/30b3d49b0de46d812b7485e708174a07c73dbdd0)) + + +### Bug Fixes + +* change kill to signal the whole process group to terminate ([#6859](https://github.com/puppeteer/puppeteer/issues/6859)) ([0eb9c78](https://github.com/puppeteer/puppeteer/commit/0eb9c7861717ebba7012c03e76b7a46063e4e5dd)) +* element screenshot issue in headful mode ([#8018](https://github.com/puppeteer/puppeteer/issues/8018)) ([5346e70](https://github.com/puppeteer/puppeteer/commit/5346e70ffc15b33c1949657cf1b465f1acc5d84d)), closes [#7999](https://github.com/puppeteer/puppeteer/issues/7999) +* ensure dom binding is not called after detach ([#8024](https://github.com/puppeteer/puppeteer/issues/8024)) ([5c308b0](https://github.com/puppeteer/puppeteer/commit/5c308b0704123736ddb085f97596c201ea18cf4a)), closes [#7814](https://github.com/puppeteer/puppeteer/issues/7814) +* use both __dirname and require.resolve to support different bundlers ([#8046](https://github.com/puppeteer/puppeteer/issues/8046)) ([e6a6295](https://github.com/puppeteer/puppeteer/commit/e6a6295d9a7480bb59ee58a2cc7785171fa0fa2c)), closes [#8044](https://github.com/puppeteer/puppeteer/issues/8044) + +### [13.3.2](https://github.com/puppeteer/puppeteer/compare/v13.3.1...v13.3.2) (2022-02-14) + + +### Bug Fixes + +* always use ENV executable path when present ([#7985](https://github.com/puppeteer/puppeteer/issues/7985)) ([6d6ea9b](https://github.com/puppeteer/puppeteer/commit/6d6ea9bf59daa3fb851b3da8baa27887e0aa2c28)) +* use require.resolve instead of __dirname ([#8003](https://github.com/puppeteer/puppeteer/issues/8003)) ([bbb186d](https://github.com/puppeteer/puppeteer/commit/bbb186d88cb99e4914299c983c822fa41a80f356)) + +### [13.3.1](https://github.com/puppeteer/puppeteer/compare/v13.3.0...v13.3.1) (2022-02-10) + + +### Bug Fixes + +* **puppeteer:** revert: esm modules ([#7986](https://github.com/puppeteer/puppeteer/issues/7986)) ([179eded](https://github.com/puppeteer/puppeteer/commit/179ededa1400c35c1f2edc015548e0f2a1bcee14)) + +## [13.3.0](https://github.com/puppeteer/puppeteer/compare/v13.2.0...v13.3.0) (2022-02-09) + + +### Features + +* **puppeteer:** export esm modules in package.json ([#7964](https://github.com/puppeteer/puppeteer/issues/7964)) ([523b487](https://github.com/puppeteer/puppeteer/commit/523b487e8802824cecff86d256b4f7dbc4c47c8a)) + +## [13.2.0](https://github.com/puppeteer/puppeteer/compare/v13.1.3...v13.2.0) (2022-02-07) + + +### Features + +* add more models to DeviceDescriptors ([#7904](https://github.com/puppeteer/puppeteer/issues/7904)) ([6a655cb](https://github.com/puppeteer/puppeteer/commit/6a655cb647e12eaf1055be0b298908d83bebac25)) +* **chromium:** roll to Chromium 99.0.4844.16 (r961656) ([#7960](https://github.com/puppeteer/puppeteer/issues/7960)) ([96c3f94](https://github.com/puppeteer/puppeteer/commit/96c3f943b2f6e26bd871ecfcce71b6a33e214ebf)) + + +### Bug Fixes + +* make projectRoot optional in Puppeteer and launchers ([#7967](https://github.com/puppeteer/puppeteer/issues/7967)) ([9afdc63](https://github.com/puppeteer/puppeteer/commit/9afdc6300b80f01091dc4cb42d4ebe952c7d60f0)) +* migrate more files to strict-mode TypeScript ([#7950](https://github.com/puppeteer/puppeteer/issues/7950)) ([aaac8d9](https://github.com/puppeteer/puppeteer/commit/aaac8d9c44327a2c503ffd6c97b7f21e8010c3e4)) +* typos in documentation ([#7968](https://github.com/puppeteer/puppeteer/issues/7968)) ([41ab4e9](https://github.com/puppeteer/puppeteer/commit/41ab4e9127df64baa6c43ecde2f7ddd702ba7b0c)) + +### [13.1.3](https://github.com/puppeteer/puppeteer/compare/v13.1.2...v13.1.3) (2022-01-31) + + +### Bug Fixes + +* issue with reading versions.js in doclint ([#7940](https://github.com/puppeteer/puppeteer/issues/7940)) ([06ba963](https://github.com/puppeteer/puppeteer/commit/06ba9632a4c63859244068d32c312817d90daf63)) +* make more files work in strict-mode TypeScript ([#7936](https://github.com/puppeteer/puppeteer/issues/7936)) ([0636513](https://github.com/puppeteer/puppeteer/commit/0636513e34046f4d40b5e88beb2b18b16dab80aa)) +* page.pdf producing an invalid pdf ([#7868](https://github.com/puppeteer/puppeteer/issues/7868)) ([afea509](https://github.com/puppeteer/puppeteer/commit/afea509544fb99bfffe5b0bebe6f3575c53802f0)), closes [#7757](https://github.com/puppeteer/puppeteer/issues/7757) + +### [13.1.2](https://github.com/puppeteer/puppeteer/compare/v13.1.1...v13.1.2) (2022-01-25) + + +### Bug Fixes + +* **package.json:** update node-fetch package ([#7924](https://github.com/puppeteer/puppeteer/issues/7924)) ([e4c48d3](https://github.com/puppeteer/puppeteer/commit/e4c48d3b8c2a812752094ed8163e4f2f32c4b6cb)) +* types in Browser.ts to be compatible with strict mode Typescript ([#7918](https://github.com/puppeteer/puppeteer/issues/7918)) ([a8ec0aa](https://github.com/puppeteer/puppeteer/commit/a8ec0aadc9c90d224d568d9e418d14261e6e85b1)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769) +* types in Connection.ts to be compatible with strict mode Typescript ([#7919](https://github.com/puppeteer/puppeteer/issues/7919)) ([d80d602](https://github.com/puppeteer/puppeteer/commit/d80d6027ea8e1b7fcdaf045398629cf8e6512658)), closes [#6769](https://github.com/puppeteer/puppeteer/issues/6769) + +### [13.1.1](https://github.com/puppeteer/puppeteer/compare/v13.1.0...v13.1.1) (2022-01-18) + + +### Bug Fixes + +* use content box for OOPIF offset calculations ([#7911](https://github.com/puppeteer/puppeteer/issues/7911)) ([344feb5](https://github.com/puppeteer/puppeteer/commit/344feb53c28ce018a4c600d408468f6d9d741eee)) + +## [13.1.0](https://github.com/puppeteer/puppeteer/compare/v13.0.1...v13.1.0) (2022-01-17) + + +### Features + +* **chromium:** roll to Chromium 98.0.4758.0 (r950341) ([#7907](https://github.com/puppeteer/puppeteer/issues/7907)) ([a55c86f](https://github.com/puppeteer/puppeteer/commit/a55c86fac504b5e89ba23735fb3a1b1d54a4e1e5)) + + +### Bug Fixes + +* apply OOPIF offsets to bounding box and box model calls ([#7906](https://github.com/puppeteer/puppeteer/issues/7906)) ([a566263](https://github.com/puppeteer/puppeteer/commit/a566263ba28e58ff648bffbdb628606f75d5876f)) +* correctly compute clickable points for elements inside OOPIFs ([#7900](https://github.com/puppeteer/puppeteer/issues/7900)) ([486bbe0](https://github.com/puppeteer/puppeteer/commit/486bbe010d5ee5c446d9e8daf61a080232379c3f)), closes [#7849](https://github.com/puppeteer/puppeteer/issues/7849) +* error for pre-existing OOPIFs ([#7899](https://github.com/puppeteer/puppeteer/issues/7899)) ([d7937b8](https://github.com/puppeteer/puppeteer/commit/d7937b806d331bf16c2016aaf16e932b1334eac8)), closes [#7844](https://github.com/puppeteer/puppeteer/issues/7844) [#7896](https://github.com/puppeteer/puppeteer/issues/7896) + +### [13.0.1](https://github.com/puppeteer/puppeteer/compare/v13.0.0...v13.0.1) (2021-12-22) + + +### Bug Fixes + +* disable a test failing on Firefox ([#7846](https://github.com/puppeteer/puppeteer/issues/7846)) ([36207c5](https://github.com/puppeteer/puppeteer/commit/36207c5efe8ca21f4b3fc5b00212700326a701d2)) +* make sure ElementHandle.waitForSelector is evaluated in the right context ([#7843](https://github.com/puppeteer/puppeteer/issues/7843)) ([8d8e874](https://github.com/puppeteer/puppeteer/commit/8d8e874b072b17fc763f33d08e51c046b7435244)) +* predicate arguments for waitForFunction ([#7845](https://github.com/puppeteer/puppeteer/issues/7845)) ([1c44551](https://github.com/puppeteer/puppeteer/commit/1c44551f1b5bb19455b4a1eb7061715717ec880e)), closes [#7836](https://github.com/puppeteer/puppeteer/issues/7836) + +## [13.0.0](https://github.com/puppeteer/puppeteer/compare/v12.0.1...v13.0.0) (2021-12-10) + + +### ⚠ BREAKING CHANGES + +* typo in 'already-handled' constant of the request interception API (#7813) + +### Features + +* expose HTTPRequest intercept resolution state and clarify docs ([#7796](https://github.com/puppeteer/puppeteer/issues/7796)) ([dc23b75](https://github.com/puppeteer/puppeteer/commit/dc23b7535cb958c00d1eecfe85b4ee26e52e2e39)) +* implement Element.waitForSelector ([#7825](https://github.com/puppeteer/puppeteer/issues/7825)) ([c034294](https://github.com/puppeteer/puppeteer/commit/c03429444d05b39549489ad3da67d93b2be59f51)) + + +### Bug Fixes + +* handle multiple/duplicate Fetch.requestPaused events ([#7802](https://github.com/puppeteer/puppeteer/issues/7802)) ([636b086](https://github.com/puppeteer/puppeteer/commit/636b0863a169da132e333eb53b17eb2601daabe6)), closes [#7475](https://github.com/puppeteer/puppeteer/issues/7475) [#6696](https://github.com/puppeteer/puppeteer/issues/6696) [#7225](https://github.com/puppeteer/puppeteer/issues/7225) +* revert "feat(typescript): allow using puppeteer without dom lib" ([02c9af6](https://github.com/puppeteer/puppeteer/commit/02c9af62d64060a83f53368640f343ae2e30e38a)), closes [#6998](https://github.com/puppeteer/puppeteer/issues/6998) +* typo in 'already-handled' constant of the request interception API ([#7813](https://github.com/puppeteer/puppeteer/issues/7813)) ([8242422](https://github.com/puppeteer/puppeteer/commit/824242246de9e158aacb85f71350a79cb386ed92)), closes [#7745](https://github.com/puppeteer/puppeteer/issues/7745) [#7747](https://github.com/puppeteer/puppeteer/issues/7747) [#7780](https://github.com/puppeteer/puppeteer/issues/7780) + +### [12.0.1](https://github.com/puppeteer/puppeteer/compare/v12.0.0...v12.0.1) (2021-11-29) + + +### Bug Fixes + +* handle extraInfo events even if event.hasExtraInfo === false ([#7808](https://github.com/puppeteer/puppeteer/issues/7808)) ([6ee2feb](https://github.com/puppeteer/puppeteer/commit/6ee2feb1eafdd399f0af50cdc4517f21bcb55121)), closes [#7805](https://github.com/puppeteer/puppeteer/issues/7805) + +## [12.0.0](https://github.com/puppeteer/puppeteer/compare/v11.0.0...v12.0.0) (2021-11-26) + + +### ⚠ BREAKING CHANGES + +* **chromium:** roll to Chromium 97.0.4692.0 (r938248) + +### Features + +* **chromium:** roll to Chromium 97.0.4692.0 (r938248) ([ac162c5](https://github.com/puppeteer/puppeteer/commit/ac162c561ee43dd69eff38e1b354a41bb42c9eba)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458) +* support for custom user data (profile) directory for Firefox ([#7684](https://github.com/puppeteer/puppeteer/issues/7684)) ([790c7a0](https://github.com/puppeteer/puppeteer/commit/790c7a0eb92291efebaa37e80c72f5cb5f46bbdb)) + + +### Bug Fixes + +* **ariaqueryhandler:** allow single quotes in aria attribute selector ([#7750](https://github.com/puppeteer/puppeteer/issues/7750)) ([b0319ec](https://github.com/puppeteer/puppeteer/commit/b0319ecc89f8ea3d31ab9aee5e1cd33d2a4e62be)), closes [#7721](https://github.com/puppeteer/puppeteer/issues/7721) +* clearer jsdoc for behavior of `headless` when `devtools` is true ([#7748](https://github.com/puppeteer/puppeteer/issues/7748)) ([9f9b4ed](https://github.com/puppeteer/puppeteer/commit/9f9b4ed72ab0bb43d002a0024122d6f5eab231aa)) +* null check for frame in FrameManager ([#7773](https://github.com/puppeteer/puppeteer/issues/7773)) ([23ee295](https://github.com/puppeteer/puppeteer/commit/23ee295f348d114617f2a86d0bb792936f413ac5)), closes [#7749](https://github.com/puppeteer/puppeteer/issues/7749) +* only kill the process when there is no browser instance available ([#7762](https://github.com/puppeteer/puppeteer/issues/7762)) ([51e6169](https://github.com/puppeteer/puppeteer/commit/51e61696c1c20cc09bd4fc068ae1dfa259c41745)), closes [#7668](https://github.com/puppeteer/puppeteer/issues/7668) +* parse statusText from the extraInfo event ([#7798](https://github.com/puppeteer/puppeteer/issues/7798)) ([a26b12b](https://github.com/puppeteer/puppeteer/commit/a26b12b7c775c36271cd4c98e39bbd59f4356320)), closes [#7458](https://github.com/puppeteer/puppeteer/issues/7458) +* try to remove the temporary user data directory after the process has been killed ([#7761](https://github.com/puppeteer/puppeteer/issues/7761)) ([fc94a28](https://github.com/puppeteer/puppeteer/commit/fc94a28778cfdb3cb8bcd882af3ebcdacf85c94e)) + +## [11.0.0](https://github.com/puppeteer/puppeteer/compare/v10.4.0...v11.0.0) (2021-11-02) + + +### ⚠ BREAKING CHANGES + +* **oop iframes:** integrate OOP iframes with the frame manager (#7556) + +### Features + +* improve error message for response.buffer() ([#7669](https://github.com/puppeteer/puppeteer/issues/7669)) ([03c9ecc](https://github.com/puppeteer/puppeteer/commit/03c9ecca400a02684cd60229550dbad1190a5b6e)) +* **oop iframes:** integrate OOP iframes with the frame manager ([#7556](https://github.com/puppeteer/puppeteer/issues/7556)) ([4d9dc8c](https://github.com/puppeteer/puppeteer/commit/4d9dc8c0e613f22d4cdf237e8bd0b0da3c588edb)), closes [#2548](https://github.com/puppeteer/puppeteer/issues/2548) +* add custom debugging port option ([#4993](https://github.com/puppeteer/puppeteer/issues/4993)) ([26145e9](https://github.com/puppeteer/puppeteer/commit/26145e9a24af7caed6ece61031f2cafa6abd505f)) +* add initiator to HTTPRequest ([#7614](https://github.com/puppeteer/puppeteer/issues/7614)) ([a271145](https://github.com/puppeteer/puppeteer/commit/a271145b0663ef9de1903dd0eb9fd5366465bed7)) +* allow to customize tmpdir ([#7243](https://github.com/puppeteer/puppeteer/issues/7243)) ([b1f6e86](https://github.com/puppeteer/puppeteer/commit/b1f6e8692b0bc7e8551b2a78169c830cd80a7acb)) +* handle unhandled promise rejections in tests ([#7722](https://github.com/puppeteer/puppeteer/issues/7722)) ([07febca](https://github.com/puppeteer/puppeteer/commit/07febca04b391893cfc872250e4391da142d4fe2)) + + +### Bug Fixes + +* add support for relative install paths to BrowserFetcher ([#7613](https://github.com/puppeteer/puppeteer/issues/7613)) ([eebf452](https://github.com/puppeteer/puppeteer/commit/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899)), closes [#7592](https://github.com/puppeteer/puppeteer/issues/7592) +* add webp to screenshot quality option allow list ([#7631](https://github.com/puppeteer/puppeteer/issues/7631)) ([b20c2bf](https://github.com/puppeteer/puppeteer/commit/b20c2bfa24cbdd4a1b9cefca2e0a9407e442baf5)) +* prevent Target closed errors on streams ([#7728](https://github.com/puppeteer/puppeteer/issues/7728)) ([5b792de](https://github.com/puppeteer/puppeteer/commit/5b792de7a97611441777d1ac99cb95516301d7dc)) +* request an animation frame to fix flaky clickablePoint test ([#7587](https://github.com/puppeteer/puppeteer/issues/7587)) ([7341d9f](https://github.com/puppeteer/puppeteer/commit/7341d9fadd1466a5b2f2bde8631f3b02cf9a7d8a)) +* setup husky properly ([#7727](https://github.com/puppeteer/puppeteer/issues/7727)) ([8b712e7](https://github.com/puppeteer/puppeteer/commit/8b712e7b642b58193437f26d4e104a9e412f388d)), closes [#7726](https://github.com/puppeteer/puppeteer/issues/7726) +* updated troubleshooting.md to meet latest dependencies changes ([#7656](https://github.com/puppeteer/puppeteer/issues/7656)) ([edb0197](https://github.com/puppeteer/puppeteer/commit/edb01972b9606d8b05b979a588eda0d622315981)) +* **launcher:** launcher.launch() should pass 'timeout' option [#5180](https://github.com/puppeteer/puppeteer/issues/5180) ([#7596](https://github.com/puppeteer/puppeteer/issues/7596)) ([113489d](https://github.com/puppeteer/puppeteer/commit/113489d3b58e2907374a4e6e5133bf46630695d1)) +* **page:** fallback to default in exposeFunction when using imported module ([#6365](https://github.com/puppeteer/puppeteer/issues/6365)) ([44c9ec6](https://github.com/puppeteer/puppeteer/commit/44c9ec67c57dccf3e186c86f14f3a8da9a8eb971)) +* **page:** fix page.off method for request event ([#7624](https://github.com/puppeteer/puppeteer/issues/7624)) ([d0cb943](https://github.com/puppeteer/puppeteer/commit/d0cb9436a302418086f6763e0e58ae3732a20b62)), closes [#7572](https://github.com/puppeteer/puppeteer/issues/7572) + +## [10.4.0](https://github.com/puppeteer/puppeteer/compare/v10.2.0...v10.4.0) (2021-09-21) + + +### Features + +* add webp to screenshot options ([#7565](https://github.com/puppeteer/puppeteer/issues/7565)) ([43a9268](https://github.com/puppeteer/puppeteer/commit/43a926832505a57922016907a264165676424557)) +* **page:** expose page.client() ([#7582](https://github.com/puppeteer/puppeteer/issues/7582)) ([99ca842](https://github.com/puppeteer/puppeteer/commit/99ca842124a1edef5e66426621885141a9feaca5)) +* **page:** mark page.client() as internal ([#7585](https://github.com/puppeteer/puppeteer/issues/7585)) ([8451951](https://github.com/puppeteer/puppeteer/commit/84519514831f304f9076ca235fe474f797616b2c)) +* add ability to specify offsets for JSHandle.click ([#7573](https://github.com/puppeteer/puppeteer/issues/7573)) ([2b5c001](https://github.com/puppeteer/puppeteer/commit/2b5c0019dc3744196c5858edeaa901dff9973ef5)) +* add durableStorage to allowed permissions ([#5295](https://github.com/puppeteer/puppeteer/issues/5295)) ([eda5171](https://github.com/puppeteer/puppeteer/commit/eda51712790b9260626dc53cfb58a72805c45582)) +* add id option to addScriptTag ([#5477](https://github.com/puppeteer/puppeteer/issues/5477)) ([300be5d](https://github.com/puppeteer/puppeteer/commit/300be5d167b6e7e532e725fdb86966081a5d0093)) +* add more Android models to DeviceDescriptors ([#7210](https://github.com/puppeteer/puppeteer/issues/7210)) ([b5020dc](https://github.com/puppeteer/puppeteer/commit/b5020dc04121b265c77662237dfb177d6de06053)), closes [/github.com/aerokube/moon-deploy/blob/master/moon-local.yaml#L199](https://github.com/puppeteer//github.com/aerokube/moon-deploy/blob/master/moon-local.yaml/issues/L199) +* add proxy and bypass list parameters to createIncognitoBrowserContext ([#7516](https://github.com/puppeteer/puppeteer/issues/7516)) ([8e45a1c](https://github.com/puppeteer/puppeteer/commit/8e45a1c882207cc36e87be2a917b661eb841c4bf)), closes [#678](https://github.com/puppeteer/puppeteer/issues/678) +* add threshold to Page.isIntersectingViewport ([#6497](https://github.com/puppeteer/puppeteer/issues/6497)) ([54c4318](https://github.com/puppeteer/puppeteer/commit/54c43180161c3c512e4698e7f2e85ce3c6f0ab50)) +* add unit test support for bisect ([#7553](https://github.com/puppeteer/puppeteer/issues/7553)) ([a0b1f6b](https://github.com/puppeteer/puppeteer/commit/a0b1f6b401abae2fbc5a8987061644adfaa7b482)) +* add User-Agent with Puppeteer version to WebSocket request ([#5614](https://github.com/puppeteer/puppeteer/issues/5614)) ([6a2bf0a](https://github.com/puppeteer/puppeteer/commit/6a2bf0aabaa4df72c7838f5a6cd742e8f9c72be6)) +* extend husky checks ([#7574](https://github.com/puppeteer/puppeteer/issues/7574)) ([7316086](https://github.com/puppeteer/puppeteer/commit/73160869417275200be19bd37372b6218dbc5f63)) +* **api:** implement `Page.waitForNetworkIdle()` ([#5140](https://github.com/puppeteer/puppeteer/issues/5140)) ([3c6029c](https://github.com/puppeteer/puppeteer/commit/3c6029c702291ca7ef637b66e78d72e03156fe58)) +* **coverage:** option for raw V8 script coverage ([#6454](https://github.com/puppeteer/puppeteer/issues/6454)) ([cb4470a](https://github.com/puppeteer/puppeteer/commit/cb4470a6d9b0a7f73836458bb3d5779eb85ac5f2)) +* support timeout for page.pdf() call ([#7508](https://github.com/puppeteer/puppeteer/issues/7508)) ([f90af66](https://github.com/puppeteer/puppeteer/commit/f90af6639d801e764bdb479b9543b7f8f2b926df)) +* **typescript:** allow using puppeteer without dom lib ([#6998](https://github.com/puppeteer/puppeteer/issues/6998)) ([723052d](https://github.com/puppeteer/puppeteer/commit/723052d5bb3c3d1d3908508467512bea4d8fdc80)), closes [#6989](https://github.com/puppeteer/puppeteer/issues/6989) + + +### Bug Fixes + +* **docs:** deploy includes website documentation ([#7469](https://github.com/puppeteer/puppeteer/issues/7469)) ([6fde41c](https://github.com/puppeteer/puppeteer/commit/6fde41c6b6657986df1bbce3f2e0f7aa499f2be4)) +* **docs:** names in version 9.1.1 ([#7517](https://github.com/puppeteer/puppeteer/issues/7517)) ([44b22bb](https://github.com/puppeteer/puppeteer/commit/44b22bbc2629e3c75c1494b299a66790b371fb0a)) +* **frame:** fix Frame.waitFor's XPath pattern detection ([#5184](https://github.com/puppeteer/puppeteer/issues/5184)) ([caa2b73](https://github.com/puppeteer/puppeteer/commit/caa2b732fe58f32ec03f2a9fa8568f20188203c5)) +* **install:** respect environment proxy config when downloading Firef… ([#6577](https://github.com/puppeteer/puppeteer/issues/6577)) ([9399c97](https://github.com/puppeteer/puppeteer/commit/9399c9786fba4e45e1c5485ddbb197d2d4f1735f)), closes [#6573](https://github.com/puppeteer/puppeteer/issues/6573) +* added names in V9.1.1 ([#7547](https://github.com/puppeteer/puppeteer/issues/7547)) ([d132b8b](https://github.com/puppeteer/puppeteer/commit/d132b8b041696e6d5b9a99d0be1acf1cf943efef)) +* **test:** tweak waitForNetworkIdle delay in test between downloads ([#7564](https://github.com/puppeteer/puppeteer/issues/7564)) ([a21b737](https://github.com/puppeteer/puppeteer/commit/a21b7376e7feaf23066d67948d52480516f42496)) +* **types:** allow evaluate functions to take a readonly array as an argument ([#7072](https://github.com/puppeteer/puppeteer/issues/7072)) ([491614c](https://github.com/puppeteer/puppeteer/commit/491614c7f8cfa50b902d0275064e611c2a48c3b2)) +* update firefox prefs documentation link ([#7539](https://github.com/puppeteer/puppeteer/issues/7539)) ([2aec355](https://github.com/puppeteer/puppeteer/commit/2aec35553bc6e0305f40837bb3665ddbd02aa889)) +* use non-deprecated tracing categories api ([#7413](https://github.com/puppeteer/puppeteer/issues/7413)) ([040a0e5](https://github.com/puppeteer/puppeteer/commit/040a0e561b4f623f7929130b90be129f94ebb642)) + +## [10.2.0](https://github.com/puppeteer/puppeteer/compare/v10.1.0...v10.2.0) (2021-08-04) + + +### Features + +* **api:** make `page.isDragInterceptionEnabled` a method ([#7419](https://github.com/puppeteer/puppeteer/issues/7419)) ([dd470c7](https://github.com/puppeteer/puppeteer/commit/dd470c7a226a8422a938a7b0fffa58ffc6b78512)), closes [#7150](https://github.com/puppeteer/puppeteer/issues/7150) +* **chromium:** roll to Chromium 93.0.4577.0 (r901912) ([#7387](https://github.com/puppeteer/puppeteer/issues/7387)) ([e10faad](https://github.com/puppeteer/puppeteer/commit/e10faad4f239b1120491bb54fcba0216acd3a646)) +* add channel parameter for puppeteer.launch ([#7389](https://github.com/puppeteer/puppeteer/issues/7389)) ([d70f60e](https://github.com/puppeteer/puppeteer/commit/d70f60e0619b8659d191fa492e3db4bc221ae982)) +* add cooperative request intercepts ([#6735](https://github.com/puppeteer/puppeteer/issues/6735)) ([b5e6474](https://github.com/puppeteer/puppeteer/commit/b5e6474374ae6a88fc73cdb1a9906764c2ac5d70)) +* add support for useragentdata ([#7378](https://github.com/puppeteer/puppeteer/issues/7378)) ([7200b1a](https://github.com/puppeteer/puppeteer/commit/7200b1a6fb9dfdfb65d50f0000339333e71b1b2a)) + + +### Bug Fixes + +* **browser-runner:** reject promise on error ([#7338](https://github.com/puppeteer/puppeteer/issues/7338)) ([5eb20e2](https://github.com/puppeteer/puppeteer/commit/5eb20e29a21ea0e0368fa8937ef38f7c7693ab34)) +* add script to remove html comments from docs markdown ([#7394](https://github.com/puppeteer/puppeteer/issues/7394)) ([ea3df80](https://github.com/puppeteer/puppeteer/commit/ea3df80ed136a03d7698d2319106af5df8d48b58)) + +## [10.1.0](https://github.com/puppeteer/puppeteer/compare/v10.0.0...v10.1.0) (2021-06-29) + + +### Features + +* add a streaming version for page.pdf ([e3699e2](https://github.com/puppeteer/puppeteer/commit/e3699e248bc9c1f7a6ead9a07d68ae8b65905443)) +* add drag-and-drop support ([#7150](https://github.com/puppeteer/puppeteer/issues/7150)) ([a91b8ac](https://github.com/puppeteer/puppeteer/commit/a91b8aca3728b2c2e310e9446897d729bf983377)) +* add page.emulateCPUThrottling ([#7343](https://github.com/puppeteer/puppeteer/issues/7343)) ([4ce4110](https://github.com/puppeteer/puppeteer/commit/4ce41106288938b9d366c550e7a424812920683d)) + + +### Bug Fixes + +* remove redundant await while fetching target ([#7351](https://github.com/puppeteer/puppeteer/issues/7351)) ([083b297](https://github.com/puppeteer/puppeteer/commit/083b297a6741c6b1dd23867f441130655fac8f7d)) + +## [10.0.0](https://github.com/puppeteer/puppeteer/compare/v9.1.1...v10.0.0) (2021-05-31) + + +### ⚠ BREAKING CHANGES + +* Node.js 10 is no longer supported. + +### Features + +* **chromium:** roll to Chromium 92.0.4512.0 (r884014) ([#7288](https://github.com/puppeteer/puppeteer/issues/7288)) ([f863f4b](https://github.com/puppeteer/puppeteer/commit/f863f4bfe015e57ea1f9fbb322f1cedee468b857)) +* **requestinterception:** remove cacheSafe flag ([#7217](https://github.com/puppeteer/puppeteer/issues/7217)) ([d01aa6c](https://github.com/puppeteer/puppeteer/commit/d01aa6c84a1e41f15ffed3a8d36ad26a404a7187)) +* expose other sessions from connection ([#6863](https://github.com/puppeteer/puppeteer/issues/6863)) ([cb285a2](https://github.com/puppeteer/puppeteer/commit/cb285a237921259eac99ade1d8b5550e068a55eb)) +* **launcher:** add new launcher option `waitForInitialPage` ([#7105](https://github.com/puppeteer/puppeteer/issues/7105)) ([2605309](https://github.com/puppeteer/puppeteer/commit/2605309f74b43da160cda4d214016e4422bf7676)), closes [#3630](https://github.com/puppeteer/puppeteer/issues/3630) + + +### Bug Fixes + +* added comments for browsercontext, startCSSCoverage, and startJSCoverage. ([#7264](https://github.com/puppeteer/puppeteer/issues/7264)) ([b750397](https://github.com/puppeteer/puppeteer/commit/b75039746ac6bddf1411538242b5e70b0f2e6e8a)) +* modified comment for method product, platform and newPage ([#7262](https://github.com/puppeteer/puppeteer/issues/7262)) ([159d283](https://github.com/puppeteer/puppeteer/commit/159d2835450697dabea6f9adf6e67d158b5b8ae3)) +* **requestinterception:** fix font loading issue ([#7060](https://github.com/puppeteer/puppeteer/issues/7060)) ([c9978d2](https://github.com/puppeteer/puppeteer/commit/c9978d20d5584c9fd2dc902e4b4ac86ed8ea5d6e)), closes [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-811546501](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-811546501) [/github.com/puppeteer/puppeteer/pull/6996#issuecomment-813797393](https://github.com/puppeteer//github.com/puppeteer/puppeteer/pull/6996/issues/issuecomment-813797393) [#7038](https://github.com/puppeteer/puppeteer/issues/7038) + + +* drop support for Node.js 10 ([#7200](https://github.com/puppeteer/puppeteer/issues/7200)) ([97c9fe2](https://github.com/puppeteer/puppeteer/commit/97c9fe2520723d45a5a86da06b888ae888d400be)), closes [#6753](https://github.com/puppeteer/puppeteer/issues/6753) + +### [9.1.1](https://github.com/puppeteer/puppeteer/compare/v9.1.0...v9.1.1) (2021-05-05) + + +### Bug Fixes + +* make targetFilter synchronous ([#7203](https://github.com/puppeteer/puppeteer/issues/7203)) ([bcc85a0](https://github.com/puppeteer/puppeteer/commit/bcc85a0969077d122e5d8d2fb5c1061999a8ae48)) + +## [9.1.0](https://github.com/puppeteer/puppeteer/compare/v9.0.0...v9.1.0) (2021-05-03) + + +### Features + +* add option to filter targets ([#7192](https://github.com/puppeteer/puppeteer/issues/7192)) ([ec3fc2e](https://github.com/puppeteer/puppeteer/commit/ec3fc2e035bb5ca14a576180fff612e1ecf6bad7)) + + +### Bug Fixes + +* change rm -rf to rimraf ([#7168](https://github.com/puppeteer/puppeteer/issues/7168)) ([ad6b736](https://github.com/puppeteer/puppeteer/commit/ad6b736039436fcc5c0a262e5b575aa041427be3)) + +## [9.0.0](https://github.com/puppeteer/puppeteer/compare/v8.0.0...v9.0.0) (2021-04-21) + + +### ⚠ BREAKING CHANGES + +* **filechooser:** FileChooser.cancel() is now synchronous. + +### Features + +* **chromium:** roll to Chromium 91.0.4469.0 (r869685) ([#7110](https://github.com/puppeteer/puppeteer/issues/7110)) ([715e7a8](https://github.com/puppeteer/puppeteer/commit/715e7a8d62901d1c7ec602425c2fce8d8148b742)) +* **launcher:** fix installation error on Apple M1 chips ([#7099](https://github.com/puppeteer/puppeteer/issues/7099)) ([c239d9e](https://github.com/puppeteer/puppeteer/commit/c239d9edc72d85697b4875c98fff3ec592848082)), closes [#6622](https://github.com/puppeteer/puppeteer/issues/6622) +* **network:** request interception and caching compatibility ([#6996](https://github.com/puppeteer/puppeteer/issues/6996)) ([8695759](https://github.com/puppeteer/puppeteer/commit/8695759a223bc1bd31baecb00dc28721216e4c6f)) +* **page:** emit the event after removing the Worker ([#7080](https://github.com/puppeteer/puppeteer/issues/7080)) ([e34a6d5](https://github.com/puppeteer/puppeteer/commit/e34a6d53183c3e1f63a375ba6a26bee0dcfcf542)) +* **types:** improve type of predicate function ([#6997](https://github.com/puppeteer/puppeteer/issues/6997)) ([943477c](https://github.com/puppeteer/puppeteer/commit/943477cc1eb4b129870142873b3554737d5ef252)), closes [/github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts#L1883-L1885](https://github.com/puppeteer//github.com/DefinitelyTyped/DefinitelyTyped/blob/c43191a8f7a7d2a47bbff0bc3a7d95ecc64d2269/types/puppeteer/index.d.ts/issues/L1883-L1885) +* accept captureBeyondViewport as optional screenshot param ([#7063](https://github.com/puppeteer/puppeteer/issues/7063)) ([0e092d2](https://github.com/puppeteer/puppeteer/commit/0e092d2ea0ec18ad7f07ad3507deb80f96086e7a)) +* **page:** add omitBackground option for page.pdf method ([#6981](https://github.com/puppeteer/puppeteer/issues/6981)) ([dc8ab6d](https://github.com/puppeteer/puppeteer/commit/dc8ab6d8ca1661f8e56d329e6d9c49c891e8b975)) + + +### Bug Fixes + +* **aria:** fix parsing of ARIA selectors ([#7037](https://github.com/puppeteer/puppeteer/issues/7037)) ([4426135](https://github.com/puppeteer/puppeteer/commit/4426135692ae3ee7ed2841569dd9375e7ca8286c)) +* **page:** fix mouse.click method ([#7097](https://github.com/puppeteer/puppeteer/issues/7097)) ([ba7c367](https://github.com/puppeteer/puppeteer/commit/ba7c367de33ace7753fd9d8b8cc894b2c14ab6c2)), closes [#6462](https://github.com/puppeteer/puppeteer/issues/6462) [#3347](https://github.com/puppeteer/puppeteer/issues/3347) +* make `$` and `$$` selectors generic ([#6883](https://github.com/puppeteer/puppeteer/issues/6883)) ([b349c91](https://github.com/puppeteer/puppeteer/commit/b349c91e7df76630b7411d6645e649945c4609bd)) +* type page event listeners correctly ([#6891](https://github.com/puppeteer/puppeteer/issues/6891)) ([866d34e](https://github.com/puppeteer/puppeteer/commit/866d34ee1122e89eab00743246676845bb065968)) +* **typescript:** allow defaultViewport to be 'null' ([#6942](https://github.com/puppeteer/puppeteer/issues/6942)) ([e31e68d](https://github.com/puppeteer/puppeteer/commit/e31e68dfa12dd50482b700472bc98876b9031829)), closes [#6885](https://github.com/puppeteer/puppeteer/issues/6885) +* make screenshots work in puppeteer-web ([#6936](https://github.com/puppeteer/puppeteer/issues/6936)) ([5f24f60](https://github.com/puppeteer/puppeteer/commit/5f24f608194fd4252da7b288461427cabc9dabb3)) +* **filechooser:** cancel is sync ([#6937](https://github.com/puppeteer/puppeteer/issues/6937)) ([2ba61e0](https://github.com/puppeteer/puppeteer/commit/2ba61e04e923edaac09c92315212552f2d4ce676)) +* **network:** don't disable cache for auth challenge ([#6962](https://github.com/puppeteer/puppeteer/issues/6962)) ([1c2479a](https://github.com/puppeteer/puppeteer/commit/1c2479a6cd4bd09a577175ffd31c40ca6f4279b8)) + +## [8.0.0](https://github.com/puppeteer/puppeteer/compare/v7.1.0...v8.0.0) (2021-02-26) + + +### ⚠ BREAKING CHANGES + +* renamed type `ChromeArgOptions` to `BrowserLaunchArgumentOptions` +* renamed type `BrowserOptions` to `BrowserConnectOptions` + +### Features + +* **chromium:** roll Chromium to r856583 ([#6927](https://github.com/puppeteer/puppeteer/issues/6927)) ([0c688bd](https://github.com/puppeteer/puppeteer/commit/0c688bd75ef1d1fc3afd14cbe8966757ecda68fb)) + + +### Bug Fixes + +* explicit HTTPRequest.resourceType type defs ([#6882](https://github.com/puppeteer/puppeteer/issues/6882)) ([ff26c62](https://github.com/puppeteer/puppeteer/commit/ff26c62647b60cd0d8d7ea66ee998adaadc3fcc2)), closes [#6854](https://github.com/puppeteer/puppeteer/issues/6854) +* expose `Viewport` type ([#6881](https://github.com/puppeteer/puppeteer/issues/6881)) ([be7c229](https://github.com/puppeteer/puppeteer/commit/be7c22933c1dcf5eee797d61463171bd0ef44582)) +* improve TS types for launching browsers ([#6888](https://github.com/puppeteer/puppeteer/issues/6888)) ([98c8145](https://github.com/puppeteer/puppeteer/commit/98c81458c27f378eb66c38e1620e79e2ffde418e)) +* move CI npm config out of .npmrc ([#6901](https://github.com/puppeteer/puppeteer/issues/6901)) ([f7de60b](https://github.com/puppeteer/puppeteer/commit/f7de60be22d9bc6433ada7bfefeaa7f6f6f62047)) + +## [7.1.0](https://github.com/puppeteer/puppeteer/compare/v7.0.4...v7.1.0) (2021-02-12) + + +### Features + +* **page:** add color-gamut support to Page.emulateMediaFeatures ([#6857](https://github.com/puppeteer/puppeteer/issues/6857)) ([ad59357](https://github.com/puppeteer/puppeteer/commit/ad5935738d869cfce386a0d28b4bc6131457f962)), closes [#6761](https://github.com/puppeteer/puppeteer/issues/6761) + + +### Bug Fixes + +* add favicon test asset ([#6868](https://github.com/puppeteer/puppeteer/issues/6868)) ([a63f53c](https://github.com/puppeteer/puppeteer/commit/a63f53c9380545550503f5539494c72c607e19ac)) +* expose `ScreenshotOptions` type in type defs ([#6869](https://github.com/puppeteer/puppeteer/issues/6869)) ([63d48b2](https://github.com/puppeteer/puppeteer/commit/63d48b2ecba317b6c0a3acad87a7a3671c769dbc)), closes [#6866](https://github.com/puppeteer/puppeteer/issues/6866) +* expose puppeteer.Permission type ([#6856](https://github.com/puppeteer/puppeteer/issues/6856)) ([a5e174f](https://github.com/puppeteer/puppeteer/commit/a5e174f696eb192c541db64a603ea5cdf385a643)) +* jsonValue() type is generic ([#6865](https://github.com/puppeteer/puppeteer/issues/6865)) ([bdaba78](https://github.com/puppeteer/puppeteer/commit/bdaba7829da366aabbc81885d84bb2401ab3eaff)) +* wider compat TS types and CI checks to ensure correct type defs ([#6855](https://github.com/puppeteer/puppeteer/issues/6855)) ([6a0eb78](https://github.com/puppeteer/puppeteer/commit/6a0eb7841fd82493903b0b9fa153d2de181350eb)) + +### [7.0.4](https://github.com/puppeteer/puppeteer/compare/v7.0.3...v7.0.4) (2021-02-09) + + +### Bug Fixes + +* make publish bot run full build, not just tsc ([#6848](https://github.com/puppeteer/puppeteer/issues/6848)) ([f718b14](https://github.com/puppeteer/puppeteer/commit/f718b14b64df8be492d344ddd35e40961ff750c5)) + +### [7.0.3](https://github.com/puppeteer/puppeteer/compare/v7.0.2...v7.0.3) (2021-02-09) + + +### Bug Fixes + +* include lib/types.d.ts in files list ([#6844](https://github.com/puppeteer/puppeteer/issues/6844)) ([e34f317](https://github.com/puppeteer/puppeteer/commit/e34f317b37533256a063c1238609b488d263b998)) + +### [7.0.2](https://github.com/puppeteer/puppeteer/compare/v7.0.1...v7.0.2) (2021-02-09) + + +### Bug Fixes + +* much better TypeScript definitions ([#6837](https://github.com/puppeteer/puppeteer/issues/6837)) ([f1b46ab](https://github.com/puppeteer/puppeteer/commit/f1b46ab5faa262f893c17923579d0cf52268a764)) +* **domworld:** reset bindings when context changes ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6836](https://github.com/puppeteer/puppeteer/issues/6836)) ([4e8d074](https://github.com/puppeteer/puppeteer/commit/4e8d074c2f8384a2f283f5edf9ef69c40bd8464f)) +* **launcher:** output correct error message for browser ([#6815](https://github.com/puppeteer/puppeteer/issues/6815)) ([6c61874](https://github.com/puppeteer/puppeteer/commit/6c618747979c3a08f2727e9e22fe45cade8c926a)) + +### [7.0.1](https://github.com/puppeteer/puppeteer/compare/v7.0.0...v7.0.1) (2021-02-04) + + +### Bug Fixes + +* **typescript:** ship .d.ts file in npm package ([#6811](https://github.com/puppeteer/puppeteer/issues/6811)) ([a7e3c2e](https://github.com/puppeteer/puppeteer/commit/a7e3c2e09e9163eee2f15221aafa4400e6a75f91)) + +## [7.0.0](https://github.com/puppeteer/puppeteer/compare/v6.0.0...v7.0.0) (2021-02-03) + + +### ⚠ BREAKING CHANGES + +* - `page.screenshot` makes a screenshot with the clip dimensions, not cutting it by the ViewPort size. +* **chromium:** - `page.screenshot` cuts screenshot content by the ViewPort size, not ViewPort position. + +### Features + +* use `captureBeyondViewport` in `Page.captureScreenshot` ([#6805](https://github.com/puppeteer/puppeteer/issues/6805)) ([401d84e](https://github.com/puppeteer/puppeteer/commit/401d84e4a3508f9ca5c24dbfcad2a71571b1b8eb)) +* **chromium:** roll Chromium to r848005 ([#6801](https://github.com/puppeteer/puppeteer/issues/6801)) ([890d5c2](https://github.com/puppeteer/puppeteer/commit/890d5c2e57cdee7d73915a878bda86b72e26b608)) + +## [6.0.0](https://github.com/puppeteer/puppeteer/compare/v5.5.0...v6.0.0) (2021-02-02) + + +### ⚠ BREAKING CHANGES + +* **chromium:** The built-in `aria/` selector query handler doesn’t return ignored elements anymore. + +### Features + +* **chromium:** roll Chromium to r843427 ([#6797](https://github.com/puppeteer/puppeteer/issues/6797)) ([8f9fbdb](https://github.com/puppeteer/puppeteer/commit/8f9fbdbae68254600a9c73ab05f36146c975dba6)), closes [#6758](https://github.com/puppeteer/puppeteer/issues/6758) +* add page.emulateNetworkConditions ([#6759](https://github.com/puppeteer/puppeteer/issues/6759)) ([5ea76e9](https://github.com/puppeteer/puppeteer/commit/5ea76e9333c42ab5a751ca01aa5676a662f6c063)) +* **types:** expose typedefs to consumers ([#6745](https://github.com/puppeteer/puppeteer/issues/6745)) ([ebd087a](https://github.com/puppeteer/puppeteer/commit/ebd087a31661a1b701650d0be3e123cc5a813bd8)) +* add iPhone 11 models to DeviceDescriptors ([#6467](https://github.com/puppeteer/puppeteer/issues/6467)) ([50b810d](https://github.com/puppeteer/puppeteer/commit/50b810dab7fae5950ba086295462788f91ff1e6f)) +* support fetching and launching on Apple M1 ([9a8479a](https://github.com/puppeteer/puppeteer/commit/9a8479a52a7d8b51690b0732b2a10816cd1b8aef)), closes [#6495](https://github.com/puppeteer/puppeteer/issues/6495) [#6634](https://github.com/puppeteer/puppeteer/issues/6634) [#6641](https://github.com/puppeteer/puppeteer/issues/6641) [#6614](https://github.com/puppeteer/puppeteer/issues/6614) +* support promise as return value for page.waitForResponse predicate ([#6624](https://github.com/puppeteer/puppeteer/issues/6624)) ([b57f3fc](https://github.com/puppeteer/puppeteer/commit/b57f3fcd5393c68f51d82e670b004f5b116dcbc3)) + + +### Bug Fixes + +* **domworld:** fix waitfor bindings ([#6766](https://github.com/puppeteer/puppeteer/issues/6766)) ([#6775](https://github.com/puppeteer/puppeteer/issues/6775)) ([cac540b](https://github.com/puppeteer/puppeteer/commit/cac540be3ab8799a1d77b0951b16bc22ea1c2adb)) +* **launcher:** rename TranslateUI to Translate to match Chrome ([#6692](https://github.com/puppeteer/puppeteer/issues/6692)) ([d901696](https://github.com/puppeteer/puppeteer/commit/d901696e0d8901bcb23cf676a5e5ac562f821a0d)) +* do not use old utility world ([#6528](https://github.com/puppeteer/puppeteer/issues/6528)) ([fb85911](https://github.com/puppeteer/puppeteer/commit/fb859115c0e2829bae1d1b32edbf642988e2ef76)), closes [#6527](https://github.com/puppeteer/puppeteer/issues/6527) +* update to https-proxy-agent@^5.0.0 to fix `ERR_INVALID_PROTOCOL` ([#6555](https://github.com/puppeteer/puppeteer/issues/6555)) ([3bf5a55](https://github.com/puppeteer/puppeteer/commit/3bf5a552890ee80cc4326b1e430424b0fdad4363)) + +## [5.5.0](https://github.com/puppeteer/puppeteer/compare/v5.4.1...v5.5.0) (2020-11-16) + + +### Features + +* **chromium:** roll Chromium to r818858 ([#6526](https://github.com/puppeteer/puppeteer/issues/6526)) ([b549256](https://github.com/puppeteer/puppeteer/commit/b54925695200cad32f470f8eb407259606447a85)) + + +### Bug Fixes + +* **common:** fix generic type of `_isClosedPromise` ([#6579](https://github.com/puppeteer/puppeteer/issues/6579)) ([122f074](https://github.com/puppeteer/puppeteer/commit/122f074f92f47a7b9aa08091851e51a07632d23b)) +* **domworld:** fix missing binding for waittasks ([#6562](https://github.com/puppeteer/puppeteer/issues/6562)) ([67da1cf](https://github.com/puppeteer/puppeteer/commit/67da1cf866703f5f581c9cce4923697ac38129ef)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000..162872110e741 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,315 @@ + + + +- [How to Contribute](#how-to-contribute) + * [Contributor License Agreement](#contributor-license-agreement) + * [Getting Code](#getting-code) + * [Code reviews](#code-reviews) + * [Code Style](#code-style) + * [TypeScript guidelines](#typescript-guidelines) + * [Project structure and TypeScript compilation](#project-structure-and-typescript-compilation) + - [Shipping CJS and ESM bundles](#shipping-cjs-and-esm-bundles) + - [tsconfig for the tests](#tsconfig-for-the-tests) + - [Root `tsconfig.json`](#root-tsconfigjson) + * [API guidelines](#api-guidelines) + * [Commit Messages](#commit-messages) + * [Writing Documentation](#writing-documentation) + * [Writing TSDoc Comments](#writing-tsdoc-comments) + * [Running New Documentation website locally](#running-new-documentation-website-locally) + * [Adding New Dependencies](#adding-new-dependencies) + * [Running & Writing Tests](#running--writing-tests) + * [Public API Coverage](#public-api-coverage) + * [Debugging Puppeteer](#debugging-puppeteer) +- [For Project Maintainers](#for-project-maintainers) + * [Rolling new Chromium version](#rolling-new-chromium-version) + - [Bisecting upstream changes](#bisecting-upstream-changes) + * [Releasing to npm](#releasing-to-npm) + + + + +# How to Contribute + +First of all, thank you for your interest in Puppeteer! +We'd love to accept your patches and contributions! + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Getting Code + +1. Clone this repository + +```bash +git clone https://github.com/puppeteer/puppeteer +cd puppeteer +``` + +2. Install dependencies + +```bash +npm install +``` + +3. Run Puppeteer tests locally. For more information about tests, read [Running & Writing Tests](#running--writing-tests). + +```bash +npm run unit +``` + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Code Style + +- Coding style is fully defined in [`.eslintrc`](https://github.com/puppeteer/puppeteer/blob/main/.eslintrc.js) and we automatically format our code with [Prettier](https://prettier.io). +- It's recommended to set-up Prettier into your editor, or you can run `npm run eslint-fix` to automatically format any files. +- If you're working in a JS file, code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler). +- If you're working in a TS file, you should explicitly type all variables and return types. You'll get ESLint warnings if you don't so if you're not sure use them as guidelines, and feel free to ask us for help! + +To run ESLint, use: + +```bash +npm run eslint +``` + +You can check your code (both JS & TS) type-checks by running: + +```bash +npm run tsc +``` + +## TypeScript guidelines + +- Try to avoid the use of `any` when possible. Consider `unknown` as a better alternative. You are able to use `any` if needbe, but it will generate an ESLint warning. + +## Project structure and TypeScript compilation + +The code in Puppeteer is split primarily into two folders: + +- `src` contains all source code +- `vendor` contains all dependencies that we've vendored into the codebase. See the [`vendor/README.md`](https://github.com/puppeteer/puppeteer/blob/main/vendor/README.md) for details. + +We structure these using TypeScript's project references, which lets us treat each folder like a standalone TypeScript project. + +### Shipping CJS and ESM bundles + +Currently Puppeteer ships two bundles; a CommonJS version for Node and an ESM bundle for the browser. Therefore we maintain two `tsconfig` files for each project; `tsconfig.esm.json` and `tsconfig.cjs.json`. At build time we compile twice, once outputting to CJS and another time to output to ESM. + +We compile into the `lib` directory which is what we publish on the npm repository and it's structured like so: + +``` +lib +- cjs + - puppeteer <== the output of compiling `src/tsconfig.cjs.json` + - vendor <== the output of compiling `vendor/tsconfig.cjs.json` +- esm + - puppeteer <== the output of compiling `src/tsconfig.esm.json` + - vendor <== the output of compiling `vendor/tsconfig.esm.json` +``` + +The main entry point for the Node module Puppeteer is `cjs-entry.js`. This imports `lib/cjs/puppeteer/index.js` and exposes it to Node users. + +### tsconfig for the tests + +We also maintain `test/tsconfig.test.json`. This is **only used to compile the unit test `*.spec.ts` files**. When the tests are run, we first compile Puppeteer as normal before running the unit tests **against the compiled output**. Doing this lets the test run against the compiled code we ship to users so it gives us more confidence in our compiled output being correct. + +### Root `tsconfig.json` + +The root `tsconfig.json` exists for the API Extractor; it has to find a `tsconfig.json` in the project's root directory. It is _not_ used for anything else. + +## API guidelines + +When authoring new API methods, consider the following: + +- Expose as little information as needed. When in doubt, don’t expose new information. +- Methods are used in favor of getters/setters. + - The only exception is namespaces, e.g. `page.keyboard` and `page.coverage` +- All string literals must be small case. This includes event names and option values. +- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** demanded. + +## Commit Messages + +Commit messages should follow [the Conventional Commits format](https://www.conventionalcommits.org/en/v1.0.0/#summary). This is enforced via `npm run lint`. + +In particular, breaking changes should clearly be noted as “BREAKING CHANGE:” in the commit message footer. Example: + +``` +fix(page): fix page.pizza method + +This patch fixes page.pizza so that it works with iframes. + +Issues: #123, #234 + +BREAKING CHANGE: page.pizza now delivers pizza at home by default. +To deliver to a different location, use the "deliver" option: + `page.pizza({deliver: 'work'})`. +``` + +## Writing Documentation + +All public API should have a descriptive entry in [`docs/api.md`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md). There's a [documentation linter](https://github.com/puppeteer/puppeteer/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase. + +To run the documentation linter, use: + +```bash +npm run doc +``` + +To format the documentation markdown and its code snippets, use: + +```bash +npm run prettier-fix +``` + +## Writing TSDoc Comments + +Each change to Puppeteer should be thoroughly documented using TSDoc comments. Refer to the [API Extractor documentation](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) for information on the exact syntax. + +- Every new method needs to have either `@public` or `@internal` added as a tag depending on if it is part of the public API. +- Keep each line in a comment to no more than 90 characters (ESLint will warn you if you go over this). If you're a VSCode user the [Rewrap plugin](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap) is highly recommended! + +## Running New Documentation website locally + +- In the Puppeteer's folder, install all dependencies with `npm i`. +- run `npm run generate-docs` which will generate all the `.md` files on `puppeteer/website/docs`. +- run `npm i` on `puppeteer/website`. +- run `npm start` on `puppeteer/website`. + +## Adding New Dependencies + +For all dependencies (both installation and development): + +- **Do not add** a dependency if the desired functionality is easily implementable. +- If adding a dependency, it should be well-maintained and trustworthy. + +A barrier for introducing new installation dependencies is especially high: + +- **Do not add** installation dependency unless it's critical to project success. + +There are additional considerations for dependencies that are environment agonistic. See the [`vendor/README.md`](https://github.com/puppeteer/puppeteer/blob/main/vendor/README.md) for details. + +## Running & Writing Tests + +- Every feature should be accompanied by a test. +- Every public api event/method should be accompanied by a test. +- Tests should not depend on external services. +- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests. + +Puppeteer tests are located in [the `test` directory](https://github.com/puppeteer/puppeteer/blob/main/test/) and are written using Mocha. See [`test/README.md`](https://github.com/puppeteer/puppeteer/blob/main/test/README.md) for more details. + +Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. + +- To run all tests: + +```bash +npm run unit +``` + +- To run a specific test, substitute the `it` with `it.only`: + +```js + ... + it.only('should work', async function({server, page}) { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '_cross it_'): + +```js + ... + // Using "xit" to skip specific test + xit('should work', async function({server, page}) { + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run tests in non-headless mode: + +```bash +HEADLESS=false npm run unit +``` + +- To run Firefox tests, firstly ensure you have Firefox installed locally (you only need to do this once, not on every test run) and then you can run the tests: + +```bash +PUPPETEER_PRODUCT=firefox node install.js +PUPPETEER_PRODUCT=firefox npm run unit +``` + +- To run experimental Chromium MacOS ARM tests, firstly ensure you have correct Chromium version installed locally (you only need to do this once, not on every test run) and then you can run the tests: + +```bash +PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM=1 node install.js +PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM=1 npm run unit +``` + +- To run tests with custom browser executable: + +```bash +BINARY= npm run unit +``` + +## Public API Coverage + +Every public API method or event should be called at least once in tests. To ensure this, there's a `coverage` command which tracks calls to public API and reports back if some methods/events were not called. + +Run coverage: + +```bash +npm run coverage +``` + +## Debugging Puppeteer + +See [Debugging Tips](README.md#debugging-tips) in the readme. + +# For Project Maintainers + +## Rolling new Chromium version + +The following steps are needed to update the Chromium version. + +1. Find a suitable Chromium revision + Not all revisions have builds for all platforms, so we need to find one that does. + To do so, run `utils/check_availability.js -rd` to find the latest suitable `dev` Chromium revision (see `utils/check_availability.js -help` for more options). +1. Update `src/revisions.ts` with the found revision number. +1. Update `versions.js` with the new Chromium-to-Puppeteer version mapping and update `lastMaintainedChromiumVersion` with the latest stable Chrome version. +1. Run `npm run ensure-correct-devtools-protocol-revision`. + If it fails, update `package.json` with the expected `devtools-protocol` version. +1. Run `npm run tsc` and `npm install`. +1. Run `npm run unit` and ensure that all tests pass. If a test fails, [bisect](#bisecting-upstream-changes) the upstream cause of the failure, and either update the test expectations accordingly (if it was an intended change) or work around the changes in Puppeteer (if it’s not desirable to change Puppeteer’s observable behavior). +1. Commit and push your changes and open a pull request. + The commit message must contain the version in `Chromium ()` format to ensure that [pptr.dev](https://pptr.dev/) can parse it correctly, e.g. `'feat(chromium): roll to Chromium 90.0.4427.0 (r856583)'`. + +### Bisecting upstream changes + +Sometimes, performing a Chromium roll causes tests to fail. To figure out the cause, you need to bisect Chromium revisions to figure out the earliest possible revision that changed the behavior. The script in `utils/bisect.js` can be helpful here. Given a pattern for one or more unit tests, it will automatically bisect the current range: + +```sh +node utils/bisect.js --good 686378 --bad 706915 script.js + +node utils/bisect.js --unit-test Response.fromCache +``` + +By default, it will use the Chromium revision in `src/revisions.ts` from the `main` branch and from the working tree to determine the range to bisect. + +## Releasing to npm + +We use [release-please](https://github.com/googleapis/release-please) to automate releases. When a release should be done, check for the release PR in our [pull requests](https://github.com/puppeteer/puppeteer/pulls) and merge it. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..d2c171df74e93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..9addf61a477ea --- /dev/null +++ b/README.md @@ -0,0 +1,473 @@ +# Puppeteer + + + +[![Build status](https://github.com/puppeteer/puppeteer/workflows/run-checks/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3Arun-checks) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) + + + + + +###### [API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) + +> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. + + + +###### What can I do? + +Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started: + +- Generate screenshots and PDFs of pages. +- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). +- Automate form submission, UI testing, keyboard input, etc. +- Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. +- Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues. +- Test Chrome Extensions. + + + + +## Getting Started + +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or "yarn add puppeteer" +``` + +Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, download into another path, or download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#environment-variables). + +### puppeteer-core + +Since version 1.7.0 we publish the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package, +a version of Puppeteer that doesn't download any browser by default. + +```bash +npm i puppeteer-core +# or "yarn add puppeteer-core" +``` + +`puppeteer-core` is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one. Be sure that the version of puppeteer-core you install is compatible with the browser you intend to connect to. + +See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core). + +### Usage + +Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. + +Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on +Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater. + +Puppeteer will be familiar to people using other browser testing frameworks. You create an instance +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#). + +**Example** - navigating to https://example.com and saving a screenshot as _example.png_: + +Save file as **example.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagesetviewportviewport). + +**Example** - create a PDF. + +Save file as **hn.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + await page.pdf({ path: 'hn.pdf', format: 'a4' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node hn.js +``` + +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagepdfoptions) for more information about creating pdfs. + +**Example** - evaluate script in the context of the page + +Save file as **get-dimensions.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio, + }; + }); + + console.log('Dimensions:', dimensions); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node get-dimensions.js +``` + +See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. + + + + + +## Default runtime settings + +**1. Uses Headless mode** + +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions) when launching a browser: + +```js +const browser = await puppeteer.launch({ headless: false }); // default is true +``` + +**2. Runs a bundled version of Chromium** + +By default, Puppeteer downloads and uses a specific version of Chromium so its API +is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium, +pass in the executable's path when creating a `Browser` instance: + +```js +const browser = await puppeteer.launch({ executablePath: '/path/to/Chrome' }); +``` + +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions) for more information. + +See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +**3. Creates a fresh user profile** + +Puppeteer creates its own browser user profile which it **cleans up on every run**. + + + +## Resources + +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples/) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + + + +## Debugging tips + +1. Turn off headless mode - sometimes it's useful to see what the browser is + displaying. Instead of launching in headless mode, launch a full version of + the browser using `headless: false`: + + ```js + const browser = await puppeteer.launch({ headless: false }); + ``` + +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the + specified amount of milliseconds. It's another way to help see what's going on. + + ```js + const browser = await puppeteer.launch({ + headless: false, + slowMo: 250, // slow down by 250ms + }); + ``` + +3. Capture console output - You can listen for the `console` event. + This is also handy when debugging code in `page.evaluate()`: + + ```js + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => console.log(`url is ${location.href}`)); + ``` + +4. Use debugger in application code browser + + There are two execution context: node.js that is running test code, and the browser + running application code being tested. This lets you debug code in the + application code browser; ie code inside `evaluate()`. + + - Use `{devtools: true}` when launching Puppeteer: + + ```js + const browser = await puppeteer.launch({ devtools: true }); + ``` + + - Change default test timeout: + + jest: `jest.setTimeout(100000);` + + jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;` + + mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442)) + + - Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement: + + ```js + await page.evaluate(() => { + debugger; + }); + ``` + + The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode. + +5. Use debugger in node.js + + This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser. + + Note that you won't be able to run `await page.click()` in + DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if + you want to try something out, you have to add it to your test file. + + - Add `debugger;` to your test, eg: + + ```js + debugger; + await page.click('a[target=_blank]'); + ``` + + - Set `headless` to `false` + - Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests` + - In Chrome open `chrome://inspect/#devices` and click `inspect` + - In the newly opened test browser, type `F8` to resume test execution + - Now your `debugger` will be hit and you can debug in the test browser + +6. Enable verbose logging - internal DevTools protocol traffic + will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. + + # Basic verbose logging + env DEBUG="puppeteer:*" node script.js + + # Protocol traffic can be rather noisy. This example filters out all Network domain messages + env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' + +7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) + +- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) + +- add a `debugger` to your Puppeteer (node) code + +- add `ndb` (or `npx ndb`) before your test command. For example: + + `ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) + +- debug your test inside chromium like a boss! + + + + + +## Usage with TypeScript + +We have recently completed a migration to move the Puppeteer source code from JavaScript to TypeScript and as of version 7.0.1 we ship our own built-in type definitions. + +If you are on a version older than 7, we recommend installing the Puppeteer type definitions from the [DefinitelyTyped](https://definitelytyped.org/) repository: + +```bash +npm install --save-dev @types/puppeteer +``` + +The types that you'll see appearing in the Puppeteer source code are based off the great work of those who have contributed to the `@types/puppeteer` package. We really appreciate the hard work those people put in to providing high quality TypeScript definitions for Puppeteer's users. + + + +## Contributing to Puppeteer + +Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) to get an overview of Puppeteer development. + + + +# FAQ + +#### Q: Who maintains Puppeteer? + +The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project! +See [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md). + +#### Q: What is the status of cross-browser support? + +Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention. + +From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. + +We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari. +This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome). + +#### Q: What are Puppeteer’s goals and principles? + +The goals of the project are: + +- Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer. +- Grow the adoption of headless/automated browser testing. +- Help dogfood new DevTools Protocol features...and catch bugs! +- Learn more about the pain points of automated browser testing and help fill those gaps. + +We adapt [Chromium principles](https://www.chromium.org/developers/core-principles) to help us drive product decisions: + +- **Speed**: Puppeteer has almost zero performance overhead over an automated page. +- **Security**: Puppeteer operates off-process with respect to Chromium, making it safe to automate potentially malicious pages. +- **Stability**: Puppeteer should not be flaky and should not leak memory. +- **Simplicity**: Puppeteer provides a high-level API that’s easy to use, understand, and debug. + +#### Q: Is Puppeteer replacing Selenium/WebDriver? + +**No**. Both projects are valuable for very different reasons: + +- Selenium/WebDriver focuses on cross-browser automation; its value proposition is a single standard API that works across all major browsers. +- Puppeteer focuses on Chromium; its value proposition is richer functionality and higher reliability. + +That said, you **can** use Puppeteer to run tests against Chromium, e.g. using the community-driven [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer). While this probably shouldn’t be your only testing solution, it does have a few good points compared to WebDriver: + +- Puppeteer requires zero setup and comes bundled with the Chromium version it works best with, making it [very easy to start with](https://github.com/puppeteer/puppeteer/#getting-started). At the end of the day, it’s better to have a few tests running chromium-only, than no tests at all. +- Puppeteer has event-driven architecture, which removes a lot of potential flakiness. There’s no need for evil “sleep(1000)” calls in puppeteer scripts. +- Puppeteer runs headless by default, which makes it fast to run. Puppeteer v1.5.0 also exposes browser contexts, making it possible to efficiently parallelize test execution. +- Puppeteer shines when it comes to debugging: flip the “headless” bit to false, add “slowMo”, and you’ll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment. + +#### Q: Why doesn’t Puppeteer v.XXX work with Chromium v.YYY? + +We see Puppeteer as an **indivisible entity** with Chromium. Each version of Puppeteer bundles a specific version of Chromium – **the only** version it is guaranteed to work with. + +This is not an artificial constraint: A lot of work on Puppeteer is actually taking place in the Chromium repository. Here’s a typical story: + +- A Puppeteer bug is reported: https://github.com/puppeteer/puppeteer/issues/2709 +- It turned out this is an issue with the DevTools protocol, so we’re fixing it in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/1102154 +- Once the upstream fix is landed, we roll updated Chromium into Puppeteer: https://github.com/puppeteer/puppeteer/pull/2769 + +However, oftentimes it is desirable to use Puppeteer with the official Google Chrome rather than Chromium. For this to work, you should install a `puppeteer-core` version that corresponds to the Chrome version. + +For example, in order to drive Chrome 71 with puppeteer-core, use `chrome-71` npm tag: + +```bash +npm install puppeteer-core@chrome-71 +``` + +#### Q: Which Chromium version does Puppeteer use? + +Find the version using one of the following ways: + +- Look for the `chromium` entry in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. +- Look for the `versionsPerRelease` map in [versions.js](https://github.com/puppeteer/puppeteer/blob/main/versions.js) which contains mapping between Chromium and Puppeteer versions. Note: The file contains only Puppeteer versions where Chromium is updated. Not all Puppeteer versions are listed. + +#### Q: Which Firefox version does Puppeteer use? + +Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox` in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts) is `latest` -- Puppeteer isn't tied to a particular Firefox version. + +To fetch Firefox Nightly as part of Puppeteer installation: + +```bash +PUPPETEER_PRODUCT=firefox npm i puppeteer +# or "yarn add puppeteer" +``` + +#### Q: What’s considered a “Navigation”? + +From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s URL**. +Aside from regular navigation where the browser hits the network to fetch a new document from the web server, this includes [anchor navigations](https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid) and [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) usage. + +With this definition of “navigation,” **Puppeteer works seamlessly with single-page applications.** + +#### Q: What’s the difference between a “trusted" and "untrusted" input event? + +In browsers, input events could be divided into two big groups: trusted vs. untrusted. + +- **Trusted events**: events generated by users interacting with the page, e.g. using a mouse or keyboard. +- **Untrusted event**: events generated by Web APIs, e.g. `document.createEvent` or `element.click()` methods. + +Websites can distinguish between these two groups: + +- using an [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) event flag +- sniffing for accompanying events. For example, every trusted `'click'` event is preceded by `'mousedown'` and `'mouseup'` events. + +For automation purposes it’s important to generate trusted events. **All input events generated with Puppeteer are trusted and fire proper accompanying events.** If, for some reason, one needs an untrusted event, it’s always possible to hop into a page context with `page.evaluate` and generate a fake event: + +```js +await page.evaluate(() => { + document.querySelector('button[type=submit]').click(); +}); +``` + +#### Q: What features does Puppeteer not support? + +You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this: + +- Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) +- Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). + +#### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help? + +We have a [troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) guide for various operating systems that lists the required dependencies. + +#### Q: Chromium gets downloaded on every `npm ci` run. How can I cache the download? + +The default download path is `node_modules/puppeteer/.local-chromium`. However, you can change that path with the `PUPPETEER_DOWNLOAD_PATH` environment variable. + +Puppeteer uses that variable to resolve the Chromium executable location during launch, so you don’t need to specify `PUPPETEER_EXECUTABLE_PATH` as well. + +For example, if you wish to keep the Chromium download in `~/.npm/chromium`: + +```sh +export PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium +npm ci + +# by default the Chromium executable path is inferred +# from the download path +npm test + +# a new run of npm ci will check for the existence of +# Chromium in ~/.npm/chromium +npm ci +``` + +#### Q: I have more questions! Where do I ask? + +There are many ways to get help on Puppeteer: + +- [bugtracker](https://github.com/puppeteer/puppeteer/issues) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/puppeteer) + +Make sure to search these channels before posting your question. + + diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000000000..08a64da53c404 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/lib/cjs/puppeteer/api-docs-entry.d.ts", + "bundledPackages": [], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "/docs-api-json/.api.json" + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "lib/types.d.ts" + }, + + "tsdocMetadata": { + "enabled": false + }, + + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + + "extractorMessageReporting": { + "ae-internal-missing-underscore": { + "logLevel": "none" + }, + "default": { + "logLevel": "warning" + } + }, + + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + } +} diff --git a/cjs-entry-core.js b/cjs-entry-core.js new file mode 100644 index 0000000000000..ad2e49c5f51b4 --- /dev/null +++ b/cjs-entry-core.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node-puppeteer-core.js'); +module.exports = puppeteerExport.default; diff --git a/cjs-entry.js b/cjs-entry.js new file mode 100644 index 0000000000000..70c262a9b78d8 --- /dev/null +++ b/cjs-entry.js @@ -0,0 +1,29 @@ +/** + * Copyright 2020 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. + */ + +/** + * We use `export default puppeteer` in `src/index.ts` to expose the library But + * TypeScript in CJS mode compiles that to `exports.default = `. This means that + * our CJS Node users would have to use `require('puppeteer').default` which + * isn't very nice. + * + * So instead we expose this file as our entry point. This requires the compiled + * Puppeteer output and re-exports the `default` export via `module.exports.` + * This means that we can publish to CJS and ESM whilst maintaining the expected + * import behaviour for CJS and ESM users. + */ +const puppeteerExport = require('./lib/cjs/puppeteer/node.js'); +module.exports = puppeteerExport.default; diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000000000..6f6801b097c2e --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,23 @@ +/** + * Copyright 2020 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. + */ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-max-line-length': [0, 'always', 100], + 'footer-max-line-length': [0, 'always', 100], + 'subject-case': [0, 'never'], + }, +}; diff --git a/compat/README.md b/compat/README.md new file mode 100644 index 0000000000000..a72ecab4e8a3c --- /dev/null +++ b/compat/README.md @@ -0,0 +1,16 @@ +# Compatibility layer + +This directory provides an additional compatibility layer between ES modules and CommonJS. + +## Why? + +Both `./cjs/compat.ts` and `./esm/compat.ts` are written as ES modules, but `./cjs/compat.ts` can additionally use NodeJS CommonJS globals such as `__dirname` and `require` while these are disabled in ES module mode. For more information, see [Differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs). + +## Adding exports + +In order to add exports, two things need to be done: + +- The exports must be declared in `src/compat.ts`. +- The exports must be realized in `./cjs/compat.ts` and `./esm/compat.ts`. + +In the event `compat.ts` becomes too large, you can place declarations in another file. Just make sure `./cjs`, `./esm`, and `src` have the same structure. diff --git a/compat/cjs/compat.ts b/compat/cjs/compat.ts new file mode 100644 index 0000000000000..6efa3fe9aabe3 --- /dev/null +++ b/compat/cjs/compat.ts @@ -0,0 +1,16 @@ +import { dirname } from 'path'; + +let puppeteerDirname: string; + +try { + // In some environments, like esbuild, this will throw an error. + // We suppress the error since the bundled binary is not expected + // to be used or installed in this case and, therefore, the + // root directory does not have to be known. + puppeteerDirname = dirname(require.resolve('./compat')); +} catch (error) { + // Fallback to __dirname. + puppeteerDirname = __dirname; +} + +export { puppeteerDirname }; diff --git a/compat/cjs/tsconfig.json b/compat/cjs/tsconfig.json new file mode 100644 index 0000000000000..2bcb2b984b4c8 --- /dev/null +++ b/compat/cjs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../../lib/cjs/puppeteer", + "module": "CommonJS" + }, + "references": [{ "path": "../../vendor/tsconfig.cjs.json" }] +} diff --git a/compat/esm/compat.ts b/compat/esm/compat.ts new file mode 100644 index 0000000000000..a3288e58535de --- /dev/null +++ b/compat/esm/compat.ts @@ -0,0 +1,19 @@ +import { createRequire } from 'module'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const require = createRequire(import.meta.url); + +let puppeteerDirname: string; + +try { + // In some environments, like esbuild, this will throw an error. + // We suppress the error since the bundled binary is not expected + // to be used or installed in this case and, therefore, the + // root directory does not have to be known. + puppeteerDirname = dirname(require.resolve('./compat')); +} catch (error) { + puppeteerDirname = dirname(fileURLToPath(import.meta.url)); +} + +export { puppeteerDirname }; diff --git a/compat/esm/tsconfig.json b/compat/esm/tsconfig.json new file mode 100644 index 0000000000000..42b320fcd3f28 --- /dev/null +++ b/compat/esm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../../lib/esm/puppeteer", + "module": "esnext" + }, + "references": [{ "path": "../../vendor/tsconfig.esm.json" }] +} diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000000000..28eca442ec9bd --- /dev/null +++ b/docs/api.md @@ -0,0 +1,5648 @@ +# Puppeteer API Tip-Of-Tree + + + +- Interactive Documentation: https://pptr.dev +- API Translations: [中文|Chinese](https://zhaoqize.github.io/puppeteer-api-zh_CN/#/) +- Troubleshooting: [troubleshooting.md](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) + + + + +- Releases per Chromium version: + * Chromium 103.0.5059.0 - [Puppeteer v14.2.0](https://github.com/puppeteer/puppeteer/blob/v14.2.0/docs/api.md) + * Chromium 102.0.5002.0 - [Puppeteer v14.0.0](https://github.com/puppeteer/puppeteer/blob/v14.0.0/docs/api.md) + * Chromium 101.0.4950.0 - [Puppeteer v13.6.0](https://github.com/puppeteer/puppeteer/blob/v13.6.0/docs/api.md) + * Chromium 100.0.4889.0 - [Puppeteer v13.5.0](https://github.com/puppeteer/puppeteer/blob/v13.5.0/docs/api.md) + * Chromium 99.0.4844.16 - [Puppeteer v13.2.0](https://github.com/puppeteer/puppeteer/blob/v13.2.0/docs/api.md) + * Chromium 98.0.4758.0 - [Puppeteer v13.1.0](https://github.com/puppeteer/puppeteer/blob/v13.1.0/docs/api.md) + * Chromium 97.0.4692.0 - [Puppeteer v12.0.0](https://github.com/puppeteer/puppeteer/blob/v12.0.0/docs/api.md) + * Chromium 93.0.4577.0 - [Puppeteer v10.2.0](https://github.com/puppeteer/puppeteer/blob/v10.2.0/docs/api.md) + * Chromium 92.0.4512.0 - [Puppeteer v10.0.0](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md) + * Chromium 91.0.4469.0 - [Puppeteer v9.0.0](https://github.com/puppeteer/puppeteer/blob/v9.0.0/docs/api.md) + * Chromium 90.0.4427.0 - [Puppeteer v8.0.0](https://github.com/puppeteer/puppeteer/blob/v8.0.0/docs/api.md) + * Chromium 90.0.4403.0 - [Puppeteer v7.0.0](https://github.com/puppeteer/puppeteer/blob/v7.0.0/docs/api.md) + * Chromium 89.0.4389.0 - [Puppeteer v6.0.0](https://github.com/puppeteer/puppeteer/blob/v6.0.0/docs/api.md) + * Chromium 88.0.4298.0 - [Puppeteer v5.5.0](https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md) + * Chromium 87.0.4272.0 - [Puppeteer v5.4.0](https://github.com/puppeteer/puppeteer/blob/v5.4.0/docs/api.md) + * Chromium 86.0.4240.0 - [Puppeteer v5.3.0](https://github.com/puppeteer/puppeteer/blob/v5.3.0/docs/api.md) + * Chromium 85.0.4182.0 - [Puppeteer v5.2.1](https://github.com/puppeteer/puppeteer/blob/v5.2.1/docs/api.md) + * Chromium 84.0.4147.0 - [Puppeteer v5.1.0](https://github.com/puppeteer/puppeteer/blob/v5.1.0/docs/api.md) + * Chromium 83.0.4103.0 - [Puppeteer v3.1.0](https://github.com/puppeteer/puppeteer/blob/v3.1.0/docs/api.md) + * Chromium 81.0.4044.0 - [Puppeteer v3.0.0](https://github.com/puppeteer/puppeteer/blob/v3.0.0/docs/api.md) + * Chromium 80.0.3987.0 - [Puppeteer v2.1.0](https://github.com/puppeteer/puppeteer/blob/v2.1.0/docs/api.md) + * Chromium 79.0.3942.0 - [Puppeteer v2.0.0](https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md) + * Chromium 78.0.3882.0 - [Puppeteer v1.20.0](https://github.com/puppeteer/puppeteer/blob/v1.20.0/docs/api.md) + * Chromium 77.0.3803.0 - [Puppeteer v1.19.0](https://github.com/puppeteer/puppeteer/blob/v1.19.0/docs/api.md) + * Chromium 76.0.3803.0 - [Puppeteer v1.17.0](https://github.com/puppeteer/puppeteer/blob/v1.17.0/docs/api.md) + * Chromium 75.0.3765.0 - [Puppeteer v1.15.0](https://github.com/puppeteer/puppeteer/blob/v1.15.0/docs/api.md) + * Chromium 74.0.3723.0 - [Puppeteer v1.13.0](https://github.com/puppeteer/puppeteer/blob/v1.13.0/docs/api.md) + * Chromium 73.0.3679.0 - [Puppeteer v1.12.2](https://github.com/puppeteer/puppeteer/blob/v1.12.2/docs/api.md) + * [All releases](https://github.com/puppeteer/puppeteer/releases) + + + + +##### Table of Contents + + + + +- [Overview](#overview) +- [puppeteer vs puppeteer-core](#puppeteer-vs-puppeteer-core) +- [Environment Variables](#environment-variables) +- [Working with Chrome Extensions](#working-with-chrome-extensions) +- [class: Puppeteer](#class-puppeteer) + * [puppeteer.clearCustomQueryHandlers()](#puppeteerclearcustomqueryhandlers) + * [puppeteer.connect(options)](#puppeteerconnectoptions) + * [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions) + * [puppeteer.customQueryHandlerNames()](#puppeteercustomqueryhandlernames) + * [puppeteer.defaultArgs([options])](#puppeteerdefaultargsoptions) + * [puppeteer.devices](#puppeteerdevices) + * [puppeteer.errors](#puppeteererrors) + * [puppeteer.executablePath()](#puppeteerexecutablepath) + * [puppeteer.launch([options])](#puppeteerlaunchoptions) + * [puppeteer.networkConditions](#puppeteernetworkconditions) + * [puppeteer.product](#puppeteerproduct) + * [puppeteer.registerCustomQueryHandler(name, queryHandler)](#puppeteerregistercustomqueryhandlername-queryhandler) + * [puppeteer.unregisterCustomQueryHandler(name)](#puppeteerunregistercustomqueryhandlername) +- [class: BrowserFetcher](#class-browserfetcher) + * [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision) + * [browserFetcher.download(revision[, progressCallback])](#browserfetcherdownloadrevision-progresscallback) + * [browserFetcher.host()](#browserfetcherhost) + * [browserFetcher.localRevisions()](#browserfetcherlocalrevisions) + * [browserFetcher.platform()](#browserfetcherplatform) + * [browserFetcher.product()](#browserfetcherproduct) + * [browserFetcher.remove(revision)](#browserfetcherremoverevision) + * [browserFetcher.revisionInfo(revision)](#browserfetcherrevisioninforevision) +- [class: Browser](#class-browser) + * [event: 'disconnected'](#event-disconnected) + * [event: 'targetchanged'](#event-targetchanged) + * [event: 'targetcreated'](#event-targetcreated) + * [event: 'targetdestroyed'](#event-targetdestroyed) + * [browser.browserContexts()](#browserbrowsercontexts) + * [browser.close()](#browserclose) + * [browser.createIncognitoBrowserContext([options])](#browsercreateincognitobrowsercontextoptions) + * [browser.defaultBrowserContext()](#browserdefaultbrowsercontext) + * [browser.disconnect()](#browserdisconnect) + * [browser.isConnected()](#browserisconnected) + * [browser.newPage()](#browsernewpage) + * [browser.pages()](#browserpages) + * [browser.process()](#browserprocess) + * [browser.target()](#browsertarget) + * [browser.targets()](#browsertargets) + * [browser.userAgent()](#browseruseragent) + * [browser.version()](#browserversion) + * [browser.waitForTarget(predicate[, options])](#browserwaitfortargetpredicate-options) + * [browser.wsEndpoint()](#browserwsendpoint) +- [class: BrowserContext](#class-browsercontext) + * [event: 'targetchanged'](#event-targetchanged-1) + * [event: 'targetcreated'](#event-targetcreated-1) + * [event: 'targetdestroyed'](#event-targetdestroyed-1) + * [browserContext.browser()](#browsercontextbrowser) + * [browserContext.clearPermissionOverrides()](#browsercontextclearpermissionoverrides) + * [browserContext.close()](#browsercontextclose) + * [browserContext.isIncognito()](#browsercontextisincognito) + * [browserContext.newPage()](#browsercontextnewpage) + * [browserContext.overridePermissions(origin, permissions)](#browsercontextoverridepermissionsorigin-permissions) + * [browserContext.pages()](#browsercontextpages) + * [browserContext.targets()](#browsercontexttargets) + * [browserContext.waitForTarget(predicate[, options])](#browsercontextwaitfortargetpredicate-options) +- [class: Page](#class-page) + * [event: 'close'](#event-close) + * [event: 'console'](#event-console) + * [event: 'dialog'](#event-dialog) + * [event: 'domcontentloaded'](#event-domcontentloaded) + * [event: 'error'](#event-error) + * [event: 'frameattached'](#event-frameattached) + * [event: 'framedetached'](#event-framedetached) + * [event: 'framenavigated'](#event-framenavigated) + * [event: 'load'](#event-load) + * [event: 'metrics'](#event-metrics) + * [event: 'pageerror'](#event-pageerror) + * [event: 'popup'](#event-popup) + * [event: 'request'](#event-request) + * [event: 'requestfailed'](#event-requestfailed) + * [event: 'requestfinished'](#event-requestfinished) + * [event: 'response'](#event-response) + * [event: 'workercreated'](#event-workercreated) + * [event: 'workerdestroyed'](#event-workerdestroyed) + * [page.$(selector)](#pageselector) + * [page.$$(selector)](#pageselector-1) + * [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + * [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args-1) + * [page.$x(expression)](#pagexexpression) + * [page.accessibility](#pageaccessibility) + * [page.addScriptTag(options)](#pageaddscripttagoptions) + * [page.addStyleTag(options)](#pageaddstyletagoptions) + * [page.authenticate(credentials)](#pageauthenticatecredentials) + * [page.bringToFront()](#pagebringtofront) + * [page.browser()](#pagebrowser) + * [page.browserContext()](#pagebrowsercontext) + * [page.click(selector[, options])](#pageclickselector-options) + * [page.close([options])](#pagecloseoptions) + * [page.content()](#pagecontent) + * [page.cookies([...urls])](#pagecookiesurls) + * [page.coverage](#pagecoverage) + * [page.createPDFStream([options])](#pagecreatepdfstreamoptions) + * [page.deleteCookie(...cookies)](#pagedeletecookiecookies) + * [page.emulate(options)](#pageemulateoptions) + * [page.emulateCPUThrottling(factor)](#pageemulatecputhrottlingfactor) + * [page.emulateIdleState(overrides)](#pageemulateidlestateoverrides) + * [page.emulateMediaFeatures(features)](#pageemulatemediafeaturesfeatures) + * [page.emulateMediaType(type)](#pageemulatemediatypetype) + * [page.emulateNetworkConditions(networkConditions)](#pageemulatenetworkconditionsnetworkconditions) + * [page.emulateTimezone(timezoneId)](#pageemulatetimezonetimezoneid) + * [page.emulateVisionDeficiency(type)](#pageemulatevisiondeficiencytype) + * [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) + * [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) + * [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) + * [page.exposeFunction(name, puppeteerFunction)](#pageexposefunctionname-puppeteerfunction) + * [page.focus(selector)](#pagefocusselector) + * [page.frames()](#pageframes) + * [page.goBack([options])](#pagegobackoptions) + * [page.goForward([options])](#pagegoforwardoptions) + * [page.goto(url[, options])](#pagegotourl-options) + * [page.hover(selector)](#pagehoverselector) + * [page.isClosed()](#pageisclosed) + * [page.isDragInterceptionEnabled()](#pageisdraginterceptionenabled) + * [page.isJavaScriptEnabled()](#pageisjavascriptenabled) + * [page.keyboard](#pagekeyboard) + * [page.mainFrame()](#pagemainframe) + * [page.metrics()](#pagemetrics) + * [page.mouse](#pagemouse) + * [page.pdf([options])](#pagepdfoptions) + * [page.queryObjects(prototypeHandle)](#pagequeryobjectsprototypehandle) + * [page.reload([options])](#pagereloadoptions) + * [page.screenshot([options])](#pagescreenshotoptions) + * [page.select(selector, ...values)](#pageselectselector-values) + * [page.setBypassCSP(enabled)](#pagesetbypasscspenabled) + * [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled) + * [page.setContent(html[, options])](#pagesetcontenthtml-options) + * [page.setCookie(...cookies)](#pagesetcookiecookies) + * [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) + * [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) + * [page.setDragInterception(enabled)](#pagesetdraginterceptionenabled) + * [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) + * [page.setGeolocation(options)](#pagesetgeolocationoptions) + * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) + * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled) + * [page.setRequestInterception(value)](#pagesetrequestinterceptionvalue) + - [Multiple Intercept Handlers and Asynchronous Resolutions](#multiple-intercept-handlers-and-asynchronous-resolutions) + - [Cooperative Intercept Mode](#cooperative-intercept-mode) + - [Cooperative Request Continuation](#cooperative-request-continuation) + - [Upgrading to Cooperative Intercept Mode for package maintainers](#upgrading-to-cooperative-intercept-mode-for-package-maintainers) + * [page.setUserAgent(userAgent[, userAgentMetadata])](#pagesetuseragentuseragent-useragentmetadata) + * [page.setViewport(viewport)](#pagesetviewportviewport) + * [page.tap(selector)](#pagetapselector) + * [page.target()](#pagetarget) + * [page.title()](#pagetitle) + * [page.touchscreen](#pagetouchscreen) + * [page.tracing](#pagetracing) + * [page.type(selector, text[, options])](#pagetypeselector-text-options) + * [page.url()](#pageurl) + * [page.viewport()](#pageviewport) + * [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) + * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) + * [page.waitForFrame(urlOrPredicate[, options])](#pagewaitforframeurlorpredicate-options) + * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) + * [page.waitForNavigation([options])](#pagewaitfornavigationoptions) + * [page.waitForNetworkIdle([options])](#pagewaitfornetworkidleoptions) + * [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) + * [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) + * [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) + * [page.waitForTimeout(milliseconds)](#pagewaitfortimeoutmilliseconds) + * [page.waitForXPath(xpath[, options])](#pagewaitforxpathxpath-options) + * [page.workers()](#pageworkers) + * [GeolocationOptions](#geolocationoptions) + * [WaitTimeoutOptions](#waittimeoutoptions) +- [class: WebWorker](#class-webworker) + * [webWorker.evaluate(pageFunction[, ...args])](#webworkerevaluatepagefunction-args) + * [webWorker.evaluateHandle(pageFunction[, ...args])](#webworkerevaluatehandlepagefunction-args) + * [webWorker.executionContext()](#webworkerexecutioncontext) + * [webWorker.url()](#webworkerurl) +- [class: Accessibility](#class-accessibility) + * [accessibility.snapshot([options])](#accessibilitysnapshotoptions) +- [class: Keyboard](#class-keyboard) + * [keyboard.down(key[, options])](#keyboarddownkey-options) + * [keyboard.press(key[, options])](#keyboardpresskey-options) + * [keyboard.sendCharacter(char)](#keyboardsendcharacterchar) + * [keyboard.type(text[, options])](#keyboardtypetext-options) + * [keyboard.up(key)](#keyboardupkey) +- [class: Mouse](#class-mouse) + * [mouse.click(x, y[, options])](#mouseclickx-y-options) + * [mouse.down([options])](#mousedownoptions) + * [mouse.drag(start, target)](#mousedragstart-target) + * [mouse.dragAndDrop(start, target[, options])](#mousedraganddropstart-target-options) + * [mouse.dragEnter(target, data)](#mousedragentertarget-data) + * [mouse.dragOver(target, data)](#mousedragovertarget-data) + * [mouse.drop(target, data)](#mousedroptarget-data) + * [mouse.move(x, y[, options])](#mousemovex-y-options) + * [mouse.up([options])](#mouseupoptions) + * [mouse.wheel([options])](#mousewheeloptions) +- [class: Touchscreen](#class-touchscreen) + * [touchscreen.tap(x, y)](#touchscreentapx-y) +- [class: Tracing](#class-tracing) + * [tracing.start([options])](#tracingstartoptions) + * [tracing.stop()](#tracingstop) +- [class: FileChooser](#class-filechooser) + * [fileChooser.accept(filePaths)](#filechooseracceptfilepaths) + * [fileChooser.cancel()](#filechoosercancel) + * [fileChooser.isMultiple()](#filechooserismultiple) +- [class: Dialog](#class-dialog) + * [dialog.accept([promptText])](#dialogacceptprompttext) + * [dialog.defaultValue()](#dialogdefaultvalue) + * [dialog.dismiss()](#dialogdismiss) + * [dialog.message()](#dialogmessage) + * [dialog.type()](#dialogtype) +- [class: ConsoleMessage](#class-consolemessage) + * [consoleMessage.args()](#consolemessageargs) + * [consoleMessage.location()](#consolemessagelocation) + * [consoleMessage.stackTrace()](#consolemessagestacktrace) + * [consoleMessage.text()](#consolemessagetext) + * [consoleMessage.type()](#consolemessagetype) +- [class: Frame](#class-frame) + * [frame.$(selector)](#frameselector) + * [frame.$$(selector)](#frameselector-1) + * [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + * [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args-1) + * [frame.$x(expression)](#framexexpression) + * [frame.addScriptTag(options)](#frameaddscripttagoptions) + * [frame.addStyleTag(options)](#frameaddstyletagoptions) + * [frame.childFrames()](#framechildframes) + * [frame.click(selector[, options])](#frameclickselector-options) + * [frame.content()](#framecontent) + * [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args) + * [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args) + * [frame.executionContext()](#frameexecutioncontext) + * [frame.focus(selector)](#framefocusselector) + * [frame.goto(url[, options])](#framegotourl-options) + * [frame.hover(selector)](#framehoverselector) + * [frame.isDetached()](#frameisdetached) + * [frame.isOOPFrame()](#frameisoopframe) + * [frame.name()](#framename) + * [frame.parentFrame()](#frameparentframe) + * [frame.select(selector, ...values)](#frameselectselector-values) + * [frame.setContent(html[, options])](#framesetcontenthtml-options) + * [frame.tap(selector)](#frametapselector) + * [frame.title()](#frametitle) + * [frame.type(selector, text[, options])](#frametypeselector-text-options) + * [frame.url()](#frameurl) + * [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) + * [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) + * [frame.waitForNavigation([options])](#framewaitfornavigationoptions) + * [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) + * [frame.waitForTimeout(milliseconds)](#framewaitfortimeoutmilliseconds) + * [frame.waitForXPath(xpath[, options])](#framewaitforxpathxpath-options) +- [class: ExecutionContext](#class-executioncontext) + * [executionContext.evaluate(pageFunction[, ...args])](#executioncontextevaluatepagefunction-args) + * [executionContext.evaluateHandle(pageFunction[, ...args])](#executioncontextevaluatehandlepagefunction-args) + * [executionContext.frame()](#executioncontextframe) + * [executionContext.queryObjects(prototypeHandle)](#executioncontextqueryobjectsprototypehandle) +- [class: JSHandle](#class-jshandle) + * [jsHandle.asElement()](#jshandleaselement) + * [jsHandle.dispose()](#jshandledispose) + * [jsHandle.evaluate(pageFunction[, ...args])](#jshandleevaluatepagefunction-args) + * [jsHandle.evaluateHandle(pageFunction[, ...args])](#jshandleevaluatehandlepagefunction-args) + * [jsHandle.executionContext()](#jshandleexecutioncontext) + * [jsHandle.getProperties()](#jshandlegetproperties) + * [jsHandle.getProperty(propertyName)](#jshandlegetpropertypropertyname) + * [jsHandle.jsonValue()](#jshandlejsonvalue) +- [class: ElementHandle](#class-elementhandle) + * [elementHandle.$(selector)](#elementhandleselector) + * [elementHandle.$$(selector)](#elementhandleselector-1) + * [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) + * [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args-1) + * [elementHandle.$x(expression)](#elementhandlexexpression) + * [elementHandle.asElement()](#elementhandleaselement) + * [elementHandle.boundingBox()](#elementhandleboundingbox) + * [elementHandle.boxModel()](#elementhandleboxmodel) + * [elementHandle.click([options])](#elementhandleclickoptions) + * [elementHandle.clickablePoint([offset])](#elementhandleclickablepointoffset) + * [elementHandle.contentFrame()](#elementhandlecontentframe) + * [elementHandle.dispose()](#elementhandledispose) + * [elementHandle.drag(target)](#elementhandledragtarget) + * [elementHandle.dragAndDrop(target[, options])](#elementhandledraganddroptarget-options) + * [elementHandle.dragEnter([data])](#elementhandledragenterdata) + * [elementHandle.dragOver([data])](#elementhandledragoverdata) + * [elementHandle.drop([data])](#elementhandledropdata) + * [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args) + * [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args) + * [elementHandle.executionContext()](#elementhandleexecutioncontext) + * [elementHandle.focus()](#elementhandlefocus) + * [elementHandle.getProperties()](#elementhandlegetproperties) + * [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname) + * [elementHandle.hover()](#elementhandlehover) + * [elementHandle.isIntersectingViewport([options])](#elementhandleisintersectingviewportoptions) + * [elementHandle.jsonValue()](#elementhandlejsonvalue) + * [elementHandle.press(key[, options])](#elementhandlepresskey-options) + * [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) + * [elementHandle.select(...values)](#elementhandleselectvalues) + * [elementHandle.tap()](#elementhandletap) + * [elementHandle.toString()](#elementhandletostring) + * [elementHandle.type(text[, options])](#elementhandletypetext-options) + * [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) + * [elementHandle.waitForSelector(selector[, options])](#elementhandlewaitforselectorselector-options) + * [elementHandle.waitForXPath(xpath[, options])](#elementhandlewaitforxpathxpath-options) +- [class: HTTPRequest](#class-httprequest) + * [httpRequest.abort([errorCode], [priority])](#httprequestaborterrorcode-priority) + * [httpRequest.abortErrorReason()](#httprequestaborterrorreason) + * [httpRequest.continue([overrides], [priority])](#httprequestcontinueoverrides-priority) + * [httpRequest.continueRequestOverrides()](#httprequestcontinuerequestoverrides) + * [httpRequest.enqueueInterceptAction(pendingHandler)](#httprequestenqueueinterceptactionpendinghandler) + * [httpRequest.failure()](#httprequestfailure) + * [httpRequest.finalizeInterceptions()](#httprequestfinalizeinterceptions) + * [httpRequest.frame()](#httprequestframe) + * [httpRequest.headers()](#httprequestheaders) + * [httpRequest.initiator()](#httprequestinitiator) + * [httpRequest.interceptResolutionState()](#httprequestinterceptresolutionstate) + * [httpRequest.isInterceptResolutionHandled()](#httprequestisinterceptresolutionhandled) + * [httpRequest.isNavigationRequest()](#httprequestisnavigationrequest) + * [httpRequest.method()](#httprequestmethod) + * [httpRequest.postData()](#httprequestpostdata) + * [httpRequest.redirectChain()](#httprequestredirectchain) + * [httpRequest.resourceType()](#httprequestresourcetype) + * [httpRequest.respond(response, [priority])](#httprequestrespondresponse-priority) + * [httpRequest.response()](#httprequestresponse) + * [httpRequest.responseForRequest()](#httprequestresponseforrequest) + * [httpRequest.url()](#httprequesturl) +- [class: HTTPResponse](#class-httpresponse) + * [httpResponse.buffer()](#httpresponsebuffer) + * [httpResponse.frame()](#httpresponseframe) + * [httpResponse.fromCache()](#httpresponsefromcache) + * [httpResponse.fromServiceWorker()](#httpresponsefromserviceworker) + * [httpResponse.headers()](#httpresponseheaders) + * [httpResponse.json()](#httpresponsejson) + * [httpResponse.ok()](#httpresponseok) + * [httpResponse.remoteAddress()](#httpresponseremoteaddress) + * [httpResponse.request()](#httpresponserequest) + * [httpResponse.securityDetails()](#httpresponsesecuritydetails) + * [httpResponse.status()](#httpresponsestatus) + * [httpResponse.statusText()](#httpresponsestatustext) + * [httpResponse.text()](#httpresponsetext) + * [httpResponse.timing()](#httpresponsetiming) + * [httpResponse.url()](#httpresponseurl) +- [class: SecurityDetails](#class-securitydetails) + * [securityDetails.issuer()](#securitydetailsissuer) + * [securityDetails.protocol()](#securitydetailsprotocol) + * [securityDetails.subjectAlternativeNames()](#securitydetailssubjectalternativenames) + * [securityDetails.subjectName()](#securitydetailssubjectname) + * [securityDetails.validFrom()](#securitydetailsvalidfrom) + * [securityDetails.validTo()](#securitydetailsvalidto) +- [class: Target](#class-target) + * [target.browser()](#targetbrowser) + * [target.browserContext()](#targetbrowsercontext) + * [target.createCDPSession()](#targetcreatecdpsession) + * [target.opener()](#targetopener) + * [target.page()](#targetpage) + * [target.type()](#targettype) + * [target.url()](#targeturl) + * [target.worker()](#targetworker) +- [class: CDPSession](#class-cdpsession) + * [cdpSession.connection()](#cdpsessionconnection) + * [cdpSession.detach()](#cdpsessiondetach) + * [cdpSession.id()](#cdpsessionid) + * [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs) +- [class: Coverage](#class-coverage) + * [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions) + * [coverage.startJSCoverage([options])](#coveragestartjscoverageoptions) + * [coverage.stopCSSCoverage()](#coveragestopcsscoverage) + * [coverage.stopJSCoverage()](#coveragestopjscoverage) +- [class: TimeoutError](#class-timeouterror) +- [class: EventEmitter](#class-eventemitter) + * [eventEmitter.addListener(event, handler)](#eventemitteraddlistenerevent-handler) + * [eventEmitter.emit(event, [eventData])](#eventemitteremitevent-eventdata) + * [eventEmitter.listenerCount(event)](#eventemitterlistenercountevent) + * [eventEmitter.off(event, handler)](#eventemitteroffevent-handler) + * [eventEmitter.on(event, handler)](#eventemitteronevent-handler) + * [eventEmitter.once(event, handler)](#eventemitteronceevent-handler) + * [eventEmitter.removeAllListeners([event])](#eventemitterremovealllistenersevent) + * [eventEmitter.removeListener(event, handler)](#eventemitterremovelistenerevent-handler) +- [interface: CustomQueryHandler](#interface-customqueryhandler) +- [interface: Selector](#interface-selector) + + + + +### Overview + +Puppeteer is a Node library which provides a high-level API to control Chromium or Chrome over the DevTools Protocol. + +The Puppeteer API is hierarchical and mirrors the browser structure. + +> **NOTE** On the following diagram, faded entities are not currently represented in Puppeteer. + +![puppeteer overview](https://user-images.githubusercontent.com/81942/86137523-ab2ba080-baed-11ea-9d4b-30eda784585a.png) + +- [`Puppeteer`](#class-puppeteer) communicates with the browser using [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- [`Browser`](#class-browser) instance can own multiple browser contexts. +- [`BrowserContext`](#class-browsercontext) instance defines a browsing session and can own multiple pages. +- [`Page`](#class-page) has at least one frame: main frame. There might be other frames created by [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) or [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/frame) tags. +- [`Frame`](#class-frame) has at least one execution context - the default execution context - where the frame's JavaScript is executed. A Frame might have additional execution contexts that are associated with [extensions](https://developer.chrome.com/extensions). +- [`WebWorker`](#class-webworker) has a single execution context and facilitates interacting with [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). + +(Diagram source: [link](https://docs.google.com/drawings/d/1Q_AM6KYs9kbyLZF-Lpp5mtpAWth73Cq8IKCsWYgi8MM/edit?usp=sharing)) + +### puppeteer vs puppeteer-core + +Every release since v1.7.0 we publish two packages: + +- [puppeteer](https://www.npmjs.com/package/puppeteer) +- [puppeteer-core](https://www.npmjs.com/package/puppeteer-core) + +`puppeteer` is a _product_ for browser automation. When installed, it downloads a version of +Chromium, which it then drives using `puppeteer-core`. Being an end-user product, `puppeteer` supports a bunch of convenient `PUPPETEER_*` env variables to tweak its behavior. + +`puppeteer-core` is a _library_ to help drive anything that supports DevTools protocol. `puppeteer-core` doesn't download Chromium when installed. Being a library, `puppeteer-core` is fully driven +through its programmatic interface and disregards all the `PUPPETEER_*` env variables. + +To sum up, the only differences between `puppeteer-core` and `puppeteer` are: + +- `puppeteer-core` doesn't automatically download Chromium when installed. +- `puppeteer-core` ignores all `PUPPETEER_*` env variables. + +In most cases, you'll be fine using the `puppeteer` package. + +However, you should use `puppeteer-core` if: + +- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `puppeteer-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space. +- you're bundling Puppeteer to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary. +- you're building a set of tools where `puppeteer-core` is one of the ingredients and you want to postpone `install.js` script execution until Chromium is about to be used. + +When using `puppeteer-core`, remember to change the _include_ line: + +```js +const puppeteer = require('puppeteer-core'); +``` + +You will then need to call [`puppeteer.connect([options])`](#puppeteerconnectoptions) or [`puppeteer.launch([options])`](#puppeteerlaunchoptions) with an explicit `executablePath` or `channel` option. + +### Environment Variables + +Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. +If Puppeteer doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config). + +- `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run the browser. +- `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PUPPETEER_TMP_DIR` - defines the directory to be used by Puppeteer for creating temporary files. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir). +- `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`. +- `PUPPETEER_DOWNLOAD_PATH` - overwrite the path for the downloads folder. Defaults to `/.local-chromium`, where `` is Puppeteer's package root. +- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in `puppeteer.launch`. See [puppeteer.launch([options])](#puppeteerlaunchoptions) on how the executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. +- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This can also be used during installation to fetch the recommended browser binary. Setting `product` programmatically in [puppeteer.launch([options])](#puppeteerlaunchoptions) supersedes this environment variable. The product is exposed in [`puppeteer.product`](#puppeteerproduct) +- `PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM` — specify Puppeteer download Chromium for Apple M1. On Apple M1 devices Puppeteer by default downloads the version for Intel's processor which runs via Rosetta. It works without any problems, however, with this option, you should get more efficient resource usage (CPU and RAM) that could lead to a faster execution time. **BEWARE**: it's an experimental option that makes sense only if you have an Apple M1 device, use at your own risk. + +> **NOTE** `PUPPETEER_*` env variables are not accounted for in the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package. + +### Working with Chrome Extensions + +Puppeteer can be used for testing Chrome Extensions. + +> **NOTE** Extensions in Chrome / Chromium currently only work in non-headless mode and experimental Chrome headless mode. + +The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of an extension whose source is located in `./my-extension`: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const pathToExtension = require('path').join(__dirname, 'my-extension'); + const browser = await puppeteer.launch({ + headless: 'chrome', + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + const backgroundPageTarget = await browser.waitForTarget( + (target) => target.type() === 'background_page' + ); + const backgroundPage = await backgroundPageTarget.page(); + // Test the background page as you would any other page. + await browser.close(); +})(); +``` + +> **NOTE** Chrome Manifest V3 extensions have a background ServiceWorker of type 'service_worker', instead of a page of type 'background_page'. + +> **NOTE** It is not yet possible to test extension popups or content scripts. + +### class: Puppeteer + +Puppeteer module provides a method to launch a Chromium instance. +The following is a typical example of using Puppeteer to drive automation: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.clearCustomQueryHandlers() + +Clears all registered handlers. + +#### puppeteer.connect(options) + +- `options` <[Object]> + - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserURL` a browser URL to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Puppeteer fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as DPR). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Puppeteer to use. + - `product` <[string]> Possible values are: `chrome`, `firefox`. Defaults to `chrome`. + - `targetFilter` Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. +- returns: <[Promise]<[Browser]>> + +This methods attaches Puppeteer to an existing browser instance. + +#### puppeteer.createBrowserFetcher([options]) + +- `options` <[Object]> + - `host` <[string]> A download host to be used. Defaults to `https://storage.googleapis.com`. If the `product` is `firefox`, this defaults to `https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central`. + - `path` <[string]> A path for the downloads folder. Defaults to `/.local-chromium`, where `` is Puppeteer's package root. If the `product` is `firefox`, this defaults to `/.local-firefox`. + - `platform` <"linux"|"mac"|"win32"|"win64"> [string] for the current platform. Possible values are: `mac`, `win32`, `win64`, `linux`. Defaults to the current platform. + - `product` <"chrome"|"firefox"> [string] for the product to run. Possible values are: `chrome`, `firefox`. Defaults to `chrome`. +- returns: <[BrowserFetcher]> + +#### puppeteer.customQueryHandlerNames() + +- returns: <[Array]> A list with the names of all registered custom query handlers. + +#### puppeteer.defaultArgs([options]) + +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `headless` <[boolean]|"chrome"> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. "chrome" is a new experimental headless mode (use at your own risk). + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md). + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + - `debuggingPort` <[number]> Specify custom debugging port. Pass `0` to discover a random port. Defaults to `0`. +- returns: <[Array]<[string]>> + +The default flags that Chromium will be launched with. + +#### puppeteer.devices + +- returns: <[Object]> + +Returns a list of devices to be used with [`page.emulate(options)`](#pageemulateoptions). Actual list of +devices can be found in [`src/common/DeviceDescriptors.ts`](https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts). + +```js +const puppeteer = require('puppeteer'); +const iPhone = puppeteer.devices['iPhone 6']; + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulate(iPhone); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.errors + +- returns: <[Object]> + - `TimeoutError` <[function]> A class of [TimeoutError]. + +Puppeteer methods might throw errors if they are unable to fulfill a request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +might fail if the selector doesn't match any nodes during the given timeframe. + +For certain types of errors Puppeteer uses specific error classes. +These classes are available via [`puppeteer.errors`](#puppeteererrors) + +An example of handling a timeout error: + +```js +try { + await page.waitForSelector('.foo'); +} catch (e) { + if (e instanceof puppeteer.errors.TimeoutError) { + // Do something if this is a timeout. + } +} +``` + +> **NOTE** The old way (Puppeteer versions <= v1.14.0) errors can be obtained with `require('puppeteer/Errors')`. + +#### puppeteer.executablePath() + +- returns: <[string]> A path where Puppeteer expects to find the bundled browser. The browser binary might not be there if the download was skipped with [`PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`](#environment-variables). + +> **NOTE** `puppeteer.executablePath()` is affected by the `PUPPETEER_EXECUTABLE_PATH` and `PUPPETEER_CHROMIUM_REVISION` env variables. See [Environment Variables](#environment-variables) for details. + +#### puppeteer.launch([options]) + +- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: + - `product` <[string]> Which browser to launch. At this time, this is either `chrome` or `firefox`. See also `PUPPETEER_PRODUCT`. + - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `headless` <[boolean]|"chrome"> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. "chrome" is a new experimental headless mode (use at your own risk). + - `channel` <[string]> When specified, Puppeteer will search for the locally installed release channel of Google Chrome and use it to launch. Available values are `chrome`, `chrome-beta`, `chrome-canary`, `chrome-dev`. When channel is specified, `executablePath` cannot be specified. + - `executablePath` <[string]> Path to a browser executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + - `slowMo` <[number]> Slows down Puppeteer operations by the specified amount of milliseconds. Useful so that you can see what is going on. + - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as DPR). Defaults to `1`. + - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/), and here is the list of [Firefox flags](https://wiki.mozilla.org/Firefox/CommandLineOptions). + - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`puppeteer.defaultArgs()`](#puppeteerdefaultargsoptions). If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. + - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. + - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. + - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. + - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. + - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`. + - `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md). + - `debuggingPort` <[number]> Specify custom debugging port. Pass `0` to discover a random port. Defaults to `0`. + - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. + - `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + - `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`. + - `extraPrefsFirefox` <[Object]> Additional [preferences](https://searchfox.org/mozilla-release/source/modules/libpref/init/all.js) that can be passed to Firefox (see `PUPPETEER_PRODUCT`) + - `targetFilter` Use this function to decide if Puppeteer should connect to the given target. If a `targetFilter` is provided, Puppeteer only connects to targets for which `targetFilter` returns `true`. By default, Puppeteer connects to all available targets. + - `waitForInitialPage` <[boolean]> Whether to wait for the initial page to be ready. Defaults to `true`. +- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance. + +You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: + +```js +const browser = await puppeteer.launch({ + ignoreDefaultArgs: ['--mute-audio'], +}); +``` + +> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` or `channel` option with extreme caution. +> +> If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. +> +> In [puppeteer.launch([options])](#puppeteerlaunchoptions) above, any mention of Chromium also applies to Chrome. +> +> See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +#### puppeteer.networkConditions + +- returns: <[Object]> + +Returns a list of network conditions to be used with [`page.emulateNetworkConditions(networkConditions)`](#pageemulatenetworkconditionsnetworkconditions). Actual list of +conditions can be found in [`src/common/NetworkConditions.ts`](https://github.com/puppeteer/puppeteer/blob/main/src/common/NetworkConditions.ts). + +```js +const puppeteer = require('puppeteer'); +const slow3G = puppeteer.networkConditions['Slow 3G']; + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulateNetworkConditions(slow3G); + await page.goto('https://www.google.com'); + // other actions... + await browser.close(); +})(); +``` + +#### puppeteer.product + +- returns: <[string]> returns the name of the browser that is under automation (`"chrome"` or `"firefox"`) + +The product is set by the `PUPPETEER_PRODUCT` environment variable or the `product` option in [puppeteer.launch([options])](#puppeteerlaunchoptions) and defaults to `chrome`. Firefox support is experimental and requires to install Puppeteer via `PUPPETEER_PRODUCT=firefox npm i puppeteer`. + +#### puppeteer.registerCustomQueryHandler(name, queryHandler) + +- `name` <[string]> The name that the custom query handler will be registered under. +- `queryHandler` <[CustomQueryHandler]> The [custom query handler](#interface-customqueryhandler) to register. + +Registers a [custom query handler](#interface-customqueryhandler). + +Example: + +```js +puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return element.querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return element.querySelectorAll(`.${selector}`); + }, +}); +const aHandle = await page.$('getByClass/…'); +``` + +#### puppeteer.unregisterCustomQueryHandler(name) + +- `name` <[string]> The name of the query handler to unregister. + +### class: BrowserFetcher + +BrowserFetcher can download and manage different versions of Chromium and Firefox. + +BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/). + +In the Firefox case, BrowserFetcher downloads Firefox Nightly and operates on version numbers such as `"75"`. + +An example of using BrowserFetcher to download a specific version of Chromium and running +Puppeteer against it: + +```js +const browserFetcher = puppeteer.createBrowserFetcher(); +const revisionInfo = await browserFetcher.download('533271'); +const browser = await puppeteer.launch({ + executablePath: revisionInfo.executablePath, +}); +``` + +> **NOTE** BrowserFetcher is not designed to work concurrently with other +> instances of BrowserFetcher that share the same downloads directory. + +#### browserFetcher.canDownload(revision) + +- `revision` <[string]> a revision to check availability. +- returns: <[Promise]<[boolean]>> returns `true` if the revision could be downloaded from the host. + +The method initiates a HEAD request to check if the revision is available. + +#### browserFetcher.download(revision[, progressCallback]) + +- `revision` <[string]> a revision to download. +- `progressCallback` <[function]([number], [number])> A function that will be called with two arguments: + - `downloadedBytes` <[number]> how many bytes have been downloaded + - `totalBytes` <[number]> how large is the total download +- returns: <[Promise]<[Object]>> Resolves with revision information when the revision is downloaded and extracted + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + +The method initiates a GET request to download the revision from the host. + +#### browserFetcher.host() + +- returns: <[string]> The download host being used. + +#### browserFetcher.localRevisions() + +- returns: <[Promise]<[Array]<[string]>>> A list of all revisions (for the current `product`) available locally on disk. + +#### browserFetcher.platform() + +- returns: <[string]> One of `mac`, `linux`, `win32` or `win64`. + +#### browserFetcher.product() + +- returns: <[string]> One of `chrome` or `firefox`. + +#### browserFetcher.remove(revision) + +- `revision` <[string]> a revision to remove for the current `product`. The method will throw if the revision has not been downloaded. +- returns: <[Promise]> Resolves when the revision has been removed. + +#### browserFetcher.revisionInfo(revision) + +- `revision` <[string]> a revision to get info for. +- returns: <[Object]> + - `revision` <[string]> the revision the info was created from + - `folderPath` <[string]> path to the extracted revision folder + - `executablePath` <[string]> path to the revision executable + - `url` <[string]> URL this revision can be downloaded from + - `local` <[boolean]> whether the revision is locally available on disk + - `product` <[string]> one of `chrome` or `firefox` + +> **NOTE** Many BrowserFetcher methods, like `remove` and `revisionInfo` +> are affected by the choice of `product`. See [puppeteer.createBrowserFetcher([options])](#puppeteercreatebrowserfetcheroptions). + +### class: Browser + +- extends: [EventEmitter](#class-eventemitter) + +A Browser is created when Puppeteer connects to a Chromium instance, either through [`puppeteer.launch`](#puppeteerlaunchoptions) or [`puppeteer.connect`](#puppeteerconnectoptions). + +An example of using a [Browser] to create a [Page]: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await browser.close(); +})(); +``` + +An example of disconnecting from and reconnecting to a [Browser]: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + // Store the endpoint to be able to reconnect to Chromium + const browserWSEndpoint = browser.wsEndpoint(); + // Disconnect puppeteer from Chromium + browser.disconnect(); + + // Use the endpoint to reestablish a connection + const browser2 = await puppeteer.connect({ browserWSEndpoint }); + // Close Chromium + await browser2.close(); +})(); +``` + +#### event: 'disconnected' + +Emitted when Puppeteer gets disconnected from the Chromium instance. This might happen because of one of the following: + +- Chromium is closed or crashed +- The [`browser.disconnect`](#browserdisconnect) method was called + +#### event: 'targetchanged' + +- <[Target]> + +Emitted when the URL of a target changes. + +> **NOTE** This includes target changes in incognito browser contexts. + +#### event: 'targetcreated' + +- <[Target]> + +Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browser.newPage`](#browsernewpage). + +> **NOTE** This includes target creations in incognito browser contexts. + +#### event: 'targetdestroyed' + +- <[Target]> + +Emitted when a target is destroyed, for example when a page is closed. + +> **NOTE** This includes target destructions in incognito browser contexts. + +#### browser.browserContexts() + +- returns: <[Array]<[BrowserContext]>> + +Returns an array of all open browser contexts. In a newly created browser, this will return +a single instance of [BrowserContext]. + +#### browser.close() + +- returns: <[Promise]> + +Closes Chromium and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore. + +During the process of closing the browser, Puppeteer attempts to delete the temp folder created exclusively for this browser instance. If this fails (either because a file in the temp folder is locked by another process or because of insufficient permissions) an error is logged. This implies that: a) the folder and/or its content is not fully deleted; and b) the connection with the browser is not properly disposed (see [browser.disconnect()](#browserdisconnect)). + +#### browser.createIncognitoBrowserContext([options]) + +- `options` <[Object]> Set of configurable options to set on the browserContext. Can have the following fields: + - `proxyServer` <[string]> Optional proxy server with optional port to use for all requests. Username and password can be set in [page.authenticate(credentials)](#pageauthenticatecredentials). + - `proxyBypassList` <[string]> Optional: Bypass the proxy for the given semi-colon-separated list of hosts. +- returns: <[Promise]<[BrowserContext]>> + +Creates a new incognito browser context. This won't share cookies/cache with other browser contexts. + +```js +(async () => { + const browser = await puppeteer.launch(); + // Create a new incognito browser context. + const context = await browser.createIncognitoBrowserContext(); + // Create a new page in a pristine context. + const page = await context.newPage(); + // Do stuff + await page.goto('https://example.com'); +})(); +``` + +#### browser.defaultBrowserContext() + +- returns: <[BrowserContext]> + +Returns the default browser context. The default browser context can not be closed. + +#### browser.disconnect() + +Disconnects Puppeteer from the browser but leaves the Chromium process running. After calling `disconnect`, the [Browser] object is considered disposed and cannot be used anymore. + +#### browser.isConnected() + +- returns: <[boolean]> + +Indicates that the browser is connected. + +#### browser.newPage() + +- returns: <[Promise]<[Page]>> + +Promise which resolves to a new [Page] object. The [Page] is created in a default browser context. + +#### browser.pages() + +- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). + +An array of all pages inside the Browser. In case of multiple browser contexts, +the method will return an array with all the pages in all browser contexts. + +#### browser.process() + +- returns: Spawned browser process. Returns `null` if the browser instance was created with [`puppeteer.connect`](#puppeteerconnectoptions) method. + +#### browser.target() + +- returns: <[Target]> + +A target associated with the browser. + +#### browser.targets() + +- returns: <[Array]<[Target]>> + +An array of all active targets inside the Browser. In case of multiple browser contexts, +the method will return an array with all the targets in all browser contexts. + +#### browser.userAgent() + +- returns: <[Promise]<[string]>> Promise which resolves to the browser's original user agent. + +> **NOTE** Pages can override browser user agent with [page.setUserAgent](#pagesetuseragentuseragent-useragentmetadata) + +#### browser.version() + +- returns: <[Promise]<[string]>> For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`. + +> **NOTE** the format of browser.version() might change with future releases of Chromium. + +#### browser.waitForTarget(predicate[, options]) + +- `predicate` <[function]\([Target]\):[boolean]|[Promise]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in all browser contexts. + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browser.waitForTarget( + (target) => target.url() === 'https://www.example.com/' +); +``` + +#### browser.wsEndpoint() + +- returns: <[string]> Browser websocket URL. + +Browser websocket endpoint which can be used as an argument to +[puppeteer.connect](#puppeteerconnectoptions). The format is `ws://${host}:${port}/devtools/browser/` + +You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. Learn more about the [devtools protocol](https://chromedevtools.github.io/devtools-protocol) and the [browser endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + +### class: BrowserContext + +- extends: [EventEmitter](#class-eventemitter) + +BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has +a single BrowserContext used by default. The method `browser.newPage()` creates a page in the default browser context. + +If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser +context. + +Puppeteer allows creation of "incognito" browser contexts with `browser.createIncognitoBrowserContext()` method. +"Incognito" browser contexts don't write any browsing data to disk. + +```js +// Create a new incognito browser context +const context = await browser.createIncognitoBrowserContext(); +// Create a new page inside context. +const page = await context.newPage(); +// ... do stuff with page ... +await page.goto('https://example.com'); +// Dispose context once it's no longer needed. +await context.close(); +``` + +#### event: 'targetchanged' + +- <[Target]> + +Emitted when the URL of a target inside the browser context changes. + +#### event: 'targetcreated' + +- <[Target]> + +Emitted when a new target is created inside the browser context, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage). + +#### event: 'targetdestroyed' + +- <[Target]> + +Emitted when a target inside the browser context is destroyed, for example when a page is closed. + +#### browserContext.browser() + +- returns: <[Browser]> + +The browser this browser context belongs to. + +#### browserContext.clearPermissionOverrides() + +- returns: <[Promise]> + +Clears all permission overrides for the browser context. + +```js +const context = browser.defaultBrowserContext(); +context.overridePermissions('https://example.com', ['clipboard-read']); +// do stuff .. +context.clearPermissionOverrides(); +``` + +#### browserContext.close() + +- returns: <[Promise]> + +Closes the browser context. All the targets that belong to the browser context +will be closed. + +> **NOTE** only incognito browser contexts can be closed. + +#### browserContext.isIncognito() + +- returns: <[boolean]> + +Returns whether BrowserContext is incognito. +The default browser context is the only non-incognito browser context. + +> **NOTE** the default browser context cannot be closed. + +#### browserContext.newPage() + +- returns: <[Promise]<[Page]>> + +Creates a new page in the browser context. + +#### browserContext.overridePermissions(origin, permissions) + +- `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". +- `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: + - `'geolocation'` + - `'midi'` + - `'midi-sysex'` (system-exclusive midi) + - `'notifications'` + - `'push'` + - `'camera'` + - `'microphone'` + - `'background-sync'` + - `'ambient-light-sensor'` + - `'accelerometer'` + - `'gyroscope'` + - `'magnetometer'` + - `'accessibility-events'` + - `'clipboard-read'` + - `'clipboard-write'` + - `'payment-handler'` + - `'persistent-storage'` +- returns: <[Promise]> + +```js +const context = browser.defaultBrowserContext(); +await context.overridePermissions('https://html5demos.com', ['geolocation']); +``` + +#### browserContext.pages() + +- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). + +An array of all pages inside the browser context. + +#### browserContext.targets() + +- returns: <[Array]<[Target]>> + +An array of all active targets inside the browser context. + +#### browserContext.waitForTarget(predicate[, options]) + +- `predicate` <[function]\([Target]\):[boolean]|[Promise]> A function to be run for every target +- `options` <[Object]> + - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. +- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. + +This searches for a target in this specific browser context. + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browserContext.waitForTarget( + (target) => target.url() === 'https://www.example.com/' +); +``` + +### class: Page + +- extends: [EventEmitter](#class-eventemitter) + +Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] instance might have multiple [Page] instances. + +This example creates a page, navigates it to a URL, and then saves a screenshot: + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'screenshot.png' }); + await browser.close(); +})(); +``` + +The Page class emits various events (described below) which can be handled using +any of the [`EventEmitter`](#class-eventemitter) methods, such as `on`, `once` +or `off`. + +This example logs a message for a single page `load` event: + +```js +page.once('load', () => console.log('Page loaded!')); +``` + +To unsubscribe from events use the `off` method: + +```js +function logRequest(interceptedRequest) { + console.log('A request was made:', interceptedRequest.url()); +} +page.on('request', logRequest); +// Sometime later... +page.off('request', logRequest); +``` + +#### event: 'close' + +Emitted when the page closes. + +#### event: 'console' + +- <[ConsoleMessage]> + +Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + +The arguments passed into `console.log` appear as arguments on the event handler. + +An example of handling `console` event: + +```js +page.on('console', (msg) => { + for (let i = 0; i < msg.args().length; ++i) + console.log(`${i}: ${msg.args()[i]}`); +}); +page.evaluate(() => console.log('hello', 5, { foo: 'bar' })); +``` + +#### event: 'dialog' + +- <[Dialog]> + +Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Puppeteer can respond to the dialog via [Dialog]'s [accept](#dialogacceptprompttext) or [dismiss](#dialogdismiss) methods. + +#### event: 'domcontentloaded' + +Emitted when the JavaScript [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched. + +#### event: 'error' + +- <[Error]> + +Emitted when the page crashes. + +> **NOTE** `error` event has a special meaning in Node, see [error events](https://nodejs.org/api/events.html#events_error_events) for details. + +#### event: 'frameattached' + +- <[Frame]> + +Emitted when a frame is attached. + +#### event: 'framedetached' + +- <[Frame]> + +Emitted when a frame is detached. + +#### event: 'framenavigated' + +- <[Frame]> + +Emitted when a frame is navigated to a new URL. + +#### event: 'load' + +Emitted when the JavaScript [`load`](https://developer.mozilla.org/en-US/docs/Web/Events/load) event is dispatched. + +#### event: 'metrics' + +- <[Object]> + - `title` <[string]> The title passed to `console.timeStamp`. + - `metrics` <[Object]> Object containing metrics as key/value pairs. The values + of metrics are of <[number]> type. + +Emitted when the JavaScript code makes a call to `console.timeStamp`. For the list +of metrics see `page.metrics`. + +#### event: 'pageerror' + +- <[Error]> The exception message + +Emitted when an uncaught exception happens within the page. + +#### event: 'popup' + +- <[Page]> Page corresponding to "popup" window + +Emitted when the page opens a new tab or window. + +```js +const [popup] = await Promise.all([ + new Promise((resolve) => page.once('popup', resolve)), + page.click('a[target=_blank]'), +]); +``` + +```js +const [popup] = await Promise.all([ + new Promise((resolve) => page.once('popup', resolve)), + page.evaluate(() => window.open('https://example.com')), +]); +``` + +#### event: 'request' + +- <[HTTPRequest]> + +Emitted when a page issues a request. The [HTTPRequest] object is read-only. +In order to intercept and mutate requests, see `page.setRequestInterception`. + +#### event: 'requestfailed' + +- <[HTTPRequest]> + +Emitted when a request fails, for example by timing out. + +> **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with [`'requestfinished'`](#event-requestfinished) event and not with [`'requestfailed'`](#event-requestfailed). + +#### event: 'requestfinished' + +- <[HTTPRequest]> + +Emitted when a request finishes successfully. + +#### event: 'response' + +- <[HTTPResponse]> + +Emitted when a [HTTPResponse] is received. + +#### event: 'workercreated' + +- <[WebWorker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the page. + +#### event: 'workerdestroyed' + +- <[WebWorker]> + +Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated. + +#### page.$(selector) + +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]> + +The method runs `document.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. + +Shortcut for [page.mainFrame().$(selector)](#frameselector). + +#### page.$$(selector) + +- `selector` <[string]> A [selector] to query page for +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method runs `document.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. + +Shortcut for [page.mainFrame().$$(selector)](#frameselector-1). + +#### page.$$eval(selector, pageFunction[, ...args]) + +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `Array.from(document.querySelectorAll(selector))` within the page and passes it as the first argument to `pageFunction`. + +If `pageFunction` returns a [Promise], then `page.$$eval` would wait for the promise to resolve and return its value. + +Examples: + +```js +const divCount = await page.$$eval('div', (divs) => divs.length); +``` + +```js +const options = await page.$$eval('div > span.options', (options) => + options.map((option) => option.textContent) +); +``` + +#### page.$eval(selector, pageFunction[, ...args]) + +- `selector` <[string]> A [selector] to query page for +- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context +- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` +- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction` + +This method runs `document.querySelector` within the page and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. + +If `pageFunction` returns a [Promise], then `page.$eval` would wait for the promise to resolve and return its value. + +Examples: + +```js +const searchValue = await page.$eval('#search', (el) => el.value); +const preloadHref = await page.$eval('link[rel=preload]', (el) => el.href); +const html = await page.$eval('.main-container', (e) => e.outerHTML); +``` + +Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). + +#### page.$x(expression) + +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method evaluates the XPath expression relative to the page document as its context node. If there are no such elements, the method resolves to an empty array. + +Shortcut for [page.mainFrame().$x(expression)](#framexexpression) + +#### page.accessibility + +- returns: <[Accessibility]> + +#### page.addScriptTag(options) + +- `options` <[Object]> + - `url` <[string]> URL of a script to be added. + - `path` <[string]> Path to the JavaScript file to be injected into frame. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). + - `content` <[string]> Raw JavaScript content to be injected into frame. + - `type` <[string]> Script type. Use 'module' in order to load a Javascript ES6 module. See [script](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) for more details. + - `id` <[string]> id attribute to add to the script tag. +- returns: <[Promise]<[ElementHandle]>> which resolves to the added tag when the script's onload fires or when the script content was injected into frame. + +Adds a ` + Accessible Name + + + + +
+
+
+
item1
+
item2
+
+
item3
+
+ +
+ ` + ); + }); + const getIds = async (elements: ElementHandle[]) => + Promise.all( + elements.map((element) => + element.evaluate((element: Element) => element.id) + ) + ); + it('should find by name "foo"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/foo'); + const ids = await getIds(found); + expect(ids).toEqual(['node3', 'node5', 'node6']); + }); + it('should find by name "bar"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/bar'); + const ids = await getIds(found); + expect(ids).toEqual(['node1', 'node2', 'node8']); + }); + it('should find treeitem by name', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/item1 item2 item3'); + const ids = await getIds(found); + expect(ids).toEqual(['node30']); + }); + it('should find by role "button"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/[role="button"]'); + const ids = await getIds(found); + expect(ids).toEqual([ + 'node5', + 'node6', + 'node7', + 'node8', + 'node10', + 'node21', + ]); + }); + it('should find by role "heading"', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/[role="heading"]'); + const ids = await getIds(found); + expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']); + }); + it('should find both ignored and unignored', async () => { + const { page } = getTestState(); + const found = await page.$$('aria/title'); + const ids = await getIds(found); + expect(ids).toEqual(['shown', 'hidden']); + }); + }); +}); diff --git a/test/assert-coverage-test.js b/test/assert-coverage-test.js new file mode 100644 index 0000000000000..28b25dab948e1 --- /dev/null +++ b/test/assert-coverage-test.js @@ -0,0 +1,25 @@ +const { describe, it } = require('mocha'); +const { getCoverageResults } = require('./coverage-utils.js'); +const expect = require('expect'); + +describe('API coverage test', () => { + it('calls every method', () => { + if (!process.env.COVERAGE) return; + + const coverageMap = getCoverageResults(); + const missingMethods = []; + for (const method of coverageMap.keys()) { + if (!coverageMap.get(method)) missingMethods.push(method); + } + if (missingMethods.length) { + console.error( + '\nCoverage check failed: not all API methods called. See above output for list of missing methods.' + ); + console.error(missingMethods.join('\n')); + } + + // We know this will fail because we checked above + // but we need the actual test to fail. + expect(missingMethods.length).toEqual(0); + }); +}); diff --git a/test/assets/beforeunload.html b/test/assets/beforeunload.html new file mode 100644 index 0000000000000..3cef6763f39f5 --- /dev/null +++ b/test/assets/beforeunload.html @@ -0,0 +1,10 @@ +
beforeunload demo.
+ + diff --git a/test/assets/cached/one-style-font.css b/test/assets/cached/one-style-font.css new file mode 100644 index 0000000000000..6178de0350e98 --- /dev/null +++ b/test/assets/cached/one-style-font.css @@ -0,0 +1,9 @@ +@font-face { + font-family: 'one-style'; + src: url('./one-style.woff') format('woff'); +} + +body { + background-color: pink; + font-family: 'one-style', sans-serif; +} diff --git a/test/assets/cached/one-style-font.html b/test/assets/cached/one-style-font.html new file mode 100644 index 0000000000000..8e7236dfb35e3 --- /dev/null +++ b/test/assets/cached/one-style-font.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/cached/one-style.css b/test/assets/cached/one-style.css new file mode 100644 index 0000000000000..04e7110b4142e --- /dev/null +++ b/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/cached/one-style.html b/test/assets/cached/one-style.html new file mode 100644 index 0000000000000..4760f2b9f7e37 --- /dev/null +++ b/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/chromium-linux.zip b/test/assets/chromium-linux.zip new file mode 100644 index 0000000000000..9c00ec080d0e9 Binary files /dev/null and b/test/assets/chromium-linux.zip differ diff --git a/test/assets/consolelog.html b/test/assets/consolelog.html new file mode 100644 index 0000000000000..4a27803aa9a28 --- /dev/null +++ b/test/assets/consolelog.html @@ -0,0 +1,17 @@ + + + + console.log test + + + + + diff --git a/test/assets/csp.html b/test/assets/csp.html new file mode 100644 index 0000000000000..34fc1fc1a5c41 --- /dev/null +++ b/test/assets/csp.html @@ -0,0 +1 @@ + diff --git a/test/assets/csscoverage/Dosis-Regular.ttf b/test/assets/csscoverage/Dosis-Regular.ttf new file mode 100644 index 0000000000000..4b208624e8c69 Binary files /dev/null and b/test/assets/csscoverage/Dosis-Regular.ttf differ diff --git a/test/assets/csscoverage/OFL.txt b/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000000000..a9b3c8b34eaac --- /dev/null +++ b/test/assets/csscoverage/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), +with Reserved Font Names "Dosis". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/assets/csscoverage/involved.html b/test/assets/csscoverage/involved.html new file mode 100644 index 0000000000000..bcd9845b937c8 --- /dev/null +++ b/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ + +
woof!
+fancy text + diff --git a/test/assets/csscoverage/media.html b/test/assets/csscoverage/media.html new file mode 100644 index 0000000000000..bfb89f8f75a25 --- /dev/null +++ b/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/multiple.html b/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000000000..0fd97e962a5a1 --- /dev/null +++ b/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ + + + diff --git a/test/assets/csscoverage/simple.html b/test/assets/csscoverage/simple.html new file mode 100644 index 0000000000000..3beae218293a7 --- /dev/null +++ b/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ + +
hello, world
+ diff --git a/test/assets/csscoverage/sourceurl.html b/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000000000..df4e9c276c770 --- /dev/null +++ b/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/csscoverage/stylesheet1.css b/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000000000..60f1eab97137f --- /dev/null +++ b/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/test/assets/csscoverage/stylesheet2.css b/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000000000..a87defb09878f --- /dev/null +++ b/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/test/assets/csscoverage/unused.html b/test/assets/csscoverage/unused.html new file mode 100644 index 0000000000000..5b8186a3bf799 --- /dev/null +++ b/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ + + diff --git a/test/assets/detect-touch.html b/test/assets/detect-touch.html new file mode 100644 index 0000000000000..80a4123fbd970 --- /dev/null +++ b/test/assets/detect-touch.html @@ -0,0 +1,12 @@ + + + + Detect Touch Test + + + + + + diff --git a/test/assets/digits/0.png b/test/assets/digits/0.png new file mode 100644 index 0000000000000..ac3c4768edfbe Binary files /dev/null and b/test/assets/digits/0.png differ diff --git a/test/assets/digits/1.png b/test/assets/digits/1.png new file mode 100644 index 0000000000000..6768222729b7a Binary files /dev/null and b/test/assets/digits/1.png differ diff --git a/test/assets/digits/2.png b/test/assets/digits/2.png new file mode 100644 index 0000000000000..b1daa4735d8a8 Binary files /dev/null and b/test/assets/digits/2.png differ diff --git a/test/assets/digits/3.png b/test/assets/digits/3.png new file mode 100644 index 0000000000000..6eca99b21bd93 Binary files /dev/null and b/test/assets/digits/3.png differ diff --git a/test/assets/digits/4.png b/test/assets/digits/4.png new file mode 100644 index 0000000000000..a721071e2cc4f Binary files /dev/null and b/test/assets/digits/4.png differ diff --git a/test/assets/digits/5.png b/test/assets/digits/5.png new file mode 100644 index 0000000000000..15cb19932a5c1 Binary files /dev/null and b/test/assets/digits/5.png differ diff --git a/test/assets/digits/6.png b/test/assets/digits/6.png new file mode 100644 index 0000000000000..639f38439d94e Binary files /dev/null and b/test/assets/digits/6.png differ diff --git a/test/assets/digits/7.png b/test/assets/digits/7.png new file mode 100644 index 0000000000000..5c1150b005a9f Binary files /dev/null and b/test/assets/digits/7.png differ diff --git a/test/assets/digits/8.png b/test/assets/digits/8.png new file mode 100644 index 0000000000000..abb8b48b0b1e5 Binary files /dev/null and b/test/assets/digits/8.png differ diff --git a/test/assets/digits/9.png b/test/assets/digits/9.png new file mode 100644 index 0000000000000..6a40a21c6f585 Binary files /dev/null and b/test/assets/digits/9.png differ diff --git a/test/assets/dynamic-oopif.html b/test/assets/dynamic-oopif.html new file mode 100644 index 0000000000000..38614d0289e12 --- /dev/null +++ b/test/assets/dynamic-oopif.html @@ -0,0 +1,10 @@ + diff --git a/test/assets/empty.html b/test/assets/empty.html new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/assets/error.html b/test/assets/error.html new file mode 100644 index 0000000000000..130400c00612c --- /dev/null +++ b/test/assets/error.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/es6/.eslintrc b/test/assets/es6/.eslintrc new file mode 100644 index 0000000000000..1903e176f51a8 --- /dev/null +++ b/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +} \ No newline at end of file diff --git a/test/assets/es6/es6import.js b/test/assets/es6/es6import.js new file mode 100644 index 0000000000000..9aac2d4d64e0f --- /dev/null +++ b/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; diff --git a/test/assets/es6/es6module.js b/test/assets/es6/es6module.js new file mode 100644 index 0000000000000..7a4e8a723a407 --- /dev/null +++ b/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/assets/es6/es6pathimport.js b/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000000..eb17a9a3d108f --- /dev/null +++ b/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; diff --git a/test/assets/favicon.ico b/test/assets/favicon.ico new file mode 100644 index 0000000000000..d4edd507993e8 Binary files /dev/null and b/test/assets/favicon.ico differ diff --git a/test/assets/file-to-upload.txt b/test/assets/file-to-upload.txt new file mode 100644 index 0000000000000..b4ad11848946d --- /dev/null +++ b/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file \ No newline at end of file diff --git a/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 b/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 new file mode 100644 index 0000000000000..be6d1880276d5 Binary files /dev/null and b/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 differ diff --git a/test/assets/frames/frame.html b/test/assets/frames/frame.html new file mode 100644 index 0000000000000..8f20d2da9fb10 --- /dev/null +++ b/test/assets/frames/frame.html @@ -0,0 +1,8 @@ + + + +
Hi, I'm frame
diff --git a/test/assets/frames/frameset.html b/test/assets/frames/frameset.html new file mode 100644 index 0000000000000..4d56f88839065 --- /dev/null +++ b/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/assets/frames/lazy-frame.html b/test/assets/frames/lazy-frame.html new file mode 100644 index 0000000000000..4821cd76cd2ab --- /dev/null +++ b/test/assets/frames/lazy-frame.html @@ -0,0 +1,3 @@ + +
+ \ No newline at end of file diff --git a/test/assets/frames/nested-frames.html b/test/assets/frames/nested-frames.html new file mode 100644 index 0000000000000..de1987586ff3d --- /dev/null +++ b/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ + + + + diff --git a/test/assets/frames/one-frame-url-fragment.html b/test/assets/frames/one-frame-url-fragment.html new file mode 100644 index 0000000000000..d1462641ff267 --- /dev/null +++ b/test/assets/frames/one-frame-url-fragment.html @@ -0,0 +1 @@ + diff --git a/test/assets/frames/one-frame.html b/test/assets/frames/one-frame.html new file mode 100644 index 0000000000000..e941d795a2777 --- /dev/null +++ b/test/assets/frames/one-frame.html @@ -0,0 +1 @@ + diff --git a/test/assets/frames/script.js b/test/assets/frames/script.js new file mode 100644 index 0000000000000..be22256d16b33 --- /dev/null +++ b/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/test/assets/frames/style.css b/test/assets/frames/style.css new file mode 100644 index 0000000000000..5b5436e8740e7 --- /dev/null +++ b/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/test/assets/frames/two-frames.html b/test/assets/frames/two-frames.html new file mode 100644 index 0000000000000..b2ee853edac52 --- /dev/null +++ b/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ + + + diff --git a/test/assets/global-var.html b/test/assets/global-var.html new file mode 100644 index 0000000000000..b6be975038f06 --- /dev/null +++ b/test/assets/global-var.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/grid.html b/test/assets/grid.html new file mode 100644 index 0000000000000..0bdbb1220e5a1 --- /dev/null +++ b/test/assets/grid.html @@ -0,0 +1,52 @@ + + + diff --git a/test/assets/historyapi.html b/test/assets/historyapi.html new file mode 100644 index 0000000000000..bacaf9e9a0009 --- /dev/null +++ b/test/assets/historyapi.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/idle-detector.html b/test/assets/idle-detector.html new file mode 100644 index 0000000000000..83b496c03dfaa --- /dev/null +++ b/test/assets/idle-detector.html @@ -0,0 +1,23 @@ + +
+ diff --git a/test/assets/initiator.html b/test/assets/initiator.html new file mode 100644 index 0000000000000..12889d324271c --- /dev/null +++ b/test/assets/initiator.html @@ -0,0 +1,2 @@ + + diff --git a/test/assets/initiator.js b/test/assets/initiator.js new file mode 100644 index 0000000000000..642e775f313b5 --- /dev/null +++ b/test/assets/initiator.js @@ -0,0 +1,8 @@ +const script = document.createElement('script'); +script.src = './injectedfile.js'; +document.body.appendChild(script); + +const style = document.createElement('link'); +style.rel = 'stylesheet'; +style.href = './injectedstyle.css'; +document.head.appendChild(style); diff --git a/test/assets/injectedfile.js b/test/assets/injectedfile.js new file mode 100644 index 0000000000000..c211b62c167a3 --- /dev/null +++ b/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); diff --git a/test/assets/injectedstyle.css b/test/assets/injectedstyle.css new file mode 100644 index 0000000000000..aa1634c255034 --- /dev/null +++ b/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/test/assets/inner-frame1.html b/test/assets/inner-frame1.html new file mode 100644 index 0000000000000..00f19ec166b19 --- /dev/null +++ b/test/assets/inner-frame1.html @@ -0,0 +1,10 @@ + diff --git a/test/assets/inner-frame2.html b/test/assets/inner-frame2.html new file mode 100644 index 0000000000000..9a236cc48f035 --- /dev/null +++ b/test/assets/inner-frame2.html @@ -0,0 +1 @@ + diff --git a/test/assets/input/button.html b/test/assets/input/button.html new file mode 100644 index 0000000000000..d4c6e13fd2801 --- /dev/null +++ b/test/assets/input/button.html @@ -0,0 +1,16 @@ + + + + Button test + + + + + + + \ No newline at end of file diff --git a/test/assets/input/checkbox.html b/test/assets/input/checkbox.html new file mode 100644 index 0000000000000..ca56762e2b3ab --- /dev/null +++ b/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ + + + + Selection Test + + + + + + + diff --git a/test/assets/input/drag-and-drop.html b/test/assets/input/drag-and-drop.html new file mode 100644 index 0000000000000..bc376a50451e0 --- /dev/null +++ b/test/assets/input/drag-and-drop.html @@ -0,0 +1,46 @@ + + + + Drag-and-drop test + + + +
drag me
+
+ + + diff --git a/test/assets/input/fileupload.html b/test/assets/input/fileupload.html new file mode 100644 index 0000000000000..55fd7c5006016 --- /dev/null +++ b/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ + + + + File upload test + + + + + \ No newline at end of file diff --git a/test/assets/input/keyboard.html b/test/assets/input/keyboard.html new file mode 100644 index 0000000000000..fd962c7518d66 --- /dev/null +++ b/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ + + + + Keyboard test + + + + + + \ No newline at end of file diff --git a/test/assets/input/mouse-helper.js b/test/assets/input/mouse-helper.js new file mode 100644 index 0000000000000..4f2824dceb021 --- /dev/null +++ b/test/assets/input/mouse-helper.js @@ -0,0 +1,74 @@ +// This injects a box into the page that moves with the mouse; +// Useful for debugging +(function () { + const box = document.createElement('div'); + box.classList.add('mouse-helper'); + const styleElement = document.createElement('style'); + styleElement.innerHTML = ` + .mouse-helper { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background: rgba(0,0,0,.4); + border: 1px solid white; + border-radius: 10px; + margin-left: -10px; + margin-top: -10px; + transition: background .2s, border-radius .2s, border-color .2s; + } + .mouse-helper.button-1 { + transition: none; + background: rgba(0,0,0,0.9); + } + .mouse-helper.button-2 { + transition: none; + border-color: rgba(0,0,255,0.9); + } + .mouse-helper.button-3 { + transition: none; + border-radius: 4px; + } + .mouse-helper.button-4 { + transition: none; + border-color: rgba(255,0,0,0.9); + } + .mouse-helper.button-5 { + transition: none; + border-color: rgba(0,255,0,0.9); + } + `; + document.head.appendChild(styleElement); + document.body.appendChild(box); + document.addEventListener( + 'mousemove', + (event) => { + box.style.left = event.pageX + 'px'; + box.style.top = event.pageY + 'px'; + updateButtons(event.buttons); + }, + true + ); + document.addEventListener( + 'mousedown', + (event) => { + updateButtons(event.buttons); + box.classList.add('button-' + event.which); + }, + true + ); + document.addEventListener( + 'mouseup', + (event) => { + updateButtons(event.buttons); + box.classList.remove('button-' + event.which); + }, + true + ); + function updateButtons(buttons) { + for (let i = 0; i < 5; i++) + box.classList.toggle('button-' + i, buttons & (1 << i)); + } +})(); diff --git a/test/assets/input/rotatedButton.html b/test/assets/input/rotatedButton.html new file mode 100644 index 0000000000000..1bce66cf5e261 --- /dev/null +++ b/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ + + + + Rotated button test + + + + + + + + diff --git a/test/assets/input/scrollable.html b/test/assets/input/scrollable.html new file mode 100644 index 0000000000000..75757824a4a1a --- /dev/null +++ b/test/assets/input/scrollable.html @@ -0,0 +1,37 @@ + + + + Scrollable test + + + + + + diff --git a/test/assets/input/select.html b/test/assets/input/select.html new file mode 100644 index 0000000000000..879a537a766fe --- /dev/null +++ b/test/assets/input/select.html @@ -0,0 +1,69 @@ + + + + Selection Test + + + + + + diff --git a/test/assets/input/textarea.html b/test/assets/input/textarea.html new file mode 100644 index 0000000000000..6d77f3106d31e --- /dev/null +++ b/test/assets/input/textarea.html @@ -0,0 +1,15 @@ + + + + Textarea test + + + + + + + diff --git a/test/assets/input/touches.html b/test/assets/input/touches.html new file mode 100644 index 0000000000000..4392cfacbd5bb --- /dev/null +++ b/test/assets/input/touches.html @@ -0,0 +1,35 @@ + + + + Touch test + + + + + + + \ No newline at end of file diff --git a/test/assets/input/wheel.html b/test/assets/input/wheel.html new file mode 100644 index 0000000000000..3d093a993e70f --- /dev/null +++ b/test/assets/input/wheel.html @@ -0,0 +1,43 @@ + + + + + + Element: wheel event - Scaling_an_element_via_the_wheel - code sample + + +
Scale me with your mouse wheel.
+ + + diff --git a/test/assets/jscoverage/eval.html b/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000000..838ae28763d9b --- /dev/null +++ b/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ + diff --git a/test/assets/jscoverage/involved.html b/test/assets/jscoverage/involved.html new file mode 100644 index 0000000000000..889c86bed58ed --- /dev/null +++ b/test/assets/jscoverage/involved.html @@ -0,0 +1,15 @@ + diff --git a/test/assets/jscoverage/multiple.html b/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000000000..bdef59885b295 --- /dev/null +++ b/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ + + diff --git a/test/assets/jscoverage/ranges.html b/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000000000..a537a7da6a707 --- /dev/null +++ b/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/script1.js b/test/assets/jscoverage/script1.js new file mode 100644 index 0000000000000..3bd241b50e7c4 --- /dev/null +++ b/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/script2.js b/test/assets/jscoverage/script2.js new file mode 100644 index 0000000000000..3bd241b50e7c4 --- /dev/null +++ b/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/test/assets/jscoverage/simple.html b/test/assets/jscoverage/simple.html new file mode 100644 index 0000000000000..49eeeea6ae180 --- /dev/null +++ b/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ + diff --git a/test/assets/jscoverage/sourceurl.html b/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000000000..e477750320153 --- /dev/null +++ b/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ + diff --git a/test/assets/jscoverage/unused.html b/test/assets/jscoverage/unused.html new file mode 100644 index 0000000000000..59c4a5a70b44c --- /dev/null +++ b/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ + diff --git a/test/assets/lazy-oopif-frame.html b/test/assets/lazy-oopif-frame.html new file mode 100644 index 0000000000000..7c259b6673ac1 --- /dev/null +++ b/test/assets/lazy-oopif-frame.html @@ -0,0 +1,3 @@ + +
+ \ No newline at end of file diff --git a/test/assets/main-frame.html b/test/assets/main-frame.html new file mode 100644 index 0000000000000..0c50feff85828 --- /dev/null +++ b/test/assets/main-frame.html @@ -0,0 +1,10 @@ + diff --git a/test/assets/mobile.html b/test/assets/mobile.html new file mode 100644 index 0000000000000..8e94b2fe2917e --- /dev/null +++ b/test/assets/mobile.html @@ -0,0 +1 @@ + diff --git a/test/assets/modernizr.js b/test/assets/modernizr.js new file mode 100644 index 0000000000000..7991a4ec4025e --- /dev/null +++ b/test/assets/modernizr.js @@ -0,0 +1,3 @@ +/*! modernizr 3.5.0 (Custom Build) | MIT * +* https://modernizr.com/download/?-touchevents-setclasses !*/ +!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t + async function sleep(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); + } + + async function main() { + const roundOne = Promise.all([ + fetch('fetch-request-a.js'), + fetch('fetch-request-b.js'), + fetch('fetch-request-c.js'), + ]); + + await roundOne; + await sleep(50); + await fetch('fetch-request-d.js'); + } + + main(); + diff --git a/test/assets/offscreenbuttons.html b/test/assets/offscreenbuttons.html new file mode 100644 index 0000000000000..e487caf4d35ef --- /dev/null +++ b/test/assets/offscreenbuttons.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/test/assets/one-style.css b/test/assets/one-style.css new file mode 100644 index 0000000000000..7b26410d8a15e --- /dev/null +++ b/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/one-style.html b/test/assets/one-style.html new file mode 100644 index 0000000000000..4760f2b9f7e37 --- /dev/null +++ b/test/assets/one-style.html @@ -0,0 +1,2 @@ + +
hello, world!
diff --git a/test/assets/oopif.html b/test/assets/oopif.html new file mode 100644 index 0000000000000..0761e8ab1134b --- /dev/null +++ b/test/assets/oopif.html @@ -0,0 +1,2 @@ +Navigate within document + \ No newline at end of file diff --git a/test/assets/pdf.html b/test/assets/pdf.html new file mode 100644 index 0000000000000..987df27ebefda --- /dev/null +++ b/test/assets/pdf.html @@ -0,0 +1,11 @@ + + + + + + PDF + + +
PDF Content
+ + diff --git a/test/assets/playground.html b/test/assets/playground.html new file mode 100644 index 0000000000000..828cfb1c70124 --- /dev/null +++ b/test/assets/playground.html @@ -0,0 +1,15 @@ + + + + Playground + + + + +
First div
+
+ Second div + Inner span +
+ + \ No newline at end of file diff --git a/test/assets/popup/popup.html b/test/assets/popup/popup.html new file mode 100644 index 0000000000000..b855162c25b27 --- /dev/null +++ b/test/assets/popup/popup.html @@ -0,0 +1,9 @@ + + + + Popup + + + I am a popup + + diff --git a/test/assets/popup/window-open.html b/test/assets/popup/window-open.html new file mode 100644 index 0000000000000..d138be1d22ba8 --- /dev/null +++ b/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ + + + + Popup test + + + + + diff --git a/test/assets/pptr.png b/test/assets/pptr.png new file mode 100644 index 0000000000000..65d87c68e6590 Binary files /dev/null and b/test/assets/pptr.png differ diff --git a/test/assets/resetcss.html b/test/assets/resetcss.html new file mode 100644 index 0000000000000..e4e04b1f8a393 --- /dev/null +++ b/test/assets/resetcss.html @@ -0,0 +1,50 @@ + diff --git a/test/assets/self-request.html b/test/assets/self-request.html new file mode 100644 index 0000000000000..88aff620ffd82 --- /dev/null +++ b/test/assets/self-request.html @@ -0,0 +1,5 @@ + diff --git a/test/assets/serviceworkers/empty/sw.html b/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000000000..bef85d985b072 --- /dev/null +++ b/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ + diff --git a/test/assets/serviceworkers/empty/sw.js b/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/assets/serviceworkers/fetch/style.css b/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000000..7b26410d8a15e --- /dev/null +++ b/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/test/assets/serviceworkers/fetch/sw.html b/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000000000..a9d28acb0977c --- /dev/null +++ b/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ + + diff --git a/test/assets/serviceworkers/fetch/sw.js b/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000000000..21381484b63f7 --- /dev/null +++ b/test/assets/serviceworkers/fetch/sw.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', (event) => { + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(clients.claim()); +}); diff --git a/test/assets/shadow.html b/test/assets/shadow.html new file mode 100644 index 0000000000000..3796ca768c44d --- /dev/null +++ b/test/assets/shadow.html @@ -0,0 +1,17 @@ + diff --git a/test/assets/simple-extension/content-script.js b/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000000000..0fd83b90f1eb5 --- /dev/null +++ b/test/assets/simple-extension/content-script.js @@ -0,0 +1,2 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; diff --git a/test/assets/simple-extension/index.js b/test/assets/simple-extension/index.js new file mode 100644 index 0000000000000..a0bb3f4eae75a --- /dev/null +++ b/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/test/assets/simple-extension/manifest.json b/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000000000..da2cd082edb64 --- /dev/null +++ b/test/assets/simple-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "scripts": ["index.js"] + }, + "content_scripts": [{ + "matches": [""], + "css": [], + "js": ["content-script.js"] + }], + "permissions": ["background", "activeTab"], + "manifest_version": 2 +} diff --git a/test/assets/simple.json b/test/assets/simple.json new file mode 100644 index 0000000000000..6d9590305133e --- /dev/null +++ b/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/test/assets/tamperable.html b/test/assets/tamperable.html new file mode 100644 index 0000000000000..d027e97038573 --- /dev/null +++ b/test/assets/tamperable.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/assets/title.html b/test/assets/title.html new file mode 100644 index 0000000000000..88a86ce412b07 --- /dev/null +++ b/test/assets/title.html @@ -0,0 +1 @@ +Woof-Woof diff --git a/test/assets/worker/worker.html b/test/assets/worker/worker.html new file mode 100644 index 0000000000000..7de2d9fd9e882 --- /dev/null +++ b/test/assets/worker/worker.html @@ -0,0 +1,14 @@ + + + + Worker test + + + + + \ No newline at end of file diff --git a/test/assets/worker/worker.js b/test/assets/worker/worker.js new file mode 100644 index 0000000000000..0626f13e58d70 --- /dev/null +++ b/test/assets/worker/worker.js @@ -0,0 +1,16 @@ +console.log('hello from the worker'); + +function workerFunction() { + return 'worker function result'; +} + +self.addEventListener('message', (event) => { + console.log('got this data: ' + event.data); +}); + +(async function () { + while (true) { + self.postMessage(workerFunction.toString()); + await new Promise((x) => setTimeout(x, 100)); + } +})(); diff --git a/test/assets/wrappedlink.html b/test/assets/wrappedlink.html new file mode 100644 index 0000000000000..429b6e915671b --- /dev/null +++ b/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ + +
+ 123321 +
+ diff --git a/test/browser.spec.ts b/test/browser.spec.ts new file mode 100644 index 0000000000000..00da218d35229 --- /dev/null +++ b/test/browser.spec.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2018 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 { getTestState, setupTestBrowserHooks } from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Browser specs', function () { + setupTestBrowserHooks(); + + describe('Browser.version', function () { + it('should return whether we are in headless', async () => { + const { browser, isHeadless, headless } = getTestState(); + + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + expect(version.startsWith('Headless')).toBe( + isHeadless && headless !== 'chrome' + ); + }); + }); + + describe('Browser.userAgent', function () { + it('should include WebKit', async () => { + const { browser, isChrome } = getTestState(); + + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (isChrome) expect(userAgent).toContain('WebKit'); + else expect(userAgent).toContain('Gecko'); + }); + }); + + describe('Browser.target', function () { + it('should return browser target', async () => { + const { browser } = getTestState(); + + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function () { + it('should return child_process instance', async () => { + const { browser } = getTestState(); + + const process = await browser.process(); + expect(process.pid).toBeGreaterThan(0); + }); + it('should not return child_process for remote browser', async () => { + const { browser, puppeteer } = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ browserWSEndpoint }); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe('Browser.isConnected', () => { + it('should set the browser connected state', async () => { + const { browser, puppeteer } = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await puppeteer.connect({ browserWSEndpoint }); + expect(newBrowser.isConnected()).toBe(true); + newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}); diff --git a/test/browsercontext.spec.ts b/test/browsercontext.spec.ts new file mode 100644 index 0000000000000..88ff09b2c7029 --- /dev/null +++ b/test/browsercontext.spec.ts @@ -0,0 +1,207 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestBrowserHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const { browser } = getTestState(); + expect(browser.browserContexts().length).toEqual(1); + const defaultContext = browser.browserContexts()[0]; + expect(defaultContext.isIncognito()).toBe(false); + let error = null; + await defaultContext.close().catch((error_) => (error = error_)); + expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(error.message).toContain('cannot be closed'); + }); + it('should create new incognito context', async () => { + const { browser } = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts().length).toBe(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts().length).toBe(1); + }); + it('should close all belonging targets once closing context', async () => { + const { browser } = getTestState(); + + expect((await browser.pages()).length).toBe(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect((await browser.pages()).length).toBe(2); + expect((await context.pages()).length).toBe(1); + + await context.close(); + expect((await browser.pages()).length).toBe(1); + }); + itFailsFirefox('window.open should use parent tab context', async () => { + const { browser, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const [popupTarget] = await Promise.all([ + utils.waitEvent(browser, 'targetcreated'), + page.evaluate<(url: string) => void>( + (url) => window.open(url), + server.EMPTY_PAGE + ), + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + itFailsFirefox('should fire target events', async () => { + const { browser, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const events = []; + context.on('targetcreated', (target) => + events.push('CREATED: ' + target.url()) + ); + context.on('targetchanged', (target) => + events.push('CHANGED: ' + target.url()) + ); + context.on('targetdestroyed', (target) => + events.push('DESTROYED: ' + target.url()) + ); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual([ + 'CREATED: about:blank', + `CHANGED: ${server.EMPTY_PAGE}`, + `DESTROYED: ${server.EMPTY_PAGE}`, + ]); + await context.close(); + }); + itFailsFirefox('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + + const targetPromise = context.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + }); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const { browser, puppeteer, server } = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => error_); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + await context.close(); + }); + + itFailsFirefox('should isolate localStorage and cookies', async () => { + const { browser, server } = getTestState(); + + // Create two incognito contexts. + const context1 = await browser.createIncognitoBrowserContext(); + const context2 = await browser.createIncognitoBrowserContext(); + expect(context1.targets().length).toBe(0); + expect(context2.targets().length).toBe(0); + + // Create a page in first incognito context. + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + await page1.evaluate(() => { + localStorage.setItem('name', 'page1'); + document.cookie = 'name=page1'; + }); + + expect(context1.targets().length).toBe(1); + expect(context2.targets().length).toBe(0); + + // Create a page in second incognito context. + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + await page2.evaluate(() => { + localStorage.setItem('name', 'page2'); + document.cookie = 'name=page2'; + }); + + expect(context1.targets().length).toBe(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets().length).toBe(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect(await page1.evaluate(() => localStorage.getItem('name'))).toBe( + 'page1' + ); + expect(await page1.evaluate(() => document.cookie)).toBe('name=page1'); + expect(await page2.evaluate(() => localStorage.getItem('name'))).toBe( + 'page2' + ); + expect(await page2.evaluate(() => document.cookie)).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([context1.close(), context2.close()]); + expect(browser.browserContexts().length).toBe(1); + }); + + itFailsFirefox('should work across sessions', async () => { + const { browser, puppeteer } = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); +}); diff --git a/test/chromiumonly.spec.ts b/test/chromiumonly.spec.ts new file mode 100644 index 0000000000000..7b64a70fd396c --- /dev/null +++ b/test/chromiumonly.spec.ts @@ -0,0 +1,159 @@ +/** + * Copyright 2019 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Chromium-Specific Launcher tests', function () { + describe('Puppeteer.launch |browserURL| option', function () { + it('should be able to connect using browserUrl, with and without trailing slash', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await puppeteer.connect({ browserURL }); + const page1 = await browser1.newPage(); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 8 * 7)).toBe(56); + browser2.disconnect(); + originalBrowser.close(); + }); + it('should throw when using both browserWSEndpoint and browserURL', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + let error = null; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: originalBrowser.wsEndpoint(), + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + + originalBrowser.close(); + }); + it('should throw when trying to connect to non-existing browser', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:32333'; + + let error = null; + await puppeteer + .connect({ browserURL }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Failed to fetch browser webSocket URL from' + ); + originalBrowser.close(); + }); + }); + + describe('Puppeteer.launch |pipe| option', function () { + it('should support the pipe option', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({ pipe: true }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + expect((await browser.pages()).length).toBe(1); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should support the pipe argument', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await puppeteer.launch(options); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should fire "disconnected" when closing with pipe', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({ pipe: true }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const disconnectedEventPromise = new Promise((resolve) => + browser.once('disconnected', resolve) + ); + // Emulate user exiting browser. + browser.process().kill(); + await disconnectedEventPromise; + }); + }); +}); + +describeChromeOnly('Chromium-Specific Page Tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.setRequestInterception should work with intervention headers', async () => { + const { server, page } = getTestState(); + + server.setRoute('/intervention', (req, res) => + res.end(` + + `) + ); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest = null; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.PREFIX + '/intervention'); + // Check for feature URL substring rather than https://www.chromestatus.com to + // make it work with Edgium. + expect(serverRequest.headers.intervention).toContain( + 'feature/5718547946799104' + ); + }); +}); diff --git a/test/click.spec.ts b/test/click.spec.ts new file mode 100644 index 0000000000000..947d8344451d8 --- /dev/null +++ b/test/click.spec.ts @@ -0,0 +1,373 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +describe('Page.click', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click svg', async () => { + const { page } = getTestState(); + + await page.setContent(` + + + + `); + await page.click('circle'); + expect(await page.evaluate(() => globalThis.__CLICKED)).toBe(42); + }); + itFailsFirefox( + 'should click the button if window.Node is removed', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => delete window.Node); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + } + ); + // @see https://github.com/puppeteer/puppeteer/issues/4281 + it('should click on a span with an inline element inside', async () => { + const { page } = getTestState(); + + await page.setContent(` + + + `); + await page.click('span'); + expect(await page.evaluate(() => globalThis.CLICKED)).toBe(42); + }); + it('should not throw UnhandledPromiseRejection when page closes', async () => { + const { page } = getTestState(); + + const newPage = await page.browser().newPage(); + await Promise.all([newPage.close(), newPage.mouse.click(1, 2)]).catch( + () => {} + ); + }); + it('should click the button after navigation ', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + itFailsFirefox('should click with disabled javascript', async () => { + const { page, server } = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + await Promise.all([page.click('a'), page.waitForNavigation()]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + }); + it('should click when one of inline box children is outside of viewport', async () => { + const { page } = getTestState(); + + await page.setContent(` + + woofdoggo + `); + await page.click('span'); + expect(await page.evaluate(() => globalThis.CLICKED)).toBe(42); + }); + it('should select the text by triple clicking', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + await page.keyboard.type(text); + await page.click('textarea'); + await page.click('textarea', { clickCount: 2 }); + await page.click('textarea', { clickCount: 3 }); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd + ); + }) + ).toBe(text); + }); + it('should click offscreen buttons', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages = []; + page.on('console', (msg) => messages.push(msg.text())); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => window.scrollTo(0, 0)); + await page.click(`#btn${i}`); + } + expect(messages).toEqual([ + 'button #0 clicked', + 'button #1 clicked', + 'button #2 clicked', + 'button #3 clicked', + 'button #4 clicked', + 'button #5 clicked', + 'button #6 clicked', + 'button #7 clicked', + 'button #8 clicked', + 'button #9 clicked', + 'button #10 clicked', + ]); + }); + + it('should click wrapped links', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/wrappedlink.html'); + await page.click('a'); + expect(await page.evaluate(() => globalThis.__clicked)).toBe(true); + }); + + it('should click on checkbox input and toggle', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(null); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(false); + }); + + itFailsFirefox('should click on checkbox label and toggle', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(null); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(true); + expect(await page.evaluate(() => globalThis.result.events)).toEqual([ + 'click', + 'input', + 'change', + ]); + await page.click('label[for="agree"]'); + expect(await page.evaluate(() => globalThis.result.check)).toBe(false); + }); + + it('should fail to click a missing button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + let error = null; + await page + .click('button.does-not-exist') + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'No node found for selector: button.does-not-exist' + ); + }); + // @see https://github.com/puppeteer/puppeteer/issues/161 + it('should not hang with touch-enabled viewports', async () => { + const { page, puppeteer } = getTestState(); + + await page.setViewport(puppeteer.devices['iPhone 6'].viewport); + await page.mouse.down(); + await page.mouse.move(100, 10); + await page.mouse.up(); + }); + it('should scroll and click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-5'); + expect( + await page.evaluate(() => document.querySelector('#button-5').textContent) + ).toBe('clicked'); + await page.click('#button-80'); + expect( + await page.evaluate( + () => document.querySelector('#button-80').textContent + ) + ).toBe('clicked'); + }); + it('should double click the button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + globalThis.double = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', () => { + globalThis.double = true; + }); + }); + const button = await page.$('button'); + await button.click({ clickCount: 2 }); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + const button = document.querySelector('button'); + button.textContent = 'Some really long text that will go offscreen'; + button.style.position = 'absolute'; + button.style.left = '368px'; + }); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click a rotated button', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/rotatedButton.html'); + await page.click('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should fire contextmenu event on right click', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', { button: 'right' }); + expect( + await page.evaluate(() => document.querySelector('#button-8').textContent) + ).toBe('context menu'); + }); + it('should fire aux event on middle click', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', { button: 'middle' }); + expect( + await page.evaluate(() => document.querySelector('#button-8').textContent) + ).toBe('aux click'); + }); + it('should fire back click', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', { button: 'back' }); + expect( + await page.evaluate(() => document.querySelector('#button-8').textContent) + ).toBe('back click'); + }); + it('should fire forward click', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', { button: 'forward' }); + expect( + await page.evaluate(() => document.querySelector('#button-8').textContent) + ).toBe('forward click'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/206 + it('should click links which cause navigation', async () => { + const { page, server } = getTestState(); + + await page.setContent(`empty.html`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('
spacer
'); + await utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + xit('should click the button with fixed position inside an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({ width: 500, height: 500 }); + await page.setContent( + '
spacer
' + ); + await utils.attachFrame( + page, + 'button-test', + server.CROSS_PROCESS_PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + await frame.$eval('button', (button: HTMLElement) => + button.style.setProperty('position', 'fixed') + ); + await frame.click('button'); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should click the button with deviceScaleFactor set', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 400, height: 400, deviceScaleFactor: 5 }); + expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5); + await page.setContent('
spacer
'); + await utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame.$('button'); + await button.click(); + expect(await frame.evaluate(() => globalThis.result)).toBe('Clicked'); + }); +}); diff --git a/test/cookies.spec.ts b/test/cookies.spec.ts new file mode 100644 index 0000000000000..ad7339b0d390a --- /dev/null +++ b/test/cookies.spec.ts @@ -0,0 +1,564 @@ +/** + * Copyright 2018 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 { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Cookie specs', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.cookies', function () { + it('should return no cookies in pristine browser context', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + expectCookieEquals(await page.cookies(), []); + }); + it('should get a cookie', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + + expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('should properly report httpOnly cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].httpOnly).toBe(true); + }); + it('should properly report "Strict" sameSite cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Strict'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Strict'); + }); + it('should properly report "Lax" sameSite cookie', async () => { + const { page, server } = getTestState(); + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Lax'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0].sameSite).toBe('Lax'); + }); + it('should get multiple cookies', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + document.cookie = 'password=1234'; + }); + const cookies = await page.cookies(); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expectCookieEquals(cookies, [ + { + name: 'password', + value: '1234', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 12, + httpOnly: false, + secure: false, + session: true, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox('should get cookies from multiple urls', async () => { + const { page } = getTestState(); + await page.setCookie( + { + url: 'https://foo.com', + name: 'doggo', + value: 'woofs', + }, + { + url: 'https://bar.com', + name: 'catto', + value: 'purrs', + }, + { + url: 'https://baz.com', + name: 'birdo', + value: 'tweets', + } + ); + const cookies = await page.cookies('https://foo.com', 'https://baz.com'); + cookies.sort((a, b) => a.name.localeCompare(b.name)); + expectCookieEquals(cookies, [ + { + name: 'birdo', + value: 'tweets', + domain: 'baz.com', + path: '/', + sameParty: false, + expires: -1, + size: 11, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + { + name: 'doggo', + value: 'woofs', + domain: 'foo.com', + path: '/', + sameParty: false, + expires: -1, + size: 10, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ]); + }); + }); + describe('Page.setCookie', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + expect(await page.evaluate(() => document.cookie)).toEqual( + 'password=123456' + ); + }); + itFailsFirefox('should isolate cookies in browser contexts', async () => { + const { page, server, browser } = getTestState(); + + const anotherContext = await browser.createIncognitoBrowserContext(); + const anotherPage = await anotherContext.newPage(); + + await page.goto(server.EMPTY_PAGE); + await anotherPage.goto(server.EMPTY_PAGE); + + await page.setCookie({ name: 'page1cookie', value: 'page1value' }); + await anotherPage.setCookie({ name: 'page2cookie', value: 'page2value' }); + + const cookies1 = await page.cookies(); + const cookies2 = await anotherPage.cookies(); + expect(cookies1.length).toBe(1); + expect(cookies2.length).toBe(1); + expect(cookies1[0].name).toBe('page1cookie'); + expect(cookies1[0].value).toBe('page1value'); + expect(cookies2[0].name).toBe('page2cookie'); + expect(cookies2[0].value).toBe('page2value'); + await anotherContext.close(); + }); + itFailsFirefox('should set multiple cookies', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'password', + value: '123456', + }, + { + name: 'foo', + value: 'bar', + } + ); + const cookieStrings = await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies.map((cookie) => cookie.trim()).sort(); + }); + + expect(cookieStrings).toEqual(['foo=bar', 'password=123456']); + }); + it('should have |expires| set to |-1| for session cookies', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + expect(cookies[0].session).toBe(true); + expect(cookies[0].expires).toBe(-1); + }); + itFailsFirefox('should set cookie with reasonable defaults', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + expectCookieEquals( + cookies.sort((a, b) => a.name.localeCompare(b.name)), + [ + { + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ] + ); + }); + itFailsFirefox('should set a cookie with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html', + }); + expectCookieEquals(await page.cookies(), [ + { + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + sameParty: false, + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(server.EMPTY_PAGE); + expectCookieEquals(await page.cookies(), []); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto(server.PREFIX + '/grid.html'); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + }); + it('should not set a cookie on a blank page', async () => { + const { page } = getTestState(); + + await page.goto('about:blank'); + let error = null; + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it('should not set a cookie with blank page URL', async () => { + const { page, server } = getTestState(); + + let error = null; + await page.goto(server.EMPTY_PAGE); + try { + await page.setCookie( + { name: 'example-cookie', value: 'best' }, + { url: 'about:blank', name: 'example-cookie-blank', value: 'best' } + ); + } catch (error_) { + error = error_; + } + expect(error.message).toEqual( + `Blank page can not have cookie "example-cookie-blank"` + ); + }); + it('should not set a cookie on a data URL page', async () => { + const { page } = getTestState(); + + let error = null; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({ name: 'example-cookie', value: 'best' }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + itFailsFirefox( + 'should default to setting secure cookie for HTTPS websites', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const SECURE_URL = 'https://example.com'; + await page.setCookie({ + url: SECURE_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(SECURE_URL); + expect(cookie.secure).toBe(true); + } + ); + it('should be able to set unsecure cookie for HTTP website', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie.secure).toBe(false); + }); + itFailsFirefox('should set a cookie on a different domain', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + url: 'https://www.example.com', + name: 'example-cookie', + value: 'best', + }); + expect(await page.evaluate('document.cookie')).toBe(''); + expectCookieEquals(await page.cookies(), []); + expectCookieEquals(await page.cookies('https://www.example.com'), [ + { + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + sameParty: false, + expires: -1, + size: 18, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ]); + }); + itFailsFirefox('should set cookies from a frame', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ name: 'localhost-cookie', value: 'best' }); + await page.evaluate<(src: string) => Promise>((src) => { + let fulfill; + const promise = new Promise((x) => (fulfill = x)); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, server.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-cookie', + value: 'worst', + url: server.CROSS_PROCESS_PREFIX, + }); + expect(await page.evaluate('document.cookie')).toBe( + 'localhost-cookie=best' + ); + expect(await page.frames()[1].evaluate('document.cookie')).toBe(''); + + expectCookieEquals(await page.cookies(), [ + { + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 20, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + + expectCookieEquals(await page.cookies(server.CROSS_PROCESS_PREFIX), [ + { + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + sameParty: false, + expires: -1, + size: 15, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox( + 'should set secure same-site cookies from a frame', + async () => { + const { httpsServer, puppeteer, defaultBrowserOptions } = + getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/grid.html'); + await page.evaluate<(src: string) => Promise>((src) => { + let fulfill; + const promise = new Promise((x) => (fulfill = x)); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, httpsServer.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-same-site-cookie', + value: 'best', + url: httpsServer.CROSS_PROCESS_PREFIX, + sameSite: 'None', + }); + + expect(await page.frames()[1].evaluate('document.cookie')).toBe( + '127-same-site-cookie=best' + ); + expectCookieEquals( + await page.cookies(httpsServer.CROSS_PROCESS_PREFIX), + [ + { + name: '127-same-site-cookie', + value: 'best', + domain: '127.0.0.1', + path: '/', + sameParty: false, + expires: -1, + size: 24, + httpOnly: false, + sameSite: 'None', + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ] + ); + } finally { + await page.close(); + await browser.close(); + } + } + ); + }); + + describe('Page.deleteCookie', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + }, + { + name: 'cookie3', + value: '3', + } + ); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie2=2; cookie3=3' + ); + await page.deleteCookie({ name: 'cookie2' }); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie3=3' + ); + }); + }); +}); diff --git a/test/coverage-utils.js b/test/coverage-utils.js new file mode 100644 index 0000000000000..c23e507f9de2a --- /dev/null +++ b/test/coverage-utils.js @@ -0,0 +1,164 @@ +/** + * Copyright 2020 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. + */ + +// TODO (@jackfranklin): convert this to TypeScript and enable type-checking +// @ts-nocheck + +/* We want to ensure that all of Puppeteer's public API is tested via our unit + * tests but we can't use a tool like Istanbul because the way it instruments + * code unfortunately breaks in Puppeteer where some of that code is then being + * executed in a browser context. + * + * So instead we maintain this coverage code which does the following: + * * takes every public method that we expect to be tested + * * replaces it with a method that calls the original but also updates a Map of calls + * * in an after() test callback it asserts that every public method was called. + * + * We run this when COVERAGE=1. + */ + +const path = require('path'); +const fs = require('fs'); + +/** + * This object is also used by DocLint to know which classes to check are + * documented. It's a pretty hacky solution but DocLint is going away soon as + * part of the TSDoc migration. + */ +const MODULES_TO_CHECK_FOR_COVERAGE = { + Accessibility: '../lib/cjs/puppeteer/common/Accessibility', + Browser: '../lib/cjs/puppeteer/common/Browser', + BrowserContext: '../lib/cjs/puppeteer/common/Browser', + BrowserFetcher: '../lib/cjs/puppeteer/node/BrowserFetcher', + CDPSession: '../lib/cjs/puppeteer/common/Connection', + ConsoleMessage: '../lib/cjs/puppeteer/common/ConsoleMessage', + Coverage: '../lib/cjs/puppeteer/common/Coverage', + Dialog: '../lib/cjs/puppeteer/common/Dialog', + ElementHandle: '../lib/cjs/puppeteer/common/JSHandle', + ExecutionContext: '../lib/cjs/puppeteer/common/ExecutionContext', + EventEmitter: '../lib/cjs/puppeteer/common/EventEmitter', + FileChooser: '../lib/cjs/puppeteer/common/FileChooser', + Frame: '../lib/cjs/puppeteer/common/FrameManager', + JSHandle: '../lib/cjs/puppeteer/common/JSHandle', + Keyboard: '../lib/cjs/puppeteer/common/Input', + Mouse: '../lib/cjs/puppeteer/common/Input', + Page: '../lib/cjs/puppeteer/common/Page', + Puppeteer: '../lib/cjs/puppeteer/common/Puppeteer', + PuppeteerNode: '../lib/cjs/puppeteer/node/Puppeteer', + HTTPRequest: '../lib/cjs/puppeteer/common/HTTPRequest', + HTTPResponse: '../lib/cjs/puppeteer/common/HTTPResponse', + SecurityDetails: '../lib/cjs/puppeteer/common/SecurityDetails', + Target: '../lib/cjs/puppeteer/common/Target', + TimeoutError: '../lib/cjs/puppeteer/common/Errors', + Touchscreen: '../lib/cjs/puppeteer/common/Input', + Tracing: '../lib/cjs/puppeteer/common/Tracing', + WebWorker: '../lib/cjs/puppeteer/common/WebWorker', +}; + +function traceAPICoverage(apiCoverage, className, modulePath) { + const loadedModule = require(modulePath); + const classType = loadedModule[className]; + + if (!classType || !classType.prototype) { + console.error( + `Coverage error: could not find class for ${className}. Is src/api.ts up to date?` + ); + process.exit(1); + } + for (const methodName of Reflect.ownKeys(classType.prototype)) { + const method = Reflect.get(classType.prototype, methodName); + if ( + methodName === 'constructor' || + typeof methodName !== 'string' || + methodName.startsWith('_') || + typeof method !== 'function' + ) + continue; + apiCoverage.set(`${className}.${methodName}`, false); + Reflect.set(classType.prototype, methodName, function (...args) { + apiCoverage.set(`${className}.${methodName}`, true); + return method.call(this, ...args); + }); + } + + /** + * If classes emit events, those events are exposed via an object in the same + * module named XEmittedEvents, where X is the name of the class. For example, + * the Page module exposes PageEmittedEvents. + */ + const eventsName = `${className}EmittedEvents`; + if (loadedModule[eventsName]) { + for (const event of Object.values(loadedModule[eventsName])) { + if (typeof event !== 'symbol') + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); + } + const method = Reflect.get(classType.prototype, 'emit'); + Reflect.set(classType.prototype, 'emit', function (event, ...args) { + if (typeof event !== 'symbol' && this.listenerCount(event)) + apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); + return method.call(this, event, ...args); + }); + } +} + +const coverageLocation = path.join(__dirname, 'coverage.json'); + +const clearOldCoverage = () => { + try { + fs.unlinkSync(coverageLocation); + } catch (error) { + // do nothing, the file didn't exist + } +}; +const writeCoverage = (coverage) => { + fs.writeFileSync(coverageLocation, JSON.stringify([...coverage.entries()])); +}; + +const getCoverageResults = () => { + let contents; + try { + contents = fs.readFileSync(coverageLocation, { encoding: 'utf8' }); + } catch (error) { + console.error('Warning: coverage file does not exist or is not readable.'); + } + + const coverageMap = new Map(JSON.parse(contents)); + return coverageMap; +}; + +const trackCoverage = () => { + clearOldCoverage(); + const coverageMap = new Map(); + + return { + beforeAll: () => { + for (const [className, moduleFilePath] of Object.entries( + MODULES_TO_CHECK_FOR_COVERAGE + )) { + traceAPICoverage(coverageMap, className, moduleFilePath); + } + }, + afterAll: () => { + writeCoverage(coverageMap); + }, + }; +}; + +module.exports = { + trackCoverage, + getCoverageResults, + MODULES_TO_CHECK_FOR_COVERAGE, +}; diff --git a/test/coverage.spec.ts b/test/coverage.spec.ts new file mode 100644 index 0000000000000..4801a8eed5816 --- /dev/null +++ b/test/coverage.spec.ts @@ -0,0 +1,315 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Coverage specs', function () { + describeChromeOnly('JSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page, server } = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/jscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([ + { start: 0, end: 17 }, + { start: 35, end: 61 }, + ]); + }); + it('should report sourceURLs', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/sourceurl.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.js'); + }); + it('should ignore eval() scripts by default', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + }); + it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + coverage.find((entry) => entry.url.startsWith('debugger://')) + ).not.toBe(null); + expect(coverage.length).toBe(2); + }); + it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => console.log('bar')); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should report multiple scripts', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/jscoverage/script1.js'); + expect(coverage[1].url).toContain('/jscoverage/script2.js'); + }); + it('should report right ranges', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.ranges.length).toBe(1); + const range = entry.ranges[0]; + expect(entry.text.substring(range.start, range.end)).toBe( + `console.log('used!');` + ); + }); + it('should report scripts that have no coverage', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/unused.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges.length).toBe(0); + }); + it('should work with conditionals', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/involved.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/') + ).toBeGolden('jscoverage-involved.txt'); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + describe('resetOnNavigation', function () { + it('should report scripts across navigations when disabled', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage({ resetOnNavigation: false }); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + }); + + it('should NOT report scripts across navigations when enabled', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + describe('includeRawScriptCoverage', function () { + it('should not include rawScriptCoverage field when disabled', async () => { + const { page, server } = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].rawScriptCoverage).toBeUndefined(); + }); + it('should include rawScriptCoverage field when enabled', async () => { + const { page, server } = getTestState(); + await page.coverage.startJSCoverage({ + includeRawScriptCoverage: true, + }); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].rawScriptCoverage).toBeTruthy(); + }); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const { page, server } = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describeChromeOnly('CSSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/simple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/simple.html'); + expect(coverage[0].ranges).toEqual([{ start: 1, end: 22 }]); + const range = coverage[0].ranges[0]; + expect(coverage[0].text.substring(range.start, range.end)).toBe( + 'div { color: green; }' + ); + }); + it('should report sourceURLs', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/sourceurl.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('nicename.css'); + }); + it('should report multiple stylesheets', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => a.url.localeCompare(b.url)); + expect(coverage[0].url).toContain('/csscoverage/stylesheet1.css'); + expect(coverage[1].url).toContain('/csscoverage/stylesheet2.css'); + }); + it('should report stylesheets that have no coverage', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/unused.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toBe('unused.css'); + expect(coverage[0].ranges.length).toBe(0); + }); + it('should work with media queries', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/media.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0].url).toContain('/csscoverage/media.html'); + expect(coverage[0].ranges).toEqual([{ start: 17, end: 38 }]); + }); + it('should work with complicated usecases', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/involved.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4}\//g, ':/') + ).toBeGolden('csscoverage-involved.txt'); + }); + it('should ignore injected stylesheets', async () => { + const { page } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.addStyleTag({ content: 'body { margin: 10px;}' }); + // trigger style recalc + const margin = await page.evaluate( + () => window.getComputedStyle(document.body).margin + ); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should work with a recently loaded stylesheet', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.evaluate<(url: string) => Promise>(async (url) => { + document.body.textContent = 'hello, world'; + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); + await new Promise((x) => (link.onload = x)); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + }); + describe('resetOnNavigation', function () { + it('should report stylesheets across navigations', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage({ resetOnNavigation: false }); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should NOT report scripts across navigations', async () => { + const { page, server } = getTestState(); + + await page.coverage.startCSSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + }); +}); diff --git a/test/defaultbrowsercontext.spec.ts b/test/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000000..b84428de0660d --- /dev/null +++ b/test/defaultbrowsercontext.spec.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('DefaultBrowserContext', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('page.cookies() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 8907, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox('page.setCookie() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'username', + value: 'John Doe', + }); + expect(await page.evaluate(() => document.cookie)).toBe( + 'username=John Doe' + ); + expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + }); + itFailsFirefox('page.deleteCookie() should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + } + ); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + await page.deleteCookie({ name: 'cookie2' }); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + expectCookieEquals(await page.cookies(), [ + { + name: 'cookie1', + value: '1', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 8, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + }); +}); diff --git a/test/dialog.spec.ts b/test/dialog.spec.ts new file mode 100644 index 0000000000000..39339f1fa2f0b --- /dev/null +++ b/test/dialog.spec.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2018 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 sinon from 'sinon'; + +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Page.Events.Dialog', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should fire', async () => { + const { page } = getTestState(); + + const onDialog = sinon.stub().callsFake((dialog) => { + dialog.accept(); + }); + page.on('dialog', onDialog); + + await page.evaluate(() => alert('yo')); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]; + expect(dialog.type()).toBe('alert'); + expect(dialog.defaultValue()).toBe(''); + expect(dialog.message()).toBe('yo'); + }); + + itFailsFirefox('should allow accepting prompts', async () => { + const { page } = getTestState(); + + const onDialog = sinon.stub().callsFake((dialog) => { + dialog.accept('answer!'); + }); + page.on('dialog', onDialog); + + const result = await page.evaluate(() => prompt('question?', 'yes.')); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]; + expect(dialog.type()).toBe('prompt'); + expect(dialog.defaultValue()).toBe('yes.'); + expect(dialog.message()).toBe('question?'); + + expect(result).toBe('answer!'); + }); + it('should dismiss the prompt', async () => { + const { page } = getTestState(); + + page.on('dialog', (dialog) => { + dialog.dismiss(); + }); + const result = await page.evaluate(() => prompt('question?')); + expect(result).toBe(null); + }); +}); diff --git a/test/diffstyle.css b/test/diffstyle.css new file mode 100644 index 0000000000000..202e85f41a208 --- /dev/null +++ b/test/diffstyle.css @@ -0,0 +1,13 @@ +body { + font-family: monospace; + white-space: pre; +} + +ins { + background-color: #9cffa0; + text-decoration: none; +} + +del { + background-color: #ff9e9e; +} diff --git a/test/drag-and-drop.spec.ts b/test/drag-and-drop.spec.ts new file mode 100644 index 0000000000000..01f2257f43c33 --- /dev/null +++ b/test/drag-and-drop.spec.ts @@ -0,0 +1,137 @@ +/** + * Copyright 2021 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 { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + describeChromeOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Input.drag', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should throw an exception if not enabled before usage', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + const draggable = await page.$('#drag'); + + try { + await draggable.drag({ x: 1, y: 1 }); + } catch (error) { + expect(error.message).toContain('Drag Interception is not enabled!'); + } + }); + it('should emit a dragIntercepted event when dragged', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + const data = await draggable.drag({ x: 1, y: 1 }); + + expect(data.items.length).toBe(1); + expect(await page.evaluate(() => globalThis.didDragStart)).toBe(true); + }); + it('should emit a dragEnter', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + const data = await draggable.drag({ x: 1, y: 1 }); + const dropzone = await page.$('#drop'); + await dropzone.dragEnter(data); + + expect(await page.evaluate(() => globalThis.didDragStart)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragEnter)).toBe(true); + }); + it('should emit a dragOver event', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + const data = await draggable.drag({ x: 1, y: 1 }); + const dropzone = await page.$('#drop'); + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + + expect(await page.evaluate(() => globalThis.didDragStart)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragEnter)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragOver)).toBe(true); + }); + it('can be dropped', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + const dropzone = await page.$('#drop'); + const data = await draggable.drag({ x: 1, y: 1 }); + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + await dropzone.drop(data); + + expect(await page.evaluate(() => globalThis.didDragStart)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragEnter)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragOver)).toBe(true); + expect(await page.evaluate(() => globalThis.didDrop)).toBe(true); + }); + it('can be dragged and dropped with a single function', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + const dropzone = await page.$('#drop'); + await draggable.dragAndDrop(dropzone); + + expect(await page.evaluate(() => globalThis.didDragStart)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragEnter)).toBe(true); + expect(await page.evaluate(() => globalThis.didDragOver)).toBe(true); + expect(await page.evaluate(() => globalThis.didDrop)).toBe(true); + }); + it('can be disabled', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + const draggable = await page.$('#drag'); + await draggable.drag({ x: 1, y: 1 }); + await page.setDragInterception(false); + + try { + await draggable.drag({ x: 1, y: 1 }); + } catch (error) { + expect(error.message).toContain('Drag Interception is not enabled!'); + } + }); +}); diff --git a/test/elementhandle.spec.ts b/test/elementhandle.spec.ts new file mode 100644 index 0000000000000..e46e6ca0d0e2a --- /dev/null +++ b/test/elementhandle.spec.ts @@ -0,0 +1,557 @@ +/** + * Copyright 2018 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 sinon from 'sinon'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +import utils from './utils.js'; +import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('ElementHandle.boundingBox', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const elementHandle = await page.$('.box:nth-of-type(13)'); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 100, y: 50, width: 50, height: 50 }); + }); + it('should handle nested frames', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const nestedFrame = page.frames()[1].childFrames()[1]; + const elementHandle = await nestedFrame.$('div'); + const box = await elementHandle.boundingBox(); + if (isChrome) + expect(box).toEqual({ x: 28, y: 182, width: 264, height: 18 }); + else expect(box).toEqual({ x: 28, y: 182, width: 254, height: 18 }); + }); + it('should return null for invisible elements', async () => { + const { page } = getTestState(); + + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent( + '
hello
' + ); + const elementHandle = await page.$('div'); + await page.evaluate<(element: HTMLElement) => void>( + (element) => (element.style.height = '200px'), + elementHandle + ); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({ x: 8, y: 8, width: 100, height: 200 }); + }); + it('should work with SVG nodes', async () => { + const { page } = getTestState(); + + await page.setContent(` + + + + `); + const element = await page.$('#therect'); + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate((e: HTMLElement) => { + const rect = e.getBoundingClientRect(); + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describeFailsFirefox('ElementHandle.boxModel', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/resetcss.html'); + + // Step 1: Add Frame and position it absolutely. + await utils.attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html'); + await page.evaluate(() => { + const frame = document.querySelector('#frame1'); + frame.style.position = 'absolute'; + frame.style.left = '1px'; + frame.style.top = '2px'; + }); + + // Step 2: Add div and position it absolutely inside frame. + const frame = page.frames()[1]; + const divHandle = ( + await frame.evaluateHandle(() => { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style.boxSizing = 'border-box'; + div.style.position = 'absolute'; + div.style.borderLeft = '1px solid black'; + div.style.paddingLeft = '2px'; + div.style.marginLeft = '3px'; + div.style.left = '4px'; + div.style.top = '5px'; + div.style.width = '6px'; + div.style.height = '7px'; + return div; + }) + ).asElement(); + + // Step 3: query div's boxModel and assert box values. + const box = await divHandle.boxModel(); + expect(box.width).toBe(6); + expect(box.height).toBe(7); + expect(box.margin[0]).toEqual({ + x: 1 + 4, // frame.left + div.left + y: 2 + 5, + }); + expect(box.border[0]).toEqual({ + x: 1 + 4 + 3, // frame.left + div.left + div.margin-left + y: 2 + 5, + }); + expect(box.padding[0]).toEqual({ + x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft + y: 2 + 5, + }); + expect(box.content[0]).toEqual({ + x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + dif.paddingLeft + y: 2 + 5, + }); + }); + + it('should return null for invisible elements', async () => { + const { page } = getTestState(); + + await page.setContent('
hi
'); + const element = await page.$('div'); + expect(await element.boxModel()).toBe(null); + }); + }); + + describe('ElementHandle.contentFrame', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = await page.$('#frame1'); + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.click', function () { + // See https://github.com/puppeteer/puppeteer/issues/7175 + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await button.click(); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should work for Shadow DOM v1', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + const buttonHandle = await page.evaluateHandle( + // @ts-expect-error button is expected to be in the page's scope. + () => button + ); + await buttonHandle.click(); + expect( + await page.evaluate( + // @ts-expect-error clicked is expected to be in the page's scope. + () => clicked + ) + ).toBe(true); + }); + it('should work for TextNodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle( + () => document.querySelector('button').firstChild + ); + let error = null; + await buttonTextNode.click().catch((error_) => (error = error_)); + expect(error.message).toBe('Node is not of type HTMLElement'); + }); + it('should throw for detached nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate((button: HTMLElement) => button.remove(), button); + let error = null; + await button.click().catch((error_) => (error = error_)); + expect(error.message).toBe('Node is detached from document'); + }); + it('should throw for hidden nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + it('should throw for recursively hidden nodes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.parentElement.style.display = 'none'), + button + ); + const error = await button.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + it('should throw for
elements', async () => { + const { page } = getTestState(); + + await page.setContent('hello
goodbye'); + const br = await page.$('br'); + const error = await br.click().catch((error_) => error_); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + }); + + describe('Element.waitForSelector', () => { + it('should wait correctly with waitForSelector on an element', async () => { + const { page } = getTestState(); + const waitFor = page.waitForSelector('.foo'); + // Set the page content after the waitFor has been started. + await page.setContent( + '
bar2
Foo1
' + ); + let element = await waitFor; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('.bar'); + await element.evaluate((el) => { + el.innerHTML = '
bar1
'; + }); + element = await innerWaitFor; + expect(element).toBeDefined(); + expect( + await element.evaluate((el: HTMLElement) => el.innerText) + ).toStrictEqual('bar1'); + }); + }); + + describe('Element.waitForXPath', () => { + it('should wait correctly with waitForXPath on an element', async () => { + const { page } = getTestState(); + // Set the page content after the waitFor has been started. + await page.setContent( + `
+ el1 +
+ el2 +
+
+
+ el3 +
` + ); + + const el2 = await page.waitForSelector('#el1'); + + expect( + await (await el2.waitForXPath('//div')).evaluate((el) => el.id) + ).toStrictEqual('el2'); + + expect( + await (await el2.waitForXPath('.//div')).evaluate((el) => el.id) + ).toStrictEqual('el2'); + }); + }); + + describe('ElementHandle.hover', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + const button = await page.$('#button-6'); + await button.hover(); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + }); + }); + + describe('ElementHandle.isIntersectingViewport', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + for (let i = 0; i < 11; ++i) { + const button = await page.$('#btn' + i); + // All but last button are visible. + const visible = i < 10; + expect(await button.isIntersectingViewport()).toBe(visible); + } + }); + it('should work with threshold', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + const button = await page.$('#btn11'); + expect( + await button.isIntersectingViewport({ + threshold: 0.001, + }) + ).toBe(false); + }); + it('should work with threshold of 1', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + const button = await page.$('#btn0'); + expect( + await button.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + }); + }); + + describe('Custom queries', function () { + this.afterEach(() => { + const { puppeteer } = getTestState(); + puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent('
'); + + // Register. + puppeteer.registerCustomQueryHandler('getById', { + queryOne: (element, selector) => + document.querySelector(`[id="${selector}"]`), + }); + const element = await page.$('getById/foo'); + expect( + await page.evaluate<(element: HTMLElement) => string>( + (element) => element.id, + element + ) + ).toBe('foo'); + const handlerNamesAfterRegistering = puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterRegistering.includes('getById')).toBeTruthy(); + + // Unregister. + puppeteer.unregisterCustomQueryHandler('getById'); + try { + await page.$('getById/foo'); + throw new Error('Custom query handler name not set - throw expected'); + } catch (error) { + expect(error).toStrictEqual( + new Error( + 'Query set to use "getById", but no query handler of that name was found' + ) + ); + } + const handlerNamesAfterUnregistering = + puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); + }); + it('should throw with invalid query names', () => { + try { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('1/2/3', { + queryOne: () => document.querySelector('foo'), + }); + throw new Error( + 'Custom query handler name was invalid - throw expected' + ); + } catch (error) { + expect(error).toStrictEqual( + new Error('Custom query handler names may only contain [a-zA-Z]') + ); + } + }); + it('should work for multiple elements', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$('getByClass/foo'); + const classNames = await Promise.all( + elements.map( + async (element) => + await page.evaluate<(element: HTMLElement) => string>( + (element) => element.className, + element + ) + ) + ); + + expect(classNames).toStrictEqual(['foo', 'foo baz']); + }); + it('should eval correctly', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
Foo1
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (element, selector) => + document.querySelectorAll(`.${selector}`), + }); + const elements = await page.$$eval( + 'getByClass/foo', + (divs) => divs.length + ); + + expect(elements).toBe(2); + }); + it('should wait correctly with waitForSelector', async () => { + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '
Foo1
' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + + it('should wait correctly with waitForSelector on an element', async () => { + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '
bar2
Foo1
' + ); + let element = await waitFor; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('getByClass/bar'); + + await element.evaluate((el) => { + el.innerHTML = '
bar1
'; + }); + + element = await innerWaitFor; + expect(element).toBeDefined(); + expect( + await element.evaluate((el: HTMLElement) => el.innerText) + ).toStrictEqual('bar1'); + }); + + it('should wait correctly with waitFor', async () => { + /* page.waitFor is deprecated so we silence the warning to avoid test noise */ + sinon.stub(console, 'warn').callsFake(() => {}); + const { page, puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + }); + const waitFor = page.waitFor('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '
Foo1
' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + it('should work when both queryOne and queryAll are registered', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
Foo2
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const element = await page.$('getByClass/foo'); + expect(element).toBeDefined(); + + const elements = await page.$$('getByClass/foo'); + expect(elements.length).toBe(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const { page, puppeteer } = getTestState(); + await page.setContent( + '
text
content
' + ); + puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => element.querySelector(`.${selector}`), + queryAll: (element, selector) => + element.querySelectorAll(`.${selector}`), + }); + + const txtContent = await page.$eval( + 'getByClass/foo', + (div) => div.textContent + ); + expect(txtContent).toBe('text'); + + const txtContents = await page.$$eval('getByClass/foo', (divs) => + divs.map((d) => d.textContent).join('') + ); + expect(txtContents).toBe('textcontent'); + }); + }); +}); diff --git a/test/emulation.spec.ts b/test/emulation.spec.ts new file mode 100644 index 0000000000000..ded262a77c170 --- /dev/null +++ b/test/emulation.spec.ts @@ -0,0 +1,420 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + let iPhone; + let iPhoneLandscape; + + before(() => { + const { puppeteer } = getTestState(); + iPhone = puppeteer.devices['iPhone 6']; + iPhoneLandscape = puppeteer.devices['iPhone 6 landscape']; + }); + + describe('Page.viewport', function () { + it('should get the proper viewport size', async () => { + const { page } = getTestState(); + + expect(page.viewport()).toEqual({ width: 800, height: 600 }); + await page.setViewport({ width: 123, height: 456 }); + expect(page.viewport()).toEqual({ width: 123, height: 456 }); + }); + it('should support mobile emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => window.innerWidth)).toBe(800); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + await page.setViewport({ width: 400, height: 300 }); + expect(await page.evaluate(() => window.innerWidth)).toBe(400); + }); + it('should support touch emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + await page.setViewport(iPhone.viewport); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); + + function dispatchTouch() { + let fulfill; + const promise = new Promise((x) => (fulfill = x)); + window.ontouchstart = () => { + fulfill('Received touch'); + }; + window.dispatchEvent(new Event('touchstart')); + + fulfill('Did not receive touch'); + + return promise; + } + }); + it('should be detectable by Modernizr', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe( + 'NO' + ); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect(await page.evaluate(() => document.body.textContent.trim())).toBe( + 'YES' + ); + }); + it('should detect touch when applying viewport with touches', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 800, height: 600, hasTouch: true }); + await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' }); + expect(await page.evaluate(() => globalThis.Modernizr.touchevents)).toBe( + true + ); + }); + itFailsFirefox('should support landscape emulation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'portrait-primary' + ); + await page.setViewport(iPhoneLandscape.viewport); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'landscape-primary' + ); + await page.setViewport({ width: 100, height: 100 }); + expect(await page.evaluate(() => screen.orientation.type)).toBe( + 'portrait-primary' + ); + }); + }); + + describe('Page.emulate', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + await page.emulate(iPhone); + expect(await page.evaluate(() => window.innerWidth)).toBe(375); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + itFailsFirefox('should support clicking', async () => { + const { page, server } = getTestState(); + + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + const button = await page.$('button'); + await page.evaluate( + (button: HTMLElement) => (button.style.marginTop = '200px'), + button + ); + await button.click(); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + }); + + describe('Page.emulateMediaType', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + await page.emulateMediaType('print'); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + false + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true); + await page.emulateMediaType(null); + expect(await page.evaluate(() => matchMedia('screen').matches)).toBe( + true + ); + expect(await page.evaluate(() => matchMedia('print').matches)).toBe( + false + ); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page.emulateMediaType('bad').catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe('Page.emulateMediaFeatures', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + await page.emulateMediaFeatures([ + { name: 'prefers-reduced-motion', value: 'reduce' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: reduce)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: no-preference)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'light' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: light)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-color-scheme', value: 'dark' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: light)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'prefers-reduced-motion', value: 'reduce' }, + { name: 'prefers-color-scheme', value: 'light' }, + ]); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: reduce)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-reduced-motion: no-preference)').matches + ) + ).toBe(false); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: light)').matches + ) + ).toBe(true); + expect( + await page.evaluate( + () => matchMedia('(prefers-color-scheme: dark)').matches + ) + ).toBe(false); + await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'srgb' }]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(false); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(false); + await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'p3' }]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(false); + await page.emulateMediaFeatures([ + { name: 'color-gamut', value: 'rec2020' }, + ]); + expect( + await page.evaluate(() => matchMedia('(color-gamut: p3)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches) + ).toBe(true); + expect( + await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches) + ).toBe(true); + }); + it('should throw in case of bad argument', async () => { + const { page } = getTestState(); + + let error = null; + await page + .emulateMediaFeatures([{ name: 'bad', value: '' }]) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describeFailsFirefox('Page.emulateTimezone', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + globalThis.date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)' + ); + + await page.emulateTimezone('Pacific/Honolulu'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)' + ); + + await page.emulateTimezone('America/Buenos_Aires'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)' + ); + + await page.emulateTimezone('Europe/Berlin'); + expect(await page.evaluate(() => globalThis.date.toString())).toBe( + 'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)' + ); + }); + + it('should throw for invalid timezone IDs', async () => { + const { page } = getTestState(); + + let error = null; + await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_)); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + + describeFailsFirefox('Page.emulateVisionDeficiency', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + + { + await page.emulateVisionDeficiency('achromatopsia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png'); + } + + { + await page.emulateVisionDeficiency('blurredVision'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png'); + } + + { + await page.emulateVisionDeficiency('deuteranopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png'); + } + + { + await page.emulateVisionDeficiency('protanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-protanopia.png'); + } + + { + await page.emulateVisionDeficiency('tritanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png'); + } + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + }); + + it('should throw for invalid vision deficiencies', async () => { + const { page } = getTestState(); + + let error = null; + await page + // @ts-expect-error deliberately passign invalid deficiency + .emulateVisionDeficiency('invalid') + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unsupported vision deficiency: invalid'); + }); + }); + + describeFailsFirefox('Page.emulateNetworkConditions', function () { + it('should change navigator.connection.effectiveType', async () => { + const { page, puppeteer } = getTestState(); + + const slow3G = puppeteer.networkConditions['Slow 3G']; + const fast3G = puppeteer.networkConditions['Fast 3G']; + + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('4g'); + await page.emulateNetworkConditions(fast3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('3g'); + await page.emulateNetworkConditions(slow3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('2g'); + await page.emulateNetworkConditions(null); + }); + }); + + describeFailsFirefox('Page.emulateCPUThrottling', function () { + it('should change the CPU throttling rate successfully', async () => { + const { page } = getTestState(); + + await page.emulateCPUThrottling(100); + await page.emulateCPUThrottling(null); + }); + }); +}); diff --git a/test/evaluation.spec.ts b/test/evaluation.spec.ts new file mode 100644 index 0000000000000..5ff363f1d1c60 --- /dev/null +++ b/test/evaluation.spec.ts @@ -0,0 +1,476 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const bigint = typeof BigInt !== 'undefined'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => 7 * 3); + expect(result).toBe(21); + }); + (bigint ? it : xit)('should transfer BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a: BigInt) => a, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a, [1, 2, 3]); + expect(result).toEqual([1, 2, 3]); + }); + it('should transfer arrays as arrays, not objects', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => Array.isArray(a), [1, 2, 3]); + expect(result).toBe(true); + }); + it('should modify global environment', async () => { + const { page } = getTestState(); + + await page.evaluate(() => (globalThis.globalVar = 123)); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should evaluate in the page context', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + itFailsFirefox( + 'should return undefined for objects with symbols', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined); + } + ); + it('should work with function shorthands', async () => { + const { page } = getTestState(); + + const a = { + sum(a, b) { + return a + b; + }, + + async mult(a, b) { + return a * b; + }, + }; + expect(await page.evaluate(a.sum, 1, 2)).toBe(3); + expect(await page.evaluate(a.mult, 2, 4)).toBe(8); + }); + it('should work with unicode chars', async () => { + const { page } = getTestState(); + + const result = await page.evaluate((a) => a['中文字符'], { + 中文字符: 42, + }); + expect(result).toBe(42); + }); + itFailsFirefox('should throw when evaluation triggers reload', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + location.reload(); + return new Promise(() => {}); + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => Promise.resolve(8 * 7)); + expect(result).toBe(56); + }); + it('should work right after framenavigated', async () => { + const { page, server } = getTestState(); + + let frameEvaluation = null; + page.on('framenavigated', async (frame) => { + frameEvaluation = frame.evaluate(() => 6 * 7); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + itFailsFirefox('should work from-inside an exposed function', async () => { + const { page } = getTestState(); + + // Setup inpage callback, which calls Page.evaluate + await page.exposeFunction('callController', async function (a, b) { + return await page.evaluate<(a: number, b: number) => number>( + (a, b) => a * b, + a, + b + ); + }); + const result = await page.evaluate(async function () { + return await globalThis.callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const { page } = getTestState(); + + let error = null; + await page + // @ts-expect-error we know the object doesn't exist + .evaluate(() => notExistingObject.property) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('notExistingObject'); + }); + it('should support thrown strings as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate(() => { + throw 100500; + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('100500'); + }); + it('should return complex objects', async () => { + const { page } = getTestState(); + + const object = { foo: 'bar!' }; + const result = await page.evaluate((a) => a, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + (bigint ? it : xit)('should return BigInt', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "undefined" as one of multiple parameters', async () => { + const { page } = getTestState(); + + const result = await page.evaluate( + (a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), + undefined, + 'foo' + ); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => ({ a: undefined }))).toEqual({}); + }); + itFailsFirefox( + 'should return undefined for non-serializable objects', + async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => window)).toBe(undefined); + } + ); + itFailsFirefox('should fail for circular object', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => { + const a: { [x: string]: any } = {}; + const b = { a }; + a.b = b; + return a; + }); + expect(result).toBe(undefined); + }); + itFailsFirefox('should be able to throw a tricky error', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + const errorText = await windowHandle + .jsonValue() + .catch((error_) => error_.message); + const error = await page + .evaluate<(errorText: string) => Error>((errorText) => { + throw new Error(errorText); + }, errorText) + .catch((error_) => error_); + expect(error.message).toContain(errorText); + }); + it('should accept a string', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async () => { + const { page } = getTestState(); + + const result = await page.evaluate('2 + 5;\n// do some math!'); + expect(result).toBe(7); + }); + it('should accept element handle as an argument', async () => { + const { page } = getTestState(); + + await page.setContent('
42
'); + const element = await page.$('section'); + const text = await page.evaluate<(e: HTMLElement) => string>( + (e) => e.textContent, + element + ); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async () => { + const { page } = getTestState(); + + await page.setContent('
39
'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + await element.dispose(); + let error = null; + await page + .evaluate((e: HTMLElement) => e.textContent, element) + .catch((error_) => (error = error_)); + expect(error.message).toContain('JSHandle is disposed'); + }); + itFailsFirefox( + 'should throw if elementHandles are from other frames', + async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1].$('body'); + let error = null; + await page + .evaluate((body: HTMLElement) => body.innerHTML, bodyHandle) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'JSHandles can be evaluated only in the context they were created' + ); + } + ); + itFailsFirefox('should simulate a user gesture', async () => { + const { page } = getTestState(); + + const result = await page.evaluate(() => { + document.body.appendChild(document.createTextNode('test')); + document.execCommand('selectAll'); + return document.execCommand('copy'); + }); + expect(result).toBe(true); + }); + itFailsFirefox('should throw a nice error after a navigation', async () => { + const { page } = getTestState(); + + const executionContext = await page.mainFrame().executionContext(); + + await Promise.all([ + page.waitForNavigation(), + executionContext.evaluate(() => window.location.reload()), + ]); + const error = await executionContext + .evaluate(() => null) + .catch((error_) => error_); + expect((error as Error).message).toContain('navigation'); + }); + itFailsFirefox( + 'should not throw an error when evaluation does a navigation', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/one-style.html'); + const result = await page.evaluate(() => { + (window as any).location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + } + ); + it('should transfer 100Mb of data from page to node.js', async function () { + const { page } = getTestState(); + + const a = await page.evaluate<() => string>(() => + Array(100 * 1024 * 1024 + 1).join('a') + ); + expect(a.length).toBe(100 * 1024 * 1024); + }); + it('should throw error with detailed information on exception inside promise ', async () => { + const { page } = getTestState(); + + let error = null; + await page + .evaluate( + () => + new Promise(() => { + throw new Error('Error in promise'); + }) + ) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Error in promise'); + }); + }); + + describeFailsFirefox('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const { page, server } = getTestState(); + + await page.evaluateOnNewDocument(function () { + globalThis.injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => globalThis.result)).toBe(123); + }); + it('should work with CSP', async () => { + const { page, server } = getTestState(); + + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.evaluateOnNewDocument(function () { + globalThis.injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(await page.evaluate(() => globalThis.injected)).toBe(123); + + // Make sure CSP works. + await page + .addScriptTag({ content: 'window.e = 10;' }) + .catch((error) => void error); + expect(await page.evaluate(() => (window as any).e)).toBe(undefined); + }); + }); + + describe('Frame.evaluate', function () { + it('should have different execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + await page.frames()[0].evaluate(() => (globalThis.FOO = 'foo')); + await page.frames()[1].evaluate(() => (globalThis.FOO = 'bar')); + expect(await page.frames()[0].evaluate(() => globalThis.FOO)).toBe('foo'); + expect(await page.frames()[1].evaluate(() => globalThis.FOO)).toBe('bar'); + }); + it('should have correct execution contexts', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames().length).toBe(2); + expect( + await page.frames()[0].evaluate(() => document.body.textContent.trim()) + ).toBe(''); + expect( + await page.frames()[1].evaluate(() => document.body.textContent.trim()) + ).toBe(`Hi, I'm frame`); + }); + it('should execute after cross-site navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect(await mainFrame.evaluate(() => window.location.href)).toContain( + 'localhost' + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(await mainFrame.evaluate(() => window.location.href)).toContain( + '127' + ); + }); + }); +}); diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts new file mode 100644 index 0000000000000..e2c76f67231aa --- /dev/null +++ b/test/fixtures.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2019 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. + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ + +import expect from 'expect'; +import { getTestState, itHeadlessOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +import path from 'path'; + +describe('Fixtures', function () { + itHeadlessOnly('dumpio option should work with pipe option ', async () => { + const { defaultBrowserOptions, puppeteerPath } = getTestState(); + + let dumpioData = ''; + const { spawn } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { + pipe: true, + dumpio: true, + }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', (data) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => res.on('close', resolve)); + expect(dumpioData).toContain('message from dumpio'); + }); + it('should dump browser process stderr', async () => { + const { defaultBrowserOptions, puppeteerPath } = getTestState(); + + let dumpioData = ''; + const { spawn } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { dumpio: true }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', (data) => (dumpioData += data.toString('utf8'))); + await new Promise((resolve) => res.on('close', resolve)); + expect(dumpioData).toContain('DevTools listening on ws://'); + }); + it('should close the browser when the node process closes', async () => { + const { defaultBrowserOptions, puppeteerPath, puppeteer } = getTestState(); + + const { spawn, execSync } = require('child_process'); + const options = Object.assign({}, defaultBrowserOptions, { + // Disable DUMPIO to cleanly read stdout. + dumpio: false, + }); + const res = spawn('node', [ + path.join(__dirname, 'fixtures', 'closeme.js'), + puppeteerPath, + JSON.stringify(options), + ]); + let wsEndPointCallback; + const wsEndPointPromise = new Promise( + (x) => (wsEndPointCallback = x) + ); + let output = ''; + res.stdout.on('data', (data) => { + output += data; + if (output.indexOf('\n')) + wsEndPointCallback(output.substring(0, output.indexOf('\n'))); + }); + const browser = await puppeteer.connect({ + browserWSEndpoint: await wsEndPointPromise, + }); + const promises = [ + new Promise((resolve) => browser.once('disconnected', resolve)), + new Promise((resolve) => res.on('close', resolve)), + ]; + if (process.platform === 'win32') + execSync(`taskkill /pid ${res.pid} /T /F`); + else process.kill(res.pid); + await Promise.all(promises); + }); +}); diff --git a/test/fixtures/closeme.js b/test/fixtures/closeme.js new file mode 100644 index 0000000000000..dbe798f70dfa7 --- /dev/null +++ b/test/fixtures/closeme.js @@ -0,0 +1,5 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + console.log(browser.wsEndpoint()); +})(); diff --git a/test/fixtures/dumpio.js b/test/fixtures/dumpio.js new file mode 100644 index 0000000000000..40b9714f6caeb --- /dev/null +++ b/test/fixtures/dumpio.js @@ -0,0 +1,8 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => console.error('message from dumpio')); + await page.close(); + await browser.close(); +})(); diff --git a/test/frame.spec.ts b/test/frame.spec.ts new file mode 100644 index 0000000000000..94bf572153641 --- /dev/null +++ b/test/frame.spec.ts @@ -0,0 +1,298 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { CDPSession } from '../lib/cjs/puppeteer/common/Connection.js'; + +describe('Frame specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.executionContext', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + const [frame1, frame2] = page.frames(); + const context1 = await frame1.executionContext(); + const context2 = await frame2.executionContext(); + expect(context1).toBeTruthy(); + expect(context2).toBeTruthy(); + expect(context1 !== context2).toBeTruthy(); + expect(context1.frame()).toBe(frame1); + expect(context2.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => (globalThis.a = 1)), + context2.evaluate(() => (globalThis.a = 2)), + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => globalThis.a), + context2.evaluate(() => globalThis.a), + ]); + expect(a1).toBe(1); + expect(a2).toBe(2); + }); + }); + + describe('Frame.evaluateHandle', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + const windowHandle = await mainFrame.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + itFailsFirefox('should throw for detached frames', async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.detachFrame(page, 'frame1'); + let error = null; + await frame1.evaluate(() => 7 * 8).catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Execution context is not available in detached frame' + ); + }); + + it('allows readonly array to be an argument', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + + // This test checks if Frame.evaluate allows a readonly array to be an argument. + // See https://github.com/puppeteer/puppeteer/issues/6953. + const readonlyArray: readonly string[] = ['a', 'b', 'c']; + await mainFrame.evaluate((arr) => arr, readonlyArray); + }); + }); + + describe('Frame Management', function () { + itFailsFirefox('should handle nested frames', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(utils.dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + }); + itFailsFirefox( + 'should send events when frames are manipulated dynamically', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + await utils.attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames.length).toBe(1); + expect(attachedFrames[0].url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames = []; + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await utils.navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames.length).toBe(1); + expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames = []; + page.on('framedetached', (frame) => detachedFrames.push(frame)); + await utils.detachFrame(page, 'frame1'); + expect(detachedFrames.length).toBe(1); + expect(detachedFrames[0].isDetached()).toBe(true); + } + ); + it('should send "framenavigated" when navigating on anchor URLs', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.goto(server.EMPTY_PAGE + '#foo'), + utils.waitEvent(page, 'framenavigated'), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + }); + it('should persist mainFrame on cross-process navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(page.mainFrame() === mainFrame).toBeTruthy(); + }); + it('should not send attach/detach events for main frame', async () => { + const { page, server } = getTestState(); + + let hasEvents = false; + page.on('frameattached', () => (hasEvents = true)); + page.on('framedetached', () => (hasEvents = true)); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + it('should detach child frames on navigation', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should support framesets', async () => { + const { page, server } = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', (frame) => attachedFrames.push(frame)); + page.on('framedetached', (frame) => detachedFrames.push(frame)); + page.on('framenavigated', (frame) => navigatedFrames.push(frame)); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + itFailsFirefox('should report frame from-inside shadow DOM', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + await page.evaluate(async (url: string) => { + const frame = document.createElement('iframe'); + frame.src = url; + document.body.shadowRoot.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + }, server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE); + }); + itFailsFirefox('should report frame.name()', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate((url: string) => { + const frame = document.createElement('iframe'); + frame.name = 'theFrameName'; + frame.src = url; + document.body.appendChild(frame); + return new Promise((x) => (frame.onload = x)); + }, server.EMPTY_PAGE); + expect(page.frames()[0].name()).toBe(''); + expect(page.frames()[1].name()).toBe('theFrameId'); + expect(page.frames()[2].name()).toBe('theFrameName'); + }); + itFailsFirefox('should report frame.parent()', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + expect(page.frames()[0].parentFrame()).toBe(null); + expect(page.frames()[1].parentFrame()).toBe(page.mainFrame()); + expect(page.frames()[2].parentFrame()).toBe(page.mainFrame()); + }); + itFailsFirefox( + 'should report different frame instance when frame re-attaches', + async () => { + const { page, server } = getTestState(); + + const frame1 = await utils.attachFrame( + page, + 'frame1', + server.EMPTY_PAGE + ); + await page.evaluate(() => { + globalThis.frame = document.querySelector('#frame1'); + globalThis.frame.remove(); + }); + expect(frame1.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.waitEvent(page, 'frameattached'), + page.evaluate(() => document.body.appendChild(globalThis.frame)), + ]); + expect(frame2.isDetached()).toBe(false); + expect(frame1).not.toBe(frame2); + } + ); + it('should support url fragment', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html'); + + expect(page.frames().length).toBe(2); + expect(page.frames()[1].url()).toBe( + server.PREFIX + '/frames/frame.html?param=value#fragment' + ); + }); + itFailsFirefox('should support lazy frames', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 1000, height: 1000 }); + await page.goto(server.PREFIX + '/frames/lazy-frame.html'); + + expect(page.frames().map((frame) => frame._hasStartedLoading)).toEqual([ + true, + true, + false, + ]); + }); + }); + + describe('Frame.client', function () { + it('should return the client instance', async () => { + const { page } = getTestState(); + expect(page.mainFrame().client()).toBeInstanceOf(CDPSession); + }); + }); +}); diff --git a/test/golden-chromium/csscoverage-involved.txt b/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000000000..9b851d0bd3c72 --- /dev/null +++ b/test/golden-chromium/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +] \ No newline at end of file diff --git a/test/golden-chromium/grid-cell-0.png b/test/golden-chromium/grid-cell-0.png new file mode 100644 index 0000000000000..ff282e989b7ea Binary files /dev/null and b/test/golden-chromium/grid-cell-0.png differ diff --git a/test/golden-chromium/grid-cell-1.png b/test/golden-chromium/grid-cell-1.png new file mode 100644 index 0000000000000..91a1cb8510a1d Binary files /dev/null and b/test/golden-chromium/grid-cell-1.png differ diff --git a/test/golden-chromium/grid-cell-2.png b/test/golden-chromium/grid-cell-2.png new file mode 100644 index 0000000000000..7b01753b6a63d Binary files /dev/null and b/test/golden-chromium/grid-cell-2.png differ diff --git a/test/golden-chromium/grid-cell-3.png b/test/golden-chromium/grid-cell-3.png new file mode 100644 index 0000000000000..b9b8b2922b380 Binary files /dev/null and b/test/golden-chromium/grid-cell-3.png differ diff --git a/test/golden-chromium/jscoverage-involved.txt b/test/golden-chromium/jscoverage-involved.txt new file mode 100644 index 0000000000000..6f28e1580ec9e --- /dev/null +++ b/test/golden-chromium/jscoverage-involved.txt @@ -0,0 +1,28 @@ +[ + { + "url": "http://localhost:/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 160 + }, + { + "start": 168, + "end": 207 + } + ], + "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" + } +] \ No newline at end of file diff --git a/test/golden-chromium/mock-binary-response.png b/test/golden-chromium/mock-binary-response.png new file mode 100644 index 0000000000000..8595e0598edf2 Binary files /dev/null and b/test/golden-chromium/mock-binary-response.png differ diff --git a/test/golden-chromium/screenshot-clip-odd-size.png b/test/golden-chromium/screenshot-clip-odd-size.png new file mode 100644 index 0000000000000..b010d1f87f0cd Binary files /dev/null and b/test/golden-chromium/screenshot-clip-odd-size.png differ diff --git a/test/golden-chromium/screenshot-clip-rect.png b/test/golden-chromium/screenshot-clip-rect.png new file mode 100644 index 0000000000000..ac23b7de502a0 Binary files /dev/null and b/test/golden-chromium/screenshot-clip-rect.png differ diff --git a/test/golden-chromium/screenshot-element-bounding-box.png b/test/golden-chromium/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000..32e05bf05b40c Binary files /dev/null and b/test/golden-chromium/screenshot-element-bounding-box.png differ diff --git a/test/golden-chromium/screenshot-element-fractional-offset.png b/test/golden-chromium/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000000..cc8669d598be1 Binary files /dev/null and b/test/golden-chromium/screenshot-element-fractional-offset.png differ diff --git a/test/golden-chromium/screenshot-element-fractional.png b/test/golden-chromium/screenshot-element-fractional.png new file mode 100644 index 0000000000000..35c53377f942c Binary files /dev/null and b/test/golden-chromium/screenshot-element-fractional.png differ diff --git a/test/golden-chromium/screenshot-element-larger-than-viewport.png b/test/golden-chromium/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000000..5fcdb923555dc Binary files /dev/null and b/test/golden-chromium/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden-chromium/screenshot-element-padding-border.png b/test/golden-chromium/screenshot-element-padding-border.png new file mode 100644 index 0000000000000..917dd48188d45 Binary files /dev/null and b/test/golden-chromium/screenshot-element-padding-border.png differ diff --git a/test/golden-chromium/screenshot-element-rotate.png b/test/golden-chromium/screenshot-element-rotate.png new file mode 100644 index 0000000000000..52e2a0f6d3c66 Binary files /dev/null and b/test/golden-chromium/screenshot-element-rotate.png differ diff --git a/test/golden-chromium/screenshot-element-scrolled-into-view.png b/test/golden-chromium/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000000..917dd48188d45 Binary files /dev/null and b/test/golden-chromium/screenshot-element-scrolled-into-view.png differ diff --git a/test/golden-chromium/screenshot-grid-fullpage.png b/test/golden-chromium/screenshot-grid-fullpage.png new file mode 100644 index 0000000000000..d6d38217f7f8f Binary files /dev/null and b/test/golden-chromium/screenshot-grid-fullpage.png differ diff --git a/test/golden-chromium/screenshot-offscreen-clip.png b/test/golden-chromium/screenshot-offscreen-clip.png new file mode 100644 index 0000000000000..e503f801ec6a1 Binary files /dev/null and b/test/golden-chromium/screenshot-offscreen-clip.png differ diff --git a/test/golden-chromium/screenshot-sanity.png b/test/golden-chromium/screenshot-sanity.png new file mode 100644 index 0000000000000..ecab61fe179e8 Binary files /dev/null and b/test/golden-chromium/screenshot-sanity.png differ diff --git a/test/golden-chromium/transparent.png b/test/golden-chromium/transparent.png new file mode 100644 index 0000000000000..1cf45d8688fc6 Binary files /dev/null and b/test/golden-chromium/transparent.png differ diff --git a/test/golden-chromium/vision-deficiency-achromatopsia.png b/test/golden-chromium/vision-deficiency-achromatopsia.png new file mode 100644 index 0000000000000..4d74aac44c04a Binary files /dev/null and b/test/golden-chromium/vision-deficiency-achromatopsia.png differ diff --git a/test/golden-chromium/vision-deficiency-blurredVision.png b/test/golden-chromium/vision-deficiency-blurredVision.png new file mode 100644 index 0000000000000..78979425a9738 Binary files /dev/null and b/test/golden-chromium/vision-deficiency-blurredVision.png differ diff --git a/test/golden-chromium/vision-deficiency-deuteranopia.png b/test/golden-chromium/vision-deficiency-deuteranopia.png new file mode 100644 index 0000000000000..79b4b0fa1bc6a Binary files /dev/null and b/test/golden-chromium/vision-deficiency-deuteranopia.png differ diff --git a/test/golden-chromium/vision-deficiency-protanopia.png b/test/golden-chromium/vision-deficiency-protanopia.png new file mode 100644 index 0000000000000..bede7c1ed050a Binary files /dev/null and b/test/golden-chromium/vision-deficiency-protanopia.png differ diff --git a/test/golden-chromium/vision-deficiency-tritanopia.png b/test/golden-chromium/vision-deficiency-tritanopia.png new file mode 100644 index 0000000000000..d5f6bbec2e858 Binary files /dev/null and b/test/golden-chromium/vision-deficiency-tritanopia.png differ diff --git a/test/golden-chromium/white.jpg b/test/golden-chromium/white.jpg new file mode 100644 index 0000000000000..fb9070def3dab Binary files /dev/null and b/test/golden-chromium/white.jpg differ diff --git a/test/golden-firefox/grid-cell-0.png b/test/golden-firefox/grid-cell-0.png new file mode 100644 index 0000000000000..4677bdbc4f849 Binary files /dev/null and b/test/golden-firefox/grid-cell-0.png differ diff --git a/test/golden-firefox/grid-cell-1.png b/test/golden-firefox/grid-cell-1.png new file mode 100644 index 0000000000000..532dc8db65b04 Binary files /dev/null and b/test/golden-firefox/grid-cell-1.png differ diff --git a/test/golden-firefox/screenshot-clip-odd-size.png b/test/golden-firefox/screenshot-clip-odd-size.png new file mode 100644 index 0000000000000..8e86dc90178ac Binary files /dev/null and b/test/golden-firefox/screenshot-clip-odd-size.png differ diff --git a/test/golden-firefox/screenshot-clip-rect.png b/test/golden-firefox/screenshot-clip-rect.png new file mode 100644 index 0000000000000..7a744578693d3 Binary files /dev/null and b/test/golden-firefox/screenshot-clip-rect.png differ diff --git a/test/golden-firefox/screenshot-element-bounding-box.png b/test/golden-firefox/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000..f4e059c300ccd Binary files /dev/null and b/test/golden-firefox/screenshot-element-bounding-box.png differ diff --git a/test/golden-firefox/screenshot-element-fractional-offset.png b/test/golden-firefox/screenshot-element-fractional-offset.png new file mode 100644 index 0000000000000..f554b1d62c4ab Binary files /dev/null and b/test/golden-firefox/screenshot-element-fractional-offset.png differ diff --git a/test/golden-firefox/screenshot-element-fractional.png b/test/golden-firefox/screenshot-element-fractional.png new file mode 100644 index 0000000000000..d1431bd91dea1 Binary files /dev/null and b/test/golden-firefox/screenshot-element-fractional.png differ diff --git a/test/golden-firefox/screenshot-element-larger-than-viewport.png b/test/golden-firefox/screenshot-element-larger-than-viewport.png new file mode 100644 index 0000000000000..6d28cddcea336 Binary files /dev/null and b/test/golden-firefox/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden-firefox/screenshot-element-padding-border.png b/test/golden-firefox/screenshot-element-padding-border.png new file mode 100644 index 0000000000000..2b72c7528b256 Binary files /dev/null and b/test/golden-firefox/screenshot-element-padding-border.png differ diff --git a/test/golden-firefox/screenshot-element-rotate.png b/test/golden-firefox/screenshot-element-rotate.png new file mode 100644 index 0000000000000..0a78fb1ae7ba5 Binary files /dev/null and b/test/golden-firefox/screenshot-element-rotate.png differ diff --git a/test/golden-firefox/screenshot-element-scrolled-into-view.png b/test/golden-firefox/screenshot-element-scrolled-into-view.png new file mode 100644 index 0000000000000..2b72c7528b256 Binary files /dev/null and b/test/golden-firefox/screenshot-element-scrolled-into-view.png differ diff --git a/test/golden-firefox/screenshot-grid-fullpage.png b/test/golden-firefox/screenshot-grid-fullpage.png new file mode 100644 index 0000000000000..ac47ec83b19f2 Binary files /dev/null and b/test/golden-firefox/screenshot-grid-fullpage.png differ diff --git a/test/golden-firefox/screenshot-offscreen-clip.png b/test/golden-firefox/screenshot-offscreen-clip.png new file mode 100644 index 0000000000000..846b810386f7c Binary files /dev/null and b/test/golden-firefox/screenshot-offscreen-clip.png differ diff --git a/test/golden-firefox/screenshot-sanity.png b/test/golden-firefox/screenshot-sanity.png new file mode 100644 index 0000000000000..07890a04b342a Binary files /dev/null and b/test/golden-firefox/screenshot-sanity.png differ diff --git a/test/golden-utils.js b/test/golden-utils.js new file mode 100644 index 0000000000000..f820afe6bfbb7 --- /dev/null +++ b/test/golden-utils.js @@ -0,0 +1,160 @@ +// @ts-nocheck +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); +const fs = require('fs'); +const Diff = require('text-diff'); +const mime = require('mime'); +const PNG = require('pngjs').PNG; +const jpeg = require('jpeg-js'); +const pixelmatch = require('pixelmatch'); + +module.exports = { compare }; + +const GoldenComparators = { + 'image/png': compareImages, + 'image/jpeg': compareImages, + 'text/plain': compareText, +}; + +/** + * @param {?Object} actualBuffer + * @param {!Buffer} expectedBuffer + * @param {!string} mimeType + * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareImages(actualBuffer, expectedBuffer, mimeType) { + if (!actualBuffer || !(actualBuffer instanceof Buffer)) + return { errorMessage: 'Actual result should be Buffer.' }; + + const actual = + mimeType === 'image/png' + ? PNG.sync.read(actualBuffer) + : jpeg.decode(actualBuffer); + const expected = + mimeType === 'image/png' + ? PNG.sync.read(expectedBuffer) + : jpeg.decode(expectedBuffer); + if (expected.width !== actual.width || expected.height !== actual.height) { + return { + errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `, + }; + } + const diff = new PNG({ width: expected.width, height: expected.height }); + const count = pixelmatch( + expected.data, + actual.data, + diff.data, + expected.width, + expected.height, + { threshold: 0.1 } + ); + return count > 0 ? { diff: PNG.sync.write(diff) } : null; +} + +/** + * @param {?Object} actual + * @param {!Buffer} expectedBuffer + * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} + */ +function compareText(actual, expectedBuffer) { + if (typeof actual !== 'string') + return { errorMessage: 'Actual result should be string' }; + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) return null; + const diff = new Diff(); + const result = diff.main(expected, actual); + diff.cleanupSemantic(result); + let html = diff.prettyHtml(result); + const diffStylePath = path.join(__dirname, 'diffstyle.css'); + html = `` + html; + return { + diff: html, + diffExtension: '.html', + }; +} + +/** + * @param {?Object} actual + * @param {string} goldenName + * @returns {!{pass: boolean, message: (undefined|string)}} + */ +function compare(goldenPath, outputPath, actual, goldenName) { + goldenPath = path.normalize(goldenPath); + outputPath = path.normalize(outputPath); + const expectedPath = path.join(goldenPath, goldenName); + const actualPath = path.join(outputPath, goldenName); + + const messageSuffix = + 'Output is saved in "' + path.basename(outputPath + '" directory'); + + if (!fs.existsSync(expectedPath)) { + ensureOutputDir(); + fs.writeFileSync(actualPath, actual); + return { + pass: false, + message: goldenName + ' is missing in golden results. ' + messageSuffix, + }; + } + const expected = fs.readFileSync(expectedPath); + const mimeType = mime.getType(goldenName); + const comparator = GoldenComparators[mimeType]; + if (!comparator) { + return { + pass: false, + message: + 'Failed to find comparator with type ' + mimeType + ': ' + goldenName, + }; + } + const result = comparator(actual, expected, mimeType); + if (!result) return { pass: true }; + ensureOutputDir(); + if (goldenPath === outputPath) { + fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); + } else { + fs.writeFileSync(actualPath, actual); + // Copy expected to the output/ folder for convenience. + fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); + } + if (result.diff) { + const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); + fs.writeFileSync(diffPath, result.diff); + } + + let message = goldenName + ' mismatch!'; + if (result.errorMessage) message += ' ' + result.errorMessage; + return { + pass: false, + message: message + ' ' + messageSuffix, + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); + } +} + +/** + * @param {string} filePath + * @param {string} suffix + * @param {string=} customExtension + * @returns {string} + */ +function addSuffix(filePath, suffix, customExtension) { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +} diff --git a/test/headful.spec.ts b/test/headful.spec.ts new file mode 100644 index 0000000000000..b41eabc8ea934 --- /dev/null +++ b/test/headful.spec.ts @@ -0,0 +1,354 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import os from 'os'; +import fs from 'fs'; +import { promisify } from 'util'; +import expect from 'expect'; +import { + getTestState, + describeChromeOnly, + itFailsWindows, +} from './mocha-utils'; // eslint-disable-line import/extensions +import rimraf from 'rimraf'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, 'assets', 'simple-extension'); + +describeChromeOnly('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions; + let headlessOptions; + let extensionOptions; + let forcedOopifOptions; + let devtoolsOptions; + const browsers = []; + + beforeEach(() => { + const { server, defaultBrowserOptions } = getTestState(); + headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + }); + headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true, + }); + + extensionOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + + forcedOopifOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + args: [ + `--host-rules=MAP oopifdomain 127.0.0.1`, + `--isolate-origins=${server.PREFIX.replace( + 'localhost', + 'oopifdomain' + )}`, + ], + }); + + devtoolsOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + }); + }); + + async function launchBrowser(puppeteer, options) { + const browser = await puppeteer.launch(options); + browsers.push(browser); + return browser; + } + + afterEach(() => { + for (const i in browsers) { + const browser = browsers[i]; + if (browser.isConnected()) { + browser.close(); + } + delete browsers[i]; + } + }); + + describe('HEADFUL', function () { + it('background_page target type should be available', async () => { + const { puppeteer } = getTestState(); + const browserWithExtension = await launchBrowser( + puppeteer, + extensionOptions + ); + const page = await browserWithExtension.newPage(); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + it('target.page() should return a background_page', async function () { + const { puppeteer } = getTestState(); + const browserWithExtension = await launchBrowser( + puppeteer, + extensionOptions + ); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + (target) => target.type() === 'background_page' + ); + const page = await backgroundPageTarget.page(); + expect(await page.evaluate(() => 2 * 3)).toBe(6); + expect(await page.evaluate(() => globalThis.MAGIC)).toBe(42); + await browserWithExtension.close(); + }); + it('target.page() should return a DevTools page if custom isPageTarget is provided', async function () { + const { puppeteer } = getTestState(); + const originalBrowser = await launchBrowser(puppeteer, devtoolsOptions); + + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + isPageTarget: (target) => { + return ( + target.type === 'other' && target.url.startsWith('devtools://') + ); + }, + }); + const devtoolsPageTarget = await browser.waitForTarget( + (target) => target.type() === 'other' + ); + const page = await devtoolsPageTarget.page(); + expect(await page.evaluate(() => 2 * 3)).toBe(6); + await browser.close(); + }); + it('should have default url when launching browser', async function () { + const { puppeteer } = getTestState(); + const browser = await launchBrowser(puppeteer, extensionOptions); + const pages = (await browser.pages()).map((page) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + itFailsWindows( + 'headless should be able to read cookies written by headful', + async () => { + /* Needs investigation into why but this fails consistently on Windows CI. */ + const { server, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await launchBrowser( + puppeteer, + Object.assign({ userDataDir }, headfulOptions) + ); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate( + () => + (document.cookie = + 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT') + ); + await headfulBrowser.close(); + // Read the cookie from headless chrome + const headlessBrowser = await launchBrowser( + puppeteer, + Object.assign({ userDataDir }, headlessOptions) + ); + const headlessPage = await headlessBrowser.newPage(); + await headlessPage.goto(server.EMPTY_PAGE); + const cookie = await headlessPage.evaluate(() => document.cookie); + await headlessBrowser.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + expect(cookie).toBe('foo=true'); + } + ); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async () => { + const { server, puppeteer } = getTestState(); + + // https://google.com is isolated by default in Chromium embedder. + const browser = await launchBrowser(puppeteer, headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (r) => r.respond({ body: 'YO, GOOGLE.COM' })); + await page.evaluate(() => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', 'https://google.com/'); + document.body.appendChild(frame); + return new Promise((x) => (frame.onload = x)); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page + .frames() + .map((frame) => frame.url()) + .sort(); + expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); + await browser.close(); + }); + it('OOPIF: should expose events within OOPIFs', async () => { + const { server, puppeteer } = getTestState(); + + const browser = await launchBrowser(puppeteer, forcedOopifOptions); + const page = await browser.newPage(); + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents = []; + const otherSessions = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.on('sessionattached', async (session) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params) => + networkEvents.push(params) + ); + await session.send('Network.enable'); + await session.send('Runtime.runIfWaitingForDebugger'); + }); + + // Navigate to the empty page and add an OOPIF iframe with at least one request. + await page.goto(server.EMPTY_PAGE); + await page.evaluate((frameUrl) => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', frameUrl); + document.body.appendChild(frame); + return new Promise((x, y) => { + frame.onload = x; + frame.onerror = y; + }); + }, server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html'); + await page.waitForSelector('iframe'); + + // Ensure we found the iframe session. + expect(otherSessions).toHaveLength(1); + + // Resume the iframe and trigger another request. + const iframeSession = otherSessions[0]; + await iframeSession.send('Runtime.evaluate', { + expression: `fetch('/fetch')`, + awaitPromise: true, + }); + await browser.close(); + + const requests = networkEvents.map((event) => event.request.url); + expect(requests).toContain(`http://oopifdomain:${server.PORT}/fetch`); + }); + it('should close browser with beforeunload page', async () => { + const { server, puppeteer } = getTestState(); + + const browser = await launchBrowser(puppeteer, headfulOptions); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await page.click('body'); + await browser.close(); + }); + it('should open devtools when "devtools: true" option is given', async () => { + const { puppeteer } = getTestState(); + + const browser = await launchBrowser( + puppeteer, + Object.assign({ devtools: true }, headfulOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + browser.waitForTarget((target) => target.url().includes('devtools://')), + ]); + await browser.close(); + }); + }); + + describe('Page.bringToFront', function () { + it('should work', async () => { + const { puppeteer } = getTestState(); + const browser = await launchBrowser(puppeteer, headfulOptions); + const page1 = await browser.newPage(); + const page2 = await browser.newPage(); + + await page1.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + + await page2.bringToFront(); + expect(await page1.evaluate(() => document.visibilityState)).toBe( + 'hidden' + ); + expect(await page2.evaluate(() => document.visibilityState)).toBe( + 'visible' + ); + + await page1.close(); + await page2.close(); + await browser.close(); + }); + }); + + describe('Page.screenshot', function () { + it('should run in parallel in multiple pages', async () => { + const { server, puppeteer } = getTestState(); + const browser = await puppeteer.launch(headfulOptions); + const context = await browser.createIncognitoBrowserContext(); + + const N = 2; + const pages = await Promise.all( + Array(N) + .fill(0) + .map(async () => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + return page; + }) + ); + const promises = []; + for (let i = 0; i < N; ++i) + promises.push( + pages[i].screenshot({ + clip: { x: 50 * i, y: 0, width: 50, height: 50 }, + }) + ); + const screenshots = await Promise.all(promises); + for (let i = 0; i < N; ++i) + expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); + await Promise.all(pages.map((page) => page.close())); + + await browser.close(); + }); + }); +}); diff --git a/test/idle_override.spec.ts b/test/idle_override.spec.ts new file mode 100644 index 0000000000000..18e7da3986c70 --- /dev/null +++ b/test/idle_override.spec.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2020 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('Emulate idle state', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + async function getIdleState() { + const { page } = getTestState(); + + const stateElement = await page.$('#state'); + return await page.evaluate((element: HTMLElement) => { + return element.innerText; + }, stateElement); + } + + async function verifyState(expectedState: string) { + const actualState = await getIdleState(); + expect(actualState).toEqual(expectedState); + } + + it('changing idle state emulation causes change of the IdleDetector state', async () => { + const { page, server, context } = getTestState(); + await context.overridePermissions(server.PREFIX + '/idle-detector.html', [ + 'idle-detection', + ]); + + await page.goto(server.PREFIX + '/idle-detector.html'); + + // Store initial state, as soon as it is not guaranteed to be `active, unlocked`. + const initialState = await getIdleState(); + + // Emulate Idle states and verify IdleDetector updates state accordingly. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: false, + }); + await verifyState('Idle state: active, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: true, + }); + await verifyState('Idle state: active, unlocked.'); + + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: true, + }); + await verifyState('Idle state: idle, unlocked.'); + + // Remove Idle emulation and verify IdleDetector is in initial state. + await page.emulateIdleState(); + await verifyState(initialState); + + // Emulate idle state again after removing emulation. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState('Idle state: idle, locked.'); + + // Remove emulation second time. + await page.emulateIdleState(); + await verifyState(initialState); + }); +}); diff --git a/test/ignorehttpserrors.spec.ts b/test/ignorehttpserrors.spec.ts new file mode 100644 index 0000000000000..de6f05040c01f --- /dev/null +++ b/test/ignorehttpserrors.spec.ts @@ -0,0 +1,135 @@ +/** + * Copyright 2018 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 { + getTestState, + describeFailsFirefox, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('ignoreHTTPSErrors', function () { + /* Note that this test creates its own browser rather than use + * the one provided by the test set-up as we need one + * with ignoreHTTPSErrors set to true + */ + let browser; + let context; + let page; + + before(async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign( + { ignoreHTTPSErrors: true }, + defaultBrowserOptions + ); + browser = await puppeteer.launch(options); + }); + + after(async () => { + await browser.close(); + browser = null; + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + context = null; + page = null; + }); + + describeFailsFirefox('Response.securityDetails', function () { + it('should work', async () => { + const { httpsServer } = getTestState(); + + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE), + ]); + const securityDetails = response.securityDetails(); + expect(securityDetails.issuer()).toBe('puppeteer-tests'); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + expect(securityDetails.subjectName()).toBe('puppeteer-tests'); + expect(securityDetails.validFrom()).toBe(1589357069); + expect(securityDetails.validTo()).toBe(1904717069); + expect(securityDetails.subjectAlternativeNames()).toEqual([ + 'www.puppeteer-tests.test', + 'www.puppeteer-tests-1.test', + ]); + }); + it('should be |null| for non-secure requests', async () => { + const { server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async () => { + const { httpsServer } = getTestState(); + + httpsServer.setRedirect('/plzredirect', '/empty.html'); + const responses = []; + page.on('response', (response) => responses.push(response)); + const [serverRequest] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect'), + ]); + expect(responses.length).toBe(2); + expect(responses[0].status()).toBe(302); + const securityDetails = responses[0].securityDetails(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async () => { + const { httpsServer } = getTestState(); + + let error = null; + const response = await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + }); + itFailsFirefox('should work with request interception', async () => { + const { httpsServer } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.goto(httpsServer.EMPTY_PAGE); + expect(response.status()).toBe(200); + }); + itFailsFirefox('should work with mixed content', async () => { + const { server, httpsServer } = getTestState(); + + httpsServer.setRoute('/mixedcontent.html', (req, res) => { + res.end(``); + }); + await page.goto(httpsServer.PREFIX + '/mixedcontent.html', { + waitUntil: 'load', + }); + expect(page.frames().length).toBe(2); + // Make sure blocked iframe has functional execution context + // @see https://github.com/puppeteer/puppeteer/issues/2709 + expect(await page.frames()[0].evaluate('1 + 2')).toBe(3); + expect(await page.frames()[1].evaluate('2 + 3')).toBe(5); + }); +}); diff --git a/test/input.spec.ts b/test/input.spec.ts new file mode 100644 index 0000000000000..9244356333a34 --- /dev/null +++ b/test/input.spec.ts @@ -0,0 +1,343 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +const FILE_TO_UPLOAD = path.join(__dirname, '/assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('input', function () { + it('should upload the file', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const input = await page.$('input'); + await page.evaluate((e: HTMLElement) => { + globalThis._inputEvents = []; + e.addEventListener('change', (ev) => + globalThis._inputEvents.push(ev.type) + ); + e.addEventListener('input', (ev) => + globalThis._inputEvents.push(ev.type) + ); + }, input); + await input.uploadFile(filePath); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].name, input) + ).toBe('file-to-upload.txt'); + expect( + await page.evaluate((e: HTMLInputElement) => e.files[0].type, input) + ).toBe('text/plain'); + expect(await page.evaluate(() => globalThis._inputEvents)).toEqual([ + 'input', + 'change', + ]); + expect( + await page.evaluate((e: HTMLInputElement) => { + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(e.files[0]); + return promise.then(() => reader.result); + }, input) + ).toBe('contents of the file'); + }); + }); + + describeFailsFirefox('Page.waitForFileChooser', function () { + it('should work when file input is attached to DOM', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser).toBeTruthy(); + }); + it('should work when file input is not attached to DOM', async () => { + const { page } = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.evaluate(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFileChooser().catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(0); + let error = null; + await page + .waitForFileChooser({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const { page } = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({ timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }, 50) + ), + ]); + expect(chooser).toBeTruthy(); + }); + it('should return the same file chooser when there are many watchdogs simultaneously', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describeFailsFirefox('FileChooser.accept', function () { + it('should accept single file', async () => { + const { page } = getTestState(); + + await page.setContent( + `` + ); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + new Promise((x) => page.once('metrics', x)), + ]); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files.length + ) + ).toBe(1); + expect( + await page.$eval( + 'input', + (input: HTMLInputElement) => input.files[0].name + ) + ).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async () => { + const { page } = getTestState(); + + await page.setContent(``); + page + .waitForFileChooser() + .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + const reader = new FileReader(); + const promise = new Promise((fulfill) => (reader.onload = fulfill)); + reader.readAsText(picker.files[0]); + return promise.then(() => reader.result); + }) + ).toBe('contents of the file'); + }); + it('should be able to reset selected files with empty file list', async () => { + const { page } = getTestState(); + + await page.setContent(``); + page + .waitForFileChooser() + .then((chooser) => chooser.accept([FILE_TO_UPLOAD])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.files.length; + }) + ).toBe(1); + page.waitForFileChooser().then((chooser) => chooser.accept([])); + expect( + await page.$eval('input', async (picker: HTMLInputElement) => { + picker.click(); + await new Promise((x) => (picker.oninput = x)); + return picker.files.length; + }) + ).toBe(0); + }); + it('should not accept multiple files for single-file input', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser + .accept([ + path.relative( + process.cwd(), + __dirname + '/assets/file-to-upload.txt' + ), + path.relative(process.cwd(), __dirname + '/assets/pptr.png'), + ]) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + it('should fail for non-existent files', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error = null; + await chooser + .accept(['file-does-not-exist.txt']) + .catch((error_) => (error = error_)); + expect(error).not.toBe(null); + }); + it('should fail when accepting file chooser twice', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser.accept([]); + let error = null; + await fileChooser.accept([]).catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Cannot accept FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('FileChooser.cancel', function () { + it('should cancel dialog', async () => { + const { page } = getTestState(); + + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(``); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + }); + it('should fail when canceling file chooser twice', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', (input: HTMLInputElement) => input.click()), + ]); + await fileChooser.cancel(); + let error = null; + + try { + fileChooser.cancel(); + } catch (error_) { + error = error_; + } + + expect(error.message).toBe( + 'Cannot cancel FileChooser which is already handled!' + ); + }); + }); + + describeFailsFirefox('FileChooser.isMultiple', () => { + it('should work for single file pick', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async () => { + const { page } = getTestState(); + + await page.setContent(``); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}); diff --git a/test/jshandle.spec.ts b/test/jshandle.spec.ts new file mode 100644 index 0000000000000..ae1c186e43097 --- /dev/null +++ b/test/jshandle.spec.ts @@ -0,0 +1,401 @@ +/** + * Copyright 2018 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 { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + shortWaitForArrayToHaveAtLeastNElements, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('JSHandle', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const { page } = getTestState(); + + const windowHandle = await page.evaluateHandle(() => window); + expect(windowHandle).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const { page } = getTestState(); + + const navigatorHandle = await page.evaluateHandle(() => navigator); + const text = await page.evaluate( + (e: Navigator) => e.userAgent, + navigatorHandle + ); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 5); + const isFive = await page.evaluate((e) => Object.is(e, 5), aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn on nested object handles', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => document.body); + let error = null; + await page + // @ts-expect-error we are deliberately passing a bad type here (nested object) + .evaluateHandle((opts) => opts.elem.querySelector('p'), { + elem: aHandle, + }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Are you passing a nested JSHandle?'); + }); + it('should accept object handle to unserializable value', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => Infinity); + expect(await page.evaluate((e) => Object.is(e, Infinity), aHandle)).toBe( + true + ); + }); + it('should use the same JS wrappers', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + globalThis.FOO = 123; + return window; + }); + expect(await page.evaluate((e: { FOO: number }) => e.FOO, aHandle)).toBe( + 123 + ); + }); + }); + + describe('JSHandle.getProperty', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + one: 1, + two: 2, + three: 3, + })); + const twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + + it('should return a JSHandle even if the property does not exist', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + one: 1, + two: 2, + three: 3, + })); + const undefinedHandle = await aHandle.getProperty('doesnotexist'); + expect(undefinedHandle).toBeInstanceOf(JSHandle); + expect(await undefinedHandle.jsonValue()).toBe(undefined); + }); + }); + + describe('JSHandle.jsonValue', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ foo: 'bar' })); + const json = await aHandle.jsonValue>(); + expect(json).toEqual({ foo: 'bar' }); + }); + + it('works with jsonValues that are not objects', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ['a', 'b']); + const json = await aHandle.jsonValue(); + expect(json).toEqual(['a', 'b']); + }); + + it('works with jsonValues that are primitives', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 'foo'); + const json = await aHandle.jsonValue(); + expect(json).toEqual('foo'); + }); + + itFailsFirefox('should not work with dates', async () => { + const { page } = getTestState(); + + const dateHandle = await page.evaluateHandle( + () => new Date('2017-09-26T00:00:00.000Z') + ); + const json = await dateHandle.jsonValue(); + expect(json).toEqual({}); + }); + it('should throw for circular objects', async () => { + const { page, isChrome } = getTestState(); + + const windowHandle = await page.evaluateHandle('window'); + let error = null; + await windowHandle.jsonValue().catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Object reference chain is too long'); + else expect(error.message).toContain('Object is not serializable'); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => ({ + foo: 'bar', + })); + const properties = await aHandle.getProperties(); + const foo = properties.get('foo'); + expect(foo).toBeTruthy(); + expect(await foo.jsonValue()).toBe('bar'); + }); + it('should return even non-own properties', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + class A { + a: string; + constructor() { + this.a = '1'; + } + } + class B extends A { + b: string; + constructor() { + super(); + this.b = '2'; + } + } + return new B(); + }); + const properties = await aHandle.getProperties(); + expect(await properties.get('a').jsonValue()).toBe('1'); + expect(await properties.get('b').jsonValue()).toBe('2'); + }); + }); + + describe('JSHandle.asElement', function () { + it('should work', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => document.body); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => 2); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const { page } = getTestState(); + + await page.setContent('
ee!
'); + const aHandle = await page.evaluateHandle( + () => document.querySelector('div').firstChild + ); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect( + await page.evaluate( + (e: HTMLElement) => e.nodeType === Node.TEXT_NODE, + element + ) + ); + }); + }); + + describe('JSHandle.toString', function () { + it('should work for primitives', async () => { + const { page } = getTestState(); + + const numberHandle = await page.evaluateHandle(() => 2); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => 'a'); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const { page } = getTestState(); + + const aHandle = await page.evaluateHandle(() => window); + expect(aHandle.toString()).toBe('JSHandle@object'); + }); + it('should work with different subtypes', async () => { + const { page } = getTestState(); + + expect((await page.evaluateHandle('(function(){})')).toString()).toBe( + 'JSHandle@function' + ); + expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12'); + expect((await page.evaluateHandle('true')).toString()).toBe( + 'JSHandle:true' + ); + expect((await page.evaluateHandle('undefined')).toString()).toBe( + 'JSHandle:undefined' + ); + expect((await page.evaluateHandle('"foo"')).toString()).toBe( + 'JSHandle:foo' + ); + expect((await page.evaluateHandle('Symbol()')).toString()).toBe( + 'JSHandle@symbol' + ); + expect((await page.evaluateHandle('new Map()')).toString()).toBe( + 'JSHandle@map' + ); + expect((await page.evaluateHandle('new Set()')).toString()).toBe( + 'JSHandle@set' + ); + expect((await page.evaluateHandle('[]')).toString()).toBe( + 'JSHandle@array' + ); + expect((await page.evaluateHandle('null')).toString()).toBe( + 'JSHandle:null' + ); + expect((await page.evaluateHandle('/foo/')).toString()).toBe( + 'JSHandle@regexp' + ); + expect((await page.evaluateHandle('document.body')).toString()).toBe( + 'JSHandle@node' + ); + expect((await page.evaluateHandle('new Date()')).toString()).toBe( + 'JSHandle@date' + ); + expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe( + 'JSHandle@weakmap' + ); + expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe( + 'JSHandle@weakset' + ); + expect((await page.evaluateHandle('new Error()')).toString()).toBe( + 'JSHandle@error' + ); + expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe( + 'JSHandle@typedarray' + ); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe( + 'JSHandle@proxy' + ); + }); + }); + + describe('JSHandle.clickablePoint', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` +
+ `; + }); + await page.evaluate(async () => { + return new Promise((resolve) => window.requestAnimationFrame(resolve)); + }); + const divHandle = await page.$('div'); + expect(await divHandle.clickablePoint()).toEqual({ + x: 45 + 60, // margin + middle point offset + y: 45 + 30, // margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 30 + 10, // margin + offset + y: 30 + 15, // margin + offset + }); + }); + + it('should work for iframes', async () => { + const { page } = getTestState(); + await page.evaluate(() => { + document.body.style.padding = '10px'; + document.body.style.margin = '10px'; + document.body.innerHTML = ` + + `; + }); + await page.evaluate(async () => { + return new Promise((resolve) => window.requestAnimationFrame(resolve)); + }); + const frame = page.frames()[1]; + const divHandle = await frame.$('div'); + expect(await divHandle.clickablePoint()).toEqual({ + x: 20 + 45 + 60, // iframe pos + margin + middle point offset + y: 20 + 45 + 30, // iframe pos + margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 20 + 30 + 10, // iframe pos + margin + offset + y: 20 + 30 + 15, // iframe pos + margin + offset + }); + }); + }); + + describe('JSHandle.click', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + const clicks = []; + + await page.exposeFunction('reportClick', (x: number, y: number): void => { + clicks.push([x, y]); + }); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` +
+ `; + document.body.addEventListener('click', (e) => { + (window as any).reportClick(e.clientX, e.clientY); + }); + }); + + const divHandle = await page.$('div'); + await divHandle.click(); + await divHandle.click({ + offset: { + x: 10, + y: 15, + }, + }); + await shortWaitForArrayToHaveAtLeastNElements(clicks, 2); + expect(clicks).toEqual([ + [45 + 60, 45 + 30], // margin + middle point offset + [30 + 10, 30 + 15], // margin + offset + ]); + }); + }); +}); diff --git a/test/keyboard.spec.ts b/test/keyboard.spec.ts new file mode 100644 index 0000000000000..5aff0f95e7945 --- /dev/null +++ b/test/keyboard.spec.ts @@ -0,0 +1,408 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +describe('Keyboard', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should type into a textarea', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + const textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.focus(); + }); + const text = 'Hello world. I am the text that was typed!'; + await page.keyboard.type(text); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe(text); + }); + itFailsFirefox('should press the metaKey', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + (window as any).keyPromise = new Promise((resolve) => + document.addEventListener('keydown', (event) => resolve(event.key)) + ); + }); + await page.keyboard.press('Meta'); + expect(await page.evaluate('keyPromise')).toBe( + isFirefox && os.platform() !== 'darwin' ? 'OS' : 'Meta' + ); + }); + it('should move with the arrow keys', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'Hello World!'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello World!'); + for (let i = 0; i < 'World!'.length; i++) page.keyboard.press('ArrowLeft'); + await page.keyboard.type('inserted '); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello inserted World!'); + page.keyboard.down('Shift'); + for (let i = 0; i < 'inserted '.length; i++) + page.keyboard.press('ArrowLeft'); + page.keyboard.up('Shift'); + await page.keyboard.press('Backspace'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('Hello World!'); + }); + it('should send a character with ElementHandle.press', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('a'); + + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + + await textarea.press('b'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('a'); + }); + itFailsFirefox( + 'ElementHandle.press should support |text| option', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const textarea = await page.$('textarea'); + await textarea.press('a', { text: 'ё' }); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('ё'); + } + ); + itFailsFirefox('should send a character with sendCharacter', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.keyboard.sendCharacter('嗨'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('嗨'); + await page.evaluate(() => + window.addEventListener('keydown', (e) => e.preventDefault(), true) + ); + await page.keyboard.sendCharacter('a'); + expect( + await page.evaluate(() => document.querySelector('textarea').value) + ).toBe('嗨a'); + }); + itFailsFirefox('should report shiftKey', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = new Map([ + ['Shift', 16], + ['Alt', 18], + ['Control', 17], + ]); + for (const [modifierKey, modifierCode] of codeForKey) { + await keyboard.down(modifierKey); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' [' + + modifierKey + + ']' + ); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + + modifierKey + + ']\nKeypress: ! Digit1 33 33 [' + + modifierKey + + ']' + ); + else + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ! Digit1 49 [' + modifierKey + ']' + ); + + await keyboard.up('!'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ! Digit1 49 [' + modifierKey + ']' + ); + await keyboard.up(modifierKey); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' []' + ); + } + }); + it('should report multiple modifiers', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Control'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: Control ControlLeft 17 [Control]' + ); + await keyboard.down('Alt'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: Alt AltLeft 18 [Alt Control]' + ); + await keyboard.down(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keydown: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up(';'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: ; Semicolon 186 [Alt Control]' + ); + await keyboard.up('Control'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: Control ControlLeft 17 [Alt]' + ); + await keyboard.up('Alt'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + 'Keyup: Alt AltLeft 18 []' + ); + }); + it('should send proper codes while typing', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + await page.keyboard.type('!'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: ^ Digit6 54 []', + 'Keypress: ^ Digit6 94 94 []', + 'Keyup: ^ Digit6 54 []', + ].join('\n') + ); + }); + it('should send proper codes while typing with shift', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Shift'); + await page.keyboard.type('~'); + expect(await page.evaluate(() => globalThis.getResult())).toBe( + [ + 'Keydown: Shift ShiftLeft 16 [Shift]', + 'Keydown: ~ Backquote 192 [Shift]', // 192 is ` keyCode + 'Keypress: ~ Backquote 126 126 [Shift]', // 126 is ~ charCode + 'Keyup: ~ Backquote 192 [Shift]', + ].join('\n') + ); + await keyboard.up('Shift'); + }); + it('should not type canceled events', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + (event) => { + event.stopPropagation(); + event.stopImmediatePropagation(); + if (event.key === 'l') event.preventDefault(); + if (event.key === 'o') event.preventDefault(); + }, + false + ); + }); + await page.keyboard.type('Hello World!'); + expect(await page.evaluate(() => globalThis.textarea.value)).toBe( + 'He Wrd!' + ); + }); + itFailsFirefox('should specify repeat property', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => + document + .querySelector('textarea') + .addEventListener('keydown', (e) => (globalThis.lastEvent = e), true) + ); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.press('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + await page.keyboard.down('b'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect(await page.evaluate(() => globalThis.lastEvent.repeat)).toBe(false); + }); + itFailsFirefox('should type all kinds of characters', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This text goes onto two lines.\nThis character is 嗨.'; + await page.keyboard.type(text); + expect(await page.evaluate('result')).toBe(text); + }); + itFailsFirefox('should specify location', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + (event) => (globalThis.keyLocation = event.location), + true + ); + }); + const textarea = await page.$('textarea'); + + await textarea.press('Digit5'); + expect(await page.evaluate('keyLocation')).toBe(0); + + await textarea.press('ControlLeft'); + expect(await page.evaluate('keyLocation')).toBe(1); + + await textarea.press('ControlRight'); + expect(await page.evaluate('keyLocation')).toBe(2); + + await textarea.press('NumpadSubtract'); + expect(await page.evaluate('keyLocation')).toBe(3); + }); + it('should throw on unknown keys', async () => { + const { page } = getTestState(); + + let error = await page.keyboard + // @ts-expect-error bad input + .press('NotARealKey') + .catch((error_) => error_); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('ё').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "ё"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('😊').catch((error_) => error_); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + itFailsFirefox('should type emoji', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); + expect( + await page.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + itFailsFirefox('should type emoji into an iframe', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame( + page, + 'emoji-test', + server.PREFIX + '/input/textarea.html' + ); + const frame = page.frames()[1]; + const textarea = await frame.$('textarea'); + await textarea.type('👹 Tokyo street Japan 🇯🇵'); + expect( + await frame.$eval( + 'textarea', + (textarea: HTMLInputElement) => textarea.value + ) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + itFailsFirefox('should press the meta key', async () => { + const { page, isFirefox } = getTestState(); + + await page.evaluate(() => { + globalThis.result = null; + document.addEventListener('keydown', (event) => { + globalThis.result = [event.key, event.code, event.metaKey]; + }); + }); + await page.keyboard.press('Meta'); + // Have to do this because we lose a lot of type info when evaluating a + // string not a function. This is why functions are recommended rather than + // using strings (although we'll leave this test so we have coverage of both + // approaches.) + const [key, code, metaKey] = (await page.evaluate('result')) as [ + string, + string, + boolean + ]; + if (isFirefox && os.platform() !== 'darwin') expect(key).toBe('OS'); + else expect(key).toBe('Meta'); + + if (isFirefox) expect(code).toBe('OSLeft'); + else expect(code).toBe('MetaLeft'); + + if (isFirefox && os.platform() !== 'darwin') expect(metaKey).toBe(false); + else expect(metaKey).toBe(true); + }); +}); diff --git a/test/launcher.spec.ts b/test/launcher.spec.ts new file mode 100644 index 0000000000000..33cb1ff71fbd4 --- /dev/null +++ b/test/launcher.spec.ts @@ -0,0 +1,925 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import sinon from 'sinon'; +import { promisify } from 'util'; +import Protocol from 'devtools-protocol'; +import { + getTestState, + itChromeOnly, + itFailsFirefox, + itFirefoxOnly, + itOnlyRegularInstall, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import expect from 'expect'; +import rimraf from 'rimraf'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; + +const mkdtempAsync = promisify(fs.mkdtemp); +const readFileAsync = promisify(fs.readFile); +const rmAsync = promisify(rimraf); +const statAsync = promisify(fs.stat); +const writeFileAsync = promisify(fs.writeFile); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const FIREFOX_TIMEOUT = 30 * 1000; + +describe('Launcher specs', function () { + if (getTestState().isFirefox) this.timeout(FIREFOX_TIMEOUT); + + describe('Puppeteer', function () { + describe('BrowserFetcher', function () { + it('should download and extract chrome linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + }); + const expectedRevision = '123456'; + let revisionInfo = browserFetcher.revisionInfo(expectedRevision); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('chrome'); + expect(!!browserFetcher.host()).toBe(true); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedRevision)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedRevision); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedRevision, + ]); + await browserFetcher.remove(expectedRevision); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + it('should download and extract firefox linux binary', async () => { + const { server, puppeteer } = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + product: 'firefox', + }); + const expectedVersion = '75.0a1'; + let revisionInfo = browserFetcher.revisionInfo(expectedVersion); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile( + req, + res, + `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2` + ); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('firefox'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedVersion)).toBe(true); + + revisionInfo = await browserFetcher.download(expectedVersion); + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'FIREFOX LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedVersion, + ]); + await browserFetcher.remove(expectedVersion); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + + describe('Browser.disconnect', function () { + it('should reject navigation when browser closes', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + server.setRoute('/one-style.css', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const navigationPromise = page + .goto(server.PREFIX + '/one-style.html', { timeout: 60000 }) + .catch((error_) => error_); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect(error.message).toBe( + 'Navigation failed because browser has disconnected!' + ); + await browser.close(); + }); + it('should reject waitForSelector when browser closes', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + server.setRoute('/empty.html', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const watchdog = page + .waitForSelector('div', { timeout: 60000 }) + .catch((error_) => error_); + remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); + await browser.close(); + }); + }); + describe('Browser.close', function () { + it('should terminate network waiters', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error), + browser.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + await browser.close(); + }); + }); + describe('Puppeteer.launch', function () { + it('should reject all promises when browser is closed', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const page = await browser.newPage(); + let error = null; + const neverResolves = page + .evaluate(() => new Promise(() => {})) + .catch((error_) => (error = error_)); + await browser.close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + let waitError = null; + const options = Object.assign({}, defaultBrowserOptions, { + executablePath: 'random-invalid-path', + }); + await puppeteer.launch(options).catch((error) => (waitError = error)); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + itFirefoxOnly('userDataDir option restores preferences', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + + const prefsJSPath = path.join(userDataDir, 'prefs.js'); + const prefsJSContent = 'user_pref("browser.warnOnQuit", true)'; + await writeFileAsync(prefsJSPath, prefsJSContent); + + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + + expect(await readFileAsync(prefsJSPath, 'utf8')).toBe(prefsJSContent); + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir argument', async () => { + const { isChrome, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + itChromeOnly('userDataDir argument with non-existent dir', async () => { + const { isChrome, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + await rmAsync(userDataDir); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir option should restore state', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (localStorage.hey = 'hello')); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => localStorage.hey)).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + // This mysteriously fails on Windows on AppVeyor. See + // https://github.com/puppeteer/puppeteer/issues/4111 + xit('userDataDir option should restore cookies', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({ userDataDir }, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate( + () => + (document.cookie = + 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT') + ); + await browser.close(); + + const browser2 = await puppeteer.launch(options); + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect(await page2.evaluate(() => document.cookie)).toBe( + 'doSomethingOnlyOnce=true' + ); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('should return the default arguments', async () => { + const { isChrome, isFirefox, puppeteer } = getTestState(); + + if (isChrome) { + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + `--user-data-dir=${path.resolve('foo')}` + ); + } else if (isFirefox) { + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs()).toContain('--no-remote'); + if (os.platform() === 'darwin') { + expect(puppeteer.defaultArgs()).toContain('--foreground'); + } else { + expect(puppeteer.defaultArgs()).not.toContain('--foreground'); + } + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + '--profile' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + 'foo' + ); + } else { + expect(puppeteer.defaultArgs()).toContain('-headless'); + expect(puppeteer.defaultArgs({ headless: false })).not.toContain( + '-headless' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + '-profile' + ); + expect(puppeteer.defaultArgs({ userDataDir: 'foo' })).toContain( + path.resolve('foo') + ); + } + }); + it('should report the correct product', async () => { + const { isChrome, isFirefox, puppeteer } = getTestState(); + if (isChrome) expect(puppeteer.product).toBe('chrome'); + else if (isFirefox) expect(puppeteer.product).toBe('firefox'); + }); + it('should work with no default arguments', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + itChromeOnly( + 'should filter out ignored default arguments in Chrome', + async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [defaultArgs[0], defaultArgs[2]], + }) + ); + const spawnargs = browser.process().spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2])).toBe(-1); + await browser.close(); + } + ); + itFirefoxOnly( + 'should filter out ignored default argument in Firefox', + async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Only the first argument is fixed, others are optional. + ignoreDefaultArgs: [defaultArgs[0]], + }) + ); + const spawnargs = browser.process().spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0])).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1])).not.toBe(-1); + await browser.close(); + } + ); + it('should have default URL when launching browser', async function () { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map((page) => page.url()); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + itFailsFirefox( + 'should have custom URL when launching browser', + async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(1); + const page = pages[0]; + if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await browser.close(); + } + ); + it('should pass the timeout parameter to browser.waitForTarget', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + timeout: 1, + }); + let error = null; + await puppeteer.launch(options).catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should set the default viewport', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: { + width: 456, + height: 789, + }, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + await browser.close(); + }); + it('should disable the default viewport', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(page.viewport()).toBe(null); + await browser.close(); + }); + it('should take fullPage screenshots when defaultViewport is null', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + }); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeInstanceOf(Buffer); + await browser.close(); + }); + it('should set the debugging port', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + debuggingPort: 9999, + }); + const browser = await puppeteer.launch(options); + const url = new URL(browser.wsEndpoint()); + await browser.close(); + expect(url.port).toBe('9999'); + }); + it('should not allow setting debuggingPort and pipe', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + defaultViewport: null, + debuggingPort: 9999, + pipe: true, + }); + + let error = null; + await puppeteer.launch(options).catch((error_) => (error = error_)); + expect(error.message).toContain('either pipe or debugging port'); + }); + itChromeOnly( + 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', + async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + const options = { + waitForInitialPage: false, + // This is needed to prevent Puppeteer from adding an initial blank page. + // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200 + ignoreDefaultArgs: true, + ...defaultBrowserOptions, + args: ['--no-startup-window'], + }; + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(0); + await browser.close(); + } + ); + }); + + describe('Puppeteer.launch', function () { + let productName; + + before(async () => { + const { puppeteer } = getTestState(); + productName = puppeteer._productName; + }); + + after(async () => { + const { puppeteer } = getTestState(); + // @ts-expect-error launcher is a private property that users can't + // touch, but for testing purposes we need to reset it. + puppeteer._lazyLauncher = undefined; + puppeteer._productName = productName; + }); + + itOnlyRegularInstall('should be able to launch Chrome', async () => { + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch({ product: 'chrome' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + }); + + itOnlyRegularInstall( + 'falls back to launching chrome if there is an unknown product but logs a warning', + async () => { + const { puppeteer } = getTestState(); + const consoleStub = sinon.stub(console, 'warn'); + const browser = await puppeteer.launch({ + // @ts-expect-error purposeful bad input + product: 'SO_NOT_A_PRODUCT', + }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + expect(consoleStub.callCount).toEqual(1); + expect(consoleStub.firstCall.args).toEqual([ + 'Warning: unknown product name SO_NOT_A_PRODUCT. Falling back to chrome.', + ]); + } + ); + + itOnlyRegularInstall( + 'should be able to launch Firefox', + async function () { + this.timeout(FIREFOX_TIMEOUT); + const { puppeteer } = getTestState(); + const browser = await puppeteer.launch({ product: 'firefox' }); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Firefox'); + } + ); + }); + + describe('Puppeteer.connect', function () { + it('should be able to connect multiple times to the same browser', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const otherBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + const page = await otherBrowser.newPage(); + expect(await page.evaluate(() => 7 * 8)).toBe(56); + otherBrowser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect(await secondPage.evaluate(() => 7 * 6)).toBe(42); + await originalBrowser.close(); + }); + it('should be able to close remote browser', async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + await Promise.all([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async () => { + const { httpsServer, puppeteer, defaultBrowserOptions } = + getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + ignoreHTTPSErrors: true, + }); + const page = await browser.newPage(); + let error = null; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch((error_) => (error = error_)), + ]); + expect(error).toBe(null); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = serverRequest.socket.getProtocol().replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4197 + itFailsFirefox('should support targetFilter option', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const page1 = await originalBrowser.newPage(); + await page1.goto(server.EMPTY_PAGE); + + const page2 = await originalBrowser.newPage(); + await page2.goto(server.EMPTY_PAGE + '?should-be-ignored'); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + targetFilter: (targetInfo: Protocol.Target.TargetInfo) => + !targetInfo.url?.includes('should-be-ignored'), + }); + + const pages = await browser.pages(); + + await page2.close(); + await page1.close(); + await browser.disconnect(); + await originalBrowser.close(); + + expect(pages.map((p: Page) => p.url()).sort()).toEqual([ + 'about:blank', + server.EMPTY_PAGE, + ]); + }); + itFailsFirefox( + 'should be able to reconnect to a disconnected browser', + async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const page = await originalBrowser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + originalBrowser.disconnect(); + + const browser = await puppeteer.connect({ browserWSEndpoint }); + const pages = await browser.pages(); + const restoredPage = pages.find( + (page) => + page.url() === server.PREFIX + '/frames/nested-frames.html' + ); + expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:/frames/nested-frames.html', + ' http://localhost:/frames/two-frames.html (2frames)', + ' http://localhost:/frames/frame.html (uno)', + ' http://localhost:/frames/frame.html (dos)', + ' http://localhost:/frames/frame.html (aframe)', + ]); + expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); + await browser.close(); + } + ); + // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 + itFailsFirefox( + 'should be able to connect to the same page simultaneously', + async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + + const browserOne = await puppeteer.launch(defaultBrowserOptions); + const browserTwo = await puppeteer.connect({ + browserWSEndpoint: browserOne.wsEndpoint(), + }); + const [page1, page2] = await Promise.all([ + new Promise((x) => + browserOne.once('targetcreated', (target) => x(target.page())) + ), + browserTwo.newPage(), + ]); + expect(await page1.evaluate(() => 7 * 8)).toBe(56); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); + await browserOne.close(); + } + ); + it('should be able to reconnect', async () => { + const { puppeteer, server, defaultBrowserOptions } = getTestState(); + const browserOne = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = browserOne.wsEndpoint(); + const pageOne = await browserOne.newPage(); + await pageOne.goto(server.EMPTY_PAGE); + browserOne.disconnect(); + + const browserTwo = await puppeteer.connect({ browserWSEndpoint }); + const pages = await browserTwo.pages(); + const pageTwo = pages.find((page) => page.url() === server.EMPTY_PAGE); + await pageTwo.reload(); + const bodyHandle = await pageTwo.waitForSelector('body', { + timeout: 10000, + }); + await bodyHandle.dispose(); + await browserTwo.close(); + }); + }); + describe('Puppeteer.executablePath', function () { + itOnlyRegularInstall('should work', async () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + it('returns executablePath for channel', () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath('chrome'); + expect(executablePath).toBeTruthy(); + }); + describe('when PUPPETEER_EXECUTABLE_PATH is set', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + process.env.PUPPETEER_EXECUTABLE_PATH = ''; + sandbox + .stub(process.env, 'PUPPETEER_EXECUTABLE_PATH') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => sandbox.restore()); + + it('its value is returned', async () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('SOME_CUSTOM_EXECUTABLE'); + }); + }); + + describe('when the product is chrome, platform is not darwin, and arch is arm64', () => { + describe('and the executable exists', () => { + itChromeOnly('returns /usr/bin/chromium-browser', async () => { + const { puppeteer } = getTestState(); + const osPlatformStub = sinon.stub(os, 'platform').returns('linux'); + const osArchStub = sinon.stub(os, 'arch').returns('arm64'); + const fsExistsStub = sinon.stub(fs, 'existsSync'); + fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(true); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('/usr/bin/chromium-browser'); + + osPlatformStub.restore(); + osArchStub.restore(); + fsExistsStub.restore(); + }); + describe('and PUPPETEER_EXECUTABLE_PATH is set', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + process.env.PUPPETEER_EXECUTABLE_PATH = ''; + sandbox + .stub(process.env, 'PUPPETEER_EXECUTABLE_PATH') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => sandbox.restore()); + + it('its value is returned', async () => { + const { puppeteer } = getTestState(); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('SOME_CUSTOM_EXECUTABLE'); + }); + }); + }); + describe('and the executable does not exist', () => { + itChromeOnly( + 'does not return /usr/bin/chromium-browser', + async () => { + const { puppeteer } = getTestState(); + const osPlatformStub = sinon + .stub(os, 'platform') + .returns('linux'); + const osArchStub = sinon.stub(os, 'arch').returns('arm64'); + const fsExistsStub = sinon.stub(fs, 'existsSync'); + fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).not.toEqual('/usr/bin/chromium-browser'); + + osPlatformStub.restore(); + osArchStub.restore(); + fsExistsStub.restore(); + } + ); + }); + }); + }); + }); + + describe('Browser target events', function () { + itFailsFirefox('should work', async () => { + const { server, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const events = []; + browser.on('targetcreated', () => events.push('CREATED')); + browser.on('targetchanged', () => events.push('CHANGED')); + browser.on('targetdestroyed', () => events.push('DESTROYED')); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + await browser.close(); + }); + }); + + describe('Browser.Events.disconnected', function () { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + const remoteBrowser1 = await puppeteer.connect({ browserWSEndpoint }); + const remoteBrowser2 = await puppeteer.connect({ browserWSEndpoint }); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + originalBrowser.on('disconnected', () => ++disconnectedOriginal); + remoteBrowser1.on('disconnected', () => ++disconnectedRemote1); + remoteBrowser2.on('disconnected', () => ++disconnectedRemote2); + + await Promise.all([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); +}); diff --git a/test/mocha-ts-require.js b/test/mocha-ts-require.js new file mode 100644 index 0000000000000..a0ac64fa62b7a --- /dev/null +++ b/test/mocha-ts-require.js @@ -0,0 +1,11 @@ +const path = require('path'); + +require('ts-node').register({ + /** + * We ignore the lib/ directory because that's already been TypeScript + * compiled and checked. So we don't want to check it again as part of running + * the unit tests. + */ + ignore: ['lib/*', 'node_modules'], + project: path.join(__dirname, 'tsconfig.test.json'), +}); diff --git a/test/mocha-utils.ts b/test/mocha-utils.ts new file mode 100644 index 0000000000000..9593c04f9be0d --- /dev/null +++ b/test/mocha-utils.ts @@ -0,0 +1,345 @@ +/** + * Copyright 2020 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 { TestServer } from '../utils/testserver/index.js'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import sinon from 'sinon'; +import puppeteer from '../lib/cjs/puppeteer/node.js'; +import { + Browser, + BrowserContext, +} from '../lib/cjs/puppeteer/common/Browser.js'; +import { Page } from '../lib/cjs/puppeteer/common/Page.js'; +import { PuppeteerNode } from '../lib/cjs/puppeteer/node/Puppeteer.js'; +import utils from './utils.js'; +import rimraf from 'rimraf'; +import expect from 'expect'; + +import { trackCoverage } from './coverage-utils.js'; +import Protocol from 'devtools-protocol'; + +const setupServer = async () => { + const assetsPath = path.join(__dirname, 'assets'); + const cachedPath = path.join(__dirname, 'assets', 'cached'); + + const port = 8907; + const server = await TestServer.create(assetsPath, port); + server.enableHTTPCache(cachedPath); + server.PORT = port; + server.PREFIX = `http://localhost:${port}`; + server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; + server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; + + const httpsPort = port + 1; + const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); + httpsServer.enableHTTPCache(cachedPath); + httpsServer.PORT = httpsPort; + httpsServer.PREFIX = `https://localhost:${httpsPort}`; + httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; + httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; + + return { server, httpsServer }; +}; + +export const getTestState = (): PuppeteerTestState => + state as PuppeteerTestState; + +const product = + process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium'; + +const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false; + +const headless = (process.env.HEADLESS || 'true').trim().toLowerCase(); +const isHeadless = headless === 'true' || headless === 'chrome'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'Chromium'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}'); +} catch (error) { + console.warn( + `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` + ); +} + +const defaultBrowserOptions = Object.assign( + { + handleSIGINT: true, + executablePath: process.env.BINARY, + headless: headless === 'chrome' ? ('chrome' as const) : isHeadless, + dumpio: !!process.env.DUMPIO, + }, + extraLaunchOptions +); + +(async (): Promise => { + if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); + } else { + // TODO(jackfranklin): declare updateRevision in some form for the Firefox + // launcher. + // @ts-expect-error _updateRevision is defined on the FF launcher + // but not the Chrome one. The types need tidying so that TS can infer that + // properly and not error here. + if (product === 'firefox') await puppeteer._launcher._updateRevision(); + const executablePath = puppeteer.executablePath(); + if (!fs.existsSync(executablePath)) + throw new Error( + `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` + ); + } +})(); + +declare module 'expect/build/types' { + interface Matchers { + toBeGolden(x: string): R; + } +} + +const setupGoldenAssertions = (): void => { + const suffix = product.toLowerCase(); + const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix); + const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix); + if (fs.existsSync(OUTPUT_DIR)) rimraf.sync(OUTPUT_DIR); + utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); +}; + +setupGoldenAssertions(); + +interface PuppeteerTestState { + browser: Browser; + context: BrowserContext; + page: Page; + puppeteer: PuppeteerNode; + defaultBrowserOptions: { + [x: string]: any; + }; + server: any; + httpsServer: any; + isFirefox: boolean; + isChrome: boolean; + isHeadless: boolean; + headless: string; + puppeteerPath: string; +} +const state: Partial = {}; + +export const itFailsFirefox = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) return xit(description, body); + else return it(description, body); +}; + +export const itChromeOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome) return it(description, body); + else return xit(description, body); +}; + +export const itHeadlessOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome && isHeadless === true) return it(description, body); + else return xit(description, body); +}; + +export const itFirefoxOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) return it(description, body); + else return xit(description, body); +}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (alternativeInstall || process.env.BINARY) return xit(description, body); + else return it(description, body); +}; + +export const itFailsWindowsUntilDate = ( + date: Date, + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32' && Date.now() < date.getTime()) { + // we are within the deferred time so skip the test + return xit(description, body); + } + + return it(description, body); +}; + +export const itFailsWindows = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32') { + return xit(description, body); + } + return it(description, body); +}; + +export const describeFailsFirefox = ( + description: string, + body: (this: Mocha.Suite) => void +): void | Mocha.Suite => { + if (isFirefox) return xdescribe(description, body); + else return describe(description, body); +}; + +export const describeChromeOnly = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite => { + if (isChrome) return describe(description, body); +}; + +let coverageHooks = { + beforeAll: (): void => {}, + afterAll: (): void => {}, +}; + +if (process.env.COVERAGE) { + coverageHooks = trackCoverage(); +} + +console.log( + `Running unit tests with: + -> product: ${product} + -> binary: ${ + defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + }` +); + +process.on('unhandledRejection', (reason) => { + throw reason; +}); + +export const setupTestBrowserHooks = (): void => { + before(async () => { + const browser = await puppeteer.launch(defaultBrowserOptions); + state.browser = browser; + }); + + after(async () => { + await state.browser.close(); + state.browser = null; + }); +}; + +export const setupTestPageAndContextHooks = (): void => { + beforeEach(async () => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context.close(); + state.context = null; + state.page = null; + }); +}; + +export const mochaHooks = { + beforeAll: [ + async (): Promise => { + const { server, httpsServer } = await setupServer(); + + state.puppeteer = puppeteer; + state.defaultBrowserOptions = defaultBrowserOptions; + state.server = server; + state.httpsServer = httpsServer; + state.isFirefox = isFirefox; + state.isChrome = isChrome; + state.isHeadless = isHeadless; + state.headless = headless; + state.puppeteerPath = path.resolve(path.join(__dirname, '..')); + }, + coverageHooks.beforeAll, + ], + + beforeEach: async (): Promise => { + state.server.reset(); + state.httpsServer.reset(); + }, + + afterAll: [ + async (): Promise => { + await state.server.stop(); + state.server = null; + await state.httpsServer.stop(); + state.httpsServer = null; + }, + coverageHooks.afterAll, + ], + + afterEach: (): void => { + sinon.restore(); + }, +}; + +export const expectCookieEquals = ( + cookies: Protocol.Network.Cookie[], + expectedCookies: Array> +): void => { + const { isChrome } = getTestState(); + if (!isChrome) { + // Only keep standard properties when testing on a browser other than Chrome. + expectedCookies = expectedCookies.map((cookie) => { + return { + domain: cookie.domain, + expires: cookie.expires, + httpOnly: cookie.httpOnly, + name: cookie.name, + path: cookie.path, + secure: cookie.secure, + session: cookie.session, + size: cookie.size, + value: cookie.value, + }; + }); + } + + expect(cookies).toEqual(expectedCookies); +}; + +export const shortWaitForArrayToHaveAtLeastNElements = async ( + data: unknown[], + minLength: number, + attempts = 3, + timeout = 50 +): Promise => { + for (let i = 0; i < attempts; i++) { + if (data.length >= minLength) { + break; + } + await new Promise((resolve) => setTimeout(resolve, timeout)); + } +}; diff --git a/test/mouse.spec.ts b/test/mouse.spec.ts new file mode 100644 index 0000000000000..33d0053386607 --- /dev/null +++ b/test/mouse.spec.ts @@ -0,0 +1,239 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +interface Dimensions { + x: number; + y: number; + width: number; + height: number; +} + +function dimensions(): Dimensions { + const rect = document.querySelector('textarea').getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; +} + +describe('Mouse', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the document', async () => { + const { page } = getTestState(); + + await page.evaluate(() => { + globalThis.clickPromise = new Promise((resolve) => { + document.addEventListener('click', (event) => { + resolve({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button, + }); + }); + }); + }); + await page.mouse.click(50, 60); + const event = await page.evaluate<() => MouseEvent>( + () => globalThis.clickPromise + ); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); + }); + it('should resize the textarea', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const { x, y, width, height } = await page.evaluate<() => Dimensions>( + dimensions + ); + const mouse = page.mouse; + await mouse.move(x + width - 4, y + height - 4); + await mouse.down(); + await mouse.move(x + width + 100, y + height + 100); + await mouse.up(); + const newDimensions = await page.evaluate<() => Dimensions>(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + it('should select the text with mouse', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + await page.keyboard.type(text); + // Firefox needs an extra frame here after typing or it will fail to set the scrollTop + await page.evaluate(() => new Promise(requestAnimationFrame)); + await page.evaluate( + () => (document.querySelector('textarea').scrollTop = 0) + ); + const { x, y } = await page.evaluate(dimensions); + await page.mouse.move(x + 2, y + 2); + await page.mouse.down(); + await page.mouse.move(100, 100); + await page.mouse.up(); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd + ); + }) + ).toBe(text); + }); + itFailsFirefox('should trigger hover state', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.hover('#button-6'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + await page.hover('#button-2'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-2'); + await page.hover('#button-91'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-91'); + }); + itFailsFirefox( + 'should trigger hover state with removed window.Node', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => delete window.Node); + await page.hover('#button-6'); + expect( + await page.evaluate(() => document.querySelector('button:hover').id) + ).toBe('button-6'); + } + ); + it('should set modifier keys on click', async () => { + const { page, server, isFirefox } = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => + document + .querySelector('#button-3') + .addEventListener('mousedown', (e) => (globalThis.lastEvent = e), true) + ); + const modifiers = new Map([ + ['Shift', 'shiftKey'], + ['Control', 'ctrlKey'], + ['Alt', 'altKey'], + ['Meta', 'metaKey'], + ]); + // In Firefox, the Meta modifier only exists on Mac + if (isFirefox && os.platform() !== 'darwin') delete modifiers['Meta']; + for (const [modifier, key] of modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if ( + !(await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) + ) + throw new Error(key + ' should be true'); + await page.keyboard.up(modifier); + } + await page.click('#button-3'); + for (const [modifier, key] of modifiers) { + if (await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) + throw new Error(modifiers[modifier] + ' should be false'); + } + }); + itFailsFirefox('should send mouse wheel events', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/wheel.html'); + const elem = await page.$('div'); + const boundingBoxBefore = await elem.boundingBox(); + expect(boundingBoxBefore).toMatchObject({ + width: 115, + height: 115, + }); + + await page.mouse.move( + boundingBoxBefore.x + boundingBoxBefore.width / 2, + boundingBoxBefore.y + boundingBoxBefore.height / 2 + ); + + await page.mouse.wheel({ deltaY: -100 }); + const boundingBoxAfter = await elem.boundingBox(); + expect(boundingBoxAfter).toMatchObject({ + width: 230, + height: 230, + }); + }); + itFailsFirefox('should tween mouse movement', async () => { + const { page } = getTestState(); + + await page.mouse.move(100, 100); + await page.evaluate(() => { + globalThis.result = []; + document.addEventListener('mousemove', (event) => { + globalThis.result.push([event.clientX, event.clientY]); + }); + }); + await page.mouse.move(200, 300, { steps: 5 }); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300], + ]); + }); + // @see https://crbug.com/929806 + it('should work with mobile viewports and cross process navigations', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({ width: 360, height: 640, isMobile: true }); + await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); + await page.evaluate(() => { + document.addEventListener('click', (event) => { + globalThis.result = { x: event.clientX, y: event.clientY }; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 }); + }); +}); diff --git a/test/navigation.spec.ts b/test/navigation.spec.ts new file mode 100644 index 0000000000000..a7d0e36b8988c --- /dev/null +++ b/test/navigation.spec.ts @@ -0,0 +1,776 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import os from 'os'; + +describe('navigation', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.goto', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with anchor navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE + '#foo'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + await page.goto(server.EMPTY_PAGE + '#bar'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#bar'); + }); + it('should work with redirects', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + await page.goto(server.PREFIX + '/redirect/1.html'); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should navigate to about:blank', async () => { + const { page } = getTestState(); + + const response = await page.goto('about:blank'); + expect(response).toBe(null); + }); + it('should return response when page changes its URL after load', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/historyapi.html'); + expect(response.status()).toBe(200); + }); + itFailsFirefox('should work with subframes return 204', async () => { + const { page, server } = getTestState(); + + server.setRoute('/frames/frame.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page + .goto(server.PREFIX + '/frames/one-frame.html') + .catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + itFailsFirefox('should fail when server returns 204', async () => { + const { page, server, isChrome } = getTestState(); + + server.setRoute('/empty.html', (req, res) => { + res.statusCode = 204; + res.end(); + }); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).not.toBe(null); + if (isChrome) expect(error.message).toContain('net::ERR_ABORTED'); + else expect(error.message).toContain('NS_BINDING_ABORTED'); + }); + it('should navigate to empty page with domcontentloaded', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'domcontentloaded', + }); + expect(response.status()).toBe(200); + }); + it('should work when page calls history API in beforeunload', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.addEventListener( + 'beforeunload', + () => history.replaceState(null, 'initial', window.location.href), + false + ); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response.status()).toBe(200); + }); + itFailsFirefox( + 'should navigate to empty page with networkidle0', + async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle0', + }); + expect(response.status()).toBe(200); + } + ); + itFailsFirefox( + 'should navigate to empty page with networkidle2', + async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle2', + }); + expect(response.status()).toBe(200); + } + ); + itFailsFirefox('should fail when navigating to bad url', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page.goto('asdfasdf').catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('Cannot navigate to invalid URL'); + else expect(error.message).toContain('Invalid url'); + }); + + /* If you are running this on pre-Catalina versions of macOS this will fail locally. + /* Mac OSX Catalina outputs a different message than other platforms. + * See https://support.google.com/chrome/thread/18125056?hl=en for details. + * If you're running pre-Catalina Mac OSX this test will fail locally. + */ + const EXPECTED_SSL_CERT_MESSAGE = + os.platform() === 'darwin' + ? 'net::ERR_CERT_INVALID' + : 'net::ERR_CERT_AUTHORITY_INVALID'; + + itFailsFirefox('should fail when navigating to bad SSL', async () => { + const { page, httpsServer, isChrome } = getTestState(); + + // Make sure that network events do not emit 'undefined'. + // @see https://crbug.com/750469 + const requests = []; + page.on('request', () => requests.push('request')); + page.on('requestfinished', () => requests.push('requestfinished')); + page.on('requestfailed', () => requests.push('requestfailed')); + + let error = null; + await page + .goto(httpsServer.EMPTY_PAGE) + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + + expect(requests.length).toBe(2); + expect(requests[0]).toBe('request'); + expect(requests[1]).toBe('requestfailed'); + }); + it('should fail when navigating to bad SSL after redirects', async () => { + const { page, server, httpsServer, isChrome } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + let error = null; + await page + .goto(httpsServer.PREFIX + '/redirect/1.html') + .catch((error_) => (error = error_)); + if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); + else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + }); + it('should throw if networkidle is passed as an option', async () => { + const { page, server } = getTestState(); + + let error = null; + await page + // @ts-expect-error purposefully passing an old option + .goto(server.EMPTY_PAGE, { waitUntil: 'networkidle' }) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + '"networkidle" option is no longer supported' + ); + }); + it('should fail when main resources failed to load', async () => { + const { page, isChrome } = getTestState(); + + let error = null; + await page + .goto('http://localhost:44123/non-existing-url') + .catch((error_) => (error = error_)); + if (isChrome) + expect(error.message).toContain('net::ERR_CONNECTION_REFUSED'); + else expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED'); + }); + it('should fail when exceeding maximum navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + await page + .goto(server.PREFIX + '/empty.html', { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error = null; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page + .goto(server.PREFIX + '/empty.html') + .catch((error_) => (error = error_)); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should disable timeout when its set to 0', async () => { + const { page, server } = getTestState(); + + let error = null; + let loaded = false; + page.once('load', () => (loaded = true)); + await page + .goto(server.PREFIX + '/grid.html', { timeout: 0, waitUntil: ['load'] }) + .catch((error_) => (error = error_)); + expect(error).toBe(null); + expect(loaded).toBe(true); + }); + it('should work when navigating to valid url', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + itFailsFirefox('should work when navigating to data url', async () => { + const { page } = getTestState(); + + const response = await page.goto('data:text/html,hello'); + expect(response.ok()).toBe(true); + }); + it('should work when navigating to 404', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/not-found'); + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should return last response in redirect chain', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/redirect/3.html'); + server.setRedirect('/redirect/3.html', server.EMPTY_PAGE); + const response = await page.goto(server.PREFIX + '/redirect/1.html'); + expect(response.ok()).toBe(true); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + itFailsFirefox( + 'should wait for network idle to succeed navigation', + async () => { + const { page, server } = getTestState(); + + let responses = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-b.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-c.js', (req, res) => + responses.push(res) + ); + server.setRoute('/fetch-request-d.js', (req, res) => + responses.push(res) + ); + const initialFetchResourcesRequested = Promise.all([ + server.waitForRequest('/fetch-request-a.js'), + server.waitForRequest('/fetch-request-b.js'), + server.waitForRequest('/fetch-request-c.js'), + ]); + const secondFetchResourceRequested = server.waitForRequest( + '/fetch-request-d.js' + ); + + // Navigate to a page which loads immediately and then does a bunch of + // requests via javascript's fetch method. + const navigationPromise = page.goto( + server.PREFIX + '/networkidle.html', + { + waitUntil: 'networkidle0', + } + ); + // Track when the navigation gets completed. + let navigationFinished = false; + navigationPromise.then(() => (navigationFinished = true)); + + // Wait for the page's 'load' event. + await new Promise((fulfill) => page.once('load', fulfill)); + expect(navigationFinished).toBe(false); + + // Wait for the initial three resources to be requested. + await initialFetchResourcesRequested; + + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to initial requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + // Reset responses array + responses = []; + + // Wait for the second round to be requested. + await secondFetchResourceRequested; + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + const response = await navigationPromise; + // Expect navigation to succeed. + expect(response.ok()).toBe(true); + } + ); + it('should not leak listeners during navigation', async () => { + const { page, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) await page.goto(server.EMPTY_PAGE); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during bad navigation', async () => { + const { page } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) + await page.goto('asdf').catch(() => { + /* swallow navigation error */ + }); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during navigation of 11 pages', async () => { + const { context, server } = getTestState(); + + let warning = null; + const warningHandler = (w) => (warning = w); + process.on('warning', warningHandler); + await Promise.all( + [...Array(20)].map(async () => { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + }) + ); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + itFailsFirefox( + 'should navigate to dataURL and fire dataURL requests', + async () => { + const { page } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + const dataURL = 'data:text/html,
yo
'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + } + ); + itFailsFirefox( + 'should navigate to URL with hash and fire requests without hash', + async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + } + ); + it('should work with self requesting page', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/self-request.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it('should fail when navigating and show the url at the error message', async () => { + const { page, httpsServer } = getTestState(); + + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error = null; + try { + await page.goto(url); + } catch (error_) { + error = error_; + } + expect(error.message).toContain(url); + }); + itFailsFirefox('should send referer', async () => { + const { page, server } = getTestState(); + + const [request1, request2] = await Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]); + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate( + (url: string) => (window.location.href = url), + server.PREFIX + '/grid.html' + ), + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + }); + it('should work with both domcontentloaded and load', async () => { + const { page, server } = getTestState(); + + let response = null; + server.setRoute('/one-style.css', (req, res) => (response = res)); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + const domContentLoadedPromise = page.waitForNavigation({ + waitUntil: 'domcontentloaded', + }); + + let bothFired = false; + const bothFiredPromise = page + .waitForNavigation({ + waitUntil: ['load', 'domcontentloaded'], + }) + .then(() => (bothFired = true)); + + await server.waitForRequest('/one-style.css'); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await bothFiredPromise; + await navigationPromise; + }); + it('should work with clicking on anchor links', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(`foobar`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + itFailsFirefox('should work with history.pushState()', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + itFailsFirefox('should work with history.replaceState()', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + SPA + + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + itFailsFirefox( + 'should work with DOM history.back()/history.forward()', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + back + forward + + `); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + const [backResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#back'), + ]); + expect(backResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + const [forwardResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#forward'), + ]); + expect(forwardResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + } + ); + itFailsFirefox( + 'should work when subframe issues window.stop()', + async () => { + const { page, server } = getTestState(); + + server.setRoute('/frames/style.css', () => {}); + const navigationPromise = page.goto( + server.PREFIX + '/frames/one-frame.html' + ); + const frame = await utils.waitEvent(page, 'frameattached'); + await new Promise((fulfill) => { + page.on('framenavigated', (f) => { + if (f === frame) fulfill(); + }); + }); + await Promise.all([ + frame.evaluate(() => window.stop()), + navigationPromise, + ]); + } + ); + }); + + describe('Page.goBack', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/grid.html'); + + let response = await page.goBack(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain(server.EMPTY_PAGE); + + response = await page.goForward(); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('/grid.html'); + + response = await page.goForward(); + expect(response).toBe(null); + }); + itFailsFirefox('should work with HistoryAPI', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + }); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + + await page.goBack(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + await page.goBack(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goForward(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + }); + }); + + describeFailsFirefox('Frame.goto', function () { + it('should navigate subframes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames()[0].url()).toContain('/frames/one-frame.html'); + expect(page.frames()[1].url()).toContain('/frames/frame.html'); + + const response = await page.frames()[1].goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.frame()).toBe(page.frames()[1]); + }); + it('should reject when frame detaches', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + + server.setRoute('/empty.html', () => {}); + const navigationPromise = page + .frames()[1] + .goto(server.EMPTY_PAGE) + .catch((error_) => error_); + await server.waitForRequest('/empty.html'); + + await page.$eval('iframe', (frame) => frame.remove()); + const error = await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + it('should return matching responses', async () => { + const { page, server } = getTestState(); + + // Disable cache: otherwise, chromium will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame2', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses = []; + server.setRoute('/one-style.html', (req, res) => + serverResponses.push(res) + ); + const navigations = []; + for (let i = 0; i < 3; ++i) { + navigations.push(frames[i].goto(server.PREFIX + '/one-style.html')); + await server.waitForRequest('/one-style.html'); + } + // Respond from server out-of-order. + const serverResponseTexts = ['AAA', 'BBB', 'CCC']; + for (const i of [1, 2, 0]) { + serverResponses[i].end(serverResponseTexts[i]); + const response = await navigations[i]; + expect(response.frame()).toBe(frames[i]); + expect(await response.text()).toBe(serverResponseTexts[i]); + } + }); + }); + + describeFailsFirefox('Frame.waitForNavigation', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + const [response] = await Promise.all([ + frame.waitForNavigation(), + frame.evaluate( + (url: string) => (window.location.href = url), + server.PREFIX + '/grid.html' + ), + ]); + expect(response.ok()).toBe(true); + expect(response.url()).toContain('grid.html'); + expect(response.frame()).toBe(frame); + expect(page.url()).toContain('/frames/one-frame.html'); + }); + it('should fail when frame detaches', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]; + + server.setRoute('/empty.html', () => {}); + let error = null; + const navigationPromise = frame + .waitForNavigation() + .catch((error_) => (error = error_)); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => ((window as any).location = '/empty.html')), + ]); + await page.$eval('iframe', (frame) => frame.remove()); + await navigationPromise; + expect(error.message).toBe('Navigating frame was detached'); + }); + }); + + describe('Page.reload', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (globalThis._foo = 10)); + await page.reload(); + expect(await page.evaluate(() => globalThis._foo)).toBe(undefined); + }); + }); +}); diff --git a/test/network.spec.ts b/test/network.spec.ts new file mode 100644 index 0000000000000..f4d4b5069cfc5 --- /dev/null +++ b/test/network.spec.ts @@ -0,0 +1,809 @@ +/** + * Copyright 2018 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 fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, + itChromeOnly, + itFirefoxOnly, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { HTTPResponse } from '../lib/cjs/puppeteer/api-docs-entry.js'; + +describe('network', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + }); + it('should fire for iframes', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(2); + }); + it('should fire for fetches', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => fetch('/empty.html')); + expect(requests.length).toBe(2); + }); + }); + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + itFailsFirefox('should work for subframe navigation request', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.frames()[1]); + }); + it('should work for fetch requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let requests = []; + page.on( + 'request', + (request) => !utils.isFavicon(request) && requests.push(request) + ); + await page.evaluate(() => fetch('/digits/1.png')); + requests = requests.filter( + (request) => !request.url().includes('favicon') + ); + expect(requests.length).toBe(1); + expect(requests[0].frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + itChromeOnly('should define Chrome as user agent header', async () => { + const { page, server } = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().headers()['user-agent']).toContain('Chrome'); + }); + + itFirefoxOnly('should define Firefox as user agent header', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().headers()['user-agent']).toContain('Firefox'); + }); + }); + + describe('Response.headers', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setRoute('/empty.html', (req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + describeFailsFirefox('Request.initiator', () => { + it('should return the initiator', async () => { + const { page, server } = getTestState(); + + const initiators = new Map(); + page.on('request', (request) => + initiators.set(request.url().split('/').pop(), request.initiator()) + ); + await page.goto(server.PREFIX + '/initiator.html'); + + expect(initiators.get('initiator.html').type).toBe('other'); + expect(initiators.get('initiator.js').type).toBe('parser'); + expect(initiators.get('initiator.js').url).toBe( + server.PREFIX + '/initiator.html' + ); + expect(initiators.get('frame.html').type).toBe('parser'); + expect(initiators.get('frame.html').url).toBe( + server.PREFIX + '/initiator.html' + ); + expect(initiators.get('script.js').type).toBe('parser'); + expect(initiators.get('script.js').url).toBe( + server.PREFIX + '/frames/frame.html' + ); + expect(initiators.get('style.css').type).toBe('parser'); + expect(initiators.get('style.css').url).toBe( + server.PREFIX + '/frames/frame.html' + ); + expect(initiators.get('initiator.js').type).toBe('parser'); + expect(initiators.get('injectedfile.js').type).toBe('script'); + expect(initiators.get('injectedfile.js').stack.callFrames[0].url).toBe( + server.PREFIX + '/initiator.js' + ); + expect(initiators.get('injectedstyle.css').type).toBe('script'); + expect(initiators.get('injectedstyle.css').stack.callFrames[0].url).toBe( + server.PREFIX + '/initiator.js' + ); + expect(initiators.get('initiator.js').url).toBe( + server.PREFIX + '/initiator.html' + ); + }); + }); + + describeFailsFirefox('Response.fromCache', function () { + it('should return |false| for non-cached content', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromCache()).toBe(false); + }); + + it('should work', async () => { + const { page, server } = getTestState(); + + const responses = new Map(); + page.on( + 'response', + (r) => + !utils.isFavicon(r.request()) && + responses.set(r.url().split('/').pop(), r) + ); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describeFailsFirefox('Response.fromServiceWorker', function () { + it('should return |false| for non-service-worker content', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.fromServiceWorker()).toBe(false); + }); + + it('Response.fromServiceWorker', async () => { + const { page, server } = getTestState(); + + const responses = new Map(); + page.on( + 'response', + (r) => !utils.isFavicon(r) && responses.set(r.url().split('/').pop(), r) + ); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', { + waitUntil: 'networkidle2', + }); + await page.evaluate(async () => await globalThis.activationPromise); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + }); + }); + + describeFailsFirefox('Request.postData', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (req, res) => res.end()); + let request = null; + page.on('request', (r) => { + if (!utils.isFavicon(r)) request = r; + }); + await page.evaluate(() => + fetch('./post', { + method: 'POST', + body: JSON.stringify({ foo: 'bar' }), + }) + ); + expect(request).toBeTruthy(); + expect(request.postData()).toBe('{"foo":"bar"}'); + }); + it('should be |undefined| when there is no post data', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.EMPTY_PAGE); + expect(response.request().postData()).toBe(undefined); + }); + }); + + describeFailsFirefox('Response.text', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/simple.json'); + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should return uncompressed text', async () => { + const { page, server } = getTestState(); + + server.enableGzip('/simple.json'); + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(response.headers()['content-encoding']).toBe('gzip'); + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should throw when requesting body of redirected response', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/foo.html', '/empty.html'); + const response = await page.goto(server.PREFIX + '/foo.html'); + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + const redirected = redirectChain[0].response(); + expect(redirected.status()).toBe(302); + let error = null; + await redirected.text().catch((error_) => (error = error_)); + expect(error.message).toContain( + 'Response body is unavailable for redirect responses' + ); + }); + it('should wait until response completes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // Setup server to trap request. + let serverResponse = null; + server.setRoute('/get', (req, res) => { + serverResponse = res; + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.write('hello '); + }); + // Setup page to trap response. + let requestFinished = false; + page.on( + 'requestfinished', + (r) => (requestFinished = requestFinished || r.url().includes('/get')) + ); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse((r) => !utils.isFavicon(r.request())), + page.evaluate(() => fetch('./get', { method: 'GET' })), + server.waitForRequest('/get'), + ]); + + expect(serverResponse).toBeTruthy(); + expect(pageResponse).toBeTruthy(); + expect(pageResponse.status()).toBe(200); + expect(requestFinished).toBe(false); + + const responseText = pageResponse.text(); + // Write part of the response and wait for it to be flushed. + await new Promise((x) => serverResponse.write('wor', x)); + // Finish response. + await new Promise((x) => serverResponse.end('ld!', x)); + expect(await responseText).toBe('hello world!'); + }); + }); + + describeFailsFirefox('Response.json', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/simple.json'); + expect(await response.json()).toEqual({ foo: 'bar' }); + }); + }); + + describeFailsFirefox('Response.buffer', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should work with compression', async () => { + const { page, server } = getTestState(); + + server.enableGzip('/pptr.png'); + const response = await page.goto(server.PREFIX + '/pptr.png'); + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should throw if the response does not have a body', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/empty.html'); + + server.setRoute('/test.html', (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'x-ping'); + res.end('Hello World'); + }); + const url = server.CROSS_PROCESS_PREFIX + '/test.html'; + const responsePromise = new Promise((resolve) => { + page.on('response', (response) => { + // Get the preflight response. + if ( + response.request().method() === 'OPTIONS' && + response.url() === url + ) { + resolve(response); + } + }); + }); + + // Trigger a request with a preflight. + await page.evaluate<(src: string) => void>(async (src) => { + const response = await fetch(src, { + method: 'POST', + headers: { 'x-ping': 'pong' }, + }); + return response; + }, url); + + const response = await responsePromise; + await expect(response.buffer()).rejects.toThrowError( + 'Could not load body for this request. This might happen if the request is a preflight request.' + ); + }); + }); + + describe('Response.statusText', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setRoute('/cool', (req, res) => { + res.writeHead(200, 'cool!'); + res.end(); + }); + const response = await page.goto(server.PREFIX + '/cool'); + expect(response.statusText()).toBe('cool!'); + }); + + it('handles missing status text', async () => { + const { page, server } = getTestState(); + + server.setRoute('/nostatus', (req, res) => { + res.writeHead(200, ''); + res.end(); + }); + const response = await page.goto(server.PREFIX + '/nostatus'); + expect(response.statusText()).toBe(''); + }); + }); + + describeFailsFirefox('Response.timing', function () { + it('returns timing information', async () => { + const { page, server } = getTestState(); + const responses = []; + page.on('response', (response) => responses.push(response)); + await page.goto(server.EMPTY_PAGE); + expect(responses.length).toBe(1); + expect(responses[0].timing().receiveHeadersEnd).toBeGreaterThan(0); + }); + }); + + describeFailsFirefox('Network Events', function () { + it('Page.Events.Request', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[0].method()).toBe('GET'); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + it('Page.Events.RequestServedFromCache', async () => { + const { page, server } = getTestState(); + + const cached = []; + page.on('requestservedfromcache', (r) => + cached.push(r.url().split('/').pop()) + ); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + expect(cached).toEqual([]); + + await page.reload(); + expect(cached).toEqual(['one-style.css']); + }); + it('Page.Events.Response', async () => { + const { page, server } = getTestState(); + + const responses = []; + page.on('response', (response) => responses.push(response)); + await page.goto(server.EMPTY_PAGE); + expect(responses.length).toBe(1); + expect(responses[0].url()).toBe(server.EMPTY_PAGE); + expect(responses[0].status()).toBe(200); + expect(responses[0].ok()).toBe(true); + expect(responses[0].request()).toBeTruthy(); + const remoteAddress = responses[0].remoteAddress(); + // Either IPv6 or IPv4, depending on environment. + expect( + remoteAddress.ip.includes('::1') || remoteAddress.ip === '127.0.0.1' + ).toBe(true); + expect(remoteAddress.port).toBe(server.PORT); + }); + + it('Page.Events.RequestFailed', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('css')) request.abort(); + else request.continue(); + }); + const failedRequests = []; + page.on('requestfailed', (request) => failedRequests.push(request)); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests.length).toBe(1); + expect(failedRequests[0].url()).toContain('one-style.css'); + expect(failedRequests[0].response()).toBe(null); + expect(failedRequests[0].resourceType()).toBe('stylesheet'); + if (isChrome) + expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); + else + expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); + expect(failedRequests[0].frame()).toBeTruthy(); + }); + it('Page.Events.RequestFinished', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('requestfinished', (request) => requests.push(request)); + await page.goto(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + expect(requests[0].response()).toBeTruthy(); + expect(requests[0].frame() === page.mainFrame()).toBe(true); + expect(requests[0].frame().url()).toBe(server.EMPTY_PAGE); + }); + it('should fire events in proper order', async () => { + const { page, server } = getTestState(); + + const events = []; + page.on('request', () => events.push('request')); + page.on('response', () => events.push('response')); + page.on('requestfinished', () => events.push('requestfinished')); + await page.goto(server.EMPTY_PAGE); + expect(events).toEqual(['request', 'response', 'requestfinished']); + }); + it('should support redirects', async () => { + const { page, server } = getTestState(); + + const events = []; + page.on('request', (request) => + events.push(`${request.method()} ${request.url()}`) + ); + page.on('response', (response) => + events.push(`${response.status()} ${response.url()}`) + ); + page.on('requestfinished', (request) => + events.push(`DONE ${request.url()}`) + ); + page.on('requestfailed', (request) => + events.push(`FAIL ${request.url()}`) + ); + server.setRedirect('/foo.html', '/empty.html'); + const FOO_URL = server.PREFIX + '/foo.html'; + const response = await page.goto(FOO_URL); + expect(events).toEqual([ + `GET ${FOO_URL}`, + `302 ${FOO_URL}`, + `DONE ${FOO_URL}`, + `GET ${server.EMPTY_PAGE}`, + `200 ${server.EMPTY_PAGE}`, + `DONE ${server.EMPTY_PAGE}`, + ]); + + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + expect(redirectChain[0].url()).toContain('/foo.html'); + expect(redirectChain[0].response().remoteAddress().port).toBe( + server.PORT + ); + }); + }); + + describe('Request.isNavigationRequest', () => { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + const requests = new Map(); + page.on('request', (request) => + requests.set(request.url().split('/').pop(), request) + ); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + itFailsFirefox('should work with request interception', async () => { + const { page, server } = getTestState(); + + const requests = new Map(); + page.on('request', (request) => { + requests.set(request.url().split('/').pop(), request); + request.continue(); + }); + await page.setRequestInterception(true); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + itFailsFirefox('should work when navigating to image', async () => { + const { page, server } = getTestState(); + + const requests = []; + page.on('request', (request) => requests.push(request)); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0].isNavigationRequest()).toBe(true); + }); + }); + + describeFailsFirefox('Page.setExtraHTTPHeaders', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should throw for non-string header values', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposeful bad input + await page.setExtraHTTPHeaders({ foo: 1 }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Expected value of header "foo" to be String, but "number" is found.' + ); + }); + }); + + describeFailsFirefox('Page.authenticate', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + server.setAuth('/empty.html', 'user', 'pass'); + let response; + try { + response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + } catch (error) { + // In headful, an error is thrown instead of 401. + if (!error.message.startsWith('net::ERR_INVALID_AUTH_CREDENTIALS')) { + throw error; + } + } + await page.authenticate({ + username: 'user', + password: 'pass', + }); + response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should fail if wrong credentials', async () => { + const { page, server } = getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user2', 'pass2'); + await page.authenticate({ + username: 'foo', + password: 'bar', + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + }); + it('should allow disable authentication', async () => { + const { page, server } = getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user3', 'pass3'); + await page.authenticate({ + username: 'user3', + password: 'pass3', + }); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + await page.authenticate(null); + // Navigate to a different origin to bust Chrome's credential caching. + try { + response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response.status()).toBe(401); + } catch (error) { + // In headful, an error is thrown instead of 401. + if (!error.message.startsWith('net::ERR_INVALID_AUTH_CREDENTIALS')) { + throw error; + } + } + }); + it('should not disable caching', async () => { + const { page, server } = getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/cached/one-style.css', 'user4', 'pass4'); + server.setAuth('/cached/one-style.html', 'user4', 'pass4'); + await page.authenticate({ + username: 'user4', + password: 'pass4', + }); + + const responses = new Map(); + page.on('response', (r) => responses.set(r.url().split('/').pop(), r)); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describeFailsFirefox('raw network headers', async () => { + it('Same-origin set-cookie navigation', async () => { + const { page, server } = getTestState(); + + const setCookieString = 'foo=bar'; + server.setRoute('/empty.html', (req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Same-origin set-cookie subresource', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + + const setCookieString = 'foo=bar'; + server.setRoute('/foo', (req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + + const responsePromise = new Promise((resolve) => + page.on('response', (response) => resolve(response)) + ); + page.evaluate(() => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/foo'); + xhr.send(); + }); + const subresourceResponse = await responsePromise; + expect(subresourceResponse.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Cross-origin set-cookie', async () => { + const { httpsServer, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/empty.html'); + + const setCookieString = 'hello=world'; + httpsServer.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('set-cookie', setCookieString); + res.end(); + }); + await page.goto(httpsServer.PREFIX + '/setcookie.html'); + + const response = await new Promise((resolve) => { + page.on('response', resolve); + const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; + page.evaluate<(src: string) => void>((src) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', src); + xhr.send(); + }, url); + }); + expect(response.headers()['set-cookie']).toBe(setCookieString); + } finally { + await page.close(); + await browser.close(); + } + }); + }); +}); diff --git a/test/oopif.spec.ts b/test/oopif.spec.ts new file mode 100644 index 0000000000000..2c7d9baca92e3 --- /dev/null +++ b/test/oopif.spec.ts @@ -0,0 +1,429 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + describeChromeOnly, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser; + let context; + let page; + + before(async () => { + const { puppeteer, defaultBrowserOptions } = getTestState(); + browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat([ + '--site-per-process', + '--remote-debugging-port=21222', + '--host-rules=MAP * 127.0.0.1', + ]), + }) + ); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + page = null; + context = null; + }); + + after(async () => { + await browser.close(); + browser = null; + }); + it('should treat OOP iframes and normal iframes the same', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => + frame.url().endsWith('/empty.html') + ); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(page.mainFrame().childFrames()).toHaveLength(2); + }); + it('should track navigations within OOP iframes', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/assets/frame.html' + ); + expect(frame.url()).toContain('/assets/frame.html'); + }); + it('should support OOP iframes becoming normal iframes again', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.isOOPFrame()).toBe(false); + expect(page.frames()).toHaveLength(2); + }); + it('should support frames within OOP frames', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame1Promise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + const frame2Promise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 2; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html' + ); + + const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]); + + expect(await frame1.evaluate(() => document.location.href)).toMatch( + /one-frame\.html$/ + ); + expect(await frame2.evaluate(() => document.location.href)).toMatch( + /frames\/frame\.html$/ + ); + }); + it('should support OOP iframes getting detached', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await utils.detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should support wait for navigation for transitions from local to OOPIF', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + const nav = frame.waitForNavigation(); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await nav; + expect(frame.isOOPFrame()).toBe(true); + await utils.detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should keep track of a frames OOP state', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.url()).toBe(server.EMPTY_PAGE); + }); + it('should support evaluating in oop iframes', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + _test = 'Test 123!'; + }); + const result = await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return window._test; + }); + expect(result).toBe('Test 123!'); + }); + it('should provide access to elements', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame = await framePromise; + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + button.onclick = () => { + button.id = 'clicked'; + }; + document.body.appendChild(button); + }); + await page.evaluate(() => { + document.body.style.border = '150px solid black'; + document.body.style.margin = '250px'; + document.body.style.padding = '50px'; + }); + await frame.waitForSelector('#test-button', { visible: true }); + await frame.click('#test-button'); + await frame.waitForSelector('#clicked'); + }); + it('should report oopif frames', async () => { + const { server } = getTestState(); + + const frame = page.waitForFrame((frame) => + frame.url().endsWith('/oopif.html') + ); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + }); + + it('should wait for inner OOPIFs', async () => { + const { server } = getTestState(); + await page.goto(`http://mainframe:${server.PORT}/main-frame.html`); + const frame2 = await page.waitForFrame((frame) => + frame.url().endsWith('inner-frame2.html') + ); + expect(oopifs(context).length).toBe(2); + expect(page.frames().filter((frame) => frame.isOOPFrame()).length).toBe(2); + expect( + await frame2.evaluate(() => document.querySelectorAll('button').length) + ).toStrictEqual(1); + }); + + it('should load oopif iframes with subresources and request interception', async () => { + const { server } = getTestState(); + + const frame = page.waitForFrame((frame) => + frame.url().endsWith('/oopif.html') + ); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context).length).toBe(1); + }); + it('should support frames within OOP iframes', async () => { + const { server } = getTestState(); + + const oopIframePromise = page.waitForFrame((frame) => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + const oopIframe = await oopIframePromise; + await utils.attachFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame1 = oopIframe.childFrames()[0]; + expect(frame1.url()).toMatch(/empty.html$/); + await utils.navigateFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/oopif.html' + ); + expect(frame1.url()).toMatch(/oopif.html$/); + await frame1.goto( + server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document', + { waitUntil: 'load' } + ); + expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/); + await utils.detachFrame(oopIframe, 'frame1'); + expect(oopIframe.childFrames()).toHaveLength(0); + }); + + it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => { + const { server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame((frame) => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await page.evaluate(() => { + document.body.style.border = '50px solid black'; + document.body.style.margin = '50px'; + document.body.style.padding = '50px'; + }); + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + document.body.appendChild(button); + }); + const button = await frame.waitForSelector('#test-button', { + visible: true, + }); + const result = await button.clickablePoint(); + expect(result.x).toBeGreaterThan(150); // padding + margin + border left + expect(result.y).toBeGreaterThan(150); // padding + margin + border top + const resultBoxModel = await button.boxModel(); + for (const quad of [ + resultBoxModel.content, + resultBoxModel.border, + resultBoxModel.margin, + resultBoxModel.padding, + ]) { + for (const part of quad) { + expect(part.x).toBeGreaterThan(150); // padding + margin + border left + expect(part.y).toBeGreaterThan(150); // padding + margin + border top + } + } + const resultBoundingBox = await button.boundingBox(); + expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left + expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top + }); + + it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => { + const { server, puppeteer } = getTestState(); + + const frame = page.waitForFrame((frame) => + frame.url().endsWith('/oopif.html') + ); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + + const browserURL = 'http://127.0.0.1:21222'; + const browser1 = await puppeteer.connect({ browserURL }); + const target = await browser1.waitForTarget((target) => + target.url().endsWith('dynamic-oopif.html') + ); + await target.page(); + browser1.disconnect(); + }); + itFailsFirefox('should support lazy OOP frames', async () => { + const { server } = getTestState(); + + await page.goto(server.PREFIX + '/lazy-oopif-frame.html'); + await page.setViewport({ width: 1000, height: 1000 }); + + expect(page.frames().map((frame) => frame._hasStartedLoading)).toEqual([ + true, + true, + false, + ]); + }); + + describe('waitForFrame', () => { + it('should resolve immediately if the frame already exists', async () => { + const { server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + await page.waitForFrame((frame) => frame.url().endsWith('/empty.html')); + }); + }); +}); + +/** + * @param {!BrowserContext} context + */ +function oopifs(context) { + return context + .targets() + .filter((target) => target._targetInfo.type === 'iframe'); +} diff --git a/test/page.spec.ts b/test/page.spec.ts new file mode 100644 index 0000000000000..9c31f5b7a4fe2 --- /dev/null +++ b/test/page.spec.ts @@ -0,0 +1,1976 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import sinon from 'sinon'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Page, Metrics } from '../lib/cjs/puppeteer/common/Page.js'; +import { CDPSession } from '../lib/cjs/puppeteer/common/Connection.js'; +import { JSHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; + +describe('Page', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.close', function () { + it('should reject all promises when page is closed', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + let error = null; + await Promise.all([ + newPage + .evaluate(() => new Promise(() => {})) + .catch((error_) => (error = error_)), + newPage.close(), + ]); + expect(error.message).toContain('Protocol error'); + }); + it('should not be visible in browser.pages', async () => { + const { browser } = getTestState(); + + const newPage = await browser.newPage(); + expect(await browser.pages()).toContain(newPage); + await newPage.close(); + expect(await browser.pages()).not.toContain(newPage); + }); + itFailsFirefox('should run beforeunload if asked for', async () => { + const { context, server, isChrome } = getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + const pageClosingPromise = newPage.close({ runBeforeUnload: true }); + const dialog = await waitEvent(newPage, 'dialog'); + expect(dialog.type()).toBe('beforeunload'); + expect(dialog.defaultValue()).toBe(''); + if (isChrome) expect(dialog.message()).toBe(''); + else expect(dialog.message()).toBeTruthy(); + await dialog.accept(); + await pageClosingPromise; + }); + itFailsFirefox('should *not* run beforeunload by default', async () => { + const { context, server } = getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + await newPage.close(); + }); + it('should set the page close state', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + expect(newPage.isClosed()).toBe(false); + await newPage.close(); + expect(newPage.isClosed()).toBe(true); + }); + itFailsFirefox('should terminate network waiters', async () => { + const { context, server } = getTestState(); + + const newPage = await context.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch((error) => error), + newPage.waitForResponse(server.EMPTY_PAGE).catch((error) => error), + newPage.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Target closed'); + expect(message).not.toContain('Timeout'); + } + }); + }); + + describe('Page.Events.Load', function () { + it('should fire when expected', async () => { + const { page } = getTestState(); + + await Promise.all([ + page.goto('about:blank'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + describe('removing and adding event handlers', () => { + it('should correctly fire event handlers as they are added and then removed', async () => { + const { page, server } = getTestState(); + + const handler = sinon.spy(); + const onResponse = (response) => { + // Ignore default favicon requests. + if (!response.url().endsWith('favicon.ico')) { + handler(); + } + }; + page.on('response', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(1); + page.off('response', onResponse); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(1); + page.on('response', onResponse); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(2); + }); + + it('should correctly added and removed request events', async () => { + const { page, server } = getTestState(); + + const handler = sinon.spy(); + const onResponse = (response) => { + // Ignore default favicon requests. + if (!response.url().endsWith('favicon.ico')) { + handler(); + } + }; + + page.on('request', onResponse); + page.on('request', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(2); + page.off('request', onResponse); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(3); + page.off('request', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(3); + page.on('request', onResponse); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(4); + }); + }); + + describeFailsFirefox('Page.Events.error', function () { + it('should throw when page crashes', async () => { + const { page } = getTestState(); + + let error = null; + page.on('error', (err) => (error = err)); + page.goto('chrome://crash').catch(() => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describeFailsFirefox('Page.Events.Popup', function () { + it('should work', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with noopener', async () => { + const { page } = getTestState(); + + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.evaluate(() => window.open('about:blank', null, 'noopener')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank and without rel=opener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank and with rel=opener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + }); + it('should work with fake-clicking target=_blank and rel=noopener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.$eval('a', (a: HTMLAnchorElement) => a.click()), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + it('should work with clicking target=_blank and rel=noopener', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + 'yo' + ); + const [popup] = await Promise.all([ + new Promise((x) => page.once('popup', x)), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + }); + }); + + describe('BrowserContext.overridePermissions', function () { + function getPermission(page, name) { + return page.evaluate( + (name) => + navigator.permissions.query({ name }).then((result) => result.state), + name + ); + } + + it('should be prompt by default', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + itFailsFirefox('should deny permission when not listed', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + }); + it('should fail when bad permission is given', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + await context + // @ts-expect-error purposeful bad input for test + .overridePermissions(server.EMPTY_PAGE, ['foo']) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Unknown permission: foo'); + }); + itFailsFirefox('should grant permission when listed', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + itFailsFirefox('should reset permissions', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + itFailsFirefox('should trigger permission onchange', async () => { + const { page, server, context, isHeadless } = getTestState(); + + // TODO: re-enable this test in headful once crbug.com/1324480 rolls out. + if (!isHeadless) return; + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + globalThis.events = []; + return navigator.permissions + .query({ name: 'geolocation' }) + .then(function (result) { + globalThis.events.push(result.state); + result.onchange = function () { + globalThis.events.push(result.state); + }; + }); + }); + expect(await page.evaluate(() => globalThis.events)).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + ]); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + ]); + await context.clearPermissionOverrides(); + expect(await page.evaluate(() => globalThis.events)).toEqual([ + 'prompt', + 'denied', + 'granted', + 'prompt', + ]); + }); + itFailsFirefox( + 'should isolate permissions between browser contexts', + async () => { + const { page, server, context, browser } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const otherContext = await browser.createIncognitoBrowserContext(); + const otherPage = await otherContext.newPage(); + await otherPage.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); + + await context.overridePermissions(server.EMPTY_PAGE, []); + await otherContext.overridePermissions(server.EMPTY_PAGE, [ + 'geolocation', + ]); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await otherContext.close(); + } + ); + itFailsFirefox('should grant persistent-storage', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'persistent-storage')).not.toBe( + 'granted' + ); + await context.overridePermissions(server.EMPTY_PAGE, [ + 'persistent-storage', + ]); + expect(await getPermission(page, 'persistent-storage')).toBe('granted'); + }); + }); + + describe('Page.setGeolocation', function () { + itFailsFirefox('should work', async () => { + const { page, server, context } = getTestState(); + + await context.overridePermissions(server.PREFIX, ['geolocation']); + await page.goto(server.EMPTY_PAGE); + await page.setGeolocation({ longitude: 10, latitude: 10 }); + const geolocation = await page.evaluate( + () => + new Promise((resolve) => + navigator.geolocation.getCurrentPosition((position) => { + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + }) + ) + ); + expect(geolocation).toEqual({ + latitude: 10, + longitude: 10, + }); + }); + it('should throw when invalid longitude', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.setGeolocation({ longitude: 200, latitude: 10 }); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describeFailsFirefox('Page.setOfflineMode', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setOfflineMode(true); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + await page.setOfflineMode(false); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should emulate navigator.onLine', async () => { + const { page } = getTestState(); + + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + await page.setOfflineMode(true); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(false); + await page.setOfflineMode(false); + expect(await page.evaluate(() => window.navigator.onLine)).toBe(true); + }); + }); + + describe('ExecutionContext.queryObjects', function () { + itFailsFirefox('should work', async () => { + const { page } = getTestState(); + + // Instantiate an object + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + const values = await page.evaluate( + (objects) => Array.from(objects[0].values()), + objectsHandle + ); + expect(values).toEqual(['hello', 'world']); + }); + itFailsFirefox('should work for non-blank page', async () => { + const { page, server } = getTestState(); + + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => (globalThis.set = new Set(['hello', 'world']))); + const prototypeHandle = await page.evaluateHandle(() => Set.prototype); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate( + (objects: JSHandle[]) => objects.length, + objectsHandle + ); + expect(count).toBe(1); + }); + it('should fail for disposed handles', async () => { + const { page } = getTestState(); + + const prototypeHandle = await page.evaluateHandle( + () => HTMLBodyElement.prototype + ); + await prototypeHandle.dispose(); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (error = error_)); + expect(error.message).toBe('Prototype JSHandle is disposed!'); + }); + it('should fail primitive values as prototypes', async () => { + const { page } = getTestState(); + + const prototypeHandle = await page.evaluateHandle(() => 42); + let error = null; + await page + .queryObjects(prototypeHandle) + .catch((error_) => (error = error_)); + expect(error.message).toBe( + 'Prototype JSHandle must not be referencing primitive value' + ); + }); + }); + + describeFailsFirefox('Page.Events.Console', function () { + it('should work', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (m) => (message = m)); + await Promise.all([ + page.evaluate(() => console.log('hello', 5, { foo: 'bar' })), + waitEvent(page, 'console'), + ]); + expect(message.text()).toEqual('hello 5 JSHandle@object'); + expect(message.type()).toEqual('log'); + expect(message.args()).toHaveLength(3); + expect(message.location()).toEqual({ + url: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), + }); + + expect(await message.args()[0].jsonValue()).toEqual('hello'); + expect(await message.args()[1].jsonValue()).toEqual(5); + expect(await message.args()[2].jsonValue()).toEqual({ foo: 'bar' }); + }); + it('should work for different console API calls', async () => { + const { page } = getTestState(); + + const messages = []; + page.on('console', (msg) => messages.push(msg)); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + // A pair of time/timeEnd generates only one Console API call. + console.time('calling console.time'); + console.timeEnd('calling console.time'); + console.trace('calling console.trace'); + console.dir('calling console.dir'); + console.warn('calling console.warn'); + console.error('calling console.error'); + console.log(Promise.resolve('should not wait until resolved!')); + }); + expect(messages.map((msg) => msg.type())).toEqual([ + 'timeEnd', + 'trace', + 'dir', + 'warning', + 'error', + 'log', + ]); + expect(messages[0].text()).toContain('calling console.time'); + expect(messages.slice(1).map((msg) => msg.text())).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should not fail for window object', async () => { + const { page } = getTestState(); + + let message = null; + page.once('console', (msg) => (message = msg)); + await Promise.all([ + page.evaluate(() => console.error(window)), + waitEvent(page, 'console'), + ]); + expect(message.text()).toBe('JSHandle@object'); + }); + it('should trigger correct Log', async () => { + const { page, server, isChrome } = getTestState(); + + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate( + async (url: string) => fetch(url).catch(() => {}), + server.EMPTY_PAGE + ), + ]); + expect(message.text()).toContain('Access-Control-Allow-Origin'); + if (isChrome) expect(message.type()).toEqual('error'); + else expect(message.type()).toEqual('warn'); + }); + it('should have location when fetch fails', async () => { + const { page, server } = getTestState(); + + // The point of this test is to make sure that we report console messages from + // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.setContent(``), + ]); + expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`); + expect(message.type()).toEqual('error'); + expect(message.location()).toEqual({ + url: 'http://wat/', + lineNumber: undefined, + }); + }); + it('should have location and stack trace for console API calls', async () => { + const { page, server, isChrome } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.goto(server.PREFIX + '/consolelog.html'), + ]); + expect(message.text()).toBe('yellow'); + expect(message.type()).toBe('log'); + expect(message.location()).toEqual({ + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }); + expect(message.stackTrace()).toEqual([ + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 11, + columnNumber: 8, + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 13, + columnNumber: 6, + }, + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3865 + it('should not throw when there are console messages in detached iframes', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async () => { + // 1. Create a popup that Puppeteer is not connected to. + const win = window.open( + window.location.href, + 'Title', + 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0' + ); + await new Promise((x) => (win.onload = x)); + // 2. In this popup, create an iframe that console.logs a message. + win.document.body.innerHTML = ``; + const frame = win.document.querySelector('iframe'); + await new Promise((x) => (frame.onload = x)); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page + .browserContext() + .targets() + .find((target) => target !== page.target()); + // 4. Connect to the popup and make sure it doesn't throw. + await popupTarget.page(); + }); + }); + + describe('Page.Events.DOMContentLoaded', function () { + it('should fire when expected', async () => { + const { page } = getTestState(); + + page.goto('about:blank'); + await waitEvent(page, 'domcontentloaded'); + }); + }); + + describeFailsFirefox('Page.metrics', function () { + it('should get metrics from a page', async () => { + const { page } = getTestState(); + + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async () => { + const { page } = getTestState(); + + const metricsPromise = new Promise<{ metrics: Metrics; title: string }>( + (fulfill) => page.once('metrics', fulfill) + ); + await page.evaluate(() => console.timeStamp('test42')); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(metrics) { + const metricsToCheck = new Set([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', + ]); + for (const name in metrics) { + expect(metricsToCheck.has(name)).toBeTruthy(); + expect(metrics[name]).toBeGreaterThanOrEqual(0); + metricsToCheck.delete(name); + } + expect(metricsToCheck.size).toBe(0); + } + }); + + describe('Page.waitForRequest', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with predicate', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest( + (request) => request.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForRequest(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', { timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForResponse', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForResponse(() => false, { timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => false) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with predicate', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse( + (response) => response.url() === server.PREFIX + '/digits/2.png' + ), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with async predicate', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(async (response) => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png', { timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForNetworkIdle', function () { + it('should work', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + let res; + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle().then((r) => { + res = r; + return Date.now(); + }), + page + .evaluate(() => + (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise((resolve) => setTimeout(resolve, 200)); + await fetch('/digits/3.png'); + await new Promise((resolve) => setTimeout(resolve, 200)); + await fetch('/digits/4.png'); + })() + ) + .then(() => Date.now()), + ]); + expect(res).toBe(undefined); + expect(t1).toBeGreaterThan(t2); + expect(t1 - t2).toBeGreaterThanOrEqual(400); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + let error = null; + await page + .waitForNetworkIdle({ timeout: 1 }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect idleTime', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle({ idleTime: 10 }).then(() => Date.now()), + page + .evaluate(() => + (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise((resolve) => setTimeout(resolve, 250)); + })() + ) + .then(() => Date.now()), + ]); + expect(t2).toBeGreaterThan(t1); + }); + it('should work with no timeout', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [result] = await Promise.all([ + page.waitForNetworkIdle({ timeout: 0 }), + page.evaluate(() => + setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50) + ), + ]); + expect(result).toBe(undefined); + }); + }); + + describeFailsFirefox('Page.exposeFunction', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const { page } = getTestState(); + + await page.exposeFunction('woof', function () { + throw new Error('WOOF WOOF'); + }); + const { message, stack } = await page.evaluate(async () => { + try { + await globalThis.woof(); + } catch (error) { + return { message: error.message, stack: error.stack }; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain(__filename); + }); + it('should support throwing "null"', async () => { + const { page } = getTestState(); + + await page.exposeFunction('woof', function () { + throw null; + }); + const thrown = await page.evaluate(async () => { + try { + await globalThis.woof(); + } catch (error) { + return error; + } + }); + expect(thrown).toBe(null); + }); + it('should be callable from-inside evaluateOnNewDocument', async () => { + const { page } = getTestState(); + + let called = false; + await page.exposeFunction('woof', function () { + called = true; + }); + await page.evaluateOnNewDocument(() => globalThis.woof()); + await page.reload(); + expect(called).toBe(true); + }); + it('should survive navigation', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const { page } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const { page, server } = getTestState(); + + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const frame = page.frames()[1]; + const result = await frame.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames before navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]; + const result = await frame.evaluate(async function () { + return await globalThis.compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should not throw when frames detach', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.exposeFunction('compute', function (a, b) { + return Promise.resolve(a * b); + }); + await utils.detachFrame(page, 'frame1'); + + await expect( + page.evaluate(async function () { + return await globalThis.compute(3, 5); + }) + ).resolves.toEqual(15); + }); + it('should work with complex objects', async () => { + const { page } = getTestState(); + + await page.exposeFunction('complexObject', function (a, b) { + return { x: a.x + b.x }; + }); + const result = await page.evaluate<() => Promise<{ x: number }>>( + async () => globalThis.complexObject({ x: 5 }, { x: 2 }) + ); + expect(result.x).toBe(7); + }); + it('should fallback to default export when passed a module object', async () => { + const { page, server } = getTestState(); + const moduleObject = { + default: function (a, b) { + return a * b; + }, + }; + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('compute', moduleObject); + const result = await page.evaluate(async function () { + return await globalThis.compute(9, 4); + }); + expect(result).toBe(36); + }); + }); + + describeFailsFirefox('Page.Events.PageError', function () { + it('should fire', async () => { + const { page, server } = getTestState(); + + let error = null; + page.once('pageerror', (e) => (error = e)); + await Promise.all([ + page.goto(server.PREFIX + '/error.html'), + waitEvent(page, 'pageerror'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'Mozilla' + ); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should work for subframes', async () => { + const { page, server } = getTestState(); + + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'Mozilla' + ); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const { page, server, puppeteer } = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain( + 'iPhone' + ); + await page.setUserAgent(puppeteer.devices['iPhone 6'].userAgent); + expect(await page.evaluate(() => navigator.userAgent)).toContain( + 'iPhone' + ); + }); + itFailsFirefox('should work with additional userAgentMetdata', async () => { + const { page, server } = getTestState(); + + await page.setUserAgent('MockBrowser', { + architecture: 'Mock1', + mobile: false, + model: 'Mockbook', + platform: 'MockOS', + platformVersion: '3.1', + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect( + await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: userAgentData not yet in TypeScript DOM API + return navigator.userAgentData.mobile; + }) + ).toBe(false); + + const uaData = await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: userAgentData not yet in TypeScript DOM API + return navigator.userAgentData.getHighEntropyValues([ + 'architecture', + 'model', + 'platform', + 'platformVersion', + ]); + }); + expect(uaData['architecture']).toBe('Mock1'); + expect(uaData['model']).toBe('Mockbook'); + expect(uaData['platform']).toBe('MockOS'); + expect(uaData['platformVersion']).toBe('3.1'); + expect(request.headers['user-agent']).toBe('MockBrowser'); + }); + }); + + describe('Page.setContent', function () { + const expectedOutput = + '
hello
'; + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent('
hello
'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async () => { + const { page } = getTestState(); + + const doctype = ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async () => { + const { page } = getTestState(); + + const doctype = + ''; + await page.setContent(`${doctype}
hello
`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should respect timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(``, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const { page, server, puppeteer } = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error = null; + await page + .setContent(``) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should await resources to load', async () => { + const { page, server } = getTestState(); + + const imgPath = '/img.png'; + let imgResponse = null; + server.setRoute(imgPath, (req, res) => (imgResponse = res)); + let loaded = false; + const contentPromise = page + .setContent(``) + .then(() => (loaded = true)); + await server.waitForRequest(imgPath); + expect(loaded).toBe(false); + imgResponse.end(); + await contentPromise; + }); + it('should work fast enough', async () => { + const { page } = getTestState(); + + for (let i = 0; i < 20; ++i) await page.setContent('
yo
'); + }); + it('should work with tricky content', async () => { + const { page } = getTestState(); + + await page.setContent('
hello world
' + '\x7F'); + expect(await page.$eval('div', (div) => div.textContent)).toBe( + 'hello world' + ); + }); + it('should work with accents', async () => { + const { page } = getTestState(); + + await page.setContent('
aberración
'); + expect(await page.$eval('div', (div) => div.textContent)).toBe( + 'aberración' + ); + }); + it('should work with emojis', async () => { + const { page } = getTestState(); + + await page.setContent('
🐥
'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('🐥'); + }); + it('should work with newline', async () => { + const { page } = getTestState(); + + await page.setContent('
\n
'); + expect(await page.$eval('div', (div) => div.textContent)).toBe('\n'); + }); + }); + + describeFailsFirefox('Page.setBypassCSP', function () { + it('should bypass CSP meta tag', async () => { + const { page, server } = getTestState(); + + // Make sure CSP prohibits addScriptTag. + await page.goto(server.PREFIX + '/csp.html'); + await page + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await page.evaluate(() => globalThis.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should bypass CSP header', async () => { + const { page, server } = getTestState(); + + // Make sure CSP prohibits addScriptTag. + server.setCSP('/empty.html', 'default-src "self"'); + await page.goto(server.EMPTY_PAGE); + await page + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await page.evaluate(() => globalThis.__injected)).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should bypass after cross-process navigation', async () => { + const { page, server } = getTestState(); + + await page.setBypassCSP(true); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({ content: 'window.__injected = 42;' }); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + it('should bypass CSP in iframes as well', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + { + // Make sure CSP prohibits addScriptTag in an iframe. + const frame = await utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe( + undefined + ); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = await utils.attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ); + await frame + .addScriptTag({ content: 'window.__injected = 42;' }) + .catch((error) => void error); + expect(await frame.evaluate(() => globalThis.__injected)).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function () { + it('should throw an error if no options are provided', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposefully passing bad options + await page.addScriptTag('/injectedfile.js'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + it('should work with a url', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ url: '/injectedfile.js' }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should work with a url and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ url: '/es6/es6import.js', type: 'module' }); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should work with a path and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, 'assets/es6/es6pathimport.js'), + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should work with a content and type=module', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + content: `import num from '/es6/es6module.js';window.__es6injected = num;`, + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect(await page.evaluate(() => globalThis.__es6injected)).toBe(42); + }); + + it('should throw an error if loading from url fail', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addScriptTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading script from /nonexistfile.js failed'); + }); + + it('should work with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ + path: path.join(__dirname, 'assets/injectedfile.js'), + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(42); + }); + + it('should include sourcemap when path is provided', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, 'assets/injectedfile.js'), + }); + const result = await page.evaluate( + () => globalThis.__injectedError.stack + ); + expect(result).toContain(path.join('assets', 'injectedfile.js')); + }); + + it('should work with content', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const scriptHandle = await page.addScriptTag({ + content: 'window.__injected = 35;', + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect(await page.evaluate(() => globalThis.__injected)).toBe(35); + }); + + it('should add id when provided', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ content: 'window.__injected = 1;', id: 'one' }); + await page.addScriptTag({ url: '/injectedfile.js', id: 'two' }); + expect(await page.$('#one')).not.toBeNull(); + expect(await page.$('#two')).not.toBeNull(); + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4840 + xit('should throw when added with content to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addScriptTag({ content: 'window.__injected = 35;' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addScriptTag({ url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.addStyleTag', function () { + it('should throw an error if no options are provided', async () => { + const { page } = getTestState(); + + let error = null; + try { + // @ts-expect-error purposefully passing bad input + await page.addStyleTag('/injectedstyle.css'); + } catch (error_) { + error = error_; + } + expect(error.message).toBe( + 'Provide an object with a `url`, `path` or `content` property' + ); + }); + + it('should work with a url', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ url: '/injectedstyle.css' }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should throw an error if loading from url fail', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error = null; + try { + await page.addStyleTag({ url: '/nonexistfile.js' }); + } catch (error_) { + error = error_; + } + expect(error.message).toBe('Loading style from /nonexistfile.js failed'); + }); + + it('should work with a path', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ + path: path.join(__dirname, 'assets/injectedstyle.css'), + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should include sourcemap when path is provided', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addStyleTag({ + path: path.join(__dirname, 'assets/injectedstyle.css'), + }); + const styleHandle = await page.$('style'); + const styleContent = await page.evaluate( + (style: HTMLStyleElement) => style.innerHTML, + styleHandle + ); + expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); + }); + + it('should work with content', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const styleHandle = await page.addStyleTag({ + content: 'body { background-color: green; }', + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(0, 128, 0)'); + }); + + itFailsFirefox( + 'should throw when added with content to the CSP page', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addStyleTag({ content: 'body { background-color: green; }' }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + } + ); + + it('should throw when added with URL to the CSP page', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error = null; + await page + .addStyleTag({ + url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css', + }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.url', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + expect(page.url()).toBe('about:blank'); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + }); + + describeFailsFirefox('Page.setJavaScriptEnabled', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, ' + ); + let error = null; + await page.evaluate('something').catch((error_) => (error = error_)); + expect(error.message).toContain('something is not defined'); + + await page.setJavaScriptEnabled(true); + await page.goto( + 'data:text/html, ' + ); + expect(await page.evaluate('something')).toBe('forbidden'); + }); + }); + + describe('Page.setCacheEnabled', function () { + it('should enable or disable the cache based on the state passed', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [cachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + // Rely on "if-modified-since" caching in our test server. + expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined); + + await page.setCacheEnabled(false); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + itFailsFirefox( + 'should stay disabled when toggling request interception on/off', + async () => { + const { page, server } = getTestState(); + + await page.setCacheEnabled(false); + await page.setRequestInterception(true); + await page.setRequestInterception(false); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + } + ); + }); + + describe('printing to PDF', function () { + it('can print to PDF and save to file', async () => { + // Printing to pdf is currently only supported in headless + const { isHeadless, page } = getTestState(); + + if (!isHeadless) return; + + const outputFile = __dirname + '/assets/output.pdf'; + await page.pdf({ path: outputFile }); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + + it('can print to PDF and stream the result', async () => { + // Printing to pdf is currently only supported in headless + const { isHeadless, page } = getTestState(); + + if (!isHeadless) return; + + const stream = await page.createPDFStream(); + let size = 0; + for await (const chunk of stream) { + size += chunk.length; + } + expect(size).toBeGreaterThan(0); + }); + + it('should respect timeout', async () => { + const { isHeadless, page, server, puppeteer } = getTestState(); + if (!isHeadless) return; + + await page.goto(server.PREFIX + '/pdf.html'); + + let error = null; + await page.pdf({ timeout: 1 }).catch((_error) => (error = _error)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + }); + + describe('Page.title', function () { + it('should return the page title', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function () { + it('should select single option', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + }); + it('should select only first option', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + }); + it('should not throw when select causes navigation', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.$eval('select', (select) => + select.addEventListener( + 'input', + () => ((window as any).location = '/empty.html') + ) + ); + await Promise.all([ + page.select('select', 'blue'), + page.waitForNavigation(), + ]); + expect(page.url()).toContain('empty.html'); + }); + it('should select multiple options', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'green', 'red'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + 'green', + 'red', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + 'green', + 'red', + ]); + }); + it('should respect event bubbling', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => globalThis.result.onBubblingInput) + ).toEqual(['blue']); + expect( + await page.evaluate(() => globalThis.result.onBubblingChange) + ).toEqual(['blue']); + }); + it('should throw when element is not a element.'); + }); + it('should return [] on no matched values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select', '42', 'abc'); + expect(result).toEqual([]); + }); + it('should return an array of matched values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + const result = await page.select('select', 'blue', 'black', 'magenta'); + expect( + result.reduce( + (accumulator, current) => + ['blue', 'black', 'magenta'].includes(current) && accumulator, + true + ) + ).toEqual(true); + }); + it('should return an array of one element when multiple is not set', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select( + 'select', + '42', + 'blue', + 'black', + 'magenta' + ); + expect(result.length).toEqual(1); + }); + it('should return [] on no values', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select'); + expect(result).toEqual([]); + }); + it('should deselect all options when passed no values for a multiple select', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => globalThis.makeMultiple()); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', (select: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !option.selected + ) + ) + ).toEqual(true); + }); + it('should deselect all options when passed no values for a select without multiple', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', (select: HTMLSelectElement) => + Array.from(select.options).every( + (option: HTMLOptionElement) => !option.selected + ) + ) + ).toEqual(true); + }); + it('should throw if passed in non-strings', async () => { + const { page } = getTestState(); + + await page.setContent(''); + let error = null; + try { + // @ts-expect-error purposefully passing bad input + await page.select('select', 12); + } catch (error_) { + error = error_; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3327 + itFailsFirefox( + 'should work when re-defining top-level Event class', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => (window.Event = null)); + await page.select('select', 'blue'); + expect(await page.evaluate(() => globalThis.result.onInput)).toEqual([ + 'blue', + ]); + expect(await page.evaluate(() => globalThis.result.onChange)).toEqual([ + 'blue', + ]); + } + ); + }); + + describe('Page.Events.Close', function () { + itFailsFirefox('should work with window.close', async () => { + const { page, context } = getTestState(); + + const newPagePromise = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target.page())) + ); + await page.evaluate( + () => (window['newPage'] = window.open('about:blank')) + ); + const newPage = await newPagePromise; + const closedPromise = new Promise((x) => newPage.on('close', x)); + await page.evaluate(() => window['newPage'].close()); + await closedPromise; + }); + it('should work with page.close', async () => { + const { context } = getTestState(); + + const newPage = await context.newPage(); + const closedPromise = new Promise((x) => newPage.on('close', x)); + await newPage.close(); + await closedPromise; + }); + }); + + describe('Page.browser', function () { + it('should return the correct browser instance', async () => { + const { page, browser } = getTestState(); + + expect(page.browser()).toBe(browser); + }); + }); + + describe('Page.browserContext', function () { + it('should return the correct browser context instance', async () => { + const { page, context } = getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); + + describe('Page.client', function () { + it('should return the client instance', async () => { + const { page } = getTestState(); + expect(page.client()).toBeInstanceOf(CDPSession); + }); + }); +}); diff --git a/test/proxy.spec.ts b/test/proxy.spec.ts new file mode 100644 index 0000000000000..d26d64e69afe6 --- /dev/null +++ b/test/proxy.spec.ts @@ -0,0 +1,226 @@ +/** + * Copyright 2021 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 http from 'http'; +import os from 'os'; +import { + getTestState, + describeFailsFirefox, + itFailsWindows, +} from './mocha-utils'; // eslint-disable-line import/extensions +import type { Server, IncomingMessage, ServerResponse } from 'http'; +import type { Browser } from '../lib/cjs/puppeteer/common/Browser.js'; +import type { AddressInfo } from 'net'; + +const HOSTNAME = os.hostname().toLowerCase(); + +/** + * Requests to localhost do not get proxied by default. Create a URL using the hostname + * instead. + */ +function getEmptyPageUrl(server: { PORT: number; EMPTY_PAGE: string }): string { + const emptyPagePath = new URL(server.EMPTY_PAGE).pathname; + + return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`; +} + +describeFailsFirefox('request proxy', () => { + let browser: Browser; + let proxiedRequestUrls: string[]; + let proxyServer: Server; + let proxyServerUrl: string; + const defaultArgs = [ + '--disable-features=NetworkTimeServiceQuerying', // We disable this in tests so that proxy-related tests don't intercept queries from this service in headful. + ]; + + beforeEach(() => { + proxiedRequestUrls = []; + + proxyServer = http + .createServer( + ( + originalRequest: IncomingMessage, + originalResponse: ServerResponse + ) => { + proxiedRequestUrls.push(originalRequest.url as string); + + const proxyRequest = http.request( + originalRequest.url as string, + { + method: originalRequest.method, + headers: originalRequest.headers, + }, + (proxyResponse) => { + originalResponse.writeHead( + proxyResponse.statusCode as number, + proxyResponse.headers + ); + proxyResponse.pipe(originalResponse, { end: true }); + } + ); + + originalRequest.pipe(proxyRequest, { end: true }); + } + ) + .listen(); + + proxyServerUrl = `http://${HOSTNAME}:${ + (proxyServer.address() as AddressInfo).port + }`; + }); + + afterEach(async () => { + await browser.close(); + + await new Promise((resolve, reject) => { + proxyServer.close((error) => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); + }); + + it('should proxy requests when configured', async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + const page = await browser.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + }); + + it('should respect proxy bypass list', async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const page = await browser.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + describe('in incognito browser context', () => { + it('should proxy requests when configured at browser level', async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + }); + + it('should respect proxy bypass list when configured at browser level', async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + /** + * See issues #7873, #7719, and #7698. + */ + itFailsWindows( + 'should proxy requests when configured at context level', + async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + }); + const page = await context.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + } + ); + + it('should respect proxy bypass list when configured at context level', async () => { + const { puppeteer, defaultBrowserOptions, server } = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + proxyBypassList: [new URL(emptyPageUrl).host], + }); + const page = await context.newPage(); + const response = await page.goto(emptyPageUrl); + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + }); +}); diff --git a/test/queryselector.spec.ts b/test/queryselector.spec.ts new file mode 100644 index 0000000000000..be05fb3bd6da6 --- /dev/null +++ b/test/queryselector.spec.ts @@ -0,0 +1,527 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js'; + +describe('querySelector', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent('
43543
'); + const idAttribute = await page.$eval('section', (e) => e.id); + expect(idAttribute).toBe('testAttribute'); + }); + it('should accept arguments', async () => { + const { page } = getTestState(); + + await page.setContent('
hello
'); + const text = await page.$eval( + 'section', + (e, suffix) => e.textContent + suffix, + ' world!' + ); + expect(text).toBe('hello world!'); + }); + it('should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + + await page.setContent('
hello
world
'); + const divHandle = await page.$('div'); + const text = await page.$eval( + 'section', + (e, div: HTMLElement) => e.textContent + div.textContent, + divHandle + ); + expect(text).toBe('hello world'); + }); + it('should throw error if no element is found', async () => { + const { page } = getTestState(); + + let error = null; + await page + .$eval('section', (e) => e.id) + .catch((error_) => (error = error_)); + expect(error.message).toContain( + 'failed to find element matching selector "section"' + ); + }); + }); + + describe('pierceHandler', function () { + beforeEach(async () => { + const { page } = getTestState(); + await page.setContent( + `` + ); + }); + it('should find first element in shadow', async () => { + const { page } = getTestState(); + const div = await page.$('pierce/.foo'); + const text = await div.evaluate( + (element: Element) => element.textContent + ); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const { page } = getTestState(); + const divs = await page.$$('pierce/.foo'); + const text = await Promise.all( + divs.map((div) => + div.evaluate((element: Element) => element.textContent) + ) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + it('should find first child element', async () => { + const { page } = getTestState(); + const parentElement = await page.$('html > div'); + const childElement = await parentElement.$('pierce/div'); + const text = await childElement.evaluate( + (element: Element) => element.textContent + ); + expect(text).toBe('Hello'); + }); + it('should find all child elements', async () => { + const { page } = getTestState(); + const parentElement = await page.$('html > div'); + const childElements = await parentElement.$$('pierce/div'); + const text = await Promise.all( + childElements.map((div) => + div.evaluate((element: Element) => element.textContent) + ) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + }); + + // The tests for $$eval are repeated later in this file in the test group 'QueryAll'. + // This is done to also test a query handler where QueryAll returns an Element[] + // as opposed to NodeListOf. + describe('Page.$$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCount = await page.$$eval('div', (divs) => divs.length); + expect(divsCount).toBe(3); + }); + it('should accept extra arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCountPlus5 = await page.$$eval( + 'div', + (divs, two: number, three: number) => divs.length + two + three, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '
2
2
1
3
' + ); + const divHandle = await page.$('div'); + const sum = await page.$$eval( + 'section', + (sections, div: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('should handle many elements', async () => { + const { page } = getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('section', (sections) => + sections.reduce((acc, section) => acc + Number(section.textContent), 0) + ); + expect(sum).toBe(500500); + }); + }); + + describe('Page.$', function () { + it('should query existing element', async () => { + const { page } = getTestState(); + + await page.setContent('
test
'); + const element = await page.$('section'); + expect(element).toBeTruthy(); + }); + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + const element = await page.$('non-existing-element'); + expect(element).toBe(null); + }); + }); + + describe('Page.$$', function () { + it('should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent('
A

B
'); + const elements = await page.$$('div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + it('should return empty array if nothing is found', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const elements = await page.$$('div'); + expect(elements.length).toBe(0); + }); + }); + + describe('Path.$x', function () { + it('should query existing element', async () => { + const { page } = getTestState(); + + await page.setContent('
test
'); + const elements = await page.$x('/html/body/section'); + expect(elements[0]).toBeTruthy(); + expect(elements.length).toBe(1); + }); + it('should return empty array for non-existing element', async () => { + const { page } = getTestState(); + + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async () => { + const { page } = getTestState(); + + await page.setContent('
'); + const elements = await page.$x('/html/body/div'); + expect(elements.length).toBe(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '
A
' + ); + const html = await page.$('html'); + const second = await html.$('.second'); + const inner = await second.$('.inner'); + const content = await page.evaluate( + (e: HTMLElement) => e.textContent, + inner + ); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + await page.setContent( + '
B
' + ); + const html = await page.$('html'); + const second = await html.$('.third'); + expect(second).toBe(null); + }); + }); + describe('ElementHandle.$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '
10
' + ); + const tweet = await page.$('.tweet'); + const content = await tweet.$eval( + '.like', + (node: HTMLElement) => node.innerText + ); + expect(content).toBe('100'); + }); + + it('should retrieve content from subtree', async () => { + const { page } = getTestState(); + + const htmlContent = + '
not-a-child-div
a-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const content = await elementHandle.$eval( + '.a', + (node: HTMLElement) => node.innerText + ); + expect(content).toBe('a-child-div'); + }); + + it('should throw in case of missing selector', async () => { + const { page } = getTestState(); + + const htmlContent = + '
not-a-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const errorMessage = await elementHandle + .$eval('.a', (node: HTMLElement) => node.innerText) + .catch((error) => error.message); + expect(errorMessage).toBe( + `Error: failed to find element matching selector ".a"` + ); + }); + }); + describe('ElementHandle.$$eval', function () { + it('should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '
' + ); + const tweet = await page.$('.tweet'); + const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) => + nodes.map((n) => n.innerText) + ); + expect(content).toEqual(['100', '10']); + }); + + it('should retrieve content from subtree', async () => { + const { page } = getTestState(); + + const htmlContent = + '
not-a-child-div
a1-child-div
a2-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) => + nodes.map((n) => n.innerText) + ); + expect(content).toEqual(['a1-child-div', 'a2-child-div']); + }); + + it('should not throw in case of missing selector', async () => { + const { page } = getTestState(); + + const htmlContent = + '
not-a-child-div
'; + await page.setContent(htmlContent); + const elementHandle = await page.$('#myId'); + const nodesLength = await elementHandle.$$eval( + '.a', + (nodes) => nodes.length + ); + expect(nodesLength).toBe(0); + }); + }); + + describe('ElementHandle.$$', function () { + it('should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '
A

B
' + ); + const html = await page.$('html'); + const elements = await html.$$('div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('should return empty array for non-existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + 'A
B' + ); + const html = await page.$('html'); + const elements = await html.$$('div'); + expect(elements.length).toBe(0); + }); + }); + + describe('ElementHandle.$x', function () { + it('should query existing element', async () => { + const { page, server } = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '
A
' + ); + const html = await page.$('html'); + const second = await html.$x(`./body/div[contains(@class, 'second')]`); + const inner = await second[0].$x(`./div[contains(@class, 'inner')]`); + const content = await page.evaluate( + (e: HTMLElement) => e.textContent, + inner[0] + ); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const { page } = getTestState(); + + await page.setContent( + '
B
' + ); + const html = await page.$('html'); + const second = await html.$x(`/div[contains(@class, 'third')]`); + expect(second).toEqual([]); + }); + }); + + // This is the same tests for `$$eval` and `$$` as above, but with a queryAll + // handler that returns an array instead of a list of nodes. + describe('QueryAll', function () { + const handler: CustomQueryHandler = { + queryAll: (element: Element, selector: string) => + Array.from(element.querySelectorAll(selector)), + }; + before(() => { + const { puppeteer } = getTestState(); + puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + const { puppeteer } = getTestState(); + expect( + puppeteer.customQueryHandlerNames().includes('allArray') + ).toBeTruthy(); + }); + it('$$ should query existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + '
A

B
' + ); + const html = await page.$('html'); + const elements = await html.$$('allArray/div'); + expect(elements.length).toBe(2); + const promises = elements.map((element) => + page.evaluate((e: HTMLElement) => e.textContent, element) + ); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('$$ should return empty array for non-existing elements', async () => { + const { page } = getTestState(); + + await page.setContent( + 'A
B' + ); + const html = await page.$('html'); + const elements = await html.$$('allArray/div'); + expect(elements.length).toBe(0); + }); + it('$$eval should work', async () => { + const { page } = getTestState(); + + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCount = await page.$$eval( + 'allArray/div', + (divs) => divs.length + ); + expect(divsCount).toBe(3); + }); + it('$$eval should accept extra arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '
hello
beautiful
world!
' + ); + const divsCountPlus5 = await page.$$eval( + 'allArray/div', + (divs, two: number, three: number) => divs.length + two + three, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('$$eval should accept ElementHandles as arguments', async () => { + const { page } = getTestState(); + await page.setContent( + '
2
2
1
3
' + ); + const divHandle = await page.$('div'); + const sum = await page.$$eval( + 'allArray/section', + (sections, div: HTMLElement) => + sections.reduce( + (acc, section) => acc + Number(section.textContent), + 0 + ) + Number(div.textContent), + divHandle + ); + expect(sum).toBe(8); + }); + it('$$eval should handle many elements', async () => { + const { page } = getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('allArray/section', (sections) => + sections.reduce((acc, section) => acc + Number(section.textContent), 0) + ); + expect(sum).toBe(500500); + }); + }); +}); diff --git a/test/requestinterception-experimental.spec.ts b/test/requestinterception-experimental.spec.ts new file mode 100644 index 0000000000000..70f87b3604b57 --- /dev/null +++ b/test/requestinterception-experimental.spec.ts @@ -0,0 +1,873 @@ +/** + * Copyright 2021 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 fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { ActionResult } from '../lib/cjs/puppeteer/api-docs-entry.js'; +import { InterceptResolutionAction } from '../lib/cjs/puppeteer/common/HTTPRequest.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describeFailsFirefox('Page.setRequestInterception', function () { + const expectedActions: ActionResult[] = ['abort', 'continue', 'respond']; + while (expectedActions.length > 0) { + const expectedAction = expectedActions.pop(); + it(`should cooperatively ${expectedAction} by priority`, async () => { + const { page, server } = getTestState(); + + const actionResults: ActionResult[] = []; + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('.css')) + request.continue( + { headers: { ...request.headers(), xaction: 'continue' } }, + expectedAction === 'continue' ? 1 : 0 + ); + else request.continue({}, 0); + }); + page.on('request', (request) => { + if (request.url().endsWith('.css')) + request.respond( + { headers: { xaction: 'respond' } }, + expectedAction === 'respond' ? 1 : 0 + ); + else request.continue({}, 0); + }); + page.on('request', (request) => { + if (request.url().endsWith('.css')) + request.abort('aborted', expectedAction === 'abort' ? 1 : 0); + else request.continue({}, 0); + }); + page.on('response', (response) => { + const { xaction } = response.headers(); + if (response.url().endsWith('.css') && !!xaction) + actionResults.push(xaction as ActionResult); + }); + page.on('requestfailed', (request) => { + if (request.url().endsWith('.css')) actionResults.push('abort'); + }); + + const response = await (async () => { + if (expectedAction === 'continue') { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/one-style.css'), + page.goto(server.PREFIX + '/one-style.html'), + ]); + actionResults.push(serverRequest.headers.xaction as ActionResult); + return response; + } else { + return await page.goto(server.PREFIX + '/one-style.html'); + } + })(); + + expect(actionResults.length).toBe(1); + expect(actionResults[0]).toBe(expectedAction); + expect(response.ok()).toBe(true); + }); + } + + it('should intercept', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue({}, 0); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame().url()).toBe('about:blank'); + request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + await page.setContent(` +
+ +
+ `); + await Promise.all([ + page.$eval('form', (form: HTMLFormElement) => form.submit()), + page.waitForNavigation(), + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3973 + it('should work when header manipulation headers with redirect', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + request.continue({ headers }, 0); + + expect(request.continueRequestOverrides()).toEqual({ headers }); + }); + // Make sure that the goto does not time out. + await page.goto(server.PREFIX + '/rrredirect'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4743 + it('should be able to remove headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + request.continue({ headers }, 0); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/empty.html'), + ]); + + expect(serverRequest.headers.origin).toBe(undefined); + }); + it('should contain referer header', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + if (!utils.isFavicon(request)) requests.push(request); + request.continue({}, 0); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1].url()).toContain('/one-style.css'); + expect(requests[1].headers().referer).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const { page, server } = getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ name: 'foo', value: 'bar' }); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should stop intercepting', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.once('request', (request) => request.continue({}, 0)); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(false); + await page.goto(server.EMPTY_PAGE); + }); + it('should show custom HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['foo']).toBe('bar'); + request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4337 + it('should work with redirect inside sync XHR', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRedirect('/logo.png', '/pptr.png'); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + const status = await page.evaluate(async () => { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }); + expect(status).toBe(200); + }); + it('should work with custom referer headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it('should be abortable', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('.css')) request.abort('failed', 0); + else request.continue({}, 0); + }); + let failedRequests = 0; + page.on('requestfailed', () => ++failedRequests); + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.ok()).toBe(true); + expect(response.request().failure()).toBe(null); + expect(failedRequests).toBe(1); + }); + it('should be able to access the error reason', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.abort('failed', 0); + }); + let abortReason = null; + page.on('request', (request) => { + abortReason = request.abortErrorReason(); + request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(abortReason).toBe('Failed'); + }); + it('should be abortable with custom error codes', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.abort('internetdisconnected', 0); + }); + let failedRequest = null; + page.on('requestfailed', (request) => (failedRequest = request)); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure().errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + const [request] = await Promise.all([ + server.waitForRequest('/grid.html'), + page.goto(server.PREFIX + '/grid.html'), + ]); + expect(request.headers['referer']).toBe('http://google.com/'); + }); + it('should fail navigation when aborting main resource', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.abort('failed', 0)); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + if (isChrome) expect(error.message).toContain('net::ERR_FAILED'); + else expect(error.message).toContain('NS_ERROR_FAILURE'); + }); + it('should work with redirects', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue({}, 0); + requests.push(request); + }); + server.setRedirect( + '/non-existing-page.html', + '/non-existing-page-2.html' + ); + server.setRedirect( + '/non-existing-page-2.html', + '/non-existing-page-3.html' + ); + server.setRedirect( + '/non-existing-page-3.html', + '/non-existing-page-4.html' + ); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); + const response = await page.goto( + server.PREFIX + '/non-existing-page.html' + ); + expect(response.status()).toBe(200); + expect(response.url()).toContain('empty.html'); + expect(requests.length).toBe(5); + expect(requests[2].resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0].url()).toContain('/non-existing-page.html'); + expect(redirectChain[2].url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue({}, 0); + if (!utils.isFavicon(request)) requests.push(request); + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (req, res) => + res.end('body {box-sizing: border-box; }') + ); + + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('one-style.html'); + expect(requests.length).toBe(5); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[1].resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1].redirectChain(); + expect(redirectChain.length).toBe(3); + expect(redirectChain[0].url()).toContain('/one-style.css'); + expect(redirectChain[2].url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', (request) => { + if (request.url().includes('non-existing-2')) + request.abort('failed', 0); + else request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + await fetch('/non-existing.json'); + } catch (error) { + return error.message; + } + }); + if (isChrome) expect(result).toContain('Failed to fetch'); + else expect(result).toContain('NetworkError'); + }); + it('should work with equal requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + '')); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue({}, 0); + return; + } + spinner ? request.abort('failed', 0) : request.continue({}, 0); + spinner = !spinner; + }); + const results = await page.evaluate(() => + Promise.all([ + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + ]) + ); + expect(results).toEqual(['11', 'FAILED', '22']); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,
yo
'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + !utils.isFavicon(request) && requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,
yo
'; + const text = await page.evaluate( + (url: string) => fetch(url).then((r) => r.text()), + dataURL + ); + expect(text).toBe('
yo
'); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + const response = await page.goto( + server.PREFIX + '/some nonexisting page' + ); + expect(response.status()).toBe(404); + }); + it('should work with badly encoded server', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + server.setRoute('/malformed?rnd=%911', (req, res) => res.end()); + page.on('request', (request) => request.continue({}, 0)); + const response = await page.goto(server.PREFIX + '/malformed?rnd=%911'); + expect(response.status()).toBe(200); + }); + it('should work with encoded server - 2', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue({}, 0); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,` + ); + expect(response.status()).toBe(200); + expect(requests.length).toBe(2); + expect(requests[1].response().status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const { page, server } = getTestState(); + + await page.setContent(''); + await page.setRequestInterception(true); + let request = null; + page.on('request', async (r) => (request = r)); + page.$eval( + 'iframe', + (frame: HTMLIFrameElement, url: string) => (frame.src = url), + server.EMPTY_PAGE + ), + // Wait for request interception. + await utils.waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', (frame) => frame.remove()); + let error = null; + await request.continue({}, 0).catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + it('should throw if interception is not enabled', async () => { + const { page, server } = getTestState(); + + let error = null; + page.on('request', async (request) => { + try { + await request.continue({}, 0); + } catch (error_) { + error = error_; + } + }); + await page.goto(server.EMPTY_PAGE); + expect(error.message).toContain('Request Interception is not enabled'); + }); + it('should work with file URLs', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', (request) => { + urls.add(request.url().split('/').pop()); + request.continue({}, 0); + }); + await page.goto( + pathToFileURL(path.join(__dirname, 'assets', 'one-style.html')) + ); + expect(urls.size).toBe(2); + expect(urls.has('one-style.html')).toBe(true); + expect(urls.has('one-style.css')).toBe(true); + }); + it('should not cache if cache disabled', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(false); + page.on('request', (request) => request.continue({}, 0)); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache enabled', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', (request) => request.continue({}, 0)); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(1); + }); + it('should load fonts if cache enabled', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', (request) => request.continue({}, 0)); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse((r) => r.url().endsWith('/one-style.woff')); + }); + }); + + describeFailsFirefox('Request.continue', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue({}, 0)); + await page.goto(server.EMPTY_PAGE); + }); + it('should amend HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + request.continue({ headers }, 0); + }); + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should redirect in a way non-observable to page', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + request.continue({ url: redirectURL }, 0); + }); + let consoleMessage = null; + page.on('console', (msg) => (consoleMessage = msg)); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST' }, 0); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.method).toBe('POST'); + }); + it('should amend post data', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ postData: 'doggo' }, 0); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => + fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }) + ), + ]); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should amend both post data and method on navigation', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST', postData: 'doggo' }, 0); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(serverRequest.method).toBe('POST'); + expect(await serverRequest.postBody).toBe('doggo'); + }); + }); + + describeFailsFirefox('Request.respond', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond( + { + status: 201, + headers: { + foo: 'bar', + }, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(201); + expect(response.headers().foo).toBe('bar'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should be able to access the response', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond( + { + status: 200, + body: 'Yo, page!', + }, + 0 + ); + }); + let response = null; + page.on('request', (request) => { + response = request.responseForRequest(); + request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE); + expect(response).toEqual({ status: 200, body: 'Yo, page!' }); + }); + it('should work with status code 422', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond( + { + status: 422, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(422); + expect(response.statusText()).toBe('Unprocessable Entity'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should redirect', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (!request.url().includes('rrredirect')) { + request.continue({}, 0); + return; + } + request.respond( + { + status: 302, + headers: { + location: server.EMPTY_PAGE, + }, + }, + 0 + ); + }); + const response = await page.goto(server.PREFIX + '/rrredirect'); + expect(response.request().redirectChain().length).toBe(1); + expect(response.request().redirectChain()[0].url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking binary responses', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + request.respond( + { + contentType: 'image/png', + body: imageBuffer, + }, + 0 + ); + }); + await page.evaluate((PREFIX) => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise((fulfill) => (img.onload = fulfill)); + }, server.PREFIX); + const img = await page.$('img'); + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond( + { + status: 200, + headers: { + foo: true, + }, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + const headers = response.headers(); + expect(headers.foo).toBe('true'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should indicate already-handled if an intercept has been handled', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue(); + }); + page.on('request', (request) => { + expect(request.isInterceptResolutionHandled()).toBeTruthy(); + }); + page.on('request', (request) => { + const { action } = request.interceptResolutionState(); + expect(action).toBe(InterceptResolutionAction.AlreadyHandled); + }); + await page.goto(server.EMPTY_PAGE); + }); + }); +}); + +function pathToFileURL(path: string): string { + let pathName = path.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash. + if (!pathName.startsWith('/')) pathName = '/' + pathName; + return 'file://' + pathName; +} diff --git a/test/requestinterception.spec.ts b/test/requestinterception.spec.ts new file mode 100644 index 0000000000000..0602e2f915c94 --- /dev/null +++ b/test/requestinterception.spec.ts @@ -0,0 +1,818 @@ +/** + * Copyright 2018 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 fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describeFailsFirefox('Page.setRequestInterception', function () { + it('should intercept', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.headers()['accept']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame().url()).toBe('about:blank'); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.setContent(` +
+ +
+ `); + await Promise.all([ + page.$eval('form', (form: HTMLFormElement) => form.submit()), + page.waitForNavigation(), + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3973 + it('should work when header manipulation headers with redirect', async () => { + const { page, server } = getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + request.continue({ headers }); + }); + await page.goto(server.PREFIX + '/rrredirect'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4743 + it('should be able to remove headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + request.continue({ headers }); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/empty.html'), + ]); + + expect(serverRequest.headers.origin).toBe(undefined); + }); + it('should contain referer header', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + if (!utils.isFavicon(request)) requests.push(request); + request.continue(); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1].url()).toContain('/one-style.css'); + expect(requests[1].headers().referer).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const { page, server } = getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ name: 'foo', value: 'bar' }); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.reload(); + expect(response.status()).toBe(200); + }); + it('should stop intercepting', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.once('request', (request) => request.continue()); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(false); + await page.goto(server.EMPTY_PAGE); + }); + it('should show custom HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['foo']).toBe('bar'); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4337 + it('should work with redirect inside sync XHR', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRedirect('/logo.png', '/pptr.png'); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const status = await page.evaluate(async () => { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }); + expect(status).toBe(200); + }); + it('should work with custom referer headers', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE }); + await page.setRequestInterception(true); + page.on('request', (request) => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); + }); + it('should be abortable', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (request.url().endsWith('.css')) request.abort(); + else request.continue(); + }); + let failedRequests = 0; + page.on('requestfailed', () => ++failedRequests); + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.ok()).toBe(true); + expect(response.request().failure()).toBe(null); + expect(failedRequests).toBe(1); + }); + it('should be abortable with custom error codes', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.abort('internetdisconnected'); + }); + let failedRequest = null; + page.on('requestfailed', (request) => (failedRequest = request)); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure().errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const { page, server } = getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const [request] = await Promise.all([ + server.waitForRequest('/grid.html'), + page.goto(server.PREFIX + '/grid.html'), + ]); + expect(request.headers['referer']).toBe('http://google.com/'); + }); + it('should fail navigation when aborting main resource', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.abort()); + let error = null; + await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + if (isChrome) expect(error.message).toContain('net::ERR_FAILED'); + else expect(error.message).toContain('NS_ERROR_FAILURE'); + }); + it('should work with redirects', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + requests.push(request); + }); + server.setRedirect( + '/non-existing-page.html', + '/non-existing-page-2.html' + ); + server.setRedirect( + '/non-existing-page-2.html', + '/non-existing-page-3.html' + ); + server.setRedirect( + '/non-existing-page-3.html', + '/non-existing-page-4.html' + ); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); + const response = await page.goto( + server.PREFIX + '/non-existing-page.html' + ); + expect(response.status()).toBe(200); + expect(response.url()).toContain('empty.html'); + expect(requests.length).toBe(5); + expect(requests[2].resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0].url()).toContain('/non-existing-page.html'); + expect(redirectChain[2].url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + if (!utils.isFavicon(request)) requests.push(request); + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (req, res) => + res.end('body {box-sizing: border-box; }') + ); + + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response.status()).toBe(200); + expect(response.url()).toContain('one-style.html'); + expect(requests.length).toBe(5); + expect(requests[0].resourceType()).toBe('document'); + expect(requests[1].resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1].redirectChain(); + expect(redirectChain.length).toBe(3); + expect(redirectChain[0].url()).toContain('/one-style.css'); + expect(redirectChain[2].url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const { page, server, isChrome } = getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', (request) => { + if (request.url().includes('non-existing-2')) request.abort(); + else request.continue(); + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + await fetch('/non-existing.json'); + } catch (error) { + return error.message; + } + }); + if (isChrome) expect(result).toContain('Failed to fetch'); + else expect(result).toContain('NetworkError'); + }); + it('should work with equal requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + '')); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', (request) => { + if (utils.isFavicon(request)) { + request.continue(); + return; + } + spinner ? request.abort() : request.continue(); + spinner = !spinner; + }); + const results = await page.evaluate(() => + Promise.all([ + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + fetch('/zzz') + .then((response) => response.text()) + .catch(() => 'FAILED'), + ]) + ); + expect(results).toEqual(['11', 'FAILED', '22']); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,
yo
'; + const response = await page.goto(dataURL); + expect(response.status()).toBe(200); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + !utils.isFavicon(request) && requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,
yo
'; + const text = await page.evaluate( + (url: string) => fetch(url).then((r) => r.text()), + dataURL + ); + expect(text).toBe('
yo
'); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + requests.push(request); + request.continue(); + }); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0].url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + const response = await page.goto( + server.PREFIX + '/some nonexisting page' + ); + expect(response.status()).toBe(404); + }); + it('should work with badly encoded server', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + server.setRoute('/malformed?rnd=%911', (req, res) => res.end()); + page.on('request', (request) => request.continue()); + const response = await page.goto(server.PREFIX + '/malformed?rnd=%911'); + expect(response.status()).toBe(200); + }); + it('should work with encoded server - 2', async () => { + const { page, server } = getTestState(); + + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await page.setRequestInterception(true); + const requests = []; + page.on('request', (request) => { + request.continue(); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,` + ); + expect(response.status()).toBe(200); + expect(requests.length).toBe(2); + expect(requests[1].response().status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const { page, server } = getTestState(); + + await page.setContent(''); + await page.setRequestInterception(true); + let request = null; + page.on('request', async (r) => (request = r)); + page.$eval( + 'iframe', + (frame: HTMLIFrameElement, url: string) => (frame.src = url), + server.EMPTY_PAGE + ), + // Wait for request interception. + await utils.waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', (frame) => frame.remove()); + let error = null; + await request.continue().catch((error_) => (error = error_)); + expect(error).toBe(null); + }); + it('should throw if interception is not enabled', async () => { + const { page, server } = getTestState(); + + let error = null; + page.on('request', async (request) => { + try { + await request.continue(); + } catch (error_) { + error = error_; + } + }); + await page.goto(server.EMPTY_PAGE); + expect(error.message).toContain('Request Interception is not enabled'); + }); + it('should work with file URLs', async () => { + const { page } = getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', (request) => { + urls.add(request.url().split('/').pop()); + request.continue(); + }); + await page.goto( + pathToFileURL(path.join(__dirname, 'assets', 'one-style.html')) + ); + expect(urls.size).toBe(2); + expect(urls.has('one-style.html')).toBe(true); + expect(urls.has('one-style.css')).toBe(true); + }); + it('should not cache if cache disabled', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(false); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache enabled', async () => { + const { page, server } = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', (request) => request.continue()); + + const cached = []; + page.on('requestservedfromcache', (r) => cached.push(r)); + + await page.reload(); + expect(cached.length).toBe(1); + }); + it('should load fonts if cache enabled', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', (request) => request.continue()); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse((r) => r.url().endsWith('/one-style.woff')); + }); + }); + + describeFailsFirefox('Request.continue', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => request.continue()); + await page.goto(server.EMPTY_PAGE); + }); + it('should amend HTTP headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + request.continue({ headers }); + }); + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should redirect in a way non-observable to page', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + request.continue({ url: redirectURL }); + }); + let consoleMessage = null; + page.on('console', (msg) => (consoleMessage = msg)); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST' }); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => fetch('/sleep.zzz')), + ]); + expect(request.method).toBe('POST'); + }); + it('should amend post data', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ postData: 'doggo' }); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => + fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }) + ), + ]); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should amend both post data and method on navigation', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.continue({ method: 'POST', postData: 'doggo' }); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(serverRequest.method).toBe('POST'); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should fail if the header value is invalid', async () => { + const { page, server } = getTestState(); + + let error; + await page.setRequestInterception(true); + page.on('request', async (request) => { + await request + .continue({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch((error_) => { + error = error_; + }); + await request.continue(); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); + + describeFailsFirefox('Request.respond', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 201, + headers: { + foo: 'bar', + }, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(201); + expect(response.headers().foo).toBe('bar'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should work with status code 422', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 422, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(422); + expect(response.statusText()).toBe('Unprocessable Entity'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should redirect', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + if (!request.url().includes('rrredirect')) { + request.continue(); + return; + } + request.respond({ + status: 302, + headers: { + location: server.EMPTY_PAGE, + }, + }); + }); + const response = await page.goto(server.PREFIX + '/rrredirect'); + expect(response.request().redirectChain().length).toBe(1); + expect(response.request().redirectChain()[0].url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking multiple headers with same key', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 200, + headers: { + foo: 'bar', + arr: ['1', '2'], + 'set-cookie': ['first=1', 'second=2'], + }, + body: 'Hello world', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + const firstCookie = cookies.find((cookie) => cookie.name === 'first'); + const secondCookie = cookies.find((cookie) => cookie.name === 'second'); + expect(response.status()).toBe(200); + expect(response.headers().foo).toBe('bar'); + expect(response.headers().arr).toBe('1\n2'); + // request.respond() will not trigger Network.responseReceivedExtraInfo + // fail to get 'set-cookie' header from response + expect(firstCookie?.value).toBe('1'); + expect(secondCookie?.value).toBe('2'); + }); + it('should allow mocking binary responses', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, 'assets', 'pptr.png') + ); + request.respond({ + contentType: 'image/png', + body: imageBuffer, + }); + }); + await page.evaluate((PREFIX) => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise((fulfill) => (img.onload = fulfill)); + }, server.PREFIX); + const img = await page.$('img'); + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const { page, server } = getTestState(); + + await page.setRequestInterception(true); + page.on('request', (request) => { + request.respond({ + status: 200, + headers: { + foo: true, + }, + body: 'Yo, page!', + }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + const headers = response.headers(); + expect(headers.foo).toBe('true'); + expect(await page.evaluate(() => document.body.textContent)).toBe( + 'Yo, page!' + ); + }); + it('should fail if the header value is invalid', async () => { + const { page, server } = getTestState(); + + let error; + await page.setRequestInterception(true); + page.on('request', async (request) => { + await request + .respond({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch((error_) => { + error = error_; + }); + await request.respond({ + status: 200, + body: 'Hello World', + }); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); +}); + +/** + * @param {string} path + * @returns {string} + */ +function pathToFileURL(path) { + let pathName = path.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash. + if (!pathName.startsWith('/')) pathName = '/' + pathName; + return 'file://' + pathName; +} diff --git a/test/run_static_server.js b/test/run_static_server.js new file mode 100755 index 0000000000000..0cd10f2646624 --- /dev/null +++ b/test/run_static_server.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); +const { TestServer } = require('../utils/testserver/index.js'); + +const port = 8907; +const httpsPort = 8908; +const assetsPath = path.join(__dirname, 'assets'); +const cachedPath = path.join(__dirname, 'assets', 'cached'); + +Promise.all([ + TestServer.create(assetsPath, port), + TestServer.createHTTPS(assetsPath, httpsPort), +]).then(([server, httpsServer]) => { + server.enableHTTPCache(cachedPath); + httpsServer.enableHTTPCache(cachedPath); + console.log(`HTTP: server is running on http://localhost:${port}`); + console.log(`HTTPS: server is running on https://localhost:${httpsPort}`); +}); diff --git a/test/screenshot.spec.ts b/test/screenshot.spec.ts new file mode 100644 index 0000000000000..f2a6adc6d5fe1 --- /dev/null +++ b/test/screenshot.spec.ts @@ -0,0 +1,340 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('Screenshots', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.screenshot', function () { + itFailsFirefox('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + }); + itFailsFirefox('should clip rect', 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, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-rect.png'); + }); + itFailsFirefox( + 'should get screenshot bigger than the viewport', + async () => { + const { page, server } = getTestState(); + await page.setViewport({ width: 50, height: 50 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 25, + y: 25, + width: 100, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-offscreen-clip.png'); + } + ); + it('should run in parallel', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const promises = []; + for (let i = 0; i < 3; ++i) { + promises.push( + page.screenshot({ + clip: { + x: 50 * i, + y: 0, + width: 50, + height: 50, + }, + }) + ); + } + const screenshots = await Promise.all(promises); + expect(screenshots[1]).toBeGolden('grid-cell-1.png'); + }); + itFailsFirefox('should take fullPage screenshots', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeGolden('screenshot-grid-fullpage.png'); + }); + it('should run in parallel in multiple pages', async () => { + const { server, context } = getTestState(); + + const N = 2; + const pages = await Promise.all( + Array(N) + .fill(0) + .map(async () => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + return page; + }) + ); + const promises = []; + for (let i = 0; i < N; ++i) + promises.push( + pages[i].screenshot({ + clip: { x: 50 * i, y: 0, width: 50, height: 50 }, + }) + ); + const screenshots = await Promise.all(promises); + for (let i = 0; i < N; ++i) + expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); + await Promise.all(pages.map((page) => page.close())); + }); + itFailsFirefox('should allow transparency', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 100, height: 100 }); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ omitBackground: true }); + expect(screenshot).toBeGolden('transparent.png'); + }); + itFailsFirefox('should render white background on jpeg file', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 100, height: 100 }); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ + omitBackground: true, + type: 'jpeg', + }); + expect(screenshot).toBeGolden('white.jpg'); + }); + itFailsFirefox('should work with webp', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 100, height: 100 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + type: 'webp', + }); + + expect(screenshot).toBeInstanceOf(Buffer); + }); + it('should work with odd clip size on Retina displays', async () => { + const { page } = getTestState(); + + const screenshot = await page.screenshot({ + clip: { + x: 0, + y: 0, + width: 11, + height: 11, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-odd-size.png'); + }); + itFailsFirefox('should return base64', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + encoding: 'base64', + }); + // TODO (@jackfranklin): improve the screenshot types. + // - if we pass encoding: 'base64', it returns a string + // - else it returns a buffer. + // If we can fix that we can avoid this "as string" here. + expect(Buffer.from(screenshot as string, 'base64')).toBeGolden( + 'screenshot-sanity.png' + ); + }); + }); + + describe('ElementHandle.screenshot', function () { + it('should work', async () => { + const { page, server } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => window.scrollBy(50, 100)); + const elementHandle = await page.$('.box:nth-of-type(3)'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-bounding-box.png'); + }); + it('should take into account padding and border', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(` + something above + +
+ `); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); + }); + itFailsFirefox( + 'should capture full element when larger than viewport', + async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + + await page.setContent(` + something above + +
+ `); + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-larger-than-viewport.png' + ); + + expect( + await page.evaluate(() => ({ + w: window.innerWidth, + h: window.innerHeight, + })) + ).toEqual({ w: 500, h: 500 }); + } + ); + it('should scroll element into view', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(` + something above + +
+
+ `); + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-scrolled-into-view.png' + ); + }); + itFailsFirefox('should work with a rotated element', async () => { + const { page } = getTestState(); + + await page.setViewport({ width: 500, height: 500 }); + await page.setContent(`
 
`); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-rotate.png'); + }); + itFailsFirefox('should fail to screenshot a detached element', async () => { + const { page } = getTestState(); + + await page.setContent('

remove this

'); + const elementHandle = await page.$('h1'); + await page.evaluate( + (element: HTMLElement) => element.remove(), + elementHandle + ); + const screenshotError = await elementHandle + .screenshot() + .catch((error) => error); + expect(screenshotError.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + it('should not hang with zero width/height element', async () => { + const { page } = getTestState(); + + await page.setContent('
'); + const div = await page.$('div'); + const error = await div.screenshot().catch((error_) => error_); + expect(error.message).toBe('Node has 0 height.'); + }); + it('should work for an element with fractional dimensions', async () => { + const { page } = getTestState(); + + await page.setContent( + '
' + ); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional.png'); + }); + itFailsFirefox('should work for an element with an offset', async () => { + const { page } = getTestState(); + + await page.setContent( + '
' + ); + const elementHandle = await page.$('div'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); + }); + }); +}); diff --git a/test/target.spec.ts b/test/target.spec.ts new file mode 100644 index 0000000000000..d1081d65f838b --- /dev/null +++ b/test/target.spec.ts @@ -0,0 +1,318 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +const { waitEvent } = utils; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import { Target } from '../lib/cjs/puppeteer/common/Target.js'; + +describe('Target', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('Browser.targets should return all of the targets', async () => { + const { browser } = getTestState(); + + // The pages will be the testing page and the original newtab page + const targets = browser.targets(); + expect( + targets.some( + (target) => target.type() === 'page' && target.url() === 'about:blank' + ) + ).toBeTruthy(); + expect(targets.some((target) => target.type() === 'browser')).toBeTruthy(); + }); + it('Browser.pages should return all of the pages', async () => { + const { page, context } = getTestState(); + + // The pages will be the testing page + const allPages = await context.pages(); + expect(allPages.length).toBe(1); + expect(allPages).toContain(page); + }); + it('should contain browser target', async () => { + const { browser } = getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find((target) => target.type() === 'browser'); + expect(browserTarget).toBeTruthy(); + }); + it('should be able to use the default page in the browser', async () => { + const { page, browser } = getTestState(); + + // The pages will be the testing page and the original newtab page + const allPages = await browser.pages(); + const originalPage = allPages.find((p) => p !== page); + expect( + await originalPage.evaluate(() => ['Hello', 'world'].join(' ')) + ).toBe('Hello world'); + expect(await originalPage.$('body')).toBeTruthy(); + }); + itFailsFirefox('should be able to use async waitForTarget', async () => { + const { page, server, context } = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget((target) => + target + .page() + .then( + (page) => + page.url() === server.CROSS_PROCESS_PREFIX + '/empty.html' + ) + ) + .then((target) => target.page()), + page.evaluate( + (url: string) => window.open(url), + server.CROSS_PROCESS_PREFIX + '/empty.html' + ), + ]); + expect(otherPage.url()).toEqual( + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(page).not.toEqual(otherPage); + }); + itFailsFirefox( + 'should report when a new page is created and closed', + async () => { + const { page, server, context } = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget( + (target) => + target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html' + ) + .then((target) => target.page()), + page.evaluate( + (url: string) => window.open(url), + server.CROSS_PROCESS_PREFIX + '/empty.html' + ), + ]); + expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX); + expect(await otherPage.evaluate(() => ['Hello', 'world'].join(' '))).toBe( + 'Hello world' + ); + expect(await otherPage.$('body')).toBeTruthy(); + + let allPages = await context.pages(); + expect(allPages).toContain(page); + expect(allPages).toContain(otherPage); + + const closePagePromise = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target.page())) + ); + await otherPage.close(); + expect(await closePagePromise).toBe(otherPage); + + allPages = await Promise.all( + context.targets().map((target) => target.page()) + ); + expect(allPages).toContain(page); + expect(allPages).not.toContain(otherPage); + } + ); + itFailsFirefox( + 'should report when a service worker is created and destroyed', + async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const createdTarget = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + expect((await createdTarget).type()).toBe('service_worker'); + expect((await createdTarget).url()).toBe( + server.PREFIX + '/serviceworkers/empty/sw.js' + ); + + const destroyedTarget = new Promise((fulfill) => + context.once('targetdestroyed', (target) => fulfill(target)) + ); + await page.evaluate(() => + globalThis.registrationPromise.then((registration) => + registration.unregister() + ) + ); + expect(await destroyedTarget).toBe(await createdTarget); + } + ); + itFailsFirefox('should create a worker from a service worker', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + const target = await context.waitForTarget( + (target) => target.type() === 'service_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object ServiceWorkerGlobalScope]' + ); + }); + itFailsFirefox('should create a worker from a shared worker', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + new SharedWorker('data:text/javascript,console.log("hi")'); + }); + const target = await context.waitForTarget( + (target) => target.type() === 'shared_worker' + ); + const worker = await target.worker(); + expect(await worker.evaluate(() => self.toString())).toBe( + '[object SharedWorkerGlobalScope]' + ); + }); + itFailsFirefox('should report when a target url changes', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let changedTarget = new Promise((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = new Promise((fulfill) => + context.once('targetchanged', (target) => fulfill(target)) + ); + await page.goto(server.EMPTY_PAGE); + expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); + }); + itFailsFirefox('should not report uninitialized pages', async () => { + const { context } = getTestState(); + + let targetChanged = false; + const listener = () => (targetChanged = true); + context.on('targetchanged', listener); + const targetPromise = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ); + const evaluatePromise = newPage.evaluate(() => window.open('about:blank')); + const target2 = await targetPromise2; + expect(target2.url()).toBe('about:blank'); + await evaluatePromise; + await newPage.close(); + expect(targetChanged).toBe(false); + context.removeListener('targetchanged', listener); + }); + itFailsFirefox( + 'should not crash while redirecting if original request was missed', + async () => { + const { page, server, context } = getTestState(); + + let serverResponse = null; + server.setRoute('/one-style.css', (req, res) => (serverResponse = res)); + // Open a new page. Use window.open to connect to the page later. + await Promise.all([ + page.evaluate( + (url: string) => window.open(url), + server.PREFIX + '/one-style.html' + ), + server.waitForRequest('/one-style.css'), + ]); + // Connect to the opened page. + const target = await context.waitForTarget((target) => + target.url().includes('one-style.html') + ); + const newPage = await target.page(); + // Issue a redirect. + serverResponse.writeHead(302, { location: '/injectedstyle.css' }); + serverResponse.end(); + // Wait for the new page to load. + await waitEvent(newPage, 'load'); + // Cleanup. + await newPage.close(); + } + ); + itFailsFirefox('should have an opener', async () => { + const { page, server, context } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + new Promise((fulfill) => + context.once('targetcreated', (target) => fulfill(target)) + ), + page.goto(server.PREFIX + '/popup/window-open.html'), + ]); + expect((await createdTarget.page()).url()).toBe( + server.PREFIX + '/popup/popup.html' + ); + expect(createdTarget.opener()).toBe(page.target()); + expect(page.target().opener()).toBe(null); + }); + + describe('Browser.waitForTarget', () => { + itFailsFirefox('should wait for a target', async () => { + const { browser, puppeteer, server } = getTestState(); + + let resolved = false; + const targetPromise = browser.waitForTarget( + (target) => target.url() === server.EMPTY_PAGE + ); + targetPromise + .then(() => (resolved = true)) + .catch((error) => { + resolved = true; + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + }); + const page = await browser.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else throw error; + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const { browser, server, puppeteer } = getTestState(); + + let error = null; + await browser + .waitForTarget((target) => target.url() === server.EMPTY_PAGE, { + timeout: 1, + }) + .catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + }); +}); diff --git a/test/touchscreen.spec.ts b/test/touchscreen.spec.ts new file mode 100644 index 0000000000000..36931d5852699 --- /dev/null +++ b/test/touchscreen.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2018 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describeFailsFirefox('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect(await page.evaluate(() => globalThis.result)).toBe('Clicked'); + }); + it('should report touches', async () => { + const { puppeteer, page, server } = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches.html'); + const button = await page.$('button'); + await button.tap(); + expect(await page.evaluate(() => globalThis.getResult())).toEqual([ + 'Touchstart: 0', + 'Touchend: 0', + ]); + }); +}); diff --git a/test/tracing.spec.ts b/test/tracing.spec.ts new file mode 100644 index 0000000000000..8425652e97b35 --- /dev/null +++ b/test/tracing.spec.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import expect from 'expect'; +import { getTestState, describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +describeChromeOnly('Tracing', function () { + let outputFile; + let browser; + let page; + + /* we manually manage the browser here as we want a new browser for each + * individual test, which isn't the default behaviour of getTestState() + */ + + beforeEach(async () => { + const { defaultBrowserOptions, puppeteer } = getTestState(); + browser = await puppeteer.launch(defaultBrowserOptions); + page = await browser.newPage(); + outputFile = path.join(__dirname, 'assets', 'trace.json'); + }); + + afterEach(async () => { + await browser.close(); + browser = null; + page = null; + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + outputFile = null; + } + }); + it('should output a trace', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true, path: outputFile }); + await page.goto(server.PREFIX + '/grid.html'); + await page.tracing.stop(); + expect(fs.existsSync(outputFile)).toBe(true); + }); + + it('should run with custom categories if provided', async () => { + await page.tracing.start({ + path: outputFile, + categories: ['-*', 'disabled-by-default-devtools.timeline.frame'], + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, { encoding: 'utf8' }) + ); + const traceConfig = JSON.parse(traceJson.metadata['trace-config']); + expect(traceConfig.included_categories).toEqual([ + 'disabled-by-default-devtools.timeline.frame', + ]); + expect(traceConfig.excluded_categories).toEqual(['*']); + expect(traceJson.traceEvents).not.toContainEqual( + expect.objectContaining({ + cat: 'toplevel', + }) + ); + }); + + it('should run with default categories', async () => { + await page.tracing.start({ + path: outputFile, + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, { encoding: 'utf8' }) + ); + expect(traceJson.traceEvents).toContainEqual( + expect.objectContaining({ + cat: 'toplevel', + }) + ); + }); + it('should throw if tracing on two pages', async () => { + await page.tracing.start({ path: outputFile }); + const newPage = await browser.newPage(); + let error = null; + await newPage.tracing + .start({ path: outputFile }) + .catch((error_) => (error = error_)); + await newPage.close(); + expect(error).toBeTruthy(); + await page.tracing.stop(); + }); + it('should return a buffer', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true, path: outputFile }); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + const buf = fs.readFileSync(outputFile); + expect(trace.toString()).toEqual(buf.toString()); + }); + it('should work without options', async () => { + const { server } = getTestState(); + + await page.tracing.start(); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + expect(trace).toBeTruthy(); + }); + + it('should return null in case of Buffer error', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true }); + await page.goto(server.PREFIX + '/grid.html'); + const oldBufferConcat = Buffer.concat; + Buffer.concat = () => { + throw 'error'; + }; + const trace = await page.tracing.stop(); + expect(trace).toEqual(null); + Buffer.concat = oldBufferConcat; + }); + + it('should support a buffer without a path', async () => { + const { server } = getTestState(); + + await page.tracing.start({ screenshots: true }); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + expect(trace.toString()).toContain('screenshot'); + }); + + it('should properly fail if readProtocolStream errors out', async () => { + await page.tracing.start({ path: __dirname }); + + let error: Error = null; + try { + await page.tracing.stop(); + } catch (error_) { + error = error_; + } + expect(error).toBeDefined(); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000000000..8b1f1e866cbd4 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["*.ts", "*.js"] +} diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json new file mode 100644 index 0000000000000..3432441200fc1 --- /dev/null +++ b/test/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000000000..f70cb47e714c5 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,135 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO (@jackfranklin): convert to TS and enable type checking. + +// @ts-nocheck +const fs = require('fs'); +const path = require('path'); +const expect = require('expect'); +const GoldenUtils = require('./golden-utils.js'); +const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) + ? path.join(__dirname, '..') + : path.join(__dirname, '..', '..'); + +const utils = (module.exports = { + extendExpectWithToBeGolden: function (goldenDir, outputDir) { + expect.extend({ + toBeGolden: (testScreenshot, goldenFilePath) => { + const result = GoldenUtils.compare( + goldenDir, + outputDir, + testScreenshot, + goldenFilePath + ); + + return { + message: () => result.message, + pass: result.pass, + }; + }, + }); + }, + + /** + * @returns {string} + */ + projectRoot: function () { + return PROJECT_ROOT; + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + * @returns {!Frame} + */ + attachFrame: async function (page, frameId, url) { + const handle = await page.evaluateHandle(attachFrame, frameId, url); + return await handle.asElement().contentFrame(); + + async function attachFrame(frameId, url) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise((x) => (frame.onload = x)); + return frame; + } + }, + + isFavicon: function (request) { + return request.url().includes('favicon.ico'); + }, + + /** + * @param {!Page} page + * @param {string} frameId + */ + detachFrame: async function (page, frameId) { + await page.evaluate(detachFrame, frameId); + + function detachFrame(frameId) { + const frame = document.getElementById(frameId); + frame.remove(); + } + }, + + /** + * @param {!Page} page + * @param {string} frameId + * @param {string} url + */ + navigateFrame: async function (page, frameId, url) { + await page.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId, url) { + const frame = document.getElementById(frameId); + frame.src = url; + return new Promise((x) => (frame.onload = x)); + } + }, + + /** + * @param {!Frame} frame + * @param {string=} indentation + * @returns {Array} + */ + dumpFrames: function (frame, indentation) { + indentation = indentation || ''; + let description = frame.url().replace(/:\d{4}\//, ':/'); + if (frame.name()) description += ' (' + frame.name() + ')'; + const result = [indentation + description]; + for (const child of frame.childFrames()) + result.push(...utils.dumpFrames(child, ' ' + indentation)); + return result; + }, + + /** + * @param {!EventEmitter} emitter + * @param {string} eventName + * @returns {!Promise} + */ + waitEvent: function (emitter, eventName, predicate = () => true) { + return new Promise((fulfill) => { + emitter.on(eventName, function listener(event) { + if (!predicate(event)) return; + emitter.removeListener(eventName, listener); + fulfill(event); + }); + }); + }, +}); diff --git a/test/waittask.spec.ts b/test/waittask.spec.ts new file mode 100644 index 0000000000000..7b7f409e8cea1 --- /dev/null +++ b/test/waittask.spec.ts @@ -0,0 +1,789 @@ +/** + * Copyright 2018 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 utils from './utils.js'; +import sinon from 'sinon'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions + +describe('waittask specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.waitFor', function () { + /* This method is deprecated but we don't want the warnings showing up in + * tests. Until we remove this method we still want to ensure we don't break + * it. + */ + beforeEach(() => sinon.stub(console, 'warn').callsFake(() => {})); + + it('should wait for selector', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + + it('should wait for an xpath', async () => { + const { page, server } = getTestState(); + + let found = false; + const waitFor = page.waitFor('//div').then(() => (found = true)); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + it('should allow you to select an element with parenthesis-starting xpath', async () => { + const { page, server } = getTestState(); + let found = false; + const waitFor = page.waitFor('(//img)[200]').then(() => { + found = true; + }); + await page.goto(server.EMPTY_PAGE); + expect(found).toBe(false); + await page.goto(server.PREFIX + '/grid.html'); + await waitFor; + expect(found).toBe(true); + }); + it('should not allow you to select an element with single slash xpath', async () => { + const { page } = getTestState(); + + await page.setContent(`
some text
`); + let error = null; + await page.waitFor('/html/body/div').catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + }); + it('should timeout', async () => { + const { page } = getTestState(); + + const startTime = Date.now(); + const timeout = 42; + await page.waitFor(timeout); + expect(Date.now() - startTime).not.toBeLessThan(timeout / 2); + }); + it('should work with multiline body', async () => { + const { page } = getTestState(); + + const result = await page.waitForFunction(` + (() => true)() + `); + expect(await result.jsonValue()).toBe(true); + }); + it('should wait for predicate', async () => { + const { page } = getTestState(); + + await Promise.all([ + page.waitFor(() => window.innerWidth < 100), + page.setViewport({ width: 10, height: 10 }), + ]); + }); + it('should throw when unknown type', async () => { + const { page } = getTestState(); + + let error = null; + // @ts-expect-error purposefully passing bad type for test + await page.waitFor({ foo: 'bar' }).catch((error_) => (error = error_)); + expect(error.message).toContain('Unsupported target type'); + }); + it('should wait for predicate with arguments', async () => { + const { page } = getTestState(); + + await page.waitFor((arg1, arg2) => arg1 !== arg2, {}, 1, 2); + }); + + it('should log a deprecation warning', async () => { + const { page } = getTestState(); + + await page.waitFor(() => true); + + const consoleWarnStub = console.warn as sinon.SinonSpy; + + expect(consoleWarnStub.calledOnce).toBe(true); + expect( + consoleWarnStub.firstCall.calledWith( + 'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.' + ) + ).toBe(true); + expect((console.warn as sinon.SinonSpy).calledOnce).toBe(true); + }); + }); + + describe('Frame.waitForFunction', function () { + it('should accept a string', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction('window.__FOO === 1'); + await page.evaluate(() => (globalThis.__FOO = 1)); + await watchdog; + }); + it('should work when resolved right before execution context disposal', async () => { + const { page } = getTestState(); + + await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true)); + await page.waitForFunction(() => { + if (!globalThis.__RELOADED) window.location.reload(); + return true; + }); + }); + it('should poll on interval', async () => { + const { page } = getTestState(); + + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on interval async', async () => { + const { page } = getTestState(); + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction(async () => globalThis.__FOO === 'hit', { polling }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on mutation', async () => { + const { page } = getTestState(); + + let success = false; + const watchdog = page + .waitForFunction(() => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(() => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + }); + it('should poll on mutation async', async () => { + const { page } = getTestState(); + + let success = false; + const watchdog = page + .waitForFunction(async () => globalThis.__FOO === 'hit', { + polling: 'mutation', + }) + .then(() => (success = true)); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + expect(success).toBe(false); + await page.evaluate(async () => + document.body.appendChild(document.createElement('div')) + ); + await watchdog; + }); + it('should poll on raf', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__FOO === 'hit', { + polling: 'raf', + }); + await page.evaluate(() => (globalThis.__FOO = 'hit')); + await watchdog; + }); + it('should poll on raf async', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + async () => globalThis.__FOO === 'hit', + { + polling: 'raf', + } + ); + await page.evaluate(async () => (globalThis.__FOO = 'hit')); + await watchdog; + }); + itFailsFirefox('should work with strict CSP policy', async () => { + const { page, server } = getTestState(); + + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.goto(server.EMPTY_PAGE); + let error = null; + await Promise.all([ + page + .waitForFunction(() => globalThis.__FOO === 'hit', { polling: 'raf' }) + .catch((error_) => (error = error_)), + page.evaluate(() => (globalThis.__FOO = 'hit')), + ]); + expect(error).toBe(null); + }); + it('should throw on bad polling value', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { + polling: 'unknown', + }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('polling'); + }); + it('should throw negative polling interval', async () => { + const { page } = getTestState(); + + let error = null; + try { + await page.waitForFunction(() => !!document.body, { polling: -10 }); + } catch (error_) { + error = error_; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('Cannot poll with non-positive interval'); + }); + it('should return the success value as a JSHandle', async () => { + const { page } = getTestState(); + + expect(await (await page.waitForFunction(() => 5)).jsonValue()).toBe(5); + }); + it('should return the window as a success value', async () => { + const { page } = getTestState(); + + expect(await page.waitForFunction(() => window)).toBeTruthy(); + }); + it('should accept ElementHandle arguments', async () => { + const { page } = getTestState(); + + await page.setContent('
'); + const div = await page.$('div'); + let resolved = false; + const waitForFunction = page + .waitForFunction( + (element) => element.localName === 'div' && !element.parentElement, + {}, + div + ) + .then(() => (resolved = true)); + expect(resolved).toBe(false); + await page.evaluate((element: HTMLElement) => element.remove(), div); + await waitForFunction; + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForFunction('false', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain('waiting for function failed: timeout'); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const { page, puppeteer } = getTestState(); + + page.setDefaultTimeout(1); + let error = null; + await page.waitForFunction('false').catch((error_) => (error = error_)); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error.message).toContain('waiting for function failed: timeout'); + }); + it('should disable timeout when its set to 0', async () => { + const { page } = getTestState(); + + const watchdog = page.waitForFunction( + () => { + globalThis.__counter = (globalThis.__counter || 0) + 1; + return globalThis.__injected; + }, + { timeout: 0, polling: 10 } + ); + await page.waitForFunction(() => globalThis.__counter > 10); + await page.evaluate(() => (globalThis.__injected = true)); + await watchdog; + }); + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let fooFound = false; + const waitForFunction = page + .waitForFunction('globalThis.__FOO === 1') + .then(() => (fooFound = true)); + await page.goto(server.EMPTY_PAGE); + expect(fooFound).toBe(false); + await page.reload(); + expect(fooFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + expect(fooFound).toBe(false); + await page.evaluate(() => (globalThis.__FOO = 1)); + await waitForFunction; + expect(fooFound).toBe(true); + }); + it('should survive navigations', async () => { + const { page, server } = getTestState(); + + const watchdog = page.waitForFunction(() => globalThis.__done); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/consolelog.html'); + await page.evaluate(() => (globalThis.__done = true)); + await watchdog; + }); + }); + + describe('Page.waitForTimeout', () => { + it('waits for the given timeout before resolving', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const startTime = Date.now(); + await page.waitForTimeout(1000); + const endTime = Date.now(); + /* In a perfect world endTime - startTime would be exactly 1000 but we + * expect some fluctuations and for it to be off by a little bit. So to + * avoid a flaky test we'll make sure it waited for roughly 1 second. + */ + expect(endTime - startTime).toBeGreaterThan(700); + expect(endTime - startTime).toBeLessThan(1300); + }); + }); + + describe('Frame.waitForTimeout', () => { + it('waits for the given timeout before resolving', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + const startTime = Date.now(); + await frame.waitForTimeout(1000); + const endTime = Date.now(); + /* In a perfect world endTime - startTime would be exactly 1000 but we + * expect some fluctuations and for it to be off by a little bit. So to + * avoid a flaky test we'll make sure it waited for roughly 1 second + */ + expect(endTime - startTime).toBeGreaterThan(700); + expect(endTime - startTime).toBeLessThan(1300); + }); + }); + + describe('Frame.waitForSelector', function () { + const addElement = (tag) => + document.body.appendChild(document.createElement(tag)); + + it('should immediately resolve promise if node exists', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + await frame.waitForSelector('*'); + await frame.evaluate(addElement, 'div'); + await frame.waitForSelector('div'); + }); + + itFailsFirefox('should work with removed MutationObserver', async () => { + const { page } = getTestState(); + + await page.evaluate(() => delete window.MutationObserver); + const [handle] = await Promise.all([ + page.waitForSelector('.zombo'), + page.setContent(`
anything
`), + ]); + expect( + await page.evaluate((x: HTMLElement) => x.textContent, handle) + ).toBe('anything'); + }); + + it('should resolve promise when node is added', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame = page.mainFrame(); + const watchdog = frame.waitForSelector('div'); + await frame.evaluate(addElement, 'br'); + await frame.evaluate(addElement, 'div'); + const eHandle = await watchdog; + const tagName = await eHandle + .getProperty('tagName') + .then((e) => e.jsonValue()); + expect(tagName).toBe('DIV'); + }); + + it('should work when node is added through innerHTML', async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const watchdog = page.waitForSelector('h3 div'); + await page.evaluate(addElement, 'span'); + await page.evaluate( + () => + (document.querySelector('span').innerHTML = '

') + ); + await watchdog; + }); + + itFailsFirefox( + 'Page.waitForSelector is shortcut for main frame', + async () => { + const { page, server } = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const otherFrame = page.frames()[1]; + const watchdog = page.waitForSelector('div'); + await otherFrame.evaluate(addElement, 'div'); + await page.evaluate(addElement, 'div'); + const eHandle = await watchdog; + expect(eHandle.executionContext().frame()).toBe(page.mainFrame()); + } + ); + + itFailsFirefox('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForSelectorPromise = frame2.waitForSelector('div'); + await frame1.evaluate(addElement, 'div'); + await frame2.evaluate(addElement, 'div'); + const eHandle = await waitForSelectorPromise; + expect(eHandle.executionContext().frame()).toBe(frame2); + }); + + itFailsFirefox('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForSelector('.box') + .catch((error) => (waitError = error)); + await utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + it('should survive cross-process navigation', async () => { + const { page, server } = getTestState(); + + let boxFound = false; + const waitForSelector = page + .waitForSelector('.box') + .then(() => (boxFound = true)); + await page.goto(server.EMPTY_PAGE); + expect(boxFound).toBe(false); + await page.reload(); + expect(boxFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + await waitForSelector; + expect(boxFound).toBe(true); + }); + it('should wait for visible', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('div', { visible: true }) + .then(() => (divFound = true)); + await page.setContent( + `
1
` + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divFound).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divFound).toBe(true); + }); + it('should wait for visible recursively', async () => { + const { page } = getTestState(); + + let divVisible = false; + const waitForSelector = page + .waitForSelector('div#inner', { visible: true }) + .then(() => (divVisible = true)); + await page.setContent( + `
hi
` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('display') + ); + expect(divVisible).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.removeProperty('visibility') + ); + expect(await waitForSelector).toBe(true); + expect(divVisible).toBe(true); + }); + it('hidden should wait for visibility: hidden', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`
`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('visibility', 'hidden') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + it('hidden should wait for display: none', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`
`); + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForSelector('div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + it('hidden should wait for removal', async () => { + const { page } = getTestState(); + + await page.setContent(`
`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('div', { hidden: true }) + .then(() => (divRemoved = true)); + await page.waitForSelector('div'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => document.querySelector('div').remove()); + expect(await waitForSelector).toBe(true); + expect(divRemoved).toBe(true); + }); + it('should return null if waiting to hide non-existing element', async () => { + const { page } = getTestState(); + + const handle = await page.waitForSelector('non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForSelector('div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const { page } = getTestState(); + + await page.setContent(`
`); + let error = null; + await page + .waitForSelector('div', { hidden: true, timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `div` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const { page } = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('.zombo') + .then(() => (divFound = true)); + await page.setContent(`
`); + expect(divFound).toBe(false); + await page.evaluate( + () => (document.querySelector('div').className = 'zombo') + ); + expect(await waitForSelector).toBe(true); + }); + it('should return the element handle', async () => { + const { page } = getTestState(); + + const waitForSelector = page.waitForSelector('.zombo'); + await page.setContent(`
anything
`); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForSelector + ) + ).toBe('anything'); + }); + it('should have correct stack trace for timeout', async () => { + const { page } = getTestState(); + + let error; + await page + .waitForSelector('.zombo', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error.stack).toContain('waiting for selector `.zombo` failed'); + // The extension is ts here as Mocha maps back via sourcemaps. + expect(error.stack).toContain('waittask.spec.ts'); + }); + }); + + describe('Frame.waitForXPath', function () { + const addElement = (tag) => + document.body.appendChild(document.createElement(tag)); + + it('should support some fancy xpath', async () => { + const { page } = getTestState(); + + await page.setContent(`

red herring

hello world

`); + const waitForXPath = page.waitForXPath( + '//p[normalize-space(.)="hello world"]' + ); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('hello world '); + }); + it('should respect timeout', async () => { + const { page, puppeteer } = getTestState(); + + let error = null; + await page + .waitForXPath('//div', { timeout: 10 }) + .catch((error_) => (error = error_)); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for XPath `//div` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + itFailsFirefox('should run in specified frame', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForXPathPromise = frame2.waitForXPath('//div'); + await frame1.evaluate(addElement, 'div'); + await frame2.evaluate(addElement, 'div'); + const eHandle = await waitForXPathPromise; + expect(eHandle.executionContext().frame()).toBe(frame2); + }); + itFailsFirefox('should throw when frame is detached', async () => { + const { page, server } = getTestState(); + + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError = null; + const waitPromise = frame + .waitForXPath('//*[@class="box"]') + .catch((error) => (waitError = error)); + await utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + it('hidden should wait for display: none', async () => { + const { page } = getTestState(); + + let divHidden = false; + await page.setContent(`
`); + const waitForXPath = page + .waitForXPath('//div', { hidden: true }) + .then(() => (divHidden = true)); + await page.waitForXPath('//div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => + document.querySelector('div').style.setProperty('display', 'none') + ); + expect(await waitForXPath).toBe(true); + expect(divHidden).toBe(true); + }); + it('should return the element handle', async () => { + const { page } = getTestState(); + + const waitForXPath = page.waitForXPath('//*[@class="zombo"]'); + await page.setContent(`
anything
`); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('anything'); + }); + it('should allow you to select a text node', async () => { + const { page } = getTestState(); + + await page.setContent(`
some text
`); + const text = await page.waitForXPath('//div/text()'); + expect(await (await text.getProperty('nodeType')).jsonValue()).toBe( + 3 /* Node.TEXT_NODE */ + ); + }); + it('should allow you to select an element with single slash', async () => { + const { page } = getTestState(); + + await page.setContent(`
some text
`); + const waitForXPath = page.waitForXPath('/html/body/div'); + expect( + await page.evaluate( + (x: HTMLElement) => x.textContent, + await waitForXPath + ) + ).toBe('some text'); + }); + }); +}); diff --git a/test/worker.spec.ts b/test/worker.spec.ts new file mode 100644 index 0000000000000..b4f81dbb8cf04 --- /dev/null +++ b/test/worker.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2020 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 { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; +import { WebWorker } from '../lib/cjs/puppeteer/common/WebWorker.js'; +import { ConsoleMessage } from '../lib/cjs/puppeteer/common/ConsoleMessage.js'; +const { waitEvent } = utils; + +describeFailsFirefox('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const { page, server } = getTestState(); + + await Promise.all([ + new Promise((x) => page.once('workercreated', x)), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]; + expect(worker.url()).toContain('worker.js'); + + expect(await worker.evaluate(() => globalThis.workerFunction())).toBe( + 'worker function result' + ); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers().length).toBe(0); + }); + it('should emit created and destroyed events', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise((x) => + page.once('workercreated', x) + ); + const workerObj = await page.evaluateHandle( + () => new Worker('data:text/javascript,1') + ); + const worker = await workerCreatedPromise; + const workerThisObj = await worker.evaluateHandle(() => this); + const workerDestroyedPromise = new Promise((x) => + page.once('workerdestroyed', x) + ); + await page.evaluate( + (workerObj: Worker) => workerObj.terminate(), + workerObj + ); + expect(await workerDestroyedPromise).toBe(worker); + const error = await workerThisObj + .getProperty('self') + .catch((error) => error); + expect(error.message).toContain('Most likely the worker has been closed.'); + }); + it('should report console logs', async () => { + const { page } = getTestState(); + + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(() => new Worker(`data:text/javascript,console.log(1)`)), + ]); + expect(message.text()).toBe('1'); + expect(message.location()).toEqual({ + url: '', + lineNumber: 0, + columnNumber: 8, + }); + }); + it('should have JSHandles for console logs', async () => { + const { page } = getTestState(); + + const logPromise = new Promise((x) => + page.on('console', x) + ); + await page.evaluate( + () => new Worker(`data:text/javascript,console.log(1,2,3,this)`) + ); + const log = await logPromise; + expect(log.text()).toBe('1 2 3 JSHandle@object'); + expect(log.args().length).toBe(4); + expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const { page } = getTestState(); + + const workerCreatedPromise = new Promise((x) => + page.once('workercreated', x) + ); + await page.evaluate( + () => new Worker(`data:text/javascript,console.log(1)`) + ); + const worker = await workerCreatedPromise; + expect(await (await worker.executionContext()).evaluate('1+1')).toBe(2); + }); + it('should report errors', async () => { + const { page } = getTestState(); + + const errorPromise = new Promise((x) => page.on('pageerror', x)); + await page.evaluate( + () => + new Worker(`data:text/javascript, throw new Error('this is my error');`) + ); + const errorLog = await errorPromise; + expect(errorLog.message).toContain('this is my error'); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000000000..571f4cc62e4e2 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "allowJs": true, + "checkJs": true, + "target": "ES2019", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "sourceMap": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000..0fe7e06ad83c7 --- /dev/null +++ b/tsconfig.json @@ -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": { + "noEmit": true, + /* This module setting is just for VSCode so it doesn't throw error when we + use dynamic imports. + */ + "module": "esnext" + }, + "include": ["src"] +} diff --git a/typescript-if-required.js b/typescript-if-required.js new file mode 100644 index 0000000000000..96e6b541a7d27 --- /dev/null +++ b/typescript-if-required.js @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const child_process = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const { promisify } = require('util'); + +const exec = promisify(child_process.exec); +const fsAccess = promisify(fs.access); + +const fileExists = async (filePath) => + fsAccess(filePath) + .then(() => true) + .catch(() => false); +/* + + * Now Puppeteer is built with TypeScript, we need to ensure that + * locally we have the generated output before trying to install. + * + * For users installing puppeteer this is fine, they will have the + * generated lib/ directory as we ship it when we publish to npm. + * + * However, if you're cloning the repo to contribute, you won't have the + * generated lib/ directory so this script checks if we need to run + * TypeScript first to ensure the output exists and is in the right + * place. + */ +async function compileTypeScript() { + return exec('npm run tsc').catch((error) => { + console.error('Error running TypeScript', error); + process.exit(1); + }); +} + +async function compileTypeScriptIfRequired() { + const libPath = path.join(__dirname, 'lib'); + const libExists = await fileExists(libPath); + if (libExists) return; + + console.log('Puppeteer:', 'Compiling TypeScript...'); + await compileTypeScript(); +} + +// It's being run as node typescript-if-required.js, not require('..') +if (require.main === module) compileTypeScriptIfRequired(); + +module.exports = compileTypeScriptIfRequired; diff --git a/utils/ESTreeWalker.js b/utils/ESTreeWalker.js new file mode 100644 index 0000000000000..1c6c6d47828ed --- /dev/null +++ b/utils/ESTreeWalker.js @@ -0,0 +1,135 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @unrestricted + */ +class ESTreeWalker { + /** + * @param {function(!ESTree.Node):(!Object|undefined)} beforeVisit + * @param {function(!ESTree.Node)=} afterVisit + */ + constructor(beforeVisit, afterVisit) { + this._beforeVisit = beforeVisit; + this._afterVisit = afterVisit || new Function(); + } + + /** + * @param {!ESTree.Node} ast + */ + walk(ast) { + this._innerWalk(ast, null); + } + + /** + * @param {!ESTree.Node} node + * @param {?ESTree.Node} parent + */ + _innerWalk(node, parent) { + if (!node) return; + node.parent = parent; + + if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) { + this._afterVisit.call(null, node); + return; + } + + const walkOrder = ESTreeWalker._walkOrder[node.type]; + if (!walkOrder) return; + + if (node.type === 'TemplateLiteral') { + const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node); + const expressionsLength = templateLiteral.expressions.length; + for (let i = 0; i < expressionsLength; ++i) { + this._innerWalk(templateLiteral.quasis[i], templateLiteral); + this._innerWalk(templateLiteral.expressions[i], templateLiteral); + } + this._innerWalk( + templateLiteral.quasis[expressionsLength], + templateLiteral + ); + } else { + for (let i = 0; i < walkOrder.length; ++i) { + const entity = node[walkOrder[i]]; + if (Array.isArray(entity)) this._walkArray(entity, node); + else this._innerWalk(entity, node); + } + } + + this._afterVisit.call(null, node); + } + + /** + * @param {!Array.} nodeArray + * @param {?ESTree.Node} parentNode + */ + _walkArray(nodeArray, parentNode) { + for (let i = 0; i < nodeArray.length; ++i) + this._innerWalk(nodeArray[i], parentNode); + } +} + +/** @typedef {!Object} ESTreeWalker.SkipSubtree */ +ESTreeWalker.SkipSubtree = {}; + +/** @enum {!Array.} */ +ESTreeWalker._walkOrder = { + AwaitExpression: ['argument'], + ArrayExpression: ['elements'], + ArrowFunctionExpression: ['params', 'body'], + AssignmentExpression: ['left', 'right'], + AssignmentPattern: ['left', 'right'], + BinaryExpression: ['left', 'right'], + BlockStatement: ['body'], + BreakStatement: ['label'], + CallExpression: ['callee', 'arguments'], + CatchClause: ['param', 'body'], + ClassBody: ['body'], + ClassDeclaration: ['id', 'superClass', 'body'], + ClassExpression: ['id', 'superClass', 'body'], + ConditionalExpression: ['test', 'consequent', 'alternate'], + ContinueStatement: ['label'], + DebuggerStatement: [], + DoWhileStatement: ['body', 'test'], + EmptyStatement: [], + ExpressionStatement: ['expression'], + ForInStatement: ['left', 'right', 'body'], + ForOfStatement: ['left', 'right', 'body'], + ForStatement: ['init', 'test', 'update', 'body'], + FunctionDeclaration: ['id', 'params', 'body'], + FunctionExpression: ['id', 'params', 'body'], + Identifier: [], + IfStatement: ['test', 'consequent', 'alternate'], + LabeledStatement: ['label', 'body'], + Literal: [], + LogicalExpression: ['left', 'right'], + MemberExpression: ['object', 'property'], + MethodDefinition: ['key', 'value'], + NewExpression: ['callee', 'arguments'], + ObjectExpression: ['properties'], + ObjectPattern: ['properties'], + ParenthesizedExpression: ['expression'], + Program: ['body'], + Property: ['key', 'value'], + ReturnStatement: ['argument'], + SequenceExpression: ['expressions'], + Super: [], + SwitchCase: ['test', 'consequent'], + SwitchStatement: ['discriminant', 'cases'], + TaggedTemplateExpression: ['tag', 'quasi'], + TemplateElement: [], + TemplateLiteral: ['quasis', 'expressions'], + ThisExpression: [], + ThrowStatement: ['argument'], + TryStatement: ['block', 'handler', 'finalizer'], + UnaryExpression: ['argument'], + UpdateExpression: ['argument'], + VariableDeclaration: ['declarations'], + VariableDeclarator: ['id', 'init'], + WhileStatement: ['test', 'body'], + WithStatement: ['object', 'body'], + YieldExpression: ['argument'], +}; + +module.exports = ESTreeWalker; diff --git a/utils/apply_next_version.js b/utils/apply_next_version.js new file mode 100644 index 0000000000000..2882eed45b791 --- /dev/null +++ b/utils/apply_next_version.js @@ -0,0 +1,31 @@ +const path = require('path'); +const fs = require('fs'); +const execSync = require('child_process').execSync; + +// Compare current HEAD to upstream main SHA. +// If they are not equal - refuse to publish since +// we're not tip-of-tree. +const upstream_sha = execSync( + `git ls-remote https://github.com/puppeteer/puppeteer --tags main | cut -f1` +).toString('utf8'); +const current_sha = execSync(`git rev-parse HEAD`).toString('utf8'); +if (upstream_sha.trim() !== current_sha.trim()) { + console.log('REFUSING TO PUBLISH: this is not tip-of-tree!'); + process.exit(1); +} + +const package = require('../package.json'); +let version = package.version; +const dashIndex = version.indexOf('-'); +if (dashIndex !== -1) version = version.substring(0, dashIndex); +version += '-next.' + Date.now(); +console.log('Setting version to ' + version); +package.version = version; +fs.writeFileSync( + path.join(__dirname, '..', 'package.json'), + JSON.stringify(package, undefined, 2) + '\n' +); + +console.log( + 'IMPORTANT: you should update the pinned version of devtools-protocol to match the new revision.' +); diff --git a/utils/bisect.js b/utils/bisect.js new file mode 100755 index 0000000000000..60b7ba07c61d5 --- /dev/null +++ b/utils/bisect.js @@ -0,0 +1,288 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const URL = require('url'); +const debug = require('debug'); +const pptr = require('..'); +const browserFetcher = pptr.createBrowserFetcher(); +const path = require('path'); +const fs = require('fs'); +const { fork, spawn, execSync } = require('child_process'); + +const COLOR_RESET = '\x1b[0m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; +const COLOR_YELLOW = '\x1b[33m'; + +const argv = require('minimist')(process.argv.slice(2), {}); + +const help = ` +Usage: + node bisect.js --good --bad +``` + +You can find the library on `window.mitt`. + +## Usage + +```js +import mitt from 'mitt' + +const emitter = mitt() + +// listen to an event +emitter.on('foo', e => console.log('foo', e) ) + +// listen to all events +emitter.on('*', (type, e) => console.log(type, e) ) + +// fire an event +emitter.emit('foo', { a: 'b' }) + +// clearing all events +emitter.all.clear() + +// working with handler references: +function onFoo() {} +emitter.on('foo', onFoo) // listen +emitter.off('foo', onFoo) // unlisten +``` + +### Typescript + +```ts +import mitt from 'mitt'; +const emitter: mitt.Emitter = mitt(); +``` + +## Examples & Demos + + + Preact + Mitt Codepen Demo +
+ preact + mitt preview +
+ +* * * + +## API + + + +#### Table of Contents + +- [mitt](#mitt) +- [all](#all) +- [on](#on) + - [Parameters](#parameters) +- [off](#off) + - [Parameters](#parameters-1) +- [emit](#emit) + - [Parameters](#parameters-2) + +### mitt + +Mitt: Tiny (~200b) functional event emitter / pubsub. + +Returns **Mitt** + +### all + +A Map of event names to registered handler functions. + +### on + +Register an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `"*"` for all events +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event + +### off + +Remove an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `"*"` +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove + +### emit + +Invoke all handlers for the given type. +If present, `"*"` handlers are invoked after type-matched handlers. + +Note: Manually firing "\*" handlers is not supported. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke +- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler + +## Contribute + +First off, thanks for taking the time to contribute! +Now, take a moment to be sure your contributions make sense to everyone else. + +### Reporting Issues + +Found a problem? Want a new feature? First of all see if your issue or idea has [already been reported](../../issues). +If don't, just open a [new clear and descriptive issue](../../issues/new). + +### Submitting pull requests + +Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits. + +- Fork it! +- Clone your fork: `git clone https://github.com//mitt` +- Navigate to the newly cloned directory: `cd mitt` +- Create a new branch for the new feature: `git checkout -b my-new-feature` +- Install the tools necessary for development: `npm install` +- Make your changes. +- Commit your changes: `git commit -am 'Add some feature'` +- Push to the branch: `git push origin my-new-feature` +- Submit a pull request with full remarks documenting your changes. + +## License + +[MIT License](https://opensource.org/licenses/MIT) © [Jason Miller](https://jasonformat.com/) diff --git a/vendor/mitt/dist/mitt.es.js b/vendor/mitt/dist/mitt.es.js new file mode 100644 index 0000000000000..889e27282f65c --- /dev/null +++ b/vendor/mitt/dist/mitt.es.js @@ -0,0 +1,2 @@ +export default function(n){return{all:n=n||new Map,on:function(t,e){var i=n.get(t);i&&i.push(e)||n.set(t,[e])},off:function(t,e){var i=n.get(t);i&&i.splice(i.indexOf(e)>>>0,1)},emit:function(t,e){(n.get(t)||[]).slice().map(function(n){n(e)}),(n.get("*")||[]).slice().map(function(n){n(t,e)})}}} +//# sourceMappingURL=mitt.es.js.map diff --git a/vendor/mitt/dist/mitt.es.js.map b/vendor/mitt/dist/mitt.es.js.map new file mode 100644 index 0000000000000..6576278e2da07 --- /dev/null +++ b/vendor/mitt/dist/mitt.es.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.es.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.js b/vendor/mitt/dist/mitt.js new file mode 100644 index 0000000000000..2bd0cf9e44e53 --- /dev/null +++ b/vendor/mitt/dist/mitt.js @@ -0,0 +1,2 @@ +module.exports=function(n){return{all:n=n||new Map,on:function(e,t){var i=n.get(e);i&&i.push(t)||n.set(e,[t])},off:function(e,t){var i=n.get(e);i&&i.splice(i.indexOf(t)>>>0,1)},emit:function(e,t){(n.get(e)||[]).slice().map(function(n){n(t)}),(n.get("*")||[]).slice().map(function(n){n(e,t)})}}}; +//# sourceMappingURL=mitt.js.map diff --git a/vendor/mitt/dist/mitt.js.map b/vendor/mitt/dist/mitt.js.map new file mode 100644 index 0000000000000..37f6f59ebd6bd --- /dev/null +++ b/vendor/mitt/dist/mitt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.modern.js b/vendor/mitt/dist/mitt.modern.js new file mode 100644 index 0000000000000..0777f6de72093 --- /dev/null +++ b/vendor/mitt/dist/mitt.modern.js @@ -0,0 +1,2 @@ +export default function(e){return{all:e=e||new Map,on(t,n){const s=e.get(t);s&&s.push(n)||e.set(t,[n])},off(t,n){const s=e.get(t);s&&s.splice(s.indexOf(n)>>>0,1)},emit(t,n){(e.get(t)||[]).slice().map(e=>{e(n)}),(e.get("*")||[]).slice().map(e=>{e(t,n)})}}} +//# sourceMappingURL=mitt.modern.js.map diff --git a/vendor/mitt/dist/mitt.modern.js.map b/vendor/mitt/dist/mitt.modern.js.map new file mode 100644 index 0000000000000..5f669b2d61d0b --- /dev/null +++ b/vendor/mitt/dist/mitt.modern.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.modern.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,GAAYC,EAAiBC,GAC5B,MAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,IAAaN,EAAiBC,GAC7B,MAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,KAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAKX,IAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAKX,IAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/dist/mitt.umd.js b/vendor/mitt/dist/mitt.umd.js new file mode 100644 index 0000000000000..ce0e0059aef8f --- /dev/null +++ b/vendor/mitt/dist/mitt.umd.js @@ -0,0 +1,2 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f&&f.push(t)||e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&f.splice(f.indexOf(t)>>>0,1)},emit:function(n,t){(e.get(n)||[]).slice().map(function(e){e(t)}),(e.get("*")||[]).slice().map(function(e){e(n,t)})}}}}); +//# sourceMappingURL=mitt.umd.js.map diff --git a/vendor/mitt/dist/mitt.umd.js.map b/vendor/mitt/dist/mitt.umd.js.map new file mode 100644 index 0000000000000..642c89400653d --- /dev/null +++ b/vendor/mitt/dist/mitt.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.umd.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array;\nexport type WildCardEventHandlerList = Array;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton(type: EventType, handler: Handler): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff(type: EventType, handler: Handler): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff(type: EventType, handler: Handler) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"6LAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"} \ No newline at end of file diff --git a/vendor/mitt/index.d.ts b/vendor/mitt/index.d.ts new file mode 100644 index 0000000000000..55346dd9769a8 --- /dev/null +++ b/vendor/mitt/index.d.ts @@ -0,0 +1,21 @@ +export declare type EventType = string | symbol; +export declare type Handler = (event?: T) => void; +export declare type WildcardHandler = (type: EventType, event?: any) => void; +export declare type EventHandlerList = Array; +export declare type WildCardEventHandlerList = Array; +export declare type EventHandlerMap = Map; +export interface Emitter { + all: EventHandlerMap; + on(type: EventType, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; + off(type: EventType, handler: Handler): void; + off(type: '*', handler: WildcardHandler): void; + emit(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter; diff --git a/vendor/mitt/package.json b/vendor/mitt/package.json new file mode 100644 index 0000000000000..0105524a0d290 --- /dev/null +++ b/vendor/mitt/package.json @@ -0,0 +1,141 @@ +{ + "_from": "mitt@latest", + "_id": "mitt@2.1.0", + "_inBundle": false, + "_integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "_location": "/mitt", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "mitt@latest", + "name": "mitt", + "escapedName": "mitt", + "rawSpec": "latest", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "_shasum": "f740577c23176c6205b121b2973514eade1b2230", + "_spec": "mitt@latest", + "_where": "/Users/jacktfranklin/src/puppeteer", + "authors": [ + "Jason Miller " + ], + "bugs": { + "url": "https://github.com/developit/mitt/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Tiny 200b functional Event Emitter / pubsub.", + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.4", + "@types/sinon-chai": "^3.2.4", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "chai": "^4.2.0", + "documentation": "^13.0.0", + "eslint": "^7.1.0", + "eslint-config-developit": "^1.2.0", + "esm": "^3.2.25", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "sinon": "^9.0.2", + "sinon-chai": "^3.5.0", + "ts-node": "^8.10.2", + "typescript": "^3.9.3" + }, + "eslintConfig": { + "extends": [ + "developit", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "env": { + "browser": true, + "mocha": true, + "jest": false, + "es6": true + }, + "globals": { + "expect": true + }, + "rules": { + "semi": [ + 2, + "always" + ], + "jest/valid-expect": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0 + } + }, + "eslintIgnore": [ + "dist", + "index.d.ts" + ], + "esmodules": "dist/mitt.modern.js", + "files": [ + "src", + "dist", + "index.d.ts" + ], + "homepage": "https://github.com/developit/mitt", + "jsnext:main": "dist/mitt.es.js", + "keywords": [ + "events", + "eventemitter", + "emitter", + "pubsub" + ], + "license": "MIT", + "main": "dist/mitt.js", + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, + "module": "dist/mitt.es.js", + "name": "mitt", + "repository": { + "type": "git", + "url": "git+https://github.com/developit/mitt.git" + }, + "scripts": { + "build": "npm-run-all --silent clean -p bundle -s docs", + "bundle": "microbundle", + "clean": "rimraf dist", + "docs": "documentation readme src/index.ts --section API -q --parse-extension ts", + "lint": "eslint src test --ext ts --ext js", + "mocha": "mocha test", + "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "test": "npm-run-all --silent typecheck lint mocha test-types", + "test-types": "tsc test/test-types-compilation.ts --noEmit", + "typecheck": "tsc --noEmit" + }, + "source": "src/index.ts", + "typings": "index.d.ts", + "umd:main": "dist/mitt.umd.js", + "version": "2.1.0" +} diff --git a/vendor/mitt/src/index.ts b/vendor/mitt/src/index.ts new file mode 100644 index 0000000000000..7b5342f09e1e4 --- /dev/null +++ b/vendor/mitt/src/index.ts @@ -0,0 +1,92 @@ + +/** + * @public + */ +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +/** + * @public + */ +export type Handler = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array; +export type WildCardEventHandlerList = Array; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap = Map; + +export interface Emitter { + all: EventHandlerMap; + + on(type: EventType, handler: Handler): void; + on(type: '*', handler: WildcardHandler): void; + + off(type: EventType, handler: Handler): void; + off(type: '*', handler: WildcardHandler): void; + + emit(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} + +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter { + all = all || new Map(); + + return { + + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + on(type: EventType, handler: Handler) { + const handlers = all.get(type); + const added = handlers && handlers.push(handler); + if (!added) { + all.set(type, [handler]); + } + }, + + /** + * Remove an event handler for the given type. + * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` + * @param {Function} handler Handler function to remove + * @memberOf mitt + */ + off(type: EventType, handler: Handler) { + const handlers = all.get(type); + if (handlers) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + }, + + /** + * Invoke all handlers for the given type. + * If present, `"*"` handlers are invoked after type-matched handlers. + * + * Note: Manually firing "*" handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + emit(type: EventType, evt: T) { + ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); + ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); + } + }; +} diff --git a/vendor/tsconfig.cjs.json b/vendor/tsconfig.cjs.json new file mode 100644 index 0000000000000..0b74a00b15434 --- /dev/null +++ b/vendor/tsconfig.cjs.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/vendor", + "module": "CommonJS", + "strict": false + } +} diff --git a/vendor/tsconfig.esm.json b/vendor/tsconfig.esm.json new file mode 100644 index 0000000000000..8f3ff529589d1 --- /dev/null +++ b/vendor/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/vendor", + "module": "esnext", + "strict": false + } +} diff --git a/versions.js b/versions.js new file mode 100644 index 0000000000000..3e7c11efc6af8 --- /dev/null +++ b/versions.js @@ -0,0 +1,62 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const versionsPerRelease = new Map([ + // This is a mapping from Chromium version => Puppeteer version. + // In Chromium roll patches, use 'NEXT' for the Puppeteer version. + ['103.0.5059.0', 'v14.2.0'], + ['102.0.5002.0', 'v14.0.0'], + ['101.0.4950.0', 'v13.6.0'], + ['100.0.4889.0', 'v13.5.0'], + ['99.0.4844.16', 'v13.2.0'], + ['98.0.4758.0', 'v13.1.0'], + ['97.0.4692.0', 'v12.0.0'], + ['93.0.4577.0', 'v10.2.0'], + ['92.0.4512.0', 'v10.0.0'], + ['91.0.4469.0', 'v9.0.0'], + ['90.0.4427.0', 'v8.0.0'], + ['90.0.4403.0', 'v7.0.0'], + ['89.0.4389.0', 'v6.0.0'], + ['88.0.4298.0', 'v5.5.0'], + ['87.0.4272.0', 'v5.4.0'], + ['86.0.4240.0', 'v5.3.0'], + ['85.0.4182.0', 'v5.2.1'], + ['84.0.4147.0', 'v5.1.0'], + ['83.0.4103.0', 'v3.1.0'], + ['81.0.4044.0', 'v3.0.0'], + ['80.0.3987.0', 'v2.1.0'], + ['79.0.3942.0', 'v2.0.0'], + ['78.0.3882.0', 'v1.20.0'], + ['77.0.3803.0', 'v1.19.0'], + ['76.0.3803.0', 'v1.17.0'], + ['75.0.3765.0', 'v1.15.0'], + ['74.0.3723.0', 'v1.13.0'], + ['73.0.3679.0', 'v1.12.2'], +]); + +// The same major version as the current Chrome Stable per https://chromestatus.com/roadmap. +const lastMaintainedChromiumVersion = '101.0.4950.0'; + +if (!versionsPerRelease.has(lastMaintainedChromiumVersion)) { + throw new Error( + 'lastMaintainedChromiumVersion is missing from versionsPerRelease' + ); +} + +module.exports = { + versionsPerRelease, + lastMaintainedChromiumVersion, +}; diff --git a/web-test-runner.config.js b/web-test-runner.config.js new file mode 100644 index 0000000000000..39a9b0167466e --- /dev/null +++ b/web-test-runner.config.js @@ -0,0 +1,44 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const { chromeLauncher } = require('@web/test-runner-chrome'); + +module.exports = { + files: ['test-browser/**/*.spec.js'], + browserStartTimeout: 60 * 1000, + browsers: [ + chromeLauncher({ + async createPage({ browser }) { + const page = await browser.newPage(); + page.evaluateOnNewDocument((wsEndpoint) => { + window.__ENV__ = { wsEndpoint }; + }, browser.wsEndpoint()); + + return page; + }, + }), + ], + plugins: [ + { + // turn expect UMD into an es module + name: 'esmify-expect', + transform(context) { + if (context.path === '/node_modules/expect/build-es5/index.js') { + return `const module = {}; const exports = {};\n${context.body};\n export default module.exports;`; + } + }, + }, + ], +}; diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000000000..b2d6de30624f6 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000000000..1c9cdcb1ff6db --- /dev/null +++ b/website/README.md @@ -0,0 +1,31 @@ +# Website + +This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. + +Its dependencies are purposefully kept separate from the main Puppeteer codebase's in order to avoid having all our end users install them when installing Puppeteer. In the future we may move this website into its own repository. + +## Installation + +```console +npm install +``` + +## Local Development + +```console +npm start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +## Build + +```console +npm build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Deployment + +Deploys are automatically handled by the `deploy-docs.yml` workflow. diff --git a/website/babel.config.js b/website/babel.config.js new file mode 100644 index 0000000000000..cea7e04a43c80 --- /dev/null +++ b/website/babel.config.js @@ -0,0 +1,18 @@ +/** + * Copyright 2021 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. + */ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/website/blog/CONTRIBUTING.md b/website/blog/CONTRIBUTING.md new file mode 100644 index 0000000000000..ba0b0936e65e8 --- /dev/null +++ b/website/blog/CONTRIBUTING.md @@ -0,0 +1,3 @@ + + +Head to GitHub to view our CONTRIBUTING.md document diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 0000000000000..94cdc36a47897 --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,95 @@ +/** + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const lightCodeTheme = require('prism-react-renderer/themes/github'); +const darkCodeTheme = require('prism-react-renderer/themes/dracula'); + +/** @type {import('@docusaurus/types').DocusaurusConfig} */ +module.exports = { + title: 'Puppeteer documentation', + tagline: 'Note: this documentation is WIP. Please use https://pptr.dev.', + url: 'https://puppeteer.github.io/', + baseUrl: '/puppeteer/', + onBrokenLinks: 'warn', + onBrokenMarkdownLinks: 'ignore', + favicon: 'img/favicon.ico', + organizationName: 'puppeteer', // Usually your GitHub org/user name. + projectName: 'puppeteer', // Usually your repo name. + themeConfig: { + hideableSidebar: true, + navbar: { + style: "primary", + title: 'Puppeteer', + logo: { + alt: 'My Site Logo', + src: 'img/logo.svg', + }, + hideOnScroll: true, + items: [ + { + to: 'docs/puppeteer.puppeteer', + label: 'APIs', + position: 'left', + }, + { + to: 'blog/contributing', + label: 'Contribute', + position: 'left', + }, + { + type: 'docsVersionDropdown', + }, + { + label: 'Github', + href: 'https://github.com/puppeteer/puppeteer', + position: 'right', + }, + { + label: 'Stack', + href: 'https://stackoverflow.com/questions/tagged/puppeteer', + position: 'right' + }, + { + label: 'Version 1.0', + href: 'https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md', + position: 'right' + } + ], + }, + footer: { + style: 'dark', + links: [], + }, + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme, + }, + }, + presets: [ + [ + '@docusaurus/preset-classic', + { + docs: { + sidebarPath: require.resolve('./sidebars.js'), + // Please change this to your repo. + editUrl: 'https://github.com/facebook/puppeteer/edit/main/website/', + }, + theme: { + customCss: require.resolve("./src/css/custom.css"), + }, + }, + ], + ], +}; diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000000000..e1e0542f3b89a --- /dev/null +++ b/website/package.json @@ -0,0 +1,39 @@ +{ + "name": "website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build --out-dir=../docs-dist/", + "swizzle": "docusaurus swizzle", + "clear": "docusaurus clear", + "serve": "docusaurus serve --out-dir=../docs-dist/", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "^2.0.0-beta.6", + "@docusaurus/preset-classic": "^2.0.0-beta.6", + "@mdx-js/react": "^1.6.21", + "@svgr/webpack": "^5.5.0", + "clsx": "^1.1.1", + "file-loader": "^6.2.0", + "prism-react-renderer": "^1.2.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "url-loader": "^4.1.1" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/website/sidebars.js b/website/sidebars.js new file mode 100644 index 0000000000000..ee061505d3007 --- /dev/null +++ b/website/sidebars.js @@ -0,0 +1,1714 @@ +module.exports = { + docs: { + Puppeteer: [ + { + type: 'doc', + id: 'puppeteer.puppeteer', + label: 'Puppeteer', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.puppeteer.clearcustomqueryhandlers', + label: 'clearcustomqueryhandlers', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.connect', + label: 'connect', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.customqueryhandlernames', + label: 'customqueryhandlersnames', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.devices', + label: 'devices', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.errors', + label: 'errors', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.networkconditions', + label: 'networkconditions', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.registercustomqueryhandler', + label: 'registercustomqueryhandler', + }, + { + type: 'doc', + id: 'puppeteer.puppeteer.unregistercustomqueryhandler', + label: 'unregistercustomqueryhandler', + }, + ] + } + ], + "BrowserFetcher": [ + { + type: 'doc', + id: 'puppeteer.browserfetcher', + label: 'BrowserFetcher', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.browserfetcher.candownload', + label: 'candownload', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.download', + label: 'download', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.host', + label: 'host', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.localrevisions', + label: 'localrevisions', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.platform', + label: 'platform', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.product', + label: 'product', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.remove', + label: 'remove', + }, + { + type: 'doc', + id: 'puppeteer.browserfetcher.revisioninfo', + label: 'revisioninfo', + }, + ] + }, + ], + "Browser": [ + { + type: 'doc', + id: 'puppeteer.browser', + label: 'Browser', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.browser.browsercontexts', + label: 'browsercontexts', + }, + { + type: 'doc', + id: 'puppeteer.browser.close', + label: 'close', + }, + { + type: 'doc', + id: 'puppeteer.browser.createincognitobrowsercontext', + label: 'createincognitobrowsercontext', + }, + { + type: 'doc', + id: 'puppeteer.browser.defaultbrowsercontext', + label: 'defaultbrowsercontext', + }, + { + type: 'doc', + id: 'puppeteer.browser.disconnect', + label: 'disconnect', + }, + { + type: 'doc', + id: 'puppeteer.browser.isconnected', + label: 'isconnected', + }, + { + type: 'doc', + id: 'puppeteer.browser.newpage', + label: 'newpage', + }, + { + type: 'doc', + id: 'puppeteer.browser.pages', + label: 'pages', + }, + { + type: 'doc', + id: 'puppeteer.browser.process', + label: 'process', + }, + { + type: 'doc', + id: 'puppeteer.browser.target', + label: 'target', + }, + { + type: 'doc', + id: 'puppeteer.browser.useragent', + label: 'useragent', + }, + { + type: 'doc', + id: 'puppeteer.browser.waitfortarget', + label: 'waitfortarget', + }, + { + type: 'doc', + id: 'puppeteer.browser.wsendpoint', + label: 'wsendpoint', + }, + ] + }, + ], + "BrowserContext": [ + { + type: 'doc', + id: 'puppeteer.browsercontext', + label: 'BrowserContext', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.browsercontext.browser', + label: 'browser', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.overridepermissions', + label: 'overridepermissions', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.close', + label: 'close', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.isincognito', + label: 'isincognito', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.newpage', + label: 'newpage', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.overridepermissions', + label: 'overridepermissions', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.pages', + label: 'pages', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.targets', + label: 'targets', + }, + { + type: 'doc', + id: 'puppeteer.browsercontext.waitfortarget', + label: 'waitfortarget', + }, + ] + }, + ], + "Page": [ + { + type: 'doc', + id: 'puppeteer.page', + label: 'Page', + }, + { + Namespaces: [ + { + type: 'doc', + id: 'puppeteer.page.accessibility', + label: 'accessibility', + }, + { + type: 'doc', + id: 'puppeteer.page.coverage', + label: 'coverage', + }, + { + type: 'doc', + id: 'puppeteer.page.isdraginterceptionenabled', + label: 'isDragInterceptionEnabled', + }, + { + type: 'doc', + id: 'puppeteer.page.keyboard', + label: 'keyboard', + }, + { + type: 'doc', + id: 'puppeteer.page.mouse', + label: 'mouse', + }, + { + type: 'doc', + id: 'puppeteer.page.touchscreen', + label: 'touchScreen', + }, + { + type: 'doc', + id: 'puppeteer.page.tracing', + label: 'tracing', + }, + ] + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.page._', + label: '$', + }, + { + type: 'doc', + id: 'puppeteer.page.__', + label: '$$', + }, + { + type: 'doc', + id: 'puppeteer.page.__eval', + label: '$$eval', + }, + { + type: 'doc', + id: 'puppeteer.page._eval', + label: '$eval', + }, + { + type: 'doc', + id: 'puppeteer.page._x', + label: '$x', + }, + { + type: 'doc', + id: 'puppeteer.page.addscripttag', + label: 'addScriptTag', + }, + { + type: 'doc', + id: 'puppeteer.page.addstyletag', + label: 'addStyleTag', + }, + { + type: 'doc', + id: 'puppeteer.page.authenticate', + label: 'authenticate', + }, + { + type: 'doc', + id: 'puppeteer.page.bringtofront', + label: 'bringToFront', + }, + { + type: 'doc', + id: 'puppeteer.page.browser', + label: 'browser', + }, + { + type: 'doc', + id: 'puppeteer.page.browsercontext', + label: 'browserContext', + }, + { + type: 'doc', + id: 'puppeteer.page.click', + label: 'click', + }, + { + type: 'doc', + id: 'puppeteer.page.close', + label: 'close', + }, + { + type: 'doc', + id: 'puppeteer.page.content', + label: 'content()', + }, + { + type: 'doc', + id: 'puppeteer.page.cookies', + label: 'cookies', + }, + { + type: 'doc', + id: 'puppeteer.page.createpdfstream', + label: 'createPDFStream', + }, + { + type: 'doc', + id: 'puppeteer.page.deletecookie', + label: 'deleteCookie', + }, + { + type: 'doc', + id: 'puppeteer.page.emulate', + label: 'emulate', + }, + { + type: 'doc', + id: 'puppeteer.page.emulatecputhrottling', + label: 'emulateCPUThrottling', + }, + { + type: 'doc', + id: 'puppeteer.page.emulateidlestate', + label: 'emulateIdleState', + }, + { + type: 'doc', + id: 'puppeteer.page.emulatemediafeatures', + label: 'emulateMediaFeatures', + }, + { + type: 'doc', + id: 'puppeteer.page.emulatenetworkconditions', + label: 'emulateNetworkConditions', + }, + { + type: 'doc', + id: 'puppeteer.page.emulatetimezone', + label: 'emulateTimeZone', + }, + { + type: 'doc', + id: 'puppeteer.page.emulatevisiondeficiency', + label: 'emulateVisionDefinciency', + }, + { + type: 'doc', + id: 'puppeteer.page.evaluate', + label: 'evaluate', + }, + { + type: 'doc', + id: 'puppeteer.page.evaluatehandle', + label: 'evaluateHandle', + }, + { + type: 'doc', + id: 'puppeteer.page.evaluateonnewdocument', + label: 'evaluateOnNewDocument', + }, + { + type: 'doc', + id: 'puppeteer.page.exposefunction', + label: 'exposeFunction', + }, + { + type: 'doc', + id: 'puppeteer.page.focus', + label: 'focus', + }, + { + type: 'doc', + id: 'puppeteer.page.frames', + label: 'frames', + }, + { + type: 'doc', + id: 'puppeteer.page.goback', + label: 'goBack', + }, + { + type: 'doc', + id: 'puppeteer.page.goforward', + label: 'goForward', + }, + { + type: 'doc', + id: 'puppeteer.page.goto', + label: 'goTo', + }, + { + type: 'doc', + id: 'puppeteer.page.hover', + label: 'hover', + }, + { + type: 'doc', + id: 'puppeteer.page.isclosed', + label: 'isClosed', + }, + { + type: 'doc', + id: 'puppeteer.page.isjavascriptenabled', + label: 'isJavaScriptEnbled', + }, + { + type: 'doc', + id: 'puppeteer.page.mainframe', + label: 'mainFrame', + }, + { + type: 'doc', + id: 'puppeteer.page.metrics', + label: 'metrics', + }, + { + type: 'doc', + id: 'puppeteer.page.once', + label: 'once', + }, + { + type: 'doc', + id: 'puppeteer.page.pdf', + label: 'PDF', + }, + { + type: 'doc', + id: 'puppeteer.page.queryobjects', + label: 'queryObjects', + }, + { + type: 'doc', + id: 'puppeteer.page.reload', + label: 'reload', + }, + { + type: 'doc', + id: 'puppeteer.page.screenshot', + label: 'screenshot', + }, + { + type: 'doc', + id: 'puppeteer.page.select', + label: 'select', + }, + { + type: 'doc', + id: 'puppeteer.page.setbypasscsp', + label: 'setByPassCSP', + }, + { + type: 'doc', + id: 'puppeteer.page.setcacheenabled', + label: 'setCacheEnaled', + }, + { + type: 'doc', + id: 'puppeteer.page.setcontent', + label: 'setContent', + }, + { + type: 'doc', + id: 'puppeteer.page.setcookie', + label: 'setCookie', + }, + { + type: 'doc', + id: 'puppeteer.page.setdefaultnavigationtimeout', + label: 'setDefaultNavigationTimeOut', + }, + { + type: 'doc', + id: 'puppeteer.page.setdefaulttimeout', + label: 'setDefaultTimeOut', + }, + { + type: 'doc', + id: 'puppeteer.page.setdraginterception', + label: 'setDragInterception', + }, + { + type: 'doc', + id: 'puppeteer.page.setextrahttpheaders', + label: 'setExtraHTTPHeader', + }, + { + type: 'doc', + id: 'puppeteer.page.setgeolocation', + label: 'setGeoLocation', + }, + { + type: 'doc', + id: 'puppeteer.page.setjavascriptenabled', + label: 'setJavaScriptEnabled', + }, + { + type: 'doc', + id: 'puppeteer.page.setofflinemode', + label: 'setOfflineMode', + }, + { + type: 'doc', + id: 'puppeteer.page.setrequestinterception', + label: 'setRequestInterception', + }, + { + type: 'doc', + id: 'puppeteer.page.setuseragent', + label: 'setUserAgent', + }, + { + type: 'doc', + id: 'puppeteer.page.setviewport', + label: 'setViewPort', + }, + { + type: 'doc', + id: 'puppeteer.page.tap', + label: 'tap', + }, + { + type: 'doc', + id: 'puppeteer.page.target', + label: 'target', + }, + { + type: 'doc', + id: 'puppeteer.page.title', + label: 'title', + }, + { + type: 'doc', + id: 'puppeteer.page.type', + label: 'type', + }, + { + type: 'doc', + id: 'puppeteer.page.url', + label: 'url', + }, + { + type: 'doc', + id: 'puppeteer.page.viewport', + label: 'viewport', + }, + { + type: 'doc', + id: 'puppeteer.page.waitfor', + label: 'waitFor', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforfilechooser', + label: 'waitForFileChooser', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforfunction', + label: 'waitForFunction', + }, + { + type: 'doc', + id: 'puppeteer.page.waitfornavigation', + label: 'waitForNavigation', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforrequest', + label: 'waitForRequest', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforresponse', + label: 'waitForResponse', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforselector', + label: 'waitForSelector', + }, + { + type: 'doc', + id: 'puppeteer.page.waitfortimeout', + label: 'waittimeout', + }, + { + type: 'doc', + id: 'puppeteer.page.waitforxpath', + label: 'waitForXPath', + }, + { + type: 'doc', + id: 'puppeteer.page.workers', + label: 'workers', + }, + ] + }, + ], + "WebWorker": [ + { + type: 'doc', + id: 'puppeteer.webworker', + label: 'WebWorker', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.webworker.evaluate', + label: 'evaluate', + }, + { + type: 'doc', + id: 'puppeteer.webworker.evaluatehandle', + label: 'evaluatehandle', + }, + { + type: 'doc', + id: 'puppeteer.webworker.executioncontext', + label: 'executioncontext', + }, + { + type: 'doc', + id: 'puppeteer.webworker.url', + label: 'url', + }, + ] + }, + ], + "Accessibility": [ + { + type: 'doc', + id: 'puppeteer.accessibility', + label: 'Accessibility', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.accessibility.snapshot', + label: 'snapshot', + }, + ] + }, + ], + "Keyboard": [ + { + type: 'doc', + id: 'puppeteer.keyboard', + label: 'keyboard', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.keyboard.down', + label: 'down', + }, + { + type: 'doc', + id: 'puppeteer.keyboard.press', + label: 'press', + }, + { + type: 'doc', + id: 'puppeteer.keyboard.sendcharacter', + label: 'sendCharacter', + }, + { + type: 'doc', + id: 'puppeteer.keyboard.type', + label: 'type', + }, + { + type: 'doc', + id: 'puppeteer.keyboard.up', + label: 'up', + }, + ] + }, + ], + "Mouse": [ + { + type: 'doc', + id: 'puppeteer.mouse', + label: 'mouse', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.mouse.click', + label: 'click', + }, + { + type: 'doc', + id: 'puppeteer.mouse.down', + label: 'down', + }, + { + type: 'doc', + id: 'puppeteer.mouse.drag', + label: 'drag', + }, + { + type: 'doc', + id: 'puppeteer.mouse.draganddrop', + label: 'dragAndDrop', + }, + { + type: 'doc', + id: 'puppeteer.mouse.dragenter', + label: 'dragEnter', + }, + { + type: 'doc', + id: 'puppeteer.mouse.dragover', + label: 'dragOver', + }, + { + type: 'doc', + id: 'puppeteer.mouse.drop', + label: 'drop', + }, + { + type: 'doc', + id: 'puppeteer.mouse.move', + label: 'move', + }, + { + type: 'doc', + id: 'puppeteer.mouse.up', + label: 'up', + }, + { + type: 'doc', + id: 'puppeteer.mouse.wheel', + label: 'wheel', + }, + ] + }, + ], + "TouchScreen": [ + { + type: 'doc', + id: 'puppeteer.touchscreen', + label: 'touchScreen', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.touchscreen.tap', + label: 'tap' + } + ] + }, + ], + "Tracing": [ + { + type: 'doc', + id: 'puppeteer.tracing', + label: 'Tracing', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.tracing._client', + label: 'client', + }, + { + type: 'doc', + id: 'puppeteer.tracing._path', + label: 'path', + }, + { + type: 'doc', + id: 'puppeteer.tracing._recording', + label: 'recording', + }, + { + type: 'doc', + id: 'puppeteer.tracing.start', + label: 'start', + }, + { + type: 'doc', + id: 'puppeteer.tracing.stop', + label: 'stop', + }, + ] + }, + ], + "Dialog": [ + { + type: 'doc', + id: 'puppeteer.dialog', + label: 'dialog', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.dialog.accept', + label: 'accept', + }, + { + type: 'doc', + id: 'puppeteer.dialog.defaultvalue', + label: 'defaultValue', + }, + { + type: 'doc', + id: 'puppeteer.dialog.dismiss', + label: 'dismiss', + }, + { + type: 'doc', + id: 'puppeteer.dialog.message', + label: 'message', + }, + { + type: 'doc', + id: 'puppeteer.dialog.type', + label: 'type', + }, + ] + }, + ], + "ConsoleMessage": [ + { + type: 'doc', + id: 'puppeteer.consolemessage', + label: 'consoleMessage', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.consolemessage.args', + label: 'args', + }, + { + type: 'doc', + id: 'puppeteer.consolemessage.location', + label: 'location', + }, + { + type: 'doc', + id: 'puppeteer.consolemessage.stacktrace', + label: 'stackTrace', + }, + { + type: 'doc', + id: 'puppeteer.consolemessage.text', + label: 'text', + }, + { + type: 'doc', + id: 'puppeteer.consolemessage.type', + label: 'type', + }, + ] + }, + ], + "Frame": [ + { + type: 'doc', + id: 'puppeteer.frame', + label: 'frame', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.frame._', + label: '$', + }, + { + type: 'doc', + id: 'puppeteer.frame.__', + label: '$$', + }, + { + type: 'doc', + id: 'puppeteer.frame.__eval', + label: '$$eval', + }, + { + type: 'doc', + id: 'puppeteer.frame._eval', + label: '$eval', + }, + { + type: 'doc', + id: 'puppeteer.frame._x', + label: '$x', + }, + { + type: 'doc', + id: 'puppeteer.frame.addscripttag', + label: 'addScriptTag', + }, + { + type: 'doc', + id: 'puppeteer.frame.addstyletag', + label: 'addStyleTag', + }, + { + type: 'doc', + id: 'puppeteer.frame.childframes', + label: 'childFrames', + }, + { + type: 'doc', + id: 'puppeteer.frame.click', + label: 'click', + }, + { + type: 'doc', + id: 'puppeteer.frame.content', + label: 'content', + }, + { + type: 'doc', + id: 'puppeteer.frame.evaluate', + label: 'evaluate', + }, + { + type: 'doc', + id: 'puppeteer.frame.evaluatehandle', + label: 'evaluateHandle', + }, + { + type: 'doc', + id: 'puppeteer.frame.executioncontext', + label: 'executionContext', + }, + { + type: 'doc', + id: 'puppeteer.frame.focus', + label: 'focus', + }, + { + type: 'doc', + id: 'puppeteer.frame.goto', + label: 'goTo', + }, + { + type: 'doc', + id: 'puppeteer.frame.hover', + label: 'hover', + }, + { + type: 'doc', + id: 'puppeteer.frame.isdetached', + label: 'isDetached', + }, + { + type: 'doc', + id: 'puppeteer.frame.name', + label: 'name', + }, + { + type: 'doc', + id: 'puppeteer.frame.parentframe', + label: 'parentFrame', + }, + { + type: 'doc', + id: 'puppeteer.frame.select', + label: 'select', + }, + { + type: 'doc', + id: 'puppeteer.frame.setcontent', + label: 'setContent', + }, + { + type: 'doc', + id: 'puppeteer.frame.tap', + label: 'tap', + }, + { + type: 'doc', + id: 'puppeteer.frame.title', + label: 'title', + }, + { + type: 'doc', + id: 'puppeteer.frame.type', + label: 'type', + }, + { + type: 'doc', + id: 'puppeteer.frame.url', + label: 'url', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitfor', + label: 'waitFor', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitforfunction', + label: 'waitForFunction', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitfornavigation', + label: 'waitForNavigation', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitforselector', + label: 'waitForSelector', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitfortimeout', + label: 'waitForTimeOut', + }, + { + type: 'doc', + id: 'puppeteer.frame.waitforxpath', + label: 'waitForXPath', + }, + ] + } + ], + "FileChooser": [ + { + type: 'doc', + id: 'puppeteer.filechooser', + label: 'FileChooser', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.filechooser.accept', + label: 'accept', + }, + { + type: 'doc', + id: 'puppeteer.filechooser.cancel', + label: 'cancel', + }, + { + type: 'doc', + id: 'puppeteer.filechooser.ismultiple', + label: 'ismultiple', + }, + ] + }, + ], + "ExecutionContext": [ + { + type: 'doc', + id: 'puppeteer.executioncontext', + label: 'executionContext', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.executioncontext.evaluate', + label: 'evaluate', + }, + { + type: 'doc', + id: 'puppeteer.executioncontext.evaluatehandle', + label: 'evaluateHandle', + }, + { + type: 'doc', + id: 'puppeteer.executioncontext.frame', + label: 'frame', + }, + { + type: 'doc', + id: 'puppeteer.executioncontext.queryobjects', + label: 'queryobjects', + }, + ] + }, + ], + "JSHandle": [ + { + type: 'doc', + id: 'puppeteer.jshandle', + label: 'JSHandle', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.jshandle.aselement', + label: 'asElement', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.dispose', + label: 'dispose', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.evaluate', + label: 'evaluate', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.evaluatehandle', + label: 'evaluateHandle', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.executioncontext', + label: 'executionContext', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.getproperties', + label: 'getProperties', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.getproperty', + label: 'getProperty', + }, + { + type: 'doc', + id: 'puppeteer.jshandle.jsonvalue', + label: 'JSONValue', + }, + ] + }, + ], + "ElementHandle": [ + { + type: 'doc', + id: 'puppeteer.elementhandle', + label: 'elementHandle', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.elementhandle._', + label: '$', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.__', + label: '$$', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.__eval', + label: '$$eval', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle._eval', + label: '$eval', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle._x', + label: '$x', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.aselement', + label: 'asElement', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.boundingbox', + label: 'boundingBox', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.boxmodel', + label: 'boxModel', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.click', + label: 'click', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.contentframe', + label: 'contentFrame', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.focus', + label: 'focus', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.hover', + label: 'hover', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.clickablepoint', + label: 'clickablePoint', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.drag', + label: 'drag', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.draganddrop', + label: 'dragAndDrop', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.dragenter', + label: 'dragEnter', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.dragover', + label: 'dragOver', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.drop', + label: 'drop', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.isintersectingviewport', + label: 'isIntersectingViewPort', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.press', + label: 'press', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.screenshot', + label: 'screenshot', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.select', + label: 'select', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.tap', + label: 'tap', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.type', + label: 'type', + }, + { + type: 'doc', + id: 'puppeteer.elementhandle.uploadfile', + label: 'uploadFile', + }, + ] + }, + ], + "HTTPRequest": [ + { + type: 'doc', + id: 'puppeteer.httprequest', + label: 'httpRequest', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.httprequest.abort', + label: 'abort', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.aborterrorreason', + label: 'abortErrorReason', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.enqueueinterceptaction', + label: 'enqueueInterCeptaction', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.failure', + label: 'failure', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.finalizeinterceptions', + label: 'finalizeInterception', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.frame', + label: 'frame', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.headers', + label: 'headers', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.isnavigationrequest', + label: 'isNavigationRequest', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.method', + label: 'method', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.postdata', + label: 'postData', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.redirectchain', + label: 'redirectChain', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.resourcetype', + label: 'resourceType', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.respond', + label: 'respond', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.response', + label: 'response', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.responseforrequest', + label: 'responseForRequest', + }, + { + type: 'doc', + id: 'puppeteer.httprequest.url', + label: 'hurl', + }, + ] + }, + ], + "HTTPRespose": [ + { + type: 'doc', + id: 'puppeteer.httpresponse', + label: 'httpResponse', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.httpresponse.buffer', + label: 'buffer', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.frame', + label: 'frame', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.fromcache', + label: 'fromCache', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.fromserviceworker', + label: 'fromServiceWorker', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.headers', + label: 'headers', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.json', + label: 'JSON', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.ok', + label: 'OK', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.remoteaddress', + label: 'remoteAddress', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.request', + label: 'request', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.securitydetails', + label: 'securityDetails', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.status', + label: 'status', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.statustext', + label: 'statusText', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.text', + label: 'text', + }, + { + type: 'doc', + id: 'puppeteer.httpresponse.url', + label: 'URL', + }, + ] + }, + ], + "SecurityDetails": [ + { + type: 'doc', + id: 'puppeteer.securitydetails', + label: 'securityDetails', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.securitydetails.issuer', + label: 'issuer', + }, + { + type: 'doc', + id: 'puppeteer.securitydetails.protocol', + label: 'protocol', + }, + { + type: 'doc', + id: 'puppeteer.securitydetails.subjectalternativenames', + label: 'subjectAlternativeNames', + }, + { + type: 'doc', + id: 'puppeteer.securitydetails.subjectname', + label: 'subjectName', + }, + { + type: 'doc', + id: 'puppeteer.securitydetails.validfrom', + label: 'validFrom', + }, + { + type: 'doc', + id: 'puppeteer.securitydetails.validto', + label: 'validTo', + }, + ] + }, + ], + "Target": [ + { + type: 'doc', + id: 'puppeteer.target', + label: 'target', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.target.browser', + label: 'browser', + }, + { + type: 'doc', + id: 'puppeteer.target.browsercontext', + label: 'browserContext', + }, + { + type: 'doc', + id: 'puppeteer.target.createcdpsession', + label: 'createCDPSSession', + }, + { + type: 'doc', + id: 'puppeteer.target.opener', + label: 'opener', + }, + { + type: 'doc', + id: 'puppeteer.target.page', + label: 'page', + }, + { + type: 'doc', + id: 'puppeteer.target.type', + label: 'type', + }, + { + type: 'doc', + id: 'puppeteer.target.url', + label: 'url', + }, + { + type: 'doc', + id: 'puppeteer.target.worker', + label: 'worker', + }, + ] + }, + ], + "CDPSession": [ + { + type: 'doc', + id: 'puppeteer.cdpsession', + label: 'CDPSession', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.cdpsession.connection', + label: 'connection', + }, + { + type: 'doc', + id: 'puppeteer.cdpsession.detach', + label: 'detach', + }, + { + type: 'doc', + id: 'puppeteer.cdpsession.send', + label: 'send', + }, + ] + }, + ], + "Coverage": [ + { + type: 'doc', + id: 'puppeteer.coverage', + label: 'coverage', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.coverage.startcsscoverage', + label: 'startCSSCoverage', + }, + { + type: 'doc', + id: 'puppeteer.coverage.startjscoverage', + label: 'startJSCoverage', + }, + { + type: 'doc', + id: 'puppeteer.coverage.stopcsscoverage', + label: 'stopCSSCoverage', + }, + { + type: 'doc', + id: 'puppeteer.coverage.stopjscoverage', + label: 'stopJSCoverage', + }, + ] + }, + ], + "TimeOutError": [ + { + type: 'doc', + id: 'puppeteer.timeouterror', + label: 'timeOutError', + }, + ], + "EventEmitter": [ + { + type: 'doc', + id: 'puppeteer.eventemitter', + label: 'eventEmitter', + }, + { + Methods: [ + { + type: 'doc', + id: 'puppeteer.eventemitter.addlistener', + label: 'addListener', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.emit', + label: 'emit', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.listenercount', + label: 'listenerCount', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.off', + label: 'off', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.on', + label: 'on', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.once', + label: 'once', + }, + { + type: 'doc', + id: 'puppeteer.eventemitter.removelistener', + label: 'removeListener', + },{ + type: 'doc', + id: 'puppeteer.eventemitter.removealllisteners', + label: 'removeAllListener', + }, + ] + }, + ], + }, +}; diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 0000000000000..ae8906ba7ed8f --- /dev/null +++ b/website/src/css/custom.css @@ -0,0 +1,45 @@ +/** + * Copyright 2021 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. + */ + +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: rgb(64 181 164); + --ifm-color-primary-dark: rgb(33, 175, 144); + --ifm-color-primary-darker: rgb(31, 165, 136); + --ifm-color-primary-darkest: rgb(26, 136, 112); + --ifm-color-primary-light: rgb(70, 203, 174); + --ifm-color-primary-lighter: rgb(102, 212, 189); + --ifm-color-primary-lightest: rgb(146, 224, 208); + --ifm-code-font-size: 95%; +} + +.docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.1); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +html[data-theme='dark'] .docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.3); +} diff --git a/website/src/pages/index.md b/website/src/pages/index.md new file mode 100644 index 0000000000000..e3a2ad653ccbc --- /dev/null +++ b/website/src/pages/index.md @@ -0,0 +1,462 @@ +# Puppeteer + + + +[![Build status](https://github.com/puppeteer/puppeteer/workflows/run-checks/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3Arun-checks) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) + + + + + +###### [API](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md) | [FAQ](#faq) | [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) | [Troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) + +> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. + + + +###### What can I do? + +Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started: + +- Generate screenshots and PDFs of pages. +- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). +- Automate form submission, UI testing, keyboard input, etc. +- Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. +- Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues. +- Test Chrome Extensions. + + +Give it a spin: https://try-puppeteer.appspot.com/ + + + +## Getting Started + +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or "yarn add puppeteer" +``` + +Note: When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API. To skip the download, or to download a different browser, see [Environment variables](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#environment-variables). + +### puppeteer-core + +Since version 1.7.0 we publish the [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) package, +a version of Puppeteer that doesn't download any browser by default. + +```bash +npm i puppeteer-core +# or "yarn add puppeteer-core" +``` + +`puppeteer-core` is intended to be a lightweight version of Puppeteer for launching an existing browser installation or for connecting to a remote one. Be sure that the version of puppeteer-core you install is compatible with the +browser you intend to connect to. + +See [puppeteer vs puppeteer-core](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteer-vs-puppeteer-core). + +### Usage + +Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. + +Note: Prior to v1.18.1, Puppeteer required at least Node v6.4.0. Versions from v1.18.1 to v2.1.0 rely on +Node 8.9.0+. Starting from v3.0.0 Puppeteer starts to rely on Node 10.18.1+. All examples below use async/await which is only supported in Node v7.6.0 or greater. + +Puppeteer will be familiar to people using other browser testing frameworks. You create an instance +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#). + +**Example** - navigating to https://example.com and saving a screenshot as _example.png_: + +Save file as **example.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({ path: 'example.png' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#pagesetviewportviewport). + +**Example** - create a PDF. + +Save file as **hn.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + await page.pdf({ path: 'hn.pdf', format: 'a4' }); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node hn.js +``` + +See [`Page.pdf()`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#pagepdfoptions) for more information about creating pdfs. + +**Example** - evaluate script in the context of the page + +Save file as **get-dimensions.js** + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio, + }; + }); + + console.log('Dimensions:', dimensions); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node get-dimensions.js +``` + +See [`Page.evaluate()`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#pageevaluatepagefunction-args) for more information on `evaluate` and related methods like `evaluateOnNewDocument` and `exposeFunction`. + + + + + +## Default runtime settings + +**1. Uses Headless mode** + +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#puppeteerlaunchoptions) when launching a browser: + +```js +const browser = await puppeteer.launch({ headless: false }); // default is true +``` + +**2. Runs a bundled version of Chromium** + +By default, Puppeteer downloads and uses a specific version of Chromium so its API +is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium, +pass in the executable's path when creating a `Browser` instance: + +```js +const browser = await puppeteer.launch({ executablePath: '/path/to/Chrome' }); +``` + +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch()`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#puppeteerlaunchoptions) for more information. + +See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/master/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. + +**3. Creates a fresh user profile** + +Puppeteer creates its own browser user profile which it **cleans up on every run**. + + + +## Resources + +- [API Documentation](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples/) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + + + +## Debugging tips + +1. Turn off headless mode - sometimes it's useful to see what the browser is + displaying. Instead of launching in headless mode, launch a full version of + the browser using `headless: false`: + + ```js + const browser = await puppeteer.launch({ headless: false }); + ``` + +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the + specified amount of milliseconds. It's another way to help see what's going on. + + ```js + const browser = await puppeteer.launch({ + headless: false, + slowMo: 250, // slow down by 250ms + }); + ``` + +3. Capture console output - You can listen for the `console` event. + This is also handy when debugging code in `page.evaluate()`: + + ```js + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => console.log(`url is ${location.href}`)); + ``` + +4. Use debugger in application code browser + + There are two execution context: node.js that is running test code, and the browser + running application code being tested. This lets you debug code in the + application code browser; ie code inside `evaluate()`. + + - Use `{devtools: true}` when launching Puppeteer: + + ```js + const browser = await puppeteer.launch({ devtools: true }); + ``` + + - Change default test timeout: + + jest: `jest.setTimeout(100000);` + + jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;` + + mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442)) + + - Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement: + + ```js + await page.evaluate(() => { + debugger; + }); + ``` + + The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode. + +5. Use debugger in node.js + + This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser. + + Note that you won't be able to run `await page.click()` in + DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if + you want to try something out, you have to add it to your test file. + + - Add `debugger;` to your test, eg: + + ```js + debugger; + await page.click('a[target=_blank]'); + ``` + + - Set `headless` to `false` + - Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests` + - In Chrome open `chrome://inspect/#devices` and click `inspect` + - In the newly opened test browser, type `F8` to resume test execution + - Now your `debugger` will be hit and you can debug in the test browser + +6. Enable verbose logging - internal DevTools protocol traffic + will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. + + # Basic verbose logging + env DEBUG="puppeteer:*" node script.js + + # Protocol traffic can be rather noisy. This example filters out all Network domain messages + env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' + +7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) + +- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) + +- add a `debugger` to your Puppeteer (node) code + +- add `ndb` (or `npx ndb`) before your test command. For example: + + `ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) + +- debug your test inside chromium like a boss! + + + + + +## Usage with TypeScript + +We have recently completed a migration to move the Puppeteer source code from JavaScript to TypeScript and as of version 7.0.1 we ship our own built-in type definitions. + +If you are on a version older than 7, we recommend installing the Puppeteer type definitions from the [DefinitelyTyped](https://definitelytyped.org/) repository: + +```bash +npm install --save-dev @types/puppeteer +``` + +The types that you'll see appearing in the Puppeteer source code are based off the great work of those who have contributed to the `@types/puppeteer` package. We really appreciate the hard work those people put in to providing high quality TypeScript definitions for Puppeteer's users. + + + +## Contributing to Puppeteer + +Check out [contributing guide](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md) to get an overview of Puppeteer development. + + + +# FAQ + +#### Q: Who maintains Puppeteer? + +The Chrome DevTools team maintains the library, but we'd love your help and expertise on the project! +See [Contributing](https://github.com/puppeteer/puppeteer/blob/main/CONTRIBUTING.md). + +#### Q: What is the status of cross-browser support? + +Official Firefox support is currently experimental. The ongoing collaboration with Mozilla aims to support common end-to-end testing use cases, for which developers expect cross-browser coverage. The Puppeteer team needs input from users to stabilize Firefox support and to bring missing APIs to our attention. + +From Puppeteer v2.1.0 onwards you can specify [`puppeteer.launch({product: 'firefox'})`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#puppeteerlaunchoptions) to run your Puppeteer scripts in Firefox Nightly, without any additional custom patches. While [an older experiment](https://www.npmjs.com/package/puppeteer-firefox) required a patched version of Firefox, [the current approach](https://wiki.mozilla.org/Remote) works with “stock” Firefox. + +We will continue to collaborate with other browser vendors to bring Puppeteer support to browsers such as Safari. +This effort includes exploration of a standard for executing cross-browser commands (instead of relying on the non-standard DevTools Protocol used by Chrome). + +#### Q: What are Puppeteer’s goals and principles? + +The goals of the project are: + +- Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer. +- Grow the adoption of headless/automated browser testing. +- Help dogfood new DevTools Protocol features...and catch bugs! +- Learn more about the pain points of automated browser testing and help fill those gaps. + +We adapt [Chromium principles](https://www.chromium.org/developers/core-principles) to help us drive product decisions: + +- **Speed**: Puppeteer has almost zero performance overhead over an automated page. +- **Security**: Puppeteer operates off-process with respect to Chromium, making it safe to automate potentially malicious pages. +- **Stability**: Puppeteer should not be flaky and should not leak memory. +- **Simplicity**: Puppeteer provides a high-level API that’s easy to use, understand, and debug. + +#### Q: Is Puppeteer replacing Selenium/WebDriver? + +**No**. Both projects are valuable for very different reasons: + +- Selenium/WebDriver focuses on cross-browser automation; its value proposition is a single standard API that works across all major browsers. +- Puppeteer focuses on Chromium; its value proposition is richer functionality and higher reliability. + +That said, you **can** use Puppeteer to run tests against Chromium, e.g. using the community-driven [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer). While this probably shouldn’t be your only testing solution, it does have a few good points compared to WebDriver: + +- Puppeteer requires zero setup and comes bundled with the Chromium version it works best with, making it [very easy to start with](https://github.com/puppeteer/puppeteer/#getting-started). At the end of the day, it’s better to have a few tests running chromium-only, than no tests at all. +- Puppeteer has event-driven architecture, which removes a lot of potential flakiness. There’s no need for evil “sleep(1000)” calls in puppeteer scripts. +- Puppeteer runs headless by default, which makes it fast to run. Puppeteer v1.5.0 also exposes browser contexts, making it possible to efficiently parallelize test execution. +- Puppeteer shines when it comes to debugging: flip the “headless” bit to false, add “slowMo”, and you’ll see what the browser is doing. You can even open Chrome DevTools to inspect the test environment. + +#### Q: Why doesn’t Puppeteer v.XXX work with Chromium v.YYY? + +We see Puppeteer as an **indivisible entity** with Chromium. Each version of Puppeteer bundles a specific version of Chromium – **the only** version it is guaranteed to work with. + +This is not an artificial constraint: A lot of work on Puppeteer is actually taking place in the Chromium repository. Here’s a typical story: + +- A Puppeteer bug is reported: https://github.com/puppeteer/puppeteer/issues/2709 +- It turned out this is an issue with the DevTools protocol, so we’re fixing it in Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/1102154 +- Once the upstream fix is landed, we roll updated Chromium into Puppeteer: https://github.com/puppeteer/puppeteer/pull/2769 + +However, oftentimes it is desirable to use Puppeteer with the official Google Chrome rather than Chromium. For this to work, you should install a `puppeteer-core` version that corresponds to the Chrome version. + +For example, in order to drive Chrome 71 with puppeteer-core, use `chrome-71` npm tag: + +```bash +npm install puppeteer-core@chrome-71 +``` + +#### Q: Which Chromium version does Puppeteer use? + +Look for the `chromium` entry in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts). To find the corresponding Chromium commit and version number, search for the revision prefixed by an `r` in [OmahaProxy](https://omahaproxy.appspot.com/)'s "Find Releases" section. + +#### Q: Which Firefox version does Puppeteer use? + +Since Firefox support is experimental, Puppeteer downloads the latest [Firefox Nightly](https://wiki.mozilla.org/Nightly) when the `PUPPETEER_PRODUCT` environment variable is set to `firefox`. That's also why the value of `firefox` in [revisions.ts](https://github.com/puppeteer/puppeteer/blob/main/src/revisions.ts) is `latest` -- Puppeteer isn't tied to a particular Firefox version. + +To fetch Firefox Nightly as part of Puppeteer installation: + +```bash +PUPPETEER_PRODUCT=firefox npm i puppeteer +# or "yarn add puppeteer" +``` + +#### Q: What’s considered a “Navigation”? + +From Puppeteer’s standpoint, **“navigation” is anything that changes a page’s URL**. +Aside from regular navigation where the browser hits the network to fetch a new document from the web server, this includes [anchor navigations](https://www.w3.org/TR/html5/single-page.html#scroll-to-fragid) and [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) usage. + +With this definition of “navigation,” **Puppeteer works seamlessly with single-page applications.** + +#### Q: What’s the difference between a “trusted" and "untrusted" input event? + +In browsers, input events could be divided into two big groups: trusted vs. untrusted. + +- **Trusted events**: events generated by users interacting with the page, e.g. using a mouse or keyboard. +- **Untrusted event**: events generated by Web APIs, e.g. `document.createEvent` or `element.click()` methods. + +Websites can distinguish between these two groups: + +- using an [`Event.isTrusted`](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) event flag +- sniffing for accompanying events. For example, every trusted `'click'` event is preceded by `'mousedown'` and `'mouseup'` events. + +For automation purposes it’s important to generate trusted events. **All input events generated with Puppeteer are trusted and fire proper accompanying events.** If, for some reason, one needs an untrusted event, it’s always possible to hop into a page context with `page.evaluate` and generate a fake event: + +```js +await page.evaluate(() => { + document.querySelector('button[type=submit]').click(); +}); +``` + +#### Q: What features does Puppeteer not support? + +You may find that Puppeteer does not behave as expected when controlling pages that incorporate audio and video. (For example, [video playback/screenshots is likely to fail](https://github.com/puppeteer/puppeteer/issues/291).) There are two reasons for this: + +- Puppeteer is bundled with Chromium — not Chrome — and so by default, it inherits all of [Chromium's media-related limitations](https://www.chromium.org/audio-video). This means that Puppeteer does not support licensed formats such as AAC or H.264. (However, it is possible to force Puppeteer to use a separately-installed version Chrome instead of Chromium via the [`executablePath` option to `puppeteer.launch`](https://github.com/puppeteer/puppeteer/blob/v10.0.0/docs/api.md#puppeteerlaunchoptions). You should only use this configuration if you need an official release of Chrome that supports these media formats.) +- Since Puppeteer (in all configurations) controls a desktop version of Chromium/Chrome, features that are only supported by the mobile version of Chrome are not supported. This means that Puppeteer [does not support HTTP Live Streaming (HLS)](https://caniuse.com/#feat=http-live-streaming). + +#### Q: I am having trouble installing / running Puppeteer in my test environment. Where should I look for help? + +We have a [troubleshooting](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) guide for various operating systems that lists the required dependencies. + +#### Q: How do I try/test a prerelease version of Puppeteer? + +You can check out this repo or install the latest prerelease from npm: + +```bash +npm i --save puppeteer@next +``` + +Please note that prerelease may be unstable and contain bugs. + +#### Q: I have more questions! Where do I ask? + +There are many ways to get help on Puppeteer: + +- [bugtracker](https://github.com/puppeteer/puppeteer/issues) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/puppeteer) + +Make sure to search these channels before posting your question. + + diff --git a/website/static/.nojekyll b/website/static/.nojekyll new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/website/static/img/docusaurus.png b/website/static/img/docusaurus.png new file mode 100644 index 0000000000000..f458149e3c8f5 Binary files /dev/null and b/website/static/img/docusaurus.png differ diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico new file mode 100644 index 0000000000000..c01d54bcd39a5 Binary files /dev/null and b/website/static/img/favicon.ico differ diff --git a/website/static/img/logo.svg b/website/static/img/logo.svg new file mode 100644 index 0000000000000..9db6d0d066e3d --- /dev/null +++ b/website/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/img/tutorial/docsVersionDropdown.png b/website/static/img/tutorial/docsVersionDropdown.png new file mode 100644 index 0000000000000..ff1cbe68893d2 Binary files /dev/null and b/website/static/img/tutorial/docsVersionDropdown.png differ diff --git a/website/static/img/tutorial/localeDropdown.png b/website/static/img/tutorial/localeDropdown.png new file mode 100644 index 0000000000000..d7163f9675249 Binary files /dev/null and b/website/static/img/tutorial/localeDropdown.png differ diff --git a/website/static/img/undraw_docusaurus_mountain.svg b/website/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000000000..431cef2f7fece --- /dev/null +++ b/website/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/undraw_docusaurus_react.svg b/website/static/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000000000..e417050433381 --- /dev/null +++ b/website/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/undraw_docusaurus_tree.svg b/website/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000000000..a05cc03dda90f --- /dev/null +++ b/website/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1 @@ +docu_tree \ No newline at end of file diff --git a/website/versioned_docs/version-10.0.0/index.md b/website/versioned_docs/version-10.0.0/index.md new file mode 100644 index 0000000000000..ee99080bb5206 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [puppeteer](./puppeteer.md) | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.accessibility.md b/website/versioned_docs/version-10.0.0/puppeteer.accessibility.md new file mode 100644 index 0000000000000..45f0550e40fe5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.accessibility.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Accessibility](./puppeteer.accessibility.md) + +## Accessibility class + +The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or [switches](https://en.wikipedia.org/wiki/Switch_access). + +Signature: + +```typescript +export declare class Accessibility +``` + +## Remarks + +Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might have wildly different output. + +Blink - Chrome's rendering engine - has a concept of "accessibility tree", which is then translated into different platform-specific APIs. Accessibility namespace gives users access to the Blink Accessibility Tree. + +Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by assistive technologies themselves. By default, Puppeteer tries to approximate this filtering, exposing only the "interesting" nodes of the tree. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `Accessibility` class. + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [snapshot(options)](./puppeteer.accessibility.snapshot.md) | | Captures the current state of the accessibility tree. The returned object represents the root accessible node of the page. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.accessibility.snapshot.md b/website/versioned_docs/version-10.0.0/puppeteer.accessibility.snapshot.md new file mode 100644 index 0000000000000..5304955f2ddeb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.accessibility.snapshot.md @@ -0,0 +1,61 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Accessibility](./puppeteer.accessibility.md) > [snapshot](./puppeteer.accessibility.snapshot.md) + +## Accessibility.snapshot() method + +Captures the current state of the accessibility tree. The returned object represents the root accessible node of the page. + +Signature: + +```typescript +snapshot(options?: SnapshotOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | [SnapshotOptions](./puppeteer.snapshotoptions.md) | | + +Returns: + +Promise<[SerializedAXNode](./puppeteer.serializedaxnode.md)> + +An AXNode object representing the snapshot. + +## Remarks + +\*\*NOTE\*\* The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers. Puppeteer will discard them as well for an easier to process tree, unless `interestingOnly` is set to `false`. + +## Example 1 + +An example of dumping the entire accessibility tree: + +```js +const snapshot = await page.accessibility.snapshot(); +console.log(snapshot); + +``` + +## Example 2 + +An example of logging the focused node's name: + +```js +const snapshot = await page.accessibility.snapshot(); +const node = findFocusedNode(snapshot); +console.log(node && node.name); + +function findFocusedNode(node) { + if (node.focused) + return node; + for (const child of node.children || []) { + const foundNode = findFocusedNode(child); + return foundNode; + } + return null; +} + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.actionresult.md b/website/versioned_docs/version-10.0.0/puppeteer.actionresult.md new file mode 100644 index 0000000000000..e8e6334dace98 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.actionresult.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ActionResult](./puppeteer.actionresult.md) + +## ActionResult type + + +Signature: + +```typescript +export declare type ActionResult = 'continue' | 'abort' | 'respond'; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.height.md b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.height.md new file mode 100644 index 0000000000000..9fb33326555d9 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.height.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoundingBox](./puppeteer.boundingbox.md) > [height](./puppeteer.boundingbox.height.md) + +## BoundingBox.height property + +the height of the element in pixels. + +Signature: + +```typescript +height: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.md b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.md new file mode 100644 index 0000000000000..3ff8637291869 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoundingBox](./puppeteer.boundingbox.md) + +## BoundingBox interface + + +Signature: + +```typescript +export interface BoundingBox +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [height](./puppeteer.boundingbox.height.md) | number | the height of the element in pixels. | +| [width](./puppeteer.boundingbox.width.md) | number | the width of the element in pixels. | +| [x](./puppeteer.boundingbox.x.md) | number | the x coordinate of the element in pixels. | +| [y](./puppeteer.boundingbox.y.md) | number | the y coordinate of the element in pixels. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.width.md b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.width.md new file mode 100644 index 0000000000000..f0fc401e4c945 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.width.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoundingBox](./puppeteer.boundingbox.md) > [width](./puppeteer.boundingbox.width.md) + +## BoundingBox.width property + +the width of the element in pixels. + +Signature: + +```typescript +width: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.x.md b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.x.md new file mode 100644 index 0000000000000..b64a4bdb3c8e4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.x.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoundingBox](./puppeteer.boundingbox.md) > [x](./puppeteer.boundingbox.x.md) + +## BoundingBox.x property + +the x coordinate of the element in pixels. + +Signature: + +```typescript +x: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.y.md b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.y.md new file mode 100644 index 0000000000000..73194ab629914 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boundingbox.y.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoundingBox](./puppeteer.boundingbox.md) > [y](./puppeteer.boundingbox.y.md) + +## BoundingBox.y property + +the y coordinate of the element in pixels. + +Signature: + +```typescript +y: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.border.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.border.md new file mode 100644 index 0000000000000..7c0d7c2b9300e --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.border.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [border](./puppeteer.boxmodel.border.md) + +## BoxModel.border property + +Signature: + +```typescript +border: Array<{ + x: number; + y: number; + }>; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.content.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.content.md new file mode 100644 index 0000000000000..1abc3e8598edc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.content.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [content](./puppeteer.boxmodel.content.md) + +## BoxModel.content property + +Signature: + +```typescript +content: Array<{ + x: number; + y: number; + }>; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.height.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.height.md new file mode 100644 index 0000000000000..c28cb8bf53c36 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.height.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [height](./puppeteer.boxmodel.height.md) + +## BoxModel.height property + +Signature: + +```typescript +height: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.margin.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.margin.md new file mode 100644 index 0000000000000..876a8d089f808 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.margin.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [margin](./puppeteer.boxmodel.margin.md) + +## BoxModel.margin property + +Signature: + +```typescript +margin: Array<{ + x: number; + y: number; + }>; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.md new file mode 100644 index 0000000000000..eadbcab83e2ff --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) + +## BoxModel interface + + +Signature: + +```typescript +export interface BoxModel +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [border](./puppeteer.boxmodel.border.md) | Array<{ x: number; y: number; }> | | +| [content](./puppeteer.boxmodel.content.md) | Array<{ x: number; y: number; }> | | +| [height](./puppeteer.boxmodel.height.md) | number | | +| [margin](./puppeteer.boxmodel.margin.md) | Array<{ x: number; y: number; }> | | +| [padding](./puppeteer.boxmodel.padding.md) | Array<{ x: number; y: number; }> | | +| [width](./puppeteer.boxmodel.width.md) | number | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.padding.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.padding.md new file mode 100644 index 0000000000000..11761bb412d6d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.padding.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [padding](./puppeteer.boxmodel.padding.md) + +## BoxModel.padding property + +Signature: + +```typescript +padding: Array<{ + x: number; + y: number; + }>; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.width.md b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.width.md new file mode 100644 index 0000000000000..4e9cfb3239d59 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.boxmodel.width.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BoxModel](./puppeteer.boxmodel.md) > [width](./puppeteer.boxmodel.width.md) + +## BoxModel.width property + +Signature: + +```typescript +width: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.browsercontexts.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.browsercontexts.md new file mode 100644 index 0000000000000..4ec2e04d917ae --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.browsercontexts.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [browserContexts](./puppeteer.browser.browsercontexts.md) + +## Browser.browserContexts() method + +Returns an array of all open browser contexts. In a newly created browser, this will return a single instance of [BrowserContext](./puppeteer.browsercontext.md). + +Signature: + +```typescript +browserContexts(): BrowserContext[]; +``` +Returns: + +[BrowserContext](./puppeteer.browsercontext.md)\[\] + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.close.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.close.md new file mode 100644 index 0000000000000..725492710b023 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.close.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [close](./puppeteer.browser.close.md) + +## Browser.close() method + +Closes Chromium and all of its pages (if any were opened). The [Browser](./puppeteer.browser.md) object itself is considered to be disposed and cannot be used anymore. + +Signature: + +```typescript +close(): Promise; +``` +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.createincognitobrowsercontext.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.createincognitobrowsercontext.md new file mode 100644 index 0000000000000..8bd4f9ebed3e5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.createincognitobrowsercontext.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [createIncognitoBrowserContext](./puppeteer.browser.createincognitobrowsercontext.md) + +## Browser.createIncognitoBrowserContext() method + +Creates a new incognito browser context. This won't share cookies/cache with other browser contexts. + +Signature: + +```typescript +createIncognitoBrowserContext(): Promise; +``` +Returns: + +Promise<[BrowserContext](./puppeteer.browsercontext.md)> + +## Example + + +```js +(async () => { + const browser = await puppeteer.launch(); + // Create a new incognito browser context. + const context = await browser.createIncognitoBrowserContext(); + // Create a new page in a pristine context. + const page = await context.newPage(); + // Do stuff + await page.goto('https://example.com'); +})(); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.defaultbrowsercontext.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.defaultbrowsercontext.md new file mode 100644 index 0000000000000..92fd82c81744c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.defaultbrowsercontext.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [defaultBrowserContext](./puppeteer.browser.defaultbrowsercontext.md) + +## Browser.defaultBrowserContext() method + +Returns the default browser context. The default browser context cannot be closed. + +Signature: + +```typescript +defaultBrowserContext(): BrowserContext; +``` +Returns: + +[BrowserContext](./puppeteer.browsercontext.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.disconnect.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.disconnect.md new file mode 100644 index 0000000000000..2b4a3f9f80fea --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.disconnect.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [disconnect](./puppeteer.browser.disconnect.md) + +## Browser.disconnect() method + +Disconnects Puppeteer from the browser, but leaves the Chromium process running. After calling `disconnect`, the [Browser](./puppeteer.browser.md) object is considered disposed and cannot be used anymore. + +Signature: + +```typescript +disconnect(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.isconnected.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.isconnected.md new file mode 100644 index 0000000000000..756998990e567 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.isconnected.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [isConnected](./puppeteer.browser.isconnected.md) + +## Browser.isConnected() method + +Indicates that the browser is connected. + +Signature: + +```typescript +isConnected(): boolean; +``` +Returns: + +boolean + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.md new file mode 100644 index 0000000000000..9d001d205df1e --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.md @@ -0,0 +1,79 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) + +## Browser class + +A Browser is created when Puppeteer connects to a Chromium instance, either through [PuppeteerNode.launch()](./puppeteer.puppeteernode.launch.md) or [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). + +Signature: + +```typescript +export declare class Browser extends EventEmitter +``` +Extends: [EventEmitter](./puppeteer.eventemitter.md) + +## Remarks + +The Browser class extends from Puppeteer's [EventEmitter](./puppeteer.eventemitter.md) class and will emit various events which are documented in the [BrowserEmittedEvents](./puppeteer.browseremittedevents.md) enum. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `Browser` class. + +## Example 1 + +An example of using a [Browser](./puppeteer.browser.md) to create a [Page](./puppeteer.page.md): + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await browser.close(); +})(); + +``` + +## Example 2 + +An example of disconnecting from and reconnecting to a [Browser](./puppeteer.browser.md): + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + // Store the endpoint to be able to reconnect to Chromium + const browserWSEndpoint = browser.wsEndpoint(); + // Disconnect puppeteer from Chromium + browser.disconnect(); + + // Use the endpoint to reestablish a connection + const browser2 = await puppeteer.connect({browserWSEndpoint}); + // Close Chromium + await browser2.close(); +})(); + +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [browserContexts()](./puppeteer.browser.browsercontexts.md) | | Returns an array of all open browser contexts. In a newly created browser, this will return a single instance of [BrowserContext](./puppeteer.browsercontext.md). | +| [close()](./puppeteer.browser.close.md) | | Closes Chromium and all of its pages (if any were opened). The [Browser](./puppeteer.browser.md) object itself is considered to be disposed and cannot be used anymore. | +| [createIncognitoBrowserContext()](./puppeteer.browser.createincognitobrowsercontext.md) | | Creates a new incognito browser context. This won't share cookies/cache with other browser contexts. | +| [defaultBrowserContext()](./puppeteer.browser.defaultbrowsercontext.md) | | Returns the default browser context. The default browser context cannot be closed. | +| [disconnect()](./puppeteer.browser.disconnect.md) | | Disconnects Puppeteer from the browser, but leaves the Chromium process running. After calling disconnect, the [Browser](./puppeteer.browser.md) object is considered disposed and cannot be used anymore. | +| [isConnected()](./puppeteer.browser.isconnected.md) | | Indicates that the browser is connected. | +| [newPage()](./puppeteer.browser.newpage.md) | | Promise which resolves to a new [Page](./puppeteer.page.md) object. The Page is created in a default browser context. | +| [pages()](./puppeteer.browser.pages.md) | | An array of all open pages inside the Browser. | +| [process()](./puppeteer.browser.process.md) | | The spawned browser process. Returns null if the browser instance was created with [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). | +| [target()](./puppeteer.browser.target.md) | | The target associated with the browser. | +| [targets()](./puppeteer.browser.targets.md) | | All active targets inside the Browser. In case of multiple browser contexts, returns an array with all the targets in all browser contexts. | +| [userAgent()](./puppeteer.browser.useragent.md) | | The browser's original user agent. Pages can override the browser user agent with [Page.setUserAgent()](./puppeteer.page.setuseragent.md). | +| [version()](./puppeteer.browser.version.md) | | A string representing the browser name and version. | +| [waitForTarget(predicate, options)](./puppeteer.browser.waitfortarget.md) | | Searches for a target in all browser contexts. | +| [wsEndpoint()](./puppeteer.browser.wsendpoint.md) | | The browser websocket endpoint which can be used as an argument to [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.newpage.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.newpage.md new file mode 100644 index 0000000000000..7a4b47ef1fb8a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.newpage.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [newPage](./puppeteer.browser.newpage.md) + +## Browser.newPage() method + +Promise which resolves to a new [Page](./puppeteer.page.md) object. The Page is created in a default browser context. + +Signature: + +```typescript +newPage(): Promise; +``` +Returns: + +Promise<[Page](./puppeteer.page.md)> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.pages.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.pages.md new file mode 100644 index 0000000000000..f16533c0fc9ad --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.pages.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [pages](./puppeteer.browser.pages.md) + +## Browser.pages() method + +An array of all open pages inside the Browser. + +Signature: + +```typescript +pages(): Promise; +``` +Returns: + +Promise<[Page](./puppeteer.page.md)\[\]> + +## Remarks + +In case of multiple browser contexts, returns an array with all the pages in all browser contexts. Non-visible pages, such as `"background_page"`, will not be listed here. You can find them using [Target.page()](./puppeteer.target.page.md). + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.process.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.process.md new file mode 100644 index 0000000000000..5b34ad71cea2c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.process.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [process](./puppeteer.browser.process.md) + +## Browser.process() method + +The spawned browser process. Returns `null` if the browser instance was created with [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). + +Signature: + +```typescript +process(): ChildProcess | null; +``` +Returns: + +ChildProcess \| null + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.target.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.target.md new file mode 100644 index 0000000000000..0546aace49048 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.target.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [target](./puppeteer.browser.target.md) + +## Browser.target() method + +The target associated with the browser. + +Signature: + +```typescript +target(): Target; +``` +Returns: + +[Target](./puppeteer.target.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.targets.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.targets.md new file mode 100644 index 0000000000000..14eb2d4a17517 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.targets.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [targets](./puppeteer.browser.targets.md) + +## Browser.targets() method + +All active targets inside the Browser. In case of multiple browser contexts, returns an array with all the targets in all browser contexts. + +Signature: + +```typescript +targets(): Target[]; +``` +Returns: + +[Target](./puppeteer.target.md)\[\] + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.useragent.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.useragent.md new file mode 100644 index 0000000000000..ae1f1f5919d54 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.useragent.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [userAgent](./puppeteer.browser.useragent.md) + +## Browser.userAgent() method + +The browser's original user agent. Pages can override the browser user agent with [Page.setUserAgent()](./puppeteer.page.setuseragent.md). + +Signature: + +```typescript +userAgent(): Promise; +``` +Returns: + +Promise<string> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.version.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.version.md new file mode 100644 index 0000000000000..8e2f3e5b048c0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.version.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [version](./puppeteer.browser.version.md) + +## Browser.version() method + +A string representing the browser name and version. + +Signature: + +```typescript +version(): Promise; +``` +Returns: + +Promise<string> + +## Remarks + +For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`. + +The format of browser.version() might change with future releases of Chromium. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.waitfortarget.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.waitfortarget.md new file mode 100644 index 0000000000000..a607dcbc98196 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.waitfortarget.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [waitForTarget](./puppeteer.browser.waitfortarget.md) + +## Browser.waitForTarget() method + +Searches for a target in all browser contexts. + +Signature: + +```typescript +waitForTarget(predicate: (x: Target) => boolean, options?: WaitForTargetOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| predicate | (x: [Target](./puppeteer.target.md)) => boolean | A function to be run for every target. | +| options | [WaitForTargetOptions](./puppeteer.waitfortargetoptions.md) | | + +Returns: + +Promise<[Target](./puppeteer.target.md)> + +The first target found that matches the `predicate` function. + +## Example + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browser.waitForTarget(target => target.url() === 'https://www.example.com/'); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browser.wsendpoint.md b/website/versioned_docs/version-10.0.0/puppeteer.browser.wsendpoint.md new file mode 100644 index 0000000000000..751cad5c18075 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browser.wsendpoint.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Browser](./puppeteer.browser.md) > [wsEndpoint](./puppeteer.browser.wsendpoint.md) + +## Browser.wsEndpoint() method + +The browser websocket endpoint which can be used as an argument to [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). + +Signature: + +```typescript +wsEndpoint(): string; +``` +Returns: + +string + +The Browser websocket url. + +## Remarks + +The format is `ws://${host}:${port}/devtools/browser/`. + +You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. Learn more about the [devtools protocol](https://chromedevtools.github.io/devtools-protocol) and the [browser endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.defaultviewport.md b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.defaultviewport.md new file mode 100644 index 0000000000000..d610fd65fa4b2 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.defaultviewport.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) > [defaultViewport](./puppeteer.browserconnectoptions.defaultviewport.md) + +## BrowserConnectOptions.defaultViewport property + +Sets the viewport for each page. + +Signature: + +```typescript +defaultViewport?: Viewport | null; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.ignorehttpserrors.md b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.ignorehttpserrors.md new file mode 100644 index 0000000000000..627cbe2c95661 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.ignorehttpserrors.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) > [ignoreHTTPSErrors](./puppeteer.browserconnectoptions.ignorehttpserrors.md) + +## BrowserConnectOptions.ignoreHTTPSErrors property + +Whether to ignore HTTPS errors during navigation. + +Signature: + +```typescript +ignoreHTTPSErrors?: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.md b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.md new file mode 100644 index 0000000000000..c473f69d559d1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) + +## BrowserConnectOptions interface + +Generic browser options that can be passed when launching any browser or when connecting to an existing browser instance. + +Signature: + +```typescript +export interface BrowserConnectOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [defaultViewport?](./puppeteer.browserconnectoptions.defaultviewport.md) | [Viewport](./puppeteer.viewport.md) \| null | (Optional) Sets the viewport for each page. | +| [ignoreHTTPSErrors?](./puppeteer.browserconnectoptions.ignorehttpserrors.md) | boolean | (Optional) Whether to ignore HTTPS errors during navigation. | +| [slowMo?](./puppeteer.browserconnectoptions.slowmo.md) | number | (Optional) Slows down Puppeteer operations by the specified amount of milliseconds to aid debugging. | +| [targetFilter?](./puppeteer.browserconnectoptions.targetfilter.md) | [TargetFilterCallback](./puppeteer.targetfiltercallback.md) | (Optional) Callback to decide if Puppeteer should connect to a given target or not. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.slowmo.md b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.slowmo.md new file mode 100644 index 0000000000000..f0eacae09353c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.slowmo.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) > [slowMo](./puppeteer.browserconnectoptions.slowmo.md) + +## BrowserConnectOptions.slowMo property + +Slows down Puppeteer operations by the specified amount of milliseconds to aid debugging. + +Signature: + +```typescript +slowMo?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.targetfilter.md b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.targetfilter.md new file mode 100644 index 0000000000000..6c58b075a7877 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserconnectoptions.targetfilter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) > [targetFilter](./puppeteer.browserconnectoptions.targetfilter.md) + +## BrowserConnectOptions.targetFilter property + +Callback to decide if Puppeteer should connect to a given target or not. + +Signature: + +```typescript +targetFilter?: TargetFilterCallback; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.browser.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.browser.md new file mode 100644 index 0000000000000..85dadca18e3d9 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.browser.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [browser](./puppeteer.browsercontext.browser.md) + +## BrowserContext.browser() method + +The browser this browser context belongs to. + +Signature: + +```typescript +browser(): Browser; +``` +Returns: + +[Browser](./puppeteer.browser.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.clearpermissionoverrides.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.clearpermissionoverrides.md new file mode 100644 index 0000000000000..85872769eb80b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.clearpermissionoverrides.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [clearPermissionOverrides](./puppeteer.browsercontext.clearpermissionoverrides.md) + +## BrowserContext.clearPermissionOverrides() method + +Clears all permission overrides for the browser context. + +Signature: + +```typescript +clearPermissionOverrides(): Promise; +``` +Returns: + +Promise<void> + +## Example + + +```js +const context = browser.defaultBrowserContext(); +context.overridePermissions('https://example.com', ['clipboard-read']); +// do stuff .. +context.clearPermissionOverrides(); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.close.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.close.md new file mode 100644 index 0000000000000..7b6a9c75f2628 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.close.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [close](./puppeteer.browsercontext.close.md) + +## BrowserContext.close() method + +Closes the browser context. All the targets that belong to the browser context will be closed. + +Signature: + +```typescript +close(): Promise; +``` +Returns: + +Promise<void> + +## Remarks + +Only incognito browser contexts can be closed. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.isincognito.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.isincognito.md new file mode 100644 index 0000000000000..412665d0386b1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.isincognito.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [isIncognito](./puppeteer.browsercontext.isincognito.md) + +## BrowserContext.isIncognito() method + +Returns whether BrowserContext is incognito. The default browser context is the only non-incognito browser context. + +Signature: + +```typescript +isIncognito(): boolean; +``` +Returns: + +boolean + +## Remarks + +The default browser context cannot be closed. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.md new file mode 100644 index 0000000000000..3379d33dc6dd6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.md @@ -0,0 +1,54 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) + +## BrowserContext class + +BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has a single BrowserContext used by default. The method [Browser.newPage](./puppeteer.browser.newpage.md) creates a page in the default browser context. + +Signature: + +```typescript +export declare class BrowserContext extends EventEmitter +``` +Extends: [EventEmitter](./puppeteer.eventemitter.md) + +## Remarks + +The Browser class extends from Puppeteer's [EventEmitter](./puppeteer.eventemitter.md) class and will emit various events which are documented in the [BrowserContextEmittedEvents](./puppeteer.browsercontextemittedevents.md) enum. + +If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser context. + +Puppeteer allows creation of "incognito" browser contexts with [Browser.createIncognitoBrowserContext](./puppeteer.browser.createincognitobrowsercontext.md) method. "Incognito" browser contexts don't write any browsing data to disk. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `BrowserContext` class. + +## Example + + +```js +// Create a new incognito browser context +const context = await browser.createIncognitoBrowserContext(); +// Create a new page inside context. +const page = await context.newPage(); +// ... do stuff with page ... +await page.goto('https://example.com'); +// Dispose context once it's no longer needed. +await context.close(); + +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [browser()](./puppeteer.browsercontext.browser.md) | | The browser this browser context belongs to. | +| [clearPermissionOverrides()](./puppeteer.browsercontext.clearpermissionoverrides.md) | | Clears all permission overrides for the browser context. | +| [close()](./puppeteer.browsercontext.close.md) | | Closes the browser context. All the targets that belong to the browser context will be closed. | +| [isIncognito()](./puppeteer.browsercontext.isincognito.md) | | Returns whether BrowserContext is incognito. The default browser context is the only non-incognito browser context. | +| [newPage()](./puppeteer.browsercontext.newpage.md) | | Creates a new page in the browser context. | +| [overridePermissions(origin, permissions)](./puppeteer.browsercontext.overridepermissions.md) | | | +| [pages()](./puppeteer.browsercontext.pages.md) | | An array of all pages inside the browser context. | +| [targets()](./puppeteer.browsercontext.targets.md) | | An array of all active targets inside the browser context. | +| [waitForTarget(predicate, options)](./puppeteer.browsercontext.waitfortarget.md) | | This searches for a target in this specific browser context. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.newpage.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.newpage.md new file mode 100644 index 0000000000000..440e0e0121427 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.newpage.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [newPage](./puppeteer.browsercontext.newpage.md) + +## BrowserContext.newPage() method + +Creates a new page in the browser context. + +Signature: + +```typescript +newPage(): Promise; +``` +Returns: + +Promise<[Page](./puppeteer.page.md)> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.overridepermissions.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.overridepermissions.md new file mode 100644 index 0000000000000..a79725be37df2 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.overridepermissions.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [overridePermissions](./puppeteer.browsercontext.overridepermissions.md) + +## BrowserContext.overridePermissions() method + +Signature: + +```typescript +overridePermissions(origin: string, permissions: Permission[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| origin | string | The origin to grant permissions to, e.g. "https://example.com". | +| permissions | [Permission](./puppeteer.permission.md)\[\] | An array of permissions to grant. All permissions that are not listed here will be automatically denied. | + +Returns: + +Promise<void> + +## Example + + +```js +const context = browser.defaultBrowserContext(); +await context.overridePermissions('https://html5demos.com', ['geolocation']); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.pages.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.pages.md new file mode 100644 index 0000000000000..b56ec861383cf --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.pages.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [pages](./puppeteer.browsercontext.pages.md) + +## BrowserContext.pages() method + +An array of all pages inside the browser context. + +Signature: + +```typescript +pages(): Promise; +``` +Returns: + +Promise<[Page](./puppeteer.page.md)\[\]> + +Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [the target page](./puppeteer.target.page.md). + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.targets.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.targets.md new file mode 100644 index 0000000000000..b3bb64d88292b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.targets.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [targets](./puppeteer.browsercontext.targets.md) + +## BrowserContext.targets() method + +An array of all active targets inside the browser context. + +Signature: + +```typescript +targets(): Target[]; +``` +Returns: + +[Target](./puppeteer.target.md)\[\] + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.waitfortarget.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.waitfortarget.md new file mode 100644 index 0000000000000..46f3fee4493d7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontext.waitfortarget.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContext](./puppeteer.browsercontext.md) > [waitForTarget](./puppeteer.browsercontext.waitfortarget.md) + +## BrowserContext.waitForTarget() method + +This searches for a target in this specific browser context. + +Signature: + +```typescript +waitForTarget(predicate: (x: Target) => boolean, options?: { + timeout?: number; + }): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| predicate | (x: [Target](./puppeteer.target.md)) => boolean | A function to be run for every target | +| options | { timeout?: number; } | An object of options. Accepts a timout, which is the maximum wait time in milliseconds. Pass 0 to disable the timeout. Defaults to 30 seconds. | + +Returns: + +Promise<[Target](./puppeteer.target.md)> + +Promise which resolves to the first target found that matches the `predicate` function. + +## Example + +An example of finding a target for a page opened via `window.open`: + +```js +await page.evaluate(() => window.open('https://www.example.com/')); +const newWindowTarget = await browserContext.waitForTarget(target => target.url() === 'https://www.example.com/'); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browsercontextemittedevents.md b/website/versioned_docs/version-10.0.0/puppeteer.browsercontextemittedevents.md new file mode 100644 index 0000000000000..2302875de6584 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browsercontextemittedevents.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserContextEmittedEvents](./puppeteer.browsercontextemittedevents.md) + +## BrowserContextEmittedEvents enum + + +Signature: + +```typescript +export declare const enum BrowserContextEmittedEvents +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| TargetChanged | "targetchanged" | Emitted when the url of a target inside the browser context changes. Contains a [Target](./puppeteer.target.md) instance. | +| TargetCreated | "targetcreated" | Emitted when a target is created within the browser context, for example when a new page is opened by [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or by [browserContext.newPage](./puppeteer.browsercontext.newpage.md)Contains a [Target](./puppeteer.target.md) instance. | +| TargetDestroyed | "targetdestroyed" | Emitted when a target is destroyed within the browser context, for example when a page is closed. Contains a [Target](./puppeteer.target.md) instance. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browseremittedevents.md b/website/versioned_docs/version-10.0.0/puppeteer.browseremittedevents.md new file mode 100644 index 0000000000000..779d0ba75dff4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browseremittedevents.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserEmittedEvents](./puppeteer.browseremittedevents.md) + +## BrowserEmittedEvents enum + +All the events a [browser instance](./puppeteer.browser.md) may emit. + +Signature: + +```typescript +export declare const enum BrowserEmittedEvents +``` + +## Enumeration Members + +| Member | Value | Description | +| --- | --- | --- | +| Disconnected | "disconnected" | Emitted when Puppeteer gets disconnected from the Chromium instance. This might happen because of one of the following:- Chromium is closed or crashed- The [browser.disconnect](./puppeteer.browser.disconnect.md) method was called. | +| TargetChanged | "targetchanged" | Emitted when the url of a target changes. Contains a [Target](./puppeteer.target.md) instance. | +| TargetCreated | "targetcreated" | Emitted when a target is created, for example when a new page is opened by [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or by [browser.newPage](./puppeteer.browser.newpage.md)Contains a [Target](./puppeteer.target.md) instance. | +| TargetDestroyed | "targetdestroyed" | Emitted when a target is destroyed, for example when a page is closed. Contains a [Target](./puppeteer.target.md) instance. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.candownload.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.candownload.md new file mode 100644 index 0000000000000..11da9ded7e791 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.candownload.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [canDownload](./puppeteer.browserfetcher.candownload.md) + +## BrowserFetcher.canDownload() method + +Initiates a HEAD request to check if the revision is available. + +Signature: + +```typescript +canDownload(revision: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| revision | string | The revision to check availability for. | + +Returns: + +Promise<boolean> + +A promise that resolves to `true` if the revision could be downloaded from the host. + +## Remarks + +This method is affected by the current `product`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.download.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.download.md new file mode 100644 index 0000000000000..d026a7cfe1996 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.download.md @@ -0,0 +1,31 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [download](./puppeteer.browserfetcher.download.md) + +## BrowserFetcher.download() method + +Initiates a GET request to download the revision from the host. + +Signature: + +```typescript +download(revision: string, progressCallback?: (x: number, y: number) => void): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| revision | string | The revision to download. | +| progressCallback | (x: number, y: number) => void | A function that will be called with two arguments: How many bytes have been downloaded and the total number of bytes of the download. | + +Returns: + +Promise<[BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md)> + +A promise with revision information when the revision is downloaded and extracted. + +## Remarks + +This method is affected by the current `product`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.host.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.host.md new file mode 100644 index 0000000000000..863932bf0961a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.host.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [host](./puppeteer.browserfetcher.host.md) + +## BrowserFetcher.host() method + +Signature: + +```typescript +host(): string; +``` +Returns: + +string + +The download host being used. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.localrevisions.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.localrevisions.md new file mode 100644 index 0000000000000..e0a9bb93c02c4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.localrevisions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [localRevisions](./puppeteer.browserfetcher.localrevisions.md) + +## BrowserFetcher.localRevisions() method + +Signature: + +```typescript +localRevisions(): Promise; +``` +Returns: + +Promise<string\[\]> + +A promise with a list of all revision strings (for the current `product`) available locally on disk. + +## Remarks + +This method is affected by the current `product`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.md new file mode 100644 index 0000000000000..0b674fc12a54d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) + +## BrowserFetcher class + +BrowserFetcher can download and manage different versions of Chromium and Firefox. + +Signature: + +```typescript +export declare class BrowserFetcher +``` + +## Remarks + +BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from [omahaproxy.appspot.com](http://omahaproxy.appspot.com/). In the Firefox case, BrowserFetcher downloads Firefox Nightly and operates on version numbers such as `"75"`. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `BrowserFetcher` class. + +## Example + +An example of using BrowserFetcher to download a specific version of Chromium and running Puppeteer against it: + +```js +const browserFetcher = puppeteer.createBrowserFetcher(); +const revisionInfo = await browserFetcher.download('533271'); +const browser = await puppeteer.launch({executablePath: revisionInfo.executablePath}) + +``` +\*\*NOTE\*\* BrowserFetcher is not designed to work concurrently with other instances of BrowserFetcher that share the same downloads directory. + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [canDownload(revision)](./puppeteer.browserfetcher.candownload.md) | | Initiates a HEAD request to check if the revision is available. | +| [download(revision, progressCallback)](./puppeteer.browserfetcher.download.md) | | Initiates a GET request to download the revision from the host. | +| [host()](./puppeteer.browserfetcher.host.md) | | | +| [localRevisions()](./puppeteer.browserfetcher.localrevisions.md) | | | +| [platform()](./puppeteer.browserfetcher.platform.md) | | | +| [product()](./puppeteer.browserfetcher.product.md) | | | +| [remove(revision)](./puppeteer.browserfetcher.remove.md) | | | +| [revisionInfo(revision)](./puppeteer.browserfetcher.revisioninfo.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.platform.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.platform.md new file mode 100644 index 0000000000000..c02300923e82f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.platform.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [platform](./puppeteer.browserfetcher.platform.md) + +## BrowserFetcher.platform() method + +Signature: + +```typescript +platform(): Platform; +``` +Returns: + +[Platform](./puppeteer.platform.md) + +Returns the current `Platform`, which is one of `mac`, `linux`, `win32` or `win64`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.product.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.product.md new file mode 100644 index 0000000000000..42fe893feca33 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.product.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [product](./puppeteer.browserfetcher.product.md) + +## BrowserFetcher.product() method + +Signature: + +```typescript +product(): Product; +``` +Returns: + +[Product](./puppeteer.product.md) + +Returns the current `Product`, which is one of `chrome` or `firefox`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.remove.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.remove.md new file mode 100644 index 0000000000000..9577aada68346 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.remove.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [remove](./puppeteer.browserfetcher.remove.md) + +## BrowserFetcher.remove() method + +Signature: + +```typescript +remove(revision: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| revision | string | A revision to remove for the current product. | + +Returns: + +Promise<void> + +A promise that resolves when the revision has been removes or throws if the revision has not been downloaded. + +## Remarks + +This method is affected by the current `product`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.revisioninfo.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.revisioninfo.md new file mode 100644 index 0000000000000..6b1636b8ab978 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcher.revisioninfo.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcher](./puppeteer.browserfetcher.md) > [revisionInfo](./puppeteer.browserfetcher.revisioninfo.md) + +## BrowserFetcher.revisionInfo() method + +Signature: + +```typescript +revisionInfo(revision: string): BrowserFetcherRevisionInfo; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| revision | string | The revision to get info for. | + +Returns: + +[BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) + +The revision info for the given revision. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.host.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.host.md new file mode 100644 index 0000000000000..c30723c16dece --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.host.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) > [host](./puppeteer.browserfetcheroptions.host.md) + +## BrowserFetcherOptions.host property + +Signature: + +```typescript +host?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.md new file mode 100644 index 0000000000000..16d69d6482295 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) + +## BrowserFetcherOptions interface + + +Signature: + +```typescript +export interface BrowserFetcherOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [host?](./puppeteer.browserfetcheroptions.host.md) | string | (Optional) | +| [path?](./puppeteer.browserfetcheroptions.path.md) | string | (Optional) | +| [platform?](./puppeteer.browserfetcheroptions.platform.md) | [Platform](./puppeteer.platform.md) | (Optional) | +| [product?](./puppeteer.browserfetcheroptions.product.md) | string | (Optional) | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.path.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.path.md new file mode 100644 index 0000000000000..e6a4bcdb265a3 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.path.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) > [path](./puppeteer.browserfetcheroptions.path.md) + +## BrowserFetcherOptions.path property + +Signature: + +```typescript +path?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.platform.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.platform.md new file mode 100644 index 0000000000000..b37d0ead489ff --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.platform.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) > [platform](./puppeteer.browserfetcheroptions.platform.md) + +## BrowserFetcherOptions.platform property + +Signature: + +```typescript +platform?: Platform; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.product.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.product.md new file mode 100644 index 0000000000000..c5caa876ac867 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcheroptions.product.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherOptions](./puppeteer.browserfetcheroptions.md) > [product](./puppeteer.browserfetcheroptions.product.md) + +## BrowserFetcherOptions.product property + +Signature: + +```typescript +product?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.executablepath.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.executablepath.md new file mode 100644 index 0000000000000..22e8fbfb6f662 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.executablepath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [executablePath](./puppeteer.browserfetcherrevisioninfo.executablepath.md) + +## BrowserFetcherRevisionInfo.executablePath property + +Signature: + +```typescript +executablePath: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.folderpath.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.folderpath.md new file mode 100644 index 0000000000000..bc31e043e38e4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.folderpath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [folderPath](./puppeteer.browserfetcherrevisioninfo.folderpath.md) + +## BrowserFetcherRevisionInfo.folderPath property + +Signature: + +```typescript +folderPath: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.local.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.local.md new file mode 100644 index 0000000000000..f35fc71741283 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.local.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [local](./puppeteer.browserfetcherrevisioninfo.local.md) + +## BrowserFetcherRevisionInfo.local property + +Signature: + +```typescript +local: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.md new file mode 100644 index 0000000000000..e7dabf4dc05dc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) + +## BrowserFetcherRevisionInfo interface + + +Signature: + +```typescript +export interface BrowserFetcherRevisionInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [executablePath](./puppeteer.browserfetcherrevisioninfo.executablepath.md) | string | | +| [folderPath](./puppeteer.browserfetcherrevisioninfo.folderpath.md) | string | | +| [local](./puppeteer.browserfetcherrevisioninfo.local.md) | boolean | | +| [product](./puppeteer.browserfetcherrevisioninfo.product.md) | string | | +| [revision](./puppeteer.browserfetcherrevisioninfo.revision.md) | string | | +| [url](./puppeteer.browserfetcherrevisioninfo.url.md) | string | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.product.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.product.md new file mode 100644 index 0000000000000..99bdca573e00a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.product.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [product](./puppeteer.browserfetcherrevisioninfo.product.md) + +## BrowserFetcherRevisionInfo.product property + +Signature: + +```typescript +product: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.revision.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.revision.md new file mode 100644 index 0000000000000..e084a3327e3b4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.revision.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [revision](./puppeteer.browserfetcherrevisioninfo.revision.md) + +## BrowserFetcherRevisionInfo.revision property + +Signature: + +```typescript +revision: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.url.md b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.url.md new file mode 100644 index 0000000000000..eee943e9897d7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserfetcherrevisioninfo.url.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserFetcherRevisionInfo](./puppeteer.browserfetcherrevisioninfo.md) > [url](./puppeteer.browserfetcherrevisioninfo.url.md) + +## BrowserFetcherRevisionInfo.url property + +Signature: + +```typescript +url: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.args.md b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.args.md new file mode 100644 index 0000000000000..7fcc88938afd5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.args.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserLaunchArgumentOptions](./puppeteer.browserlaunchargumentoptions.md) > [args](./puppeteer.browserlaunchargumentoptions.args.md) + +## BrowserLaunchArgumentOptions.args property + +Additional command line arguments to pass to the browser instance. + +Signature: + +```typescript +args?: string[]; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.devtools.md b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.devtools.md new file mode 100644 index 0000000000000..0bc2fbf8018e0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.devtools.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserLaunchArgumentOptions](./puppeteer.browserlaunchargumentoptions.md) > [devtools](./puppeteer.browserlaunchargumentoptions.devtools.md) + +## BrowserLaunchArgumentOptions.devtools property + +Whether to auto-open a DevTools panel for each tab. If this is set to `true`, then `headless` will be set to `false` automatically. + +Signature: + +```typescript +devtools?: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.headless.md b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.headless.md new file mode 100644 index 0000000000000..cc881e3ce10bf --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.headless.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserLaunchArgumentOptions](./puppeteer.browserlaunchargumentoptions.md) > [headless](./puppeteer.browserlaunchargumentoptions.headless.md) + +## BrowserLaunchArgumentOptions.headless property + +Whether to run the browser in headless mode. + +Signature: + +```typescript +headless?: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.md b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.md new file mode 100644 index 0000000000000..ec48b01c55c27 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserLaunchArgumentOptions](./puppeteer.browserlaunchargumentoptions.md) + +## BrowserLaunchArgumentOptions interface + +Launcher options that only apply to Chrome. + +Signature: + +```typescript +export interface BrowserLaunchArgumentOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [args?](./puppeteer.browserlaunchargumentoptions.args.md) | string\[\] | (Optional) Additional command line arguments to pass to the browser instance. | +| [devtools?](./puppeteer.browserlaunchargumentoptions.devtools.md) | boolean | (Optional) Whether to auto-open a DevTools panel for each tab. If this is set to true, then headless will be set to false automatically. | +| [headless?](./puppeteer.browserlaunchargumentoptions.headless.md) | boolean | (Optional) Whether to run the browser in headless mode. | +| [userDataDir?](./puppeteer.browserlaunchargumentoptions.userdatadir.md) | string | (Optional) Path to a user data directory. [see the Chromium docs](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) for more info. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.userdatadir.md b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.userdatadir.md new file mode 100644 index 0000000000000..03badd8a30a5c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.browserlaunchargumentoptions.userdatadir.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [BrowserLaunchArgumentOptions](./puppeteer.browserlaunchargumentoptions.md) > [userDataDir](./puppeteer.browserlaunchargumentoptions.userdatadir.md) + +## BrowserLaunchArgumentOptions.userDataDir property + +Path to a user data directory. [see the Chromium docs](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md) for more info. + +Signature: + +```typescript +userDataDir?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.connection.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.connection.md new file mode 100644 index 0000000000000..e4ca749950ade --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.connection.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSession](./puppeteer.cdpsession.md) > [connection](./puppeteer.cdpsession.connection.md) + +## CDPSession.connection() method + +Signature: + +```typescript +connection(): Connection; +``` +Returns: + +[Connection](./puppeteer.connection.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.detach.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.detach.md new file mode 100644 index 0000000000000..b90f2c6210b0c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.detach.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSession](./puppeteer.cdpsession.md) > [detach](./puppeteer.cdpsession.detach.md) + +## CDPSession.detach() method + +Detaches the cdpSession from the target. Once detached, the cdpSession object won't emit any events and can't be used to send messages. + +Signature: + +```typescript +detach(): Promise; +``` +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.md new file mode 100644 index 0000000000000..4bac2335af9a8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSession](./puppeteer.cdpsession.md) + +## CDPSession class + +The `CDPSession` instances are used to talk raw Chrome Devtools Protocol. + +Signature: + +```typescript +export declare class CDPSession extends EventEmitter +``` +Extends: [EventEmitter](./puppeteer.eventemitter.md) + +## Remarks + +Protocol methods can be called with [CDPSession.send()](./puppeteer.cdpsession.send.md) method and protocol events can be subscribed to with `CDPSession.on` method. + +Useful links: [DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/) and [Getting Started with DevTools Protocol](https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md). + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `CDPSession` class. + +## Example + + +```js +const client = await page.target().createCDPSession(); +await client.send('Animation.enable'); +client.on('Animation.animationCreated', () => console.log('Animation created!')); +const response = await client.send('Animation.getPlaybackRate'); +console.log('playback rate is ' + response.playbackRate); +await client.send('Animation.setPlaybackRate', { + playbackRate: response.playbackRate / 2 +}); + +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [connection()](./puppeteer.cdpsession.connection.md) | | | +| [detach()](./puppeteer.cdpsession.detach.md) | | Detaches the cdpSession from the target. Once detached, the cdpSession object won't emit any events and can't be used to send messages. | +| [send(method, paramArgs)](./puppeteer.cdpsession.send.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.send.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.send.md new file mode 100644 index 0000000000000..317d01c999368 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsession.send.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSession](./puppeteer.cdpsession.md) > [send](./puppeteer.cdpsession.send.md) + +## CDPSession.send() method + +Signature: + +```typescript +send(method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType']): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| method | T | | +| paramArgs | ProtocolMapping.Commands\[T\]\['paramsType'\] | | + +Returns: + +Promise<ProtocolMapping.Commands\[T\]\['returnType'\]> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.error.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.error.md new file mode 100644 index 0000000000000..d4583855267c4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.error.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) > [error](./puppeteer.cdpsessiononmessageobject.error.md) + +## CDPSessionOnMessageObject.error property + +Signature: + +```typescript +error: { + message: string; + data: any; + }; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.id.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.id.md new file mode 100644 index 0000000000000..3e8d0a561e2eb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) > [id](./puppeteer.cdpsessiononmessageobject.id.md) + +## CDPSessionOnMessageObject.id property + +Signature: + +```typescript +id?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.md new file mode 100644 index 0000000000000..d7354b824901b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) + +## CDPSessionOnMessageObject interface + + +Signature: + +```typescript +export interface CDPSessionOnMessageObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./puppeteer.cdpsessiononmessageobject.error.md) | { message: string; data: any; } | | +| [id?](./puppeteer.cdpsessiononmessageobject.id.md) | number | (Optional) | +| [method](./puppeteer.cdpsessiononmessageobject.method.md) | string | | +| [params](./puppeteer.cdpsessiononmessageobject.params.md) | Record<string, unknown> | | +| [result?](./puppeteer.cdpsessiononmessageobject.result.md) | any | (Optional) | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.method.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.method.md new file mode 100644 index 0000000000000..c5217865b5699 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) > [method](./puppeteer.cdpsessiononmessageobject.method.md) + +## CDPSessionOnMessageObject.method property + +Signature: + +```typescript +method: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.params.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.params.md new file mode 100644 index 0000000000000..df6d129a94edd --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.params.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) > [params](./puppeteer.cdpsessiononmessageobject.params.md) + +## CDPSessionOnMessageObject.params property + +Signature: + +```typescript +params: Record; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.result.md b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.result.md new file mode 100644 index 0000000000000..c53f73437394f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.cdpsessiononmessageobject.result.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CDPSessionOnMessageObject](./puppeteer.cdpsessiononmessageobject.md) > [result](./puppeteer.cdpsessiononmessageobject.result.md) + +## CDPSessionOnMessageObject.result property + +Signature: + +```typescript +result?: any; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.chromereleasechannel.md b/website/versioned_docs/version-10.0.0/puppeteer.chromereleasechannel.md new file mode 100644 index 0000000000000..c4496235bb470 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.chromereleasechannel.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ChromeReleaseChannel](./puppeteer.chromereleasechannel.md) + +## ChromeReleaseChannel type + + +Signature: + +```typescript +export declare type ChromeReleaseChannel = 'chrome' | 'chrome-beta' | 'chrome-canary' | 'chrome-dev'; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.clearcustomqueryhandlers.md b/website/versioned_docs/version-10.0.0/puppeteer.clearcustomqueryhandlers.md new file mode 100644 index 0000000000000..f6bb8422e4ddd --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.clearcustomqueryhandlers.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [clearCustomQueryHandlers](./puppeteer.clearcustomqueryhandlers.md) + +## clearCustomQueryHandlers() function + +Clears all registered handlers. + +Signature: + +```typescript +export declare function clearCustomQueryHandlers(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.button.md b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.button.md new file mode 100644 index 0000000000000..2b9d0629954e0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.button.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ClickOptions](./puppeteer.clickoptions.md) > [button](./puppeteer.clickoptions.button.md) + +## ClickOptions.button property + +Signature: + +```typescript +button?: 'left' | 'right' | 'middle'; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.clickcount.md b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.clickcount.md new file mode 100644 index 0000000000000..942570833d429 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.clickcount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ClickOptions](./puppeteer.clickoptions.md) > [clickCount](./puppeteer.clickoptions.clickcount.md) + +## ClickOptions.clickCount property + +Signature: + +```typescript +clickCount?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.delay.md b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.delay.md new file mode 100644 index 0000000000000..c96b46a1a0954 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.delay.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ClickOptions](./puppeteer.clickoptions.md) > [delay](./puppeteer.clickoptions.delay.md) + +## ClickOptions.delay property + +Time to wait between `mousedown` and `mouseup` in milliseconds. + +Signature: + +```typescript +delay?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.md b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.md new file mode 100644 index 0000000000000..2ed03e2f64f0e --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.clickoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ClickOptions](./puppeteer.clickoptions.md) + +## ClickOptions interface + + +Signature: + +```typescript +export interface ClickOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [button?](./puppeteer.clickoptions.button.md) | 'left' \| 'right' \| 'middle' | (Optional) | +| [clickCount?](./puppeteer.clickoptions.clickcount.md) | number | (Optional) | +| [delay?](./puppeteer.clickoptions.delay.md) | number | (Optional) Time to wait between mousedown and mouseup in milliseconds. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.addlistener.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.addlistener.md new file mode 100644 index 0000000000000..60f0fe3d26b00 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.addlistener.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [addListener](./puppeteer.commoneventemitter.addlistener.md) + +## CommonEventEmitter.addListener() method + +Signature: + +```typescript +addListener(event: EventType, handler: Handler): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.emit.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.emit.md new file mode 100644 index 0000000000000..75e72f375d9b9 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.emit.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [emit](./puppeteer.commoneventemitter.emit.md) + +## CommonEventEmitter.emit() method + +Signature: + +```typescript +emit(event: EventType, eventData?: unknown): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| eventData | unknown | | + +Returns: + +boolean + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.listenercount.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.listenercount.md new file mode 100644 index 0000000000000..2e68d936cdc46 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.listenercount.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [listenerCount](./puppeteer.commoneventemitter.listenercount.md) + +## CommonEventEmitter.listenerCount() method + +Signature: + +```typescript +listenerCount(event: string): number; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | string | | + +Returns: + +number + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.md new file mode 100644 index 0000000000000..1ec59b257addb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) + +## CommonEventEmitter interface + + +Signature: + +```typescript +export interface CommonEventEmitter +``` + +## Methods + +| Method | Description | +| --- | --- | +| [addListener(event, handler)](./puppeteer.commoneventemitter.addlistener.md) | | +| [emit(event, eventData)](./puppeteer.commoneventemitter.emit.md) | | +| [listenerCount(event)](./puppeteer.commoneventemitter.listenercount.md) | | +| [off(event, handler)](./puppeteer.commoneventemitter.off.md) | | +| [on(event, handler)](./puppeteer.commoneventemitter.on.md) | | +| [once(event, handler)](./puppeteer.commoneventemitter.once.md) | | +| [removeAllListeners(event)](./puppeteer.commoneventemitter.removealllisteners.md) | | +| [removeListener(event, handler)](./puppeteer.commoneventemitter.removelistener.md) | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.off.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.off.md new file mode 100644 index 0000000000000..bf8f9437ccca5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.off.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [off](./puppeteer.commoneventemitter.off.md) + +## CommonEventEmitter.off() method + +Signature: + +```typescript +off(event: EventType, handler: Handler): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.on.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.on.md new file mode 100644 index 0000000000000..15193a28cf0cc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.on.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [on](./puppeteer.commoneventemitter.on.md) + +## CommonEventEmitter.on() method + +Signature: + +```typescript +on(event: EventType, handler: Handler): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.once.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.once.md new file mode 100644 index 0000000000000..9066bd6aa909b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.once.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [once](./puppeteer.commoneventemitter.once.md) + +## CommonEventEmitter.once() method + +Signature: + +```typescript +once(event: EventType, handler: Handler): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removealllisteners.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removealllisteners.md new file mode 100644 index 0000000000000..a52938a09e49b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removealllisteners.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [removeAllListeners](./puppeteer.commoneventemitter.removealllisteners.md) + +## CommonEventEmitter.removeAllListeners() method + +Signature: + +```typescript +removeAllListeners(event?: EventType): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removelistener.md b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removelistener.md new file mode 100644 index 0000000000000..9053478c28f73 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.commoneventemitter.removelistener.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CommonEventEmitter](./puppeteer.commoneventemitter.md) > [removeListener](./puppeteer.commoneventemitter.removelistener.md) + +## CommonEventEmitter.removeListener() method + +Signature: + +```typescript +removeListener(event: EventType, handler: Handler): CommonEventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[CommonEventEmitter](./puppeteer.commoneventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connect.md b/website/versioned_docs/version-10.0.0/puppeteer.connect.md new file mode 100644 index 0000000000000..57b13bca920f7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connect.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [connect](./puppeteer.connect.md) + +## connect() function + +This method attaches Puppeteer to an existing browser instance. + +Signature: + +```typescript +export declare function connect(options: ConnectOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | [ConnectOptions](./puppeteer.connectoptions.md) | Set of configurable options to set on the browser. | + +Returns: + +Promise<[Browser](./puppeteer.browser.md)> + +Promise which resolves to browser instance. + +## Remarks + + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._callbacks.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._callbacks.md new file mode 100644 index 0000000000000..69922d9e55b04 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._callbacks.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_callbacks](./puppeteer.connection._callbacks.md) + +## Connection.\_callbacks property + +Signature: + +```typescript +_callbacks: Map; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._closed.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._closed.md new file mode 100644 index 0000000000000..7c5aa0625ff71 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._closed.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_closed](./puppeteer.connection._closed.md) + +## Connection.\_closed property + +Signature: + +```typescript +_closed: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._constructor_.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._constructor_.md new file mode 100644 index 0000000000000..b5539c1239daa --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._constructor_.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [(constructor)](./puppeteer.connection._constructor_.md) + +## Connection.(constructor) + +Constructs a new instance of the `Connection` class + +Signature: + +```typescript +constructor(url: string, transport: ConnectionTransport, delay?: number); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| url | string | | +| transport | [ConnectionTransport](./puppeteer.connectiontransport.md) | | +| delay | number | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._delay.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._delay.md new file mode 100644 index 0000000000000..14839d6208248 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._delay.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_delay](./puppeteer.connection._delay.md) + +## Connection.\_delay property + +Signature: + +```typescript +_delay: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._lastid.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._lastid.md new file mode 100644 index 0000000000000..66c8641b67b89 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._lastid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_lastId](./puppeteer.connection._lastid.md) + +## Connection.\_lastId property + +Signature: + +```typescript +_lastId: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._onclose.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._onclose.md new file mode 100644 index 0000000000000..31d6153ec2859 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._onclose.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_onClose](./puppeteer.connection._onclose.md) + +## Connection.\_onClose() method + +Signature: + +```typescript +_onClose(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._onmessage.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._onmessage.md new file mode 100644 index 0000000000000..7dca08b4da3d3 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._onmessage.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_onMessage](./puppeteer.connection._onmessage.md) + +## Connection.\_onMessage() method + +Signature: + +```typescript +_onMessage(message: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | string | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._rawsend.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._rawsend.md new file mode 100644 index 0000000000000..6a83d3104b996 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._rawsend.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_rawSend](./puppeteer.connection._rawsend.md) + +## Connection.\_rawSend() method + +Signature: + +```typescript +_rawSend(message: Record): number; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | Record<string, unknown> | | + +Returns: + +number + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._sessions.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._sessions.md new file mode 100644 index 0000000000000..de2d752218d8f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._sessions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_sessions](./puppeteer.connection._sessions.md) + +## Connection.\_sessions property + +Signature: + +```typescript +_sessions: Map; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._transport.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._transport.md new file mode 100644 index 0000000000000..c8692e633ea9a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._transport.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_transport](./puppeteer.connection._transport.md) + +## Connection.\_transport property + +Signature: + +```typescript +_transport: ConnectionTransport; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection._url.md b/website/versioned_docs/version-10.0.0/puppeteer.connection._url.md new file mode 100644 index 0000000000000..fc5bce5806268 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection._url.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [\_url](./puppeteer.connection._url.md) + +## Connection.\_url property + +Signature: + +```typescript +_url: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.createsession.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.createsession.md new file mode 100644 index 0000000000000..19ccedbe794a4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.createsession.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [createSession](./puppeteer.connection.createsession.md) + +## Connection.createSession() method + +Signature: + +```typescript +createSession(targetInfo: Protocol.Target.TargetInfo): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| targetInfo | Protocol.Target.TargetInfo | The target info | + +Returns: + +Promise<[CDPSession](./puppeteer.cdpsession.md)> + +The CDP session that is created + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.dispose.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.dispose.md new file mode 100644 index 0000000000000..07d8e9b585a8a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.dispose.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [dispose](./puppeteer.connection.dispose.md) + +## Connection.dispose() method + +Signature: + +```typescript +dispose(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.fromsession.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.fromsession.md new file mode 100644 index 0000000000000..8663934a5fd47 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.fromsession.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [fromSession](./puppeteer.connection.fromsession.md) + +## Connection.fromSession() method + +Signature: + +```typescript +static fromSession(session: CDPSession): Connection; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| session | [CDPSession](./puppeteer.cdpsession.md) | | + +Returns: + +[Connection](./puppeteer.connection.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.md new file mode 100644 index 0000000000000..385faea9236df --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) + +## Connection class + + +Signature: + +```typescript +export declare class Connection extends EventEmitter +``` +Extends: [EventEmitter](./puppeteer.eventemitter.md) + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(url, transport, delay)](./puppeteer.connection._constructor_.md) | | Constructs a new instance of the Connection class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_callbacks](./puppeteer.connection._callbacks.md) | | Map<number, [ConnectionCallback](./puppeteer.connectioncallback.md)> | | +| [\_closed](./puppeteer.connection._closed.md) | | boolean | | +| [\_delay](./puppeteer.connection._delay.md) | | number | | +| [\_lastId](./puppeteer.connection._lastid.md) | | number | | +| [\_sessions](./puppeteer.connection._sessions.md) | | Map<string, [CDPSession](./puppeteer.cdpsession.md)> | | +| [\_transport](./puppeteer.connection._transport.md) | | [ConnectionTransport](./puppeteer.connectiontransport.md) | | +| [\_url](./puppeteer.connection._url.md) | | string | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [\_onClose()](./puppeteer.connection._onclose.md) | | | +| [\_onMessage(message)](./puppeteer.connection._onmessage.md) | | | +| [\_rawSend(message)](./puppeteer.connection._rawsend.md) | | | +| [createSession(targetInfo)](./puppeteer.connection.createsession.md) | | | +| [dispose()](./puppeteer.connection.dispose.md) | | | +| [fromSession(session)](./puppeteer.connection.fromsession.md) | static | | +| [send(method, paramArgs)](./puppeteer.connection.send.md) | | | +| [session(sessionId)](./puppeteer.connection.session.md) | | | +| [url()](./puppeteer.connection.url.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.send.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.send.md new file mode 100644 index 0000000000000..b5c4185416e10 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.send.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [send](./puppeteer.connection.send.md) + +## Connection.send() method + +Signature: + +```typescript +send(method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType']): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| method | T | | +| paramArgs | ProtocolMapping.Commands\[T\]\['paramsType'\] | | + +Returns: + +Promise<ProtocolMapping.Commands\[T\]\['returnType'\]> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.session.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.session.md new file mode 100644 index 0000000000000..901679b7aab4f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.session.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [session](./puppeteer.connection.session.md) + +## Connection.session() method + +Signature: + +```typescript +session(sessionId: string): CDPSession | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| sessionId | string | The session id | + +Returns: + +[CDPSession](./puppeteer.cdpsession.md) \| null + +The current CDP session if it exists + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connection.url.md b/website/versioned_docs/version-10.0.0/puppeteer.connection.url.md new file mode 100644 index 0000000000000..90bd6c6155ac8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connection.url.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Connection](./puppeteer.connection.md) > [url](./puppeteer.connection.url.md) + +## Connection.url() method + +Signature: + +```typescript +url(): string; +``` +Returns: + +string + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.error.md b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.error.md new file mode 100644 index 0000000000000..e5488d211fcbc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionCallback](./puppeteer.connectioncallback.md) > [error](./puppeteer.connectioncallback.error.md) + +## ConnectionCallback.error property + +Signature: + +```typescript +error: Error; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.md b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.md new file mode 100644 index 0000000000000..ca5120494456e --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionCallback](./puppeteer.connectioncallback.md) + +## ConnectionCallback interface + + +Signature: + +```typescript +export interface ConnectionCallback +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./puppeteer.connectioncallback.error.md) | Error | | +| [method](./puppeteer.connectioncallback.method.md) | string | | +| [reject](./puppeteer.connectioncallback.reject.md) | Function | | +| [resolve](./puppeteer.connectioncallback.resolve.md) | Function | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.method.md b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.method.md new file mode 100644 index 0000000000000..1cb9db85245df --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionCallback](./puppeteer.connectioncallback.md) > [method](./puppeteer.connectioncallback.method.md) + +## ConnectionCallback.method property + +Signature: + +```typescript +method: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.reject.md b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.reject.md new file mode 100644 index 0000000000000..2810ebcbe28a1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.reject.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionCallback](./puppeteer.connectioncallback.md) > [reject](./puppeteer.connectioncallback.reject.md) + +## ConnectionCallback.reject property + +Signature: + +```typescript +reject: Function; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.resolve.md b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.resolve.md new file mode 100644 index 0000000000000..41dd33698e9ed --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectioncallback.resolve.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionCallback](./puppeteer.connectioncallback.md) > [resolve](./puppeteer.connectioncallback.resolve.md) + +## ConnectionCallback.resolve property + +Signature: + +```typescript +resolve: Function; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.close.md b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.close.md new file mode 100644 index 0000000000000..8985544adf2b2 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.close.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionTransport](./puppeteer.connectiontransport.md) > [close](./puppeteer.connectiontransport.close.md) + +## ConnectionTransport.close() method + +Signature: + +```typescript +close(): any; +``` +Returns: + +any + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.md b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.md new file mode 100644 index 0000000000000..9c918538ce6e7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionTransport](./puppeteer.connectiontransport.md) + +## ConnectionTransport interface + + +Signature: + +```typescript +export interface ConnectionTransport +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [onclose?](./puppeteer.connectiontransport.onclose.md) | () => void | (Optional) | +| [onmessage?](./puppeteer.connectiontransport.onmessage.md) | (message: string) => void | (Optional) | + +## Methods + +| Method | Description | +| --- | --- | +| [close()](./puppeteer.connectiontransport.close.md) | | +| [send(string)](./puppeteer.connectiontransport.send.md) | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onclose.md b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onclose.md new file mode 100644 index 0000000000000..7c87797c4cadb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onclose.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionTransport](./puppeteer.connectiontransport.md) > [onclose](./puppeteer.connectiontransport.onclose.md) + +## ConnectionTransport.onclose property + +Signature: + +```typescript +onclose?: () => void; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onmessage.md b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onmessage.md new file mode 100644 index 0000000000000..7af1a876c772f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.onmessage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionTransport](./puppeteer.connectiontransport.md) > [onmessage](./puppeteer.connectiontransport.onmessage.md) + +## ConnectionTransport.onmessage property + +Signature: + +```typescript +onmessage?: (message: string) => void; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.send.md b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.send.md new file mode 100644 index 0000000000000..0a87f13a8c9ea --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectiontransport.send.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectionTransport](./puppeteer.connectiontransport.md) > [send](./puppeteer.connectiontransport.send.md) + +## ConnectionTransport.send() method + +Signature: + +```typescript +send(string: any): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| string | any | | + +Returns: + +any + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserurl.md b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserurl.md new file mode 100644 index 0000000000000..987e5214302f1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectOptions](./puppeteer.connectoptions.md) > [browserURL](./puppeteer.connectoptions.browserurl.md) + +## ConnectOptions.browserURL property + +Signature: + +```typescript +browserURL?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserwsendpoint.md b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserwsendpoint.md new file mode 100644 index 0000000000000..2b79ea3c5b723 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.browserwsendpoint.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectOptions](./puppeteer.connectoptions.md) > [browserWSEndpoint](./puppeteer.connectoptions.browserwsendpoint.md) + +## ConnectOptions.browserWSEndpoint property + +Signature: + +```typescript +browserWSEndpoint?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.md b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.md new file mode 100644 index 0000000000000..4c465dda01ba0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectOptions](./puppeteer.connectoptions.md) + +## ConnectOptions interface + + +Signature: + +```typescript +export interface ConnectOptions extends BrowserConnectOptions +``` +Extends: [BrowserConnectOptions](./puppeteer.browserconnectoptions.md) + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [browserURL?](./puppeteer.connectoptions.browserurl.md) | string | (Optional) | +| [browserWSEndpoint?](./puppeteer.connectoptions.browserwsendpoint.md) | string | (Optional) | +| [product?](./puppeteer.connectoptions.product.md) | [Product](./puppeteer.product.md) | (Optional) | +| [transport?](./puppeteer.connectoptions.transport.md) | [ConnectionTransport](./puppeteer.connectiontransport.md) | (Optional) | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.product.md b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.product.md new file mode 100644 index 0000000000000..b828ce7ba05eb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.product.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectOptions](./puppeteer.connectoptions.md) > [product](./puppeteer.connectoptions.product.md) + +## ConnectOptions.product property + +Signature: + +```typescript +product?: Product; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.transport.md b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.transport.md new file mode 100644 index 0000000000000..50a40a1e14827 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.connectoptions.transport.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConnectOptions](./puppeteer.connectoptions.md) > [transport](./puppeteer.connectoptions.transport.md) + +## ConnectOptions.transport property + +Signature: + +```typescript +transport?: ConnectionTransport; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage._constructor_.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage._constructor_.md new file mode 100644 index 0000000000000..11614f1bac14c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage._constructor_.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [(constructor)](./puppeteer.consolemessage._constructor_.md) + +## ConsoleMessage.(constructor) + +Constructs a new instance of the `ConsoleMessage` class + +Signature: + +```typescript +constructor(type: ConsoleMessageType, text: string, args: JSHandle[], stackTraceLocations: ConsoleMessageLocation[]); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | [ConsoleMessageType](./puppeteer.consolemessagetype.md) | | +| text | string | | +| args | [JSHandle](./puppeteer.jshandle.md)\[\] | | +| stackTraceLocations | [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md)\[\] | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.args.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.args.md new file mode 100644 index 0000000000000..a346e18d4f2de --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.args.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [args](./puppeteer.consolemessage.args.md) + +## ConsoleMessage.args() method + +Signature: + +```typescript +args(): JSHandle[]; +``` +Returns: + +[JSHandle](./puppeteer.jshandle.md)\[\] + +An array of arguments passed to the console. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.location.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.location.md new file mode 100644 index 0000000000000..1eac0154d58a1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.location.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [location](./puppeteer.consolemessage.location.md) + +## ConsoleMessage.location() method + +Signature: + +```typescript +location(): ConsoleMessageLocation; +``` +Returns: + +[ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) + +The location of the console message. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.md new file mode 100644 index 0000000000000..e56e5bd20930f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) + +## ConsoleMessage class + +ConsoleMessage objects are dispatched by page via the 'console' event. + +Signature: + +```typescript +export declare class ConsoleMessage +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(type, text, args, stackTraceLocations)](./puppeteer.consolemessage._constructor_.md) | | Constructs a new instance of the ConsoleMessage class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [args()](./puppeteer.consolemessage.args.md) | | | +| [location()](./puppeteer.consolemessage.location.md) | | | +| [stackTrace()](./puppeteer.consolemessage.stacktrace.md) | | | +| [text()](./puppeteer.consolemessage.text.md) | | | +| [type()](./puppeteer.consolemessage.type.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.stacktrace.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.stacktrace.md new file mode 100644 index 0000000000000..b326e6c7bb6b6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.stacktrace.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [stackTrace](./puppeteer.consolemessage.stacktrace.md) + +## ConsoleMessage.stackTrace() method + +Signature: + +```typescript +stackTrace(): ConsoleMessageLocation[]; +``` +Returns: + +[ConsoleMessageLocation](./puppeteer.consolemessagelocation.md)\[\] + +The array of locations on the stack of the console message. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.text.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.text.md new file mode 100644 index 0000000000000..6b4edb5ca63fe --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.text.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [text](./puppeteer.consolemessage.text.md) + +## ConsoleMessage.text() method + +Signature: + +```typescript +text(): string; +``` +Returns: + +string + +The text of the console message. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.type.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.type.md new file mode 100644 index 0000000000000..685b61957c70d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessage.type.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessage](./puppeteer.consolemessage.md) > [type](./puppeteer.consolemessage.type.md) + +## ConsoleMessage.type() method + +Signature: + +```typescript +type(): ConsoleMessageType; +``` +Returns: + +[ConsoleMessageType](./puppeteer.consolemessagetype.md) + +The type of the console message. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.columnnumber.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.columnnumber.md new file mode 100644 index 0000000000000..23a6b3d20a078 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.columnnumber.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) > [columnNumber](./puppeteer.consolemessagelocation.columnnumber.md) + +## ConsoleMessageLocation.columnNumber property + +0-based column number in the resource if known or `undefined` otherwise. + +Signature: + +```typescript +columnNumber?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.linenumber.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.linenumber.md new file mode 100644 index 0000000000000..cdc1fd4579231 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.linenumber.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) > [lineNumber](./puppeteer.consolemessagelocation.linenumber.md) + +## ConsoleMessageLocation.lineNumber property + +0-based line number in the resource if known or `undefined` otherwise. + +Signature: + +```typescript +lineNumber?: number; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.md new file mode 100644 index 0000000000000..b6f032160a651 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) + +## ConsoleMessageLocation interface + + +Signature: + +```typescript +export interface ConsoleMessageLocation +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [columnNumber?](./puppeteer.consolemessagelocation.columnnumber.md) | number | (Optional) 0-based column number in the resource if known or undefined otherwise. | +| [lineNumber?](./puppeteer.consolemessagelocation.linenumber.md) | number | (Optional) 0-based line number in the resource if known or undefined otherwise. | +| [url?](./puppeteer.consolemessagelocation.url.md) | string | (Optional) URL of the resource if known or undefined otherwise. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.url.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.url.md new file mode 100644 index 0000000000000..fe30226eb6ac4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagelocation.url.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessageLocation](./puppeteer.consolemessagelocation.md) > [url](./puppeteer.consolemessagelocation.url.md) + +## ConsoleMessageLocation.url property + +URL of the resource if known or `undefined` otherwise. + +Signature: + +```typescript +url?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.consolemessagetype.md b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagetype.md new file mode 100644 index 0000000000000..d9a3e883d7de5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.consolemessagetype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ConsoleMessageType](./puppeteer.consolemessagetype.md) + +## ConsoleMessageType type + +The supported types for console messages. + +Signature: + +```typescript +export declare type ConsoleMessageType = 'log' | 'debug' | 'info' | 'error' | 'warning' | 'dir' | 'dirxml' | 'table' | 'trace' | 'clear' | 'startGroup' | 'startGroupCollapsed' | 'endGroup' | 'assert' | 'profile' | 'profileEnd' | 'count' | 'timeEnd' | 'verbose'; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.headers.md b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.headers.md new file mode 100644 index 0000000000000..85a062bb0b3eb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.headers.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) > [headers](./puppeteer.continuerequestoverrides.headers.md) + +## ContinueRequestOverrides.headers property + +Signature: + +```typescript +headers?: Record; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.md b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.md new file mode 100644 index 0000000000000..38e7a2decdbe6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) + +## ContinueRequestOverrides interface + + +Signature: + +```typescript +export interface ContinueRequestOverrides +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers?](./puppeteer.continuerequestoverrides.headers.md) | Record<string, string> | (Optional) | +| [method?](./puppeteer.continuerequestoverrides.method.md) | string | (Optional) | +| [postData?](./puppeteer.continuerequestoverrides.postdata.md) | string | (Optional) | +| [url?](./puppeteer.continuerequestoverrides.url.md) | string | (Optional) If set, the request URL will change. This is not a redirect. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.method.md b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.method.md new file mode 100644 index 0000000000000..ef17b396e1be6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.method.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) > [method](./puppeteer.continuerequestoverrides.method.md) + +## ContinueRequestOverrides.method property + +Signature: + +```typescript +method?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.postdata.md b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.postdata.md new file mode 100644 index 0000000000000..7d88853284c7b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.postdata.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) > [postData](./puppeteer.continuerequestoverrides.postdata.md) + +## ContinueRequestOverrides.postData property + +Signature: + +```typescript +postData?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.url.md b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.url.md new file mode 100644 index 0000000000000..53ec6d6d87073 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.continuerequestoverrides.url.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ContinueRequestOverrides](./puppeteer.continuerequestoverrides.md) > [url](./puppeteer.continuerequestoverrides.url.md) + +## ContinueRequestOverrides.url property + +If set, the request URL will change. This is not a redirect. + +Signature: + +```typescript +url?: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage._constructor_.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage._constructor_.md new file mode 100644 index 0000000000000..727461d119001 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [(constructor)](./puppeteer.coverage._constructor_.md) + +## Coverage.(constructor) + +Constructs a new instance of the `Coverage` class + +Signature: + +```typescript +constructor(client: CDPSession); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| client | [CDPSession](./puppeteer.cdpsession.md) | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage.md new file mode 100644 index 0000000000000..a5a1a57bbfc4a --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage.md @@ -0,0 +1,62 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) + +## Coverage class + +The Coverage class provides methods to gathers information about parts of JavaScript and CSS that were used by the page. + +Signature: + +```typescript +export declare class Coverage +``` + +## Remarks + +To output coverage in a form consumable by [Istanbul](https://github.com/istanbuljs), see [puppeteer-to-istanbul](https://github.com/istanbuljs/puppeteer-to-istanbul). + +## Example + +An example of using JavaScript and CSS coverage to get percentage of initially executed code: + +```js +// Enable both JavaScript and CSS coverage +await Promise.all([ + page.coverage.startJSCoverage(), + page.coverage.startCSSCoverage() +]); +// Navigate to page +await page.goto('https://example.com'); +// Disable both JavaScript and CSS coverage +const [jsCoverage, cssCoverage] = await Promise.all([ + page.coverage.stopJSCoverage(), + page.coverage.stopCSSCoverage(), +]); +let totalBytes = 0; +let usedBytes = 0; +const coverage = [...jsCoverage, ...cssCoverage]; +for (const entry of coverage) { + totalBytes += entry.text.length; + for (const range of entry.ranges) + usedBytes += range.end - range.start - 1; +} +console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`); + +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(client)](./puppeteer.coverage._constructor_.md) | | Constructs a new instance of the Coverage class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [startCSSCoverage(options)](./puppeteer.coverage.startcsscoverage.md) | | | +| [startJSCoverage(options)](./puppeteer.coverage.startjscoverage.md) | | | +| [stopCSSCoverage()](./puppeteer.coverage.stopcsscoverage.md) | | | +| [stopJSCoverage()](./puppeteer.coverage.stopjscoverage.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage.startcsscoverage.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage.startcsscoverage.md new file mode 100644 index 0000000000000..2a5996d097be2 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage.startcsscoverage.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [startCSSCoverage](./puppeteer.coverage.startcsscoverage.md) + +## Coverage.startCSSCoverage() method + +Signature: + +```typescript +startCSSCoverage(options?: CSSCoverageOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) | Set of configurable options for coverage, defaults to resetOnNavigation : true | + +Returns: + +Promise<void> + +Promise that resolves when coverage is started. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage.startjscoverage.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage.startjscoverage.md new file mode 100644 index 0000000000000..b4e195f3036f8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage.startjscoverage.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [startJSCoverage](./puppeteer.coverage.startjscoverage.md) + +## Coverage.startJSCoverage() method + +Signature: + +```typescript +startJSCoverage(options?: JSCoverageOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | [JSCoverageOptions](./puppeteer.jscoverageoptions.md) | Set of configurable options for coverage defaults to resetOnNavigation : true, reportAnonymousScripts : false | + +Returns: + +Promise<void> + +Promise that resolves when coverage is started. + +## Remarks + +Anonymous scripts are ones that don't have an associated url. These are scripts that are dynamically created on the page using `eval` or `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous scripts will have `__puppeteer_evaluation_script__` as their URL. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopcsscoverage.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopcsscoverage.md new file mode 100644 index 0000000000000..e926fd00624e1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopcsscoverage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [stopCSSCoverage](./puppeteer.coverage.stopcsscoverage.md) + +## Coverage.stopCSSCoverage() method + +Signature: + +```typescript +stopCSSCoverage(): Promise; +``` +Returns: + +Promise<[CoverageEntry](./puppeteer.coverageentry.md)\[\]> + +Promise that resolves to the array of coverage reports for all stylesheets. + +## Remarks + +CSS Coverage doesn't include dynamically injected style tags without sourceURLs. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopjscoverage.md b/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopjscoverage.md new file mode 100644 index 0000000000000..9a66b035e48f6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverage.stopjscoverage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Coverage](./puppeteer.coverage.md) > [stopJSCoverage](./puppeteer.coverage.stopjscoverage.md) + +## Coverage.stopJSCoverage() method + +Signature: + +```typescript +stopJSCoverage(): Promise; +``` +Returns: + +Promise<[CoverageEntry](./puppeteer.coverageentry.md)\[\]> + +Promise that resolves to the array of coverage reports for all scripts. + +## Remarks + +JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.md b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.md new file mode 100644 index 0000000000000..2991f0d539fe9 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) + +## CoverageEntry interface + +The CoverageEntry class represents one entry of the coverage report. + +Signature: + +```typescript +export interface CoverageEntry +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [ranges](./puppeteer.coverageentry.ranges.md) | Array<{ start: number; end: number; }> | The covered range as start and end positions. | +| [text](./puppeteer.coverageentry.text.md) | string | The content of the style sheet or script. | +| [url](./puppeteer.coverageentry.url.md) | string | The URL of the style sheet or script. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.ranges.md b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.ranges.md new file mode 100644 index 0000000000000..8a0ad196125d4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.ranges.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [ranges](./puppeteer.coverageentry.ranges.md) + +## CoverageEntry.ranges property + +The covered range as start and end positions. + +Signature: + +```typescript +ranges: Array<{ + start: number; + end: number; + }>; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.text.md b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.text.md new file mode 100644 index 0000000000000..a5d3ddc629a66 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.text.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [text](./puppeteer.coverageentry.text.md) + +## CoverageEntry.text property + +The content of the style sheet or script. + +Signature: + +```typescript +text: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.url.md b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.url.md new file mode 100644 index 0000000000000..9f33d73053bb5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.coverageentry.url.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CoverageEntry](./puppeteer.coverageentry.md) > [url](./puppeteer.coverageentry.url.md) + +## CoverageEntry.url property + +The URL of the style sheet or script. + +Signature: + +```typescript +url: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.credentials.md b/website/versioned_docs/version-10.0.0/puppeteer.credentials.md new file mode 100644 index 0000000000000..106acd7596317 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.credentials.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Credentials](./puppeteer.credentials.md) + +## Credentials interface + + +Signature: + +```typescript +export interface Credentials +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [password](./puppeteer.credentials.password.md) | string | | +| [username](./puppeteer.credentials.username.md) | string | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.credentials.password.md b/website/versioned_docs/version-10.0.0/puppeteer.credentials.password.md new file mode 100644 index 0000000000000..988017ac966c1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.credentials.password.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Credentials](./puppeteer.credentials.md) > [password](./puppeteer.credentials.password.md) + +## Credentials.password property + +Signature: + +```typescript +password: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.credentials.username.md b/website/versioned_docs/version-10.0.0/puppeteer.credentials.username.md new file mode 100644 index 0000000000000..1cf2011cd6d43 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.credentials.username.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Credentials](./puppeteer.credentials.md) > [username](./puppeteer.credentials.username.md) + +## Credentials.username property + +Signature: + +```typescript +username: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._client.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._client.md new file mode 100644 index 0000000000000..1c0e553db1c67 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._client.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_client](./puppeteer.csscoverage._client.md) + +## CSSCoverage.\_client property + +Signature: + +```typescript +_client: CDPSession; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._constructor_.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._constructor_.md new file mode 100644 index 0000000000000..1511bc6704d9c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [(constructor)](./puppeteer.csscoverage._constructor_.md) + +## CSSCoverage.(constructor) + +Constructs a new instance of the `CSSCoverage` class + +Signature: + +```typescript +constructor(client: CDPSession); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| client | [CDPSession](./puppeteer.cdpsession.md) | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._enabled.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._enabled.md new file mode 100644 index 0000000000000..2bcf040238771 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._enabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_enabled](./puppeteer.csscoverage._enabled.md) + +## CSSCoverage.\_enabled property + +Signature: + +```typescript +_enabled: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._eventlisteners.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._eventlisteners.md new file mode 100644 index 0000000000000..6d37743663bc8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._eventlisteners.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_eventListeners](./puppeteer.csscoverage._eventlisteners.md) + +## CSSCoverage.\_eventListeners property + +Signature: + +```typescript +_eventListeners: PuppeteerEventListener[]; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onexecutioncontextscleared.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onexecutioncontextscleared.md new file mode 100644 index 0000000000000..70210cc2b837f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onexecutioncontextscleared.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_onExecutionContextsCleared](./puppeteer.csscoverage._onexecutioncontextscleared.md) + +## CSSCoverage.\_onExecutionContextsCleared() method + +Signature: + +```typescript +_onExecutionContextsCleared(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onstylesheet.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onstylesheet.md new file mode 100644 index 0000000000000..311e6e2fc2824 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._onstylesheet.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_onStyleSheet](./puppeteer.csscoverage._onstylesheet.md) + +## CSSCoverage.\_onStyleSheet() method + +Signature: + +```typescript +_onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | Protocol.CSS.StyleSheetAddedEvent | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._reportanonymousscripts.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._reportanonymousscripts.md new file mode 100644 index 0000000000000..d34910e683ed3 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._reportanonymousscripts.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_reportAnonymousScripts](./puppeteer.csscoverage._reportanonymousscripts.md) + +## CSSCoverage.\_reportAnonymousScripts property + +Signature: + +```typescript +_reportAnonymousScripts: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._resetonnavigation.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._resetonnavigation.md new file mode 100644 index 0000000000000..ecb994ca071b7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._resetonnavigation.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_resetOnNavigation](./puppeteer.csscoverage._resetonnavigation.md) + +## CSSCoverage.\_resetOnNavigation property + +Signature: + +```typescript +_resetOnNavigation: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheetsources.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheetsources.md new file mode 100644 index 0000000000000..5044bc3a39544 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheetsources.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_stylesheetSources](./puppeteer.csscoverage._stylesheetsources.md) + +## CSSCoverage.\_stylesheetSources property + +Signature: + +```typescript +_stylesheetSources: Map; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheeturls.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheeturls.md new file mode 100644 index 0000000000000..2ea0794bfa80e --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage._stylesheeturls.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [\_stylesheetURLs](./puppeteer.csscoverage._stylesheeturls.md) + +## CSSCoverage.\_stylesheetURLs property + +Signature: + +```typescript +_stylesheetURLs: Map; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.md new file mode 100644 index 0000000000000..f76249c2f1283 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.md @@ -0,0 +1,40 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) + +## CSSCoverage class + + +Signature: + +```typescript +export declare class CSSCoverage +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(client)](./puppeteer.csscoverage._constructor_.md) | | Constructs a new instance of the CSSCoverage class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_client](./puppeteer.csscoverage._client.md) | | [CDPSession](./puppeteer.cdpsession.md) | | +| [\_enabled](./puppeteer.csscoverage._enabled.md) | | boolean | | +| [\_eventListeners](./puppeteer.csscoverage._eventlisteners.md) | | [PuppeteerEventListener](./puppeteer.puppeteereventlistener.md)\[\] | | +| [\_reportAnonymousScripts](./puppeteer.csscoverage._reportanonymousscripts.md) | | boolean | | +| [\_resetOnNavigation](./puppeteer.csscoverage._resetonnavigation.md) | | boolean | | +| [\_stylesheetSources](./puppeteer.csscoverage._stylesheetsources.md) | | Map<string, string> | | +| [\_stylesheetURLs](./puppeteer.csscoverage._stylesheeturls.md) | | Map<string, string> | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [\_onExecutionContextsCleared()](./puppeteer.csscoverage._onexecutioncontextscleared.md) | | | +| [\_onStyleSheet(event)](./puppeteer.csscoverage._onstylesheet.md) | | | +| [start(options)](./puppeteer.csscoverage.start.md) | | | +| [stop()](./puppeteer.csscoverage.stop.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.start.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.start.md new file mode 100644 index 0000000000000..dc986e9465fef --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.start.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [start](./puppeteer.csscoverage.start.md) + +## CSSCoverage.start() method + +Signature: + +```typescript +start(options?: { + resetOnNavigation?: boolean; + }): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | { resetOnNavigation?: boolean; } | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.stop.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.stop.md new file mode 100644 index 0000000000000..d08b2c6945a51 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverage.stop.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverage](./puppeteer.csscoverage.md) > [stop](./puppeteer.csscoverage.stop.md) + +## CSSCoverage.stop() method + +Signature: + +```typescript +stop(): Promise; +``` +Returns: + +Promise<[CoverageEntry](./puppeteer.coverageentry.md)\[\]> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.md new file mode 100644 index 0000000000000..699d2f04a7532 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) + +## CSSCoverageOptions interface + +Set of configurable options for CSS coverage. + +Signature: + +```typescript +export interface CSSCoverageOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [resetOnNavigation?](./puppeteer.csscoverageoptions.resetonnavigation.md) | boolean | (Optional) Whether to reset coverage on every navigation. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.resetonnavigation.md b/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.resetonnavigation.md new file mode 100644 index 0000000000000..079ffe47c50d0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.csscoverageoptions.resetonnavigation.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CSSCoverageOptions](./puppeteer.csscoverageoptions.md) > [resetOnNavigation](./puppeteer.csscoverageoptions.resetonnavigation.md) + +## CSSCoverageOptions.resetOnNavigation property + +Whether to reset coverage on every navigation. + +Signature: + +```typescript +resetOnNavigation?: boolean; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customerror._constructor_.md b/website/versioned_docs/version-10.0.0/puppeteer.customerror._constructor_.md new file mode 100644 index 0000000000000..1d0a6b107b291 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customerror._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CustomError](./puppeteer.customerror.md) > [(constructor)](./puppeteer.customerror._constructor_.md) + +## CustomError.(constructor) + +Constructs a new instance of the `CustomError` class + +Signature: + +```typescript +constructor(message: string); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | string | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customerror.md b/website/versioned_docs/version-10.0.0/puppeteer.customerror.md new file mode 100644 index 0000000000000..6ff0b16151f02 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customerror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CustomError](./puppeteer.customerror.md) + +## CustomError class + + +Signature: + +```typescript +export declare class CustomError extends Error +``` +Extends: Error + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(message)](./puppeteer.customerror._constructor_.md) | | Constructs a new instance of the CustomError class | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.md b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.md new file mode 100644 index 0000000000000..6aa1aa93e719d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CustomQueryHandler](./puppeteer.customqueryhandler.md) + +## CustomQueryHandler interface + +Contains two functions `queryOne` and `queryAll` that can be [registered](./puppeteer.puppeteer.registercustomqueryhandler.md) as alternative querying strategies. The functions `queryOne` and `queryAll` are executed in the page context. `queryOne` should take an `Element` and a selector string as argument and return a single `Element` or `null` if no element is found. `queryAll` takes the same arguments but should instead return a `NodeListOf` or `Array` with all the elements that match the given query selector. + +Signature: + +```typescript +export interface CustomQueryHandler +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [queryAll?](./puppeteer.customqueryhandler.queryall.md) | (element: Element \| Document, selector: string) => Element\[\] \| NodeListOf<Element> | (Optional) | +| [queryOne?](./puppeteer.customqueryhandler.queryone.md) | (element: Element \| Document, selector: string) => Element \| null | (Optional) | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryall.md b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryall.md new file mode 100644 index 0000000000000..71b35ca8ca86f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryall.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CustomQueryHandler](./puppeteer.customqueryhandler.md) > [queryAll](./puppeteer.customqueryhandler.queryall.md) + +## CustomQueryHandler.queryAll property + +Signature: + +```typescript +queryAll?: (element: Element | Document, selector: string) => Element[] | NodeListOf; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryone.md b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryone.md new file mode 100644 index 0000000000000..7f00edadd0764 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandler.queryone.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [CustomQueryHandler](./puppeteer.customqueryhandler.md) > [queryOne](./puppeteer.customqueryhandler.queryone.md) + +## CustomQueryHandler.queryOne property + +Signature: + +```typescript +queryOne?: (element: Element | Document, selector: string) => Element | null; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandlernames.md b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandlernames.md new file mode 100644 index 0000000000000..d077b98b76f9c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.customqueryhandlernames.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [customQueryHandlerNames](./puppeteer.customqueryhandlernames.md) + +## customQueryHandlerNames() function + +Signature: + +```typescript +export declare function customQueryHandlerNames(): string[]; +``` +Returns: + +string\[\] + +a list with the names of all registered custom query handlers. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.device.md b/website/versioned_docs/version-10.0.0/puppeteer.device.md new file mode 100644 index 0000000000000..1382b587225fc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.device.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Device](./puppeteer.device.md) + +## Device interface + + +Signature: + +```typescript +export interface Device +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [name](./puppeteer.device.name.md) | string | | +| [userAgent](./puppeteer.device.useragent.md) | string | | +| [viewport](./puppeteer.device.viewport.md) | { width: number; height: number; deviceScaleFactor: number; isMobile: boolean; hasTouch: boolean; isLandscape: boolean; } | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.device.name.md b/website/versioned_docs/version-10.0.0/puppeteer.device.name.md new file mode 100644 index 0000000000000..6d044886b48bf --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.device.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Device](./puppeteer.device.md) > [name](./puppeteer.device.name.md) + +## Device.name property + +Signature: + +```typescript +name: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.device.useragent.md b/website/versioned_docs/version-10.0.0/puppeteer.device.useragent.md new file mode 100644 index 0000000000000..1d79e8f6093e4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.device.useragent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Device](./puppeteer.device.md) > [userAgent](./puppeteer.device.useragent.md) + +## Device.userAgent property + +Signature: + +```typescript +userAgent: string; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.device.viewport.md b/website/versioned_docs/version-10.0.0/puppeteer.device.viewport.md new file mode 100644 index 0000000000000..9e16753edc343 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.device.viewport.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Device](./puppeteer.device.md) > [viewport](./puppeteer.device.viewport.md) + +## Device.viewport property + +Signature: + +```typescript +viewport: { + width: number; + height: number; + deviceScaleFactor: number; + isMobile: boolean; + hasTouch: boolean; + isLandscape: boolean; + }; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.devices.md b/website/versioned_docs/version-10.0.0/puppeteer.devices.md new file mode 100644 index 0000000000000..97e3587397f80 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.devices.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [devices](./puppeteer.devices.md) + +## devices variable + +Signature: + +```typescript +devices: DevicesMap +``` + +## Remarks + +A list of devices to be used with `page.emulate(options)`. Actual list of devices can be found in [src/common/DeviceDescriptors.ts](https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts). + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.devicesmap.md b/website/versioned_docs/version-10.0.0/puppeteer.devicesmap.md new file mode 100644 index 0000000000000..fb1afe4a2ad4c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.devicesmap.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [DevicesMap](./puppeteer.devicesmap.md) + +## DevicesMap type + + +Signature: + +```typescript +export declare type DevicesMap = { + [name: string]: Device; +}; +``` +References: [Device](./puppeteer.device.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.accept.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.accept.md new file mode 100644 index 0000000000000..cf7386371f7be --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.accept.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) > [accept](./puppeteer.dialog.accept.md) + +## Dialog.accept() method + +Signature: + +```typescript +accept(promptText?: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| promptText | string | optional text that will be entered in the dialog prompt. Has no effect if the dialog's type is not prompt. | + +Returns: + +Promise<void> + +A promise that resolves when the dialog has been accepted. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.defaultvalue.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.defaultvalue.md new file mode 100644 index 0000000000000..fe83eecc105bb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.defaultvalue.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) > [defaultValue](./puppeteer.dialog.defaultvalue.md) + +## Dialog.defaultValue() method + +Signature: + +```typescript +defaultValue(): string; +``` +Returns: + +string + +The default value of the prompt, or an empty string if the dialog is not a `prompt`. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.dismiss.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.dismiss.md new file mode 100644 index 0000000000000..900c286305b2d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.dismiss.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) > [dismiss](./puppeteer.dialog.dismiss.md) + +## Dialog.dismiss() method + +Signature: + +```typescript +dismiss(): Promise; +``` +Returns: + +Promise<void> + +A promise which will resolve once the dialog has been dismissed + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.md new file mode 100644 index 0000000000000..13c9c277c6ae1 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.md @@ -0,0 +1,47 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) + +## Dialog class + +Dialog instances are dispatched by the [Page](./puppeteer.page.md) via the `dialog` event. + +Signature: + +```typescript +export declare class Dialog +``` + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `Dialog` class. + +## Example + + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + page.on('dialog', async dialog => { + console.log(dialog.message()); + await dialog.dismiss(); + await browser.close(); + }); + page.evaluate(() => alert('1')); +})(); + +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [accept(promptText)](./puppeteer.dialog.accept.md) | | | +| [defaultValue()](./puppeteer.dialog.defaultvalue.md) | | | +| [dismiss()](./puppeteer.dialog.dismiss.md) | | | +| [message()](./puppeteer.dialog.message.md) | | | +| [type()](./puppeteer.dialog.type.md) | | | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.message.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.message.md new file mode 100644 index 0000000000000..e725b67f68f15 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.message.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) > [message](./puppeteer.dialog.message.md) + +## Dialog.message() method + +Signature: + +```typescript +message(): string; +``` +Returns: + +string + +The message displayed in the dialog. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.dialog.type.md b/website/versioned_docs/version-10.0.0/puppeteer.dialog.type.md new file mode 100644 index 0000000000000..07e789bd326c4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.dialog.type.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Dialog](./puppeteer.dialog.md) > [type](./puppeteer.dialog.type.md) + +## Dialog.type() method + +Signature: + +```typescript +type(): Protocol.Page.DialogType; +``` +Returns: + +Protocol.Page.DialogType + +The type of the dialog. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._.md new file mode 100644 index 0000000000000..80479eebc6edc --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [$](./puppeteer.elementhandle._.md) + +## ElementHandle.$() method + +Runs `element.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. + +Signature: + +```typescript +$(selector: string): Promise | null>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | | + +Returns: + +Promise<[ElementHandle](./puppeteer.elementhandle.md)<T> \| null> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__.md new file mode 100644 index 0000000000000..06986b9d0f5c4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [$$](./puppeteer.elementhandle.__.md) + +## ElementHandle.$$() method + +Runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`. + +Signature: + +```typescript +$$(selector: string): Promise>>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | | + +Returns: + +Promise<Array<[ElementHandle](./puppeteer.elementhandle.md)<T>>> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__eval.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__eval.md new file mode 100644 index 0000000000000..a93ca0025efc0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.__eval.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [$$eval](./puppeteer.elementhandle.__eval.md) + +## ElementHandle.$$eval() method + +This method runs `document.querySelectorAll` within the element and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. + +If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the promise to resolve and return its value. + +Signature: + +```typescript +$$eval(selector: string, pageFunction: (elements: Element[], ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | | +| pageFunction | (elements: Element\[\], ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | + +Returns: + +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> + +## Example 1 + + +```html +
+
Hello!
+
Hi!
+
+ +``` + +## Example 2 + + +```js +const feedHandle = await page.$('.feed'); +expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))) + .toEqual(['Hello!', 'Hi!']); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._eval.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._eval.md new file mode 100644 index 0000000000000..9c05c4488d886 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._eval.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [$eval](./puppeteer.elementhandle._eval.md) + +## ElementHandle.$eval() method + +This method runs `document.querySelector` within the element and passes it as the first argument to `pageFunction`. If there's no element matching `selector`, the method throws an error. + +If `pageFunction` returns a Promise, then `frame.$eval` would wait for the promise to resolve and return its value. + +Signature: + +```typescript +$eval(selector: string, pageFunction: (element: Element, ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | | +| pageFunction | (element: Element, ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | + +Returns: + +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> + +## Example + + +```js +const tweetHandle = await page.$('.tweet'); +expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe('100'); +expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10'); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._x.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._x.md new file mode 100644 index 0000000000000..db94315bb944d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle._x.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [$x](./puppeteer.elementhandle._x.md) + +## ElementHandle.$x() method + +The method evaluates the XPath expression relative to the elementHandle. If there are no such elements, the method will resolve to an empty array. + +Signature: + +```typescript +$x(expression: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| expression | string | Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate) | + +Returns: + +Promise<[ElementHandle](./puppeteer.elementhandle.md)\[\]> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.aselement.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.aselement.md new file mode 100644 index 0000000000000..0df27995230f3 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.aselement.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [asElement](./puppeteer.elementhandle.aselement.md) + +## ElementHandle.asElement() method + +Signature: + +```typescript +asElement(): ElementHandle | null; +``` +Returns: + +[ElementHandle](./puppeteer.elementhandle.md)<ElementType> \| null + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boundingbox.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boundingbox.md new file mode 100644 index 0000000000000..eda3aaebded22 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boundingbox.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [boundingBox](./puppeteer.elementhandle.boundingbox.md) + +## ElementHandle.boundingBox() method + +This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible. + +Signature: + +```typescript +boundingBox(): Promise; +``` +Returns: + +Promise<[BoundingBox](./puppeteer.boundingbox.md) \| null> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boxmodel.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boxmodel.md new file mode 100644 index 0000000000000..0e1a6679fcb77 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.boxmodel.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [boxModel](./puppeteer.elementhandle.boxmodel.md) + +## ElementHandle.boxModel() method + +This method returns boxes of the element, or `null` if the element is not visible. + +Signature: + +```typescript +boxModel(): Promise; +``` +Returns: + +Promise<[BoxModel](./puppeteer.boxmodel.md) \| null> + +## Remarks + +Boxes are represented as an array of points; Each Point is an object `{x, y}`. Box points are sorted clock-wise. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.click.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.click.md new file mode 100644 index 0000000000000..23a2d94a11b89 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.click.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [click](./puppeteer.elementhandle.click.md) + +## ElementHandle.click() method + +This method scrolls element into view if needed, and then uses [Page.mouse](./puppeteer.page.mouse.md) to click in the center of the element. If the element is detached from DOM, the method throws an error. + +Signature: + +```typescript +click(options?: ClickOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | [ClickOptions](./puppeteer.clickoptions.md) | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.clickablepoint.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.clickablepoint.md new file mode 100644 index 0000000000000..b12f9217a1b9c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.clickablepoint.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [clickablePoint](./puppeteer.elementhandle.clickablepoint.md) + +## ElementHandle.clickablePoint() method + +Signature: + +```typescript +clickablePoint(): Promise; +``` +Returns: + +Promise<[Point](./puppeteer.point.md)> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.contentframe.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.contentframe.md new file mode 100644 index 0000000000000..186e012652ea9 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.contentframe.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [contentFrame](./puppeteer.elementhandle.contentframe.md) + +## ElementHandle.contentFrame() method + +Resolves to the content frame for element handles referencing iframe nodes, or null otherwise + +Signature: + +```typescript +contentFrame(): Promise; +``` +Returns: + +Promise<[Frame](./puppeteer.frame.md) \| null> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drag.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drag.md new file mode 100644 index 0000000000000..7e16df4e21e4b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drag.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [drag](./puppeteer.elementhandle.drag.md) + +## ElementHandle.drag() method + +This method creates and captures a dragevent from the element. + +Signature: + +```typescript +drag(target: Point): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| target | [Point](./puppeteer.point.md) | | + +Returns: + +Promise<Protocol.Input.DragData> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.draganddrop.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.draganddrop.md new file mode 100644 index 0000000000000..80c69427199d6 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.draganddrop.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [dragAndDrop](./puppeteer.elementhandle.draganddrop.md) + +## ElementHandle.dragAndDrop() method + +This method triggers a dragenter, dragover, and drop on the element. + +Signature: + +```typescript +dragAndDrop(target: ElementHandle, options?: { + delay: number; + }): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| target | [ElementHandle](./puppeteer.elementhandle.md) | | +| options | { delay: number; } | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragenter.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragenter.md new file mode 100644 index 0000000000000..f54d33b134f1f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragenter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [dragEnter](./puppeteer.elementhandle.dragenter.md) + +## ElementHandle.dragEnter() method + +This method creates a `dragenter` event on the element. + +Signature: + +```typescript +dragEnter(data?: Protocol.Input.DragData): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| data | Protocol.Input.DragData | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragover.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragover.md new file mode 100644 index 0000000000000..73566c1606296 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.dragover.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [dragOver](./puppeteer.elementhandle.dragover.md) + +## ElementHandle.dragOver() method + +This method creates a `dragover` event on the element. + +Signature: + +```typescript +dragOver(data?: Protocol.Input.DragData): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| data | Protocol.Input.DragData | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drop.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drop.md new file mode 100644 index 0000000000000..b6470e42a7d89 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.drop.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [drop](./puppeteer.elementhandle.drop.md) + +## ElementHandle.drop() method + +This method triggers a drop on the element. + +Signature: + +```typescript +drop(data?: Protocol.Input.DragData): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| data | Protocol.Input.DragData | | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.focus.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.focus.md new file mode 100644 index 0000000000000..4509b290a9036 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.focus.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [focus](./puppeteer.elementhandle.focus.md) + +## ElementHandle.focus() method + +Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element. + +Signature: + +```typescript +focus(): Promise; +``` +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.hover.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.hover.md new file mode 100644 index 0000000000000..30943ba1147ea --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.hover.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [hover](./puppeteer.elementhandle.hover.md) + +## ElementHandle.hover() method + +This method scrolls element into view if needed, and then uses [Page.mouse](./puppeteer.page.mouse.md) to hover over the center of the element. If the element is detached from DOM, the method throws an error. + +Signature: + +```typescript +hover(): Promise; +``` +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.isintersectingviewport.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.isintersectingviewport.md new file mode 100644 index 0000000000000..4621338b8c070 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.isintersectingviewport.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [isIntersectingViewport](./puppeteer.elementhandle.isintersectingviewport.md) + +## ElementHandle.isIntersectingViewport() method + +Resolves to true if the element is visible in the current viewport. + +Signature: + +```typescript +isIntersectingViewport(): Promise; +``` +Returns: + +Promise<boolean> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.md new file mode 100644 index 0000000000000..8670c0793373b --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.md @@ -0,0 +1,63 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) + +## ElementHandle class + +ElementHandle represents an in-page DOM element. + +Signature: + +```typescript +export declare class ElementHandle extends JSHandle +``` +Extends: [JSHandle](./puppeteer.jshandle.md)<ElementType> + +## Remarks + +ElementHandles can be created with the [Page.$()](./puppeteer.page._.md) method. + +```js +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + const hrefElement = await page.$('a'); + await hrefElement.click(); + // ... +})(); + +``` +ElementHandle prevents the DOM element from being garbage-collected unless the handle is [disposed](./puppeteer.jshandle.dispose.md). ElementHandles are auto-disposed when their origin frame gets navigated. + +ElementHandle instances can be used as arguments in [Page.$eval()](./puppeteer.page._eval.md) and [Page.evaluate()](./puppeteer.page.evaluate.md) methods. + +If you're using TypeScript, ElementHandle takes a generic argument that denotes the type of element the handle is holding within. For example, if you have a handle to a `` element matching `selector`, the method throws an error. + +Signature: + +```typescript +select(...values: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| values | string\[\] | Values of options to select. If the <select> has the multiple attribute, all values are considered, otherwise only the first one is taken into account. | + +Returns: + +Promise<string\[\]> + +## Example + + +```js +handle.select('blue'); // single selection +handle.select('red', 'green', 'blue'); // multiple selections + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.tap.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.tap.md new file mode 100644 index 0000000000000..a694242045e3f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.tap.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [tap](./puppeteer.elementhandle.tap.md) + +## ElementHandle.tap() method + +This method scrolls element into view if needed, and then uses [Touchscreen.tap()](./puppeteer.touchscreen.tap.md) to tap in the center of the element. If the element is detached from DOM, the method throws an error. + +Signature: + +```typescript +tap(): Promise; +``` +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.type.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.type.md new file mode 100644 index 0000000000000..3a64609b67ad5 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.type.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [type](./puppeteer.elementhandle.type.md) + +## ElementHandle.type() method + +Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. + +To press a special key, like `Control` or `ArrowDown`, use [ElementHandle.press()](./puppeteer.elementhandle.press.md). + +Signature: + +```typescript +type(text: string, options?: { + delay: number; + }): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| text | string | | +| options | { delay: number; } | | + +Returns: + +Promise<void> + +## Example 1 + + +```js +await elementHandle.type('Hello'); // Types instantly +await elementHandle.type('World', {delay: 100}); // Types slower, like a user + +``` + +## Example 2 + +An example of typing into a text field and then submitting the form: + +```js +const elementHandle = await page.$('input'); +await elementHandle.type('some text'); +await elementHandle.press('Enter'); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.uploadfile.md b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.uploadfile.md new file mode 100644 index 0000000000000..a4d1b4cd02ebe --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.elementhandle.uploadfile.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ElementHandle](./puppeteer.elementhandle.md) > [uploadFile](./puppeteer.elementhandle.uploadfile.md) + +## ElementHandle.uploadFile() method + +This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + +Signature: + +```typescript +uploadFile(...filePaths: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| filePaths | string\[\] | Sets the value of the file input to these paths. If some of the filePaths are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd) | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.errorcode.md b/website/versioned_docs/version-10.0.0/puppeteer.errorcode.md new file mode 100644 index 0000000000000..5ab270c54289d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.errorcode.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ErrorCode](./puppeteer.errorcode.md) + +## ErrorCode type + + +Signature: + +```typescript +export declare type ErrorCode = 'aborted' | 'accessdenied' | 'addressunreachable' | 'blockedbyclient' | 'blockedbyresponse' | 'connectionaborted' | 'connectionclosed' | 'connectionfailed' | 'connectionrefused' | 'connectionreset' | 'internetdisconnected' | 'namenotresolved' | 'timedout' | 'failed'; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.errors.md b/website/versioned_docs/version-10.0.0/puppeteer.errors.md new file mode 100644 index 0000000000000..15c99b8764d86 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.errors.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [errors](./puppeteer.errors.md) + +## errors variable + + +Signature: + +```typescript +errors: PuppeteerErrors +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.evaluatefn.md b/website/versioned_docs/version-10.0.0/puppeteer.evaluatefn.md new file mode 100644 index 0000000000000..23f80a3476061 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.evaluatefn.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EvaluateFn](./puppeteer.evaluatefn.md) + +## EvaluateFn type + + +Signature: + +```typescript +export declare type EvaluateFn = string | ((arg1: T, ...args: any[]) => any); +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.evaluatefnreturntype.md b/website/versioned_docs/version-10.0.0/puppeteer.evaluatefnreturntype.md new file mode 100644 index 0000000000000..6829af5ad0385 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.evaluatefnreturntype.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EvaluateFnReturnType](./puppeteer.evaluatefnreturntype.md) + +## EvaluateFnReturnType type + + +Signature: + +```typescript +export declare type EvaluateFnReturnType = T extends (...args: any[]) => infer R ? R : any; +``` +References: [EvaluateFn](./puppeteer.evaluatefn.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.evaluatehandlefn.md b/website/versioned_docs/version-10.0.0/puppeteer.evaluatehandlefn.md new file mode 100644 index 0000000000000..22191cfef5c83 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.evaluatehandlefn.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EvaluateHandleFn](./puppeteer.evaluatehandlefn.md) + +## EvaluateHandleFn type + + +Signature: + +```typescript +export declare type EvaluateHandleFn = string | ((...args: any[]) => any); +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.evaluation_script_url.md b/website/versioned_docs/version-10.0.0/puppeteer.evaluation_script_url.md new file mode 100644 index 0000000000000..ab5912e6c2e03 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.evaluation_script_url.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EVALUATION\_SCRIPT\_URL](./puppeteer.evaluation_script_url.md) + +## EVALUATION\_SCRIPT\_URL variable + + +Signature: + +```typescript +EVALUATION_SCRIPT_URL = "__puppeteer_evaluation_script__" +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.addlistener.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.addlistener.md new file mode 100644 index 0000000000000..d492b3d88960d --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.addlistener.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [addListener](./puppeteer.eventemitter.addlistener.md) + +## EventEmitter.addListener() method + +> Warning: This API is now obsolete. +> +> please use [EventEmitter.on()](./puppeteer.eventemitter.on.md) instead. +> + +Add an event listener. + +Signature: + +```typescript +addListener(event: EventType, handler: Handler): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.emit.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.emit.md new file mode 100644 index 0000000000000..1db43ac72701c --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.emit.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [emit](./puppeteer.eventemitter.emit.md) + +## EventEmitter.emit() method + +Emit an event and call any associated listeners. + +Signature: + +```typescript +emit(event: EventType, eventData?: unknown): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event you'd like to emit | +| eventData | unknown | any data you'd like to emit with the event | + +Returns: + +boolean + +`true` if there are any listeners, `false` if there are not. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.listenercount.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.listenercount.md new file mode 100644 index 0000000000000..6b6d7792b01eb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.listenercount.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [listenerCount](./puppeteer.eventemitter.listenercount.md) + +## EventEmitter.listenerCount() method + +Gets the number of listeners for a given event. + +Signature: + +```typescript +listenerCount(event: EventType): number; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event to get the listener count for | + +Returns: + +number + +the number of listeners bound to the given event + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.md new file mode 100644 index 0000000000000..634d9a569edf2 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.md @@ -0,0 +1,34 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) + +## EventEmitter class + +The EventEmitter class that many Puppeteer classes extend. + +Signature: + +```typescript +export declare class EventEmitter implements CommonEventEmitter +``` +Implements: [CommonEventEmitter](./puppeteer.commoneventemitter.md) + +## Remarks + +This allows you to listen to events that Puppeteer classes fire and act accordingly. Therefore you'll mostly use [on](./puppeteer.eventemitter.on.md) and [off](./puppeteer.eventemitter.off.md) to bind and unbind to event listeners. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `EventEmitter` class. + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [addListener(event, handler)](./puppeteer.eventemitter.addlistener.md) | | Add an event listener. | +| [emit(event, eventData)](./puppeteer.eventemitter.emit.md) | | Emit an event and call any associated listeners. | +| [listenerCount(event)](./puppeteer.eventemitter.listenercount.md) | | Gets the number of listeners for a given event. | +| [off(event, handler)](./puppeteer.eventemitter.off.md) | | Remove an event listener from firing. | +| [on(event, handler)](./puppeteer.eventemitter.on.md) | | Bind an event listener to fire when an event occurs. | +| [once(event, handler)](./puppeteer.eventemitter.once.md) | | Like on but the listener will only be fired once and then it will be removed. | +| [removeAllListeners(event)](./puppeteer.eventemitter.removealllisteners.md) | | Removes all listeners. If given an event argument, it will remove only listeners for that event. | +| [removeListener(event, handler)](./puppeteer.eventemitter.removelistener.md) | | Remove an event listener. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.off.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.off.md new file mode 100644 index 0000000000000..71578a24dcd91 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.off.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [off](./puppeteer.eventemitter.off.md) + +## EventEmitter.off() method + +Remove an event listener from firing. + +Signature: + +```typescript +off(event: EventType, handler: Handler): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event type you'd like to stop listening to. | +| handler | [Handler](./puppeteer.handler.md) | the function that should be removed. | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + +`this` to enable you to chain method calls. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.on.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.on.md new file mode 100644 index 0000000000000..d6bcf47a58c24 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.on.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [on](./puppeteer.eventemitter.on.md) + +## EventEmitter.on() method + +Bind an event listener to fire when an event occurs. + +Signature: + +```typescript +on(event: EventType, handler: Handler): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event type you'd like to listen to. Can be a string or symbol. | +| handler | [Handler](./puppeteer.handler.md) | the function to be called when the event occurs. | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + +`this` to enable you to chain method calls. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.once.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.once.md new file mode 100644 index 0000000000000..39cd7dd483ee4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.once.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [once](./puppeteer.eventemitter.once.md) + +## EventEmitter.once() method + +Like `on` but the listener will only be fired once and then it will be removed. + +Signature: + +```typescript +once(event: EventType, handler: Handler): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event you'd like to listen to | +| handler | [Handler](./puppeteer.handler.md) | the handler function to run when the event occurs | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + +`this` to enable you to chain method calls. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removealllisteners.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removealllisteners.md new file mode 100644 index 0000000000000..d03627c97070f --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removealllisteners.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [removeAllListeners](./puppeteer.eventemitter.removealllisteners.md) + +## EventEmitter.removeAllListeners() method + +Removes all listeners. If given an event argument, it will remove only listeners for that event. + +Signature: + +```typescript +removeAllListeners(event?: EventType): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | the event to remove listeners for. | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + +`this` to enable you to chain method calls. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removelistener.md b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removelistener.md new file mode 100644 index 0000000000000..c388eb33002d0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventemitter.removelistener.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventEmitter](./puppeteer.eventemitter.md) > [removeListener](./puppeteer.eventemitter.removelistener.md) + +## EventEmitter.removeListener() method + +> Warning: This API is now obsolete. +> +> please use [EventEmitter.off()](./puppeteer.eventemitter.off.md) instead. +> + +Remove an event listener. + +Signature: + +```typescript +removeListener(event: EventType, handler: Handler): EventEmitter; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| event | [EventType](./puppeteer.eventtype.md) | | +| handler | [Handler](./puppeteer.handler.md) | | + +Returns: + +[EventEmitter](./puppeteer.eventemitter.md) + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.eventtype.md b/website/versioned_docs/version-10.0.0/puppeteer.eventtype.md new file mode 100644 index 0000000000000..81a51014baaee --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.eventtype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [EventType](./puppeteer.eventtype.md) + +## EventType type + + +Signature: + +```typescript +export declare type EventType = string | symbol; +``` diff --git a/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluate.md b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluate.md new file mode 100644 index 0000000000000..e1e532814ab63 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluate.md @@ -0,0 +1,64 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ExecutionContext](./puppeteer.executioncontext.md) > [evaluate](./puppeteer.executioncontext.evaluate.md) + +## ExecutionContext.evaluate() method + +Signature: + +```typescript +evaluate(pageFunction: Function | string, ...args: unknown[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| pageFunction | Function \| string | a function to be evaluated in the executionContext | +| args | unknown\[\] | argument to pass to the page function | + +Returns: + +Promise<ReturnType> + +A promise that resolves to the return value of the given function. + +## Remarks + +If the function passed to the `executionContext.evaluate` returns a Promise, then `executionContext.evaluate` would wait for the promise to resolve and return its value. If the function passed to the `executionContext.evaluate` returns a non-serializable value, then `executionContext.evaluate` resolves to `undefined`. DevTools Protocol also supports transferring some additional values that are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and bigint literals. + +## Example 1 + + +```js +const executionContext = await page.mainFrame().executionContext(); +const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ; +console.log(result); // prints "56" + +``` + +## Example 2 + +A string can also be passed in instead of a function. + +```js +console.log(await executionContext.evaluate('1 + 2')); // prints "3" + +``` + +## Example 3 + +[JSHandle](./puppeteer.jshandle.md) instances can be passed as arguments to the `executionContext.* evaluate`: + +```js +const oneHandle = await executionContext.evaluateHandle(() => 1); +const twoHandle = await executionContext.evaluateHandle(() => 2); +const result = await executionContext.evaluate( + (a, b) => a + b, oneHandle, * twoHandle +); +await oneHandle.dispose(); +await twoHandle.dispose(); +console.log(result); // prints '3'. + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluatehandle.md b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluatehandle.md new file mode 100644 index 0000000000000..e8c9e3d4da891 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.evaluatehandle.md @@ -0,0 +1,62 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ExecutionContext](./puppeteer.executioncontext.md) > [evaluateHandle](./puppeteer.executioncontext.evaluatehandle.md) + +## ExecutionContext.evaluateHandle() method + +Signature: + +```typescript +evaluateHandle(pageFunction: EvaluateHandleFn, ...args: SerializableOrJSHandle[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| pageFunction | [EvaluateHandleFn](./puppeteer.evaluatehandlefn.md) | a function to be evaluated in the executionContext | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | argument to pass to the page function | + +Returns: + +Promise<HandleType> + +A promise that resolves to the return value of the given function as an in-page object (a [JSHandle](./puppeteer.jshandle.md)). + +## Remarks + +The only difference between `executionContext.evaluate` and `executionContext.evaluateHandle` is that `executionContext.evaluateHandle` returns an in-page object (a [JSHandle](./puppeteer.jshandle.md)). If the function passed to the `executionContext.evaluateHandle` returns a Promise, then `executionContext.evaluateHandle` would wait for the promise to resolve and return its value. + +## Example 1 + + +```js +const context = await page.mainFrame().executionContext(); +const aHandle = await context.evaluateHandle(() => Promise.resolve(self)); +aHandle; // Handle for the global object. + +``` + +## Example 2 + +A string can also be passed in instead of a function. + +```js +// Handle for the '3' * object. +const aHandle = await context.evaluateHandle('1 + 2'); + +``` + +## Example 3 + +JSHandle instances can be passed as arguments to the `executionContext.* evaluateHandle`: + +```js +const aHandle = await context.evaluateHandle(() => document.body); +const resultHandle = await context.evaluateHandle(body => body.innerHTML, * aHandle); +console.log(await resultHandle.jsonValue()); // prints body's innerHTML +await aHandle.dispose(); +await resultHandle.dispose(); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.frame.md b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.frame.md new file mode 100644 index 0000000000000..be7d61d40b0f8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.frame.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ExecutionContext](./puppeteer.executioncontext.md) > [frame](./puppeteer.executioncontext.frame.md) + +## ExecutionContext.frame() method + +Signature: + +```typescript +frame(): Frame | null; +``` +Returns: + +[Frame](./puppeteer.frame.md) \| null + +The frame associated with this execution context. + +## Remarks + +Not every execution context is associated with a frame. For example, workers and extensions have execution contexts that are not associated with frames. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.md b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.md new file mode 100644 index 0000000000000..88699a5d32d77 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ExecutionContext](./puppeteer.executioncontext.md) + +## ExecutionContext class + +This class represents a context for JavaScript execution. A \[Page\] might have many execution contexts: - each [frame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) has "default" execution context that is always created after frame is attached to DOM. This context is returned by the [Frame.executionContext()](./puppeteer.frame.executioncontext.md) method. - [Extension](https://developer.chrome.com/extensions)'s content scripts create additional execution contexts. + +Besides pages, execution contexts can be found in [workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). + +Signature: + +```typescript +export declare class ExecutionContext +``` + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `ExecutionContext` class. + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [evaluate(pageFunction, args)](./puppeteer.executioncontext.evaluate.md) | | | +| [evaluateHandle(pageFunction, args)](./puppeteer.executioncontext.evaluatehandle.md) | | | +| [frame()](./puppeteer.executioncontext.frame.md) | | | +| [queryObjects(prototypeHandle)](./puppeteer.executioncontext.queryobjects.md) | | This method iterates the JavaScript heap and finds all the objects with the given prototype. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.queryobjects.md b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.queryobjects.md new file mode 100644 index 0000000000000..abeea9c340f53 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.executioncontext.queryobjects.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [ExecutionContext](./puppeteer.executioncontext.md) > [queryObjects](./puppeteer.executioncontext.queryobjects.md) + +## ExecutionContext.queryObjects() method + +This method iterates the JavaScript heap and finds all the objects with the given prototype. + +Signature: + +```typescript +queryObjects(prototypeHandle: JSHandle): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| prototypeHandle | [JSHandle](./puppeteer.jshandle.md) | a handle to the object prototype | + +Returns: + +Promise<[JSHandle](./puppeteer.jshandle.md)> + +A handle to an array of objects with the given prototype. + +## Remarks + + +## Example + + +```js +// 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(); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.filechooser.accept.md b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.accept.md new file mode 100644 index 0000000000000..173ec7c659fe8 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.accept.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [FileChooser](./puppeteer.filechooser.md) > [accept](./puppeteer.filechooser.accept.md) + +## FileChooser.accept() method + +Accept the file chooser request with given paths. + +Signature: + +```typescript +accept(filePaths: string[]): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| filePaths | string\[\] | If some of the filePaths are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). | + +Returns: + +Promise<void> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.filechooser.cancel.md b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.cancel.md new file mode 100644 index 0000000000000..6e557e07ac1ed --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.cancel.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [FileChooser](./puppeteer.filechooser.md) > [cancel](./puppeteer.filechooser.cancel.md) + +## FileChooser.cancel() method + +Closes the file chooser without selecting any files. + +Signature: + +```typescript +cancel(): void; +``` +Returns: + +void + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.filechooser.ismultiple.md b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.ismultiple.md new file mode 100644 index 0000000000000..58e54a02baeaf --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.ismultiple.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [FileChooser](./puppeteer.filechooser.md) > [isMultiple](./puppeteer.filechooser.ismultiple.md) + +## FileChooser.isMultiple() method + +Whether file chooser allow for [multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple) file selection. + +Signature: + +```typescript +isMultiple(): boolean; +``` +Returns: + +boolean + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.filechooser.md b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.md new file mode 100644 index 0000000000000..77738b1a4e0c0 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.filechooser.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [FileChooser](./puppeteer.filechooser.md) + +## FileChooser class + +File choosers let you react to the page requesting for a file. + +Signature: + +```typescript +export declare class FileChooser +``` + +## Remarks + +`FileChooser` objects are returned via the `page.waitForFileChooser` method. + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `FileChooser` class. + +## Example + +An example of using `FileChooser`: + +```js +const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('#upload-file-button'), // some button that triggers file selection +]); +await fileChooser.accept(['/tmp/myfile.pdf']); + +``` +\*\*NOTE\*\* In browsers, only one file chooser can be opened at a time. All file choosers must be accepted or canceled. Not doing so will prevent subsequent file choosers from appearing. + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [accept(filePaths)](./puppeteer.filechooser.accept.md) | | Accept the file chooser request with given paths. | +| [cancel()](./puppeteer.filechooser.cancel.md) | | Closes the file chooser without selecting any files. | +| [isMultiple()](./puppeteer.filechooser.ismultiple.md) | | Whether file chooser allow for [multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple) file selection. | + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame._.md b/website/versioned_docs/version-10.0.0/puppeteer.frame._.md new file mode 100644 index 0000000000000..8250f209e7fa4 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame._.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [$](./puppeteer.frame._.md) + +## Frame.$() method + +This method queries the frame for the given selector. + +Signature: + +```typescript +$(selector: string): Promise | null>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | a selector to query for. | + +Returns: + +Promise<[ElementHandle](./puppeteer.elementhandle.md)<T> \| null> + +A promise which resolves to an `ElementHandle` pointing at the element, or `null` if it was not found. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame.__.md b/website/versioned_docs/version-10.0.0/puppeteer.frame.__.md new file mode 100644 index 0000000000000..32b33da9831c7 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame.__.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [$$](./puppeteer.frame.__.md) + +## Frame.$$() method + +This runs `document.querySelectorAll` in the frame and returns the result. + +Signature: + +```typescript +$$(selector: string): Promise>>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | a selector to search for | + +Returns: + +Promise<Array<[ElementHandle](./puppeteer.elementhandle.md)<T>>> + +An array of element handles pointing to the found frame elements. + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame.__eval.md b/website/versioned_docs/version-10.0.0/puppeteer.frame.__eval.md new file mode 100644 index 0000000000000..909b8510c2e65 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame.__eval.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [$$eval](./puppeteer.frame.__eval.md) + +## Frame.$$eval() method + +Signature: + +```typescript +$$eval(selector: string, pageFunction: (elements: Element[], ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | the selector to query for | +| pageFunction | (elements: Element\[\], ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | the function to be evaluated in the frame's context | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | additional arguments to pass to pageFunction | + +Returns: + +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> + +## Remarks + +This method runs `Array.from(document.querySelectorAll(selector))` within the frame and passes it as the first argument to `pageFunction`. + +If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the promise to resolve and return its value. + +## Example + + +```js +const divsCounts = await frame.$$eval('div', divs => divs.length); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame._eval.md b/website/versioned_docs/version-10.0.0/puppeteer.frame._eval.md new file mode 100644 index 0000000000000..32da2b07eeddb --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame._eval.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [$eval](./puppeteer.frame._eval.md) + +## Frame.$eval() method + +Signature: + +```typescript +$eval(selector: string, pageFunction: (element: Element, ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| selector | string | the selector to query for | +| pageFunction | (element: Element, ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | the function to be evaluated in the frame's context | +| args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | additional arguments to pass to pageFunction | + +Returns: + +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> + +## Remarks + +This method runs `document.querySelector` within the frame and passes it as the first argument to `pageFunction`. + +If `pageFunction` returns a Promise, then `frame.$eval` would wait for the promise to resolve and return its value. + +## Example + + +```js +const searchValue = await frame.$eval('#search', el => el.value); + +``` + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame._x.md b/website/versioned_docs/version-10.0.0/puppeteer.frame._x.md new file mode 100644 index 0000000000000..dc07d590893d3 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame._x.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [$x](./puppeteer.frame._x.md) + +## Frame.$x() method + +This method evaluates the given XPath expression and returns the results. + +Signature: + +```typescript +$x(expression: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| expression | string | the XPath expression to evaluate. | + +Returns: + +Promise<[ElementHandle](./puppeteer.elementhandle.md)\[\]> + diff --git a/website/versioned_docs/version-10.0.0/puppeteer.frame.addscripttag.md b/website/versioned_docs/version-10.0.0/puppeteer.frame.addscripttag.md new file mode 100644 index 0000000000000..658a6fa85b276 --- /dev/null +++ b/website/versioned_docs/version-10.0.0/puppeteer.frame.addscripttag.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [puppeteer](./puppeteer.md) > [Frame](./puppeteer.frame.md) > [addScriptTag](./puppeteer.frame.addscripttag.md) + +## Frame.addScriptTag() method + +Adds a `