diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /remote/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test')
524 files changed, 89668 insertions, 0 deletions
diff --git a/remote/test/puppeteer/.editorconfig b/remote/test/puppeteer/.editorconfig new file mode 100644 index 0000000000..c6c8b36219 --- /dev/null +++ b/remote/test/puppeteer/.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/remote/test/puppeteer/.eslintignore b/remote/test/puppeteer/.eslintignore new file mode 100644 index 0000000000..1700c34b9f --- /dev/null +++ b/remote/test/puppeteer/.eslintignore @@ -0,0 +1,46 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# Generated files +**/*.tsbuildinfo +*.api.json +*.tgz +yarn.lock +.docusaurus/ +.cache-loader +test/output-*/ +.dev_profile* +coverage/ +generated/ +.eslintcache +/.cache/ + +# IDE Artifacts +.vscode +!.vscode/extensions.json +!.vscode/*.template.json +.devcontainer + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Wireit +.wireit +## [END] Keep in sync with .gitignore + +# ESLint ignores. +assets/ +third_party/ diff --git a/remote/test/puppeteer/.eslintplugin.js b/remote/test/puppeteer/.eslintplugin.js new file mode 100644 index 0000000000..0e61040f45 --- /dev/null +++ b/remote/test/puppeteer/.eslintplugin.js @@ -0,0 +1,95 @@ +const prettier = require('prettier'); + +const cleanupBlockComment = value => { + return value + .trim() + .split('\n') + .map(value => { + value = value.trim(); + if (value.startsWith('*')) { + value = value.slice(1); + if (value.startsWith(' ')) { + value = value.slice(1); + } + } + return value.trimEnd(); + }) + .join('\n') + .trim(); +}; + +const format = (value, offset, prettierOptions) => { + return prettier + .format(value, { + ...prettierOptions, + // This is the print width minus 3 (the length of ` * `) and the offset. + printWidth: prettierOptions.printWidth - (offset + 3), + }) + .trim(); +}; + +const buildBlockComment = (value, offset) => { + const spaces = ' '.repeat(offset); + const lines = value.split('\n').map(line => { + return ` * ${line}`; + }); + lines.unshift('/**'); + lines.push(' */'); + lines.forEach((line, i) => { + lines[i] = `${spaces}${line}`; + }); + return lines.join('\n'); +}; + +/** + * @type import("eslint").Rule.RuleModule + */ +const rule = { + meta: { + type: 'suggestion', + docs: { + description: 'Enforce Prettier formatting on comments', + recommended: false, + }, + fixable: 'code', + schema: [], + messages: {}, + }, + + create(context) { + const prettierOptions = { + printWidth: 80, + ...prettier.resolveConfig.sync(context.getPhysicalFilename()), + parser: 'markdown', + }; + for (const comment of context.getSourceCode().getAllComments()) { + switch (comment.type) { + case 'Block': { + const offset = comment.loc.start.column; + const value = cleanupBlockComment(comment.value); + const formattedValue = format(value, offset, prettierOptions); + if (formattedValue !== value) { + context.report({ + node: comment, + message: `Comment is not formatted correctly.`, + fix(fixer) { + return fixer.replaceText( + comment, + buildBlockComment(formattedValue, offset).trimStart() + ); + }, + }); + } + break; + } + } + } + return {}; + }, +}; + +module.exports = { + rules: { + 'prettier-comments': rule, + }, +}; diff --git a/remote/test/puppeteer/.eslintrc.js b/remote/test/puppeteer/.eslintrc.js new file mode 100644 index 0000000000..7de1595db1 --- /dev/null +++ b/remote/test/puppeteer/.eslintrc.js @@ -0,0 +1,205 @@ +module.exports = { + root: true, + env: { + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: ['mocha', '@typescript-eslint', 'import'], + + extends: ['plugin:prettier/recommended'], + + rules: { + // Brackets keep code readable. + curly: ['error', 'all'], + // Brackets keep code readable and `return` intentions clear. + 'arrow-body-style': ['error', 'always'], + // Error if files are not formatted with Prettier correctly. + 'prettier/prettier': 'error', + // syntax preferences + 'spaced-comment': [ + 'error', + 'always', + { + markers: ['*'], + }, + ], + eqeqeq: ['error'], + 'accessor-pairs': [ + 'error', + { + getWithoutSet: false, + setWithoutGet: false, + }, + ], + 'new-parens': 'error', + 'func-call-spacing': 'error', + 'prefer-const': 'error', + + 'max-len': [ + 'error', + { + /* 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': 'error', + 'no-with': 'error', + 'no-multi-str': 'error', + 'no-caller': 'error', + 'no-implied-eval': 'error', + 'no-labels': 'error', + 'no-new-object': 'error', + 'no-octal-escape': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-cond-assign': 'error', + 'no-debugger': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-empty-character-class': 'error', + 'no-unreachable': 'error', + 'no-unsafe-negation': 'error', + radix: 'error', + 'valid-typeof': 'error', + 'no-unused-vars': [ + 'error', + { + args: 'none', + vars: 'local', + varsIgnorePattern: + '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', + }, + ], + 'no-implicit-globals': ['error'], + + // es2015 features + 'require-yield': 'error', + 'template-curly-spacing': ['error', 'never'], + + // ensure we don't have any it.only or describe.only in prod + 'mocha/no-exclusive-tests': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import `mitt` from the vendored location: third_party/mitt/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + alphabetize: {order: 'asc', caseInsensitive: true}, + }, + ], + + 'no-restricted-syntax': [ + 'error', + // Don't allow underscored declarations on camelCased variables/properties. + // ...RESTRICTED_UNDERSCORED_IDENTIFIERS, + ], + }, + overrides: [ + { + files: ['*.ts'], + parserOptions: { + allowAutomaticSingleRunInference: true, + project: './tsconfig.base.json', + }, + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + plugins: ['eslint-plugin-tsdoc', 'local'], + rules: { + // Keeps comments formatted. + 'local/prettier-comments': 'error', + // Brackets keep code readable. + curly: ['error', 'all'], + // Brackets keep code readable and `return` intentions clear. + 'arrow-body-style': ['error', 'always'], + // Error if comments do not adhere to `tsdoc`. + 'tsdoc/syntax': 'error', + // Keeps array types simple only when they are simple for readability. + '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + {argsIgnorePattern: '^_'}, + ], + 'func-call-spacing': 'off', + '@typescript-eslint/func-call-spacing': 'error', + semi: 'off', + '@typescript-eslint/semi': 'error', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-use-before-define': 'off', + // We have to use any on some types so the warning isn't valuable. + '@typescript-eslint/no-explicit-any': 'off', + // We don't require explicit return types on basic functions or + // dummy functions in tests, for example + '@typescript-eslint/explicit-function-return-type': 'off', + // We allow non-null assertions if the value was asserted using `assert` API. + '@typescript-eslint/no-non-null-assertion': 'off', + /** + * 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, + }, + }, + ], + // By default this is a warning but we want it to error. + '@typescript-eslint/explicit-module-boundary-types': 'error', + 'no-restricted-syntax': [ + 'error', + { + // Never use `require` in TypeScript since they are transpiled out. + selector: "CallExpression[callee.name='require']", + message: '`require` statements are not allowed. Use `import`.', + }, + ], + '@typescript-eslint/no-floating-promises': [ + 'error', + {ignoreVoid: true, ignoreIIFE: true}, + ], + }, + }, + ], +}; diff --git a/remote/test/puppeteer/.eslintrc.types.cjs b/remote/test/puppeteer/.eslintrc.types.cjs new file mode 100644 index 0000000000..f266ee754b --- /dev/null +++ b/remote/test/puppeteer/.eslintrc.types.cjs @@ -0,0 +1,17 @@ +module.exports = { + plugins: ['unused-imports'], + parser: '@typescript-eslint/parser', + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], + }, +}; diff --git a/remote/test/puppeteer/.mocharc.cjs b/remote/test/puppeteer/.mocharc.cjs new file mode 100644 index 0000000000..54fae5d77b --- /dev/null +++ b/remote/test/puppeteer/.mocharc.cjs @@ -0,0 +1,27 @@ +/** + * 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 = { + reporter: 'dot', + logLevel: 'debug', + require: ['./test/build/mocha-utils.js', 'source-map-support/register'], + spec: 'test/build/**/*.spec.js', + exit: !!process.env.CI, + retries: process.env.CI ? 3 : 0, + parallel: !!process.env.PARALLEL, + timeout: 10_000, + reporter: process.env.CI ? 'spec' : 'dot', +}; diff --git a/remote/test/puppeteer/.npmrc b/remote/test/puppeteer/.npmrc new file mode 100644 index 0000000000..94a06c2180 --- /dev/null +++ b/remote/test/puppeteer/.npmrc @@ -0,0 +1 @@ +access=public diff --git a/remote/test/puppeteer/.prettierignore b/remote/test/puppeteer/.prettierignore new file mode 100644 index 0000000000..7a08fe069b --- /dev/null +++ b/remote/test/puppeteer/.prettierignore @@ -0,0 +1,53 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# Generated files +**/*.tsbuildinfo +*.api.json +*.tgz +yarn.lock +.docusaurus/ +.cache-loader +test/output-*/ +.dev_profile* +coverage/ +generated/ +.eslintcache +/.cache/ + +# IDE Artifacts +.vscode/* +!.vscode/extensions.json +!.vscode/*.template.json +.devcontainer + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Wireit +.wireit +## [END] Keep in sync with .gitignore + +# Prettier-only ignores. +CHANGELOG.md +package-lock.json +test/assets/ +docs/api +versioned_*/ + +# Ng-schematics +/packages/ng-schematics/files/ +/packages/ng-schematics/sandbox/
\ No newline at end of file diff --git a/remote/test/puppeteer/.prettierrc.cjs b/remote/test/puppeteer/.prettierrc.cjs new file mode 100644 index 0000000000..46c608ced5 --- /dev/null +++ b/remote/test/puppeteer/.prettierrc.cjs @@ -0,0 +1,7 @@ +/** + * @type {import('prettier').Config} + */ +module.exports = { + ...require('gts/.prettierrc.json'), + // proseWrap: 'always', // Uncomment this while working on Markdown documents. MAKE SURE TO COMMENT THIS BEFORE RUNNING CHECKS/FORMATS OR EVERYTHING WILL BE MODIFIED. +}; diff --git a/remote/test/puppeteer/.release-please-manifest.json b/remote/test/puppeteer/.release-please-manifest.json new file mode 100644 index 0000000000..ebd5be1459 --- /dev/null +++ b/remote/test/puppeteer/.release-please-manifest.json @@ -0,0 +1,7 @@ +{ + "packages/puppeteer": "20.1.0", + "packages/puppeteer-core": "20.1.0", + "packages/testserver": "0.6.0", + "packages/ng-schematics": "0.2.0", + "packages/browsers": "1.0.0" +} diff --git a/remote/test/puppeteer/LICENSE b/remote/test/puppeteer/LICENSE new file mode 100644 index 0000000000..d2c171df74 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/README.md b/remote/test/puppeteer/README.md new file mode 100644 index 0000000000..a7df543c82 --- /dev/null +++ b/remote/test/puppeteer/README.md @@ -0,0 +1,253 @@ +# Puppeteer + +[![Build status](https://github.com/puppeteer/puppeteer/workflows/CI/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI) +[![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) + +<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/> + +#### [Guides](https://pptr.dev/category/guides) | [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting) + +> Puppeteer is a Node.js library which provides a high-level API to control +> Chrome/Chromium over the +> [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). +> Puppeteer runs in +> [headless](https://developer.chrome.com/articles/new-headless/) +> mode by default, but can be configured to run in full ("headful") +> Chrome/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 automated testing environment 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](https://pptr.dev/guides/chrome-extensions). + +## Getting Started + +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or using yarn +yarn add puppeteer +# or using pnpm +pnpm i puppeteer +``` + +When you install Puppeteer, it automatically downloads a recent version of +[Chrome for Testing](https://goo.gle/chrome-for-testing) (~170MB macOS, ~282MB Linux, ~280MB Windows) that is [guaranteed to +work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) +with Puppeteer. The browser is downloaded to the `$HOME/.cache/puppeteer` folder +by default (starting with Puppeteer v19.0.0). + +If you deploy a project using Puppeteer to a hosting provider, such as Render or +Heroku, you might need to reconfigure the location of the cache to be within +your project folder (see an example below) because not all hosting providers +include `$HOME/.cache` into the project's deployment. + +For a version of Puppeteer without the browser installation, see +[`puppeteer-core`](#puppeteer-core). + +#### Configuration + +Puppeteer uses several defaults that can be customized through configuration +files. + +For example, to change the default cache directory Puppeteer uses to install +browsers, you can add a `.puppeteerrc.cjs` (or `puppeteer.config.cjs`) at the +root of your application with the contents + +```js +const {join} = require('path'); + +/** + * @type {import("puppeteer").Configuration} + */ +module.exports = { + // Changes the cache location for Puppeteer. + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; +``` + +After adding the configuration file, you will need to remove and reinstall +`puppeteer` for it to take effect. + +See the [configuration guide](https://pptr.dev/guides/configuration) for more +information. + +#### `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 Chrome, which it then drives using `puppeteer-core`. Being an +end-user product, `puppeteer` automates several workflows using reasonable +defaults [that can be customized](https://pptr.dev/guides/configuration). + +`puppeteer-core` is a _library_ to help drive anything that supports DevTools +protocol. Being a library, `puppeteer-core` is fully driven through its +programmatic interface implying no defaults are assumed and `puppeteer-core` +will not download Chrome when installed. + +You should use `puppeteer-core` if you are +[connecting to a remote browser](https://pptr.dev/api/puppeteer.puppeteer.connect) +or [managing browsers yourself](https://pptr.dev/api/puppeteer.browserfetcher). +If you are managing browsers yourself, you will need to call +[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with +an an explicit +[`executablePath`](https://pptr.dev/api/puppeteer.launchoptions) +(or [`channel`](https://pptr.dev/api/puppeteer.launchoptions) if it's +installed in a standard location). + +When using `puppeteer-core`, remember to change the import: + +```ts +import puppeteer from 'puppeteer-core'; +``` + +### Usage + +Puppeteer follows the latest +[maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of +Node. + +Puppeteer will be familiar to people using other browser testing frameworks. You +[launch](https://pptr.dev/api/puppeteer.puppeteernode.launch)/[connect](https://pptr.dev/api/puppeteer.puppeteernode.connect) +a [browser](https://pptr.dev/api/puppeteer.browser), +[create](https://pptr.dev/api/puppeteer.browser.newpage) some +[pages](https://pptr.dev/api/puppeteer.page), and then manipulate them with +[Puppeteer's API](https://pptr.dev/api). + +For more in-depth usage, check our [guides](https://pptr.dev/category/guides) +and [examples](https://github.com/puppeteer/puppeteer/tree/main/examples). + +#### Example + +The following example searches [developer.chrome.com](https://developer.chrome.com/) for blog posts with text "automate beyond recorder", click on the first result and print the full title of the blog post. + +```ts +import puppeteer from 'puppeteer'; + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.goto('https://developer.chrome.com/'); + + // Set screen size + await page.setViewport({width: 1080, height: 1024}); + + // Type into search box + await page.type('.search-box__input', 'automate beyond recorder'); + + // Wait and click on first result + const searchResultSelector = '.search-box__link'; + await page.waitForSelector(searchResultSelector); + await page.click(searchResultSelector); + + // Locate the full title with a unique string + const textSelector = await page.waitForSelector( + 'text/Customize and automate' + ); + const fullTitle = await textSelector.evaluate(el => el.textContent); + + // Print the full title + console.log('The title of this blog post is "%s".', fullTitle); + + await browser.close(); +})(); +``` + +### Default runtime settings + +**1. Uses Headless mode** + +By default Puppeteer launches Chrome in +[old Headless mode](https://developer.chrome.com/articles/new-headless/). + +```ts +const browser = await puppeteer.launch(); +// Equivalent to +const browser = await puppeteer.launch({headless: true}); +``` + +[Chrome 112 launched a new Headless mode](https://developer.chrome.com/articles/new-headless/) that might cause some differences in behavior compared to the old Headless implementation. +In the future Puppeteer will start defaulting to new implementation. +We recommend you try it out before the switch: + +```ts +const browser = await puppeteer.launch({headless: 'new'}); +``` + +To launch a "headful" version of Chrome, set the +[`headless`](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions) to `false` +option when launching a browser: + +```ts +const browser = await puppeteer.launch({headless: false}); +``` + +**2. Runs a bundled version of Chrome** + +By default, Puppeteer downloads and uses a specific version of Chrome 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: + +```ts +const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); +``` + +You can also use Puppeteer with Firefox. See +[status of cross-browser support](https://pptr.dev/faq/#q-what-is-the-status-of-cross-browser-support) 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**. + +#### Using Docker + +See our [Docker guide](https://pptr.dev/guides/docker). + +#### Using Chrome Extensions + +See our [Chrome extensions guide](https://pptr.dev/guides/chrome-extensions). + +## Resources + +- [API Documentation](https://pptr.dev/api) +- [Guides](https://pptr.dev/category/guides) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + +## Contributing + +Check out our [contributing guide](https://pptr.dev/contributing) to get an +overview of Puppeteer development. + +## FAQ + +Our [FAQ](https://pptr.dev/faq) has migrated to +[our site](https://pptr.dev/faq). diff --git a/remote/test/puppeteer/SECURITY.md b/remote/test/puppeteer/SECURITY.md new file mode 100644 index 0000000000..a202138d31 --- /dev/null +++ b/remote/test/puppeteer/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +The Puppeteer project takes security very seriously. Please use Chromium's process to report security issues. + +## Reporting a Vulnerability + +See https://www.chromium.org/Home/chromium-security/reporting-security-bugs/ diff --git a/remote/test/puppeteer/commitlint.config.js b/remote/test/puppeteer/commitlint.config.js new file mode 100644 index 0000000000..af86dc9735 --- /dev/null +++ b/remote/test/puppeteer/commitlint.config.js @@ -0,0 +1,28 @@ +/** + * 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. + */ + +// See https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-rules.md +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + // Override. The subject may be the name of a class. + 'subject-case': [0], + // Override. Most UIs wrap the body. + 'body-max-line-length': [0], + // Override. Most UIs wrap the footer. + 'footer-max-line-length': [0], + }, +}; diff --git a/remote/test/puppeteer/examples/README.md b/remote/test/puppeteer/examples/README.md new file mode 100644 index 0000000000..9dd85c6ba7 --- /dev/null +++ b/remote/test/puppeteer/examples/README.md @@ -0,0 +1,42 @@ +# Running the examples + +Assuming you have a checkout of the Puppeteer repo and have run npm i (or yarn) to install the dependencies, the examples can be run from the root folder like so: + +```bash +NODE_PATH=../ node examples/search.js +``` + +## Larger examples + +More complex and use case driven examples can be found at [github.com/GoogleChromeLabs/puppeteer-examples](https://github.com/GoogleChromeLabs/puppeteer-examples). + +# Other resources + +> Other useful tools, articles, and projects that use Puppeteer. + +## Rendering and web scraping + +- [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron). +- [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e 'An article on medium') - Getting started with Puppeteer and Chrome Headless for Web Scraping. +- [pupperender](https://github.com/LasaleFamine/pupperender) - Express middleware that checks the User-Agent header of incoming requests, and if it matches one of a configurable set of bots, render the page using Puppeteer. Useful for PWA rendering. +- [headless-chrome-crawler](https://github.com/yujiosaka/headless-chrome-crawler) - Crawler that provides simple APIs to manipulate Headless Chrome and allows you to crawl dynamic websites. +- [puppeteer-examples](https://github.com/checkly/puppeteer-examples) - Puppeteer Headless Chrome examples for real life use cases such as getting useful info from the web pages or common login scenarios. +- [browserless](https://github.com/joelgriffith/browserless) - Headless Chrome as a service letting you execute Puppeteer scripts remotely. Provides a docker image with configuration for concurrency, launch arguments and more. +- [Puppeteer on AWS Lambda](https://github.com/jay-deshmukh/headless-chrome-with-puppeteer-on-AWS-lambda-with-serverless-framework) - Running puppeteer on AWS Lambda with Serverless framework +- [Apify SDK](https://github.com/apifytech/apify-js) - The scalable web crawling and scraping library for JavaScript. Automatically manages a pool of Puppeteer browsers and provides easy error handling, task management, proxy rotation and more. + +## Testing + +- [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma. +- [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome. +- [puppeteer-to-istanbul-example](https://github.com/bcoe/puppeteer-to-istanbul-example) - Demo repository demonstrating how to output Puppeteer coverage in Istanbul format. +- [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer) - (almost) Zero configuration tool for setting up and running Jest and Puppeteer easily. Also includes an assertion library for Puppeteer. +- [puppeteer-har](https://github.com/Everettss/puppeteer-har) - Generate HAR file with puppeteer. +- [puppetry](https://puppetry.app/) - A desktop app to build Puppeteer/Jest driven tests without coding. +- [puppeteer-loadtest](https://github.com/svenkatreddy/puppeteer-loadtest) - commandline interface for performing load test on puppeteer scripts. +- [cucumber-puppeteer-example](https://github.com/mlampedx/cucumber-puppeteer-example) - Example repository demonstrating how to use Puppeeteer and Cucumber for integration testing. + +## Services + +- [Checkly](https://checklyhq.com) - Monitoring SaaS that uses Puppeteer to check availability and correctness of web pages and apps. +- [Doppio](https://doppio.sh) - SaaS API to create screenshots or PDFs from HTML/CSS/JS diff --git a/remote/test/puppeteer/examples/block-images.js b/remote/test/puppeteer/examples/block-images.js new file mode 100644 index 0000000000..73a87eb089 --- /dev/null +++ b/remote/test/puppeteer/examples/block-images.js @@ -0,0 +1,36 @@ +/** + * Copyright 2017 Google Inc., PhantomJS Authors 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', request => { + if (request.resourceType() === 'image') { + request.abort(); + } else { + request.continue(); + } + }); + await page.goto('https://news.google.com/news/'); + await page.screenshot({path: 'news.png', fullPage: true}); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/cross-browser.js b/remote/test/puppeteer/examples/cross-browser.js new file mode 100644 index 0000000000..5027afb875 --- /dev/null +++ b/remote/test/puppeteer/examples/cross-browser.js @@ -0,0 +1,48 @@ +const puppeteer = require('puppeteer'); + +/** + * To have Puppeteer fetch a Firefox binary for you, first run: + * + * PUPPETEER_PRODUCT=firefox npm install + * + * To get additional logging about which browser binary is executed, + * run this example as: + * + * DEBUG=puppeteer:launcher NODE_PATH=../ node examples/cross-browser.js + * + * You can set a custom binary with the `executablePath` launcher option. + * + * + */ + +const firefoxOptions = { + product: 'firefox', + extraPrefsFirefox: { + // Enable additional Firefox logging from its protocol implementation + // 'remote.log.level': 'Trace', + }, + // Make browser logs visible + dumpio: true, +}; + +(async () => { + const browser = await puppeteer.launch(firefoxOptions); + + const page = await browser.newPage(); + console.log(await browser.version()); + + await page.goto('https://news.ycombinator.com/'); + + // Extract articles from the page. + const resultsSelector = '.titleline > a'; + const links = await page.evaluate(resultsSelector => { + const anchors = Array.from(document.querySelectorAll(resultsSelector)); + return anchors.map(anchor => { + const title = anchor.textContent.trim(); + return `${title} - ${anchor.href}`; + }); + }, resultsSelector); + console.log(links.join('\n')); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/custom-event.js b/remote/test/puppeteer/examples/custom-event.js new file mode 100644 index 0000000000..cedf33e999 --- /dev/null +++ b/remote/test/puppeteer/examples/custom-event.js @@ -0,0 +1,50 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + // Define a window.onCustomEvent function on the page. + await page.exposeFunction('onCustomEvent', e => { + console.log(`${e.type} fired`, e.detail || ''); + }); + + /** + * Attach an event listener to page to capture a custom event on page load/navigation. + * @param {string} type Event name. + * @returns {!Promise} + */ + function listenFor(type) { + return page.evaluateOnNewDocument(type => { + document.addEventListener(type, e => { + window.onCustomEvent({type, detail: e.detail}); + }); + }, type); + } + + await listenFor('app-ready'); // Listen for "app-ready" custom event on page load. + + await page.goto('https://www.chromestatus.com/features', { + waitUntil: 'networkidle0', + }); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/detect-sniff.js b/remote/test/puppeteer/examples/detect-sniff.js new file mode 100644 index 0000000000..2900236fb8 --- /dev/null +++ b/remote/test/puppeteer/examples/detect-sniff.js @@ -0,0 +1,49 @@ +/** + * Copyright 2017 Google Inc., PhantomJS Authors 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +function sniffDetector() { + const userAgent = window.navigator.userAgent; + const platform = window.navigator.platform; + + window.navigator.__defineGetter__('userAgent', function () { + window.navigator.sniffed = true; + return userAgent; + }); + + window.navigator.__defineGetter__('platform', function () { + window.navigator.sniffed = true; + return platform; + }); +} + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.evaluateOnNewDocument(sniffDetector); + await page.goto('https://www.google.com', {waitUntil: 'networkidle2'}); + console.log( + 'Sniffed: ' + + (await page.evaluate(() => { + return !!navigator.sniffed; + })) + ); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/oopif.js b/remote/test/puppeteer/examples/oopif.js new file mode 100644 index 0000000000..a8631abc54 --- /dev/null +++ b/remote/test/puppeteer/examples/oopif.js @@ -0,0 +1,49 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +async function attachFrame(frameId, url) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => { + return (frame.onload = x); + }); + return frame; +} + +(async () => { + // Launch browser in non-headless mode. + const browser = await puppeteer.launch({headless: false}); + const page = await browser.newPage(); + + // Load a page from one origin: + await page.goto('http://example.org/'); + + // Inject iframe with the another origin. + await page.evaluateHandle(attachFrame, 'frame1', 'https://example.com/'); + + // At this point there should be a message in the output: + // puppeteer:frame The frame '...' moved to another session. Out-of-process + // iframes (OOPIF) are not supported by Puppeteer yet. + // https://github.com/puppeteer/puppeteer/issues/2548 + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/pdf.js b/remote/test/puppeteer/examples/pdf.js new file mode 100644 index 0000000000..9f7ac92e0a --- /dev/null +++ b/remote/test/puppeteer/examples/pdf.js @@ -0,0 +1,35 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + // page.pdf() is currently supported only in headless mode. + // @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118 + await page.pdf({ + path: 'hn.pdf', + format: 'letter', + }); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/proxy.js b/remote/test/puppeteer/examples/proxy.js new file mode 100644 index 0000000000..5490822b8b --- /dev/null +++ b/remote/test/puppeteer/examples/proxy.js @@ -0,0 +1,35 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch({ + // Launch chromium using a proxy server on port 9876. + // More on proxying: + // https://www.chromium.org/developers/design-documents/network-settings + args: [ + '--proxy-server=127.0.0.1:9876', + // Use proxy for localhost URLs + '--proxy-bypass-list=<-loopback>', + ], + }); + const page = await browser.newPage(); + await page.goto('https://google.com'); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/screenshot-fullpage.js b/remote/test/puppeteer/examples/screenshot-fullpage.js new file mode 100644 index 0000000000..95f469eb7f --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot-fullpage.js @@ -0,0 +1,28 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.emulate(puppeteer.devices['iPhone 6']); + await page.goto('https://www.nytimes.com/'); + await page.screenshot({path: 'full.png', fullPage: true}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/screenshot.js b/remote/test/puppeteer/examples/screenshot.js new file mode 100644 index 0000000000..7d54806c4c --- /dev/null +++ b/remote/test/puppeteer/examples/screenshot.js @@ -0,0 +1,27 @@ +/** + * 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. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.screenshot({path: 'example.png'}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/examples/search.js b/remote/test/puppeteer/examples/search.js new file mode 100644 index 0000000000..df0abc213b --- /dev/null +++ b/remote/test/puppeteer/examples/search.js @@ -0,0 +1,55 @@ +/** + * 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. + */ + +/** + * @fileoverview Search developers.google.com/web for articles tagged + * "Headless Chrome" and scrape results from the results page. + */ + +'use strict'; + +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + await page.goto('https://developers.google.com/web/'); + + // Type into search box. + await page.type('.devsite-search-field', 'Headless Chrome'); + + // Wait for suggest overlay to appear and click "show all results". + const allResultsSelector = '.devsite-suggest-all-results'; + await page.waitForSelector(allResultsSelector); + await page.click(allResultsSelector); + + // Wait for the results page to load and display the results. + const resultsSelector = '.gsc-table-result a.gs-title[href]'; + await page.waitForSelector(resultsSelector); + + // Extract the results from the page. + const links = await page.evaluate(resultsSelector => { + const anchors = Array.from(document.querySelectorAll(resultsSelector)); + return anchors.map(anchor => { + const title = anchor.textContent.split('|')[0].trim(); + return `${title} - ${anchor.href}`; + }); + }, resultsSelector); + console.log(links.join('\n')); + + await browser.close(); +})(); diff --git a/remote/test/puppeteer/json-mocha-reporter.js b/remote/test/puppeteer/json-mocha-reporter.js new file mode 100644 index 0000000000..ffe1d60675 --- /dev/null +++ b/remote/test/puppeteer/json-mocha-reporter.js @@ -0,0 +1,69 @@ +const mocha = require('mocha'); +module.exports = JSONExtra; + +const constants = mocha.Runner.constants; + +/* + +This is a copy of +https://github.com/mochajs/mocha/blob/master/lib/reporters/json-stream.js +with more event hooks. mocha does not support extending reporters or using +multiple reporters so a custom reporter is needed and it must be local +to the project. + +*/ + +function JSONExtra(runner, options) { + mocha.reporters.Base.call(this, runner, options); + mocha.reporters.JSON.call(this, runner, options); + const self = this; + + runner.once(constants.EVENT_RUN_BEGIN, function () { + writeEvent(['start', {total: runner.total}]); + }); + + runner.on(constants.EVENT_TEST_PASS, function (test) { + writeEvent(['pass', clean(test)]); + }); + + runner.on(constants.EVENT_TEST_FAIL, function (test, err) { + test = clean(test); + test.err = err.message; + test.stack = err.stack || null; + writeEvent(['fail', test]); + }); + + runner.once(constants.EVENT_RUN_END, function () { + writeEvent(['end', self.stats]); + }); + + runner.on(constants.EVENT_TEST_BEGIN, function (test) { + writeEvent(['test-start', clean(test)]); + }); + + runner.on(constants.EVENT_TEST_PENDING, function (test) { + writeEvent(['pending', clean(test)]); + }); +} + +function writeEvent(event) { + process.stdout.write(JSON.stringify(event) + '\n'); +} + +/** + * Returns an object literal representation of `test` + * free of cyclic properties, etc. + * + * @private + * @param {Object} test - Instance used as data source. + * @return {Object} object containing pared-down test instance data + */ +function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + file: test.file, + duration: test.duration, + currentRetry: test.currentRetry(), + }; +} diff --git a/remote/test/puppeteer/moz.yaml b/remote/test/puppeteer/moz.yaml new file mode 100644 index 0000000000..abedd615f6 --- /dev/null +++ b/remote/test/puppeteer/moz.yaml @@ -0,0 +1,10 @@ +bugzilla: + component: Agent + product: Remote Protocol +origin: + description: Headless Chrome Node API + license: Apache-2.0 + name: puppeteer + release: puppeteer-v20.1.0 + url: https://github.com/puppeteer/puppeteer.git +schema: 1 diff --git a/remote/test/puppeteer/package-lock.json b/remote/test/puppeteer/package-lock.json new file mode 100644 index 0000000000..133066234d --- /dev/null +++ b/remote/test/puppeteer/package-lock.json @@ -0,0 +1,16194 @@ +{ + "name": "puppeteer-repo", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "puppeteer-repo", + "hasInstallScript": true, + "workspaces": [ + "packages/*", + "test", + "test/installation" + ], + "devDependencies": { + "@actions/core": "1.10.0", + "@commitlint/cli": "17.6.1", + "@commitlint/config-conventional": "17.6.1", + "@microsoft/api-documenter": "7.21.7", + "@microsoft/api-extractor": "7.34.4", + "@microsoft/api-extractor-model": "7.26.4", + "@pptr/testserver": "file:packages/testserver", + "@rollup/plugin-commonjs": "24.0.1", + "@rollup/plugin-node-resolve": "15.0.1", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/mime": "3.0.1", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/semver": "7.3.13", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.4", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", + "c8": "7.13.0", + "commitlint": "17.6.1", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "^0.17.11", + "eslint": "8.39.0", + "eslint-config-prettier": "8.8.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.17", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "29.4.3", + "glob": "8.1.0", + "gts": "4.0.0", + "husky": "8.0.3", + "jpeg-js": "0.4.4", + "license-checker": "25.0.1", + "mime": "3.0.0", + "minimist": "1.2.8", + "mocha": "10.2.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "7.0.0", + "prettier": "2.8.8", + "puppeteer": "file:packages/puppeteer", + "rimraf": "3.0.2", + "rollup": "3.18.0", + "semver": "7.3.8", + "sinon": "15.0.1", + "source-map-support": "0.5.21", + "spdx-satisfies": "5.0.1", + "text-diff": "1.0.1", + "tsd": "0.26.0", + "tsx": "3.12.3", + "typescript": "4.9.5", + "wireit": "0.9.5", + "zod": "3.21.2" + } + }, + "node_modules/@actions/core": { + "version": "1.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/http-client": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.7.tgz", + "integrity": "sha512-MzB6D/yUo6cBJfQ31zNDHJ3C3iKmBtxP3i9WIRnnkZwS1VUfO8OX3TZ6lycYbREF1oL/AQ/r9GK+KA5DNEBSAw==", + "dependencies": { + "@angular-devkit/core": "15.2.7", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.7.tgz", + "integrity": "sha512-k2MKUm4ygTD9+89neqMmBphDr0o8Tp9RtgfzbS8VHgGkGYlbu0KPsxHyHB3Mvzl1EkSz6EHyrU3t89m+Rcj1lw==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.12.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/source-map": { + "version": "0.7.4", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.7.tgz", + "integrity": "sha512-umQ+SgEMjqPHimHOBVhDn5NNGVoMLKQkI2fwbENXV72BqQqdh1K3D4QSNlUXitTaH0NEZZaAawE1vZHzzeAoNA==", + "dependencies": { + "@angular-devkit/core": "15.2.7", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/cli": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.2", + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "@schematics/angular": "15.2.2", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.1", + "ora": "5.4.1", + "pacote": "15.1.0", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1502.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.2.tgz", + "integrity": "sha512-+NE2IV+tuPgcBdC+1ac7eYIBqZDH0VskXTqbhHiRKySbK0vF3/cwTw6Ie07phl0xC1dxLXeRE52L5YwX5jERFQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.2.2", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.2.tgz", + "integrity": "sha512-YPwDSldpVcuSJuIkXy5iRzaPA78ySXKc80OicHR6XtMsrHlwY7DuxQoSWr+ih9LAqpeeBz9ECMalztwohdy0MA==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.2", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@schematics/angular": { + "version": "15.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular/cli/node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular/cli/node_modules/ini": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@angular/cli/node_modules/inquirer": { + "version": "8.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@angular/cli/node_modules/inquirer/node_modules/rxjs": { + "version": "7.8.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/cli/node_modules/magic-string": { + "version": "0.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/cli/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "2.5.0", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular/cli/node_modules/yargs": { + "version": "17.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/cli/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@commitlint/cli": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.6.1.tgz", + "integrity": "sha512-kCnDD9LE2ySiTnj/VPaxy4/oRayRcdv4aCuVxtoum8SxIU7OADHc0nJPQfheE8bHcs3zZdWzDMWltRosuT13bg==", + "dev": true, + "dependencies": { + "@commitlint/format": "^17.4.4", + "@commitlint/lint": "^17.6.1", + "@commitlint/load": "^17.5.0", + "@commitlint/read": "^17.5.1", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.6.1.tgz", + "integrity": "sha512-ng/ybaSLuTCH9F+7uavSOnEQ9EFMl7lHEjfAEgRh1hwmEe8SpLKpQeMo2aT1IWvHaGMuTb+gjfbzoRf2IR23NQ==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.4.4.tgz", + "integrity": "sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/ensure": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.4.4.tgz", + "integrity": "sha512-AHsFCNh8hbhJiuZ2qHv/m59W/GRE9UeOXbkOqxYMNNg9pJ7qELnFcwj5oYpa6vzTSHtPGKf3C2yUFNy1GGHq6g==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.4.0.tgz", + "integrity": "sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/format": { + "version": "17.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^17.4.4", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.4.4.tgz", + "integrity": "sha512-Y3eo1SFJ2JQDik4rWkBC4tlRIxlXEFrRWxcyrzb1PUT2k3kZ/XGNuCDfk/u0bU2/yS0tOA/mTjFsV+C4qyACHw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "semver": "7.3.8" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/lint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.6.1.tgz", + "integrity": "sha512-VARJ9kxH64isgwVnC+ABPafCYzqxpsWJIpDaTuI0gh8aX4GQ0i7cn9tvxtFNfJj4ER2BAJeWJ0vURdNYjK2RQQ==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^17.4.4", + "@commitlint/parse": "^17.4.4", + "@commitlint/rules": "^17.6.1", + "@commitlint/types": "^17.4.4" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/load": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.5.0.tgz", + "integrity": "sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/execute-rule": "^17.4.0", + "@commitlint/resolve-extends": "^17.4.4", + "@commitlint/types": "^17.4.4", + "@types/node": "*", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^4.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4 || ^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/message": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.4.2.tgz", + "integrity": "sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/parse": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.4.4.tgz", + "integrity": "sha512-EKzz4f49d3/OU0Fplog7nwz/lAfXMaDxtriidyGF9PtR+SRbgv4FhsfF310tKxs6EPj8Y+aWWuX3beN5s+yqGg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.4.4", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/read": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.5.1.tgz", + "integrity": "sha512-7IhfvEvB//p9aYW09YVclHbdf1u7g7QhxeYW9ZHSO8Huzp8Rz7m05aCO1mFG7G8M+7yfFnXB5xOmG18brqQIBg==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^17.4.0", + "@commitlint/types": "^17.4.4", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.4.4.tgz", + "integrity": "sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/types": "^17.4.4", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/rules": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.6.1.tgz", + "integrity": "sha512-lUdHw6lYQ1RywExXDdLOKxhpp6857/4c95Dc/1BikrHgdysVUXz26yV0vp1GL7Gv+avx9WqZWTIVB7pNouxlfw==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^17.4.4", + "@commitlint/message": "^17.4.2", + "@commitlint/to-lines": "^17.4.0", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.4.0.tgz", + "integrity": "sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/top-level": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.4.0.tgz", + "integrity": "sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/types": { + "version": "17.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/cjs-loader": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.17.6", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.5.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", + "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz", + "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz", + "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz", + "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz", + "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz", + "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz", + "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz", + "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz", + "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz", + "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz", + "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz", + "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz", + "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz", + "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz", + "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz", + "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz", + "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz", + "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz", + "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz", + "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz", + "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@microsoft/api-documenter": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.21.7.tgz", + "integrity": "sha512-qlCJ9dSefL6Rmuv1FKtxg7i6N7Bv6LUPrA5Bb0dR/9Ffac2xQuXtuSXOJ09seM7G0WkpQOzta2Kc1CPixzDaIw==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.22.1" + }, + "bin": { + "api-documenter": "bin/api-documenter" + } + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.34.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.26.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "4.8.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pptr/testserver": { + "resolved": "packages/testserver", + "link": true + }, + "node_modules/@puppeteer-test/installation": { + "resolved": "test/installation", + "link": true + }, + "node_modules/@puppeteer-test/test": { + "resolved": "test", + "link": true + }, + "node_modules/@puppeteer/browsers": { + "resolved": "packages/browsers", + "link": true + }, + "node_modules/@puppeteer/ng-schematics": { + "resolved": "packages/ng-schematics", + "link": true + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "24.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.0", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@types/resolve": { + "version": "1.20.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.55.2", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.3.18", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@schematics/angular": { + "version": "14.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "14.2.8", + "@angular-devkit/schematics": "14.2.8", + "jsonc-parser": "3.1.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "14.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/schematics": { + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.8.tgz", + "integrity": "sha512-L5GEgueZV4vqZy9Ar0zxVJOHK/4ttF1nPjW4Ut1vRFJGxsHFVEpxq5eGBf2JYSiOhqmFYc6GnJOxA6C4xAIHjA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.8", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/jsonc-parser": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@schematics/angular/node_modules/magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@schematics/angular/node_modules/source-map": { + "version": "0.7.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "7.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@tsd/typescript": { + "version": "4.9.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/models": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^6.1.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "6.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.14.6", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/pixelmatch": { + "version": "5.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pngjs": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/progress": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/proxy-from-env": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sinon": { + "version": "10.0.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tar-fs": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "node_modules/@types/tar-stream": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.30", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unbzip2-stream": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/through": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.22", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.8.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-ify": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/c8": { + "version": "7.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "license": "ISC" + }, + "node_modules/chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "dependencies": { + "mitt": "3.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colors": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/commitlint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/commitlint/-/commitlint-17.6.1.tgz", + "integrity": "sha512-yO11o5DmN/X4VCL+aLzgfJ1YXOM7qFzMN659SpISS4EBiv+QO16A0jeJU9rgVRbM2K06M7AfBQkZz7EPR3sAnQ==", + "dev": true, + "dependencies": { + "@commitlint/cli": "^17.6.1", + "@commitlint/types": "^17.4.4" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/commonmark": { + "version": "0.30.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "entities": "~2.0", + "mdurl": "~1.0.1", + "minimist": ">=1.2.2", + "string.prototype.repeat": "^0.2.0" + }, + "bin": { + "commonmark": "bin/commonmark" + }, + "engines": { + "node": "*" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=7", + "ts-node": ">=10", + "typescript": ">=3" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1120988", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", + "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz", + "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.11", + "@esbuild/android-arm64": "0.17.11", + "@esbuild/android-x64": "0.17.11", + "@esbuild/darwin-arm64": "0.17.11", + "@esbuild/darwin-x64": "0.17.11", + "@esbuild/freebsd-arm64": "0.17.11", + "@esbuild/freebsd-x64": "0.17.11", + "@esbuild/linux-arm": "0.17.11", + "@esbuild/linux-arm64": "0.17.11", + "@esbuild/linux-ia32": "0.17.11", + "@esbuild/linux-loong64": "0.17.11", + "@esbuild/linux-mips64el": "0.17.11", + "@esbuild/linux-ppc64": "0.17.11", + "@esbuild/linux-riscv64": "0.17.11", + "@esbuild/linux-s390x": "0.17.11", + "@esbuild/linux-x64": "0.17.11", + "@esbuild/netbsd-x64": "0.17.11", + "@esbuild/openbsd-x64": "0.17.11", + "@esbuild/sunos-x64": "0.17.11", + "@esbuild/win32-arm64": "0.17.11", + "@esbuild/win32-ia32": "0.17.11", + "@esbuild/win32-x64": "0.17.11" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-formatter-codeframe": { + "version": "7.32.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-formatter-pretty": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-local": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/eslint-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.4.3", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.4.3", + "jest-message-util": "^29.4.3", + "jest-util": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/gts": { + "version": "4.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "chalk": "^4.1.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "~2.7.0", + "rimraf": "^3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/meow": { + "version": "9.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gts/node_modules/prettier": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/gts/node_modules/type-fest": { + "version": "0.18.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "dev": true, + "license": "MIT", + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^6.1.6" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "6.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/irregular-plurals": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-module": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.4.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.4.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/jest-util": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.4.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/just-extend": { + "version": "4.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/memorystream": { + "version": "0.3.1", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "4.2.4", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.2.0", + "license": "MIT", + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/nise": { + "version": "5.1.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "1.8.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "9.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nopt": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "15.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parsel-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.0.tgz", + "integrity": "sha512-+CAY5A3p8b6he3OzlY/naXpeeiLMjEqFUyMwiPrwnemG5yh0/sgygYMKRhtn6/YSriyy4KZwQLnpBfs36GnXUg==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/plur": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "resolved": "packages/puppeteer", + "link": true + }, + "node_modules/puppeteer-core": { + "resolved": "packages/puppeteer-core", + "link": true + }, + "node_modules/q": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rambda": { + "version": "7.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "dev": true, + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-installed/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-installed/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-installed/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-installed/node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "node_modules/read-installed/node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "dev": true, + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/read-installed/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/read-package-json/node_modules/normalize-package-data": { + "version": "5.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.18.0", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.3.8", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.4", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "license": "ISC" + }, + "node_modules/sigstore": { + "version": "1.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.0.0" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/sigstore/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/sinon": { + "version": "15.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.12", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true + }, + "node_modules/spdx-satisfies": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", + "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", + "dev": true, + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "0.2.0", + "dev": true + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tar": { + "version": "6.1.13", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-diff": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsd": { + "version": "0.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tsd/typescript": "~4.9.5", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + }, + "bin": { + "tsd": "dist/cli.js" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tsd/node_modules/meow": { + "version": "9.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsd/node_modules/type-fest": { + "version": "0.18.1", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsx": { + "version": "3.12.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.0.0", + "@esbuild-kit/esm-loader": "^2.5.5" + }, + "bin": { + "tsx": "dist/cli.js" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tuf-js": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "1.0.0", + "make-fetch-happen": "^11.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/tuf-js/node_modules/make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/minipass-fetch": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/validator": { + "version": "13.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wireit": { + "version": "0.9.5", + "dev": true, + "license": "Apache-2.0", + "workspaces": [ + "vscode-extension", + "website" + ], + "dependencies": { + "braces": "^3.0.2", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "bin": { + "wireit": "bin/wireit.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.1", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/zod": { + "version": "3.21.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/browsers": { + "name": "@puppeteer/browsers", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/browsers/node_modules/@types/node": { + "version": "14.18.36", + "dev": true, + "license": "MIT" + }, + "packages/ng-schematics": { + "name": "@puppeteer/ng-schematics", + "version": "0.2.0", + "license": "Apache-2.0", + "dependencies": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7" + }, + "devDependencies": { + "@angular/cli": "^15.2.2", + "@schematics/angular": "^14.2.8", + "@types/node": "^14.15.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/ng-schematics/node_modules/@types/node": { + "version": "14.18.33", + "dev": true, + "license": "MIT" + }, + "packages/puppeteer": { + "version": "20.1.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.0.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "20.1.0" + } + }, + "packages/puppeteer-core": { + "version": "20.1.0", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.0.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + }, + "devDependencies": { + "mitt": "3.0.0", + "parsel-js": "1.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/testserver": { + "name": "@pptr/testserver", + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "mime": "3.0.0", + "ws": "8.13.0" + } + }, + "test": { + "name": "@puppeteer-test/test", + "version": "latest", + "dependencies": { + "puppeteer": "file:../packages/puppeteer" + } + }, + "test/installation": { + "name": "@puppeteer-test/installation", + "version": "latest", + "dependencies": { + "glob": "8.1.0", + "mocha": "10.2.0" + }, + "devDependencies": { + "@types/glob": "8.1.0" + } + }, + "test/installation/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "test/installation/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "test/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + } + }, + "dependencies": { + "@actions/core": { + "version": "1.10.0", + "dev": true, + "requires": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "@actions/http-client": { + "version": "2.0.1", + "dev": true, + "requires": { + "tunnel": "^0.0.6" + } + }, + "@angular-devkit/architect": { + "version": "0.1502.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.7.tgz", + "integrity": "sha512-MzB6D/yUo6cBJfQ31zNDHJ3C3iKmBtxP3i9WIRnnkZwS1VUfO8OX3TZ6lycYbREF1oL/AQ/r9GK+KA5DNEBSAw==", + "requires": { + "@angular-devkit/core": "15.2.7", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.7.tgz", + "integrity": "sha512-k2MKUm4ygTD9+89neqMmBphDr0o8Tp9RtgfzbS8VHgGkGYlbu0KPsxHyHB3Mvzl1EkSz6EHyrU3t89m+Rcj1lw==", + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "source-map": { + "version": "0.7.4" + } + } + }, + "@angular-devkit/schematics": { + "version": "15.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.7.tgz", + "integrity": "sha512-umQ+SgEMjqPHimHOBVhDn5NNGVoMLKQkI2fwbENXV72BqQqdh1K3D4QSNlUXitTaH0NEZZaAawE1vZHzzeAoNA==", + "requires": { + "@angular-devkit/core": "15.2.7", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "dependencies": { + "magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + } + } + }, + "@angular/cli": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1502.2", + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "@schematics/angular": "15.2.2", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.1", + "ora": "5.4.1", + "pacote": "15.1.0", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1502.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.2.tgz", + "integrity": "sha512-+NE2IV+tuPgcBdC+1ac7eYIBqZDH0VskXTqbhHiRKySbK0vF3/cwTw6Ie07phl0xC1dxLXeRE52L5YwX5jERFQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.2.tgz", + "integrity": "sha512-YPwDSldpVcuSJuIkXy5iRzaPA78ySXKc80OicHR6XtMsrHlwY7DuxQoSWr+ih9LAqpeeBz9ECMalztwohdy0MA==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "jsonc-parser": "3.2.0", + "magic-string": "0.29.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@schematics/angular": { + "version": "15.2.2", + "dev": true, + "requires": { + "@angular-devkit/core": "15.2.2", + "@angular-devkit/schematics": "15.2.2", + "jsonc-parser": "3.2.0" + } + }, + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.3", + "dev": true + }, + "ini": { + "version": "3.0.1", + "dev": true + }, + "inquirer": { + "version": "8.2.4", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "rxjs": { + "version": "7.8.0", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "magic-string": { + "version": "0.29.0", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "tslib": { + "version": "2.5.0", + "dev": true + }, + "yargs": { + "version": "17.6.2", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.12.11", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1" + }, + "@babel/highlight": { + "version": "7.18.6", + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3" + }, + "escape-string-regexp": { + "version": "1.0.5" + }, + "has-flag": { + "version": "3.0.0" + }, + "supports-color": { + "version": "5.5.0", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true + }, + "@commitlint/cli": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.6.1.tgz", + "integrity": "sha512-kCnDD9LE2ySiTnj/VPaxy4/oRayRcdv4aCuVxtoum8SxIU7OADHc0nJPQfheE8bHcs3zZdWzDMWltRosuT13bg==", + "dev": true, + "requires": { + "@commitlint/format": "^17.4.4", + "@commitlint/lint": "^17.6.1", + "@commitlint/load": "^17.5.0", + "@commitlint/read": "^17.5.1", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + } + }, + "@commitlint/config-conventional": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.6.1.tgz", + "integrity": "sha512-ng/ybaSLuTCH9F+7uavSOnEQ9EFMl7lHEjfAEgRh1hwmEe8SpLKpQeMo2aT1IWvHaGMuTb+gjfbzoRf2IR23NQ==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^5.0.0" + } + }, + "@commitlint/config-validator": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.4.4.tgz", + "integrity": "sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "ajv": "^8.11.0" + } + }, + "@commitlint/ensure": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.4.4.tgz", + "integrity": "sha512-AHsFCNh8hbhJiuZ2qHv/m59W/GRE9UeOXbkOqxYMNNg9pJ7qELnFcwj5oYpa6vzTSHtPGKf3C2yUFNy1GGHq6g==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + } + }, + "@commitlint/execute-rule": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.4.0.tgz", + "integrity": "sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==", + "dev": true + }, + "@commitlint/format": { + "version": "17.4.4", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "chalk": "^4.1.0" + } + }, + "@commitlint/is-ignored": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.4.4.tgz", + "integrity": "sha512-Y3eo1SFJ2JQDik4rWkBC4tlRIxlXEFrRWxcyrzb1PUT2k3kZ/XGNuCDfk/u0bU2/yS0tOA/mTjFsV+C4qyACHw==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "semver": "7.3.8" + } + }, + "@commitlint/lint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.6.1.tgz", + "integrity": "sha512-VARJ9kxH64isgwVnC+ABPafCYzqxpsWJIpDaTuI0gh8aX4GQ0i7cn9tvxtFNfJj4ER2BAJeWJ0vURdNYjK2RQQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^17.4.4", + "@commitlint/parse": "^17.4.4", + "@commitlint/rules": "^17.6.1", + "@commitlint/types": "^17.4.4" + } + }, + "@commitlint/load": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.5.0.tgz", + "integrity": "sha512-l+4W8Sx4CD5rYFsrhHH8HP01/8jEP7kKf33Xlx2Uk2out/UKoKPYMOIRcDH5ppT8UXLMV+x6Wm5osdRKKgaD1Q==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/execute-rule": "^17.4.0", + "@commitlint/resolve-extends": "^17.4.4", + "@commitlint/types": "^17.4.4", + "@types/node": "*", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^4.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4 || ^5.0.0" + } + }, + "@commitlint/message": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.4.2.tgz", + "integrity": "sha512-3XMNbzB+3bhKA1hSAWPCQA3lNxR4zaeQAQcHj0Hx5sVdO6ryXtgUBGGv+1ZCLMgAPRixuc6en+iNAzZ4NzAa8Q==", + "dev": true + }, + "@commitlint/parse": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.4.4.tgz", + "integrity": "sha512-EKzz4f49d3/OU0Fplog7nwz/lAfXMaDxtriidyGF9PtR+SRbgv4FhsfF310tKxs6EPj8Y+aWWuX3beN5s+yqGg==", + "dev": true, + "requires": { + "@commitlint/types": "^17.4.4", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + } + }, + "@commitlint/read": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.5.1.tgz", + "integrity": "sha512-7IhfvEvB//p9aYW09YVclHbdf1u7g7QhxeYW9ZHSO8Huzp8Rz7m05aCO1mFG7G8M+7yfFnXB5xOmG18brqQIBg==", + "dev": true, + "requires": { + "@commitlint/top-level": "^17.4.0", + "@commitlint/types": "^17.4.4", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + } + }, + "@commitlint/resolve-extends": { + "version": "17.4.4", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.4.4.tgz", + "integrity": "sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.4.4", + "@commitlint/types": "^17.4.4", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/rules": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.6.1.tgz", + "integrity": "sha512-lUdHw6lYQ1RywExXDdLOKxhpp6857/4c95Dc/1BikrHgdysVUXz26yV0vp1GL7Gv+avx9WqZWTIVB7pNouxlfw==", + "dev": true, + "requires": { + "@commitlint/ensure": "^17.4.4", + "@commitlint/message": "^17.4.2", + "@commitlint/to-lines": "^17.4.0", + "@commitlint/types": "^17.4.4", + "execa": "^5.0.0" + } + }, + "@commitlint/to-lines": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.4.0.tgz", + "integrity": "sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==", + "dev": true + }, + "@commitlint/top-level": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.4.0.tgz", + "integrity": "sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + } + }, + "@commitlint/types": { + "version": "17.4.4", + "dev": true, + "requires": { + "chalk": "^4.1.0" + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@esbuild-kit/cjs-loader": { + "version": "2.4.2", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "@esbuild-kit/core-utils": { + "version": "3.1.0", + "dev": true, + "requires": { + "esbuild": "~0.17.6", + "source-map-support": "^0.5.21" + } + }, + "@esbuild-kit/esm-loader": { + "version": "2.5.5", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, + "@esbuild/android-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.11.tgz", + "integrity": "sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.11.tgz", + "integrity": "sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.11.tgz", + "integrity": "sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.11.tgz", + "integrity": "sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.11.tgz", + "integrity": "sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.11.tgz", + "integrity": "sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.11.tgz", + "integrity": "sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.11.tgz", + "integrity": "sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.11.tgz", + "integrity": "sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.11.tgz", + "integrity": "sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.11.tgz", + "integrity": "sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.11.tgz", + "integrity": "sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.11.tgz", + "integrity": "sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.11.tgz", + "integrity": "sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.11.tgz", + "integrity": "sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.11", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.11.tgz", + "integrity": "sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.11.tgz", + "integrity": "sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.11.tgz", + "integrity": "sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.11.tgz", + "integrity": "sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.11.tgz", + "integrity": "sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz", + "integrity": "sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.8", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "dev": true + }, + "@jest/expect-utils": { + "version": "29.4.3", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/schemas": { + "version": "29.4.3", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/types": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@microsoft/api-documenter": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.21.7.tgz", + "integrity": "sha512-qlCJ9dSefL6Rmuv1FKtxg7i6N7Bv6LUPrA5Bb0dR/9Ffac2xQuXtuSXOJ09seM7G0WkpQOzta2Kc1CPixzDaIw==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.22.1" + } + }, + "@microsoft/api-extractor": { + "version": "7.34.4", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.26.4", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2", + "@rushstack/rig-package": "0.3.18", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.8.4" + }, + "dependencies": { + "typescript": { + "version": "4.8.4", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.26.4", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.55.2" + } + }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "4.0.3", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "requires": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + } + }, + "@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "requires": { + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/run-script": { + "version": "6.0.0", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "dependencies": { + "which": { + "version": "3.0.0", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@pptr/testserver": { + "version": "file:packages/testserver", + "requires": { + "mime": "3.0.0", + "ws": "8.13.0" + } + }, + "@puppeteer-test/installation": { + "version": "file:test/installation", + "requires": { + "@types/glob": "8.1.0", + "glob": "8.1.0", + "mocha": "10.2.0" + }, + "dependencies": { + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@puppeteer-test/test": { + "version": "file:test", + "requires": { + "puppeteer": "file:../packages/puppeteer" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "@puppeteer/browsers": { + "version": "file:packages/browsers", + "requires": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22", + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "dependencies": { + "@types/node": { + "version": "14.18.36", + "dev": true + } + } + }, + "@puppeteer/ng-schematics": { + "version": "file:packages/ng-schematics", + "requires": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7", + "@angular/cli": "^15.2.2", + "@schematics/angular": "^14.2.8", + "@types/node": "^14.15.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.33", + "dev": true + } + } + }, + "@rollup/plugin-commonjs": { + "version": "24.0.1", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "dev": true + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "15.0.1", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.0", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "@types/resolve": { + "version": "1.20.2", + "dev": true + } + } + }, + "@rollup/pluginutils": { + "version": "5.0.2", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "dev": true + } + } + }, + "@rushstack/node-core-library": { + "version": "3.55.2", + "dev": true, + "requires": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.3.18", + "dev": true, + "requires": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "@rushstack/ts-command-line": { + "version": "4.13.2", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@schematics/angular": { + "version": "14.2.8", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.8", + "@angular-devkit/schematics": "14.2.8", + "jsonc-parser": "3.1.0" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "14.2.8", + "dev": true, + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.8.tgz", + "integrity": "sha512-L5GEgueZV4vqZy9Ar0zxVJOHK/4ttF1nPjW4Ut1vRFJGxsHFVEpxq5eGBf2JYSiOhqmFYc6GnJOxA6C4xAIHjA==", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.8", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "jsonc-parser": { + "version": "3.1.0", + "dev": true + }, + "magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "source-map": { + "version": "0.7.4", + "dev": true + } + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "dev": true + }, + "@sinonjs/commons": { + "version": "2.0.0", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0" + } + }, + "@sinonjs/samsam": { + "version": "7.0.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "dev": true + }, + "@tootallnate/once": { + "version": "2.0.0", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@tsd/typescript": { + "version": "4.9.5", + "dev": true + }, + "@tufjs/models": { + "version": "1.0.0", + "dev": true, + "requires": { + "minimatch": "^6.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "6.2.0", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@types/argparse": { + "version": "1.0.38", + "dev": true + }, + "@types/debug": { + "version": "4.1.7", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/diff": { + "version": "5.0.2", + "dev": true + }, + "@types/eslint": { + "version": "7.29.0", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.0", + "dev": true + }, + "@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "requires": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "dev": true + }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.2", + "dev": true + }, + "@types/mocha": { + "version": "10.0.1", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "dev": true + }, + "@types/node": { + "version": "18.14.6", + "devOptional": true + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "dev": true + }, + "@types/pixelmatch": { + "version": "5.2.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/pngjs": { + "version": "6.0.1", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/progress": { + "version": "2.0.5", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/proxy-from-env": { + "version": "1.0.1", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/semver": { + "version": "7.3.13", + "dev": true + }, + "@types/sinon": { + "version": "10.0.13", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "dev": true + }, + "@types/tar-fs": { + "version": "2.0.1", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "@types/tar-stream": { + "version": "2.2.2", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/through": { + "version": "0.0.30", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/unbzip2-stream": { + "version": "1.4.0", + "dev": true, + "requires": { + "@types/through": "*" + } + }, + "@types/ws": { + "version": "8.5.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.22", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.0", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz", + "integrity": "sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/type-utils": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.1.tgz", + "integrity": "sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz", + "integrity": "sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz", + "integrity": "sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.59.1", + "@typescript-eslint/utils": "5.59.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.1.tgz", + "integrity": "sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz", + "integrity": "sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/visitor-keys": "5.59.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.1.tgz", + "integrity": "sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.1", + "@typescript-eslint/types": "5.59.1", + "@typescript-eslint/typescript-estree": "5.59.1", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz", + "integrity": "sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.59.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "dev": true + }, + "acorn": { + "version": "8.8.1", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.3.0", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.11.0", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-colors": { + "version": "4.1.1" + }, + "ansi-escapes": { + "version": "4.3.2", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1" + }, + "ansi-styles": { + "version": "4.3.0", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "dev": true + }, + "are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "dev": true + }, + "array-includes": { + "version": "3.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "arrify": { + "version": "1.0.1", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2" + }, + "base64-js": { + "version": "1.5.1" + }, + "binary-extensions": { + "version": "2.2.0" + }, + "bl": { + "version": "4.1.0", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1" + }, + "buffer": { + "version": "5.7.1", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13" + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "builtin-modules": { + "version": "3.3.0", + "dev": true + }, + "builtins": { + "version": "5.0.1", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, + "c8": { + "version": "7.13.0", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.1.4", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } + } + }, + "cacache": { + "version": "17.0.4", + "dev": true, + "requires": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.2", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0" + }, + "camelcase": { + "version": "5.3.1", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "chownr": { + "version": "1.1.4" + }, + "chromium-bidi": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.7.tgz", + "integrity": "sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==", + "requires": { + "mitt": "3.0.0" + } + }, + "ci-info": { + "version": "3.8.0", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0" + }, + "cli-width": { + "version": "3.0.0", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4" + }, + "color-convert": { + "version": "2.0.1", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4" + }, + "color-support": { + "version": "1.1.3", + "dev": true + }, + "colors": { + "version": "1.2.5", + "dev": true + }, + "commander": { + "version": "9.5.0", + "dev": true, + "optional": true + }, + "commitlint": { + "version": "17.6.1", + "resolved": "https://registry.npmjs.org/commitlint/-/commitlint-17.6.1.tgz", + "integrity": "sha512-yO11o5DmN/X4VCL+aLzgfJ1YXOM7qFzMN659SpISS4EBiv+QO16A0jeJU9rgVRbM2K06M7AfBQkZz7EPR3sAnQ==", + "dev": true, + "requires": { + "@commitlint/cli": "^17.6.1", + "@commitlint/types": "^17.4.4" + } + }, + "commondir": { + "version": "1.0.1", + "dev": true + }, + "commonmark": { + "version": "0.30.0", + "dev": true, + "requires": { + "entities": "~2.0", + "mdurl": "~1.0.1", + "minimist": ">=1.2.2", + "string.prototype.repeat": "^0.2.0" + } + }, + "compare-func": { + "version": "2.0.0", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "concat-map": { + "version": "0.0.1" + }, + "console-control-strings": { + "version": "1.1.0", + "dev": true + }, + "conventional-changelog-angular": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", + "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "conventional-commits-parser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", + "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", + "dev": true, + "requires": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.0.4", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + } + }, + "convert-source-map": { + "version": "1.9.0", + "dev": true + }, + "cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1" + }, + "js-yaml": { + "version": "4.1.0", + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "cosmiconfig-typescript-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", + "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", + "dev": true, + "requires": {} + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-env": { + "version": "7.0.3", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-fetch": { + "version": "3.1.5", + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "requires": { + "ms": "2.1.2" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delegates": { + "version": "1.0.0", + "dev": true + }, + "depd": { + "version": "2.0.0", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1120988", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", + "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" + }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "5.1.0", + "dev": true + }, + "diff-sequences": { + "version": "29.4.3", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0" + }, + "encoding": { + "version": "0.1.13", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "2.0.3", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.17.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.11.tgz", + "integrity": "sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.17.11", + "@esbuild/android-arm64": "0.17.11", + "@esbuild/android-x64": "0.17.11", + "@esbuild/darwin-arm64": "0.17.11", + "@esbuild/darwin-x64": "0.17.11", + "@esbuild/freebsd-arm64": "0.17.11", + "@esbuild/freebsd-x64": "0.17.11", + "@esbuild/linux-arm": "0.17.11", + "@esbuild/linux-arm64": "0.17.11", + "@esbuild/linux-ia32": "0.17.11", + "@esbuild/linux-loong64": "0.17.11", + "@esbuild/linux-mips64el": "0.17.11", + "@esbuild/linux-ppc64": "0.17.11", + "@esbuild/linux-riscv64": "0.17.11", + "@esbuild/linux-s390x": "0.17.11", + "@esbuild/linux-x64": "0.17.11", + "@esbuild/netbsd-x64": "0.17.11", + "@esbuild/openbsd-x64": "0.17.11", + "@esbuild/sunos-x64": "0.17.11", + "@esbuild/win32-arm64": "0.17.11", + "@esbuild/win32-ia32": "0.17.11", + "@esbuild/win32-x64": "0.17.11" + } + }, + "escalade": { + "version": "3.1.1" + }, + "escape-string-regexp": { + "version": "4.0.0" + }, + "eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "dev": true + }, + "eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "requires": {} + }, + "eslint-formatter-codeframe": { + "version": "7.32.1", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + } + }, + "eslint-formatter-pretty": { + "version": "4.1.0", + "dev": true, + "requires": { + "@types/eslint": "^7.2.13", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "eslint-rule-docs": "^1.1.5", + "log-symbols": "^4.0.0", + "plur": "^4.0.0", + "string-width": "^4.2.0", + "supports-hyperlinks": "^2.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.7", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.27.5", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "eslint-plugin-local": { + "version": "1.0.0", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "10.1.0", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "dev": true + }, + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-tsdoc": { + "version": "0.2.17", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2" + } + }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "dev": true + }, + "eslint-rule-docs": { + "version": "1.1.235", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true + }, + "espree": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.0" + } + }, + "esprima": { + "version": "4.0.1", + "dev": true + }, + "esquery": { + "version": "1.5.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "execa": { + "version": "5.1.1", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "expect": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.4.3", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.4.3", + "jest-message-util": "^29.4.3", + "jest-util": "^29.4.3" + } + }, + "external-editor": { + "version": "3.1.0", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extract-zip": { + "version": "2.0.1", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "fast-diff": { + "version": "1.2.0", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fd-slicer": { + "version": "1.1.0", + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "3.2.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + } + } + }, + "file-entry-cache": { + "version": "6.0.1", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2" + }, + "flat-cache": { + "version": "3.0.4", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fs-constants": { + "version": "1.0.0" + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "3.0.1", + "dev": true, + "requires": { + "minipass": "^4.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0" + }, + "function-bind": { + "version": "1.1.1", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "dev": true + }, + "gauge": { + "version": "4.0.4", + "dev": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "get-caller-file": { + "version": "2.0.5" + }, + "get-intrinsic": { + "version": "1.1.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-tsconfig": { + "version": "4.4.0", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "requires": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "dev": true + }, + "gts": { + "version": "4.0.0", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "chalk": "^4.1.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "~2.7.0", + "rimraf": "^3.0.2", + "write-file-atomic": "^4.0.0" + }, + "dependencies": { + "meow": { + "version": "9.0.0", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "prettier": { + "version": "2.7.1", + "dev": true + }, + "type-fest": { + "version": "0.18.1", + "dev": true + } + } + }, + "hard-rejection": { + "version": "2.1.0", + "dev": true + }, + "has": { + "version": "1.0.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "dev": true + }, + "has-flag": { + "version": "4.0.0" + }, + "has-property-descriptors": { + "version": "1.0.0", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "dev": true + }, + "he": { + "version": "1.2.0" + }, + "hosted-git-info": { + "version": "4.1.0", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.1", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "husky": { + "version": "8.0.3", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1" + }, + "ignore": { + "version": "5.2.0", + "dev": true + }, + "ignore-walk": { + "version": "6.0.1", + "dev": true, + "requires": { + "minimatch": "^6.1.6" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "6.2.0", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "import-fresh": { + "version": "3.3.0", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0" + } + } + }, + "import-lazy": { + "version": "4.0.0", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "internal-slot": { + "version": "1.0.3", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ip": { + "version": "2.0.0", + "dev": true + }, + "irregular-plurals": { + "version": "3.3.0", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1" + }, + "is-bigint": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-builtin-module": { + "version": "3.2.0", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "is-callable": { + "version": "1.2.7", + "dev": true + }, + "is-core-module": { + "version": "2.11.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "dev": true + }, + "is-extglob": { + "version": "2.1.1" + }, + "is-fullwidth-code-point": { + "version": "3.0.0" + }, + "is-glob": { + "version": "4.0.3", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0" + }, + "is-lambda": { + "version": "1.0.1", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "dev": true + }, + "is-number": { + "version": "7.0.0" + }, + "is-number-object": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "is-regex": { + "version": "1.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-unicode-supported": { + "version": "0.1.0" + }, + "is-weakref": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest-diff": { + "version": "29.4.3", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + } + }, + "jest-get-type": { + "version": "29.4.3", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.4.3", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.4.3" + } + }, + "jest-message-util": { + "version": "29.4.3", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.4.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.4.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.18.6", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + } + } + }, + "jest-util": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/types": "^29.4.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jju": { + "version": "1.4.0", + "dev": true + }, + "jpeg-js": { + "version": "0.4.4", + "dev": true + }, + "js-sdsl": { + "version": "4.1.5", + "dev": true + }, + "js-tokens": { + "version": "4.0.0" + }, + "js-yaml": { + "version": "3.13.1", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1" + }, + "json-schema-traverse": { + "version": "1.0.0" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "json5": { + "version": "2.2.3", + "dev": true + }, + "jsonc-parser": { + "version": "3.2.0" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "just-extend": { + "version": "4.2.1", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "dev": true + }, + "levn": { + "version": "0.4.1", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "requires": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "lines-and-columns": { + "version": "1.2.4" + }, + "load-json-file": { + "version": "4.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "locate-path": { + "version": "6.0.0", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "dev": true + }, + "lodash.isfunction": { + "version": "3.0.9", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.27.0", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.13" + } + }, + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "dependencies": { + "@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "cacache": { + "version": "16.1.3", + "dev": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + } + }, + "chownr": { + "version": "2.0.0", + "dev": true + }, + "fs-minipass": { + "version": "2.1.0", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ssri": { + "version": "9.0.1", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "unique-filename": { + "version": "2.0.1", + "dev": true, + "requires": { + "unique-slug": "^3.0.0" + } + }, + "unique-slug": { + "version": "3.0.0", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + } + } + }, + "map-obj": { + "version": "4.3.0", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "dev": true + }, + "memorystream": { + "version": "0.3.1", + "dev": true + }, + "meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "dependencies": { + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, + "merge-stream": { + "version": "2.0.0", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "3.0.0" + }, + "mimic-fn": { + "version": "2.1.0" + }, + "min-indent": { + "version": "1.0.1", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "minipass": { + "version": "4.2.4", + "dev": true + }, + "minipass-collect": { + "version": "1.0.2", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-fetch": { + "version": "2.1.2", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minizlib": { + "version": "2.1.2", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mitt": { + "version": "3.0.0" + }, + "mkdirp": { + "version": "1.0.4", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3" + }, + "mocha": { + "version": "10.2.0", + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1" + }, + "cliui": { + "version": "7.0.4", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "diff": { + "version": "5.0.0" + }, + "glob": { + "version": "7.2.0", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "js-yaml": { + "version": "4.1.0", + "requires": { + "argparse": "^2.0.1" + } + }, + "minimatch": { + "version": "5.0.1", + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3" + }, + "supports-color": { + "version": "8.1.1", + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4" + } + } + }, + "ms": { + "version": "2.1.2" + }, + "mute-stream": { + "version": "0.0.8", + "dev": true + }, + "nanoid": { + "version": "3.3.3" + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "dev": true + }, + "nise": { + "version": "5.1.3", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "1.8.6", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + } + } + }, + "node-fetch": { + "version": "2.6.7", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp": { + "version": "9.3.1", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "nopt": { + "version": "6.0.0", + "dev": true, + "requires": { + "abbrev": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "3.0.3", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0" + }, + "npm-bundled": { + "version": "3.0.0", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^3.0.0" + } + }, + "npm-install-checks": { + "version": "6.0.0", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "3.0.0", + "dev": true + }, + "npm-package-arg": { + "version": "10.1.0", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "6.1.1", + "dev": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + } + } + }, + "npm-packlist": { + "version": "7.0.4", + "dev": true, + "requires": { + "ignore-walk": "^6.0.0" + } + }, + "npm-pick-manifest": { + "version": "8.0.1", + "dev": true, + "requires": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + } + }, + "npm-registry-fetch": { + "version": "14.0.3", + "dev": true, + "requires": { + "make-fetch-happen": "^11.0.0", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "npm-run-all": { + "version": "4.1.5", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "npmlog": { + "version": "6.0.2", + "dev": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.1", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.1", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "ora": { + "version": "5.4.1", + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-map": { + "version": "4.0.0", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "pacote": { + "version": "15.1.0", + "dev": true, + "requires": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + } + }, + "parent-module": { + "version": "1.0.1", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parsel-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.0.tgz", + "integrity": "sha512-+CAY5A3p8b6he3OzlY/naXpeeiLMjEqFUyMwiPrwnemG5yh0/sgygYMKRhtn6/YSriyy4KZwQLnpBfs36GnXUg==", + "dev": true + }, + "path-exists": { + "version": "4.0.0" + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0" + }, + "pend": { + "version": "1.2.0" + }, + "picomatch": { + "version": "2.3.1" + }, + "pidtree": { + "version": "0.3.1", + "dev": true + }, + "pify": { + "version": "3.0.0", + "dev": true + }, + "pixelmatch": { + "version": "5.3.0", + "dev": true, + "requires": { + "pngjs": "^6.0.0" + }, + "dependencies": { + "pngjs": { + "version": "6.0.0", + "dev": true + } + } + }, + "plur": { + "version": "4.0.0", + "dev": true, + "requires": { + "irregular-plurals": "^3.2.0" + } + }, + "pngjs": { + "version": "7.0.0", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "dev": true + }, + "prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "29.4.3", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "dev": true + } + } + }, + "proc-log": { + "version": "3.0.0", + "dev": true + }, + "progress": { + "version": "2.0.3" + }, + "promise-inflight": { + "version": "1.0.1", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proper-lockfile": { + "version": "4.1.2", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "proxy-from-env": { + "version": "1.1.0" + }, + "pump": { + "version": "3.0.0", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1" + }, + "puppeteer": { + "version": "file:packages/puppeteer", + "requires": { + "@puppeteer/browsers": "1.0.0", + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "20.1.0" + } + }, + "puppeteer-core": { + "version": "file:packages/puppeteer-core", + "requires": { + "@puppeteer/browsers": "1.0.0", + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "mitt": "3.0.0", + "parsel-js": "1.1.0", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0" + } + }, + "q": { + "version": "1.5.1", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "dev": true + }, + "rambda": { + "version": "7.3.0", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "react-is": { + "version": "18.2.0", + "dev": true + }, + "read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "dev": true, + "requires": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "read-package-json": { + "version": "6.0.0", + "dev": true, + "requires": { + "glob": "^8.0.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "6.1.1", + "dev": true, + "requires": { + "lru-cache": "^7.5.1" + } + }, + "json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "normalize-package-data": { + "version": "5.0.0", + "dev": true, + "requires": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + } + } + }, + "read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "dependencies": { + "json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true + } + } + }, + "read-pkg": { + "version": "3.0.0", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "dev": true + } + } + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "readdirp": { + "version": "3.6.0", + "requires": { + "picomatch": "^2.2.1" + } + }, + "redent": { + "version": "3.0.0", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "dev": true + }, + "require-directory": { + "version": "2.1.1" + }, + "require-from-string": { + "version": "2.0.2" + }, + "resolve": { + "version": "1.22.1", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, + "restore-cursor": { + "version": "3.1.0", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "rollup": { + "version": "3.18.0", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "run-async": { + "version": "2.4.1", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safe-regex-test": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "devOptional": true + }, + "semver": { + "version": "7.3.8", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "shell-quote": { + "version": "1.7.4", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7" + }, + "sigstore": { + "version": "1.0.0", + "dev": true, + "requires": { + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "sinon": { + "version": "15.0.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/samsam": "^7.0.1", + "diff": "^5.0.0", + "nise": "^5.1.2", + "supports-color": "^7.2.0" + } + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "dev": true + }, + "socks": { + "version": "2.7.1", + "dev": true, + "requires": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, + "source-map": { + "version": "0.6.1", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "requires": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "dev": true + }, + "spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true + }, + "spdx-satisfies": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.1.tgz", + "integrity": "sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==", + "dev": true, + "requires": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "dev": true + }, + "ssri": { + "version": "10.0.1", + "dev": true, + "requires": { + "minipass": "^4.0.0" + } + }, + "stack-utils": { + "version": "2.0.6", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.3.0", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-argv": { + "version": "0.3.1", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.padend": { + "version": "3.1.3", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "string.prototype.repeat": { + "version": "0.2.0", + "dev": true + }, + "string.prototype.trimend": { + "version": "1.0.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1" + }, + "supports-color": { + "version": "7.2.0", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true + }, + "symbol-observable": { + "version": "4.0.0", + "dev": true + }, + "tar": { + "version": "6.1.13", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "dev": true + }, + "fs-minipass": { + "version": "2.1.0", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + } + } + }, + "tar-fs": { + "version": "2.1.1", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "test-exclude": { + "version": "6.0.0", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "text-diff": { + "version": "1.0.1", + "dev": true + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "dev": true + }, + "through": { + "version": "2.3.8" + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "requires": { + "readable-stream": "3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "0.0.3" + }, + "treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true + }, + "trim-newlines": { + "version": "3.0.1", + "dev": true + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tsd": { + "version": "0.26.0", + "dev": true, + "requires": { + "@tsd/typescript": "~4.9.5", + "eslint-formatter-pretty": "^4.1.0", + "globby": "^11.0.1", + "meow": "^9.0.0", + "path-exists": "^4.0.0", + "read-pkg-up": "^7.0.0" + }, + "dependencies": { + "meow": { + "version": "9.0.0", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "type-fest": { + "version": "0.18.1", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1" + }, + "tsutils": { + "version": "3.21.0", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tsx": { + "version": "3.12.3", + "dev": true, + "requires": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.0.0", + "@esbuild-kit/esm-loader": "^2.5.5", + "fsevents": "~2.3.2" + } + }, + "tuf-js": { + "version": "1.1.1", + "dev": true, + "requires": { + "@tufjs/models": "1.0.0", + "make-fetch-happen": "^11.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "7.18.3", + "dev": true + }, + "make-fetch-happen": { + "version": "11.0.3", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + } + }, + "minipass-fetch": { + "version": "3.0.1", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^4.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + }, + "tunnel": { + "version": "0.0.6", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.9.5", + "devOptional": true + }, + "unbox-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unbzip2-stream": { + "version": "1.4.3", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "unique-filename": { + "version": "3.0.0", + "dev": true, + "requires": { + "unique-slug": "^4.0.0" + } + }, + "unique-slug": { + "version": "4.0.0", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2" + }, + "util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "v8-to-istanbul": { + "version": "9.0.1", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "requires": { + "builtins": "^5.0.0" + } + }, + "validator": { + "version": "13.9.0", + "dev": true + }, + "wcwidth": { + "version": "1.0.1", + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "3.0.1" + }, + "whatwg-url": { + "version": "5.0.0", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "wide-align": { + "version": "1.1.5", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wireit": { + "version": "0.9.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "jsonc-parser": "^3.0.0", + "proper-lockfile": "^4.1.2" + } + }, + "word-wrap": { + "version": "1.2.3", + "dev": true + }, + "workerpool": { + "version": "6.2.1" + }, + "wrap-ansi": { + "version": "7.0.0", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2" + }, + "write-file-atomic": { + "version": "4.0.2", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + }, + "y18n": { + "version": "5.0.8" + }, + "yallist": { + "version": "4.0.0", + "dev": true + }, + "yargs": { + "version": "17.7.1", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "yargs-parser": { + "version": "21.1.1" + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0" + }, + "decamelize": { + "version": "4.0.0" + }, + "is-plain-obj": { + "version": "2.1.0" + } + } + }, + "yauzl": { + "version": "2.10.0", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0" + }, + "z-schema": { + "version": "5.0.5", + "dev": true, + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } + }, + "zod": { + "version": "3.21.2", + "dev": true + } + } +} diff --git a/remote/test/puppeteer/package.json b/remote/test/puppeteer/package.json new file mode 100644 index 0000000000..cf57cb328d --- /dev/null +++ b/remote/test/puppeteer/package.json @@ -0,0 +1,193 @@ +{ + "name": "puppeteer-repo", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer" + }, + "scripts": { + "build": "wireit", + "build:docs": "wireit", + "check:pinned-deps": "tsx tools/ensure-pinned-deps", + "check": "npm run check --workspaces --if-present && run-p check:*", + "clean": "rimraf -g \"./**/.wireit\" && npm run clean --workspaces --if-present", + "commitlint": "commitlint --from=HEAD~1", + "debug": "mocha --inspect-brk", + "docs": "run-s build:docs generate:markdown", + "format:eslint": "eslint --ext js --ext ts --fix .", + "format:prettier": "prettier --write .", + "format:expectations": "node tools/sort-test-expectations.js", + "format": "run-s format:*", + "generate:markdown": "tsx tools/generate_docs.ts", + "lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", + "lint:prettier": "prettier --check .", + "lint": "run-s lint:prettier lint:eslint", + "postinstall": "npm run postinstall --workspaces --if-present", + "prepare": "husky install", + "test-install": "npm run test --workspace @puppeteer-test/installation", + "test-types": "tsd -t packages/puppeteer", + "test:chrome:headful": "wireit", + "test:chrome:new-headless": "wireit", + "test:chrome:headless": "wireit", + "test:chrome:bidi": "wireit", + "test:chrome": "wireit", + "test:firefox:bidi": "wireit", + "test:firefox:headful": "wireit", + "test:firefox:headless": "wireit", + "test:firefox": "wireit", + "test": "cross-env PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 node tools/mochaRunner/lib/main.js", + "validate-licenses": "tsx tools/third_party/validate-licenses.ts" + }, + "wireit": { + "build": { + "dependencies": [ + "./packages/browsers:build", + "./packages/ng-schematics:build", + "./packages/puppeteer-core:build", + "./packages/puppeteer:build", + "./packages/testserver:build", + "./test:build", + "./test/installation:build" + ] + }, + "build:docs": { + "dependencies": [ + "./packages/browsers:build:docs", + "./packages/puppeteer:build:docs", + "./packages/puppeteer-core:build:docs" + ] + }, + "test:chrome:headful": { + "command": "npm test -- --test-suite chrome-headful", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:headless": { + "command": "npm test -- --test-suite chrome-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:new-headless": { + "command": "npm test -- --test-suite chrome-new-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome:bidi": { + "command": "npm test -- --test-suite chrome-bidi", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:headful": { + "command": "npm test -- --test-suite firefox-headful", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:headless": { + "command": "npm test -- --test-suite firefox-headless", + "dependencies": [ + "./test:build" + ] + }, + "test:firefox:bidi": { + "command": "npm test -- --test-suite firefox-bidi", + "dependencies": [ + "./test:build" + ] + }, + "test:chrome": { + "dependencies": [ + "test:chrome:headful", + "test:chrome:headless", + "test:chrome:new-headless", + "test:chrome:bidi" + ] + }, + "test:firefox": { + "dependencies": [ + "test:firefox:headful", + "test:firefox:headless", + "test:firefox:bidi" + ] + } + }, + "devDependencies": { + "@actions/core": "1.10.0", + "@commitlint/cli": "17.6.1", + "@commitlint/config-conventional": "17.6.1", + "@microsoft/api-documenter": "7.21.7", + "@microsoft/api-extractor": "7.34.4", + "@microsoft/api-extractor-model": "7.26.4", + "@pptr/testserver": "file:packages/testserver", + "@rollup/plugin-commonjs": "24.0.1", + "@rollup/plugin-node-resolve": "15.0.1", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/mime": "3.0.1", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/semver": "7.3.13", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.4", + "@typescript-eslint/eslint-plugin": "5.59.1", + "@typescript-eslint/parser": "5.59.1", + "c8": "7.13.0", + "commitlint": "17.6.1", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "^0.17.11", + "eslint": "8.39.0", + "eslint-config-prettier": "8.8.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.17", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "29.4.3", + "glob": "8.1.0", + "gts": "4.0.0", + "husky": "8.0.3", + "jpeg-js": "0.4.4", + "license-checker": "25.0.1", + "mime": "3.0.0", + "minimist": "1.2.8", + "mocha": "10.2.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "7.0.0", + "prettier": "2.8.8", + "puppeteer": "file:packages/puppeteer", + "rimraf": "3.0.2", + "rollup": "3.18.0", + "semver": "7.3.8", + "sinon": "15.0.1", + "source-map-support": "0.5.21", + "spdx-satisfies": "5.0.1", + "text-diff": "1.0.1", + "tsd": "0.26.0", + "tsx": "3.12.3", + "typescript": "4.9.5", + "wireit": "0.9.5", + "zod": "3.21.2" + }, + "workspaces": [ + "packages/*", + "test", + "test/installation" + ] +} diff --git a/remote/test/puppeteer/packages/browsers/.gitignore b/remote/test/puppeteer/packages/browsers/.gitignore new file mode 100644 index 0000000000..23b2baa7ca --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/.gitignore @@ -0,0 +1 @@ +test/cache
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/browsers/.mocharc.cjs b/remote/test/puppeteer/packages/browsers/.mocharc.cjs new file mode 100644 index 0000000000..4cabbc3232 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/.mocharc.cjs @@ -0,0 +1,7 @@ +module.exports = { + logLevel: 'debug', + spec: 'test/build/**/*.spec.js', + exit: !!process.env.CI, + reporter: 'spec', + timeout: 10_000, +}; diff --git a/remote/test/puppeteer/packages/browsers/CHANGELOG.md b/remote/test/puppeteer/packages/browsers/CHANGELOG.md new file mode 100644 index 0000000000..9da7ab8980 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/CHANGELOG.md @@ -0,0 +1,131 @@ +# Changelog + +## [1.0.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.5.0...browsers-v1.0.0) (2023-05-02) + + +### ⚠ BREAKING CHANGES + +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) + +### Features + +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9)) +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://github.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab)) + + +### Bug Fixes + +* add Host header when used with http_proxy ([#10080](https://github.com/puppeteer/puppeteer/issues/10080)) ([edbfff7](https://github.com/puppeteer/puppeteer/commit/edbfff7b04baffc29c01c37c595d6b3355c0dea0)) + +## [0.5.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.4.1...browsers-v0.5.0) (2023-04-21) + + +### Features + +* **browser:** add a method to get installed browsers ([#10057](https://github.com/puppeteer/puppeteer/issues/10057)) ([e16e2a9](https://github.com/puppeteer/puppeteer/commit/e16e2a97284f5e7ab4073f375254572a6a89e800)) + +## [0.4.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.4.0...browsers-v0.4.1) (2023-04-13) + + +### Bug Fixes + +* report install errors properly ([#10016](https://github.com/puppeteer/puppeteer/issues/10016)) ([7381229](https://github.com/puppeteer/puppeteer/commit/7381229a164e598e7523862f2438cd0cd1cd796a)) + +## [0.4.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.3...browsers-v0.4.0) (2023-04-06) + + +### Features + +* **browsers:** support downloading chromedriver ([#9990](https://github.com/puppeteer/puppeteer/issues/9990)) ([ef0fb5d](https://github.com/puppeteer/puppeteer/commit/ef0fb5d87299c604af2387ac1c72be317c50316d)) + +## [0.3.3](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.2...browsers-v0.3.3) (2023-04-06) + + +### Bug Fixes + +* **browsers:** update package json ([#9968](https://github.com/puppeteer/puppeteer/issues/9968)) ([817288c](https://github.com/puppeteer/puppeteer/commit/817288cd901121ddc8a44226eda689bb784cee61)) +* **browsers:** various fixes and improvements ([#9966](https://github.com/puppeteer/puppeteer/issues/9966)) ([f1211cb](https://github.com/puppeteer/puppeteer/commit/f1211cbec091ec669de019aeb7fb4f011a81c1d7)) +* consider downloadHost as baseUrl ([#9973](https://github.com/puppeteer/puppeteer/issues/9973)) ([05a44af](https://github.com/puppeteer/puppeteer/commit/05a44afe5affcac9fe0f0a2e83f17807c99b2f0c)) + +## [0.3.2](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.1...browsers-v0.3.2) (2023-04-03) + + +### Bug Fixes + +* typo in the browsers package ([#9957](https://github.com/puppeteer/puppeteer/issues/9957)) ([c780384](https://github.com/puppeteer/puppeteer/commit/c7803844cf10b6edaa2da83134029b7acf5b45b2)) + +## [0.3.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.3.0...browsers-v0.3.1) (2023-03-29) + + +### Bug Fixes + +* bump @puppeteer/browsers ([#9938](https://github.com/puppeteer/puppeteer/issues/9938)) ([2a29d30](https://github.com/puppeteer/puppeteer/commit/2a29d30d1790b47c99f8d196b3844364d351acbd)) + +## [0.3.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.2.0...browsers-v0.3.0) (2023-03-27) + + +### Features + +* update Chrome browser binaries ([#9917](https://github.com/puppeteer/puppeteer/issues/9917)) ([fcb233c](https://github.com/puppeteer/puppeteer/commit/fcb233ce949f5f716aee39253e910104b04aa000)) + +## [0.2.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.1.1...browsers-v0.2.0) (2023-03-24) + + +### Features + +* implement a command to clear the cache ([#9868](https://github.com/puppeteer/puppeteer/issues/9868)) ([b8d38cb](https://github.com/puppeteer/puppeteer/commit/b8d38cb05f7eedf554ed46f2f7428b621197d1cc)) + +## [0.1.1](https://github.com/puppeteer/puppeteer/compare/browsers-v0.1.0...browsers-v0.1.1) (2023-03-14) + + +### Bug Fixes + +* export ChromeReleaseChannel ([#9851](https://github.com/puppeteer/puppeteer/issues/9851)) ([3e7a514](https://github.com/puppeteer/puppeteer/commit/3e7a514e556ddb4306aa3c15f24c512beaac65f4)) + +## [0.1.0](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.5...browsers-v0.1.0) (2023-03-14) + + +### Features + +* implement system channels for chrome in browsers ([#9844](https://github.com/puppeteer/puppeteer/issues/9844)) ([dec48a9](https://github.com/puppeteer/puppeteer/commit/dec48a95923e21a054c1d70d22c14001a0150293)) + + +### Bug Fixes + +* add browsers entry point ([#9846](https://github.com/puppeteer/puppeteer/issues/9846)) ([1a1e79d](https://github.com/puppeteer/puppeteer/commit/1a1e79d046ccad6fe843aa219501c17da08bc498)) + +## [0.0.5](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.4...browsers-v0.0.5) (2023-03-07) + + +### Bug Fixes + +* change the install output to include the executable path ([#9797](https://github.com/puppeteer/puppeteer/issues/9797)) ([8cca7bb](https://github.com/puppeteer/puppeteer/commit/8cca7bb7a2a1cdf62919d9c7eca62d6774e698db)) + +## [0.0.4](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.3...browsers-v0.0.4) (2023-03-06) + + +### Features + +* browsers: recognize chromium as a valid browser ([#9760](https://github.com/puppeteer/puppeteer/issues/9760)) ([04247a4](https://github.com/puppeteer/puppeteer/commit/04247a4e00b43683977bd8aa309d493eee663735)) + +## [0.0.3](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.2...browsers-v0.0.3) (2023-02-22) + + +### Bug Fixes + +* define options per command ([#9733](https://github.com/puppeteer/puppeteer/issues/9733)) ([8bae054](https://github.com/puppeteer/puppeteer/commit/8bae0545b7321d398dae3f522952dd981111587e)) + +## [0.0.2](https://github.com/puppeteer/puppeteer/compare/browsers-v0.0.1...browsers-v0.0.2) (2023-02-22) + + +### Bug Fixes + +* permissions for the browser CLI ([#9731](https://github.com/puppeteer/puppeteer/issues/9731)) ([e944931](https://github.com/puppeteer/puppeteer/commit/e944931de22726f35c5c83052892f8ab4667b035)) + +## 0.0.1 (2023-02-22) + + +### Features + +* initial release of browsers ([#9722](https://github.com/puppeteer/puppeteer/issues/9722)) ([#9727](https://github.com/puppeteer/puppeteer/issues/9727)) ([86a2d1d](https://github.com/puppeteer/puppeteer/commit/86a2d1dd3b2c024b886c6280e08a2d7dc8caabc5)) diff --git a/remote/test/puppeteer/packages/browsers/README.md b/remote/test/puppeteer/packages/browsers/README.md new file mode 100644 index 0000000000..4c40b78e3f --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/README.md @@ -0,0 +1,28 @@ +# @puppeteer/browsers + +Manage and launch browsers/drivers from a CLI or programmatically. + +## CLI + +Use `npx` to run the CLI: + +```bash +npx @puppeteer/browsers --help +``` + +CLI help will provide all documentation you need to use the CLI. + +```bash +npx @puppeteer/browsers --help # help for all commands +npx @puppeteer/browsers install --help # help for the install command +npx @puppeteer/browsers launch --help # help for the launch command +``` + +## Known limitations + +1. We support installing and running Firefox and Chrome/Chromium. The `latest` keyword only works during the installation. For the `launch` command you need to specify an exact build ID. The build ID is provided by the `install` command (see `npx @puppeteer/browsers install --help` for the format). +2. Launching the system browsers is only possible for Chrome/Chromium. + +## API + +The programmatic API allows installing and launching browsers from your code. See the `test` folder for examples on how to use the `install`, `canInstall`, `launch`, `computeExecutablePath`, `computeSystemExecutablePath` and other methods. diff --git a/remote/test/puppeteer/packages/browsers/api-extractor.docs.json b/remote/test/puppeteer/packages/browsers/api-extractor.docs.json new file mode 100644 index 0000000000..6a41a3b59c --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/api-extractor.docs.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/main.d.ts", + + "extends": "./api-extractor.json", + + "dtsRollup": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json" + } +} diff --git a/remote/test/puppeteer/packages/browsers/api-extractor.json b/remote/test/puppeteer/packages/browsers/api-extractor.json new file mode 100644 index 0000000000..da1caae622 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/api-extractor.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/main.d.ts", + "bundledPackages": [], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": false + }, + + "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/remote/test/puppeteer/packages/browsers/package.json b/remote/test/puppeteer/packages/browsers/package.json new file mode 100644 index 0000000000..c28787e197 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/package.json @@ -0,0 +1,123 @@ +{ + "name": "@puppeteer/browsers", + "version": "1.0.0", + "description": "Download and launch browsers", + "scripts": { + "build:docs": "wireit", + "build": "wireit", + "build:test": "wireit", + "clean": "tsc --build --clean && rm -rf lib", + "test": "wireit" + }, + "bin": { + "@puppeteer/browsers": "lib/cjs/main-cli.js" + }, + "main": "./lib/cjs/main.js", + "module": "./lib/esm/main.js", + "type": "commonjs", + "exports": { + ".": { + "import": "./lib/esm/main.js", + "require": "./lib/cjs/main.js" + } + }, + "wireit": { + "build": { + "command": "tsc -b && tsx ../../tools/chmod.ts 755 lib/cjs/main-cli.js lib/esm/main-cli.js", + "files": [ + "src/**/*.ts", + "tsconfig.json" + ], + "clean": "if-file-deleted", + "output": [ + "lib/**", + "!lib/esm/package.json" + ], + "dependencies": [ + "generate:package-json" + ] + }, + "generate:package-json": { + "command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json", + "files": [ + "../../tools/generate_module_package_json.ts" + ], + "output": [ + "lib/esm/package.json" + ] + }, + "build:docs": { + "command": "api-extractor run --local --config \"./api-extractor.docs.json\"", + "files": [ + "api-extractor.docs.json", + "lib/esm/main.d.ts", + "tsconfig.json" + ], + "dependencies": [ + "build" + ] + }, + "build:test": { + "command": "tsc -b test/src/tsconfig.json", + "files": [ + "test/**/*.ts", + "test/src/tsconfig.json" + ], + "output": [ + "test/build/**" + ], + "dependencies": [ + "build", + "../testserver:build" + ] + }, + "test": { + "command": "node tools/downloadTestBrowsers.mjs && cross-env DEBUG=puppeteer:* mocha", + "files": [ + ".mocharc.cjs" + ], + "dependencies": [ + "build:test" + ] + } + }, + "keywords": [ + "puppeteer", + "browsers" + ], + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/browsers" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "lib", + "!*.tsbuildinfo" + ], + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@types/yargs": "17.0.22" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/CLI.ts b/remote/test/puppeteer/packages/browsers/src/CLI.ts new file mode 100644 index 0000000000..6767002269 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/CLI.ts @@ -0,0 +1,313 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {stdin as input, stdout as output} from 'process'; +import * as readline from 'readline'; + +import ProgressBar from 'progress'; +import type * as Yargs from 'yargs'; +import {hideBin} from 'yargs/helpers'; +import yargs from 'yargs/yargs'; + +import { + resolveBuildId, + Browser, + BrowserPlatform, + ChromeReleaseChannel, +} from './browser-data/browser-data.js'; +import {Cache} from './Cache.js'; +import {detectBrowserPlatform} from './detectPlatform.js'; +import {install} from './install.js'; +import { + computeExecutablePath, + computeSystemExecutablePath, + launch, +} from './launch.js'; + +type InstallArgs = { + browser: { + name: Browser; + buildId: string; + }; + path?: string; + platform?: BrowserPlatform; + baseUrl?: string; +}; + +type LaunchArgs = { + browser: { + name: Browser; + buildId: string; + }; + path?: string; + platform?: BrowserPlatform; + detached: boolean; + system: boolean; +}; + +type ClearArgs = { + path?: string; +}; + +/** + * @public + */ +export class CLI { + #cachePath; + #rl?: readline.Interface; + + constructor(cachePath = process.cwd(), rl?: readline.Interface) { + this.#cachePath = cachePath; + this.#rl = rl; + } + + #defineBrowserParameter(yargs: Yargs.Argv<unknown>): void { + yargs.positional('browser', { + description: + 'Which browser to install <browser>[@<buildId|latest>]. `latest` will try to find the latest available build. `buildId` is a browser-specific identifier such as a version or a revision.', + type: 'string', + coerce: (opt): InstallArgs['browser'] => { + return { + name: this.#parseBrowser(opt), + buildId: this.#parseBuildId(opt), + }; + }, + }); + } + + #definePlatformParameter(yargs: Yargs.Argv<unknown>): void { + yargs.option('platform', { + type: 'string', + desc: 'Platform that the binary needs to be compatible with.', + choices: Object.values(BrowserPlatform), + defaultDescription: 'Auto-detected', + }); + } + + #definePathParameter(yargs: Yargs.Argv<unknown>, required = false): void { + yargs.option('path', { + type: 'string', + desc: 'Path to the root folder for the browser downloads and installation. The installation folder structure is compatible with the cache structure used by Puppeteer.', + defaultDescription: 'Current working directory', + ...(required ? {} : {default: process.cwd()}), + }); + if (required) { + yargs.demandOption('path'); + } + } + + async run(argv: string[]): Promise<void> { + const yargsInstance = yargs(hideBin(argv)); + await yargsInstance + .scriptName('@puppeteer/browsers') + .command( + 'install <browser>', + 'Download and install the specified browser. If successful, the command outputs the actual browser buildId that was installed and the absolute path to the browser executable (format: <browser>@<buildID> <path>).', + yargs => { + this.#defineBrowserParameter(yargs); + this.#definePlatformParameter(yargs); + this.#definePathParameter(yargs); + yargs.option('base-url', { + type: 'string', + desc: 'Base URL to download from', + }); + yargs.example( + '$0 install chrome', + 'Install the latest available build of the Chrome browser.' + ); + yargs.example( + '$0 install chrome@latest', + 'Install the latest available build for the Chrome browser.' + ); + yargs.example( + '$0 install chromium@1083080', + 'Install the revision 1083080 of the Chromium browser.' + ); + yargs.example( + '$0 install firefox', + 'Install the latest available build of the Firefox browser.' + ); + yargs.example( + '$0 install firefox --platform mac', + 'Install the latest Mac (Intel) build of the Firefox browser.' + ); + yargs.example( + '$0 install firefox --path /tmp/my-browser-cache', + 'Install to the specified cache directory.' + ); + }, + async argv => { + const args = argv as unknown as InstallArgs; + args.platform ??= detectBrowserPlatform(); + if (!args.platform) { + throw new Error(`Could not resolve the current platform`); + } + args.browser.buildId = await resolveBuildId( + args.browser.name, + args.platform, + args.browser.buildId + ); + await install({ + browser: args.browser.name, + buildId: args.browser.buildId, + platform: args.platform, + cacheDir: args.path ?? this.#cachePath, + downloadProgressCallback: makeProgressCallback( + args.browser.name, + args.browser.buildId + ), + baseUrl: args.baseUrl, + }); + console.log( + `${args.browser.name}@${ + args.browser.buildId + } ${computeExecutablePath({ + browser: args.browser.name, + buildId: args.browser.buildId, + cacheDir: args.path ?? this.#cachePath, + platform: args.platform, + })}` + ); + } + ) + .command( + 'launch <browser>', + 'Launch the specified browser', + yargs => { + this.#defineBrowserParameter(yargs); + this.#definePlatformParameter(yargs); + this.#definePathParameter(yargs); + yargs.option('detached', { + type: 'boolean', + desc: 'Detach the child process.', + default: false, + }); + yargs.option('system', { + type: 'boolean', + desc: 'Search for a browser installed on the system instead of the cache folder.', + default: false, + }); + yargs.example( + '$0 launch chrome@1083080', + 'Launch the Chrome browser identified by the revision 1083080.' + ); + yargs.example( + '$0 launch firefox@112.0a1', + 'Launch the Firefox browser identified by the milestone 112.0a1.' + ); + yargs.example( + '$0 launch chrome@1083080 --detached', + 'Launch the browser but detach the sub-processes.' + ); + yargs.example( + '$0 launch chrome@canary --system', + 'Try to locate the Canary build of Chrome installed on the system and launch it.' + ); + }, + async argv => { + const args = argv as unknown as LaunchArgs; + const executablePath = args.system + ? computeSystemExecutablePath({ + browser: args.browser.name, + // TODO: throw an error if not a ChromeReleaseChannel is provided. + channel: args.browser.buildId as ChromeReleaseChannel, + platform: args.platform, + }) + : computeExecutablePath({ + browser: args.browser.name, + buildId: args.browser.buildId, + cacheDir: args.path ?? this.#cachePath, + platform: args.platform, + }); + launch({ + executablePath, + detached: args.detached, + }); + } + ) + .command( + 'clear', + 'Removes all installed browsers from the specified cache directory', + yargs => { + this.#definePathParameter(yargs, true); + }, + async argv => { + const args = argv as unknown as ClearArgs; + const cacheDir = args.path ?? this.#cachePath; + const rl = this.#rl ?? readline.createInterface({input, output}); + rl.question( + `Do you want to permanently and recursively delete the content of ${cacheDir} (yes/No)? `, + answer => { + rl.close(); + if (!['y', 'yes'].includes(answer.toLowerCase().trim())) { + console.log('Cancelled.'); + return; + } + const cache = new Cache(cacheDir); + cache.clear(); + console.log(`${cacheDir} cleared.`); + } + ); + } + ) + .demandCommand(1) + .help() + .wrap(Math.min(120, yargsInstance.terminalWidth())) + .parse(); + } + + #parseBrowser(version: string): Browser { + return version.split('@').shift() as Browser; + } + + #parseBuildId(version: string): string { + return version.split('@').pop() ?? 'latest'; + } +} + +/** + * @public + */ +export function makeProgressCallback( + browser: Browser, + buildId: string +): (downloadedBytes: number, totalBytes: number) => void { + let progressBar: ProgressBar; + let lastDownloadedBytes = 0; + return (downloadedBytes: number, totalBytes: number) => { + if (!progressBar) { + progressBar = new ProgressBar( + `Downloading ${browser} r${buildId} - ${toMegabytes( + totalBytes + )} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + }; +} + +function toMegabytes(bytes: number) { + const mb = bytes / 1000 / 1000; + return `${Math.round(mb * 10) / 10} MB`; +} diff --git a/remote/test/puppeteer/packages/browsers/src/Cache.ts b/remote/test/puppeteer/packages/browsers/src/Cache.ts new file mode 100644 index 0000000000..142bceb08e --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/Cache.ts @@ -0,0 +1,119 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import {Browser, BrowserPlatform} from './browser-data/browser-data.js'; + +/** + * @public + */ +export type InstalledBrowser = { + path: string; + browser: Browser; + buildId: string; + platform: BrowserPlatform; +}; + +/** + * The cache used by Puppeteer relies on the following structure: + * + * - rootDir + * -- <browser1> | browserRoot(browser1) + * ---- <platform>-<buildId> | installationDir() + * ------ the browser-platform-buildId + * ------ specific structure. + * -- <browser2> | browserRoot(browser2) + * ---- <platform>-<buildId> | installationDir() + * ------ the browser-platform-buildId + * ------ specific structure. + * @internal + */ +export class Cache { + #rootDir: string; + + constructor(rootDir: string) { + this.#rootDir = rootDir; + } + + browserRoot(browser: Browser): string { + return path.join(this.#rootDir, browser); + } + + installationDir( + browser: Browser, + platform: BrowserPlatform, + buildId: string + ): string { + return path.join(this.browserRoot(browser), `${platform}-${buildId}`); + } + + clear(): void { + fs.rmSync(this.#rootDir, { + force: true, + recursive: true, + maxRetries: 10, + retryDelay: 500, + }); + } + + getInstalledBrowsers(): InstalledBrowser[] { + if (!fs.existsSync(this.#rootDir)) { + return []; + } + const types = fs.readdirSync(this.#rootDir); + const browsers = types.filter((t): t is Browser => { + return (Object.values(Browser) as string[]).includes(t); + }); + return browsers.flatMap(browser => { + const files = fs.readdirSync(this.browserRoot(browser)); + return files + .map(file => { + const result = parseFolderPath( + path.join(this.browserRoot(browser), file) + ); + if (!result) { + return null; + } + return { + path: path.join(this.browserRoot(browser), file), + browser, + platform: result.platform, + buildId: result.buildId, + }; + }) + .filter((item): item is InstalledBrowser => { + return item !== null; + }); + }); + } +} + +function parseFolderPath( + folderPath: string +): {platform: string; buildId: string} | undefined { + const name = path.basename(folderPath); + const splits = name.split('-'); + if (splits.length !== 2) { + return; + } + const [platform, buildId] = splits; + if (!buildId || !platform) { + return; + } + return {platform, buildId}; +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts new file mode 100644 index 0000000000..413435453a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/browser-data.ts @@ -0,0 +1,124 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chrome from './chrome.js'; +import * as chromedriver from './chromedriver.js'; +import * as chromium from './chromium.js'; +import * as firefox from './firefox.js'; +import { + Browser, + BrowserPlatform, + BrowserTag, + ChromeReleaseChannel, + ProfileOptions, +} from './types.js'; + +export {ProfileOptions}; + +export const downloadUrls = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl, + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.CHROMIUM]: chromium.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; + +export const downloadPaths = { + [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath, + [Browser.CHROME]: chrome.resolveDownloadPath, + [Browser.CHROMIUM]: chromium.resolveDownloadPath, + [Browser.FIREFOX]: firefox.resolveDownloadPath, +}; + +export const executablePathByBrowser = { + [Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath, + [Browser.CHROME]: chrome.relativeExecutablePath, + [Browser.CHROMIUM]: chromium.relativeExecutablePath, + [Browser.FIREFOX]: firefox.relativeExecutablePath, +}; + +export {Browser, BrowserPlatform, ChromeReleaseChannel}; + +/** + * @public + */ +export async function resolveBuildId( + browser: Browser, + platform: BrowserPlatform, + tag: string +): Promise<string> { + switch (browser) { + case Browser.FIREFOX: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await firefox.resolveBuildId('FIREFOX_NIGHTLY'); + } + case Browser.CHROME: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + // In CfT beta is the latest version. + return await chrome.resolveBuildId(platform, 'beta'); + } + case Browser.CHROMEDRIVER: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await chromedriver.resolveBuildId('latest'); + } + case Browser.CHROMIUM: + switch (tag as BrowserTag) { + case BrowserTag.LATEST: + return await chromium.resolveBuildId(platform, 'latest'); + } + } + // We assume the tag is the buildId if it didn't match any keywords. + return tag; +} + +/** + * @public + */ +export async function createProfile( + browser: Browser, + opts: ProfileOptions +): Promise<void> { + switch (browser) { + case Browser.FIREFOX: + return await firefox.createProfile(opts); + case Browser.CHROME: + case Browser.CHROMIUM: + throw new Error(`Profile creation is not support for ${browser} yet`); + } +} + +/** + * @public + */ +export function resolveSystemExecutablePath( + browser: Browser, + platform: BrowserPlatform, + channel: ChromeReleaseChannel +): string { + switch (browser) { + case Browser.CHROMEDRIVER: + case Browser.FIREFOX: + throw new Error( + `System browser detection is not supported for ${browser} yet.` + ); + case Browser.CHROME: + return chromium.resolveSystemExecutablePath(platform, channel); + case Browser.CHROMIUM: + return chrome.resolveSystemExecutablePath(platform, channel); + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts new file mode 100644 index 0000000000..1fbf8c9647 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chrome.ts @@ -0,0 +1,169 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform, ChromeReleaseChannel} from './types.js'; + +function folder(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'linux64'; + case BrowserPlatform.MAC_ARM: + return 'mac-arm64'; + case BrowserPlatform.MAC: + return 'mac-x64'; + case BrowserPlatform.WIN32: + return 'win32'; + case BrowserPlatform.WIN64: + return 'win64'; + } +} + +function chromiumDashPlatform(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'linux'; + case BrowserPlatform.MAC_ARM: + return 'mac'; + case BrowserPlatform.MAC: + return 'mac'; + case BrowserPlatform.WIN32: + return 'win'; + case BrowserPlatform.WIN64: + return 'win64'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [buildId, folder(platform), `chrome-${folder(platform)}.zip`]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + return path.join( + 'chrome-' + folder(platform), + 'Google Chrome for Testing.app', + 'Contents', + 'MacOS', + 'Google Chrome for Testing' + ); + case BrowserPlatform.LINUX: + return path.join('chrome-linux64', 'chrome'); + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return path.join('chrome-' + folder(platform), 'chrome.exe'); + } +} +export async function resolveBuildId( + platform: BrowserPlatform, + channel: 'beta' | 'stable' = 'beta' +): Promise<string> { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL( + `https://chromiumdash.appspot.com/fetch_releases?platform=${chromiumDashPlatform( + platform + )}&channel=${channel}` + ), + 'GET', + response => { + let data = ''; + if (response.statusCode && response.statusCode >= 400) { + return reject(new Error(`Got status code ${response.statusCode}`)); + } + response.on('data', chunk => { + data += chunk; + }); + response.on('end', () => { + try { + const response = JSON.parse(String(data)); + return resolve(response[0].version); + } catch { + return reject(new Error('Chrome version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} + +export function resolveSystemExecutablePath( + platform: BrowserPlatform, + channel: ChromeReleaseChannel +): string { + switch (platform) { + case BrowserPlatform.WIN64: + case BrowserPlatform.WIN32: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`; + case ChromeReleaseChannel.BETA: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`; + case ChromeReleaseChannel.CANARY: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`; + case ChromeReleaseChannel.DEV: + return `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`; + } + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + case ChromeReleaseChannel.BETA: + return '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'; + case ChromeReleaseChannel.CANARY: + return '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; + case ChromeReleaseChannel.DEV: + return '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'; + } + case BrowserPlatform.LINUX: + switch (channel) { + case ChromeReleaseChannel.STABLE: + return '/opt/google/chrome/chrome'; + case ChromeReleaseChannel.BETA: + return '/opt/google/chrome-beta/chrome'; + case ChromeReleaseChannel.DEV: + return '/opt/google/chrome-unstable/chrome'; + } + } + + throw new Error( + `Unable to detect browser executable path for '${channel}' on ${platform}.` + ); +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts new file mode 100644 index 0000000000..39894d2e86 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromedriver.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform} from './types.js'; + +function archive(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'chromedriver_linux64'; + case BrowserPlatform.MAC_ARM: + return 'chromedriver_mac_arm64'; + case BrowserPlatform.MAC: + return 'chromedriver_mac64'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver_win32'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://chromedriver.storage.googleapis.com' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [buildId, `${archive(platform)}.zip`]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.LINUX: + return 'chromedriver'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return 'chromedriver.exe'; + } +} +export async function resolveBuildId( + _channel: 'latest' = 'latest' +): Promise<string> { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL(`https://chromedriver.storage.googleapis.com/LATEST_RELEASE`), + 'GET', + response => { + let data = ''; + if (response.statusCode && response.statusCode >= 400) { + return reject(new Error(`Got status code ${response.statusCode}`)); + } + response.on('data', chunk => { + data += chunk; + }); + response.on('end', () => { + try { + return resolve(String(data)); + } catch { + return reject(new Error('Chrome version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts new file mode 100644 index 0000000000..71fa003e0a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/chromium.ts @@ -0,0 +1,125 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform} from './types.js'; + +export {resolveSystemExecutablePath} from './chrome.js'; + +function archive(platform: BrowserPlatform, buildId: string): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'chrome-linux'; + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + return 'chrome-mac'; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + // Windows archive name changed at r591479. + return parseInt(buildId, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; + } +} + +function folder(platform: BrowserPlatform): string { + switch (platform) { + case BrowserPlatform.LINUX: + return 'Linux_x64'; + case BrowserPlatform.MAC_ARM: + return 'Mac_Arm'; + case BrowserPlatform.MAC: + return 'Mac'; + case BrowserPlatform.WIN32: + return 'Win'; + case BrowserPlatform.WIN64: + return 'Win_x64'; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [folder(platform), buildId, `${archive(platform, buildId)}.zip`]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + return path.join( + 'chrome-mac', + 'Chromium.app', + 'Contents', + 'MacOS', + 'Chromium' + ); + case BrowserPlatform.LINUX: + return path.join('chrome-linux', 'chrome'); + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return path.join('chrome-win', 'chrome.exe'); + } +} +export async function resolveBuildId( + platform: BrowserPlatform, + // We will need it for other channels/keywords. + _channel: 'latest' = 'latest' +): Promise<string> { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL( + `https://storage.googleapis.com/chromium-browser-snapshots/${folder( + platform + )}/LAST_CHANGE` + ), + 'GET', + response => { + let data = ''; + if (response.statusCode && response.statusCode >= 400) { + return reject(new Error(`Got status code ${response.statusCode}`)); + } + response.on('data', chunk => { + data += chunk; + }); + response.on('end', () => { + try { + return resolve(String(data)); + } catch { + return reject(new Error('Chrome version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts new file mode 100644 index 0000000000..c3f337ee5a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/firefox.ts @@ -0,0 +1,355 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import {httpRequest} from '../httpUtil.js'; + +import {BrowserPlatform, ProfileOptions} from './types.js'; + +function archive(platform: BrowserPlatform, buildId: string): string { + switch (platform) { + case BrowserPlatform.LINUX: + return `firefox-${buildId}.en-US.${platform}-x86_64.tar.bz2`; + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + return `firefox-${buildId}.en-US.mac.dmg`; + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return `firefox-${buildId}.en-US.${platform}.zip`; + } +} + +export function resolveDownloadUrl( + platform: BrowserPlatform, + buildId: string, + baseUrl = 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central' +): string { + return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`; +} + +export function resolveDownloadPath( + platform: BrowserPlatform, + buildId: string +): string[] { + return [archive(platform, buildId)]; +} + +export function relativeExecutablePath( + platform: BrowserPlatform, + _buildId: string +): string { + switch (platform) { + case BrowserPlatform.MAC_ARM: + case BrowserPlatform.MAC: + return path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox'); + case BrowserPlatform.LINUX: + return path.join('firefox', 'firefox'); + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return path.join('firefox', 'firefox.exe'); + } +} + +export async function resolveBuildId( + channel: 'FIREFOX_NIGHTLY' = 'FIREFOX_NIGHTLY' +): Promise<string> { + return new Promise((resolve, reject) => { + const request = httpRequest( + new URL('https://product-details.mozilla.org/1.0/firefox_versions.json'), + 'GET', + response => { + let data = ''; + if (response.statusCode && response.statusCode >= 400) { + return reject(new Error(`Got status code ${response.statusCode}`)); + } + response.on('data', chunk => { + data += chunk; + }); + response.on('end', () => { + try { + const versions = JSON.parse(data); + return resolve(versions[channel]); + } catch { + return reject(new Error('Firefox version not found')); + } + }); + }, + false + ); + request.on('error', err => { + reject(err); + }); + }); +} + +export async function createProfile(options: ProfileOptions): Promise<void> { + if (!fs.existsSync(options.path)) { + await fs.promises.mkdir(options.path, { + recursive: true, + }); + } + await writePreferences({ + preferences: { + ...defaultProfilePreferences(options.preferences), + ...options.preferences, + }, + path: options.path, + }); +} + +function defaultProfilePreferences( + extraPrefs: Record<string, unknown> +): Record<string, unknown> { + const server = 'dummy.test'; + + const defaultPrefs = { + // Make sure Shield doesn't hit the network. + 'app.normandy.api_url': '', + // Disable Firefox old build background check + 'app.update.checkInstallTime': false, + // Disable automatically upgrading Firefox + 'app.update.disabledForTesting': true, + + // Increase the APZ content response timeout to 1 minute + 'apz.content_response_timeout': 60000, + + // Prevent various error message on the console + // jest-puppeteer asserts that no error message is emitted by the console + 'browser.contentblocking.features.standard': + '-tp,tpPrivate,cookieBehavior0,-cm,-fp', + + // Enable the dump function: which sends messages to the system + // console + // https://bugzilla.mozilla.org/show_bug.cgi?id=1543115 + 'browser.dom.window.dump.enabled': true, + // Disable topstories + 'browser.newtabpage.activity-stream.feeds.system.topstories': false, + // Always display a blank page + 'browser.newtabpage.enabled': false, + // Background thumbnails in particular cause grief: and disabling + // thumbnails in general cannot hurt + 'browser.pagethumbnails.capturing_disabled': true, + + // Disable safebrowsing components. + 'browser.safebrowsing.blockedURIs.enabled': false, + 'browser.safebrowsing.downloads.enabled': false, + 'browser.safebrowsing.malware.enabled': false, + 'browser.safebrowsing.passwords.enabled': false, + 'browser.safebrowsing.phishing.enabled': false, + + // Disable updates to search engines. + 'browser.search.update': false, + // Do not restore the last open set of tabs if the browser has crashed + 'browser.sessionstore.resume_from_crash': false, + // Skip check for default browser on startup + 'browser.shell.checkDefaultBrowser': false, + + // Disable newtabpage + 'browser.startup.homepage': 'about:blank', + // Do not redirect user when a milstone upgrade of Firefox is detected + 'browser.startup.homepage_override.mstone': 'ignore', + // Start with a blank page about:blank + 'browser.startup.page': 0, + + // Do not allow background tabs to be zombified on Android: otherwise for + // tests that open additional tabs: the test harness tab itself might get + // unloaded + 'browser.tabs.disableBackgroundZombification': false, + // Do not warn when closing all other open tabs + 'browser.tabs.warnOnCloseOtherTabs': false, + // Do not warn when multiple tabs will be opened + 'browser.tabs.warnOnOpen': false, + + // Disable page translations, which can cause issues with tests. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1836093. + 'browser.translations.enable': false, + + // Disable the UI tour. + 'browser.uitour.enabled': false, + // Turn off search suggestions in the location bar so as not to trigger + // network connections. + 'browser.urlbar.suggest.searches': false, + // Disable first run splash page on Windows 10 + 'browser.usedOnWindows10.introURL': '', + // Do not warn on quitting Firefox + 'browser.warnOnQuit': false, + + // Defensively disable data reporting systems + 'datareporting.healthreport.documentServerURI': `http://${server}/dummy/healthreport/`, + 'datareporting.healthreport.logging.consoleEnabled': false, + 'datareporting.healthreport.service.enabled': false, + 'datareporting.healthreport.service.firstRun': false, + 'datareporting.healthreport.uploadEnabled': false, + + // Do not show datareporting policy notifications which can interfere with tests + 'datareporting.policy.dataSubmissionEnabled': false, + 'datareporting.policy.dataSubmissionPolicyBypassNotification': true, + + // DevTools JSONViewer sometimes fails to load dependencies with its require.js. + // This doesn't affect Puppeteer but spams console (Bug 1424372) + 'devtools.jsonview.enabled': false, + + // Disable popup-blocker + 'dom.disable_open_during_load': false, + + // Enable the support for File object creation in the content process + // Required for |Page.setFileInputFiles| protocol method. + 'dom.file.createInChild': true, + + // Disable the ProcessHangMonitor + 'dom.ipc.reportProcessHangs': false, + + // Disable slow script dialogues + 'dom.max_chrome_script_run_time': 0, + 'dom.max_script_run_time': 0, + + // Only load extensions from the application and user profile + // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION + 'extensions.autoDisableScopes': 0, + 'extensions.enabledScopes': 5, + + // Disable metadata caching for installed add-ons by default + 'extensions.getAddons.cache.enabled': false, + + // Disable installing any distribution extensions or add-ons. + 'extensions.installDistroAddons': false, + + // Disabled screenshots extension + 'extensions.screenshots.disabled': true, + + // Turn off extension updates so they do not bother tests + 'extensions.update.enabled': false, + + // Turn off extension updates so they do not bother tests + 'extensions.update.notifyUser': false, + + // Make sure opening about:addons will not hit the network + 'extensions.webservice.discoverURL': `http://${server}/dummy/discoveryURL`, + + // Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263) + 'fission.bfcacheInParent': false, + + // Force all web content to use a single content process + 'fission.webContentIsolationStrategy': 0, + + // Allow the application to have focus even it runs in the background + 'focusmanager.testmode': true, + // Disable useragent updates + 'general.useragent.updates.enabled': false, + // Always use network provider for geolocation tests so we bypass the + // macOS dialog raised by the corelocation provider + 'geo.provider.testing': true, + // Do not scan Wifi + 'geo.wifi.scan': false, + // No hang monitor + 'hangmonitor.timeout': 0, + // Show chrome errors and warnings in the error console + 'javascript.options.showInConsole': true, + + // Disable download and usage of OpenH264: and Widevine plugins + 'media.gmp-manager.updateEnabled': false, + // Prevent various error message on the console + // jest-puppeteer asserts that no error message is emitted by the console + 'network.cookie.cookieBehavior': 0, + + // Disable experimental feature that is only available in Nightly + 'network.cookie.sameSite.laxByDefault': false, + + // Do not prompt for temporary redirects + 'network.http.prompt-temp-redirect': false, + + // Disable speculative connections so they are not reported as leaking + // when they are hanging around + 'network.http.speculative-parallel-limit': 0, + + // Do not automatically switch between offline and online + 'network.manage-offline-status': false, + + // Make sure SNTP requests do not hit the network + 'network.sntp.pools': server, + + // Disable Flash. + 'plugin.state.flash': 0, + + 'privacy.trackingprotection.enabled': false, + + // Can be removed once Firefox 89 is no longer supported + // https://bugzilla.mozilla.org/show_bug.cgi?id=1710839 + 'remote.enabled': true, + + // Don't do network connections for mitm priming + 'security.certerrors.mitm.priming.enabled': false, + // Local documents have access to all other local documents, + // including directory listings + 'security.fileuri.strict_origin_policy': false, + // Do not wait for the notification button security delay + 'security.notification_enable_delay': 0, + + // Ensure blocklist updates do not hit the network + 'services.settings.server': `http://${server}/dummy/blocklist/`, + + // Do not automatically fill sign-in forms with known usernames and + // passwords + 'signon.autofillForms': false, + // Disable password capture, so that tests that include forms are not + // influenced by the presence of the persistent doorhanger notification + 'signon.rememberSignons': false, + + // Disable first-run welcome page + 'startup.homepage_welcome_url': 'about:blank', + + // Disable first-run welcome page + 'startup.homepage_welcome_url.additional': '', + + // Disable browser animations (tabs, fullscreen, sliding alerts) + 'toolkit.cosmeticAnimations.enabled': false, + + // Prevent starting into safe mode after application crashes + 'toolkit.startup.max_resumed_crashes': -1, + }; + + return Object.assign(defaultPrefs, extraPrefs); +} + +/** + * Populates the user.js file with custom preferences as needed to allow + * Firefox's CDP support to properly function. These preferences will be + * automatically copied over to prefs.js during startup of Firefox. To be + * able to restore the original values of preferences a backup of prefs.js + * will be created. + * + * @param prefs - List of preferences to add. + * @param profilePath - Firefox profile to write the preferences to. + */ +async function writePreferences(options: ProfileOptions): Promise<void> { + const lines = Object.entries(options.preferences).map(([key, value]) => { + return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`; + }); + + await fs.promises.writeFile( + path.join(options.path, 'user.js'), + lines.join('\n') + ); + + // Create a backup of the preferences file if it already exitsts. + const prefsPath = path.join(options.path, 'prefs.js'); + if (fs.existsSync(prefsPath)) { + const prefsBackupPath = path.join(options.path, 'prefs.js.puppeteer'); + await fs.promises.copyFile(prefsPath, prefsBackupPath); + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts b/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts new file mode 100644 index 0000000000..f88d2ca098 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/browser-data/types.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chrome from './chrome.js'; +import * as firefox from './firefox.js'; + +/** + * Supported browsers. + * + * @public + */ +export enum Browser { + CHROME = 'chrome', + CHROMIUM = 'chromium', + FIREFOX = 'firefox', + CHROMEDRIVER = 'chromedriver', +} + +/** + * Platform names used to identify a OS platfrom x architecture combination in the way + * that is relevant for the browser download. + * + * @public + */ +export enum BrowserPlatform { + LINUX = 'linux', + MAC = 'mac', + MAC_ARM = 'mac_arm', + WIN32 = 'win32', + WIN64 = 'win64', +} + +export const downloadUrls = { + [Browser.CHROME]: chrome.resolveDownloadUrl, + [Browser.CHROMIUM]: chrome.resolveDownloadUrl, + [Browser.FIREFOX]: firefox.resolveDownloadUrl, +}; + +/** + * @public + */ +export enum BrowserTag { + LATEST = 'latest', +} + +/** + * @public + */ +export interface ProfileOptions { + preferences: Record<string, unknown>; + path: string; +} + +/** + * @public + */ +export enum ChromeReleaseChannel { + STABLE = 'stable', + DEV = 'dev', + CANARY = 'canary', + BETA = 'beta', +} diff --git a/remote/test/puppeteer/packages/browsers/src/debug.ts b/remote/test/puppeteer/packages/browsers/src/debug.ts new file mode 100644 index 0000000000..eee0a347e8 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/debug.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import debug from 'debug'; + +export {debug}; diff --git a/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts b/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts new file mode 100644 index 0000000000..fed8c2e2ea --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/detectPlatform.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; + +import {BrowserPlatform} from './browser-data/browser-data.js'; + +/** + * @public + */ +export function detectBrowserPlatform(): BrowserPlatform | undefined { + const platform = os.platform(); + switch (platform) { + case 'darwin': + return os.arch() === 'arm64' + ? BrowserPlatform.MAC_ARM + : BrowserPlatform.MAC; + case 'linux': + return BrowserPlatform.LINUX; + case 'win32': + return os.arch() === 'x64' || + // Windows 11 for ARM supports x64 emulation + (os.arch() === 'arm64' && isWindows11(os.release())) + ? BrowserPlatform.WIN64 + : BrowserPlatform.WIN32; + default: + return undefined; + } +} + +/** + * Windows 11 is identified by the version 10.0.22000 or greater + * @internal + */ +function isWindows11(version: string): boolean { + const parts = version.split('.'); + if (parts.length > 2) { + const major = parseInt(parts[0] as string, 10); + const minor = parseInt(parts[1] as string, 10); + const patch = parseInt(parts[2] as string, 10); + return ( + major > 10 || + (major === 10 && minor > 0) || + (major === 10 && minor === 0 && patch >= 22000) + ); + } + return false; +} diff --git a/remote/test/puppeteer/packages/browsers/src/fileUtil.ts b/remote/test/puppeteer/packages/browsers/src/fileUtil.ts new file mode 100644 index 0000000000..6139accd49 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/fileUtil.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {exec as execChildProcess} from 'child_process'; +import {createReadStream} from 'fs'; +import {mkdir, readdir} from 'fs/promises'; +import * as path from 'path'; +import {promisify} from 'util'; + +import extractZip from 'extract-zip'; +import tar from 'tar-fs'; +import bzip from 'unbzip2-stream'; + +const exec = promisify(execChildProcess); + +/** + * @internal + */ +export async function unpackArchive( + archivePath: string, + folderPath: string +): Promise<void> { + if (archivePath.endsWith('.zip')) { + await extractZip(archivePath, {dir: folderPath}); + } else if (archivePath.endsWith('.tar.bz2')) { + await extractTar(archivePath, folderPath); + } else if (archivePath.endsWith('.dmg')) { + await mkdir(folderPath); + await installDMG(archivePath, folderPath); + } else { + throw new Error(`Unsupported archive format: ${archivePath}`); + } +} + +/** + * @internal + */ +function extractTar(tarPath: string, folderPath: string): Promise<void> { + return new Promise((fulfill, reject) => { + const tarStream = tar.extract(folderPath); + tarStream.on('error', reject); + tarStream.on('finish', fulfill); + const readStream = createReadStream(tarPath); + readStream.pipe(bzip()).pipe(tarStream); + }); +} + +/** + * @internal + */ +async function installDMG(dmgPath: string, folderPath: string): Promise<void> { + const {stdout} = await exec( + `hdiutil attach -nobrowse -noautoopen "${dmgPath}"` + ); + + const volumes = stdout.match(/\/Volumes\/(.*)/m); + if (!volumes) { + throw new Error(`Could not find volume path in ${stdout}`); + } + const mountPath = volumes[0]!; + + try { + const fileNames = await readdir(mountPath); + const appName = fileNames.find(item => { + return typeof item === 'string' && item.endsWith('.app'); + }); + if (!appName) { + throw new Error(`Cannot find app in ${mountPath}`); + } + const mountedPath = path.join(mountPath!, appName); + + await exec(`cp -R "${mountedPath}" "${folderPath}"`); + } finally { + await exec(`hdiutil detach "${mountPath}" -quiet`); + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/httpUtil.ts b/remote/test/puppeteer/packages/browsers/src/httpUtil.ts new file mode 100644 index 0000000000..5b6150f734 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/httpUtil.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createWriteStream} from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import {URL} from 'url'; + +import createHttpsProxyAgent from 'https-proxy-agent'; +import {getProxyForUrl} from 'proxy-from-env'; + +export function headHttpRequest(url: URL): Promise<boolean> { + return new Promise(resolve => { + const request = httpRequest( + url, + 'HEAD', + response => { + resolve(response.statusCode === 200); + }, + false + ); + request.on('error', () => { + resolve(false); + }); + }); +} + +export function httpRequest( + url: URL, + method: string, + response: (x: http.IncomingMessage) => void, + keepAlive = true +): http.ClientRequest { + const options: http.RequestOptions = { + protocol: url.protocol, + hostname: url.hostname, + port: url.port, + path: url.pathname + url.search, + method, + headers: keepAlive ? {Connection: 'keep-alive'} : undefined, + }; + + const proxyURL = getProxyForUrl(url.toString()); + if (proxyURL) { + const proxy = new URL(proxyURL); + if (proxy.protocol === 'http:') { + options.path = url.href; + options.hostname = proxy.hostname; + options.protocol = proxy.protocol; + options.port = proxy.port; + options.headers ??= {}; + options.headers['Host'] ||= url.host; + } else { + options.agent = createHttpsProxyAgent({ + host: proxy.host, + path: proxy.pathname, + port: proxy.port, + secureProxy: proxy.protocol === 'https:', + headers: options.headers, + }); + } + } + + const requestCallback = (res: http.IncomingMessage): void => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + httpRequest(new URL(res.headers.location), method, response); + } else { + response(res); + } + }; + const request = + options.protocol === 'https:' + ? https.request(options, requestCallback) + : http.request(options, requestCallback); + request.end(); + return request; +} + +/** + * @internal + */ +export function downloadFile( + url: URL, + destinationPath: string, + progressCallback?: (downloadedBytes: number, totalBytes: number) => void +): Promise<void> { + return new Promise<void>((resolve, reject) => { + let downloadedBytes = 0; + let totalBytes = 0; + + function onData(chunk: string): void { + downloadedBytes += chunk.length; + progressCallback!(downloadedBytes, totalBytes); + } + + const request = httpRequest(url, 'GET', response => { + if (response.statusCode !== 200) { + const error = new Error( + `Download failed: server returned code ${response.statusCode}. URL: ${url}` + ); + // consume response data to free up memory + response.resume(); + reject(error); + return; + } + const file = createWriteStream(destinationPath); + file.on('finish', () => { + return resolve(); + }); + file.on('error', error => { + return reject(error); + }); + response.pipe(file); + totalBytes = parseInt(response.headers['content-length']!, 10); + if (progressCallback) { + response.on('data', onData); + } + }); + request.on('error', error => { + return reject(error); + }); + }); +} diff --git a/remote/test/puppeteer/packages/browsers/src/install.ts b/remote/test/puppeteer/packages/browsers/src/install.ts new file mode 100644 index 0000000000..054e048420 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/install.ts @@ -0,0 +1,218 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {existsSync} from 'fs'; +import {mkdir, unlink} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import { + Browser, + BrowserPlatform, + downloadUrls, +} from './browser-data/browser-data.js'; +import {Cache, InstalledBrowser} from './Cache.js'; +import {debug} from './debug.js'; +import {detectBrowserPlatform} from './detectPlatform.js'; +import {unpackArchive} from './fileUtil.js'; +import {downloadFile, headHttpRequest} from './httpUtil.js'; + +const debugInstall = debug('puppeteer:browsers:install'); + +const times = new Map<string, [number, number]>(); +function debugTime(label: string) { + times.set(label, process.hrtime()); +} + +function debugTimeEnd(label: string) { + const end = process.hrtime(); + const start = times.get(label); + if (!start) { + return; + } + const duration = + end[0] * 1000 + end[1] / 1e6 - (start[0] * 1000 + start[1] / 1e6); // calculate duration in milliseconds + debugInstall(`Duration for ${label}: ${duration}ms`); +} + +/** + * @public + */ +export interface InstallOptions { + /** + * Determines the path to download browsers to. + */ + cacheDir: string; + /** + * Determines which platform the browser will be suited for. + * + * @defaultValue **Auto-detected.** + */ + platform?: BrowserPlatform; + /** + * Determines which browser to install. + */ + browser: Browser; + /** + * Determines which buildId to dowloand. BuildId should uniquely identify + * binaries and they are used for caching. + */ + buildId: string; + /** + * Provides information about the progress of the download. + */ + downloadProgressCallback?: ( + downloadedBytes: number, + totalBytes: number + ) => void; + /** + * Determines the host that will be used for downloading. + * + * @defaultValue Either + * + * - https://storage.googleapis.com/chromium-browser-snapshots or + * - https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central + * + */ + baseUrl?: string; + /** + * Whether to unpack and install browser archives. + * + * @defaultValue `true` + */ + unpack?: boolean; +} + +/** + * @public + */ +export async function install( + options: InstallOptions +): Promise<InstalledBrowser> { + options.platform ??= detectBrowserPlatform(); + options.unpack ??= true; + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const url = getDownloadUrl( + options.browser, + options.platform, + options.buildId, + options.baseUrl + ); + const fileName = url.toString().split('/').pop(); + assert(fileName, `A malformed download URL was found: ${url}.`); + const structure = new Cache(options.cacheDir); + const browserRoot = structure.browserRoot(options.browser); + const archivePath = path.join(browserRoot, fileName); + if (!existsSync(browserRoot)) { + await mkdir(browserRoot, {recursive: true}); + } + + if (!options.unpack) { + if (existsSync(archivePath)) { + return { + path: archivePath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + debugInstall(`Downloading binary from ${url}`); + debugTime('download'); + await downloadFile(url, archivePath, options.downloadProgressCallback); + debugTimeEnd('download'); + return { + path: archivePath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + + const outputPath = structure.installationDir( + options.browser, + options.platform, + options.buildId + ); + if (existsSync(outputPath)) { + return { + path: outputPath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; + } + try { + debugInstall(`Downloading binary from ${url}`); + try { + debugTime('download'); + await downloadFile(url, archivePath, options.downloadProgressCallback); + } finally { + debugTimeEnd('download'); + } + + debugInstall(`Installing ${archivePath} to ${outputPath}`); + try { + debugTime('extract'); + await unpackArchive(archivePath, outputPath); + } finally { + debugTimeEnd('extract'); + } + } finally { + if (existsSync(archivePath)) { + await unlink(archivePath); + } + } + return { + path: outputPath, + browser: options.browser, + platform: options.platform, + buildId: options.buildId, + }; +} + +/** + * @public + */ +export async function canDownload(options: InstallOptions): Promise<boolean> { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + return await headHttpRequest( + getDownloadUrl( + options.browser, + options.platform, + options.buildId, + options.baseUrl + ) + ); +} + +function getDownloadUrl( + browser: Browser, + platform: BrowserPlatform, + buildId: string, + baseUrl?: string +): URL { + return new URL(downloadUrls[browser](platform, buildId, baseUrl)); +} diff --git a/remote/test/puppeteer/packages/browsers/src/launch.ts b/remote/test/puppeteer/packages/browsers/src/launch.ts new file mode 100644 index 0000000000..9f8c8f20ed --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/launch.ts @@ -0,0 +1,494 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import childProcess from 'child_process'; +import {accessSync} from 'fs'; +import os from 'os'; +import path from 'path'; +import readline from 'readline'; + +import { + Browser, + BrowserPlatform, + executablePathByBrowser, + resolveSystemExecutablePath, + ChromeReleaseChannel, +} from './browser-data/browser-data.js'; +import {Cache} from './Cache.js'; +import {debug} from './debug.js'; +import {detectBrowserPlatform} from './detectPlatform.js'; + +const debugLaunch = debug('puppeteer:browsers:launcher'); + +/** + * @public + */ +export interface ComputeExecutablePathOptions { + /** + * Root path to the storage directory. + */ + cacheDir: string; + /** + * Determines which platform the browser will be suited for. + * + * @defaultValue **Auto-detected.** + */ + platform?: BrowserPlatform; + /** + * Determines which browser to launch. + */ + browser: Browser; + /** + * Determines which buildId to download. BuildId should uniquely identify + * binaries and they are used for caching. + */ + buildId: string; +} + +/** + * @public + */ +export function computeExecutablePath( + options: ComputeExecutablePathOptions +): string { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const installationDir = new Cache(options.cacheDir).installationDir( + options.browser, + options.platform, + options.buildId + ); + return path.join( + installationDir, + executablePathByBrowser[options.browser](options.platform, options.buildId) + ); +} + +/** + * @public + */ +export interface SystemOptions { + /** + * Determines which platform the browser will be suited for. + * + * @defaultValue **Auto-detected.** + */ + platform?: BrowserPlatform; + /** + * Determines which browser to launch. + */ + browser: Browser; + /** + * Release channel to look for on the system. + */ + channel: ChromeReleaseChannel; +} + +/** + * @public + */ +export function computeSystemExecutablePath(options: SystemOptions): string { + options.platform ??= detectBrowserPlatform(); + if (!options.platform) { + throw new Error( + `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})` + ); + } + const path = resolveSystemExecutablePath( + options.browser, + options.platform, + options.channel + ); + try { + accessSync(path); + } catch (error) { + throw new Error( + `Could not find Google Chrome executable for channel '${options.channel}' at '${path}'.` + ); + } + return path; +} + +/** + * @public + */ +export type LaunchOptions = { + executablePath: string; + pipe?: boolean; + dumpio?: boolean; + args?: string[]; + env?: Record<string, string | undefined>; + handleSIGINT?: boolean; + handleSIGTERM?: boolean; + handleSIGHUP?: boolean; + detached?: boolean; + onExit?: () => Promise<void>; +}; + +/** + * @public + */ +export function launch(opts: LaunchOptions): Process { + return new Process(opts); +} + +/** + * @public + */ +export const CDP_WEBSOCKET_ENDPOINT_REGEX = + /^DevTools listening on (ws:\/\/.*)$/; + +/** + * @public + */ +export const WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX = + /^WebDriver BiDi listening on (ws:\/\/.*)$/; + +/** + * @public + */ +export class Process { + #executablePath; + #args: string[]; + #browserProcess: childProcess.ChildProcess; + #exited = false; + // The browser process can be closed externally or from the driver process. We + // need to invoke the hooks only once though but we don't know how many times + // we will be invoked. + #hooksRan = false; + #onExitHook = async () => {}; + #browserProcessExiting: Promise<void>; + + constructor(opts: LaunchOptions) { + this.#executablePath = opts.executablePath; + this.#args = opts.args ?? []; + + opts.pipe ??= false; + opts.dumpio ??= false; + opts.handleSIGINT ??= true; + opts.handleSIGTERM ??= true; + opts.handleSIGHUP ??= true; + // On non-windows platforms, `detached: true` makes child process a + // leader of a new process group, making it possible to kill child + // process tree with `.kill(-pid)` command. @see + // https://nodejs.org/api/child_process.html#child_process_options_detached + opts.detached ??= process.platform !== 'win32'; + + const stdio = this.#configureStdio({ + pipe: opts.pipe, + dumpio: opts.dumpio, + }); + + debugLaunch(`Launching ${this.#executablePath} ${this.#args.join(' ')}`, { + detached: opts.detached, + env: opts.env, + stdio, + }); + + this.#browserProcess = childProcess.spawn( + this.#executablePath, + this.#args, + { + detached: opts.detached, + env: opts.env, + stdio, + } + ); + + debugLaunch(`Launched ${this.#browserProcess.pid}`); + if (opts.dumpio) { + this.#browserProcess.stderr?.pipe(process.stderr); + this.#browserProcess.stdout?.pipe(process.stdout); + } + process.on('exit', this.#onDriverProcessExit); + if (opts.handleSIGINT) { + process.on('SIGINT', this.#onDriverProcessSignal); + } + if (opts.handleSIGTERM) { + process.on('SIGTERM', this.#onDriverProcessSignal); + } + if (opts.handleSIGHUP) { + process.on('SIGHUP', this.#onDriverProcessSignal); + } + if (opts.onExit) { + this.#onExitHook = opts.onExit; + } + this.#browserProcessExiting = new Promise((resolve, reject) => { + this.#browserProcess.once('exit', async () => { + debugLaunch(`Browser process ${this.#browserProcess.pid} onExit`); + this.#clearListeners(); + this.#exited = true; + try { + await this.#runHooks(); + } catch (err) { + reject(err); + return; + } + resolve(); + }); + }); + } + + async #runHooks() { + if (this.#hooksRan) { + return; + } + this.#hooksRan = true; + await this.#onExitHook(); + } + + get nodeProcess(): childProcess.ChildProcess { + return this.#browserProcess; + } + + #configureStdio(opts: { + pipe: boolean; + dumpio: boolean; + }): Array<'ignore' | 'pipe'> { + if (opts.pipe) { + if (opts.dumpio) { + return ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']; + } else { + return ['ignore', 'ignore', 'ignore', 'pipe', 'pipe']; + } + } else { + if (opts.dumpio) { + return ['pipe', 'pipe', 'pipe']; + } else { + return ['pipe', 'ignore', 'pipe']; + } + } + } + + #clearListeners(): void { + process.off('exit', this.#onDriverProcessExit); + process.off('SIGINT', this.#onDriverProcessSignal); + process.off('SIGTERM', this.#onDriverProcessSignal); + process.off('SIGHUP', this.#onDriverProcessSignal); + } + + #onDriverProcessExit = (_code: number) => { + this.kill(); + }; + + #onDriverProcessSignal = (signal: string): void => { + switch (signal) { + case 'SIGINT': + this.kill(); + process.exit(130); + case 'SIGTERM': + case 'SIGHUP': + void this.close(); + break; + } + }; + + async close(): Promise<void> { + await this.#runHooks(); + if (!this.#exited) { + this.kill(); + } + return this.#browserProcessExiting; + } + + hasClosed(): Promise<void> { + return this.#browserProcessExiting; + } + + kill(): void { + debugLaunch(`Trying to kill ${this.#browserProcess.pid}`); + // If the process failed to launch (for example if the browser executable path + // is invalid), then the process does not get a pid assigned. A call to + // `proc.kill` would error, as the `pid` to-be-killed can not be found. + if ( + this.#browserProcess && + this.#browserProcess.pid && + pidExists(this.#browserProcess.pid) + ) { + try { + debugLaunch(`Browser process ${this.#browserProcess.pid} exists`); + if (process.platform === 'win32') { + try { + childProcess.execSync( + `taskkill /pid ${this.#browserProcess.pid} /T /F` + ); + } catch (error) { + debugLaunch( + `Killing ${this.#browserProcess.pid} using taskkill failed`, + error + ); + // taskkill can fail to kill the process e.g. due to missing permissions. + // Let's kill the process via Node API. This delays killing of all child + // processes of `this.proc` until the main Node.js process dies. + this.#browserProcess.kill(); + } + } else { + // on linux the process group can be killed with the group id prefixed with + // a minus sign. The process group id is the group leader's pid. + const processGroupId = -this.#browserProcess.pid; + + try { + process.kill(processGroupId, 'SIGKILL'); + } catch (error) { + debugLaunch( + `Killing ${this.#browserProcess.pid} using process.kill failed`, + error + ); + // Killing the process group can fail due e.g. to missing permissions. + // Let's kill the process via Node API. This delays killing of all child + // processes of `this.proc` until the main Node.js process dies. + this.#browserProcess.kill('SIGKILL'); + } + } + } catch (error) { + throw new Error( + `${PROCESS_ERROR_EXPLANATION}\nError cause: ${ + isErrorLike(error) ? error.stack : error + }` + ); + } + } + this.#clearListeners(); + } + + waitForLineOutput(regex: RegExp, timeout?: number): Promise<string> { + if (!this.#browserProcess.stderr) { + throw new Error('`browserProcess` does not have stderr.'); + } + const rl = readline.createInterface(this.#browserProcess.stderr); + let stderr = ''; + + return new Promise((resolve, reject) => { + rl.on('line', onLine); + rl.on('close', onClose); + this.#browserProcess.on('exit', onClose); + this.#browserProcess.on('error', onClose); + const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0; + + const cleanup = (): void => { + if (timeoutId) { + clearTimeout(timeoutId); + } + rl.off('line', onLine); + rl.off('close', onClose); + this.#browserProcess.off('exit', onClose); + this.#browserProcess.off('error', onClose); + }; + + function onClose(error?: Error): void { + cleanup(); + reject( + new Error( + [ + `Failed to launch the browser process!${ + error ? ' ' + error.message : '' + }`, + stderr, + '', + 'TROUBLESHOOTING: https://pptr.dev/troubleshooting', + '', + ].join('\n') + ) + ); + } + + function onTimeout(): void { + cleanup(); + reject( + new TimeoutError( + `Timed out after ${timeout} ms while waiting for the WS endpoint URL to appear in stdout!` + ) + ); + } + + function onLine(line: string): void { + stderr += line + '\n'; + const match = line.match(regex); + if (!match) { + return; + } + cleanup(); + // The RegExp matches, so this will obviously exist. + resolve(match[1]!); + } + }); + } +} + +const PROCESS_ERROR_EXPLANATION = `Puppeteer was unable to kill the process which ran the browser binary. +This means that, on future Puppeteer launches, Puppeteer might not be able to launch the browser. +Please check your open processes and ensure that the browser processes that Puppeteer launched have been killed. +If you think this is a bug, please report it on the Puppeteer issue tracker.`; + +/** + * @internal + */ +function pidExists(pid: number): boolean { + try { + return process.kill(pid, 0); + } catch (error) { + if (isErrnoException(error)) { + if (error.code && error.code === 'ESRCH') { + return false; + } + } + throw error; + } +} + +/** + * @internal + */ +export interface ErrorLike extends Error { + name: string; + message: string; +} + +/** + * @internal + */ +export function isErrorLike(obj: unknown): obj is ErrorLike { + return ( + typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj + ); +} +/** + * @internal + */ +export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException { + return ( + isErrorLike(obj) && + ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj) + ); +} + +/** + * @public + */ +export class TimeoutError extends Error { + /** + * @internal + */ + constructor(message?: string) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/main-cli.ts b/remote/test/puppeteer/packages/browsers/src/main-cli.ts new file mode 100644 index 0000000000..a086c1c3b9 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/main-cli.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CLI} from './CLI.js'; + +void new CLI().run(process.argv); diff --git a/remote/test/puppeteer/packages/browsers/src/main.ts b/remote/test/puppeteer/packages/browsers/src/main.ts new file mode 100644 index 0000000000..14ca6a838a --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/main.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + launch, + computeExecutablePath, + computeSystemExecutablePath, + TimeoutError, + LaunchOptions, + ComputeExecutablePathOptions as Options, + SystemOptions, + CDP_WEBSOCKET_ENDPOINT_REGEX, + WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, + Process, +} from './launch.js'; +export {install, canDownload, InstallOptions} from './install.js'; +export {detectBrowserPlatform} from './detectPlatform.js'; +export { + resolveBuildId, + Browser, + BrowserPlatform, + ChromeReleaseChannel, + createProfile, + ProfileOptions, +} from './browser-data/browser-data.js'; +export {CLI, makeProgressCallback} from './CLI.js'; +export {Cache, InstalledBrowser} from './Cache.js'; diff --git a/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json new file mode 100644 index 0000000000..ef01b990b7 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../lib/cjs" + } +} diff --git a/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json b/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json new file mode 100644 index 0000000000..a824bc8cb8 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/src/tsconfig.esm.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../lib/esm" + } +} diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts new file mode 100644 index 0000000000..c0404baad8 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/chrome-data.spec.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import path from 'path'; + +import { + BrowserPlatform, + ChromeReleaseChannel, +} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, + resolveSystemExecutablePath, +} from '../../../lib/cjs/browser-data/chrome.js'; + +describe('Chrome', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '113.0.5672.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/linux64/chrome-linux64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '113.0.5672.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-x64/chrome-mac-x64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '113.0.5672.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-arm64/chrome-mac-arm64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '113.0.5672.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win32/chrome-win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '113.0.5672.0'), + 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win64/chrome-win64.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + path.join('chrome-linux64', 'chrome') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + path.join( + 'chrome-mac-x64', + 'Google Chrome for Testing.app', + 'Contents', + 'MacOS', + 'Google Chrome for Testing' + ) + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + path.join( + 'chrome-mac-arm64', + 'Google Chrome for Testing.app', + 'Contents', + 'MacOS', + 'Google Chrome for Testing' + ) + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + path.join('chrome-win32', 'chrome.exe') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + path.join('chrome-win64', 'chrome.exe') + ); + }); + + it('should resolve system executable path', () => { + process.env['PROGRAMFILES'] = 'C:\\ProgramFiles'; + try { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.WIN32, + ChromeReleaseChannel.DEV + ), + 'C:\\ProgramFiles\\Google\\Chrome Dev\\Application\\chrome.exe' + ); + } finally { + delete process.env['PROGRAMFILES']; + } + + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.MAC, + ChromeReleaseChannel.BETA + ), + '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta' + ); + assert.throws(() => { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.LINUX, + ChromeReleaseChannel.CANARY + ), + path.join('chrome-linux', 'chrome') + ); + }, new Error(`Unable to detect browser executable path for 'canary' on linux.`)); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts new file mode 100644 index 0000000000..a9a08c9d62 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/cli.spec.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {CLI} from '../../../lib/cjs/CLI.js'; +import { + createMockedReadlineInterface, + setupTestServer, + getServerUrl, +} from '../utils.js'; +import {testChromeBuildId} from '../versions.js'; + +describe('Chrome CLI', function () { + this.timeout(90000); + + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(async () => { + await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + `--base-url=${getServerUrl()}`, + ]); + }); + + it('should download Chrome binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chrome@${testChromeBuildId}`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chrome', + `linux-${testChromeBuildId}`, + 'chrome-linux64', + 'chrome' + ) + ) + ); + + await new CLI(tmpDir, createMockedReadlineInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chrome', + `linux-${testChromeBuildId}`, + 'chrome-linux64', + 'chrome' + ) + ) + ); + }); + + // Skipped because the current latest is not published yet. + it.skip('should download latest Chrome binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chrome@latest`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts new file mode 100644 index 0000000000..7dc3e7ebaf --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/install.spec.ts @@ -0,0 +1,239 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import os from 'os'; +import path from 'path'; + +import { + install, + canDownload, + Browser, + BrowserPlatform, + Cache, +} from '../../../lib/cjs/main.js'; +import {getServerUrl, setupTestServer} from '../utils.js'; +import {testChromeBuildId} from '../versions.js'; + +/** + * Tests in this spec use real download URLs and unpack live browser archives + * so it requires the network access. + */ +describe('Chrome install', () => { + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + new Cache(tmpDir).clear(); + }); + + it('should check if a buildId can be downloaded', async () => { + assert.ok( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }) + ); + }); + + it('should report if a buildId is not downloadable', async () => { + assert.strictEqual( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: 'unknown', + baseUrl: getServerUrl(), + }), + false + ); + }); + + it('should download a buildId that is a zip archive', async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + 'chrome', + `${BrowserPlatform.LINUX}-${testChromeBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + let browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + // Second iteration should be no-op. + browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + // Should discover installed browsers. + const cache = new Cache(tmpDir); + const installed = cache.getInstalledBrowsers(); + assert.deepStrictEqual(browser, installed[0]); + }); + + it('throws on invalid URL', async function () { + const expectedOutputPath = path.join( + tmpDir, + 'chrome', + `${BrowserPlatform.LINUX}-${testChromeBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + + async function installThatThrows(): Promise<unknown> { + try { + await install({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: 'https://127.0.0.1', + }); + return undefined; + } catch (err) { + return err; + } + } + assert.ok(await installThatThrows()); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + }); + + describe('with proxy', () => { + const proxyUrl = new URL(`http://localhost:54321`); + let proxyServer: http.Server; + let proxiedRequestUrls: string[] = []; + let proxiedRequestHosts: string[] = []; + + beforeEach(() => { + proxiedRequestUrls = []; + proxiedRequestHosts = []; + proxyServer = http + .createServer( + ( + originalRequest: http.IncomingMessage, + originalResponse: http.ServerResponse + ) => { + const url = originalRequest.url as string; + const proxyRequest = ( + url.startsWith('http:') ? http : https + ).request( + url, + { + method: originalRequest.method, + rejectUnauthorized: false, + }, + proxyResponse => { + originalResponse.writeHead( + proxyResponse.statusCode as number, + proxyResponse.headers + ); + proxyResponse.pipe(originalResponse, {end: true}); + } + ); + originalRequest.pipe(proxyRequest, {end: true}); + proxiedRequestUrls.push(url); + proxiedRequestHosts.push(originalRequest.headers?.host || ''); + } + ) + .listen({ + port: proxyUrl.port, + hostname: proxyUrl.hostname, + }); + + process.env['HTTPS_PROXY'] = proxyUrl.toString(); + process.env['HTTP_PROXY'] = proxyUrl.toString(); + }); + + afterEach(async () => { + await new Promise((resolve, reject) => { + proxyServer.close(error => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); + delete process.env['HTTP_PROXY']; + delete process.env['HTTPS_PROXY']; + }); + + it('can send canDownload requests via a proxy', async () => { + assert.strictEqual( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }), + true + ); + assert.deepStrictEqual(proxiedRequestUrls, [ + getServerUrl() + '/113.0.5672.0/linux64/chrome-linux64.zip', + ]); + assert.deepStrictEqual(proxiedRequestHosts, [ + getServerUrl().replace('http://', ''), + ]); + }); + + it('can download via a proxy', async function () { + this.timeout(120000); + const expectedOutputPath = path.join( + tmpDir, + 'chrome', + `${BrowserPlatform.LINUX}-${testChromeBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + const browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + assert.deepStrictEqual(proxiedRequestUrls, [ + getServerUrl() + '/113.0.5672.0/linux64/chrome-linux64.zip', + ]); + assert.deepStrictEqual(proxiedRequestHosts, [ + getServerUrl().replace('http://', ''), + ]); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts new file mode 100644 index 0000000000..cef690a6bb --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chrome/launch.spec.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + CDP_WEBSOCKET_ENDPOINT_REGEX, + computeExecutablePath, + launch, + install, + Browser, + BrowserPlatform, +} from '../../../lib/cjs/main.js'; +import {getServerUrl, setupTestServer, clearCache} from '../utils.js'; +import {testChromeBuildId} from '../versions.js'; + +describe('Chrome', () => { + it('should compute executable path for Chrome', () => { + assert.strictEqual( + computeExecutablePath({ + browser: Browser.CHROME, + platform: BrowserPlatform.LINUX, + buildId: '123', + cacheDir: 'cache', + }), + path.join('cache', 'chrome', 'linux-123', 'chrome-linux64', 'chrome') + ); + }); + + describe('launcher', function () { + setupTestServer(); + + this.timeout(60000); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(async () => { + tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'puppeteer-browsers-test') + ); + await install({ + cacheDir: tmpDir, + browser: Browser.CHROME, + buildId: testChromeBuildId, + baseUrl: getServerUrl(), + }); + }); + + afterEach(() => { + clearCache(tmpDir); + }); + + function getArgs() { + return [ + '--allow-pre-commit-input', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--enable-automation', + '--enable-features=NetworkServiceInProcess2', + '--export-tagged-pdf', + '--force-color-profile=srgb', + '--headless=new', + '--metrics-recording-only', + '--no-first-run', + '--password-store=basic', + '--remote-debugging-port=9222', + '--use-mock-keychain', + `--user-data-dir=${path.join(tmpDir, 'profile')}`, + 'about:blank', + ]; + } + + it('should launch a Chrome browser', async () => { + const executablePath = computeExecutablePath({ + cacheDir: tmpDir, + browser: Browser.CHROME, + buildId: testChromeBuildId, + }); + const process = launch({ + executablePath, + args: getArgs(), + }); + await process.close(); + }); + + it('should allow parsing stderr output of the browser process', async () => { + const executablePath = computeExecutablePath({ + cacheDir: tmpDir, + browser: Browser.CHROME, + buildId: testChromeBuildId, + }); + const process = launch({ + executablePath, + args: getArgs(), + }); + const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX); + await process.close(); + assert.ok(url.startsWith('ws://127.0.0.1:9222/devtools/browser')); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts new file mode 100644 index 0000000000..fb4134a663 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/chromedriver-data.spec.ts @@ -0,0 +1,71 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; + +import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, +} from '../../../lib/cjs/browser-data/chromedriver.js'; + +describe('ChromeDriver', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_linux64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_mac_arm64.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '112.0.5615.49'), + 'https://chromedriver.storage.googleapis.com/112.0.5615.49/chromedriver_win32.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + 'chromedriver' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + 'chromedriver.exe' + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + 'chromedriver.exe' + ); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts new file mode 100644 index 0000000000..52c23d22c2 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/cli.spec.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {CLI} from '../../../lib/cjs/CLI.js'; +import { + createMockedReadlineInterface, + setupTestServer, + getServerUrl, +} from '../utils.js'; +import {testChromeDriverBuildId} from '../versions.js'; + +describe('ChromeDriver CLI', function () { + this.timeout(90000); + + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(async () => { + await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + `--base-url=${getServerUrl()}`, + ]); + }); + + it('should download ChromeDriver binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `chromedriver@${testChromeDriverBuildId}`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chromedriver', + `linux-${testChromeDriverBuildId}`, + 'chromedriver' + ) + ) + ); + + await new CLI(tmpDir, createMockedReadlineInterface('no')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + ]); + assert.ok( + fs.existsSync( + path.join( + tmpDir, + 'chromedriver', + `linux-${testChromeDriverBuildId}`, + 'chromedriver' + ) + ) + ); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts new file mode 100644 index 0000000000..fb725de010 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromedriver/install.spec.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + install, + canDownload, + Browser, + BrowserPlatform, + Cache, +} from '../../../lib/cjs/main.js'; +import {getServerUrl, setupTestServer} from '../utils.js'; +import {testChromeDriverBuildId} from '../versions.js'; + +/** + * Tests in this spec use real download URLs and unpack live browser archives + * so it requires the network access. + */ +describe('ChromeDriver install', () => { + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + new Cache(tmpDir).clear(); + }); + + it('should check if a buildId can be downloaded', async () => { + assert.ok( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }) + ); + }); + + it('should report if a buildId is not downloadable', async () => { + assert.strictEqual( + await canDownload({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: 'unknown', + baseUrl: getServerUrl(), + }), + false + ); + }); + + it('should download and unpack the binary', async function () { + this.timeout(60000); + const expectedOutputPath = path.join( + tmpDir, + 'chromedriver', + `${BrowserPlatform.LINUX}-${testChromeDriverBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + let browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + // Second iteration should be no-op. + browser = await install({ + cacheDir: tmpDir, + browser: Browser.CHROMEDRIVER, + platform: BrowserPlatform.LINUX, + buildId: testChromeDriverBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts new file mode 100644 index 0000000000..5dae0d0d29 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/chromium-data.spec.ts @@ -0,0 +1,108 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import path from 'path'; + +import { + BrowserPlatform, + ChromeReleaseChannel, +} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + resolveDownloadUrl, + relativeExecutablePath, + resolveSystemExecutablePath, +} from '../../../lib/cjs/browser-data/chromium.js'; + +describe('Chromium', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/1083080/chrome-linux.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/1083080/chrome-mac.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/1083080/chrome-mac.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/1083080/chrome-win.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '1083080'), + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/1083080/chrome-win.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '12372323'), + path.join('chrome-linux', 'chrome') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '12372323'), + path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'), + path.join('chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '12372323'), + path.join('chrome-win', 'chrome.exe') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '12372323'), + path.join('chrome-win', 'chrome.exe') + ); + }); + + it('should resolve system executable path', () => { + process.env['PROGRAMFILES'] = 'C:\\ProgramFiles'; + try { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.WIN32, + ChromeReleaseChannel.DEV + ), + 'C:\\ProgramFiles\\Google\\Chrome Dev\\Application\\chrome.exe' + ); + } finally { + delete process.env['PROGRAMFILES']; + } + + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.MAC, + ChromeReleaseChannel.BETA + ), + '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta' + ); + assert.throws(() => { + assert.strictEqual( + resolveSystemExecutablePath( + BrowserPlatform.LINUX, + ChromeReleaseChannel.CANARY + ), + path.join('chrome-linux', 'chrome') + ); + }, new Error(`Unable to detect browser executable path for 'canary' on linux.`)); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts new file mode 100644 index 0000000000..7fade9e52d --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/chromium/launch.spec.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + CDP_WEBSOCKET_ENDPOINT_REGEX, + computeExecutablePath, + launch, + install, + Browser, + BrowserPlatform, +} from '../../../lib/cjs/main.js'; +import {getServerUrl, setupTestServer, clearCache} from '../utils.js'; +import {testChromiumBuildId} from '../versions.js'; + +describe('Chromium', () => { + it('should compute executable path for Chromium', () => { + assert.strictEqual( + computeExecutablePath({ + browser: Browser.CHROMIUM, + platform: BrowserPlatform.LINUX, + buildId: '123', + cacheDir: 'cache', + }), + path.join('cache', 'chromium', 'linux-123', 'chrome-linux', 'chrome') + ); + }); + + describe('launcher', function () { + setupTestServer(); + + this.timeout(120000); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(async () => { + tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'puppeteer-browsers-test') + ); + await install({ + cacheDir: tmpDir, + browser: Browser.CHROMIUM, + buildId: testChromiumBuildId, + baseUrl: getServerUrl(), + }); + }); + + afterEach(() => { + clearCache(tmpDir); + }); + + function getArgs() { + return [ + '--allow-pre-commit-input', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DialMediaRouteProvider', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--enable-automation', + '--enable-features=NetworkServiceInProcess2', + '--export-tagged-pdf', + '--force-color-profile=srgb', + '--headless=new', + '--metrics-recording-only', + '--no-first-run', + '--password-store=basic', + '--remote-debugging-port=9222', + '--use-mock-keychain', + `--user-data-dir=${path.join(tmpDir, 'profile')}`, + 'about:blank', + ]; + } + + it('should launch a Chromium browser', async () => { + const executablePath = computeExecutablePath({ + cacheDir: tmpDir, + browser: Browser.CHROMIUM, + buildId: testChromiumBuildId, + }); + const process = launch({ + executablePath, + args: getArgs(), + }); + await process.close(); + }); + + it('should allow parsing stderr output of the browser process', async () => { + const executablePath = computeExecutablePath({ + cacheDir: tmpDir, + browser: Browser.CHROMIUM, + buildId: testChromiumBuildId, + }); + const process = launch({ + executablePath, + args: getArgs(), + }); + const url = await process.waitForLineOutput(CDP_WEBSOCKET_ENDPOINT_REGEX); + await process.close(); + assert.ok(url.startsWith('ws://127.0.0.1:9222/devtools/browser')); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts new file mode 100644 index 0000000000..ec93e0c353 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/cli.spec.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {CLI} from '../../../lib/cjs/CLI.js'; +import { + createMockedReadlineInterface, + getServerUrl, + setupTestServer, +} from '../utils.js'; +import {testFirefoxBuildId} from '../versions.js'; + +describe('Firefox CLI', function () { + this.timeout(90000); + + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(async () => { + await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([ + 'npx', + '@puppeteer/browsers', + 'clear', + `--path=${tmpDir}`, + `--base-url=${getServerUrl()}`, + ]); + }); + + it('should download Firefox binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `firefox@${testFirefoxBuildId}`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + assert.ok( + fs.existsSync( + path.join(tmpDir, 'firefox', `linux-${testFirefoxBuildId}`, 'firefox') + ) + ); + }); + + it('should download latest Firefox binaries', async () => { + await new CLI(tmpDir).run([ + 'npx', + '@puppeteer/browsers', + 'install', + `firefox@latest`, + `--path=${tmpDir}`, + '--platform=linux', + `--base-url=${getServerUrl()}`, + ]); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts new file mode 100644 index 0000000000..cad4ef5fa3 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/firefox-data.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js'; +import { + createProfile, + relativeExecutablePath, + resolveDownloadUrl, +} from '../../../lib/cjs/browser-data/firefox.js'; + +describe('Firefox', () => { + it('should resolve download URLs', () => { + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.LINUX, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.linux-x86_64.tar.bz2' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.MAC_ARM, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.mac.dmg' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN32, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win32.zip' + ); + assert.strictEqual( + resolveDownloadUrl(BrowserPlatform.WIN64, '111.0a1'), + 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox-111.0a1.en-US.win64.zip' + ); + }); + + it('should resolve executable paths', () => { + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.LINUX, '111.0a1'), + path.join('firefox', 'firefox') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC, '111.0a1'), + path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.MAC_ARM, '111.0a1'), + path.join('Firefox Nightly.app', 'Contents', 'MacOS', 'firefox') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN32, '111.0a1'), + path.join('firefox', 'firefox.exe') + ); + assert.strictEqual( + relativeExecutablePath(BrowserPlatform.WIN64, '111.0a1'), + path.join('firefox', 'firefox.exe') + ); + }); + + describe('profile', () => { + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'puppeteer-browsers-test') + ); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { + force: true, + recursive: true, + maxRetries: 5, + }); + }); + + it('should create a profile', async () => { + await createProfile({ + preferences: { + test: 1, + }, + path: tmpDir, + }); + const text = fs.readFileSync(path.join(tmpDir, 'user.js'), 'utf-8'); + assert.ok( + text.includes(`user_pref("toolkit.startup.max_resumed_crashes", -1);`) + ); // default preference. + assert.ok(text.includes(`user_pref("test", 1);`)); // custom preference. + }); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts new file mode 100644 index 0000000000..29d5974c73 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/install.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import {install, Browser, BrowserPlatform} from '../../../lib/cjs/main.js'; +import {setupTestServer, getServerUrl, clearCache} from '../utils.js'; +import {testFirefoxBuildId} from '../versions.js'; + +/** + * Tests in this spec use real download URLs and unpack live browser archives + * so it requires the network access. + */ +describe('Firefox install', () => { + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test')); + }); + + afterEach(() => { + clearCache(tmpDir); + }); + + it('should download a buildId that is a bzip2 archive', async function () { + this.timeout(90000); + const expectedOutputPath = path.join( + tmpDir, + 'firefox', + `${BrowserPlatform.LINUX}-${testFirefoxBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + const browser = await install({ + cacheDir: tmpDir, + browser: Browser.FIREFOX, + platform: BrowserPlatform.LINUX, + buildId: testFirefoxBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + }); + + // install relies on the `hdiutil` utility on MacOS. + // The utility is not available on other platforms. + (os.platform() === 'darwin' ? it : it.skip)( + 'should download a buildId that is a dmg archive', + async function () { + this.timeout(180000); + const expectedOutputPath = path.join( + tmpDir, + 'firefox', + `${BrowserPlatform.MAC}-${testFirefoxBuildId}` + ); + assert.strictEqual(fs.existsSync(expectedOutputPath), false); + const browser = await install({ + cacheDir: tmpDir, + browser: Browser.FIREFOX, + platform: BrowserPlatform.MAC, + buildId: testFirefoxBuildId, + baseUrl: getServerUrl(), + }); + assert.strictEqual(browser.path, expectedOutputPath); + assert.ok(fs.existsSync(expectedOutputPath)); + } + ); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts new file mode 100644 index 0000000000..88388d8d6d --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/firefox/launch.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + computeExecutablePath, + launch, + install, + Browser, + BrowserPlatform, + createProfile, +} from '../../../lib/cjs/main.js'; +import {setupTestServer, getServerUrl, clearCache} from '../utils.js'; +import {testFirefoxBuildId} from '../versions.js'; + +describe('Firefox', () => { + it('should compute executable path for Firefox', () => { + assert.strictEqual( + computeExecutablePath({ + browser: Browser.FIREFOX, + platform: BrowserPlatform.LINUX, + buildId: '123', + cacheDir: 'cache', + }), + path.join('cache', 'firefox', 'linux-123', 'firefox', 'firefox') + ); + }); + + describe('launcher', function () { + this.timeout(120000); + + setupTestServer(); + + let tmpDir = '/tmp/puppeteer-browsers-test'; + + beforeEach(async () => { + tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'puppeteer-browsers-test') + ); + await install({ + cacheDir: tmpDir, + browser: Browser.FIREFOX, + buildId: testFirefoxBuildId, + baseUrl: getServerUrl(), + }); + }); + + afterEach(() => { + clearCache(tmpDir); + }); + + it('should launch a Firefox browser', async () => { + const userDataDir = path.join(tmpDir, 'profile'); + function getArgs(): string[] { + const firefoxArguments = ['--no-remote']; + switch (os.platform()) { + case 'darwin': + firefoxArguments.push('--foreground'); + break; + case 'win32': + firefoxArguments.push('--wait-for-browser'); + break; + } + firefoxArguments.push('--profile', userDataDir); + firefoxArguments.push('--headless'); + firefoxArguments.push('about:blank'); + return firefoxArguments; + } + await createProfile(Browser.FIREFOX, { + path: userDataDir, + preferences: {}, + }); + const executablePath = computeExecutablePath({ + cacheDir: tmpDir, + browser: Browser.FIREFOX, + buildId: testFirefoxBuildId, + }); + const process = launch({ + executablePath, + args: getArgs(), + }); + await process.close(); + }); + }); +}); diff --git a/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json new file mode 100644 index 0000000000..63dd3f1eeb --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../build" + }, + "references": [{"path": "../../tsconfig.json"}] +} diff --git a/remote/test/puppeteer/packages/browsers/test/src/utils.ts b/remote/test/puppeteer/packages/browsers/test/src/utils.ts new file mode 100644 index 0000000000..9afb1bb763 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/utils.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {execSync} from 'child_process'; +import os from 'os'; +import path from 'path'; +import * as readline from 'readline'; +import {Writable, Readable} from 'stream'; + +import {TestServer} from '@pptr/testserver'; + +import {isErrorLike} from '../../lib/cjs/launch.js'; +import {Cache} from '../../lib/cjs/main.js'; + +export function createMockedReadlineInterface( + input: string +): readline.Interface { + const readable = Readable.from([input]); + const writable = new Writable({ + write(_chunk, _encoding, callback) { + // Suppress the output to keep the test clean + callback(); + }, + }); + + return readline.createInterface({ + input: readable, + output: writable, + }); +} + +const startServer = async () => { + const assetsPath = path.join(__dirname, '..', 'cache', 'server'); + return await TestServer.create(assetsPath); +}; + +interface ServerState { + server: TestServer; +} + +const state: Partial<ServerState> = {}; + +export function setupTestServer(): void { + before(async () => { + state.server = await startServer(); + }); + + after(async () => { + await state.server!.stop(); + state.server = undefined; + }); +} + +export function getServerUrl(): string { + return `http://localhost:${state.server!.port}`; +} + +export function clearCache(tmpDir: string): void { + try { + new Cache(tmpDir).clear(); + } catch (err) { + if (os.platform() === 'win32') { + console.log(execSync('tasklist').toString('utf-8')); + // Sometimes on Windows the folder cannot be removed due to unknown reasons. + // We suppress the error to avoud flakiness. + if (isErrorLike(err) && err.message.includes('EBUSY')) { + return; + } + } + throw err; + } +} diff --git a/remote/test/puppeteer/packages/browsers/test/src/versions.ts b/remote/test/puppeteer/packages/browsers/test/src/versions.ts new file mode 100644 index 0000000000..606827fe3c --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/test/src/versions.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const testChromeBuildId = '113.0.5672.0'; +export const testChromiumBuildId = '1083080'; +// TODO: We can add a Cron job to auto-update on change. +// Firefox keeps only `latest` version of Nightly builds. +export const testFirefoxBuildId = '114.0a1'; +export const testChromeDriverBuildId = '112.0.5615.49'; diff --git a/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs b/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs new file mode 100644 index 0000000000..9ee861f878 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/tools/downloadTestBrowsers.mjs @@ -0,0 +1,81 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Downloads test browser binaries to test/cache/server folder that + * mirrors the structure of the download server. + */ + +import {BrowserPlatform, install} from '@puppeteer/browsers'; +import path from 'path'; +import fs from 'fs'; + +import * as versions from '../test/build/versions.js'; +import {downloadPaths} from '../lib/esm/browser-data/browser-data.js'; + +function getBrowser(str) { + const regex = /test(.+)BuildId/; + const match = str.match(regex); + + if (match && match[1]) { + return match[1].toLowerCase(); + } else { + return null; + } +} + +const cacheDir = path.normalize(path.join('.', 'test', 'cache')); + +for (const version of Object.keys(versions)) { + const browser = getBrowser(version); + + if (!browser) { + continue; + } + + const buildId = versions[version]; + + for (const platform of Object.values(BrowserPlatform)) { + const targetPath = path.join( + cacheDir, + 'server', + ...downloadPaths[browser](platform, buildId) + ); + + if (fs.existsSync(targetPath)) { + continue; + } + + const result = await install({ + browser, + buildId, + platform, + cacheDir: path.join(cacheDir, 'tmp'), + unpack: false, + }); + + fs.mkdirSync(path.dirname(targetPath), { + recursive: true, + }); + fs.copyFileSync(result.path, targetPath); + } +} + +fs.rmSync(path.join(cacheDir, 'tmp'), { + recursive: true, + force: true, + maxRetries: 10, +}); diff --git a/remote/test/puppeteer/packages/browsers/tsconfig.json b/remote/test/puppeteer/packages/browsers/tsconfig.json new file mode 100644 index 0000000000..a219f8b704 --- /dev/null +++ b/remote/test/puppeteer/packages/browsers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "references": [ + {"path": "src/tsconfig.esm.json"}, + {"path": "src/tsconfig.cjs.json"} + ] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/.eslintignore b/remote/test/puppeteer/packages/ng-schematics/.eslintignore new file mode 100644 index 0000000000..69132ca336 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/.eslintignore @@ -0,0 +1,5 @@ +# Ignore File that will be copied to Angular +/files/ + +# Ignore sandbox enviroment +./sandbox/
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/ng-schematics/.gitignore b/remote/test/puppeteer/packages/ng-schematics/.gitignore new file mode 100644 index 0000000000..93d9982369 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/.gitignore @@ -0,0 +1,3 @@ + +# Sandbox +sandbox/
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs new file mode 100644 index 0000000000..be9bc29919 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/.mocharc.cjs @@ -0,0 +1,6 @@ +module.exports = { + logLevel: 'debug', + spec: 'test/build/**/*.spec.js', + exit: !!process.env.CI, + reporter: process.env.CI ? 'spec' : 'dot', +}; diff --git a/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md new file mode 100644 index 0000000000..3cc843df1e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## [0.2.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.1.0...ng-schematics-v0.2.0) (2023-05-02) + + +### ⚠ BREAKING CHANGES + +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) + +### Features + +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9)) + +## 0.1.0 (2022-11-23) + + +### Features + +* **ng-schematics:** Release @puppeteer/ng-schematics ([#9244](https://github.com/puppeteer/puppeteer/issues/9244)) ([be33929](https://github.com/puppeteer/puppeteer/commit/be33929770e473992ad49029e6d038d36591e108)) diff --git a/remote/test/puppeteer/packages/ng-schematics/README.md b/remote/test/puppeteer/packages/ng-schematics/README.md new file mode 100644 index 0000000000..3efc89f17c --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/README.md @@ -0,0 +1,67 @@ +# Puppeteer Angular Schematic + +Adds Puppeteer-based e2e tests to your Angular project. + +## Usage + +Run the command below in an Angular CLI app directory and follow the prompts. +_Note this will add the schematic as a dependency to your project._ + +```bash +ng add @puppeteer/ng-schematics +``` + +Or you can use the same command followed by the [options](#options) below. + +Currently, this schematic supports the following test frameworks: + +- **Jasmine** [https://jasmine.github.io/] +- **Jest** [https://jestjs.io/] +- **Mocha** [https://mochajs.org/] +- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html] + +With the schematics installed you can run E2E tests: + +```bash +ng e2e +``` + +> Note: Command spawns it's own server on the same port `ng serve` does. + +## Options + +When adding schematics to your project you can to provide following options: + +| Option | Description | Value | Required | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- | +| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` | +| `--exportConfig` | When true, creates an empty [Puppeteer configuration](https://pptr.dev/guides/configuration) file. (`.puppeteerrc.cjs`) | `boolean` | `true` | +| `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` | + +## Contributing + +Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of what you need to develop in the Puppeteer repo. + +### Sandbox + +For easier development we provide a script to auto-generate the Angular project to test against. Simply run: + +```bash +npm run sandbox -- --init +``` + +After that to run `@puppeteer/ng-schematics` against the Sandbox Angular project run: + +```bash +npm run sandbox +# or to auto-build and then run schematics +npm run sandbox -- --build +``` + +### Unit Testing + +The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit: + +```bash +npm run test +``` diff --git a/remote/test/puppeteer/packages/ng-schematics/package-lock.json b/remote/test/puppeteer/packages/ng-schematics/package-lock.json new file mode 100644 index 0000000000..11dd119c2b --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/package-lock.json @@ -0,0 +1,1098 @@ +{ + "name": "angular", + "version": "0.2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "angular", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "^14.2.6", + "@angular-devkit/schematics": "^14.2.6", + "typescript": "~4.7.2" + }, + "devDependencies": { + "@types/jasmine": "~4.0.0", + "@types/node": "^14.15.0", + "jasmine": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", + "dependencies": { + "@angular-devkit/core": "14.2.6", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.18.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz", + "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==", + "dev": true + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jasmine": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.4.0.tgz", + "integrity": "sha512-xrbOyYkkCvgduNw7CKktDtNb+BwwBv/zvQeHpTkbxqQ37AJL5V4sY3jHoMIJPP/hTc3QxLVwOyxc87AqA+kw5g==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "jasmine-core": "^4.4.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.4.0.tgz", + "integrity": "sha512-+l482uImx5BVd6brJYlaHe2UwfKoZBqQfNp20ZmdNfsjGFTemGfqHLsXjKEW23w9R/m8WYeFc9JmIgjj6dUtAA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, + "dependencies": { + "@angular-devkit/core": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.6.tgz", + "integrity": "sha512-qtRSdRm/h7C3ya04PJTDgQXV6mM8Y4RakANX1GTSXetCf9AVSxg74NJX76DWUgiHT4JiPYnJgJU6Hr/L0H6JOQ==", + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, + "@angular-devkit/schematics": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.6.tgz", + "integrity": "sha512-mSFtc4M49mWrYsgJx/P6bA6SzXb8SeZqmppKRMoEQxiXI1bwFdGLNWzAmzEsGvS96h/nPIaOfcX5cKJSp++4FA==", + "requires": { + "@angular-devkit/core": "14.2.6", + "jsonc-parser": "3.1.0", + "magic-string": "0.26.2", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@types/jasmine": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.0.3.tgz", + "integrity": "sha512-Opp1LvvEuZdk8fSSvchK2mZwhVrsNT0JgJE9Di6MjnaIpmEXM8TLCPPrVtNTYh8+5MPdY8j9bAHMu2SSfwpZJg==", + "dev": true + }, + "@types/node": { + "version": "14.18.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.32.tgz", + "integrity": "sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==", + "dev": true + }, + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "requires": { + "clone": "^1.0.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "jasmine": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-4.4.0.tgz", + "integrity": "sha512-xrbOyYkkCvgduNw7CKktDtNb+BwwBv/zvQeHpTkbxqQ37AJL5V4sY3jHoMIJPP/hTc3QxLVwOyxc87AqA+kw5g==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "jasmine-core": "^4.4.0" + } + }, + "jasmine-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.4.0.tgz", + "integrity": "sha512-+l482uImx5BVd6brJYlaHe2UwfKoZBqQfNp20ZmdNfsjGFTemGfqHLsXjKEW23w9R/m8WYeFc9JmIgjj6dUtAA==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "jsonc-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "magic-string": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "requires": { + "defaults": "^1.0.3" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/remote/test/puppeteer/packages/ng-schematics/package.json b/remote/test/puppeteer/packages/ng-schematics/package.json new file mode 100644 index 0000000000..e7f645d4d3 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/package.json @@ -0,0 +1,68 @@ +{ + "name": "@puppeteer/ng-schematics", + "version": "0.2.0", + "description": "Puppeteer Angular schematics", + "scripts": { + "build": "wireit", + "clean": "tsc -b --clean && rm -rf lib && rm -rf test/build", + "dev:test": "npm run test --watch", + "dev": "npm run build --watch", + "test": "wireit", + "sandbox": "node tools/sandbox.js" + }, + "wireit": { + "build": { + "command": "tsc -b && node tools/copySchemaFiles.js", + "clean": "if-file-deleted", + "files": [ + "tsconfig.json", + "tsconfig.spec.json", + "src/**", + "test/src/**" + ], + "output": [ + "lib/**", + "test/build/**" + ] + }, + "test": { + "command": "mocha", + "dependencies": [ + "build" + ] + } + }, + "keywords": [ + "angular", + "puppeteer", + "schematics" + ], + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/ng-schematics" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "@angular-devkit/architect": "^0.1502.7", + "@angular-devkit/core": "^15.2.7", + "@angular-devkit/schematics": "^15.2.7" + }, + "devDependencies": { + "@types/node": "^14.15.0", + "@schematics/angular": "^14.2.8", + "@angular/cli": "^15.2.2" + }, + "files": [ + "lib", + "!*.tsbuildinfo" + ], + "ng-add": { + "save": "devDependencies" + }, + "schematics": "./lib/schematics/collection.json", + "builders": "./lib/builders/builders.json" +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json new file mode 100644 index 0000000000..41079f7731 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/builders.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../../node_modules/@angular-devkit/architect/src/builders-schema.json", + "builders": { + "puppeteer": { + "implementation": "./puppeteer", + "schema": "./puppeteer/schema.json", + "description": "Run e2e test with Puppeteer" + } + } +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts new file mode 100644 index 0000000000..45aec95152 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/index.ts @@ -0,0 +1,138 @@ +import {spawn} from 'child_process'; + +import { + createBuilder, + BuilderContext, + BuilderOutput, + targetFromTargetString, + BuilderRun, +} from '@angular-devkit/architect'; +import {JsonObject} from '@angular-devkit/core'; + +import {PuppeteerBuilderOptions} from './types.js'; + +const terminalStyles = { + blue: '\u001b[34m', + green: '\u001b[32m', + bold: '\u001b[1m', + reverse: '\u001b[7m', + clear: '\u001b[0m', +}; + +function getError(executable: string, args: string[]) { + return ( + `Puppeteer E2E tests failed!` + + '\n' + + `Error running '${executable}' with arguments '${args.join(' ')}'.` + + `\n` + + 'Please look at the output above to determine the issue!' + ); +} + +function getExecutable(command: string[]) { + const executable = command.shift()!; + const error = getError(executable, command); + + if (executable === 'node') { + return { + executable: executable, + args: command, + error, + }; + } + + return { + executable: `./node_modules/.bin/${executable}`, + args: command, + error, + }; +} + +async function executeCommand(context: BuilderContext, command: string[]) { + await new Promise((resolve, reject) => { + context.logger.debug(`Trying to execute command - ${command.join(' ')}.`); + const {executable, args, error} = getExecutable(command); + + const child = spawn(executable, args, { + cwd: context.workspaceRoot, + stdio: 'inherit', + }); + + child.on('error', message => { + console.log(message); + reject(error); + }); + + child.on('exit', code => { + if (code === 0) { + resolve(true); + } else { + reject(error); + } + }); + }); +} + +function message( + message: string, + context: BuilderContext, + type: 'info' | 'success' = 'info' +): void { + const color = type === 'info' ? terminalStyles.blue : terminalStyles.green; + context.logger.info( + `${terminalStyles.bold}${terminalStyles.reverse}${color}${message}${terminalStyles.clear}` + ); +} + +async function startServer( + options: PuppeteerBuilderOptions, + context: BuilderContext +): Promise<BuilderRun> { + context.logger.debug('Trying to start server.'); + const target = targetFromTargetString(options.devServerTarget); + const defaultServerOptions = await context.getTargetOptions(target); + + const overrides = { + watch: false, + host: defaultServerOptions['host'], + port: defaultServerOptions['port'], + } as JsonObject; + + message('Spawning test server...\n', context); + const server = await context.scheduleTarget(target, overrides); + const result = await server.result; + if (!result.success) { + throw new Error('Failed to spawn server! Stopping tests...'); + } + + return server; +} + +async function executeE2ETest( + options: PuppeteerBuilderOptions, + context: BuilderContext +): Promise<BuilderOutput> { + let server: BuilderRun | null = null; + try { + server = await startServer(options, context); + + message('\nRunning tests...\n', context); + for (const command of options.commands) { + await executeCommand(context, command); + } + + message('\nTest ran successfully!', context, 'success'); + return {success: true}; + } catch (error) { + if (error instanceof Error) { + return {success: false, error: error.message}; + } + return {success: false, error: error as any}; + } finally { + if (server) { + await server.stop(); + } + } +} + +export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest) as any; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json new file mode 100644 index 0000000000..42e80f46d0 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/schema.json @@ -0,0 +1,22 @@ +{ + "title": "Puppeteer", + "description": "Options for Puppeteer Angular Schematics", + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { + "type": "array", + "item": { + "type": "string" + } + }, + "description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')." + }, + "devServerTarget": { + "type": "string", + "description": "Angular target that spawns the server." + } + }, + "additionalProperties": true +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts new file mode 100644 index 0000000000..b25d51b5f1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/builders/puppeteer/types.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {JsonObject} from '@angular-devkit/core'; + +type Command = [string, ...string[]]; + +export interface PuppeteerBuilderOptions extends JsonObject { + commands: Command[]; + devServerTarget: string; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json new file mode 100644 index 0000000000..0cf38b799b --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/collection.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Add Puppeteer to an Angular project", + "factory": "./ng-add/index#ngAdd", + "schema": "./ng-add/schema.json" + } + } +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template new file mode 100644 index 0000000000..04f3f0d832 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/.puppeteerrc.cjs.template @@ -0,0 +1,4 @@ +/** + * @type {import("puppeteer").Configuration} + */ +module.exports = {}; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template new file mode 100644 index 0000000000..2f98ef7d46 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.e2e.ts.template @@ -0,0 +1,59 @@ +import * as puppeteer from 'puppeteer'; +<% if(testingFramework == 'node') { %> +import { + describe, + it, + before, + beforeEach, + after, + afterEach, +} from 'node:test'; +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> +import * as assert from 'assert'; +<% } %> + +describe('App test', function () { + let browser: puppeteer.Browser; + let page: puppeteer.Page; + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + beforeAll(async () => { + browser = await puppeteer.launch(); + }); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + before(async () => { + browser = await puppeteer.launch(); + }); +<% } %> + + beforeEach(async () => { + page = await browser.newPage(); + await page.goto('<%= baseUrl %>'); + }); + + afterEach(async () => { + await page.close(); + }); + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + afterAll(async () => { + await browser.close(); + }); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + after(async () => { + await browser.close(); + }); +<% } %> + + it('is running', async function () { + const element = await page.waitForSelector( + 'text/<%= project %> app is running!' + ); + +<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> + expect(element).not.toBeNull(); +<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %> + assert.ok(element); +<% } %> + }); +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template new file mode 100644 index 0000000000..438d04725f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tsconfig.json.template @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../tsconfig.json", + "compilerOptions": { +<% if(testingFramework == 'jest') { %> + "esModuleInterop": true, +<% } %><% if(testingFramework == 'node') { %> + "module": "CommonJS", + "rootDir": "tests/", + "outDir": "test/", +<% } %> + "types": ["<%= testingFramework %>"] + }, + "include": ["tests/**/*.e2e.ts"] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js new file mode 100644 index 0000000000..06259b39c8 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/helpers/babel.js @@ -0,0 +1,4 @@ +require('@babel/register')({ + extensions: ['.js', '.ts'], + presets: ['@babel/preset-env', '@babel/preset-typescript'], +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json new file mode 100644 index 0000000000..7100e102f1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jasmine/e2e/support/jasmine.json @@ -0,0 +1,9 @@ +{ + "spec_dir": "e2e", + "spec_files": ["**/*[eE]2[eE].ts"], + "helpers": ["helpers/babel.js", "helpers/**/*.{js|ts}"], + "env": { + "stopSpecOnExpectationFailure": false, + "random": true + } +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js new file mode 100644 index 0000000000..99cc594c97 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/jest/e2e/jest.config.js @@ -0,0 +1,11 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + testMatch: ['<rootDir>/tests/**/?(*.)+(e2e).[tj]s?(x)'], + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js new file mode 100644 index 0000000000..63ca85e3eb --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/.mocharc.js @@ -0,0 +1,4 @@ +module.exports = { + file: ['e2e/babel.js'], + spec: './e2e/tests/**/*.e2e.ts', +}; diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js new file mode 100644 index 0000000000..06259b39c8 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/mocha/e2e/babel.js @@ -0,0 +1,4 @@ +require('@babel/register')({ + extensions: ['.js', '.ts'], + presets: ['@babel/preset-env', '@babel/preset-typescript'], +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template new file mode 100644 index 0000000000..35250fac0e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/files/node/e2e/.gitignore.template @@ -0,0 +1,3 @@ +# Compiled e2e tests output Node auto resolves files in folders named 'test' + +test/
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts new file mode 100644 index 0000000000..49c56bfc2f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/index.ts @@ -0,0 +1,127 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics'; +import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; +import {of} from 'rxjs'; +import {concatMap, map, scan} from 'rxjs/operators'; + +import { + addBaseFiles, + addFrameworkFiles, + getNgCommandName, +} from '../utils/files.js'; +import {getAngularConfig} from '../utils/json.js'; +import { + addPackageJsonDependencies, + addPackageJsonScripts, + getDependenciesFromOptions, + getPackageLatestNpmVersion, + DependencyType, + type NodePackage, + updateAngularJsonScripts, +} from '../utils/packages.js'; +import {type SchematicsOptions} from '../utils/types.js'; + +// You don't have to export the function as default. You can also have more than one rule +// factory per file. +export function ngAdd(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + return chain([ + addDependencies(options), + addPuppeteerFiles(options), + addOtherFiles(options), + updateScripts(options), + updateAngularConfig(options), + ])(tree, context); + }; +} + +function addDependencies(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + context.logger.debug('Adding dependencies to "package.json"'); + const dependencies = getDependenciesFromOptions(options); + + return of(...dependencies).pipe( + concatMap((packageName: string) => { + return getPackageLatestNpmVersion(packageName); + }), + scan((array, nodePackage) => { + array.push(nodePackage); + return array; + }, [] as NodePackage[]), + map(packages => { + context.logger.debug('Updating dependencies...'); + addPackageJsonDependencies(tree, packages, DependencyType.Dev); + context.addTask(new NodePackageInstallTask()); + + return tree; + }) + ); + }; +} + +function updateScripts(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext): Tree => { + context.logger.debug('Updating "package.json" scripts'); + const angularJson = getAngularConfig(tree); + const projects = Object.keys(angularJson['projects']); + + if (projects.length === 1) { + const name = getNgCommandName(options); + const prefix = options.isDefaultTester ? '' : `run ${projects[0]}:`; + return addPackageJsonScripts(tree, [ + { + name, + script: `ng ${prefix}${name}`, + }, + ]); + } + return tree; + }; +} + +function addPuppeteerFiles(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + context.logger.debug('Adding Puppeteer base files.'); + const {projects} = getAngularConfig(tree); + + return addBaseFiles(tree, context, { + projects, + options, + }); + }; +} + +function addOtherFiles(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext) => { + context.logger.debug('Adding Puppeteer additional files.'); + const {projects} = getAngularConfig(tree); + + return addFrameworkFiles(tree, context, { + projects, + options, + }); + }; +} + +function updateAngularConfig(options: SchematicsOptions): Rule { + return (tree: Tree, context: SchematicContext): Tree => { + context.logger.debug('Updating "angular.json".'); + + return updateAngularJsonScripts(tree, options); + }; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json new file mode 100644 index 0000000000..77cb5bdd48 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/ng-add/schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Puppeteer", + "title": "Puppeteer Install Schema", + "type": "object", + "properties": { + "isDefaultTester": { + "description": "", + "type": "boolean", + "default": true, + "x-prompt": "Use Puppeteer as default `ng e2e` command?" + }, + "exportConfig": { + "description": "", + "type": "boolean", + "default": false, + "x-prompt": "Export default Puppeteer config file?" + }, + "testingFramework": { + "description": "", + "type": "string", + "enum": ["jasmine", "jest", "mocha", "node"], + "default": "jasmine", + "x-prompt": { + "message": "With what Testing Library do you wish to integrate?", + "type": "list", + "items": [ + { + "value": "jasmine", + "label": "Use Jasmine [https://jasmine.github.io/]" + }, + { + "value": "jest", + "label": "Use Jest [https://jestjs.io/]" + }, + { + "value": "mocha", + "label": "Use Mocha [https://mochajs.org/]" + }, + { + "value": "node", + "label": "Use Node Test Runner (Experimental: Node v18) [https://nodejs.org/api/test.html]" + } + ] + } + } + }, + "required": [] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts new file mode 100644 index 0000000000..9518e1d996 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/files.ts @@ -0,0 +1,162 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {relative, resolve} from 'path'; + +import {getSystemPath, normalize, strings} from '@angular-devkit/core'; +import { + SchematicContext, + Tree, + apply, + applyTemplates, + chain, + filter, + mergeWith, + move, + url, +} from '@angular-devkit/schematics'; + +import {SchematicsOptions, TestingFramework} from './types.js'; + +export interface FilesOptions { + projects: any; + options: SchematicsOptions; + applyPath: string; + relativeToWorkspacePath: string; + movePath?: string; + filterPredicate?: (path: string) => boolean; +} + +const PUPPETEER_CONFIG_TEMPLATE = '.puppeteerrc.cjs.template'; + +export function addFiles( + tree: Tree, + context: SchematicContext, + { + projects, + options, + applyPath, + movePath, + relativeToWorkspacePath, + filterPredicate, + }: FilesOptions +): any { + return chain( + Object.keys(projects).map(name => { + const project = projects[name]; + const projectPath = resolve(getSystemPath(normalize(project.root))); + const workspacePath = resolve(getSystemPath(normalize(''))); + + const relativeToWorkspace = relative( + `${projectPath}${relativeToWorkspacePath}`, + workspacePath + ); + + const baseUrl = getProjectBaseUrl(project); + + return mergeWith( + apply(url(applyPath), [ + filter( + filterPredicate ?? + (() => { + return true; + }) + ), + move(movePath ? `${project.root}${movePath}` : project.root), + applyTemplates({ + ...options, + ...strings, + root: project.root ? `${project.root}/` : project.root, + baseUrl, + project: name, + relativeToWorkspace, + }), + ]) + ); + }) + )(tree, context); +} + +function getProjectBaseUrl(project: any): string { + let options = {protocol: 'http', port: 4200, host: 'localhost'}; + + if (project.architect?.serve?.options) { + const projectOptions = project.architect?.serve?.options; + + options = {...options, ...projectOptions}; + options.protocol = projectOptions.ssl ? 'https' : 'http'; + } + + return `${options.protocol}://${options.host}:${options.port}`; +} + +export function addBaseFiles( + tree: Tree, + context: SchematicContext, + filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'> +): any { + const options: FilesOptions = { + ...filesOptions, + applyPath: './files/base', + relativeToWorkspacePath: `/`, + filterPredicate: path => { + return path.includes(PUPPETEER_CONFIG_TEMPLATE) && + !filesOptions.options.exportConfig + ? false + : true; + }, + }; + + return addFiles(tree, context, options); +} + +export function addFrameworkFiles( + tree: Tree, + context: SchematicContext, + filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'> +): any { + const testingFramework = filesOptions.options.testingFramework; + const options: FilesOptions = { + ...filesOptions, + applyPath: `./files/${testingFramework}`, + relativeToWorkspacePath: `/`, + }; + + return addFiles(tree, context, options); +} + +export function getScriptFromOptions(options: SchematicsOptions): string[][] { + switch (options.testingFramework) { + case TestingFramework.Jasmine: + return [[`jasmine`, '--config=./e2e/support/jasmine.json']]; + case TestingFramework.Jest: + return [[`jest`, '-c', 'e2e/jest.config.js']]; + case TestingFramework.Mocha: + return [[`mocha`, '--config=./e2e/.mocharc.js']]; + case TestingFramework.Node: + return [ + [`tsc`, '-p', 'e2e/tsconfig.json'], + ['node', '--test', 'e2e/'], + ]; + } +} + +export function getNgCommandName(options: SchematicsOptions): string { + if (options.isDefaultTester) { + return 'e2e'; + } + return 'puppeteer'; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts new file mode 100644 index 0000000000..f48eab399e --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/json.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {SchematicsException, Tree} from '@angular-devkit/schematics'; + +export function getJsonFileAsObject( + tree: Tree, + path: string +): Record<string, any> { + try { + const buffer = tree.read(path) as Buffer; + const content = buffer.toString(); + return JSON.parse(content); + } catch { + throw new SchematicsException(`Unable to retrieve file at ${path}.`); + } +} + +export function getObjectAsJson(object: Record<string, any>): string { + return JSON.stringify(object, null, 2); +} + +export function getAngularConfig(tree: Tree): Record<string, any> { + return getJsonFileAsObject(tree, './angular.json'); +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts new file mode 100644 index 0000000000..b3724be90f --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/packages.ts @@ -0,0 +1,204 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {get} from 'https'; + +import {Tree} from '@angular-devkit/schematics'; + +import {getNgCommandName, getScriptFromOptions} from './files.js'; +import { + getAngularConfig, + getJsonFileAsObject, + getObjectAsJson, +} from './json.js'; +import {SchematicsOptions, TestingFramework} from './types.js'; +export interface NodePackage { + name: string; + version: string; +} +export interface NodeScripts { + name: string; + script: string; +} + +export enum DependencyType { + Default = 'dependencies', + Dev = 'devDependencies', + Peer = 'peerDependencies', + Optional = 'optionalDependencies', +} + +export function getPackageLatestNpmVersion(name: string): Promise<NodePackage> { + return new Promise(resolve => { + let version = 'latest'; + + return get(`https://registry.npmjs.org/${name}`, res => { + let data = ''; + + res.on('data', (chunk: any) => { + data += chunk; + }); + res.on('end', () => { + try { + const response = JSON.parse(data); + version = response?.['dist-tags']?.latest ?? version; + } catch { + } finally { + resolve({ + name, + version, + }); + } + }); + }).on('error', () => { + resolve({ + name, + version, + }); + }); + }); +} + +function updateJsonValues( + json: Record<string, any>, + target: string, + updates: Array<{name: string; value: any}>, + overwrite = false +) { + updates.forEach(({name, value}) => { + if (!json[target][name] || overwrite) { + json[target] = { + ...json[target], + [name]: value, + }; + } + }); +} + +export function addPackageJsonDependencies( + tree: Tree, + packages: NodePackage[], + type: DependencyType, + overwrite?: boolean, + fileLocation = './package.json' +): Tree { + const packageJson = getJsonFileAsObject(tree, fileLocation); + + updateJsonValues( + packageJson, + type, + packages.map(({name, version}) => { + return {name, value: version}; + }), + overwrite + ); + + tree.overwrite(fileLocation, getObjectAsJson(packageJson)); + + return tree; +} + +export function getDependenciesFromOptions( + options: SchematicsOptions +): string[] { + const dependencies = ['puppeteer']; + const babelPackages = [ + '@babel/core', + '@babel/register', + '@babel/preset-env', + '@babel/preset-typescript', + ]; + + switch (options.testingFramework) { + case TestingFramework.Jasmine: + dependencies.push('jasmine', ...babelPackages); + break; + case TestingFramework.Jest: + dependencies.push('jest', '@types/jest', 'ts-jest'); + break; + case TestingFramework.Mocha: + dependencies.push('mocha', '@types/mocha', ...babelPackages); + break; + case TestingFramework.Node: + dependencies.push('@types/node'); + break; + } + + return dependencies; +} + +export function addPackageJsonScripts( + tree: Tree, + scripts: NodeScripts[], + overwrite?: boolean, + fileLocation = './package.json' +): Tree { + const packageJson = getJsonFileAsObject(tree, fileLocation); + + updateJsonValues( + packageJson, + 'scripts', + scripts.map(({name, script}) => { + return {name, value: script}; + }), + overwrite + ); + + tree.overwrite(fileLocation, getObjectAsJson(packageJson)); + + return tree; +} + +export function updateAngularJsonScripts( + tree: Tree, + options: SchematicsOptions, + overwrite = true +): Tree { + const angularJson = getAngularConfig(tree); + const commands = getScriptFromOptions(options); + const name = getNgCommandName(options); + + Object.keys(angularJson['projects']).forEach(project => { + const e2eScript = [ + { + name, + value: { + builder: '@puppeteer/ng-schematics:puppeteer', + options: { + commands, + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, + }, + }, + ]; + + updateJsonValues( + angularJson['projects'][project], + 'architect', + e2eScript, + overwrite + ); + }); + + tree.overwrite('./angular.json', getObjectAsJson(angularJson)); + + return tree; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts new file mode 100644 index 0000000000..c59f90e69d --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/src/schematics/utils/types.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum TestingFramework { + Jasmine = 'jasmine', + Jest = 'jest', + Mocha = 'mocha', + Node = 'node', +} + +export interface SchematicsOptions { + isDefaultTester: boolean; + exportConfig: boolean; + testingFramework: TestingFramework; +} diff --git a/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts b/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts new file mode 100644 index 0000000000..59636dbb38 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/test/src/index.spec.ts @@ -0,0 +1,212 @@ +import https from 'https'; +import {join} from 'path'; + +import {JsonObject} from '@angular-devkit/core'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing/schematic-test-runner'; +import expect from 'expect'; +import sinon from 'sinon'; + +const WORKSPACE_OPTIONS = { + name: 'workspace', + newProjectRoot: 'projects', + version: '14.0.0', +}; + +const APPLICATION_OPTIONS = { + name: 'sandbox', +}; + +function getProjectFile(file: string): string { + return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`; +} + +function getAngularJsonScripts( + tree: UnitTestTree, + isDefault = true +): { + builder: string; + configurations: Record<string, any>; + options: Record<string, any>; +} { + const angularJson = tree.readJson('angular.json') as any; + const e2eScript = isDefault ? 'e2e' : 'puppeteer'; + return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][ + e2eScript + ]; +} + +function getPackageJson(tree: UnitTestTree): { + scripts: Record<string, string>; + devDependencies: string[]; +} { + const packageJson = tree.readJson('package.json') as JsonObject; + return { + scripts: packageJson['scripts'] as any, + devDependencies: Object.keys( + packageJson['devDependencies'] as Record<string, string> + ), + }; +} + +async function buildTestingTree(userOptions?: Record<string, any>) { + const runner = new SchematicTestRunner( + 'schematics', + join(__dirname, '../../lib/schematics/collection.json') + ); + const options = { + isDefaultTester: true, + exportConfig: false, + testingFramework: 'jasmine', + ...userOptions, + }; + let workingTree: UnitTestTree; + + // Build workspace + workingTree = await runner + .runExternalSchematicAsync( + '@schematics/angular', + 'workspace', + WORKSPACE_OPTIONS + ) + .toPromise(); + // Build dummy application + workingTree = await runner + .runExternalSchematicAsync( + '@schematics/angular', + 'application', + APPLICATION_OPTIONS, + workingTree + ) + .toPromise(); + + return await runner + .runSchematicAsync('ng-add', options, workingTree) + .toPromise(); +} + +describe('@puppeteer/ng-schematics: ng-add', () => { + // Stop outgoing Request for version fetching + before(() => { + const httpsGetStub = sinon.stub(https, 'get'); + httpsGetStub.returns({ + on: (_: any, callback: () => void) => { + callback(); + }, + } as any); + }); + + after(() => { + sinon.restore(); + }); + + it('should create base files and update to "package.json"', async () => { + const tree = await buildTestingTree(); + const {devDependencies, scripts} = getPackageJson(tree); + const {builder, configurations} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/tsconfig.json')); + expect(tree.files).toContain(getProjectFile('e2e/tests/app.e2e.ts')); + expect(devDependencies).toContain('puppeteer'); + expect(scripts['e2e']).toBe('ng e2e'); + expect(builder).toBe('@puppeteer/ng-schematics:puppeteer'); + expect(configurations).toEqual({ + production: { + devServerTarget: 'sandbox:serve:production', + }, + }); + }); + + it('should update create proper "ng" command for non default tester', async () => { + const tree = await buildTestingTree({ + isDefaultTester: false, + }); + const {scripts} = getPackageJson(tree); + const {builder} = getAngularJsonScripts(tree, false); + + expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer'); + expect(builder).toBe('@puppeteer/ng-schematics:puppeteer'); + }); + + it('should create Puppeteer config', async () => { + const {files} = await buildTestingTree({ + exportConfig: true, + }); + + expect(files).toContain(getProjectFile('.puppeteerrc.cjs')); + }); + + it('should not create Puppeteer config', async () => { + const {files} = await buildTestingTree({ + exportConfig: false, + }); + + expect(files).not.toContain(getProjectFile('.puppeteerrc.cjs')); + }); + + it('should create Jasmine files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'jasmine', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json')); + expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js')); + expect(devDependencies).toContain('jasmine'); + expect(devDependencies).toContain('@babel/core'); + expect(devDependencies).toContain('@babel/register'); + expect(devDependencies).toContain('@babel/preset-typescript'); + expect(options['commands']).toEqual([ + [`jasmine`, '--config=./e2e/support/jasmine.json'], + ]); + }); + + it('should create Jest files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'jest', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/jest.config.js')); + expect(devDependencies).toContain('jest'); + expect(devDependencies).toContain('@types/jest'); + expect(devDependencies).toContain('ts-jest'); + expect(options['commands']).toEqual([[`jest`, '-c', 'e2e/jest.config.js']]); + }); + + it('should create Mocha files and update "package.json"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'mocha', + }); + const {devDependencies} = getPackageJson(tree); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js')); + expect(tree.files).toContain(getProjectFile('e2e/babel.js')); + expect(devDependencies).toContain('mocha'); + expect(devDependencies).toContain('@types/mocha'); + expect(devDependencies).toContain('@babel/core'); + expect(devDependencies).toContain('@babel/register'); + expect(devDependencies).toContain('@babel/preset-typescript'); + expect(options['commands']).toEqual([ + [`mocha`, '--config=./e2e/.mocharc.js'], + ]); + }); + + it('should create Node files"', async () => { + const tree = await buildTestingTree({ + testingFramework: 'node', + }); + const {options} = getAngularJsonScripts(tree); + + expect(tree.files).toContain(getProjectFile('e2e/.gitignore')); + expect(options['commands']).toEqual([ + [`tsc`, '-p', 'e2e/tsconfig.json'], + ['node', '--test', 'e2e/'], + ]); + }); +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js new file mode 100644 index 0000000000..bca61b8101 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tools/copySchemaFiles.js @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs/promises'); +const {join} = require('path'); +const path = require('path'); + +/** + * + * @param {String} directory + * @param {String[]} files + */ +async function findSchemaFiles(directory, files = []) { + const items = await fs.readdir(directory); + const promises = []; + // Match any listing that has no *.* format + // Ignore files folder + const regEx = /^.*\.[^\s]*$/; + + items.forEach(item => { + if (!item.match(regEx)) { + promises.push(findSchemaFiles(`${directory}/${item}`, files)); + } else if (item.endsWith('.json') || directory.includes('files')) { + files.push(`${directory}/${item}`); + } + }); + + await Promise.all(promises); + + return files; +} + +async function copySchemaFiles() { + const srcDir = join(__dirname, '..', 'src'); + const outputDir = join(__dirname, '..', 'lib'); + const files = await findSchemaFiles(srcDir); + + const moves = files.map(file => { + const to = file.replace(srcDir, outputDir); + + return {from: file, to}; + }); + + // Because fs.cp is Experimental (recursive support) + // We need to create directories first and copy the files + await Promise.all( + moves.map(({to}) => { + const dir = path.dirname(to); + return fs.mkdir(dir, {recursive: true}); + }) + ); + await Promise.all( + moves.map(({from, to}) => { + return fs.copyFile(from, to); + }) + ); +} + +copySchemaFiles(); diff --git a/remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js b/remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js new file mode 100644 index 0000000000..44f131bfac --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tools/sandbox.js @@ -0,0 +1,104 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const {spawn} = require('child_process'); +const {readFile, writeFile} = require('fs/promises'); +const {join} = require('path'); +const {cwd} = require('process'); + +const isInit = process.argv.indexOf('--init') !== -1; +const isBuild = process.argv.indexOf('--build') !== -1; +const commands = { + build: ['npm run build'], + createSandbox: ['npx ng new sandbox --defaults'], + runSchematics: [ + { + command: 'npm run schematics', + options: { + cwd: join(cwd(), '/sandbox/'), + }, + }, + ], +}; +const scripts = { + // Deletes all files created by Puppeteer Ng-Schematics to avoid errors + 'delete:file': + 'rm -f .puppeteerrc.cjs && rm -f tsconfig.e2e.json && rm -R -f e2e/', + // Runs the Puppeteer Ng-Schematics against the sandbox + schematics: 'npm run delete:file && schematics ../:ng-add --dry-run=false', +}; +/** + * + * @param {string | object} toExecute + * @returns {Promise<boolean>} + */ +async function executeCommand(commands) { + for (const toExecute of commands) { + let executable; + let args; + let options = {}; + if (typeof toExecute === 'string') { + [executable, ...args] = toExecute.split(' '); + } else { + [executable, ...args] = toExecute.command.split(' '); + options = toExecute.options ?? {}; + } + + await new Promise((resolve, reject) => { + const createProcess = spawn(executable, args, { + stdio: 'inherit', + shell: true, + ...options, + }); + + createProcess.on('error', message => { + console.error(message); + reject(message); + }); + + createProcess.on('exit', code => { + if (code === 0) { + resolve(true); + } else { + reject(); + } + }); + }); + } +} + +async function main() { + if (isInit) { + await executeCommand(commands.createSandbox); + + const packageJsonFile = join(cwd(), '/sandbox/package.json'); + const packageJson = JSON.parse(await readFile(packageJsonFile)); + packageJson['scripts'] = { + ...packageJson['scripts'], + ...scripts, + }; + await writeFile(packageJsonFile, JSON.stringify(packageJson, null, 2)); + } else { + if (isBuild) { + await executeCommand(commands.build); + } + await executeCommand(commands.runSchematics); + } +} + +main().catch(() => { + console.log('\n'); +}); diff --git a/remote/test/puppeteer/packages/ng-schematics/tsconfig.json b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json new file mode 100644 index 0000000000..115c833159 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "tsconfig", + "lib": ["ES2018"], + "module": "CommonJS", + "noEmitOnError": true, + "rootDir": "src/", + "outDir": "lib/", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "types": ["node"], + "target": "ES6" + }, + "include": ["src/**/*"], + "exclude": ["src/**/files/**/*"], + "references": [{"path": "./tsconfig.spec.json"}] +} diff --git a/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json b/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json new file mode 100644 index 0000000000..8afa532cb1 --- /dev/null +++ b/remote/test/puppeteer/packages/ng-schematics/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "test/src/", + "outDir": "test/build/", + "types": ["node", "mocha"] + }, + "include": ["test/src/**/*"], + "exclude": ["test/build/**/*"] +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/.gitignore b/remote/test/puppeteer/packages/puppeteer-core/.gitignore new file mode 100644 index 0000000000..42061c01a1 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/.gitignore @@ -0,0 +1 @@ +README.md
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md new file mode 100644 index 0000000000..41457fd4d4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/CHANGELOG.md @@ -0,0 +1,1322 @@ +# 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. + +## [20.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.0.0...puppeteer-core-v20.1.0) (2023-05-03) + + +### Features + +* **chrome:** roll to Chrome 113.0.5672.63 (r1121455) ([#10116](https://github.com/puppeteer/puppeteer/issues/10116)) ([19f4334](https://github.com/puppeteer/puppeteer/commit/19f43348a884edfc3e73ab60e41a9757239df013)) + +## [20.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.11.1...puppeteer-core-v20.0.0) (2023-05-02) + + +### ⚠ BREAKING CHANGES + +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) + +### Features + +* add AbortSignal to waitForFunction ([#10078](https://github.com/puppeteer/puppeteer/issues/10078)) ([4dd4cb9](https://github.com/puppeteer/puppeteer/commit/4dd4cb929242a6b1a621fd461edd3167d40e1c4c)) +* drop support for node14 ([#10019](https://github.com/puppeteer/puppeteer/issues/10019)) ([7405d65](https://github.com/puppeteer/puppeteer/commit/7405d6585aa09b240fbab09aa360674d4442b3d9)) +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://github.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab)) + + +### Bug Fixes + +* use AbortSignal.throwIfAborted ([#10105](https://github.com/puppeteer/puppeteer/issues/10105)) ([575f00a](https://github.com/puppeteer/puppeteer/commit/575f00a31d0278f7ff27096e770ff84399cd9993)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.5.0 to 1.0.0 + +## [19.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.11.0...puppeteer-core-v19.11.1) (2023-04-25) + + +### Bug Fixes + +* implement click `count` ([#10069](https://github.com/puppeteer/puppeteer/issues/10069)) ([8124a7d](https://github.com/puppeteer/puppeteer/commit/8124a7d5bfc1cfa8cb579271f78ce586efc62b8e)) +* implement flag for disabling headless warning ([#10073](https://github.com/puppeteer/puppeteer/issues/10073)) ([cfe9bbc](https://github.com/puppeteer/puppeteer/commit/cfe9bbc852d014b31c754950590b6b6c96573eeb)) + +## [19.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.10.1...puppeteer-core-v19.11.0) (2023-04-24) + + +### Features + +* add warn for `headless: true` ([#10039](https://github.com/puppeteer/puppeteer/issues/10039)) ([23d6a95](https://github.com/puppeteer/puppeteer/commit/23d6a95cf10c90f8aba2b12d7b02a73072e20382)) + + +### Bug Fixes + +* infer last pressed button in mouse move ([#10067](https://github.com/puppeteer/puppeteer/issues/10067)) ([a6eaac4](https://github.com/puppeteer/puppeteer/commit/a6eaac4c39d4b0ab3ab1a3c2f319a70fde393edb)) + +## [19.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.10.0...puppeteer-core-v19.10.1) (2023-04-21) + + +### Bug Fixes + +* move fs.js to the node folder ([#10055](https://github.com/puppeteer/puppeteer/issues/10055)) ([704624e](https://github.com/puppeteer/puppeteer/commit/704624eb2045a7e38ed14044d6863a2871e9d7e2)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.4.1 to 0.5.0 + +## [19.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.9.1...puppeteer-core-v19.10.0) (2023-04-20) + + +### Features + +* support AbortController in waitForSelector ([#10018](https://github.com/puppeteer/puppeteer/issues/10018)) ([9109b76](https://github.com/puppeteer/puppeteer/commit/9109b76276c9d86a2c521c72fc5b7189979279ca)) +* **webworker:** expose WebWorker.client ([#10042](https://github.com/puppeteer/puppeteer/issues/10042)) ([c125128](https://github.com/puppeteer/puppeteer/commit/c12512822a546e7bfdefd2c68f020aab2a308f4f)) + + +### Bug Fixes + +* continue requests without network instrumentation ([#10046](https://github.com/puppeteer/puppeteer/issues/10046)) ([8283823](https://github.com/puppeteer/puppeteer/commit/8283823cb860528a938e84cb5ba2b5f4cf980e83)) +* install bindings once ([#10049](https://github.com/puppeteer/puppeteer/issues/10049)) ([690aec1](https://github.com/puppeteer/puppeteer/commit/690aec1b5cb4e7e574abde9c533c6c0954e6f1aa)) + +## [19.9.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.9.0...puppeteer-core-v19.9.1) (2023-04-17) + + +### Bug Fixes + +* improve mouse actions ([#10021](https://github.com/puppeteer/puppeteer/issues/10021)) ([34db39e](https://github.com/puppeteer/puppeteer/commit/34db39e4474efee9d4579743026c3d6b6c8e494b)) + +## [19.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.5...puppeteer-core-v19.9.0) (2023-04-13) + + +### Features + +* add ElementHandle.isVisible and ElementHandle.isHidden ([#10007](https://github.com/puppeteer/puppeteer/issues/10007)) ([26c81b7](https://github.com/puppeteer/puppeteer/commit/26c81b7408a98cb9ef1aac9b57a038b699e6d518)) +* add ElementHandle.scrollIntoView ([#10005](https://github.com/puppeteer/puppeteer/issues/10005)) ([0d556a7](https://github.com/puppeteer/puppeteer/commit/0d556a71d6bcd5da501724ccbb4ce0be433768df)) + + +### Bug Fixes + +* make isIntersectingViewport work with SVG elements ([#10004](https://github.com/puppeteer/puppeteer/issues/10004)) ([656b562](https://github.com/puppeteer/puppeteer/commit/656b562c7488d4976a7a53264feef508c6b629dd)) + + +### Performance Improvements + +* amortize handle iterator ([#10002](https://github.com/puppeteer/puppeteer/issues/10002)) ([ab27f73](https://github.com/puppeteer/puppeteer/commit/ab27f738c9abb56f6083d02f7f45d2b8da9fc3f3)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.4.0 to 0.4.1 + +## [19.8.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.4...puppeteer-core-v19.8.5) (2023-04-06) + + +### Bug Fixes + +* add filter to setDiscoverTargets for Firefox ([#9693](https://github.com/puppeteer/puppeteer/issues/9693)) ([c09764e](https://github.com/puppeteer/puppeteer/commit/c09764e4c43d7a62096f430b598d63f2b688e860)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.3.3 to 0.4.0 + +## [19.8.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.3...puppeteer-core-v19.8.4) (2023-04-06) + + +### Bug Fixes + +* ignore extraInfo events if the response is served from cache ([#9983](https://github.com/puppeteer/puppeteer/issues/9983)) ([e7265c9](https://github.com/puppeteer/puppeteer/commit/e7265c9aa94e749de5745e5e98d45d4659f19d30)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.3.2 to 0.3.3 + +## [19.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.1...puppeteer-core-v19.8.3) (2023-04-03) + + +### Bug Fixes + +* use shadowRoot for tree walker ([#9950](https://github.com/puppeteer/puppeteer/issues/9950)) ([728547d](https://github.com/puppeteer/puppeteer/commit/728547d4608e8c601e209ede860493b1986da174)) + +## [19.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.8.0...puppeteer-core-v19.8.1) (2023-03-28) + + +### Bug Fixes + +* increase the default protocol timeout ([#9928](https://github.com/puppeteer/puppeteer/issues/9928)) ([4465f4b](https://github.com/puppeteer/puppeteer/commit/4465f4bd1900afc0b049ac863f4e372453a0c234)) + +## [19.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.5...puppeteer-core-v19.8.0) (2023-03-24) + + +### Features + +* add Page.waitForDevicePrompt ([#9299](https://github.com/puppeteer/puppeteer/issues/9299)) ([a5149d5](https://github.com/puppeteer/puppeteer/commit/a5149d52f54036a27a411bc070902b1eb3a7a629)) +* **chromium:** roll to Chromium 112.0.5614.0 (r1108766) ([#9841](https://github.com/puppeteer/puppeteer/issues/9841)) ([eddb1f6](https://github.com/puppeteer/puppeteer/commit/eddb1f6ec3958b79fea297123f7621eb7beaff04)) + + +### Bug Fixes + +* fallback to CSS ([#9876](https://github.com/puppeteer/puppeteer/issues/9876)) ([e6ec9c2](https://github.com/puppeteer/puppeteer/commit/e6ec9c295847fa0f1ec240952f0f2523bb13b7c8)) +* implement protocol-level timeouts ([#9877](https://github.com/puppeteer/puppeteer/issues/9877)) ([510b36c](https://github.com/puppeteer/puppeteer/commit/510b36c50001c95783b00dc8af42b5801ec57358)) +* viewport.deviceScaleFactor can be set to system default ([#9911](https://github.com/puppeteer/puppeteer/issues/9911)) ([022c909](https://github.com/puppeteer/puppeteer/commit/022c90932658d13ff4ae4aa51d26716f5dbe54ac)) +* waitForNavigation issue with aborted events ([#9883](https://github.com/puppeteer/puppeteer/issues/9883)) ([36c029b](https://github.com/puppeteer/puppeteer/commit/36c029b38d64a10590bfc74ecea255a58914b0d2)) + +## [19.7.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.4...puppeteer-core-v19.7.5) (2023-03-14) + + +### Bug Fixes + +* sort elements based on selector matching algorithm ([#9836](https://github.com/puppeteer/puppeteer/issues/9836)) ([9044609](https://github.com/puppeteer/puppeteer/commit/9044609be3ea78c650420533e7f6f40b83cedd99)) + + +### Performance Improvements + +* use `querySelector*` for pure CSS selectors ([#9835](https://github.com/puppeteer/puppeteer/issues/9835)) ([8aea8e0](https://github.com/puppeteer/puppeteer/commit/8aea8e047103b72c0238dde8e4777acf7897ddaa)) + +## [19.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.3...puppeteer-core-v19.7.4) (2023-03-10) + + +### Bug Fixes + +* call _detach on disconnect ([#9807](https://github.com/puppeteer/puppeteer/issues/9807)) ([bc1a04d](https://github.com/puppeteer/puppeteer/commit/bc1a04def8f699ad245c12ec69ac176e3e7e888d)) +* restore rimraf for puppeteer-core code ([#9815](https://github.com/puppeteer/puppeteer/issues/9815)) ([cefc4ea](https://github.com/puppeteer/puppeteer/commit/cefc4eab4750d2c1209eb36ca44f6963a4a6bf4c)) +* update troubleshooting guide links in errors ([#9821](https://github.com/puppeteer/puppeteer/issues/9821)) ([0165f06](https://github.com/puppeteer/puppeteer/commit/0165f06deef9e45862fd127a205ade5ad30ddaa3)) + +## [19.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.2...puppeteer-core-v19.7.3) (2023-03-06) + + +### Bug Fixes + +* update dependencies ([#9781](https://github.com/puppeteer/puppeteer/issues/9781)) ([364b23f](https://github.com/puppeteer/puppeteer/commit/364b23f8b5c7b04974f233c58e5ded9a8f912ff2)) + +## [19.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.1...puppeteer-core-v19.7.2) (2023-02-20) + + +### Bug Fixes + +* bump chromium-bidi to a version that does not declare mitt as a peer dependency ([#9701](https://github.com/puppeteer/puppeteer/issues/9701)) ([82916c1](https://github.com/puppeteer/puppeteer/commit/82916c102b2c399093ba9019e272207b5ce81849)) + +## [19.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.7.0...puppeteer-core-v19.7.1) (2023-02-15) + + +### Bug Fixes + +* fix circularity on JSHandle interface ([#9661](https://github.com/puppeteer/puppeteer/issues/9661)) ([eb13863](https://github.com/puppeteer/puppeteer/commit/eb138635d661d3cdaf2940959fece5aca482178a)) +* make chromium-bidi an opt peer dep ([#9667](https://github.com/puppeteer/puppeteer/issues/9667)) ([c6054ac](https://github.com/puppeteer/puppeteer/commit/c6054ac1a56c08ee7bf01321878699b7b4ab4e0b)) + +## [19.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.3...puppeteer-core-v19.7.0) (2023-02-13) + + +### Features + +* add touchstart, touchmove and touchend methods ([#9622](https://github.com/puppeteer/puppeteer/issues/9622)) ([c8bb11a](https://github.com/puppeteer/puppeteer/commit/c8bb11adfcf1537032730a91baa3c36a6e324926)) +* **chromium:** roll to Chromium 111.0.5556.0 (r1095492) ([#9656](https://github.com/puppeteer/puppeteer/issues/9656)) ([df59d01](https://github.com/puppeteer/puppeteer/commit/df59d010c20644da06eb4c4e28a11c4eea164aba)) + + +### Bug Fixes + +* `page.goto` error throwing on 40x/50x responses with an empty body ([#9523](https://github.com/puppeteer/puppeteer/issues/9523)) ([#9577](https://github.com/puppeteer/puppeteer/issues/9577)) ([ddb0cc1](https://github.com/puppeteer/puppeteer/commit/ddb0cc174d2a14c0948dcdaf9bae78620937c667)) + +## [19.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.2...puppeteer-core-v19.6.3) (2023-02-01) + + +### Bug Fixes + +* ignore not found contexts for console messages ([#9595](https://github.com/puppeteer/puppeteer/issues/9595)) ([390685b](https://github.com/puppeteer/puppeteer/commit/390685bbe52c22b686fc0e3119b4ac7b1073c581)) +* restore WaitTask terminate condition ([#9612](https://github.com/puppeteer/puppeteer/issues/9612)) ([e16cbc6](https://github.com/puppeteer/puppeteer/commit/e16cbc6626cffd40d0caa30801620e7293455006)) + +## [19.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.1...puppeteer-core-v19.6.2) (2023-01-27) + + +### Bug Fixes + +* atomically get Puppeteer utilities ([#9597](https://github.com/puppeteer/puppeteer/issues/9597)) ([050a7b0](https://github.com/puppeteer/puppeteer/commit/050a7b062415ebaf10bcb71c405143eacc4e5d4b)) + +## [19.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.6.0...puppeteer-core-v19.6.1) (2023-01-26) + + +### Bug Fixes + +* don't clean up previous browser versions ([#9568](https://github.com/puppeteer/puppeteer/issues/9568)) ([344bc2a](https://github.com/puppeteer/puppeteer/commit/344bc2af62e4068fe2cb8162d4b6c8242aac843b)), closes [#9533](https://github.com/puppeteer/puppeteer/issues/9533) +* mimic rejection for PuppeteerUtil on early call ([#9589](https://github.com/puppeteer/puppeteer/issues/9589)) ([1980de9](https://github.com/puppeteer/puppeteer/commit/1980de91a161523c7098a79919b20e6d8d2e5d81)) +* **revert:** use LazyArg for puppeteer utilities ([#9590](https://github.com/puppeteer/puppeteer/issues/9590)) ([6edd996](https://github.com/puppeteer/puppeteer/commit/6edd99676827de2c83f7a858e4f903b1c34e7d35)) +* use LazyArg for puppeteer utilities ([#9575](https://github.com/puppeteer/puppeteer/issues/9575)) ([496658f](https://github.com/puppeteer/puppeteer/commit/496658f02945b53096483f36cb3d64556cff045e)) + +## [19.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.2...puppeteer-core-v19.6.0) (2023-01-23) + + +### Features + +* **chromium:** roll to Chromium 110.0.5479.0 (r1083080) ([#9500](https://github.com/puppeteer/puppeteer/issues/9500)) ([06e816b](https://github.com/puppeteer/puppeteer/commit/06e816bbfa7b9ca84284929f654de7288c51169d)), closes [#9470](https://github.com/puppeteer/puppeteer/issues/9470) +* **page:** Adding support for referrerPolicy in `page.goto` ([#9561](https://github.com/puppeteer/puppeteer/issues/9561)) ([e3d69ec](https://github.com/puppeteer/puppeteer/commit/e3d69ec554beeac37bd206a21921d2fed3cb968c)) + + +### Bug Fixes + +* firefox revision resolution should not update chrome revision ([#9507](https://github.com/puppeteer/puppeteer/issues/9507)) ([f59bbf4](https://github.com/puppeteer/puppeteer/commit/f59bbf4014644dec6f395713e8403939aebe06ea)), closes [#9461](https://github.com/puppeteer/puppeteer/issues/9461) +* improve screenshot method types ([#9529](https://github.com/puppeteer/puppeteer/issues/9529)) ([6847f88](https://github.com/puppeteer/puppeteer/commit/6847f8835f28e97edba6fce76a4cbf85561482b9)) + +## [19.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.1...puppeteer-core-v19.5.2) (2023-01-11) + + +### Bug Fixes + +* make sure browser fetcher in launchers uses configuration ([#9493](https://github.com/puppeteer/puppeteer/issues/9493)) ([df55439](https://github.com/puppeteer/puppeteer/commit/df554397b51e97aea2765b325f9a887b50b9263a)), closes [#9470](https://github.com/puppeteer/puppeteer/issues/9470) + +## [19.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.5.0...puppeteer-core-v19.5.1) (2023-01-11) + + +### Bug Fixes + +* use puppeteer node for installation script ([#9489](https://github.com/puppeteer/puppeteer/issues/9489)) ([9bf90d9](https://github.com/puppeteer/puppeteer/commit/9bf90d9f4b5aeab06f8b433714712cad3259d36e)) + +## [19.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.4.1...puppeteer-core-v19.5.0) (2023-01-05) + + +### Features + +* add element validation ([#9352](https://github.com/puppeteer/puppeteer/issues/9352)) ([c7a063a](https://github.com/puppeteer/puppeteer/commit/c7a063a15274856184356e15f2ae4be41191d309)) + + +### Bug Fixes + +* **puppeteer-core:** target interceptor is not async ([#9430](https://github.com/puppeteer/puppeteer/issues/9430)) ([e3e9cc6](https://github.com/puppeteer/puppeteer/commit/e3e9cc622ac32f2067b6e74b5e8706c63169a157)) + +## [19.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.4.0...puppeteer-core-v19.4.1) (2022-12-16) + + +### Bug Fixes + +* improve a11y snapshot handling if the tree is not correct ([#9405](https://github.com/puppeteer/puppeteer/issues/9405)) ([02fe501](https://github.com/puppeteer/puppeteer/commit/02fe50194e60bd14c3a82539473a0313ab88c766)), closes [#9404](https://github.com/puppeteer/puppeteer/issues/9404) +* remove oopif expectations and fix oopif flakiness ([#9375](https://github.com/puppeteer/puppeteer/issues/9375)) ([810e0cd](https://github.com/puppeteer/puppeteer/commit/810e0cd74ecef353cfa43746c18bd5f580a3233d)) + +## [19.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.3.0...puppeteer-core-v19.4.0) (2022-12-07) + + +### Features + +* ability to send headers via ws connection to browser in node.js environment ([#9314](https://github.com/puppeteer/puppeteer/issues/9314)) ([937fffa](https://github.com/puppeteer/puppeteer/commit/937fffaedc340ea12d5f6636d3ba6598cb22e397)), closes [#7218](https://github.com/puppeteer/puppeteer/issues/7218) +* **chromium:** roll to Chromium 109.0.5412.0 (r1069273) ([#9364](https://github.com/puppeteer/puppeteer/issues/9364)) ([1875da6](https://github.com/puppeteer/puppeteer/commit/1875da61916df1fbcf98047858c01075bd9af189)), closes [#9233](https://github.com/puppeteer/puppeteer/issues/9233) +* **puppeteer-core:** keydown supports commands ([#9357](https://github.com/puppeteer/puppeteer/issues/9357)) ([b7ebc5d](https://github.com/puppeteer/puppeteer/commit/b7ebc5d9bb9b9940ffdf470e51d007f709587d40)) + + +### Bug Fixes + +* **puppeteer-core:** avoid type instantiation errors ([#9370](https://github.com/puppeteer/puppeteer/issues/9370)) ([17f31a9](https://github.com/puppeteer/puppeteer/commit/17f31a9ee408ca5a08fe6dbceb8915e710156bd3)), closes [#9369](https://github.com/puppeteer/puppeteer/issues/9369) + +## [19.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.2...puppeteer-core-v19.3.0) (2022-11-23) + + +### Features + +* **puppeteer-core:** Infer element type from complex selector ([#9253](https://github.com/puppeteer/puppeteer/issues/9253)) ([bef1061](https://github.com/puppeteer/puppeteer/commit/bef1061c064e5135d86a48fffd7278f3e7f4a29e)) +* **puppeteer-core:** update Chrome launcher flags ([#9239](https://github.com/puppeteer/puppeteer/issues/9239)) ([ae87bfc](https://github.com/puppeteer/puppeteer/commit/ae87bfc2b4361556e3660a1de2c6db348ce663ae)) + + +### Bug Fixes + +* remove boundary conditions for visibility ([#9249](https://github.com/puppeteer/puppeteer/issues/9249)) ([e003513](https://github.com/puppeteer/puppeteer/commit/e003513c0c049aad38e374a16dc96c3e54ab0de5)) + +## [19.2.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.1...puppeteer-core-v19.2.2) (2022-11-03) + + +### Bug Fixes + +* update missing product message ([#9207](https://github.com/puppeteer/puppeteer/issues/9207)) ([29f47e2](https://github.com/puppeteer/puppeteer/commit/29f47e2e150ff7bfd89e38a4ce4ca34eac7f2fdf)) + +## [19.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.2.0...puppeteer-core-v19.2.1) (2022-10-28) + + +### Bug Fixes + +* resolve navigation requests when request fails ([#9178](https://github.com/puppeteer/puppeteer/issues/9178)) ([c11297b](https://github.com/puppeteer/puppeteer/commit/c11297baa5124eb89f7686c3eb446d2ba1b7123a)), closes [#9175](https://github.com/puppeteer/puppeteer/issues/9175) + +## [19.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.1.1...puppeteer-core-v19.2.0) (2022-10-26) + + +### Features + +* **chromium:** roll to Chromium 108.0.5351.0 (r1056772) ([#9153](https://github.com/puppeteer/puppeteer/issues/9153)) ([e78a4e8](https://github.com/puppeteer/puppeteer/commit/e78a4e89c22bb1180e72d180c16b39673ff9125e)) + +## [19.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.1.0...puppeteer-core-v19.1.1) (2022-10-24) + + +### Bug Fixes + +* update documentation on configuring puppeteer ([#9150](https://github.com/puppeteer/puppeteer/issues/9150)) ([f07ad2c](https://github.com/puppeteer/puppeteer/commit/f07ad2c6616ecd2a959b0c1a65b167ba77611d61)) + +## [19.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v19.0.0...puppeteer-core-v19.1.0) (2022-10-21) + + +### Features + +* expose browser context id ([#9134](https://github.com/puppeteer/puppeteer/issues/9134)) ([122778a](https://github.com/puppeteer/puppeteer/commit/122778a1f8b60e0dcc6f0ffcb2097e95ae98f4a3)), closes [#9132](https://github.com/puppeteer/puppeteer/issues/9132) +* use configuration files ([#9140](https://github.com/puppeteer/puppeteer/issues/9140)) ([ec20174](https://github.com/puppeteer/puppeteer/commit/ec201744f077987b288e3dff52c0906fe700f6fb)), closes [#9128](https://github.com/puppeteer/puppeteer/issues/9128) + + +### Bug Fixes + +* update `BrowserFetcher` deprecation message ([#9141](https://github.com/puppeteer/puppeteer/issues/9141)) ([efcbc97](https://github.com/puppeteer/puppeteer/commit/efcbc97c60e4cfd49a9ed25a900f6133d06b290b)) + +## [19.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.2.1...puppeteer-core-v19.0.0) (2022-10-14) + + +### ⚠ BREAKING CHANGES + +* use `~/.cache/puppeteer` for browser downloads (#9095) +* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` (#9079) +* refactor custom query handler API (#9078) +* remove `puppeteer.devices` in favor of `KnownDevices` (#9075) +* deprecate indirect network condition imports (#9074) +* deprecate indirect error imports (#9072) + +### Features + +* add ability to collect JS code coverage at the function level ([#9027](https://github.com/puppeteer/puppeteer/issues/9027)) ([a032583](https://github.com/puppeteer/puppeteer/commit/a032583b6c9b469bda699bca200b180206d61247)) +* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` ([#9079](https://github.com/puppeteer/puppeteer/issues/9079)) ([7294dfe](https://github.com/puppeteer/puppeteer/commit/7294dfe9c6c3b224f95ba6d59b5ef33d379fd09a)), closes [#8999](https://github.com/puppeteer/puppeteer/issues/8999) +* use `~/.cache/puppeteer` for browser downloads ([#9095](https://github.com/puppeteer/puppeteer/issues/9095)) ([3df375b](https://github.com/puppeteer/puppeteer/commit/3df375baedad64b8773bb1e1e6f81b604ed18989)) + + +### Bug Fixes + +* deprecate indirect error imports ([#9072](https://github.com/puppeteer/puppeteer/issues/9072)) ([9f4f43a](https://github.com/puppeteer/puppeteer/commit/9f4f43a28b06787a1cf97efe904ccfe7237dffdd)) +* deprecate indirect network condition imports ([#9074](https://github.com/puppeteer/puppeteer/issues/9074)) ([41d0122](https://github.com/puppeteer/puppeteer/commit/41d0122b94f41b308536c48ced345dec8c272a49)) +* refactor custom query handler API ([#9078](https://github.com/puppeteer/puppeteer/issues/9078)) ([1847704](https://github.com/puppeteer/puppeteer/commit/1847704789e2888c755de8c739d567364b8ad645)) +* remove `puppeteer.devices` in favor of `KnownDevices` ([#9075](https://github.com/puppeteer/puppeteer/issues/9075)) ([87c08fd](https://github.com/puppeteer/puppeteer/commit/87c08fd86a79b63308ad8d46c5f7acd1927505f8)) +* remove viewport conditions in `waitForSelector` ([#9087](https://github.com/puppeteer/puppeteer/issues/9087)) ([acbc599](https://github.com/puppeteer/puppeteer/commit/acbc59999bf800eeac75c4045b75a32b4357c79e)) + +## [18.2.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.2.0...puppeteer-core-v18.2.1) (2022-10-06) + + +### Bug Fixes + +* add README to package during prepack ([#9057](https://github.com/puppeteer/puppeteer/issues/9057)) ([9374e23](https://github.com/puppeteer/puppeteer/commit/9374e23d3da5e40378461ed08db24649730a445a)) +* waitForRequest works with async predicate ([#9058](https://github.com/puppeteer/puppeteer/issues/9058)) ([8f6b2c9](https://github.com/puppeteer/puppeteer/commit/8f6b2c9b7c219d405c954bf7af082d3d29fd48ff)) + +## [18.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v18.1.0...puppeteer-core-v18.2.0) (2022-10-05) + + +### Features + +* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545)) + + +## [18.1.0](https://github.com/puppeteer/puppeteer/compare/v18.0.5...v18.1.0) (2022-10-05) + +### Features + +* **chromium:** roll to Chromium 107.0.5296.0 (r1045629) ([#9039](https://github.com/puppeteer/puppeteer/issues/9039)) ([022fbde](https://github.com/puppeteer/puppeteer/commit/022fbde85e067e8c419cf42dd571f9a1187c343c)) + +## [18.0.5](https://github.com/puppeteer/puppeteer/compare/v18.0.4...v18.0.5) (2022-09-22) + + +### Bug Fixes + +* add missing npm config environment variable ([#8996](https://github.com/puppeteer/puppeteer/issues/8996)) ([7c1be20](https://github.com/puppeteer/puppeteer/commit/7c1be20aef46aaf5029732a580ec65aa8008aa9c)) + +## [18.0.4](https://github.com/puppeteer/puppeteer/compare/v18.0.3...v18.0.4) (2022-09-21) + + +### Bug Fixes + +* hardcode binding names ([#8993](https://github.com/puppeteer/puppeteer/issues/8993)) ([7e20554](https://github.com/puppeteer/puppeteer/commit/7e2055433e79ef20f6dcdf02f92e1d64564b7d33)) + +## [18.0.3](https://github.com/puppeteer/puppeteer/compare/v18.0.2...v18.0.3) (2022-09-20) + + +### Bug Fixes + +* change injected.ts imports ([#8987](https://github.com/puppeteer/puppeteer/issues/8987)) ([10a114d](https://github.com/puppeteer/puppeteer/commit/10a114d36f2add90860950f61b3f8b93258edb5c)) + +## [18.0.2](https://github.com/puppeteer/puppeteer/compare/v18.0.1...v18.0.2) (2022-09-19) + + +### Bug Fixes + +* mark internal objects ([#8984](https://github.com/puppeteer/puppeteer/issues/8984)) ([181a148](https://github.com/puppeteer/puppeteer/commit/181a148269fce1575f5e37056929ecdec0517586)) + +## [18.0.1](https://github.com/puppeteer/puppeteer/compare/v18.0.0...v18.0.1) (2022-09-19) + + +### Bug Fixes + +* internal lazy params ([#8982](https://github.com/puppeteer/puppeteer/issues/8982)) ([d504597](https://github.com/puppeteer/puppeteer/commit/d5045976a6dd321bbd265b84c2474ff1ad5d0b77)) + +## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19) + + +### ⚠ BREAKING CHANGES + +* fix bounding box visibility conditions (#8954) + +### Features + +* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1)) + + +### Bug Fixes + +* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53)) +* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8)) +* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7)) + +## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08) + + +### Bug Fixes + +* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919) +* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915) + +## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07) + + +### Bug Fixes + +* add missing code coverage ranges that span only a single character ([#8911](https://github.com/puppeteer/puppeteer/issues/8911)) ([0c577b9](https://github.com/puppeteer/puppeteer/commit/0c577b9bf8855dc0ccb6098cd43a25c528f6d7f5)) +* add Page.getDefaultTimeout getter ([#8903](https://github.com/puppeteer/puppeteer/issues/8903)) ([3240095](https://github.com/puppeteer/puppeteer/commit/32400954c50cbddc48468ad118c3f8a47653b9d3)), closes [#8901](https://github.com/puppeteer/puppeteer/issues/8901) +* don't detect project root for puppeteer-core ([#8907](https://github.com/puppeteer/puppeteer/issues/8907)) ([b4f5ea1](https://github.com/puppeteer/puppeteer/commit/b4f5ea1167a60c870194c70d22f5372ada5b7c4c)), closes [#8896](https://github.com/puppeteer/puppeteer/issues/8896) +* support scale for screenshot clips ([#8908](https://github.com/puppeteer/puppeteer/issues/8908)) ([260e428](https://github.com/puppeteer/puppeteer/commit/260e4282275ab1d05c86e5643e2a02c01f269a9c)), closes [#5329](https://github.com/puppeteer/puppeteer/issues/5329) +* work around a race in waitForFileChooser ([#8905](https://github.com/puppeteer/puppeteer/issues/8905)) ([053d960](https://github.com/puppeteer/puppeteer/commit/053d960fb593e514e7914d7da9af436afc39a12f)), closes [#6040](https://github.com/puppeteer/puppeteer/issues/6040) + +## [17.1.1](https://github.com/puppeteer/puppeteer/compare/v17.1.0...v17.1.1) (2022-09-05) + + +### Bug Fixes + +* restore deferred promise debugging ([#8895](https://github.com/puppeteer/puppeteer/issues/8895)) ([7b42250](https://github.com/puppeteer/puppeteer/commit/7b42250c7bb91ac873307acda493726ffc4c54a8)) + +## [17.1.0](https://github.com/puppeteer/puppeteer/compare/v17.0.0...v17.1.0) (2022-09-02) + + +### Features + +* **chromium:** roll to Chromium 106.0.5249.0 (r1036745) ([#8869](https://github.com/puppeteer/puppeteer/issues/8869)) ([6e9a47a](https://github.com/puppeteer/puppeteer/commit/6e9a47a6faa06d241dec0bcf7bcdf49370517008)) + + +### Bug Fixes + +* allow getting a frame from an elementhandle ([#8875](https://github.com/puppeteer/puppeteer/issues/8875)) ([3732757](https://github.com/puppeteer/puppeteer/commit/3732757450b4363041ccbacc3b236289a156abb0)) +* typos in documentation ([#8858](https://github.com/puppeteer/puppeteer/issues/8858)) ([8d95a9b](https://github.com/puppeteer/puppeteer/commit/8d95a9bc920b98820aa655ad4eb2d8fd9b2b893a)) +* use the timeout setting in waitForFileChooser ([#8856](https://github.com/puppeteer/puppeteer/issues/8856)) ([f477b46](https://github.com/puppeteer/puppeteer/commit/f477b46f212da9206102da695697760eea539f05)) + +## [17.0.0](https://github.com/puppeteer/puppeteer/compare/v16.2.0...v17.0.0) (2022-08-26) + + +### ⚠ BREAKING CHANGES + +* remove `root` from `WaitForSelectorOptions` (#8848) +* internalize execution context (#8844) + +### Bug Fixes + +* allow multiple navigations to happen in LifecycleWatcher ([#8826](https://github.com/puppeteer/puppeteer/issues/8826)) ([341b669](https://github.com/puppeteer/puppeteer/commit/341b669a5e45ecbb9ffb0f28c45b520660f27ad2)), closes [#8811](https://github.com/puppeteer/puppeteer/issues/8811) +* internalize execution context ([#8844](https://github.com/puppeteer/puppeteer/issues/8844)) ([2f33237](https://github.com/puppeteer/puppeteer/commit/2f33237d0443de77d58dca4454b0c9a1d2b57d03)) +* remove `root` from `WaitForSelectorOptions` ([#8848](https://github.com/puppeteer/puppeteer/issues/8848)) ([1155c8e](https://github.com/puppeteer/puppeteer/commit/1155c8eac85b176c3334cc3d98adfe7d943dfbe6)) +* remove deferred promise timeouts ([#8835](https://github.com/puppeteer/puppeteer/issues/8835)) ([202ffce](https://github.com/puppeteer/puppeteer/commit/202ffce0aa4f34dba35fbb8e7d740af16efee35f)), closes [#8832](https://github.com/puppeteer/puppeteer/issues/8832) + +## [16.2.0](https://github.com/puppeteer/puppeteer/compare/v16.1.1...v16.2.0) (2022-08-18) + + +### Features + +* add Khmer (Cambodian) language support ([#8809](https://github.com/puppeteer/puppeteer/issues/8809)) ([34f8737](https://github.com/puppeteer/puppeteer/commit/34f873721804d57a5faf3eab8ef50340c69ed180)) + + +### Bug Fixes + +* handle service workers in extensions ([#8807](https://github.com/puppeteer/puppeteer/issues/8807)) ([2a0eefb](https://github.com/puppeteer/puppeteer/commit/2a0eefb99f0ae00dacc9e768a253308c0d18a4c3)), closes [#8800](https://github.com/puppeteer/puppeteer/issues/8800) + +## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16) + + +### Bug Fixes + +* custom sessions should not emit targetcreated events ([#8788](https://github.com/puppeteer/puppeteer/issues/8788)) ([3fad05d](https://github.com/puppeteer/puppeteer/commit/3fad05d333b79f41a7b58582c4ca493200bb5a79)), closes [#8787](https://github.com/puppeteer/puppeteer/issues/8787) +* deprecate `ExecutionContext` ([#8792](https://github.com/puppeteer/puppeteer/issues/8792)) ([b5da718](https://github.com/puppeteer/puppeteer/commit/b5da718e2e4a2004a36cf23cad555e1fc3b50333)) +* deprecate `root` in `WaitForSelectorOptions` ([#8795](https://github.com/puppeteer/puppeteer/issues/8795)) ([65a5ce8](https://github.com/puppeteer/puppeteer/commit/65a5ce8464c56fcc55e5ac3ed490f31311bbe32a)) +* deprecate `waitForTimeout` ([#8793](https://github.com/puppeteer/puppeteer/issues/8793)) ([8f612d5](https://github.com/puppeteer/puppeteer/commit/8f612d5ff855d48ae4b38bdaacf2a8fbda8e9ce8)) +* make sure there is a check for targets when timeout=0 ([#8765](https://github.com/puppeteer/puppeteer/issues/8765)) ([c23cdb7](https://github.com/puppeteer/puppeteer/commit/c23cdb73a7b113c1dd29f7e4a7a61326422c4080)), closes [#8763](https://github.com/puppeteer/puppeteer/issues/8763) +* resolve navigation flakiness ([#8768](https://github.com/puppeteer/puppeteer/issues/8768)) ([2580347](https://github.com/puppeteer/puppeteer/commit/2580347b50091d172b2a5591138a2e41ede072fe)), closes [#8644](https://github.com/puppeteer/puppeteer/issues/8644) +* specify Puppeteer version for Chromium 105.0.5173.0 ([#8766](https://github.com/puppeteer/puppeteer/issues/8766)) ([b5064b7](https://github.com/puppeteer/puppeteer/commit/b5064b7b8bd3bd9eb481b6807c65d9d06d23b9dd)) +* use targetFilter in puppeteer.launch ([#8774](https://github.com/puppeteer/puppeteer/issues/8774)) ([ee2540b](https://github.com/puppeteer/puppeteer/commit/ee2540baefeced44f6b336f2b979af5c3a4cb040)), closes [#8772](https://github.com/puppeteer/puppeteer/issues/8772) + +## [16.1.0](https://github.com/puppeteer/puppeteer/compare/v16.0.0...v16.1.0) (2022-08-06) + + +### Features + +* use an `xpath` query handler ([#8730](https://github.com/puppeteer/puppeteer/issues/8730)) ([5cf9b4d](https://github.com/puppeteer/puppeteer/commit/5cf9b4de8d50bd056db82bcaa23279b72c9313c5)) + + +### Bug Fixes + +* resolve target manager init if no existing targets detected ([#8748](https://github.com/puppeteer/puppeteer/issues/8748)) ([8cb5043](https://github.com/puppeteer/puppeteer/commit/8cb5043868f69cdff7f34f1cfe0c003ff09e281b)), closes [#8747](https://github.com/puppeteer/puppeteer/issues/8747) +* specify the target filter in setDiscoverTargets ([#8742](https://github.com/puppeteer/puppeteer/issues/8742)) ([49193cb](https://github.com/puppeteer/puppeteer/commit/49193cbf1c17f16f0ca59a9fd2ebf306f812f52b)) + +## [16.0.0](https://github.com/puppeteer/puppeteer/compare/v15.5.0...v16.0.0) (2022-08-02) + + +### ⚠ BREAKING CHANGES + +* With Chromium, Puppeteer will now attach to page/iframe targets immediately to allow reliable configuration of targets. + +### Features + +* add Dockerfile ([#8315](https://github.com/puppeteer/puppeteer/issues/8315)) ([936ed86](https://github.com/puppeteer/puppeteer/commit/936ed8607ec0c3798d2b22b590d0be0ad361a888)) +* detect Firefox in connect() automatically ([#8718](https://github.com/puppeteer/puppeteer/issues/8718)) ([2abd772](https://github.com/puppeteer/puppeteer/commit/2abd772c9c3d2b86deb71541eaac41aceef94356)) +* use CDP's auto-attach mechanism ([#8520](https://github.com/puppeteer/puppeteer/issues/8520)) ([2cbfdeb](https://github.com/puppeteer/puppeteer/commit/2cbfdeb0ca388a45cedfae865266230e1291bd29)) + + +### Bug Fixes + +* address flakiness in frame handling ([#8688](https://github.com/puppeteer/puppeteer/issues/8688)) ([6f81b23](https://github.com/puppeteer/puppeteer/commit/6f81b23728a511f7b89eaa2b8f850b22d6c4ab24)) +* disable AcceptCHFrame ([#8706](https://github.com/puppeteer/puppeteer/issues/8706)) ([96d9608](https://github.com/puppeteer/puppeteer/commit/96d9608d1de17877414a649a0737661894dd96c8)), closes [#8479](https://github.com/puppeteer/puppeteer/issues/8479) +* use loaderId to reduce test flakiness ([#8717](https://github.com/puppeteer/puppeteer/issues/8717)) ([d2f6db2](https://github.com/puppeteer/puppeteer/commit/d2f6db20735342bb3f419e85adbd51ed10470044)) + +## [15.5.0](https://github.com/puppeteer/puppeteer/compare/v15.4.2...v15.5.0) (2022-07-21) + + +### Features + +* **chromium:** roll to Chromium 105.0.5173.0 (r1022525) ([#8682](https://github.com/puppeteer/puppeteer/issues/8682)) ([f1b8ad3](https://github.com/puppeteer/puppeteer/commit/f1b8ad3269286800d31818ea4b6b3ee23f7437c3)) + +## [15.4.2](https://github.com/puppeteer/puppeteer/compare/v15.4.1...v15.4.2) (2022-07-21) + + +### Bug Fixes + +* taking a screenshot with null viewport should be possible ([#8680](https://github.com/puppeteer/puppeteer/issues/8680)) ([2abb9f0](https://github.com/puppeteer/puppeteer/commit/2abb9f0c144779d555ecbf337a759440d0282cba)), closes [#8673](https://github.com/puppeteer/puppeteer/issues/8673) + +## [15.4.1](https://github.com/puppeteer/puppeteer/compare/v15.4.0...v15.4.1) (2022-07-21) + + +### Bug Fixes + +* import URL ([#8670](https://github.com/puppeteer/puppeteer/issues/8670)) ([34ab5ca](https://github.com/puppeteer/puppeteer/commit/34ab5ca50353ffb6a6345a8984b724a6f42fb726)) + +## [15.4.0](https://github.com/puppeteer/puppeteer/compare/v15.3.2...v15.4.0) (2022-07-13) + + +### Features + +* expose the page getter on Frame ([#8657](https://github.com/puppeteer/puppeteer/issues/8657)) ([af08c5c](https://github.com/puppeteer/puppeteer/commit/af08c5c90380c853e8257a51298bfed4b0635779)) + + +### Bug Fixes + +* ignore *.tsbuildinfo ([#8662](https://github.com/puppeteer/puppeteer/issues/8662)) ([edcdf21](https://github.com/puppeteer/puppeteer/commit/edcdf217cefbf31aee5a2f571abac429dd81f3a0)) + +## [15.3.2](https://github.com/puppeteer/puppeteer/compare/v15.3.1...v15.3.2) (2022-07-08) + + +### Bug Fixes + +* cache dynamic imports ([#8652](https://github.com/puppeteer/puppeteer/issues/8652)) ([1de0383](https://github.com/puppeteer/puppeteer/commit/1de0383abf6be31cf06faede3e59b087a2958227)) +* expose a RemoteObject getter ([#8642](https://github.com/puppeteer/puppeteer/issues/8642)) ([d0c4291](https://github.com/puppeteer/puppeteer/commit/d0c42919956bd36ad7993a0fc1de86e886e39f62)), closes [#8639](https://github.com/puppeteer/puppeteer/issues/8639) +* **page:** fix page.#scrollIntoViewIfNeeded method ([#8631](https://github.com/puppeteer/puppeteer/issues/8631)) ([b47f066](https://github.com/puppeteer/puppeteer/commit/b47f066c2c068825e3b65cfe17b6923c77ad30b9)) + +## [15.3.1](https://github.com/puppeteer/puppeteer/compare/v15.3.0...v15.3.1) (2022-07-06) + + +### Bug Fixes + +* extends `ElementHandle` to `Node`s ([#8552](https://github.com/puppeteer/puppeteer/issues/8552)) ([5ff205d](https://github.com/puppeteer/puppeteer/commit/5ff205dc8b659eb8864b4b1862105d21dd334c8f)) + +## [15.3.0](https://github.com/puppeteer/puppeteer/compare/v15.2.0...v15.3.0) (2022-07-01) + + +### Features + +* add documentation ([#8593](https://github.com/puppeteer/puppeteer/issues/8593)) ([066f440](https://github.com/puppeteer/puppeteer/commit/066f440ba7bdc9aca9423d7205adf36f2858bd78)) + + +### Bug Fixes + +* remove unused imports ([#8613](https://github.com/puppeteer/puppeteer/issues/8613)) ([0cf4832](https://github.com/puppeteer/puppeteer/commit/0cf4832878731ffcfc84570315f326eb851d7629)) + +## [15.2.0](https://github.com/puppeteer/puppeteer/compare/v15.1.1...v15.2.0) (2022-06-29) + + +### Features + +* add fromSurface option to page.screenshot ([#8496](https://github.com/puppeteer/puppeteer/issues/8496)) ([79e1198](https://github.com/puppeteer/puppeteer/commit/79e11985ba44b72b1ad6b8cd861fe316f1945e64)) +* export public types only ([#8584](https://github.com/puppeteer/puppeteer/issues/8584)) ([7001322](https://github.com/puppeteer/puppeteer/commit/7001322cd1cf9f77ee2c370d50a6707e7aaad72d)) + + +### Bug Fixes + +* clean up tmp profile dirs when browser is closed ([#8580](https://github.com/puppeteer/puppeteer/issues/8580)) ([9787a1d](https://github.com/puppeteer/puppeteer/commit/9787a1d8df7768017b36d42327faab402695c4bb)) + +## [15.1.1](https://github.com/puppeteer/puppeteer/compare/v15.1.0...v15.1.1) (2022-06-25) + + +### Bug Fixes + +* export `ElementHandle` ([e0198a7](https://github.com/puppeteer/puppeteer/commit/e0198a79e06c8bb72dde554db0246a3db5fec4c2)) + +## [15.1.0](https://github.com/puppeteer/puppeteer/compare/v15.0.2...v15.1.0) (2022-06-24) + + +### Features + +* **chromium:** roll to Chromium 104.0.5109.0 (r1011831) ([#8569](https://github.com/puppeteer/puppeteer/issues/8569)) ([fb7d31e](https://github.com/puppeteer/puppeteer/commit/fb7d31e3698428560e1f654d33782d241192f48f)) + +## [15.0.2](https://github.com/puppeteer/puppeteer/compare/v15.0.1...v15.0.2) (2022-06-24) + + +### Bug Fixes + +* CSS coverage should work with empty stylesheets ([#8570](https://github.com/puppeteer/puppeteer/issues/8570)) ([383e855](https://github.com/puppeteer/puppeteer/commit/383e8558477fae7708734ab2160ef50f385e2983)), closes [#8535](https://github.com/puppeteer/puppeteer/issues/8535) + +## [15.0.1](https://github.com/puppeteer/puppeteer/compare/v15.0.0...v15.0.1) (2022-06-24) + + +### Bug Fixes + +* infer unioned handles ([#8562](https://github.com/puppeteer/puppeteer/issues/8562)) ([8100cbb](https://github.com/puppeteer/puppeteer/commit/8100cbb29569541541f61001983efb9a80d89890)) + +## [15.0.0](https://github.com/puppeteer/puppeteer/compare/v14.4.1...v15.0.0) (2022-06-23) + + +### ⚠ BREAKING CHANGES + +* type inference for evaluation types (#8547) + +### Features + +* add experimental `client` to `HTTPRequest` ([#8556](https://github.com/puppeteer/puppeteer/issues/8556)) ([ec79f3a](https://github.com/puppeteer/puppeteer/commit/ec79f3a58a44c9ea60a82f9cd2df4c8f19e82ab8)) +* type inference for evaluation types ([#8547](https://github.com/puppeteer/puppeteer/issues/8547)) ([26c3acb](https://github.com/puppeteer/puppeteer/commit/26c3acbb0795eb66f29479f442e156832f794f01)) + +## [14.4.1](https://github.com/puppeteer/puppeteer/compare/v14.4.0...v14.4.1) (2022-06-17) + + +### Bug Fixes + +* avoid `instanceof Object` check in `isErrorLike` ([#8527](https://github.com/puppeteer/puppeteer/issues/8527)) ([6cd5cd0](https://github.com/puppeteer/puppeteer/commit/6cd5cd043997699edca6e3458f90adc1118cf4a5)) +* export `devices`, `errors`, and more ([cba58a1](https://github.com/puppeteer/puppeteer/commit/cba58a12c4e2043f6a5acf7d4754e4a7b7f6e198)) + +## [14.4.0](https://github.com/puppeteer/puppeteer/compare/v14.3.0...v14.4.0) (2022-06-13) + + +### Features + +* export puppeteer methods ([#8493](https://github.com/puppeteer/puppeteer/issues/8493)) ([465a7c4](https://github.com/puppeteer/puppeteer/commit/465a7c405f01fcef99380ffa69d86042a1f5618f)) +* support node-like environments ([#8490](https://github.com/puppeteer/puppeteer/issues/8490)) ([f64ec20](https://github.com/puppeteer/puppeteer/commit/f64ec2051b9b2d12225abba6ffe9551da9751bf7)) + + +### Bug Fixes + +* parse empty options in \<select\> ([#8489](https://github.com/puppeteer/puppeteer/issues/8489)) ([b30f3f4](https://github.com/puppeteer/puppeteer/commit/b30f3f44cdabd9545c4661cd755b9d49e5c144cd)) +* use error-like ([#8504](https://github.com/puppeteer/puppeteer/issues/8504)) ([4d35990](https://github.com/puppeteer/puppeteer/commit/4d359906a44e4ddd5ec54a523cfd9076048d3433)) +* use OS-independent abs. path check ([#8505](https://github.com/puppeteer/puppeteer/issues/8505)) ([bfd4e68](https://github.com/puppeteer/puppeteer/commit/bfd4e68f25bec6e00fd5cbf261813f8297d362ee)) + +## [14.3.0](https://github.com/puppeteer/puppeteer/compare/v14.2.1...v14.3.0) (2022-06-07) + + +### Features + +* use absolute URL for EVALUATION_SCRIPT_URL ([#8481](https://github.com/puppeteer/puppeteer/issues/8481)) ([e142560](https://github.com/puppeteer/puppeteer/commit/e14256010d2d84d613cd3c6e7999b0705115d4bf)), closes [#8424](https://github.com/puppeteer/puppeteer/issues/8424) + + +### Bug Fixes + +* don't throw on bad access ([#8472](https://github.com/puppeteer/puppeteer/issues/8472)) ([e837866](https://github.com/puppeteer/puppeteer/commit/e8378666c671e5703aec4f52912de2aac94e1828)) +* Kill browser process when killing process group fails ([#8477](https://github.com/puppeteer/puppeteer/issues/8477)) ([7dc8e37](https://github.com/puppeteer/puppeteer/commit/7dc8e37a23d025bb2c31efb9c060c7f6e00179b4)) +* only lookup `localhost` for DNS lookups ([1b025b4](https://github.com/puppeteer/puppeteer/commit/1b025b4c8466fe64da0fa2050eaa02b7764770b1)) +* robustly check for launch executable ([#8468](https://github.com/puppeteer/puppeteer/issues/8468)) ([b54dc55](https://github.com/puppeteer/puppeteer/commit/b54dc55f7622ee2b75afd3bd9fe118dd2f144f40)) + +## [14.2.1](https://github.com/puppeteer/puppeteer/compare/v14.2.0...v14.2.1) (2022-06-02) + + +### Bug Fixes + +* use isPageTargetCallback in Browser::pages() ([#8460](https://github.com/puppeteer/puppeteer/issues/8460)) ([5c9050a](https://github.com/puppeteer/puppeteer/commit/5c9050aea0fe8d57114130fe38bd33ed2b4955d6)) + +## [14.2.0](https://github.com/puppeteer/puppeteer/compare/v14.1.2...v14.2.0) (2022-06-01) + + +### 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)) +* support node 18 ([#8447](https://github.com/puppeteer/puppeteer/issues/8447)) ([f2d8276](https://github.com/puppeteer/puppeteer/commit/f2d8276d6e745a7547b8ce54c3f50934bb70de0b)) +* use strict typescript ([#8401](https://github.com/puppeteer/puppeteer/issues/8401)) ([b4e751f](https://github.com/puppeteer/puppeteer/commit/b4e751f29cb6fd4c3cc41fe702de83721f0eb6dc)) + + +### Bug Fixes + +* multiple same request event listener ([#8404](https://github.com/puppeteer/puppeteer/issues/8404)) ([9211015](https://github.com/puppeteer/puppeteer/commit/92110151d9a33f26abc07bc805f4f2f3943697a0)) +* NodeNext incompatibility in package.json ([#8445](https://github.com/puppeteer/puppeteer/issues/8445)) ([c4898a7](https://github.com/puppeteer/puppeteer/commit/c4898a7a2e69681baac55366848da6688f0d8790)) +* 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/remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json new file mode 100644 index 0000000000..b0bcacbb34 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.docs.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer-core.d.ts", + + "extends": "./api-extractor.json", + + "dtsRollup": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/api-extractor.json b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.json new file mode 100644 index 0000000000..7b9032de29 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/api-extractor.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer-core.d.ts", + "bundledPackages": [], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": false + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "alphaTrimmedFilePath": "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/remote/test/puppeteer/packages/puppeteer-core/package.json b/remote/test/puppeteer/packages/puppeteer-core/package.json new file mode 100644 index 0000000000..27306ad882 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/package.json @@ -0,0 +1,159 @@ +{ + "name": "puppeteer-core", + "version": "20.1.0", + "description": "A high-level API to control headless Chrome over the DevTools Protocol", + "keywords": [ + "puppeteer", + "chrome", + "headless", + "automation" + ], + "type": "commonjs", + "main": "./lib/cjs/puppeteer/puppeteer-core.js", + "types": "./lib/types.d.ts", + "exports": { + ".": { + "types": "./lib/types.d.ts", + "import": "./lib/esm/puppeteer/puppeteer-core.js", + "require": "./lib/cjs/puppeteer/puppeteer-core.js" + }, + "./internal/*": { + "import": "./lib/esm/puppeteer/*", + "require": "./lib/cjs/puppeteer/*" + }, + "./*": { + "import": "./*", + "require": "./*" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core" + }, + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "build:docs": "wireit", + "build:tsc": "wireit", + "build:types": "wireit", + "build": "wireit", + "check": "tsx tools/ensure-correct-devtools-protocol-package", + "clean": "tsc -b --clean && rm -rf lib src/generated", + "generate:package-json": "wireit", + "generate:sources": "wireit", + "prepack": "wireit" + }, + "wireit": { + "prepack": { + "command": "tsx ../../tools/cp.ts ../../README.md README.md", + "files": [ + "../../README.md" + ], + "output": [ + "README.md" + ] + }, + "build": { + "dependencies": [ + "build:tsc", + "build:types" + ] + }, + "generate:sources": { + "command": "tsx tools/generate_sources.ts", + "clean": "if-file-deleted", + "files": [ + "../../versions.js", + "src/{injected,templates}/**", + "tools/generate_sources.ts" + ], + "output": [ + "src/generated/*.ts" + ] + }, + "generate:package-json": { + "command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json", + "files": [ + "../../tools/generate_module_package_json.ts" + ], + "output": [ + "lib/esm/package.json" + ] + }, + "build:docs": { + "command": "api-extractor run --local --config \"./api-extractor.docs.json\"", + "files": [ + "api-extractor.docs.json", + "lib/esm/puppeteer/puppeteer-core.d.ts", + "tsconfig.json" + ], + "dependencies": [ + "build:tsc" + ] + }, + "build:tsc": { + "command": "tsc -b && rollup --config rollup.third_party.config.mjs", + "clean": "if-file-deleted", + "dependencies": [ + "generate:package-json", + "generate:sources", + "../browsers:build" + ], + "files": [ + "{compat,src,third_party}/**", + "rollup.third_party.config.mjs" + ], + "output": [ + "lib/{cjs,esm}/**", + "!lib/esm/package.json" + ] + }, + "build:types": { + "command": "api-extractor run --local && eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts", + "files": [ + "../../.eslintrc.types.cjs", + "api-extractor.json", + "lib/esm/puppeteer/types.d.ts", + "tsconfig.json" + ], + "output": [ + "lib/types.d.ts" + ], + "dependencies": [ + "build:tsc" + ] + } + }, + "files": [ + "lib", + "!*.tsbuildinfo" + ], + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "chromium-bidi": "0.4.7", + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1120988", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.13.0", + "@puppeteer/browsers": "1.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "devDependencies": { + "mitt": "3.0.0", + "parsel-js": "1.1.0" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs b/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs new file mode 100644 index 0000000000..8b40906cbf --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/rollup.third_party.config.mjs @@ -0,0 +1,35 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import commonjs from '@rollup/plugin-commonjs'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import glob from 'glob'; + +export default ['cjs', 'esm'].flatMap(outputType => { + const configs = []; + // Note we don't use path.join here. We cannot since `glob` does not support + // the backslash path separator. + for (const file of glob.sync(`lib/${outputType}/third_party/**/*.js`)) { + configs.push({ + input: file, + output: { + file, + format: outputType, + }, + plugins: [commonjs(), nodeResolve()], + }); + } + return configs; +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts new file mode 100644 index 0000000000..5a08f6ec17 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Browser.ts @@ -0,0 +1,473 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import {ChildProcess} from 'child_process'; + +import {Protocol} from 'devtools-protocol'; + +import {EventEmitter} from '../common/EventEmitter.js'; +import type {Target} from '../common/Target.js'; // TODO: move to ./api + +import type {BrowserContext} from './BrowserContext.js'; +import type {Page} from './Page.js'; // TODO: move to ./api + +/** + * BrowserContext options. + * + * @public + */ +export interface BrowserContextOptions { + /** + * Proxy server with optional port to use for all requests. + * Username and password can be set in `Page.authenticate`. + */ + proxyServer?: string; + /** + * Bypass the proxy for the given list of hosts. + */ + proxyBypassList?: string[]; +} + +/** + * @internal + */ +export type BrowserCloseCallback = () => Promise<void> | void; + +/** + * @public + */ +export type TargetFilterCallback = ( + target: Protocol.Target.TargetInfo +) => boolean; + +/** + * @internal + */ +export type IsPageTargetCallback = ( + target: Protocol.Target.TargetInfo +) => boolean; + +/** + * @internal + */ +export const WEB_PERMISSION_TO_PROTOCOL_PERMISSION = new Map< + Permission, + Protocol.Browser.PermissionType +>([ + ['geolocation', 'geolocation'], + ['midi', 'midi'], + ['notifications', 'notifications'], + // TODO: push isn't a valid type? + // ['push', 'push'], + ['camera', 'videoCapture'], + ['microphone', 'audioCapture'], + ['background-sync', 'backgroundSync'], + ['ambient-light-sensor', 'sensors'], + ['accelerometer', 'sensors'], + ['gyroscope', 'sensors'], + ['magnetometer', 'sensors'], + ['accessibility-events', 'accessibilityEvents'], + ['clipboard-read', 'clipboardReadWrite'], + ['clipboard-write', 'clipboardReadWrite'], + ['payment-handler', 'paymentHandler'], + ['persistent-storage', 'durableStorage'], + ['idle-detection', 'idleDetection'], + // chrome-specific permissions we have. + ['midi-sysex', 'midiSysex'], +]); + +/** + * @public + */ +export type Permission = + | 'geolocation' + | 'midi' + | 'notifications' + | 'camera' + | 'microphone' + | 'background-sync' + | 'ambient-light-sensor' + | 'accelerometer' + | 'gyroscope' + | 'magnetometer' + | 'accessibility-events' + | 'clipboard-read' + | 'clipboard-write' + | 'payment-handler' + | 'persistent-storage' + | 'idle-detection' + | 'midi-sysex'; + +/** + * @public + */ +export interface WaitForTargetOptions { + /** + * Maximum wait time in milliseconds. Pass `0` to disable the timeout. + * @defaultValue `30_000` + */ + timeout?: number; +} + +/** + * All the events a {@link Browser | browser instance} may emit. + * + * @public + */ +export const enum BrowserEmittedEvents { + /** + * Emitted when Puppeteer gets disconnected from the browser instance. This + * might happen because of one of the following: + * + * - browser is closed or crashed + * + * - The {@link Browser.disconnect | browser.disconnect } method was called. + */ + Disconnected = 'disconnected', + + /** + * Emitted when the url of a target changes. Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target changes in incognito browser contexts. + */ + TargetChanged = 'targetchanged', + + /** + * Emitted when a target is created, for example when a new page is opened by + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open} + * or by {@link Browser.newPage | browser.newPage} + * + * Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target creations in incognito browser contexts. + */ + TargetCreated = 'targetcreated', + /** + * Emitted when a target is destroyed, for example when a page is closed. + * Contains a {@link Target} instance. + * + * @remarks + * + * Note that this includes target destructions in incognito browser contexts. + */ + TargetDestroyed = 'targetdestroyed', +} + +/** + * A Browser is created when Puppeteer connects to a browser instance, either through + * {@link PuppeteerNode.launch} or {@link Puppeteer.connect}. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and will + * emit various events which are documented in the {@link BrowserEmittedEvents} enum. + * + * @example + * An example of using a {@link Browser} to create a {@link Page}: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + * + * @example + * An example of disconnecting from and reconnecting to a {@link Browser}: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * // Store the endpoint to be able to reconnect to the browser. + * const browserWSEndpoint = browser.wsEndpoint(); + * // Disconnect puppeteer from the browser. + * browser.disconnect(); + * + * // Use the endpoint to reestablish a connection + * const browser2 = await puppeteer.connect({browserWSEndpoint}); + * // Close the browser. + * await browser2.close(); + * })(); + * ``` + * + * @public + */ +export class Browser extends EventEmitter { + /** + * @internal + */ + constructor() { + super(); + } + + /** + * @internal + */ + _attach(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _detach(): void { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + get _targets(): Map<string, Target> { + throw new Error('Not implemented'); + } + + /** + * The spawned browser process. Returns `null` if the browser instance was created with + * {@link Puppeteer.connect}. + */ + process(): ChildProcess | null { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _getIsPageTargetCallback(): IsPageTargetCallback | undefined { + throw new Error('Not implemented'); + } + + /** + * Creates a new incognito browser context. This won't share cookies/cache with other + * browser contexts. + * + * @example + * + * ```ts + * (async () => { + * const browser = await puppeteer.launch(); + * // Create a new incognito browser context. + * const context = await browser.createIncognitoBrowserContext(); + * // Create a new page in a pristine context. + * const page = await context.newPage(); + * // Do stuff + * await page.goto('https://example.com'); + * })(); + * ``` + */ + createIncognitoBrowserContext( + options?: BrowserContextOptions + ): Promise<BrowserContext>; + createIncognitoBrowserContext(): Promise<BrowserContext> { + throw new Error('Not implemented'); + } + + /** + * Returns an array of all open browser contexts. In a newly created browser, this will + * return a single instance of {@link BrowserContext}. + */ + browserContexts(): BrowserContext[] { + throw new Error('Not implemented'); + } + + /** + * Returns the default browser context. The default browser context cannot be closed. + */ + defaultBrowserContext(): BrowserContext { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _disposeContext(contextId?: string): Promise<void>; + _disposeContext(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * The browser websocket endpoint which can be used as an argument to + * {@link Puppeteer.connect}. + * + * @returns The Browser websocket url. + * + * @remarks + * + * The format is `ws://${host}:${port}/devtools/browser/<id>`. + * + * You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. + * Learn more about the + * {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and + * the {@link + * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target + * | browser endpoint}. + */ + wsEndpoint(): string { + throw new Error('Not implemented'); + } + + /** + * Promise which resolves to a new {@link Page} object. The Page is created in + * a default browser context. + */ + newPage(): Promise<Page> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _createPageInContext(contextId?: string): Promise<Page>; + _createPageInContext(): Promise<Page> { + throw new Error('Not implemented'); + } + + /** + * All active targets inside the Browser. In case of multiple browser contexts, returns + * an array with all the targets in all browser contexts. + */ + targets(): Target[] { + throw new Error('Not implemented'); + } + + /** + * The target associated with the browser. + */ + target(): Target { + throw new Error('Not implemented'); + } + + /** + * Searches for a target in all browser contexts. + * + * @param predicate - A function to be run for every target. + * @returns The first target found that matches the `predicate` function. + * + * @example + * + * An example of finding a target for a page opened via `window.open`: + * + * ```ts + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browser.waitForTarget( + * target => target.url() === 'https://www.example.com/' + * ); + * ``` + */ + waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options?: WaitForTargetOptions + ): Promise<Target>; + waitForTarget(): Promise<Target> { + throw new Error('Not implemented'); + } + + /** + * An array of all open pages inside the Browser. + * + * @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 {@link Target.page}. + */ + pages(): Promise<Page[]> { + throw new Error('Not implemented'); + } + + /** + * A string representing the browser name and version. + * + * @remarks + * + * For headless browser, 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 browsers. + */ + version(): Promise<string> { + throw new Error('Not implemented'); + } + + /** + * The browser's original user agent. Pages can override the browser user agent with + * {@link Page.setUserAgent}. + */ + userAgent(): Promise<string> { + throw new Error('Not implemented'); + } + + /** + * Closes the browser and all of its pages (if any were opened). The + * {@link Browser} object itself is considered to be disposed and cannot be + * used anymore. + */ + close(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Disconnects Puppeteer from the browser, but leaves the browser process running. + * After calling `disconnect`, the {@link Browser} object is considered disposed and + * cannot be used anymore. + */ + disconnect(): void { + throw new Error('Not implemented'); + } + + /** + * Indicates that the browser is connected. + */ + isConnected(): boolean { + throw new Error('Not implemented'); + } +} +/** + * @public + */ +export const enum BrowserContextEmittedEvents { + /** + * Emitted when the url of a target inside the browser context changes. + * Contains a {@link Target} instance. + */ + TargetChanged = 'targetchanged', + + /** + * Emitted when a target is created within the browser context, for example + * when a new page is opened by + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/open | window.open} + * or by {@link BrowserContext.newPage | browserContext.newPage} + * + * Contains a {@link Target} instance. + */ + TargetCreated = 'targetcreated', + /** + * Emitted when a target is destroyed within the browser context, for example + * when a page is closed. Contains a {@link Target} instance. + */ + TargetDestroyed = 'targetdestroyed', +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts new file mode 100644 index 0000000000..77fb9b1987 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/BrowserContext.ts @@ -0,0 +1,186 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {EventEmitter} from '../common/EventEmitter.js'; +import {Target} from '../common/Target.js'; + +import type {Permission, Browser} from './Browser.js'; +import {Page} from './Page.js'; + +/** + * BrowserContexts provide a way to operate multiple independent browser + * sessions. When a browser is launched, it has a single BrowserContext used by + * default. The method {@link Browser.newPage | Browser.newPage} creates a page + * in the default browser context. + * + * @remarks + * + * The Browser class extends from Puppeteer's {@link EventEmitter} class and + * will emit various events which are documented in the + * {@link BrowserContextEmittedEvents} enum. + * + * If a page opens another page, e.g. with a `window.open` call, the popup will + * belong to the parent page's browser context. + * + * Puppeteer allows creation of "incognito" browser contexts with + * {@link Browser.createIncognitoBrowserContext | Browser.createIncognitoBrowserContext} + * method. "Incognito" browser contexts don't write any browsing data to disk. + * + * @example + * + * ```ts + * // Create a new incognito browser context + * const context = await browser.createIncognitoBrowserContext(); + * // Create a new page inside context. + * const page = await context.newPage(); + * // ... do stuff with page ... + * await page.goto('https://example.com'); + * // Dispose context once it's no longer needed. + * await context.close(); + * ``` + * + * @public + */ + +export class BrowserContext extends EventEmitter { + /** + * @internal + */ + constructor() { + super(); + } + + /** + * An array of all active targets inside the browser context. + */ + targets(): Target[] { + throw new Error('Not implemented'); + } + + /** + * This searches for a target in this specific browser context. + * + * @example + * An example of finding a target for a page opened via `window.open`: + * + * ```ts + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browserContext.waitForTarget( + * target => target.url() === 'https://www.example.com/' + * ); + * ``` + * + * @param predicate - A function to be run for every target + * @param options - An object of options. Accepts a timeout, + * which is the maximum wait time in milliseconds. + * Pass `0` to disable the timeout. Defaults to 30 seconds. + * @returns Promise which resolves to the first target found + * that matches the `predicate` function. + */ + waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options?: {timeout?: number} + ): Promise<Target>; + waitForTarget(): Promise<Target> { + throw new Error('Not implemented'); + } + + /** + * An array of all pages inside the browser context. + * + * @returns Promise which resolves to an array of all open pages. + * Non visible pages, such as `"background_page"`, will not be listed here. + * You can find them using {@link Target.page | the target page}. + */ + pages(): Promise<Page[]> { + throw new Error('Not implemented'); + } + + /** + * Returns whether BrowserContext is incognito. + * The default browser context is the only non-incognito browser context. + * + * @remarks + * The default browser context cannot be closed. + */ + isIncognito(): boolean { + throw new Error('Not implemented'); + } + + /** + * @example + * + * ```ts + * const context = browser.defaultBrowserContext(); + * await context.overridePermissions('https://html5demos.com', [ + * 'geolocation', + * ]); + * ``` + * + * @param origin - The origin to grant permissions to, e.g. "https://example.com". + * @param permissions - An array of permissions to grant. + * All permissions that are not listed here will be automatically denied. + */ + overridePermissions(origin: string, permissions: Permission[]): Promise<void>; + overridePermissions(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Clears all permission overrides for the browser context. + * + * @example + * + * ```ts + * const context = browser.defaultBrowserContext(); + * context.overridePermissions('https://example.com', ['clipboard-read']); + * // do stuff .. + * context.clearPermissionOverrides(); + * ``` + */ + clearPermissionOverrides(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Creates a new page in the browser context. + */ + newPage(): Promise<Page> { + throw new Error('Not implemented'); + } + + /** + * The browser this browser context belongs to. + */ + browser(): Browser { + throw new Error('Not implemented'); + } + + /** + * Closes the browser context. All the targets that belong to the browser context + * will be closed. + * + * @remarks + * Only incognito browser contexts can be closed. + */ + close(): Promise<void> { + throw new Error('Not implemented'); + } + + get id(): string | undefined { + return undefined; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts new file mode 100644 index 0000000000..09c409736e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/ElementHandle.ts @@ -0,0 +1,917 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; + +import {CDPSession} from '../common/Connection.js'; +import {ExecutionContext} from '../common/ExecutionContext.js'; +import {Frame} from '../common/Frame.js'; +import {MouseClickOptions} from '../common/Input.js'; +import {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; +import { + ElementFor, + EvaluateFuncWith, + HandleFor, + HandleOr, + NodeFor, +} from '../common/types.js'; +import {KeyInput} from '../common/USKeyboardLayout.js'; + +import {JSHandle} from './JSHandle.js'; +import {ScreenshotOptions} from './Page.js'; + +/** + * @public + */ +export interface BoxModel { + content: Point[]; + padding: Point[]; + border: Point[]; + margin: Point[]; + width: number; + height: number; +} + +/** + * @public + */ +export interface BoundingBox extends Point { + /** + * the width of the element in pixels. + */ + width: number; + /** + * the height of the element in pixels. + */ + height: number; +} + +/** + * @public + */ +export interface Offset { + /** + * x-offset for the clickable point relative to the top-left corner of the border box. + */ + x: number; + /** + * y-offset for the clickable point relative to the top-left corner of the border box. + */ + y: number; +} + +/** + * @public + */ +export interface ClickOptions extends MouseClickOptions { + /** + * Offset for the clickable point relative to the top-left corner of the border box. + */ + offset?: Offset; +} + +/** + * @public + */ +export interface PressOptions { + /** + * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. + */ + delay?: number; + /** + * If specified, generates an input event with this text. + */ + text?: string; +} + +/** + * @public + */ +export interface Point { + x: number; + y: number; +} + +/** + * ElementHandle represents an in-page DOM element. + * + * @remarks + * ElementHandles can be created with the {@link Page.$} method. + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * const hrefElement = await page.$('a'); + * await hrefElement.click(); + * // ... + * })(); + * ``` + * + * ElementHandle prevents the DOM element from being garbage-collected unless the + * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed + * when their origin frame gets navigated. + * + * ElementHandle instances can be used as arguments in {@link Page.$eval} and + * {@link Page.evaluate} methods. + * + * If you're using TypeScript, ElementHandle takes a generic argument that + * denotes the type of element the handle is holding within. For example, if you + * have a handle to a `<select>` element, you can type it as + * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks. + * + * @public + */ + +export class ElementHandle< + ElementType extends Node = Element +> extends JSHandle<ElementType> { + /** + * @internal + */ + protected handle; + + /** + * @internal + */ + constructor(handle: JSHandle<ElementType>) { + super(); + this.handle = handle; + } + + /** + * @internal + */ + override get id(): string | undefined { + return this.handle.id; + } + + /** + * @internal + */ + override get disposed(): boolean { + return this.handle.disposed; + } + + /** + * @internal + */ + override async getProperty<K extends keyof ElementType>( + propertyName: HandleOr<K> + ): Promise<HandleFor<ElementType[K]>>; + /** + * @internal + */ + override async getProperty(propertyName: string): Promise<JSHandle<unknown>>; + override async getProperty<K extends keyof ElementType>( + propertyName: HandleOr<K> + ): Promise<HandleFor<ElementType[K]>> { + return this.handle.getProperty(propertyName); + } + + /** + * @internal + */ + override async getProperties(): Promise<Map<string, JSHandle>> { + return this.handle.getProperties(); + } + + /** + * @internal + */ + override async evaluate< + Params extends unknown[], + Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith< + ElementType, + Params + > + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.handle.evaluate(pageFunction, ...args); + } + + /** + * @internal + */ + override evaluateHandle< + Params extends unknown[], + Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith< + ElementType, + Params + > + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.handle.evaluateHandle(pageFunction, ...args); + } + + /** + * @internal + */ + override async jsonValue(): Promise<ElementType> { + return this.handle.jsonValue(); + } + + /** + * @internal + */ + override toString(): string { + return this.handle.toString(); + } + + /** + * @internal + */ + override async dispose(): Promise<void> { + return await this.handle.dispose(); + } + + override asElement(): ElementHandle<ElementType> { + return this; + } + + /** + * @internal + */ + override executionContext(): ExecutionContext { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + override get client(): CDPSession { + throw new Error('Not implemented'); + } + + get frame(): Frame { + throw new Error('Not implemented'); + } + + /** + * Queries the current element for an element matching the given selector. + * + * @param selector - The selector to query for. + * @returns A {@link ElementHandle | element handle} to the first element + * matching the given selector. Otherwise, `null`. + */ + async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null>; + async $<Selector extends string>(): Promise<ElementHandle< + NodeFor<Selector> + > | null> { + throw new Error('Not implemented'); + } + + /** + * Queries the current element for all elements matching the given selector. + * + * @param selector - The selector to query for. + * @returns An array of {@link ElementHandle | element handles} that point to + * elements matching the given selector. + */ + async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>>; + async $$<Selector extends string>(): Promise< + Array<ElementHandle<NodeFor<Selector>>> + > { + throw new Error('Not implemented'); + } + + /** + * Runs the given function on the first element matching the given selector in + * the current element. + * + * If the given function returns a promise, then this method will wait till + * the promise resolves. + * + * @example + * + * ```ts + * const tweetHandle = await page.$('.tweet'); + * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe( + * '100' + * ); + * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe( + * '10' + * ); + * ``` + * + * @param selector - The selector to query for. + * @param pageFunction - The function to be evaluated in this element's page's + * context. The first element matching the selector will be passed in as the + * first argument. + * @param args - Additional arguments to pass to `pageFunction`. + * @returns A promise to the result of the function. + */ + async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async $eval(): Promise<unknown> { + throw new Error('Not implemented'); + } + + /** + * Runs the given function on an array of elements matching the given selector + * in the current element. + * + * If the given function returns a promise, then this method will wait till + * the promise resolves. + * + * @example + * HTML: + * + * ```html + * <div class="feed"> + * <div class="tweet">Hello!</div> + * <div class="tweet">Hi!</div> + * </div> + * ``` + * + * JavaScript: + * + * ```js + * const feedHandle = await page.$('.feed'); + * expect( + * await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText)) + * ).toEqual(['Hello!', 'Hi!']); + * ``` + * + * @param selector - The selector to query for. + * @param pageFunction - The function to be evaluated in the element's page's + * context. An array of elements matching the given selector will be passed to + * the function as its first argument. + * @param args - Additional arguments to pass to `pageFunction`. + * @returns A promise to the result of the function. + */ + async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async $$eval(): Promise<unknown> { + throw new Error('Not implemented'); + } + + /** + * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix. + * + * Example: `await elementHandle.$$('xpath/' + xpathExpression)` + * + * The method evaluates the XPath expression relative to the elementHandle. + * If `xpath` starts with `//` instead of `.//`, the dot will be appended + * automatically. + * + * If there are no such elements, the method will resolve to an empty array. + * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} + */ + async $x(expression: string): Promise<Array<ElementHandle<Node>>>; + async $x(): Promise<Array<ElementHandle<Node>>> { + throw new Error('Not implemented'); + } + + /** + * Wait for an element matching the given selector to appear in the current + * element. + * + * Unlike {@link Frame.waitForSelector}, this method does not work across + * navigations or if the element is detached from DOM. + * + * @example + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .mainFrame() + * .waitForSelector('img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param selector - The selector to query and wait for. + * @param options - Options for customizing waiting behavior. + * @returns An element matching the given selector. + * @throws Throws if an element matching the given selector doesn't appear. + */ + async waitForSelector<Selector extends string>( + selector: Selector, + options?: WaitForSelectorOptions + ): Promise<ElementHandle<NodeFor<Selector>> | null>; + async waitForSelector<Selector extends string>(): Promise<ElementHandle< + NodeFor<Selector> + > | null> { + throw new Error('Not implemented'); + } + + /** + * Checks if an element is visible using the same mechanism as + * {@link ElementHandle.waitForSelector}. + */ + async isVisible(): Promise<boolean> { + throw new Error('Not implemented.'); + } + + /** + * Checks if an element is hidden using the same mechanism as + * {@link ElementHandle.waitForSelector}. + */ + async isHidden(): Promise<boolean> { + throw new Error('Not implemented.'); + } + + /** + * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath` + * prefix. + * + * Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)` + * + * The method evaluates the XPath expression relative to the elementHandle. + * + * Wait for the `xpath` within the element. If at the moment of calling the + * method the `xpath` already exists, the method will return immediately. If + * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * If `xpath` starts with `//` instead of `.//`, the dot will be appended + * automatically. + * + * @example + * This method works across navigation. + * + * ```ts + * import puppeteer from 'puppeteer'; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForXPath('//img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param xpath - A + * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an + * element to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves when element specified by xpath string is + * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is + * not found in DOM, otherwise resolves to `ElementHandle`. + * @remarks + * The optional Argument `options` have properties: + * + * - `visible`: A boolean to wait for element to be present in DOM and to be + * visible, i.e. to not have `display: none` or `visibility: hidden` CSS + * properties. Defaults to `false`. + * + * - `hidden`: A boolean wait for element to not be found in the DOM or to be + * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. + * Defaults to `false`. + * + * - `timeout`: A number which is maximum time to wait for in milliseconds. + * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + * default value can be changed by using the {@link Page.setDefaultTimeout} + * method. + */ + async waitForXPath( + xpath: string, + options?: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } + ): Promise<ElementHandle<Node> | null>; + async waitForXPath(): Promise<ElementHandle<Node> | null> { + throw new Error('Not implemented'); + } + + /** + * Converts the current handle to the given element type. + * + * @example + * + * ```ts + * const element: ElementHandle<Element> = await page.$( + * '.class-name-of-anchor' + * ); + * // DO NOT DISPOSE `element`, this will be always be the same handle. + * const anchor: ElementHandle<HTMLAnchorElement> = await element.toElement( + * 'a' + * ); + * ``` + * + * @param tagName - The tag name of the desired element type. + * @throws An error if the handle does not match. **The handle will not be + * automatically disposed.** + */ + async toElement< + K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap + >(tagName: K): Promise<HandleFor<ElementFor<K>>>; + async toElement< + K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap + >(): Promise<HandleFor<ElementFor<K>>> { + throw new Error('Not implemented'); + } + + /** + * Resolves to the content frame for element handles referencing + * iframe nodes, or null otherwise + */ + async contentFrame(): Promise<Frame | null> { + throw new Error('Not implemented'); + } + + /** + * Returns the middle point within an element unless a specific offset is provided. + */ + async clickablePoint(offset?: Offset): Promise<Point>; + async clickablePoint(): Promise<Point> { + throw new Error('Not implemented'); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page} to hover over the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async hover(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page | Page.mouse} to click in the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async click( + this: ElementHandle<Element>, + options?: ClickOptions + ): Promise<void>; + async click(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method creates and captures a dragevent from the element. + */ + async drag( + this: ElementHandle<Element>, + target: Point + ): Promise<Protocol.Input.DragData>; + async drag(this: ElementHandle<Element>): Promise<Protocol.Input.DragData> { + throw new Error('Not implemented'); + } + + /** + * This method creates a `dragenter` event on the element. + */ + async dragEnter( + this: ElementHandle<Element>, + data?: Protocol.Input.DragData + ): Promise<void>; + async dragEnter(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method creates a `dragover` event on the element. + */ + async dragOver( + this: ElementHandle<Element>, + data?: Protocol.Input.DragData + ): Promise<void>; + async dragOver(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method triggers a drop on the element. + */ + async drop( + this: ElementHandle<Element>, + data?: Protocol.Input.DragData + ): Promise<void>; + async drop(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method triggers a dragenter, dragover, and drop on the element. + */ + async dragAndDrop( + this: ElementHandle<Element>, + target: ElementHandle<Node>, + options?: {delay: number} + ): Promise<void>; + async dragAndDrop(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Triggers a `change` and `input` event once all the provided options have been + * selected. If there's no `<select>` element matching `selector`, the method + * throws an error. + * + * @example + * + * ```ts + * handle.select('blue'); // single selection + * handle.select('red', 'green', 'blue'); // multiple selections + * ``` + * + * @param values - Values of options to select. If the `<select>` has the + * `multiple` attribute, all values are considered, otherwise only the first + * one is taken into account. + */ + async select(...values: string[]): Promise<string[]>; + async select(): Promise<string[]> { + throw new Error('Not implemented'); + } + + /** + * Sets the value of an + * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element} + * to the given file paths. + * + * @remarks This will not validate whether the file paths exists. Also, if a + * path is relative, then it is resolved against the + * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. + * For locals script connecting to remote chrome environments, paths must be + * absolute. + */ + async uploadFile( + this: ElementHandle<HTMLInputElement>, + ...paths: string[] + ): Promise<void>; + async uploadFile(this: ElementHandle<HTMLInputElement>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method scrolls element into view if needed, and then uses + * {@link Touchscreen.tap} to tap in the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + async tap(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + async touchStart(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + async touchMove(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + async touchEnd(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. + */ + async focus(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and + * `keyup` event for each character in the text. + * + * To press a special key, like `Control` or `ArrowDown`, + * use {@link ElementHandle.press}. + * + * @example + * + * ```ts + * await elementHandle.type('Hello'); // Types instantly + * await elementHandle.type('World', {delay: 100}); // Types slower, like a user + * ``` + * + * @example + * An example of typing into a text field and then submitting the form: + * + * ```ts + * const elementHandle = await page.$('input'); + * await elementHandle.type('some text'); + * await elementHandle.press('Enter'); + * ``` + * + * @param options - Delay in milliseconds. Defaults to 0. + */ + async type(text: string, options?: {delay: number}): Promise<void>; + async type(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also be generated. + * The `text` option can be specified to force an input event to be generated. + * + * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift` + * will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + */ + async press(key: KeyInput, options?: PressOptions): Promise<void>; + async press(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method returns the bounding box of the element (relative to the main frame), + * or `null` if the element is not visible. + */ + async boundingBox(): Promise<BoundingBox | null> { + throw new Error('Not implemented'); + } + + /** + * This method returns boxes of the element, or `null` if the element is not visible. + * + * @remarks + * + * Boxes are represented as an array of points; + * Each Point is an object `{x, y}`. Box points are sorted clock-wise. + */ + async boxModel(): Promise<BoxModel | null> { + throw new Error('Not implemented'); + } + + /** + * This method scrolls element into view if needed, and then uses + * {@link Page.(screenshot:3) } to take a screenshot of the element. + * If the element is detached from DOM, the method throws an error. + */ + async screenshot( + this: ElementHandle<Element>, + options?: ScreenshotOptions + ): Promise<string | Buffer>; + async screenshot(this: ElementHandle<Element>): Promise<string | Buffer> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + protected async assertConnectedElement(): Promise<void> { + const error = await this.evaluate( + async (element): Promise<string | undefined> => { + if (!element.isConnected) { + return 'Node is detached from document'; + } + if (element.nodeType !== Node.ELEMENT_NODE) { + return 'Node is not of type HTMLElement'; + } + return; + } + ); + + if (error) { + throw new Error(error); + } + } + + /** + * Resolves to true if the element is visible in the current viewport. If an + * element is an SVG, we check if the svg owner element is in the viewport + * instead. See https://crbug.com/963246. + * + * @param options - Threshold for the intersection between 0 (no intersection) and 1 + * (full intersection). Defaults to 1. + */ + async isIntersectingViewport( + this: ElementHandle<Element>, + options?: { + threshold?: number; + } + ): Promise<boolean> { + await this.assertConnectedElement(); + + const {threshold = 0} = options ?? {}; + const svgHandle = await this.#asSVGElementHandle(this); + const intersectionTarget: ElementHandle<Element> = svgHandle + ? await this.#getOwnerSVGElement(svgHandle) + : this; + + try { + return await intersectionTarget.evaluate(async (element, threshold) => { + const visibleRatio = await new Promise<number>(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0]!.intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold; + }, threshold); + } finally { + if (intersectionTarget !== this) { + await intersectionTarget.dispose(); + } + } + } + + /** + * Scrolls the element into view using either the automation protocol client + * or by calling element.scrollIntoView. + */ + async scrollIntoView(this: ElementHandle<Element>): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Returns true if an element is an SVGElement (included svg, path, rect + * etc.). + */ + async #asSVGElementHandle( + handle: ElementHandle<Element> + ): Promise<ElementHandle<SVGElement> | null> { + if ( + await handle.evaluate(element => { + return element instanceof SVGElement; + }) + ) { + return handle as ElementHandle<SVGElement>; + } else { + return null; + } + } + + async #getOwnerSVGElement( + handle: ElementHandle<SVGElement> + ): Promise<ElementHandle<SVGSVGElement>> { + // SVGSVGElement.ownerSVGElement === null. + return await handle.evaluateHandle(element => { + if (element instanceof SVGSVGElement) { + return element; + } + return element.ownerSVGElement!; + }); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts new file mode 100644 index 0000000000..460077568e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPRequest.ts @@ -0,0 +1,567 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {CDPSession} from '../common/Connection.js'; +import {Frame} from '../common/Frame.js'; + +import {HTTPResponse} from './HTTPResponse.js'; + +/** + * @public + */ +export interface ContinueRequestOverrides { + /** + * If set, the request URL will change. This is not a redirect. + */ + url?: string; + method?: string; + postData?: string; + headers?: Record<string, string>; +} + +/** + * @public + */ +export interface InterceptResolutionState { + action: InterceptResolutionAction; + priority?: number; +} + +/** + * Required response data to fulfill a request with. + * + * @public + */ +export interface ResponseForRequest { + status: number; + /** + * Optional response headers. All values are converted to strings. + */ + headers: Record<string, unknown>; + contentType: string; + body: string | Buffer; +} + +/** + * Resource types for HTTPRequests as perceived by the rendering engine. + * + * @public + */ +export type ResourceType = Lowercase<Protocol.Network.ResourceType>; + +/** + * The default cooperative request interception resolution priority + * + * @public + */ +export const DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0; + +/** + * Represents an HTTP request sent by a page. + * @remarks + * + * Whenever the page sends a request, such as for a network resource, the + * following events are emitted by Puppeteer's `page`: + * + * - `request`: emitted when the request is issued by the page. + * - `requestfinished` - emitted when the response body is downloaded and the + * request is complete. + * + * If request fails at some point, then instead of `requestfinished` event the + * `requestfailed` event is emitted. + * + * All of these events provide an instance of `HTTPRequest` representing the + * request that occurred: + * + * ``` + * page.on('request', request => ...) + * ``` + * + * NOTE: HTTP Error responses, such as 404 or 503, are still successful + * responses from HTTP standpoint, so request will complete with + * `requestfinished` event. + * + * If request gets a 'redirect' response, the request is successfully finished + * with the `requestfinished` event, and a new request is issued to a + * redirected url. + * + * @public + */ +export class HTTPRequest { + /** + * @internal + */ + _requestId = ''; + /** + * @internal + */ + _interceptionId: string | undefined; + /** + * @internal + */ + _failureText: string | null = null; + /** + * @internal + */ + _response: HTTPResponse | null = null; + /** + * @internal + */ + _fromMemoryCache = false; + /** + * @internal + */ + _redirectChain: HTTPRequest[] = []; + + /** + * Warning! Using this client can break Puppeteer. Use with caution. + * + * @experimental + */ + get client(): CDPSession { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + constructor() {} + + /** + * The URL of the request + */ + url(): string { + throw new Error('Not implemented'); + } + + /** + * The `ContinueRequestOverrides` that will be used + * if the interception is allowed to continue (ie, `abort()` and + * `respond()` aren't called). + */ + continueRequestOverrides(): ContinueRequestOverrides { + throw new Error('Not implemented'); + } + + /** + * The `ResponseForRequest` that gets used if the + * interception is allowed to respond (ie, `abort()` is not called). + */ + responseForRequest(): Partial<ResponseForRequest> | null { + throw new Error('Not implemented'); + } + + /** + * The most recent reason for aborting the request + */ + abortErrorReason(): Protocol.Network.ErrorReason | null { + throw new Error('Not implemented'); + } + + /** + * An InterceptResolutionState object describing the current resolution + * action and priority. + * + * InterceptResolutionState contains: + * action: InterceptResolutionAction + * priority?: number + * + * InterceptResolutionAction is one of: `abort`, `respond`, `continue`, + * `disabled`, `none`, or `already-handled`. + */ + interceptResolutionState(): InterceptResolutionState { + throw new Error('Not implemented'); + } + + /** + * Is `true` if the intercept resolution has already been handled, + * `false` otherwise. + */ + isInterceptResolutionHandled(): boolean { + throw new Error('Not implemented'); + } + + /** + * Adds an async request handler to the processing queue. + * Deferred handlers are not guaranteed to execute in any particular order, + * but they are guaranteed to resolve before the request interception + * is finalized. + */ + enqueueInterceptAction( + pendingHandler: () => void | PromiseLike<unknown> + ): void; + enqueueInterceptAction(): void { + throw new Error('Not implemented'); + } + + /** + * Awaits pending interception handlers and then decides how to fulfill + * the request interception. + */ + async finalizeInterceptions(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Contains the request's resource type as it was perceived by the rendering + * engine. + */ + resourceType(): ResourceType { + throw new Error('Not implemented'); + } + + /** + * The method used (`GET`, `POST`, etc.) + */ + method(): string { + throw new Error('Not implemented'); + } + + /** + * The request's post body, if any. + */ + postData(): string | undefined { + throw new Error('Not implemented'); + } + + /** + * An object with HTTP headers associated with the request. All + * header names are lower-case. + */ + headers(): Record<string, string> { + throw new Error('Not implemented'); + } + + /** + * A matching `HTTPResponse` object, or null if the response has not + * been received yet. + */ + response(): HTTPResponse | null { + throw new Error('Not implemented'); + } + + /** + * The frame that initiated the request, or null if navigating to + * error pages. + */ + frame(): Frame | null { + throw new Error('Not implemented'); + } + + /** + * True if the request is the driver of the current frame's navigation. + */ + isNavigationRequest(): boolean { + throw new Error('Not implemented'); + } + + /** + * The initiator of the request. + */ + initiator(): Protocol.Network.Initiator | undefined { + throw new Error('Not implemented'); + } + + /** + * A `redirectChain` is a chain of requests initiated to fetch a resource. + * @remarks + * + * `redirectChain` is shared between all the requests of the same chain. + * + * For example, if the website `http://example.com` has a single redirect to + * `https://example.com`, then the chain will contain one request: + * + * ```ts + * const response = await page.goto('http://example.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 1 + * console.log(chain[0].url()); // 'http://example.com' + * ``` + * + * If the website `https://google.com` has no redirects, then the chain will be empty: + * + * ```ts + * const response = await page.goto('https://google.com'); + * const chain = response.request().redirectChain(); + * console.log(chain.length); // 0 + * ``` + * + * @returns the chain of requests - if a server responds with at least a + * single redirect, this chain will contain all requests that were redirected. + */ + redirectChain(): HTTPRequest[] { + throw new Error('Not implemented'); + } + + /** + * Access information about the request's failure. + * + * @remarks + * + * @example + * + * Example of logging all failed requests: + * + * ```ts + * page.on('requestfailed', request => { + * console.log(request.url() + ' ' + request.failure().errorText); + * }); + * ``` + * + * @returns `null` unless the request failed. If the request fails this can + * return an object with `errorText` containing a human-readable error + * message, e.g. `net::ERR_FAILED`. It is not guaranteed that there will be + * failure text if the request fails. + */ + failure(): {errorText: string} | null { + throw new Error('Not implemented'); + } + + /** + * Continues request with optional request overrides. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * + * ```ts + * await page.setRequestInterception(true); + * page.on('request', request => { + * // Override headers + * const headers = Object.assign({}, request.headers(), { + * foo: 'bar', // set "foo" header + * origin: undefined, // remove "origin" header + * }); + * request.continue({headers}); + * }); + * ``` + * + * @param overrides - optional overrides to apply to the request. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async continue( + overrides?: ContinueRequestOverrides, + priority?: number + ): Promise<void>; + async continue(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Fulfills a request with the given response. + * + * @remarks + * + * To use this, request + * interception should be enabled with {@link Page.setRequestInterception}. + * + * Exception is immediately thrown if the request interception is not enabled. + * + * @example + * An example of fulfilling all requests with 404 responses: + * + * ```ts + * await page.setRequestInterception(true); + * page.on('request', request => { + * request.respond({ + * status: 404, + * contentType: 'text/plain', + * body: 'Not Found!', + * }); + * }); + * ``` + * + * NOTE: Mocking responses for dataURL requests is not supported. + * Calling `request.respond` for a dataURL request is a noop. + * + * @param response - the response to fulfill the request with. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async respond( + response: Partial<ResponseForRequest>, + priority?: number + ): Promise<void>; + async respond(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Aborts a request. + * + * @remarks + * To use this, request interception should be enabled with + * {@link Page.setRequestInterception}. If it is not enabled, this method will + * throw an exception immediately. + * + * @param errorCode - optional error code to provide. + * @param priority - If provided, intercept is resolved using + * cooperative handling rules. Otherwise, intercept is resolved + * immediately. + */ + async abort(errorCode?: ErrorCode, priority?: number): Promise<void>; + async abort(): Promise<void> { + throw new Error('Not implemented'); + } +} + +/** + * @public + */ +export enum InterceptResolutionAction { + Abort = 'abort', + Respond = 'respond', + Continue = 'continue', + Disabled = 'disabled', + None = 'none', + AlreadyHandled = 'already-handled', +} + +/** + * @public + * + * @deprecated please use {@link InterceptResolutionAction} instead. + */ +export type InterceptResolutionStrategy = InterceptResolutionAction; + +/** + * @public + */ +export type ErrorCode = + | 'aborted' + | 'accessdenied' + | 'addressunreachable' + | 'blockedbyclient' + | 'blockedbyresponse' + | 'connectionaborted' + | 'connectionclosed' + | 'connectionfailed' + | 'connectionrefused' + | 'connectionreset' + | 'internetdisconnected' + | 'namenotresolved' + | 'timedout' + | 'failed'; + +/** + * @public + */ +export type ActionResult = 'continue' | 'abort' | 'respond'; + +/** + * @internal + */ +export function headersArray( + headers: Record<string, string | string[]> +): Array<{name: string; value: string}> { + const result = []; + for (const name in headers) { + const value = headers[name]; + + if (!Object.is(value, undefined)) { + const values = Array.isArray(value) ? value : [value]; + + result.push( + ...values.map(value => { + return {name, value: value + ''}; + }) + ); + } + } + return result; +} + +/** + * @internal + * + * @remarks + * List taken from {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml} + * with extra 306 and 418 codes. + */ +export const STATUS_TEXTS: {[key: string]: string | undefined} = { + '100': 'Continue', + '101': 'Switching Protocols', + '102': 'Processing', + '103': 'Early Hints', + '200': 'OK', + '201': 'Created', + '202': 'Accepted', + '203': 'Non-Authoritative Information', + '204': 'No Content', + '205': 'Reset Content', + '206': 'Partial Content', + '207': 'Multi-Status', + '208': 'Already Reported', + '226': 'IM Used', + '300': 'Multiple Choices', + '301': 'Moved Permanently', + '302': 'Found', + '303': 'See Other', + '304': 'Not Modified', + '305': 'Use Proxy', + '306': 'Switch Proxy', + '307': 'Temporary Redirect', + '308': 'Permanent Redirect', + '400': 'Bad Request', + '401': 'Unauthorized', + '402': 'Payment Required', + '403': 'Forbidden', + '404': 'Not Found', + '405': 'Method Not Allowed', + '406': 'Not Acceptable', + '407': 'Proxy Authentication Required', + '408': 'Request Timeout', + '409': 'Conflict', + '410': 'Gone', + '411': 'Length Required', + '412': 'Precondition Failed', + '413': 'Payload Too Large', + '414': 'URI Too Long', + '415': 'Unsupported Media Type', + '416': 'Range Not Satisfiable', + '417': 'Expectation Failed', + '418': "I'm a teapot", + '421': 'Misdirected Request', + '422': 'Unprocessable Entity', + '423': 'Locked', + '424': 'Failed Dependency', + '425': 'Too Early', + '426': 'Upgrade Required', + '428': 'Precondition Required', + '429': 'Too Many Requests', + '431': 'Request Header Fields Too Large', + '451': 'Unavailable For Legal Reasons', + '500': 'Internal Server Error', + '501': 'Not Implemented', + '502': 'Bad Gateway', + '503': 'Service Unavailable', + '504': 'Gateway Timeout', + '505': 'HTTP Version Not Supported', + '506': 'Variant Also Negotiates', + '507': 'Insufficient Storage', + '508': 'Loop Detected', + '510': 'Not Extended', + '511': 'Network Authentication Required', +} as const; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts new file mode 100644 index 0000000000..ddc56279a4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/HTTPResponse.ts @@ -0,0 +1,168 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Protocol from 'devtools-protocol'; + +import {Frame} from '../common/Frame.js'; +import {SecurityDetails} from '../common/SecurityDetails.js'; + +import {HTTPRequest} from './HTTPRequest.js'; + +/** + * @public + */ +export interface RemoteAddress { + ip?: string; + port?: number; +} + +/** + * The HTTPResponse class represents responses which are received by the + * {@link Page} class. + * + * @public + */ +export class HTTPResponse { + /** + * @internal + */ + constructor() {} + + /** + * @internal + */ + _resolveBody(_err: Error | null): void { + throw new Error('Not implemented'); + } + + /** + * The IP address and port number used to connect to the remote + * server. + */ + remoteAddress(): RemoteAddress { + throw new Error('Not implemented'); + } + + /** + * The URL of the response. + */ + url(): string { + throw new Error('Not implemented'); + } + + /** + * True if the response was successful (status in the range 200-299). + */ + ok(): boolean { + throw new Error('Not implemented'); + } + + /** + * The status code of the response (e.g., 200 for a success). + */ + status(): number { + throw new Error('Not implemented'); + } + + /** + * The status text of the response (e.g. usually an "OK" for a + * success). + */ + statusText(): string { + throw new Error('Not implemented'); + } + + /** + * An object with HTTP headers associated with the response. All + * header names are lower-case. + */ + headers(): Record<string, string> { + throw new Error('Not implemented'); + } + + /** + * {@link SecurityDetails} if the response was received over the + * secure connection, or `null` otherwise. + */ + securityDetails(): SecurityDetails | null { + throw new Error('Not implemented'); + } + + /** + * Timing information related to the response. + */ + timing(): Protocol.Network.ResourceTiming | null { + throw new Error('Not implemented'); + } + + /** + * Promise which resolves to a buffer with response body. + */ + buffer(): Promise<Buffer> { + throw new Error('Not implemented'); + } + + /** + * Promise which resolves to a text representation of response body. + */ + async text(): Promise<string> { + const content = await this.buffer(); + return content.toString('utf8'); + } + + /** + * Promise which resolves to a JSON representation of response body. + * + * @remarks + * + * This method will throw if the response body is not parsable via + * `JSON.parse`. + */ + async json(): Promise<any> { + const content = await this.text(); + return JSON.parse(content); + } + + /** + * A matching {@link HTTPRequest} object. + */ + request(): HTTPRequest { + throw new Error('Not implemented'); + } + + /** + * True if the response was served from either the browser's disk + * cache or memory cache. + */ + fromCache(): boolean { + throw new Error('Not implemented'); + } + + /** + * True if the response was served by a service worker. + */ + fromServiceWorker(): boolean { + throw new Error('Not implemented'); + } + + /** + * A {@link Frame} that initiated this response, or `null` if + * navigating to error pages. + */ + frame(): Frame | null { + throw new Error('Not implemented'); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts new file mode 100644 index 0000000000..8720fc0ad7 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/JSHandle.ts @@ -0,0 +1,197 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Protocol from 'devtools-protocol'; + +import {CDPSession} from '../common/Connection.js'; +import {ExecutionContext} from '../common/ExecutionContext.js'; +import {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js'; + +import {ElementHandle} from './ElementHandle.js'; + +declare const __JSHandleSymbol: unique symbol; + +/** + * Represents a reference to a JavaScript object. Instances can be created using + * {@link Page.evaluateHandle}. + * + * Handles prevent the referenced JavaScript object from being garbage-collected + * unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles + * are auto-disposed when their associated frame is navigated away or the parent + * context gets destroyed. + * + * Handles can be used as arguments for any evaluation function such as + * {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}. + * They are resolved to their referenced object. + * + * @example + * + * ```ts + * const windowHandle = await page.evaluateHandle(() => window); + * ``` + * + * @public + */ +export class JSHandle<T = unknown> { + /** + * Used for nominally typing {@link JSHandle}. + */ + [__JSHandleSymbol]?: T; + + /** + * @internal + */ + constructor() {} + + /** + * @internal + */ + get disposed(): boolean { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + executionContext(): ExecutionContext { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + get client(): CDPSession { + throw new Error('Not implemented'); + } + + /** + * Evaluates the given function with the current handle as its first argument. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async evaluate(): Promise<unknown> { + throw new Error('Not implemented'); + } + + /** + * Evaluates the given function with the current handle as its first argument. + * + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; + async evaluateHandle(): Promise<HandleFor<unknown>> { + throw new Error('Not implemented'); + } + + /** + * Fetches a single property from the referenced object. + */ + async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>>; + async getProperty(propertyName: string): Promise<JSHandle<unknown>>; + async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>>; + async getProperty<K extends keyof T>(): Promise<HandleFor<T[K]>> { + throw new Error('Not implemented'); + } + + /** + * Gets a map of handles representing the properties of the current handle. + * + * @example + * + * ```ts + * const listHandle = await page.evaluateHandle(() => document.body.children); + * const properties = await listHandle.getProperties(); + * const children = []; + * for (const property of properties.values()) { + * const element = property.asElement(); + * if (element) { + * children.push(element); + * } + * } + * children; // holds elementHandles to all children of document.body + * ``` + */ + async getProperties(): Promise<Map<string, JSHandle>> { + throw new Error('Not implemented'); + } + + /** + * A vanilla object representing the serializable portions of the + * referenced object. + * @throws Throws if the object cannot be serialized due to circularity. + * + * @remarks + * If the object has a `toJSON` function, it **will not** be called. + */ + async jsonValue(): Promise<T> { + throw new Error('Not implemented'); + } + + /** + * Either `null` or the handle itself if the handle is an + * instance of {@link ElementHandle}. + */ + asElement(): ElementHandle<Node> | null { + throw new Error('Not implemented'); + } + + /** + * Releases the object referenced by the handle for garbage collection. + */ + async dispose(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Returns a string representation of the JSHandle. + * + * @remarks + * Useful during debugging. + */ + toString(): string { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + get id(): string | undefined { + throw new Error('Not implemented'); + } + + /** + * Provides access to the + * {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject} + * backing this handle. + */ + remoteObject(): Protocol.Runtime.RemoteObject { + throw new Error('Not implemented'); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts new file mode 100644 index 0000000000..10fcbfccda --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/Page.ts @@ -0,0 +1,2748 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {Readable} from 'stream'; + +import {Protocol} from 'devtools-protocol'; + +import type {HTTPRequest} from '../api/HTTPRequest.js'; +import type {HTTPResponse} from '../api/HTTPResponse.js'; +import type {Accessibility} from '../common/Accessibility.js'; +import type {ConsoleMessage} from '../common/ConsoleMessage.js'; +import type {Coverage} from '../common/Coverage.js'; +import {Device} from '../common/Device.js'; +import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js'; +import type {Dialog} from '../common/Dialog.js'; +import {EventEmitter, Handler} from '../common/EventEmitter.js'; +import type {FileChooser} from '../common/FileChooser.js'; +import type { + Frame, + FrameAddScriptTagOptions, + FrameAddStyleTagOptions, + FrameWaitForFunctionOptions, +} from '../common/Frame.js'; +import type {Keyboard, Mouse, Touchscreen} from '../common/Input.js'; +import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; +import type {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js'; +import type {Credentials, NetworkConditions} from '../common/NetworkManager.js'; +import { + LowerCasePaperFormat, + paperFormats, + ParsedPDFOptions, + PDFOptions, +} from '../common/PDFOptions.js'; +import type {Viewport} from '../common/PuppeteerViewport.js'; +import type {Target} from '../common/Target.js'; +import type {Tracing} from '../common/Tracing.js'; +import type { + EvaluateFunc, + EvaluateFuncWith, + HandleFor, + NodeFor, +} from '../common/types.js'; +import {importFSPromises, isNumber, isString} from '../common/util.js'; +import type {WebWorker} from '../common/WebWorker.js'; +import {assert} from '../util/assert.js'; + +import type {Browser} from './Browser.js'; +import type {BrowserContext} from './BrowserContext.js'; +import type {ClickOptions, ElementHandle} from './ElementHandle.js'; +import type {JSHandle} from './JSHandle.js'; + +/** + * @public + */ +export interface Metrics { + Timestamp?: number; + Documents?: number; + Frames?: number; + JSEventListeners?: number; + Nodes?: number; + LayoutCount?: number; + RecalcStyleCount?: number; + LayoutDuration?: number; + RecalcStyleDuration?: number; + ScriptDuration?: number; + TaskDuration?: number; + JSHeapUsedSize?: number; + JSHeapTotalSize?: number; +} + +/** + * @public + */ +export interface WaitTimeoutOptions { + /** + * Maximum wait time in milliseconds. Pass 0 to disable the timeout. + * + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} method. + * + * @defaultValue `30000` + */ + timeout?: number; +} + +/** + * @public + */ +export interface WaitForOptions { + /** + * Maximum wait time in milliseconds. Pass 0 to disable the timeout. + * + * The default value can be changed by using the + * {@link Page.setDefaultTimeout} or {@link Page.setDefaultNavigationTimeout} + * methods. + * + * @defaultValue `30000` + */ + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; +} + +/** + * @public + */ +export interface GeolocationOptions { + /** + * Latitude between `-90` and `90`. + */ + longitude: number; + /** + * Longitude between `-180` and `180`. + */ + latitude: number; + /** + * Optional non-negative accuracy value. + */ + accuracy?: number; +} + +/** + * @public + */ +export interface MediaFeature { + name: string; + value: string; +} + +/** + * @public + */ +export interface ScreenshotClip { + x: number; + y: number; + width: number; + height: number; + /** + * @defaultValue `1` + */ + scale?: number; +} + +/** + * @public + */ +export interface ScreenshotOptions { + /** + * @defaultValue `png` + */ + type?: 'png' | 'jpeg' | 'webp'; + /** + * The file path to save the image to. The screenshot type will be inferred + * from file extension. If path is a relative path, then it is resolved + * relative to current working directory. If no path is provided, the image + * won't be saved to the disk. + */ + path?: string; + /** + * When `true`, takes a screenshot of the full page. + * @defaultValue `false` + */ + fullPage?: boolean; + /** + * An object which specifies the clipping region of the page. + */ + clip?: ScreenshotClip; + /** + * Quality of the image, between 0-100. Not applicable to `png` images. + */ + quality?: number; + /** + * Hides default white background and allows capturing screenshots with transparency. + * @defaultValue `false` + */ + omitBackground?: boolean; + /** + * Encoding of the image. + * @defaultValue `binary` + */ + encoding?: 'base64' | 'binary'; + /** + * Capture the screenshot beyond the viewport. + * @defaultValue `true` + */ + captureBeyondViewport?: boolean; + /** + * Capture the screenshot from the surface, rather than the view. + * @defaultValue `true` + */ + fromSurface?: boolean; +} + +/** + * All the events that a page instance may emit. + * + * @public + */ +export const enum PageEmittedEvents { + /** + * Emitted when the page closes. + */ + Close = 'close', + /** + * 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. + * + * @remarks + * A `console` event provides a {@link ConsoleMessage} representing the + * console message that was logged. + * + * @example + * An example of handling `console` event: + * + * ```ts + * 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'})); + * ``` + */ + Console = 'console', + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, + * `confirm` or `beforeunload`. Puppeteer can respond to the dialog via + * {@link Dialog.accept} or {@link Dialog.dismiss}. + */ + Dialog = 'dialog', + /** + * Emitted when the JavaScript + * {@link https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded | DOMContentLoaded } + * event is dispatched. + */ + DOMContentLoaded = 'domcontentloaded', + /** + * Emitted when the page crashes. Will contain an `Error`. + */ + Error = 'error', + /** Emitted when a frame is attached. Will contain a {@link Frame}. */ + FrameAttached = 'frameattached', + /** Emitted when a frame is detached. Will contain a {@link Frame}. */ + FrameDetached = 'framedetached', + /** + * Emitted when a frame is navigated to a new URL. Will contain a + * {@link Frame}. + */ + FrameNavigated = 'framenavigated', + /** + * Emitted when the JavaScript + * {@link https://developer.mozilla.org/en-US/docs/Web/Events/load | load} + * event is dispatched. + */ + Load = 'load', + /** + * Emitted when the JavaScript code makes a call to `console.timeStamp`. For + * the list of metrics see {@link Page.metrics | page.metrics}. + * + * @remarks + * Contains an object with two properties: + * + * - `title`: the title passed to `console.timeStamp` + * - `metrics`: object containing metrics as key/value pairs. The values will + * be `number`s. + */ + Metrics = 'metrics', + /** + * Emitted when an uncaught exception happens within the page. Contains an + * `Error`. + */ + PageError = 'pageerror', + /** + * Emitted when the page opens a new tab or window. + * + * Contains a {@link Page} corresponding to the popup window. + * + * @example + * + * ```ts + * const [popup] = await Promise.all([ + * new Promise(resolve => page.once('popup', resolve)), + * page.click('a[target=_blank]'), + * ]); + * ``` + * + * ```ts + * const [popup] = await Promise.all([ + * new Promise(resolve => page.once('popup', resolve)), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * ``` + */ + Popup = 'popup', + /** + * Emitted when a page issues a request and contains a {@link HTTPRequest}. + * + * @remarks + * The object is readonly. See {@link Page.setRequestInterception} for + * intercepting and mutating requests. + */ + Request = 'request', + /** + * Emitted when a request ended up loading from cache. Contains a + * {@link HTTPRequest}. + * + * @remarks + * For certain requests, might contain undefined. + * {@link https://crbug.com/750469} + */ + RequestServedFromCache = 'requestservedfromcache', + /** + * Emitted when a request fails, for example by timing out. + * + * Contains a {@link HTTPRequest}. + * + * @remarks + * HTTP Error responses, such as 404 or 503, are still successful responses + * from HTTP standpoint, so request will complete with `requestfinished` event + * and not with `requestfailed`. + */ + RequestFailed = 'requestfailed', + /** + * Emitted when a request finishes successfully. Contains a + * {@link HTTPRequest}. + */ + RequestFinished = 'requestfinished', + /** + * Emitted when a response is received. Contains a {@link HTTPResponse}. + */ + Response = 'response', + /** + * Emitted when a dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker} + * is spawned by the page. + */ + WorkerCreated = 'workercreated', + /** + * Emitted when a dedicated + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker} + * is destroyed by the page. + */ + WorkerDestroyed = 'workerdestroyed', +} + +/** + * Denotes the objects received by callback functions for page events. + * + * See {@link PageEmittedEvents} for more detail on the events and when they are + * emitted. + * + * @public + */ +export interface PageEventObject { + close: never; + console: ConsoleMessage; + dialog: Dialog; + domcontentloaded: never; + error: Error; + frameattached: Frame; + framedetached: Frame; + framenavigated: Frame; + load: never; + metrics: {title: string; metrics: Metrics}; + pageerror: Error; + popup: Page; + request: HTTPRequest; + response: HTTPResponse; + requestfailed: HTTPRequest; + requestfinished: HTTPRequest; + requestservedfromcache: HTTPRequest; + workercreated: WebWorker; + workerdestroyed: WebWorker; +} + +/** + * Page provides methods to interact with a single tab or + * {@link https://developer.chrome.com/extensions/background_pages | extension background page} + * in the browser. + * + * :::note + * + * One Browser instance might have multiple Page instances. + * + * ::: + * + * @example + * This example creates a page, navigates it to a URL, and then saves a screenshot: + * + * ```ts + * import puppeteer from '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 extends from Puppeteer's {@link EventEmitter} class and will + * emit various events which are documented in the {@link PageEmittedEvents} enum. + * + * @example + * This example logs a message for a single page `load` event: + * + * ```ts + * page.once('load', () => console.log('Page loaded!')); + * ``` + * + * To unsubscribe from events use the {@link Page.off} method: + * + * ```ts + * function logRequest(interceptedRequest) { + * console.log('A request was made:', interceptedRequest.url()); + * } + * page.on('request', logRequest); + * // Sometime later... + * page.off('request', logRequest); + * ``` + * + * @public + */ +export class Page extends EventEmitter { + #handlerMap = new WeakMap<Handler<any>, Handler<any>>(); + + /** + * @internal + */ + constructor() { + super(); + } + + /** + * `true` if drag events are being intercepted, `false` otherwise. + */ + isDragInterceptionEnabled(): boolean { + throw new Error('Not implemented'); + } + + /** + * `true` if the page has JavaScript enabled, `false` otherwise. + */ + isJavaScriptEnabled(): boolean { + throw new Error('Not implemented'); + } + + /** + * Listen to page events. + * + * :::note + * + * This method exists to define event typings and handle proper wireup of + * cooperative request interception. Actual event listening and dispatching is + * delegated to {@link EventEmitter}. + * + * ::: + */ + override on<K extends keyof PageEventObject>( + eventName: K, + handler: (event: PageEventObject[K]) => void + ): EventEmitter { + if (eventName === 'request') { + const wrap = + this.#handlerMap.get(handler) || + ((event: HTTPRequest) => { + event.enqueueInterceptAction(() => { + return handler(event as PageEventObject[K]); + }); + }); + + this.#handlerMap.set(handler, wrap); + + return super.on(eventName, wrap); + } + return super.on(eventName, handler); + } + + override once<K extends keyof PageEventObject>( + eventName: K, + handler: (event: PageEventObject[K]) => void + ): EventEmitter { + // Note: this method only exists to define the types; we delegate the impl + // to EventEmitter. + return super.once(eventName, handler); + } + + override off<K extends keyof PageEventObject>( + eventName: K, + handler: (event: PageEventObject[K]) => void + ): EventEmitter { + if (eventName === 'request') { + handler = this.#handlerMap.get(handler) || handler; + } + + return super.off(eventName, handler); + } + + /** + * This method is typically coupled with an action that triggers file + * choosing. + * + * :::caution + * + * This must be called before the file chooser is launched. It will not return + * a currently active file chooser. + * + * ::: + * + * @remarks + * In the "headful" browser, this method results in the native file picker + * dialog `not showing up` for the user. + * + * @example + * The following example clicks a button that issues a file chooser + * and then responds with `/tmp/myfile.pdf` as if a user has selected this file. + * + * ```ts + * const [fileChooser] = await Promise.all([ + * page.waitForFileChooser(), + * page.click('#upload-file-button'), + * // some button that triggers file selection + * ]); + * await fileChooser.accept(['/tmp/myfile.pdf']); + * ``` + */ + waitForFileChooser(options?: WaitTimeoutOptions): Promise<FileChooser>; + waitForFileChooser(): Promise<FileChooser> { + throw new Error('Not implemented'); + } + + /** + * Sets the page's geolocation. + * + * @remarks + * Consider using {@link BrowserContext.overridePermissions} to grant + * permissions for the page to read its geolocation. + * + * @example + * + * ```ts + * await page.setGeolocation({latitude: 59.95, longitude: 30.31667}); + * ``` + */ + async setGeolocation(options: GeolocationOptions): Promise<void>; + async setGeolocation(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * A target this page was created from. + */ + target(): Target { + throw new Error('Not implemented'); + } + + /** + * Get the browser the page belongs to. + */ + browser(): Browser { + throw new Error('Not implemented'); + } + + /** + * Get the browser context that the page belongs to. + */ + browserContext(): BrowserContext { + throw new Error('Not implemented'); + } + + /** + * The page's main frame. + * + * @remarks + * Page is guaranteed to have a main frame which persists during navigations. + */ + mainFrame(): Frame { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Keyboard} + */ + get keyboard(): Keyboard { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Touchscreen} + */ + get touchscreen(): Touchscreen { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Coverage} + */ + get coverage(): Coverage { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Tracing} + */ + get tracing(): Tracing { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Accessibility} + */ + get accessibility(): Accessibility { + throw new Error('Not implemented'); + } + + /** + * An array of all frames attached to the page. + */ + frames(): Frame[] { + throw new Error('Not implemented'); + } + + /** + * All of the dedicated {@link + * https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | + * WebWorkers} associated with the page. + * + * @remarks + * This does not contain ServiceWorkers + */ + workers(): WebWorker[] { + throw new Error('Not implemented'); + } + + /** + * Activating request interception enables {@link HTTPRequest.abort}, + * {@link HTTPRequest.continue} and {@link HTTPRequest.respond} methods. This + * provides the capability to modify network requests that are made by a page. + * + * Once request interception is enabled, every request will stall unless it's + * continued, responded or aborted; or completed using the browser cache. + * + * See the + * {@link https://pptr.dev/next/guides/request-interception|Request interception guide} + * for more details. + * + * @example + * An example of a naïve request interceptor that aborts all image requests: + * + * ```ts + * import puppeteer from 'puppeteer'; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.setRequestInterception(true); + * page.on('request', interceptedRequest => { + * if ( + * interceptedRequest.url().endsWith('.png') || + * interceptedRequest.url().endsWith('.jpg') + * ) + * interceptedRequest.abort(); + * else interceptedRequest.continue(); + * }); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + * + * @param value - Whether to enable request interception. + */ + async setRequestInterception(value: boolean): Promise<void>; + async setRequestInterception(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param enabled - Whether to enable drag interception. + * + * @remarks + * Activating drag interception enables the `Input.drag`, + * methods This provides the capability to capture drag events emitted + * on the page, which can then be used to simulate drag-and-drop. + */ + async setDragInterception(enabled: boolean): Promise<void>; + async setDragInterception(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Sets the network connection to offline. + * + * It does not change the parameters used in {@link Page.emulateNetworkConditions} + * + * @param enabled - When `true`, enables offline mode for the page. + */ + setOfflineMode(enabled: boolean): Promise<void>; + setOfflineMode(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This does not affect WebSockets and WebRTC PeerConnections (see + * https://crbug.com/563644). To set the page offline, you can use + * {@link Page.setOfflineMode}. + * + * A list of predefined network conditions can be used by importing + * {@link PredefinedNetworkConditions}. + * + * @example + * + * ```ts + * import {PredefinedNetworkConditions} from 'puppeteer'; + * const slow3G = PredefinedNetworkConditions['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(); + * })(); + * ``` + * + * @param networkConditions - Passing `null` disables network condition + * emulation. + */ + emulateNetworkConditions( + networkConditions: NetworkConditions | null + ): Promise<void>; + emulateNetworkConditions(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This setting will change the default maximum navigation time for the + * following methods and related shortcuts: + * + * - {@link Page.goBack | page.goBack(options)} + * + * - {@link Page.goForward | page.goForward(options)} + * + * - {@link Page.goto | page.goto(url,options)} + * + * - {@link Page.reload | page.reload(options)} + * + * - {@link Page.setContent | page.setContent(html,options)} + * + * - {@link Page.waitForNavigation | page.waitForNavigation(options)} + * @param timeout - Maximum navigation time in milliseconds. + */ + setDefaultNavigationTimeout(timeout: number): void; + setDefaultNavigationTimeout(): void { + throw new Error('Not implemented'); + } + + /** + * @param timeout - Maximum time in milliseconds. + */ + setDefaultTimeout(timeout: number): void; + setDefaultTimeout(): void { + throw new Error('Not implemented'); + } + + /** + * Maximum time in milliseconds. + */ + getDefaultTimeout(): number { + throw new Error('Not implemented'); + } + + /** + * Runs `document.querySelector` within the page. If no element matches the + * selector, the return value resolves to `null`. + * + * @param selector - A `selector` to query page for + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query page for. + */ + async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null>; + async $<Selector extends string>(): Promise<ElementHandle< + NodeFor<Selector> + > | null> { + throw new Error('Not implemented'); + } + + /** + * The method runs `document.querySelectorAll` within the page. If no elements + * match the selector, the return value resolves to `[]`. + * @remarks + * Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }. + * @param selector - A `selector` to query page for + */ + async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>>; + async $$<Selector extends string>(): Promise< + Array<ElementHandle<NodeFor<Selector>>> + > { + throw new Error('Not implemented'); + } + + /** + * @remarks + * + * The only difference between {@link Page.evaluate | page.evaluate} and + * `page.evaluateHandle` is that `evaluateHandle` will return the value + * wrapped in an in-page object. + * + * If the function passed to `page.evaluateHandle` returns a Promise, the + * function will wait for the promise to resolve and return its value. + * + * You can pass a string instead of a function (although functions are + * recommended as they are easier to debug and use with TypeScript): + * + * @example + * + * ```ts + * const aHandle = await page.evaluateHandle('document'); + * ``` + * + * @example + * {@link JSHandle} instances can be passed as arguments to the `pageFunction`: + * + * ```ts + * const aHandle = await page.evaluateHandle(() => document.body); + * const resultHandle = await page.evaluateHandle( + * body => body.innerHTML, + * aHandle + * ); + * console.log(await resultHandle.jsonValue()); + * await resultHandle.dispose(); + * ``` + * + * Most of the time this function returns a {@link JSHandle}, + * but if `pageFunction` returns a reference to an element, + * you instead get an {@link ElementHandle} back: + * + * @example + * + * ```ts + * const button = await page.evaluateHandle(() => + * document.querySelector('button') + * ); + * // can call `click` because `button` is an `ElementHandle` + * await button.click(); + * ``` + * + * The TypeScript definitions assume that `evaluateHandle` returns + * a `JSHandle`, but if you know it's going to return an + * `ElementHandle`, pass it as the generic argument: + * + * ```ts + * const button = await page.evaluateHandle<ElementHandle>(...); + * ``` + * + * @param pageFunction - a function that is run within the page + * @param args - arguments to be passed to the pageFunction + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >(): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + throw new Error('Not implemented'); + } + + /** + * This method iterates the JavaScript heap and finds all objects with the + * given prototype. + * + * @example + * + * ```ts + * // Create a Map object + * await page.evaluate(() => (window.map = new Map())); + * // Get a handle to the Map object prototype + * const mapPrototype = await page.evaluateHandle(() => Map.prototype); + * // Query all map instances into an array + * const mapInstances = await page.queryObjects(mapPrototype); + * // Count amount of map objects in heap + * const count = await page.evaluate(maps => maps.length, mapInstances); + * await mapInstances.dispose(); + * await mapPrototype.dispose(); + * ``` + * + * @param prototypeHandle - a handle to the object prototype. + * @returns Promise which resolves to a handle to an array of objects with + * this prototype. + */ + async queryObjects<Prototype>( + prototypeHandle: JSHandle<Prototype> + ): Promise<JSHandle<Prototype[]>>; + async queryObjects<Prototype>(): Promise<JSHandle<Prototype[]>> { + throw new Error('Not implemented'); + } + + /** + * This method runs `document.querySelector` within the page and passes the + * result as the first argument to the `pageFunction`. + * + * @remarks + * + * If no element is found matching `selector`, the method will throw an error. + * + * If `pageFunction` returns a promise `$eval` will wait for the promise to + * resolve and then return its value. + * + * @example + * + * ```ts + * 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', el => el.outerHTML); + * ``` + * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element`, but you may need to provide a more + * specific sub-type: + * + * @example + * + * ```ts + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * const searchValue = await page.$eval( + * '#search', + * (el: HTMLInputElement) => el.value + * ); + * ``` + * + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$eval`: + * + * @example + * + * ```ts + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const searchValue = await page.$eval<string>( + * '#search', + * (el: HTMLInputElement) => el.value + * ); + * ``` + * + * @param selector - the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query for + * @param pageFunction - the function to be evaluated in the page context. + * Will be passed the result of `document.querySelector(selector)` as its + * first argument. + * @param args - any additional arguments to pass through to `pageFunction`. + * + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. + */ + async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async $eval(): Promise<unknown> { + throw new Error('Not implemented'); + } + + /** + * This method runs `Array.from(document.querySelectorAll(selector))` within + * the page and passes the result as the first argument to the `pageFunction`. + * + * @remarks + * If `pageFunction` returns a promise `$$eval` will wait for the promise to + * resolve and then return its value. + * + * @example + * + * ```ts + * // get the amount of divs on the page + * const divCount = await page.$$eval('div', divs => divs.length); + * + * // get the text content of all the `.options` elements: + * const options = await page.$$eval('div > span.options', options => { + * return options.map(option => option.textContent); + * }); + * ``` + * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element[]`, but you may need to provide a more + * specific sub-type: + * + * @example + * + * ```ts + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * await page.$$eval('input', (elements: HTMLInputElement[]) => { + * return elements.map(e => e.value); + * }); + * ``` + * + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$$eval`: + * + * @example + * + * ```ts + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const allInputValues = await page.$$eval<string[]>( + * 'input', + * (elements: HTMLInputElement[]) => elements.map(e => e.textContent) + * ); + * ``` + * + * @param selector - the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to query for + * @param pageFunction - the function to be evaluated in the page context. + * Will be passed the result of + * `Array.from(document.querySelectorAll(selector))` as its first argument. + * @param args - any additional arguments to pass through to `pageFunction`. + * + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. + */ + async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async $$eval(): Promise<unknown> { + throw new Error('Not implemented'); + } + + /** + * 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. + * + * @remarks + * Shortcut for {@link Frame.$x | Page.mainFrame().$x(expression) }. + * + * @param expression - Expression to evaluate + */ + async $x(expression: string): Promise<Array<ElementHandle<Node>>>; + async $x(): Promise<Array<ElementHandle<Node>>> { + throw new Error('Not implemented'); + } + + /** + * If no URLs are specified, this method returns cookies for the current page + * URL. If URLs are specified, only cookies for those URLs are returned. + */ + async cookies(...urls: string[]): Promise<Protocol.Network.Cookie[]>; + async cookies(): Promise<Protocol.Network.Cookie[]> { + throw new Error('Not implemented'); + } + + async deleteCookie( + ...cookies: Protocol.Network.DeleteCookiesRequest[] + ): Promise<void>; + async deleteCookie(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @example + * + * ```ts + * await page.setCookie(cookieObject1, cookieObject2); + * ``` + */ + async setCookie(...cookies: Protocol.Network.CookieParam[]): Promise<void>; + async setCookie(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Adds a `<script>` tag into the page with the desired URL or content. + * + * @remarks + * Shortcut for + * {@link Frame.addScriptTag | page.mainFrame().addScriptTag(options)}. + * + * @param options - Options for the script. + * @returns An {@link ElementHandle | element handle} to the injected + * `<script>` element. + */ + async addScriptTag( + options: FrameAddScriptTagOptions + ): Promise<ElementHandle<HTMLScriptElement>>; + async addScriptTag(): Promise<ElementHandle<HTMLScriptElement>> { + throw new Error('Not implemented'); + } + + /** + * Adds a `<link rel="stylesheet">` tag into the page with the desired URL or + * a `<style type="text/css">` tag with the content. + * + * Shortcut for + * {@link Frame.(addStyleTag:2) | page.mainFrame().addStyleTag(options)}. + * + * @returns An {@link ElementHandle | element handle} to the injected `<link>` + * or `<style>` element. + */ + async addStyleTag( + options: Omit<FrameAddStyleTagOptions, 'url'> + ): Promise<ElementHandle<HTMLStyleElement>>; + async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLLinkElement>>; + async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>>; + async addStyleTag(): Promise< + ElementHandle<HTMLStyleElement | HTMLLinkElement> + > { + throw new Error('Not implemented'); + } + + /** + * The method adds a function called `name` on the page's `window` object. + * When called, the function executes `puppeteerFunction` in node.js and + * returns a `Promise` which resolves to the return value of + * `puppeteerFunction`. + * + * If the puppeteerFunction returns a `Promise`, it will be awaited. + * + * :::note + * + * Functions installed via `page.exposeFunction` survive navigations. + * + * :::note + * + * @example + * An example of adding an `md5` function into the page: + * + * ```ts + * import puppeteer from 'puppeteer'; + * import crypto from 'crypto'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * page.on('console', msg => console.log(msg.text())); + * await page.exposeFunction('md5', text => + * crypto.createHash('md5').update(text).digest('hex') + * ); + * await page.evaluate(async () => { + * // use window.md5 to compute hashes + * const myString = 'PUPPETEER'; + * const myHash = await window.md5(myString); + * console.log(`md5 of ${myString} is ${myHash}`); + * }); + * await browser.close(); + * })(); + * ``` + * + * @example + * An example of adding a `window.readfile` function into the page: + * + * ```ts + * import puppeteer from 'puppeteer'; + * import fs from 'fs'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * page.on('console', msg => console.log(msg.text())); + * await page.exposeFunction('readfile', async filePath => { + * return new Promise((resolve, reject) => { + * fs.readFile(filePath, 'utf8', (err, text) => { + * if (err) reject(err); + * else resolve(text); + * }); + * }); + * }); + * await page.evaluate(async () => { + * // use window.readfile to read contents of a file + * const content = await window.readfile('/etc/hosts'); + * console.log(content); + * }); + * await browser.close(); + * })(); + * ``` + * + * @param name - Name of the function on the window object + * @param pptrFunction - Callback function which will be called in Puppeteer's + * context. + */ + async exposeFunction( + name: string, + pptrFunction: Function | {default: Function} + ): Promise<void>; + async exposeFunction(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Provide credentials for `HTTP authentication`. + * + * @remarks + * To disable authentication, pass `null`. + */ + async authenticate(credentials: Credentials): Promise<void>; + async authenticate(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * The extra HTTP headers will be sent with every request the page initiates. + * + * :::tip + * + * All HTTP header names are lowercased. (HTTP headers are + * case-insensitive, so this shouldn’t impact your server code.) + * + * ::: + * + * :::note + * + * page.setExtraHTTPHeaders does not guarantee the order of headers in + * the outgoing requests. + * + * ::: + * + * @param headers - An object containing additional HTTP headers to be sent + * with every request. All header values must be strings. + */ + async setExtraHTTPHeaders(headers: Record<string, string>): Promise<void>; + async setExtraHTTPHeaders(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param userAgent - Specific user agent to use in this page + * @param userAgentData - Specific user agent client hint data to use in this + * page + * @returns Promise which resolves when the user agent is set. + */ + async setUserAgent( + userAgent: string, + userAgentMetadata?: Protocol.Emulation.UserAgentMetadata + ): Promise<void>; + async setUserAgent(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Object containing metrics as key/value pairs. + * + * @returns + * + * - `Timestamp` : The timestamp when the metrics sample was taken. + * + * - `Documents` : Number of documents in the page. + * + * - `Frames` : Number of frames in the page. + * + * - `JSEventListeners` : Number of events in the page. + * + * - `Nodes` : Number of DOM nodes in the page. + * + * - `LayoutCount` : Total number of full or partial page layout. + * + * - `RecalcStyleCount` : Total number of page style recalculations. + * + * - `LayoutDuration` : Combined durations of all page layouts. + * + * - `RecalcStyleDuration` : Combined duration of all page style + * recalculations. + * + * - `ScriptDuration` : Combined duration of JavaScript execution. + * + * - `TaskDuration` : Combined duration of all tasks performed by the browser. + * + * - `JSHeapUsedSize` : Used JavaScript heap size. + * + * - `JSHeapTotalSize` : Total JavaScript heap size. + * + * @remarks + * All timestamps are in monotonic time: monotonically increasing time + * in seconds since an arbitrary point in the past. + */ + async metrics(): Promise<Metrics> { + throw new Error('Not implemented'); + } + + /** + * The page's URL. + * @remarks Shortcut for + * {@link Frame.url | page.mainFrame().url()}. + */ + url(): string { + throw new Error('Not implemented'); + } + + /** + * The full HTML contents of the page, including the DOCTYPE. + */ + async content(): Promise<string> { + throw new Error('Not implemented'); + } + + /** + * Set the content of the page. + * + * @param html - HTML markup to assign to the page. + * @param options - Parameters that has some properties. + * @remarks + * The parameter `options` might have the following options. + * + * - `timeout` : Maximum time in milliseconds for resources to load, defaults + * to 30 seconds, pass `0` to disable timeout. The default value can be + * changed by using the {@link Page.setDefaultNavigationTimeout} or + * {@link Page.setDefaultTimeout} methods. + * + * - `waitUntil`: When to consider setting markup succeeded, defaults to + * `load`. Given an array of event strings, setting content is considered + * to be successful after all events have been fired. Events can be + * either:<br/> + * - `load` : consider setting content to be finished when the `load` event + * is fired.<br/> + * - `domcontentloaded` : consider setting content to be finished when the + * `DOMContentLoaded` event is fired.<br/> + * - `networkidle0` : consider setting content to be finished when there are + * no more than 0 network connections for at least `500` ms.<br/> + * - `networkidle2` : consider setting content to be finished when there are + * no more than 2 network connections for at least `500` ms. + */ + async setContent(html: string, options?: WaitForOptions): Promise<void>; + async setContent(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param url - URL to navigate page to. The URL should include scheme, e.g. + * `https://` + * @param options - Navigation Parameter + * @returns Promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. + * @remarks + * The argument `options` might have the following properties: + * + * - `timeout` : Maximum navigation time in milliseconds, defaults to 30 + * seconds, pass 0 to disable timeout. The default value can be changed by + * using the {@link Page.setDefaultNavigationTimeout} or + * {@link Page.setDefaultTimeout} methods. + * + * - `waitUntil`:When to consider navigation succeeded, defaults to `load`. + * Given an array of event strings, navigation is considered to be + * successful after all events have been fired. Events can be either:<br/> + * - `load` : consider navigation to be finished when the load event is + * fired.<br/> + * - `domcontentloaded` : consider navigation to be finished when the + * DOMContentLoaded event is fired.<br/> + * - `networkidle0` : consider navigation to be finished when there are no + * more than 0 network connections for at least `500` ms.<br/> + * - `networkidle2` : consider navigation to be finished when there are no + * more than 2 network connections for at least `500` ms. + * + * - `referer` : Referer header value. If provided it will take preference + * over the referer header value set by + * {@link Page.setExtraHTTPHeaders |page.setExtraHTTPHeaders()}.<br/> + * - `referrerPolicy` : ReferrerPolicy. If provided it will take preference + * over the referer-policy header value set by + * {@link Page.setExtraHTTPHeaders |page.setExtraHTTPHeaders()}. + * + * `page.goto` will throw an error if: + * + * - there's an SSL error (e.g. in case of self-signed certificates). + * - target URL is invalid. + * - the timeout is exceeded during navigation. + * - the remote server does not respond or is unreachable. + * - the main resource failed to load. + * + * `page.goto` will not throw an error when any valid HTTP status code is + * returned by the remote server, including 404 "Not Found" and 500 + * "Internal Server Error". The status code for such responses can be + * retrieved by calling response.status(). + * + * NOTE: `page.goto` either throws an error or returns a main resource + * response. The only exceptions are navigation to about:blank or navigation + * to the same URL with a different hash, which would succeed and return null. + * + * NOTE: Headless mode doesn't support navigation to a PDF document. See the + * {@link https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | + * upstream issue}. + * + * Shortcut for {@link Frame.goto | page.mainFrame().goto(url, options)}. + */ + async goto( + url: string, + options?: WaitForOptions & {referer?: string; referrerPolicy?: string} + ): Promise<HTTPResponse | null>; + async goto(): Promise<HTTPResponse | null> { + throw new Error('Not implemented'); + } + + /** + * @param options - Navigation parameters which might have the following + * properties: + * @returns Promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. + * @remarks + * The argument `options` might have the following properties: + * + * - `timeout` : Maximum navigation time in milliseconds, defaults to 30 + * seconds, pass 0 to disable timeout. The default value can be changed by + * using the {@link Page.setDefaultNavigationTimeout} or + * {@link Page.setDefaultTimeout} methods. + * + * - `waitUntil`: When to consider navigation succeeded, defaults to `load`. + * Given an array of event strings, navigation is considered to be + * successful after all events have been fired. Events can be either:<br/> + * - `load` : consider navigation to be finished when the load event is + * fired.<br/> + * - `domcontentloaded` : consider navigation to be finished when the + * DOMContentLoaded event is fired.<br/> + * - `networkidle0` : consider navigation to be finished when there are no + * more than 0 network connections for at least `500` ms.<br/> + * - `networkidle2` : consider navigation to be finished when there are no + * more than 2 network connections for at least `500` ms. + */ + async reload(options?: WaitForOptions): Promise<HTTPResponse | null>; + async reload(): Promise<HTTPResponse | null> { + throw new Error('Not implemented'); + } + + /** + * Waits for the page to navigate to a new URL or to reload. It is useful when + * you run code that will indirectly cause the page to navigate. + * + * @example + * + * ```ts + * const [response] = await Promise.all([ + * page.waitForNavigation(), // The promise resolves after navigation has finished + * page.click('a.my-link'), // Clicking the link will indirectly cause a navigation + * ]); + * ``` + * + * @remarks + * Usage of the + * {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API} + * to change the URL is considered a navigation. + * + * @param options - Navigation parameters which might have the following + * properties: + * @returns A `Promise` which resolves to the main resource response. + * + * - In case of multiple redirects, the navigation will resolve with the + * response of the last redirect. + * - In case of navigation to a different anchor or navigation due to History + * API usage, the navigation will resolve with `null`. + */ + async waitForNavigation( + options?: WaitForOptions + ): Promise<HTTPResponse | null>; + async waitForNavigation(): Promise<HTTPResponse | null> { + throw new Error('Not implemented'); + } + + /** + * @param urlOrPredicate - A URL or predicate to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves to the matched request + * @example + * + * ```ts + * const firstRequest = await page.waitForRequest( + * 'https://example.com/resource' + * ); + * const finalRequest = await page.waitForRequest( + * request => request.url() === 'https://example.com' + * ); + * return finalRequest.response()?.ok(); + * ``` + * + * @remarks + * Optional Waiting Parameters have: + * + * - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds, pass + * `0` to disable the timeout. The default value can be changed by using the + * {@link Page.setDefaultTimeout} method. + */ + async waitForRequest( + urlOrPredicate: string | ((req: HTTPRequest) => boolean | Promise<boolean>), + options?: {timeout?: number} + ): Promise<HTTPRequest>; + async waitForRequest(): Promise<HTTPRequest> { + throw new Error('Not implemented'); + } + + /** + * @param urlOrPredicate - A URL or predicate to wait for. + * @param options - Optional waiting parameters + * @returns Promise which resolves to the matched response. + * @example + * + * ```ts + * const firstResponse = await page.waitForResponse( + * 'https://example.com/resource' + * ); + * const finalResponse = await page.waitForResponse( + * response => + * response.url() === 'https://example.com' && response.status() === 200 + * ); + * const finalResponse = await page.waitForResponse(async response => { + * return (await response.text()).includes('<html>'); + * }); + * return finalResponse.ok(); + * ``` + * + * @remarks + * Optional Parameter have: + * + * - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds, + * pass `0` to disable the timeout. The default value can be changed by using + * the {@link Page.setDefaultTimeout} method. + */ + async waitForResponse( + urlOrPredicate: + | string + | ((res: HTTPResponse) => boolean | Promise<boolean>), + options?: {timeout?: number} + ): Promise<HTTPResponse>; + async waitForResponse(): Promise<HTTPResponse> { + throw new Error('Not implemented'); + } + + /** + * @param options - Optional waiting parameters + * @returns Promise which resolves when network is idle + */ + async waitForNetworkIdle(options?: { + idleTime?: number; + timeout?: number; + }): Promise<void>; + async waitForNetworkIdle(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param urlOrPredicate - A URL or predicate to wait for. + * @param options - Optional waiting parameters + * @returns Promise which resolves to the matched frame. + * @example + * + * ```ts + * const frame = await page.waitForFrame(async frame => { + * return frame.name() === 'Test'; + * }); + * ``` + * + * @remarks + * Optional Parameter have: + * + * - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds, + * pass `0` to disable the timeout. The default value can be changed by using + * the {@link Page.setDefaultTimeout} method. + */ + async waitForFrame( + urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>), + options?: {timeout?: number} + ): Promise<Frame>; + async waitForFrame(): Promise<Frame> { + throw new Error('Not implemented'); + } + + /** + * This method navigate to the previous page in history. + * @param options - Navigation parameters + * @returns Promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. If can not go back, resolves to `null`. + * @remarks + * The argument `options` might have the following properties: + * + * - `timeout` : Maximum navigation time in milliseconds, defaults to 30 + * seconds, pass 0 to disable timeout. The default value can be changed by + * using the {@link Page.setDefaultNavigationTimeout} or + * {@link Page.setDefaultTimeout} methods. + * + * - `waitUntil` : When to consider navigation succeeded, defaults to `load`. + * Given an array of event strings, navigation is considered to be + * successful after all events have been fired. Events can be either:<br/> + * - `load` : consider navigation to be finished when the load event is + * fired.<br/> + * - `domcontentloaded` : consider navigation to be finished when the + * DOMContentLoaded event is fired.<br/> + * - `networkidle0` : consider navigation to be finished when there are no + * more than 0 network connections for at least `500` ms.<br/> + * - `networkidle2` : consider navigation to be finished when there are no + * more than 2 network connections for at least `500` ms. + */ + async goBack(options?: WaitForOptions): Promise<HTTPResponse | null>; + async goBack(): Promise<HTTPResponse | null> { + throw new Error('Not implemented'); + } + + /** + * This method navigate to the next page in history. + * @param options - Navigation Parameter + * @returns Promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. If can not go forward, resolves to `null`. + * @remarks + * The argument `options` might have the following properties: + * + * - `timeout` : Maximum navigation time in milliseconds, defaults to 30 + * seconds, pass 0 to disable timeout. The default value can be changed by + * using the {@link Page.setDefaultNavigationTimeout} or + * {@link Page.setDefaultTimeout} methods. + * + * - `waitUntil`: When to consider navigation succeeded, defaults to `load`. + * Given an array of event strings, navigation is considered to be + * successful after all events have been fired. Events can be either:<br/> + * - `load` : consider navigation to be finished when the load event is + * fired.<br/> + * - `domcontentloaded` : consider navigation to be finished when the + * DOMContentLoaded event is fired.<br/> + * - `networkidle0` : consider navigation to be finished when there are no + * more than 0 network connections for at least `500` ms.<br/> + * - `networkidle2` : consider navigation to be finished when there are no + * more than 2 network connections for at least `500` ms. + */ + async goForward(options?: WaitForOptions): Promise<HTTPResponse | null>; + async goForward(): Promise<HTTPResponse | null> { + throw new Error('Not implemented'); + } + + /** + * Brings page to front (activates tab). + */ + async bringToFront(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Emulates a given device's metrics and user agent. + * + * To aid emulation, Puppeteer provides a list of known devices that can be + * via {@link KnownDevices}. + * + * @remarks + * This method is a shortcut for calling two methods: + * {@link Page.setUserAgent} and {@link Page.setViewport}. + * + * @remarks + * This method will resize the page. A lot of websites don't expect phones to + * change size, so you should emulate before navigating to the page. + * + * @example + * + * ```ts + * import {KnownDevices} from 'puppeteer'; + * const iPhone = KnownDevices['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(); + * })(); + * ``` + */ + async emulate(device: Device): Promise<void> { + await Promise.all([ + this.setUserAgent(device.userAgent), + this.setViewport(device.viewport), + ]); + } + + /** + * @param enabled - Whether or not to enable JavaScript on the page. + * @remarks + * NOTE: changing this value won't affect scripts that have already been run. + * It will take full effect on the next navigation. + */ + async setJavaScriptEnabled(enabled: boolean): Promise<void>; + async setJavaScriptEnabled(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Toggles bypassing page's Content-Security-Policy. + * @param enabled - sets bypassing of page's Content-Security-Policy. + * @remarks + * NOTE: CSP bypassing happens at the moment of CSP initialization rather than + * evaluation. Usually, this means that `page.setBypassCSP` should be called + * before navigating to the domain. + */ + async setBypassCSP(enabled: boolean): Promise<void>; + async setBypassCSP(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param type - Changes the CSS media type of the page. The only allowed + * values are `screen`, `print` and `null`. Passing `null` disables CSS media + * emulation. + * @example + * + * ```ts + * await page.evaluate(() => matchMedia('screen').matches); + * // → true + * await page.evaluate(() => matchMedia('print').matches); + * // → false + * + * await page.emulateMediaType('print'); + * await page.evaluate(() => matchMedia('screen').matches); + * // → false + * await page.evaluate(() => matchMedia('print').matches); + * // → true + * + * await page.emulateMediaType(null); + * await page.evaluate(() => matchMedia('screen').matches); + * // → true + * await page.evaluate(() => matchMedia('print').matches); + * // → false + * ``` + */ + async emulateMediaType(type?: string): Promise<void>; + async emulateMediaType(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Enables CPU throttling to emulate slow CPUs. + * @param factor - slowdown factor (1 is no throttle, 2 is 2x slowdown, etc). + */ + async emulateCPUThrottling(factor: number | null): Promise<void>; + async emulateCPUThrottling(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param features - `<?Array<Object>>` Given an array of media feature + * objects, emulates CSS media features on the page. Each media feature object + * must have the following properties: + * @example + * + * ```ts + * await page.emulateMediaFeatures([ + * {name: 'prefers-color-scheme', value: 'dark'}, + * ]); + * await page.evaluate( + * () => matchMedia('(prefers-color-scheme: dark)').matches + * ); + * // → true + * await page.evaluate( + * () => matchMedia('(prefers-color-scheme: light)').matches + * ); + * // → false + * + * await page.emulateMediaFeatures([ + * {name: 'prefers-reduced-motion', value: 'reduce'}, + * ]); + * await page.evaluate( + * () => matchMedia('(prefers-reduced-motion: reduce)').matches + * ); + * // → true + * await page.evaluate( + * () => matchMedia('(prefers-reduced-motion: no-preference)').matches + * ); + * // → false + * + * await page.emulateMediaFeatures([ + * {name: 'prefers-color-scheme', value: 'dark'}, + * {name: 'prefers-reduced-motion', value: 'reduce'}, + * ]); + * await page.evaluate( + * () => matchMedia('(prefers-color-scheme: dark)').matches + * ); + * // → true + * await page.evaluate( + * () => matchMedia('(prefers-color-scheme: light)').matches + * ); + * // → false + * await page.evaluate( + * () => matchMedia('(prefers-reduced-motion: reduce)').matches + * ); + * // → true + * await page.evaluate( + * () => matchMedia('(prefers-reduced-motion: no-preference)').matches + * ); + * // → false + * + * await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]); + * await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches); + * // → true + * await page.evaluate(() => matchMedia('(color-gamut: p3)').matches); + * // → true + * await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches); + * // → false + * ``` + */ + async emulateMediaFeatures(features?: MediaFeature[]): Promise<void>; + async emulateMediaFeatures(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @param timezoneId - Changes the timezone of the page. See + * {@link https://source.chromium.org/chromium/chromium/deps/icu.git/+/faee8bc70570192d82d2978a71e2a615788597d1:source/data/misc/metaZones.txt | ICU’s metaZones.txt} + * for a list of supported timezone IDs. Passing + * `null` disables timezone emulation. + */ + async emulateTimezone(timezoneId?: string): Promise<void>; + async emulateTimezone(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Emulates the idle state. + * If no arguments set, clears idle state emulation. + * + * @example + * + * ```ts + * // set idle emulation + * await page.emulateIdleState({isUserActive: true, isScreenUnlocked: false}); + * + * // do some checks here + * ... + * + * // clear idle emulation + * await page.emulateIdleState(); + * ``` + * + * @param overrides - Mock idle state. If not set, clears idle overrides + */ + async emulateIdleState(overrides?: { + isUserActive: boolean; + isScreenUnlocked: boolean; + }): Promise<void>; + async emulateIdleState(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Simulates the given vision deficiency on the page. + * + * @example + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://v8.dev/blog/10-years'); + * + * await page.emulateVisionDeficiency('achromatopsia'); + * await page.screenshot({path: 'achromatopsia.png'}); + * + * await page.emulateVisionDeficiency('deuteranopia'); + * await page.screenshot({path: 'deuteranopia.png'}); + * + * await page.emulateVisionDeficiency('blurredVision'); + * await page.screenshot({path: 'blurred-vision.png'}); + * + * await browser.close(); + * })(); + * ``` + * + * @param type - the type of deficiency to simulate, or `'none'` to reset. + */ + async emulateVisionDeficiency( + type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + ): Promise<void>; + async emulateVisionDeficiency(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * `page.setViewport` will resize the page. A lot of websites don't expect + * phones to change size, so you should set the viewport before navigating to + * the page. + * + * In the case of multiple pages in a single browser, each page can have its + * own viewport size. + * @example + * + * ```ts + * const page = await browser.newPage(); + * await page.setViewport({ + * width: 640, + * height: 480, + * deviceScaleFactor: 1, + * }); + * await page.goto('https://example.com'); + * ``` + * + * @param viewport - + * @remarks + * Argument viewport have following properties: + * + * - `width`: page width in pixels. required + * + * - `height`: page height in pixels. required + * + * - `deviceScaleFactor`: Specify device scale factor (can be thought of as + * DPR). Defaults to `1`. + * + * - `isMobile`: Whether the meta viewport tag is taken into account. Defaults + * to `false`. + * + * - `hasTouch`: Specifies if viewport supports touch events. Defaults to `false` + * + * - `isLandScape`: Specifies if viewport is in landscape mode. Defaults to false. + * + * NOTE: in certain cases, setting viewport will reload the page in order to + * set the isMobile or hasTouch properties. + */ + async setViewport(viewport: Viewport): Promise<void>; + async setViewport(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Current page viewport settings. + * + * @returns + * + * - `width`: page's width in pixels + * + * - `height`: page's height in pixels + * + * - `deviceScaleFactor`: Specify device scale factor (can be though of as + * dpr). Defaults to `1`. + * + * - `isMobile`: Whether the meta viewport tag is taken into account. Defaults + * to `false`. + * + * - `hasTouch`: Specifies if viewport supports touch events. Defaults to + * `false`. + * + * - `isLandScape`: Specifies if viewport is in landscape mode. Defaults to + * `false`. + */ + viewport(): Viewport | null { + throw new Error('Not implemented'); + } + + /** + * Evaluates a function in the page's context and returns the result. + * + * If the function passed to `page.evaluateHandle` returns a Promise, the + * function will wait for the promise to resolve and return its value. + * + * @example + * + * ```ts + * const result = await frame.evaluate(() => { + * return Promise.resolve(8 * 7); + * }); + * console.log(result); // prints "56" + * ``` + * + * You can pass a string instead of a function (although functions are + * recommended as they are easier to debug and use with TypeScript): + * + * @example + * + * ```ts + * const aHandle = await page.evaluate('1 + 2'); + * ``` + * + * To get the best TypeScript experience, you should pass in as the + * generic the type of `pageFunction`: + * + * ```ts + * const aHandle = await page.evaluate(() => 2); + * ``` + * + * @example + * + * {@link ElementHandle} instances (including {@link JSHandle}s) can be passed + * as arguments to the `pageFunction`: + * + * ```ts + * const bodyHandle = await page.$('body'); + * const html = await page.evaluate(body => body.innerHTML, bodyHandle); + * await bodyHandle.dispose(); + * ``` + * + * @param pageFunction - a function that is run within the page + * @param args - arguments to be passed to the pageFunction + * + * @returns the return value of `pageFunction`. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >(): Promise<Awaited<ReturnType<Func>>> { + throw new Error('Not implemented'); + } + + /** + * Adds a function which would be invoked in one of the following scenarios: + * + * - whenever the page is navigated + * + * - whenever the child frame is attached or navigated. In this case, the + * function is invoked in the context of the newly attached frame. + * + * The function is invoked after the document was created but before any of + * its scripts were run. This is useful to amend the JavaScript environment, + * e.g. to seed `Math.random`. + * @param pageFunction - Function to be evaluated in browser context + * @param args - Arguments to pass to `pageFunction` + * @example + * An example of overriding the navigator.languages property before the page loads: + * + * ```ts + * // preload.js + * + * // overwrite the `languages` property to use a custom getter + * Object.defineProperty(navigator, 'languages', { + * get: function () { + * return ['en-US', 'en', 'bn']; + * }, + * }); + * + * // In your puppeteer script, assuming the preload.js file is + * // in same folder of our script. + * const preloadFile = fs.readFileSync('./preload.js', 'utf8'); + * await page.evaluateOnNewDocument(preloadFile); + * ``` + */ + async evaluateOnNewDocument< + Params extends unknown[], + Func extends (...args: Params) => unknown = (...args: Params) => unknown + >(pageFunction: Func | string, ...args: Params): Promise<void>; + async evaluateOnNewDocument(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Toggles ignoring cache for each request based on the enabled state. By + * default, caching is enabled. + * @param enabled - sets the `enabled` state of cache + * @defaultValue `true` + */ + async setCacheEnabled(enabled?: boolean): Promise<void>; + async setCacheEnabled(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + async _maybeWriteBufferToFile( + path: string | undefined, + buffer: Buffer + ): Promise<void> { + if (!path) { + return; + } + + const fs = await importFSPromises(); + + await fs.writeFile(path, buffer); + } + + /** + * Captures screenshot of the current page. + * + * @remarks + * Options object which might have the following properties: + * + * - `path` : The file path to save the image to. The screenshot type + * will be inferred from file extension. If `path` is a relative path, then + * it is resolved relative to + * {@link https://nodejs.org/api/process.html#process_process_cwd + * | current working directory}. + * If no path is provided, the image won't be saved to the disk. + * + * - `type` : Specify screenshot type, can be either `jpeg` or `png`. + * Defaults to 'png'. + * + * - `quality` : The quality of the image, between 0-100. Not + * applicable to `png` images. + * + * - `fullPage` : When true, takes a screenshot of the full + * scrollable page. Defaults to `false`. + * + * - `clip` : An object which specifies clipping region of the page. + * Should have the following fields:<br/> + * - `x` : x-coordinate of top-left corner of clip area.<br/> + * - `y` : y-coordinate of top-left corner of clip area.<br/> + * - `width` : width of clipping area.<br/> + * - `height` : height of clipping area. + * + * - `omitBackground` : Hides default white background and allows + * capturing screenshots with transparency. Defaults to `false`. + * + * - `encoding` : The encoding of the image, can be either base64 or + * binary. Defaults to `binary`. + * + * - `captureBeyondViewport` : When true, captures screenshot + * {@link https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot + * | beyond the viewport}. When false, falls back to old behaviour, + * and cuts the screenshot by the viewport size. Defaults to `true`. + * + * - `fromSurface` : When true, captures screenshot + * {@link https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot + * | from the surface rather than the view}. When false, works only in + * headful mode and ignores page viewport (but not browser window's + * bounds). Defaults to `true`. + * + * @returns Promise which resolves to buffer or a base64 string (depending on + * the value of `encoding`) with captured screenshot. + */ + screenshot( + options: ScreenshotOptions & {encoding: 'base64'} + ): Promise<string>; + screenshot( + options?: ScreenshotOptions & {encoding?: 'binary'} + ): Promise<Buffer>; + async screenshot(options?: ScreenshotOptions): Promise<Buffer | string>; + async screenshot(): Promise<Buffer | string> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + _getPDFOptions( + options: PDFOptions = {}, + lengthUnit: 'in' | 'cm' = 'in' + ): ParsedPDFOptions { + const defaults = { + scale: 1, + displayHeaderFooter: false, + headerTemplate: '', + footerTemplate: '', + printBackground: false, + landscape: false, + pageRanges: '', + preferCSSPageSize: false, + omitBackground: false, + timeout: 30000, + }; + + let width = 8.5; + let height = 11; + if (options.format) { + const format = + paperFormats[options.format.toLowerCase() as LowerCasePaperFormat]; + assert(format, 'Unknown paper format: ' + options.format); + width = format.width; + height = format.height; + } else { + width = convertPrintParameterToInches(options.width, lengthUnit) ?? width; + height = + convertPrintParameterToInches(options.height, lengthUnit) ?? height; + } + + const margin = { + top: convertPrintParameterToInches(options.margin?.top, lengthUnit) || 0, + left: + convertPrintParameterToInches(options.margin?.left, lengthUnit) || 0, + bottom: + convertPrintParameterToInches(options.margin?.bottom, lengthUnit) || 0, + right: + convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0, + }; + + const output = { + ...defaults, + ...options, + width, + height, + margin, + }; + + return output; + } + + /** + * Generates a PDF of the page with the `print` CSS media type. + * @remarks + * + * To generate a PDF with the `screen` media type, call + * {@link Page.emulateMediaType | `page.emulateMediaType('screen')`} before + * calling `page.pdf()`. + * + * By default, `page.pdf()` generates a pdf with modified colors for printing. + * Use the + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-print-color-adjust | `-webkit-print-color-adjust`} + * property to force rendering of exact colors. + * + * @param options - options for generating the PDF. + */ + async createPDFStream(options?: PDFOptions): Promise<Readable>; + async createPDFStream(): Promise<Readable> { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Page.createPDFStream} + */ + async pdf(options?: PDFOptions): Promise<Buffer>; + async pdf(): Promise<Buffer> { + throw new Error('Not implemented'); + } + + /** + * The page's title + * + * @remarks + * Shortcut for {@link Frame.title | page.mainFrame().title()}. + */ + async title(): Promise<string> { + throw new Error('Not implemented'); + } + + async close(options?: {runBeforeUnload?: boolean}): Promise<void>; + async close(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Indicates that the page has been closed. + * @returns + */ + isClosed(): boolean { + throw new Error('Not implemented'); + } + + /** + * {@inheritDoc Mouse} + */ + get mouse(): Mouse { + throw new Error('Not implemented'); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link Page | Page.mouse} to click in the center of the + * element. If there's no element matching `selector`, the method throws an + * error. + * @remarks Bear in mind that if `click()` triggers a navigation event and + * there's a separate `page.waitForNavigation()` promise to be resolved, you + * may end up with a race condition that yields unexpected results. The + * correct pattern for click and wait for navigation is the following: + * + * ```ts + * const [response] = await Promise.all([ + * page.waitForNavigation(waitOptions), + * page.click(selector, clickOptions), + * ]); + * ``` + * + * Shortcut for {@link Frame.click | page.mainFrame().click(selector[, options]) }. + * @param selector - A `selector` to search for element to click. If there are + * multiple elements satisfying the `selector`, the first will be clicked + * @param options - `Object` + * @returns Promise which resolves when the element matching `selector` is + * successfully clicked. The Promise will be rejected if there is no element + * matching `selector`. + */ + click(selector: string, options?: Readonly<ClickOptions>): Promise<void>; + click(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method fetches an element with `selector` and focuses it. If there's no + * element matching `selector`, the method throws an error. + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector } + * of an element to focus. If there are multiple elements satisfying the + * selector, the first will be focused. + * @returns Promise which resolves when the element matching selector is + * successfully focused. The promise will be rejected if there is no element + * matching selector. + * @remarks + * Shortcut for {@link Frame.focus | page.mainFrame().focus(selector)}. + */ + focus(selector: string): Promise<void>; + focus(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link Page | Page.mouse} + * to hover over the center of the element. + * If there's no element matching `selector`, the method throws an error. + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * to search for element to hover. If there are multiple elements satisfying + * the selector, the first will be hovered. + * @returns Promise which resolves when the element matching `selector` is + * successfully hovered. Promise gets rejected if there's no element matching + * `selector`. + * @remarks + * Shortcut for {@link Page.hover | page.mainFrame().hover(selector)}. + */ + hover(selector: string): Promise<void>; + hover(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Triggers a `change` and `input` event once all the provided options have been + * selected. If there's no `<select>` element matching `selector`, the method + * throws an error. + * + * @example + * + * ```ts + * page.select('select#colors', 'blue'); // single selection + * page.select('select#colors', 'red', 'green', 'blue'); // multiple selections + * ``` + * + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | Selector} + * to query the page for + * @param values - Values of options to select. If the `<select>` has the + * `multiple` attribute, all values are considered, otherwise only the first one + * is taken into account. + * @returns + * + * @remarks + * Shortcut for {@link Frame.select | page.mainFrame().select()} + */ + select(selector: string, ...values: string[]): Promise<string[]>; + select(): Promise<string[]> { + throw new Error('Not implemented'); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link Page | Page.touchscreen} + * to tap in the center of the element. + * If there's no element matching `selector`, the method throws an error. + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | Selector} + * to search for element to tap. If there are multiple elements satisfying the + * selector, the first will be tapped. + * @returns + * @remarks + * Shortcut for {@link Frame.tap | page.mainFrame().tap(selector)}. + */ + tap(selector: string): Promise<void>; + tap(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Sends a `keydown`, `keypress/input`, and `keyup` event for each character + * in the text. + * + * To press a special key, like `Control` or `ArrowDown`, use {@link Keyboard.press}. + * @example + * + * ```ts + * await page.type('#mytextarea', 'Hello'); + * // Types instantly + * await page.type('#mytextarea', 'World', {delay: 100}); + * // Types slower, like a user + * ``` + * + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * of an element to type into. If there are multiple elements satisfying the + * selector, the first will be used. + * @param text - A text to type into a focused element. + * @param options - have property `delay` which is the Time to wait between + * key presses in milliseconds. Defaults to `0`. + * @returns + * @remarks + */ + type( + selector: string, + text: string, + options?: {delay: number} + ): Promise<void>; + type(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`. + * + * Causes your script to wait for the given number of milliseconds. + * + * @remarks + * It's generally recommended to not wait for a number of seconds, but instead + * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or + * {@link Frame.waitForFunction} to wait for exactly the conditions you want. + * + * @example + * + * Wait for 1 second: + * + * ```ts + * await page.waitForTimeout(1000); + * ``` + * + * @param milliseconds - the number of milliseconds to wait. + */ + waitForTimeout(milliseconds: number): Promise<void>; + waitForTimeout(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Wait for the `selector` to appear in page. If at the moment of calling the + * method the `selector` already exists, the method will return immediately. If + * the `selector` doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * @example + * This method works across navigations: + * + * ```ts + * import puppeteer from 'puppeteer'; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForSelector('img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param selector - A + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} + * of an element to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves when element specified by selector string + * is added to DOM. Resolves to `null` if waiting for hidden: `true` and + * selector is not found in DOM. + * @remarks + * The optional Parameter in Arguments `options` are: + * + * - `visible`: A boolean wait for element to be present in DOM and to be + * visible, i.e. to not have `display: none` or `visibility: hidden` CSS + * properties. Defaults to `false`. + * + * - `hidden`: Wait for element to not be found in the DOM or to be hidden, + * i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to + * `false`. + * + * - `timeout`: maximum time to wait for in milliseconds. Defaults to `30000` + * (30 seconds). Pass `0` to disable timeout. The default value can be changed + * by using the {@link Page.setDefaultTimeout} method. + */ + async waitForSelector<Selector extends string>( + selector: Selector, + options?: WaitForSelectorOptions + ): Promise<ElementHandle<NodeFor<Selector>> | null>; + async waitForSelector<Selector extends string>(): Promise<ElementHandle< + NodeFor<Selector> + > | null> { + throw new Error('Not implemented'); + } + + /** + * Wait for the `xpath` to appear in page. If at the moment of calling the + * method the `xpath` already exists, the method will return immediately. If + * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * @example + * This method works across navigation + * + * ```ts + * import puppeteer from 'puppeteer'; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForXPath('//img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param xpath - A + * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an + * element to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves when element specified by xpath string is + * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is + * not found in DOM, otherwise resolves to `ElementHandle`. + * @remarks + * The optional Argument `options` have properties: + * + * - `visible`: A boolean to wait for element to be present in DOM and to be + * visible, i.e. to not have `display: none` or `visibility: hidden` CSS + * properties. Defaults to `false`. + * + * - `hidden`: A boolean wait for element to not be found in the DOM or to be + * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. + * Defaults to `false`. + * + * - `timeout`: A number which is maximum time to wait for in milliseconds. + * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default + * value can be changed by using the {@link Page.setDefaultTimeout} method. + */ + waitForXPath( + xpath: string, + options?: WaitForSelectorOptions + ): Promise<ElementHandle<Node> | null>; + waitForXPath(): Promise<ElementHandle<Node> | null> { + throw new Error('Not implemented'); + } + + /** + * Waits for a function to finish evaluating in the page's context. + * + * @example + * The {@link Page.waitForFunction} can be used to observe viewport size change: + * + * ```ts + * import puppeteer from 'puppeteer'; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * const watchDog = page.waitForFunction('window.innerWidth < 100'); + * await page.setViewport({width: 50, height: 50}); + * await watchDog; + * await browser.close(); + * })(); + * ``` + * + * @example + * To pass arguments from node.js to the predicate of + * {@link Page.waitForFunction} function: + * + * ```ts + * const selector = '.foo'; + * await page.waitForFunction( + * selector => !!document.querySelector(selector), + * {}, + * selector + * ); + * ``` + * + * @example + * The predicate of {@link Page.waitForFunction} can be asynchronous too: + * + * ```ts + * const username = 'github-username'; + * await page.waitForFunction( + * async username => { + * const githubResponse = await fetch( + * `https://api.github.com/users/${username}` + * ); + * const githubUser = await githubResponse.json(); + * // show the avatar + * const img = document.createElement('img'); + * img.src = githubUser.avatar_url; + * // wait 3 seconds + * await new Promise((resolve, reject) => setTimeout(resolve, 3000)); + * img.remove(); + * }, + * {}, + * username + * ); + * ``` + * + * @param pageFunction - Function to be evaluated in browser context + * @param options - Options for configuring waiting behavior. + */ + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + options?: FrameWaitForFunctionOptions, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >(): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + throw new Error('Not implemented'); + } + + /** + * This method is typically coupled with an action that triggers a device + * request from an api such as WebBluetooth. + * + * :::caution + * + * This must be called before the device request is made. It will not return a + * currently active device prompt. + * + * ::: + * + * @example + * + * ```ts + * const [devicePrompt] = Promise.all([ + * page.waitForDevicePrompt(), + * page.click('#connect-bluetooth'), + * ]); + * await devicePrompt.select( + * await devicePrompt.waitForDevice(({name}) => name.includes('My Device')) + * ); + * ``` + */ + waitForDevicePrompt( + options?: WaitTimeoutOptions + ): Promise<DeviceRequestPrompt>; + waitForDevicePrompt(): Promise<DeviceRequestPrompt> { + throw new Error('Not implemented'); + } +} + +/** + * @internal + */ +export const supportedMetrics = new Set<string>([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', +]); + +/** + * @internal + */ +export const unitToPixels = { + px: 1, + in: 96, + cm: 37.8, + mm: 3.78, +}; + +function convertPrintParameterToInches( + parameter?: string | number, + lengthUnit: 'in' | 'cm' = 'in' +): number | undefined { + if (typeof parameter === 'undefined') { + return undefined; + } + let pixels; + if (isNumber(parameter)) { + // Treat numbers as pixel values to be aligned with phantom's paperSize. + pixels = parameter; + } else if (isString(parameter)) { + const text = parameter; + let unit = text.substring(text.length - 2).toLowerCase(); + let valueText = ''; + if (unit in unitToPixels) { + valueText = text.substring(0, text.length - 2); + } else { + // In case of unknown unit try to parse the whole parameter as number of pixels. + // This is consistent with phantom's paperSize behavior. + unit = 'px'; + valueText = text; + } + const value = Number(valueText); + assert(!isNaN(value), 'Failed to parse parameter value: ' + text); + pixels = value * unitToPixels[unit as keyof typeof unitToPixels]; + } else { + throw new Error( + 'page.pdf() Cannot handle parameter type: ' + typeof parameter + ); + } + return pixels / unitToPixels[lengthUnit]; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts b/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts new file mode 100644 index 0000000000..704c8d127f --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/api/api.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Browser.js'; +export * from './BrowserContext.js'; +export * from './Page.js'; +export * from './JSHandle.js'; +export * from './ElementHandle.js'; +export * from './HTTPResponse.js'; +export * from './HTTPRequest.js'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Accessibility.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Accessibility.ts new file mode 100644 index 0000000000..1429ecf607 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Accessibility.ts @@ -0,0 +1,577 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {ElementHandle} from '../api/ElementHandle.js'; + +import {CDPSession} from './Connection.js'; + +/** + * Represents a Node and the properties of it that are relevant to Accessibility. + * @public + */ +export interface SerializedAXNode { + /** + * The {@link https://www.w3.org/TR/wai-aria/#usage_intro | role} of the node. + */ + role: string; + /** + * A human readable name for the node. + */ + name?: string; + /** + * The current value of the node. + */ + value?: string | number; + /** + * An additional human readable description of the node. + */ + description?: string; + /** + * Any keyboard shortcuts associated with this node. + */ + keyshortcuts?: string; + /** + * A human readable alternative to the role. + */ + roledescription?: string; + /** + * A description of the current value. + */ + valuetext?: string; + disabled?: boolean; + expanded?: boolean; + focused?: boolean; + modal?: boolean; + multiline?: boolean; + /** + * Whether more than one child can be selected. + */ + multiselectable?: boolean; + readonly?: boolean; + required?: boolean; + selected?: boolean; + /** + * Whether the checkbox is checked, or in a + * {@link https://www.w3.org/TR/wai-aria-practices/examples/checkbox/checkbox-2/checkbox-2.html | mixed state}. + */ + checked?: boolean | 'mixed'; + /** + * Whether the node is checked or in a mixed state. + */ + pressed?: boolean | 'mixed'; + /** + * The level of a heading. + */ + level?: number; + valuemin?: number; + valuemax?: number; + autocomplete?: string; + haspopup?: string; + /** + * Whether and in what way this node's value is invalid. + */ + invalid?: string; + orientation?: string; + /** + * Children of this node, if there are any. + */ + children?: SerializedAXNode[]; +} + +/** + * @public + */ +export interface SnapshotOptions { + /** + * Prune uninteresting nodes from the tree. + * @defaultValue `true` + */ + interestingOnly?: boolean; + /** + * Root node to get the accessibility tree for + * @defaultValue The root node of the entire page. + */ + root?: ElementHandle<Node>; +} + +/** + * The Accessibility class provides methods for inspecting the browser's + * accessibility tree. The accessibility tree is used by assistive technology + * such as {@link https://en.wikipedia.org/wiki/Screen_reader | screen readers} or + * {@link https://en.wikipedia.org/wiki/Switch_access | switches}. + * + * @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. + * + * @public + */ +export class Accessibility { + #client: CDPSession; + + /** + * @internal + */ + constructor(client: CDPSession) { + this.#client = client; + } + + /** + * Captures the current state of the accessibility tree. + * The returned object represents the root accessible node of the page. + * + * @remarks + * + * **NOTE** The Chrome 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 + * An example of dumping the entire accessibility tree: + * + * ```ts + * const snapshot = await page.accessibility.snapshot(); + * console.log(snapshot); + * ``` + * + * @example + * An example of logging the focused node's name: + * + * ```ts + * 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; + * } + * ``` + * + * @returns An AXNode object representing the snapshot. + */ + public async snapshot( + options: SnapshotOptions = {} + ): Promise<SerializedAXNode | null> { + const {interestingOnly = true, root = null} = options; + const {nodes} = await this.#client.send('Accessibility.getFullAXTree'); + let backendNodeId: number | undefined; + if (root) { + const {node} = await this.#client.send('DOM.describeNode', { + objectId: root.id, + }); + backendNodeId = node.backendNodeId; + } + const defaultRoot = AXNode.createTree(nodes); + let needle: AXNode | null = defaultRoot; + if (backendNodeId) { + needle = defaultRoot.find(node => { + return node.payload.backendDOMNodeId === backendNodeId; + }); + if (!needle) { + return null; + } + } + if (!interestingOnly) { + return this.serializeTree(needle)[0] ?? null; + } + + const interestingNodes = new Set<AXNode>(); + this.collectInterestingNodes(interestingNodes, defaultRoot, false); + if (!interestingNodes.has(needle)) { + return null; + } + return this.serializeTree(needle, interestingNodes)[0] ?? null; + } + + private serializeTree( + node: AXNode, + interestingNodes?: Set<AXNode> + ): SerializedAXNode[] { + const children: SerializedAXNode[] = []; + for (const child of node.children) { + children.push(...this.serializeTree(child, interestingNodes)); + } + + if (interestingNodes && !interestingNodes.has(node)) { + return children; + } + + const serializedNode = node.serialize(); + if (children.length) { + serializedNode.children = children; + } + return [serializedNode]; + } + + private collectInterestingNodes( + collection: Set<AXNode>, + node: AXNode, + insideControl: boolean + ): void { + if (node.isInteresting(insideControl)) { + collection.add(node); + } + if (node.isLeafNode()) { + return; + } + insideControl = insideControl || node.isControl(); + for (const child of node.children) { + this.collectInterestingNodes(collection, child, insideControl); + } + } +} + +class AXNode { + public payload: Protocol.Accessibility.AXNode; + public children: AXNode[] = []; + + #richlyEditable = false; + #editable = false; + #focusable = false; + #hidden = false; + #name: string; + #role: string; + #ignored: boolean; + #cachedHasFocusableChild?: boolean; + + constructor(payload: Protocol.Accessibility.AXNode) { + this.payload = payload; + this.#name = this.payload.name ? this.payload.name.value : ''; + this.#role = this.payload.role ? this.payload.role.value : 'Unknown'; + this.#ignored = this.payload.ignored; + + for (const property of this.payload.properties || []) { + if (property.name === 'editable') { + this.#richlyEditable = property.value.value === 'richtext'; + this.#editable = true; + } + if (property.name === 'focusable') { + this.#focusable = property.value.value; + } + if (property.name === 'hidden') { + this.#hidden = property.value.value; + } + } + } + + #isPlainTextField(): boolean { + if (this.#richlyEditable) { + return false; + } + if (this.#editable) { + return true; + } + return this.#role === 'textbox' || this.#role === 'searchbox'; + } + + #isTextOnlyObject(): boolean { + const role = this.#role; + return role === 'LineBreak' || role === 'text' || role === 'InlineTextBox'; + } + + #hasFocusableChild(): boolean { + if (this.#cachedHasFocusableChild === undefined) { + this.#cachedHasFocusableChild = false; + for (const child of this.children) { + if (child.#focusable || child.#hasFocusableChild()) { + this.#cachedHasFocusableChild = true; + break; + } + } + } + return this.#cachedHasFocusableChild; + } + + public find(predicate: (x: AXNode) => boolean): AXNode | null { + if (predicate(this)) { + return this; + } + for (const child of this.children) { + const result = child.find(predicate); + if (result) { + return result; + } + } + return null; + } + + public isLeafNode(): boolean { + if (!this.children.length) { + return true; + } + + // These types of objects may have children that we use as internal + // implementation details, but we want to expose them as leaves to platform + // accessibility APIs because screen readers might be confused if they find + // any children. + if (this.#isPlainTextField() || this.#isTextOnlyObject()) { + return true; + } + + // Roles whose children are only presentational according to the ARIA and + // HTML5 Specs should be hidden from screen readers. + // (Note that whilst ARIA buttons can have only presentational children, HTML5 + // buttons are allowed to have content.) + switch (this.#role) { + case 'doc-cover': + case 'graphics-symbol': + case 'img': + case 'Meter': + case 'scrollbar': + case 'slider': + case 'separator': + case 'progressbar': + return true; + default: + break; + } + + // Here and below: Android heuristics + if (this.#hasFocusableChild()) { + return false; + } + if (this.#focusable && this.#name) { + return true; + } + if (this.#role === 'heading' && this.#name) { + return true; + } + return false; + } + + public isControl(): boolean { + switch (this.#role) { + case 'button': + case 'checkbox': + case 'ColorWell': + case 'combobox': + case 'DisclosureTriangle': + case 'listbox': + case 'menu': + case 'menubar': + case 'menuitem': + case 'menuitemcheckbox': + case 'menuitemradio': + case 'radio': + case 'scrollbar': + case 'searchbox': + case 'slider': + case 'spinbutton': + case 'switch': + case 'tab': + case 'textbox': + case 'tree': + case 'treeitem': + return true; + default: + return false; + } + } + + public isInteresting(insideControl: boolean): boolean { + const role = this.#role; + if (role === 'Ignored' || this.#hidden || this.#ignored) { + return false; + } + + if (this.#focusable || this.#richlyEditable) { + return true; + } + + // If it's not focusable but has a control role, then it's interesting. + if (this.isControl()) { + return true; + } + + // A non focusable child of a control is not interesting + if (insideControl) { + return false; + } + + return this.isLeafNode() && !!this.#name; + } + + public serialize(): SerializedAXNode { + const properties = new Map<string, number | string | boolean>(); + for (const property of this.payload.properties || []) { + properties.set(property.name.toLowerCase(), property.value.value); + } + if (this.payload.name) { + properties.set('name', this.payload.name.value); + } + if (this.payload.value) { + properties.set('value', this.payload.value.value); + } + if (this.payload.description) { + properties.set('description', this.payload.description.value); + } + + const node: SerializedAXNode = { + role: this.#role, + }; + + type UserStringProperty = + | 'name' + | 'value' + | 'description' + | 'keyshortcuts' + | 'roledescription' + | 'valuetext'; + + const userStringProperties: UserStringProperty[] = [ + 'name', + 'value', + 'description', + 'keyshortcuts', + 'roledescription', + 'valuetext', + ]; + const getUserStringPropertyValue = (key: UserStringProperty): string => { + return properties.get(key) as string; + }; + + for (const userStringProperty of userStringProperties) { + if (!properties.has(userStringProperty)) { + continue; + } + + node[userStringProperty] = getUserStringPropertyValue(userStringProperty); + } + + type BooleanProperty = + | 'disabled' + | 'expanded' + | 'focused' + | 'modal' + | 'multiline' + | 'multiselectable' + | 'readonly' + | 'required' + | 'selected'; + const booleanProperties: BooleanProperty[] = [ + 'disabled', + 'expanded', + 'focused', + 'modal', + 'multiline', + 'multiselectable', + 'readonly', + 'required', + 'selected', + ]; + const getBooleanPropertyValue = (key: BooleanProperty): boolean => { + return properties.get(key) as boolean; + }; + + for (const booleanProperty of booleanProperties) { + // RootWebArea's treat focus differently than other nodes. They report whether + // their frame has focus, not whether focus is specifically on the root + // node. + if (booleanProperty === 'focused' && this.#role === 'RootWebArea') { + continue; + } + const value = getBooleanPropertyValue(booleanProperty); + if (!value) { + continue; + } + node[booleanProperty] = getBooleanPropertyValue(booleanProperty); + } + + type TristateProperty = 'checked' | 'pressed'; + const tristateProperties: TristateProperty[] = ['checked', 'pressed']; + for (const tristateProperty of tristateProperties) { + if (!properties.has(tristateProperty)) { + continue; + } + const value = properties.get(tristateProperty); + node[tristateProperty] = + value === 'mixed' ? 'mixed' : value === 'true' ? true : false; + } + + type NumbericalProperty = 'level' | 'valuemax' | 'valuemin'; + const numericalProperties: NumbericalProperty[] = [ + 'level', + 'valuemax', + 'valuemin', + ]; + const getNumericalPropertyValue = (key: NumbericalProperty): number => { + return properties.get(key) as number; + }; + for (const numericalProperty of numericalProperties) { + if (!properties.has(numericalProperty)) { + continue; + } + node[numericalProperty] = getNumericalPropertyValue(numericalProperty); + } + + type TokenProperty = + | 'autocomplete' + | 'haspopup' + | 'invalid' + | 'orientation'; + const tokenProperties: TokenProperty[] = [ + 'autocomplete', + 'haspopup', + 'invalid', + 'orientation', + ]; + const getTokenPropertyValue = (key: TokenProperty): string => { + return properties.get(key) as string; + }; + for (const tokenProperty of tokenProperties) { + const value = getTokenPropertyValue(tokenProperty); + if (!value || value === 'false') { + continue; + } + node[tokenProperty] = getTokenPropertyValue(tokenProperty); + } + return node; + } + + public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode { + const nodeById = new Map<string, AXNode>(); + for (const payload of payloads) { + nodeById.set(payload.nodeId, new AXNode(payload)); + } + for (const node of nodeById.values()) { + for (const childId of node.payload.childIds || []) { + const child = nodeById.get(childId); + if (child) { + node.children.push(child); + } + } + } + return nodeById.values().next().value; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/AriaQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/AriaQueryHandler.ts new file mode 100644 index 0000000000..ac322370ba --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/AriaQueryHandler.ts @@ -0,0 +1,124 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {ElementHandle} from '../api/ElementHandle.js'; +import {assert} from '../util/assert.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; + +import {CDPSession} from './Connection.js'; +import {QueryHandler, QuerySelector} from './QueryHandler.js'; +import {AwaitableIterable} from './types.js'; + +const queryAXTree = async ( + client: CDPSession, + element: ElementHandle<Node>, + accessibleName?: string, + role?: string +): Promise<Protocol.Accessibility.AXNode[]> => { + const {nodes} = await client.send('Accessibility.queryAXTree', { + objectId: element.id, + accessibleName, + role, + }); + return nodes.filter((node: Protocol.Accessibility.AXNode) => { + return !node.role || node.role.value !== 'StaticText'; + }); +}; + +type ARIASelector = {name?: string; role?: string}; + +const KNOWN_ATTRIBUTES = Object.freeze(['name', 'role']); +const isKnownAttribute = ( + attribute: string +): attribute is keyof ARIASelector => { + return KNOWN_ATTRIBUTES.includes(attribute); +}; + +const normalizeValue = (value: string): string => { + return value.replace(/ +/g, ' ').trim(); +}; + +/** + * The selectors consist of an accessible name to query for and optionally + * further aria attributes on the form `[<attribute>=<value>]`. + * Currently, we only support the `name` and `role` attribute. + * The following examples showcase how the syntax works wrt. querying: + * + * - 'title[role="heading"]' queries for elements with name 'title' and role 'heading'. + * - '[role="img"]' queries for elements with role 'img' and any name. + * - 'label' queries for elements with name 'label' and any role. + * - '[name=""][role="button"]' queries for elements with no name and role 'button'. + */ +const ATTRIBUTE_REGEXP = + /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g; +const parseARIASelector = (selector: string): ARIASelector => { + const queryOptions: ARIASelector = {}; + const defaultName = selector.replace( + ATTRIBUTE_REGEXP, + (_, attribute, __, value) => { + attribute = attribute.trim(); + assert( + isKnownAttribute(attribute), + `Unknown aria attribute "${attribute}" in selector` + ); + queryOptions[attribute] = normalizeValue(value); + return ''; + } + ); + if (defaultName && !queryOptions.name) { + queryOptions.name = normalizeValue(defaultName); + } + return queryOptions; +}; + +/** + * @internal + */ +export class ARIAQueryHandler extends QueryHandler { + static override querySelector: QuerySelector = async ( + node, + selector, + {ariaQuerySelector} + ) => { + return ariaQuerySelector(node, selector); + }; + + static override async *queryAll( + element: ElementHandle<Node>, + selector: string + ): AwaitableIterable<ElementHandle<Node>> { + const context = element.executionContext(); + const {name, role} = parseARIASelector(selector); + const results = await queryAXTree(context._client, element, name, role); + const world = context._world!; + yield* AsyncIterableUtil.map(results, node => { + return world.adoptBackendNode(node.backendDOMNodeId) as Promise< + ElementHandle<Node> + >; + }); + } + + static override queryOne = async ( + element: ElementHandle<Node>, + selector: string + ): Promise<ElementHandle<Node> | null> => { + return ( + (await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null + ); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Binding.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Binding.ts new file mode 100644 index 0000000000..01268bbefa --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Binding.ts @@ -0,0 +1,123 @@ +import {JSHandle} from '../api/JSHandle.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {ExecutionContext} from './ExecutionContext.js'; +import {debugError} from './util.js'; + +/** + * @internal + */ +export class Binding { + #name: string; + #fn: (...args: unknown[]) => unknown; + constructor(name: string, fn: (...args: unknown[]) => unknown) { + this.#name = name; + this.#fn = fn; + } + + get name(): string { + return this.#name; + } + + /** + * @param context - Context to run the binding in; the context should have + * the binding added to it beforehand. + * @param id - ID of the call. This should come from the CDP + * `onBindingCalled` response. + * @param args - Plain arguments from CDP. + */ + async run( + context: ExecutionContext, + id: number, + args: unknown[], + isTrivial: boolean + ): Promise<void> { + const garbage = []; + try { + if (!isTrivial) { + // Getting non-trivial arguments. + const handles = await context.evaluateHandle( + (name, seq) => { + // @ts-expect-error Code is evaluated in a different context. + return globalThis[name].args.get(seq); + }, + this.#name, + id + ); + try { + const properties = await handles.getProperties(); + for (const [index, handle] of properties) { + // This is not straight-forward since some arguments can stringify, but + // aren't plain objects so add subtypes when the use-case arises. + if (index in args) { + switch (handle.remoteObject().subtype) { + case 'node': + args[+index] = handle; + break; + default: + garbage.push(handle.dispose()); + } + } else { + garbage.push(handle.dispose()); + } + } + } finally { + await handles.dispose(); + } + } + + await context.evaluate( + (name, seq, result) => { + // @ts-expect-error Code is evaluated in a different context. + const callbacks = globalThis[name].callbacks; + callbacks.get(seq).resolve(result); + callbacks.delete(seq); + }, + this.#name, + id, + await this.#fn(...args) + ); + + for (const arg of args) { + if (arg instanceof JSHandle) { + garbage.push(arg.dispose()); + } + } + } catch (error) { + if (isErrorLike(error)) { + await context + .evaluate( + (name, seq, message, stack) => { + const error = new Error(message); + error.stack = stack; + // @ts-expect-error Code is evaluated in a different context. + const callbacks = globalThis[name].callbacks; + callbacks.get(seq).reject(error); + callbacks.delete(seq); + }, + this.#name, + id, + error.message, + error.stack + ) + .catch(debugError); + } else { + await context + .evaluate( + (name, seq, error) => { + // @ts-expect-error Code is evaluated in a different context. + const callbacks = globalThis[name].callbacks; + callbacks.get(seq).reject(error); + callbacks.delete(seq); + }, + this.#name, + id, + error + ) + .catch(debugError); + } + } finally { + await Promise.all(garbage); + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Browser.ts new file mode 100644 index 0000000000..6a2b46f2e2 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Browser.ts @@ -0,0 +1,737 @@ +/** + * 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 {ChildProcess} from 'child_process'; + +import {Protocol} from 'devtools-protocol'; + +import { + Browser as BrowserBase, + BrowserCloseCallback, + TargetFilterCallback, + IsPageTargetCallback, + BrowserEmittedEvents, + BrowserContextEmittedEvents, + BrowserContextOptions, + WEB_PERMISSION_TO_PROTOCOL_PERMISSION, + WaitForTargetOptions, + Permission, +} from '../api/Browser.js'; +import {BrowserContext} from '../api/BrowserContext.js'; +import {Page} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {ChromeTargetManager} from './ChromeTargetManager.js'; +import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; +import {FirefoxTargetManager} from './FirefoxTargetManager.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {Target} from './Target.js'; +import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js'; +import {TaskQueue} from './TaskQueue.js'; +import {waitWithTimeout} from './util.js'; + +/** + * @internal + */ +export class CDPBrowser extends BrowserBase { + /** + * @internal + */ + static async _create( + product: 'firefox' | 'chrome' | undefined, + connection: Connection, + contextIds: string[], + ignoreHTTPSErrors: boolean, + defaultViewport?: Viewport | null, + process?: ChildProcess, + closeCallback?: BrowserCloseCallback, + targetFilterCallback?: TargetFilterCallback, + isPageTargetCallback?: IsPageTargetCallback + ): Promise<CDPBrowser> { + const browser = new CDPBrowser( + product, + connection, + contextIds, + ignoreHTTPSErrors, + defaultViewport, + process, + closeCallback, + targetFilterCallback, + isPageTargetCallback + ); + await browser._attach(); + return browser; + } + #ignoreHTTPSErrors: boolean; + #defaultViewport?: Viewport | null; + #process?: ChildProcess; + #connection: Connection; + #closeCallback: BrowserCloseCallback; + #targetFilterCallback: TargetFilterCallback; + #isPageTargetCallback!: IsPageTargetCallback; + #defaultContext: CDPBrowserContext; + #contexts: Map<string, CDPBrowserContext>; + #screenshotTaskQueue: TaskQueue; + #targetManager: TargetManager; + + /** + * @internal + */ + override get _targets(): Map<string, Target> { + return this.#targetManager.getAvailableTargets(); + } + + /** + * @internal + */ + constructor( + product: 'chrome' | 'firefox' | undefined, + connection: Connection, + contextIds: string[], + ignoreHTTPSErrors: boolean, + defaultViewport?: Viewport | null, + process?: ChildProcess, + closeCallback?: BrowserCloseCallback, + targetFilterCallback?: TargetFilterCallback, + isPageTargetCallback?: IsPageTargetCallback + ) { + super(); + product = product || 'chrome'; + this.#ignoreHTTPSErrors = ignoreHTTPSErrors; + this.#defaultViewport = defaultViewport; + this.#process = process; + this.#screenshotTaskQueue = new TaskQueue(); + this.#connection = connection; + this.#closeCallback = closeCallback || function (): void {}; + this.#targetFilterCallback = + targetFilterCallback || + ((): boolean => { + return true; + }); + this.#setIsPageTargetCallback(isPageTargetCallback); + if (product === 'firefox') { + this.#targetManager = new FirefoxTargetManager( + connection, + this.#createTarget, + this.#targetFilterCallback + ); + } else { + this.#targetManager = new ChromeTargetManager( + connection, + this.#createTarget, + this.#targetFilterCallback + ); + } + this.#defaultContext = new CDPBrowserContext(this.#connection, this); + this.#contexts = new Map(); + for (const contextId of contextIds) { + this.#contexts.set( + contextId, + new CDPBrowserContext(this.#connection, this, contextId) + ); + } + } + + #emitDisconnected = () => { + this.emit(BrowserEmittedEvents.Disconnected); + }; + + /** + * @internal + */ + override async _attach(): Promise<void> { + this.#connection.on( + ConnectionEmittedEvents.Disconnected, + this.#emitDisconnected + ); + this.#targetManager.on( + TargetManagerEmittedEvents.TargetAvailable, + this.#onAttachedToTarget + ); + this.#targetManager.on( + TargetManagerEmittedEvents.TargetGone, + this.#onDetachedFromTarget + ); + this.#targetManager.on( + TargetManagerEmittedEvents.TargetChanged, + this.#onTargetChanged + ); + this.#targetManager.on( + TargetManagerEmittedEvents.TargetDiscovered, + this.#onTargetDiscovered + ); + await this.#targetManager.initialize(); + } + + /** + * @internal + */ + override _detach(): void { + this.#connection.off( + ConnectionEmittedEvents.Disconnected, + this.#emitDisconnected + ); + this.#targetManager.off( + TargetManagerEmittedEvents.TargetAvailable, + this.#onAttachedToTarget + ); + this.#targetManager.off( + TargetManagerEmittedEvents.TargetGone, + this.#onDetachedFromTarget + ); + this.#targetManager.off( + TargetManagerEmittedEvents.TargetChanged, + this.#onTargetChanged + ); + this.#targetManager.off( + TargetManagerEmittedEvents.TargetDiscovered, + this.#onTargetDiscovered + ); + } + + /** + * The spawned browser process. Returns `null` if the browser instance was created with + * {@link Puppeteer.connect}. + */ + override process(): ChildProcess | null { + return this.#process ?? null; + } + + /** + * @internal + */ + _targetManager(): TargetManager { + return this.#targetManager; + } + + #setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void { + this.#isPageTargetCallback = + isPageTargetCallback || + ((target: Protocol.Target.TargetInfo): boolean => { + return ( + target.type === 'page' || + target.type === 'background_page' || + target.type === 'webview' + ); + }); + } + + /** + * @internal + */ + override _getIsPageTargetCallback(): IsPageTargetCallback | undefined { + return this.#isPageTargetCallback; + } + + /** + * Creates a new incognito browser context. This won't share cookies/cache with other + * browser contexts. + * + * @example + * + * ```ts + * (async () => { + * const browser = await puppeteer.launch(); + * // Create a new incognito browser context. + * const context = await browser.createIncognitoBrowserContext(); + * // Create a new page in a pristine context. + * const page = await context.newPage(); + * // Do stuff + * await page.goto('https://example.com'); + * })(); + * ``` + */ + override async createIncognitoBrowserContext( + options: BrowserContextOptions = {} + ): Promise<CDPBrowserContext> { + const {proxyServer, proxyBypassList} = options; + + const {browserContextId} = await this.#connection.send( + 'Target.createBrowserContext', + { + proxyServer, + proxyBypassList: proxyBypassList && proxyBypassList.join(','), + } + ); + const context = new CDPBrowserContext( + this.#connection, + this, + browserContextId + ); + this.#contexts.set(browserContextId, context); + return context; + } + + /** + * Returns an array of all open browser contexts. In a newly created browser, this will + * return a single instance of {@link BrowserContext}. + */ + override browserContexts(): CDPBrowserContext[] { + return [this.#defaultContext, ...Array.from(this.#contexts.values())]; + } + + /** + * Returns the default browser context. The default browser context cannot be closed. + */ + override defaultBrowserContext(): CDPBrowserContext { + return this.#defaultContext; + } + + /** + * @internal + */ + override async _disposeContext(contextId?: string): Promise<void> { + if (!contextId) { + return; + } + await this.#connection.send('Target.disposeBrowserContext', { + browserContextId: contextId, + }); + this.#contexts.delete(contextId); + } + + #createTarget = ( + targetInfo: Protocol.Target.TargetInfo, + session?: CDPSession + ) => { + const {browserContextId} = targetInfo; + const context = + browserContextId && this.#contexts.has(browserContextId) + ? this.#contexts.get(browserContextId) + : this.#defaultContext; + + if (!context) { + throw new Error('Missing browser context'); + } + + return new Target( + targetInfo, + session, + context, + this.#targetManager, + (isAutoAttachEmulated: boolean) => { + return this.#connection._createSession( + targetInfo, + isAutoAttachEmulated + ); + }, + this.#ignoreHTTPSErrors, + this.#defaultViewport ?? null, + this.#screenshotTaskQueue, + this.#isPageTargetCallback + ); + }; + + #onAttachedToTarget = async (target: Target) => { + if (await target._initializedPromise) { + this.emit(BrowserEmittedEvents.TargetCreated, target); + target + .browserContext() + .emit(BrowserContextEmittedEvents.TargetCreated, target); + } + }; + + #onDetachedFromTarget = async (target: Target): Promise<void> => { + target._initializedCallback(false); + target._closedCallback(); + if (await target._initializedPromise) { + this.emit(BrowserEmittedEvents.TargetDestroyed, target); + target + .browserContext() + .emit(BrowserContextEmittedEvents.TargetDestroyed, target); + } + }; + + #onTargetChanged = ({ + target, + targetInfo, + }: { + target: Target; + targetInfo: Protocol.Target.TargetInfo; + }): void => { + const previousURL = target.url(); + const wasInitialized = target._isInitialized; + target._targetInfoChanged(targetInfo); + if (wasInitialized && previousURL !== target.url()) { + this.emit(BrowserEmittedEvents.TargetChanged, target); + target + .browserContext() + .emit(BrowserContextEmittedEvents.TargetChanged, target); + } + }; + + #onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => { + this.emit('targetdiscovered', targetInfo); + }; + + /** + * The browser websocket endpoint which can be used as an argument to + * {@link Puppeteer.connect}. + * + * @returns The Browser websocket url. + * + * @remarks + * + * The format is `ws://${host}:${port}/devtools/browser/<id>`. + * + * You can find the `webSocketDebuggerUrl` from `http://${host}:${port}/json/version`. + * Learn more about the + * {@link https://chromedevtools.github.io/devtools-protocol | devtools protocol} and + * the {@link + * https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target + * | browser endpoint}. + */ + override wsEndpoint(): string { + return this.#connection.url(); + } + + /** + * Promise which resolves to a new {@link Page} object. The Page is created in + * a default browser context. + */ + override async newPage(): Promise<Page> { + return this.#defaultContext.newPage(); + } + + /** + * @internal + */ + override async _createPageInContext(contextId?: string): Promise<Page> { + const {targetId} = await this.#connection.send('Target.createTarget', { + url: 'about:blank', + browserContextId: contextId || undefined, + }); + const target = this.#targetManager.getAvailableTargets().get(targetId); + if (!target) { + throw new Error(`Missing target for page (id = ${targetId})`); + } + const initialized = await target._initializedPromise; + if (!initialized) { + throw new Error(`Failed to create target for page (id = ${targetId})`); + } + const page = await target.page(); + if (!page) { + throw new Error( + `Failed to create a page for context (id = ${contextId})` + ); + } + return page; + } + + /** + * All active targets inside the Browser. In case of multiple browser contexts, returns + * an array with all the targets in all browser contexts. + */ + override targets(): Target[] { + return Array.from( + this.#targetManager.getAvailableTargets().values() + ).filter(target => { + return target._isInitialized; + }); + } + + /** + * The target associated with the browser. + */ + override target(): Target { + const browserTarget = this.targets().find(target => { + return target.type() === 'browser'; + }); + if (!browserTarget) { + throw new Error('Browser target is not found'); + } + return browserTarget; + } + + /** + * Searches for a target in all browser contexts. + * + * @param predicate - A function to be run for every target. + * @returns The first target found that matches the `predicate` function. + * + * @example + * + * An example of finding a target for a page opened via `window.open`: + * + * ```ts + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browser.waitForTarget( + * target => target.url() === 'https://www.example.com/' + * ); + * ``` + */ + override async waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options: WaitForTargetOptions = {} + ): Promise<Target> { + const {timeout = 30000} = options; + const targetPromise = createDeferredPromise<Target | PromiseLike<Target>>(); + + this.on(BrowserEmittedEvents.TargetCreated, check); + this.on(BrowserEmittedEvents.TargetChanged, check); + try { + this.targets().forEach(check); + if (!timeout) { + return await targetPromise; + } + return await waitWithTimeout(targetPromise, 'target', timeout); + } finally { + this.off(BrowserEmittedEvents.TargetCreated, check); + this.off(BrowserEmittedEvents.TargetChanged, check); + } + + async function check(target: Target): Promise<void> { + if ((await predicate(target)) && !targetPromise.resolved()) { + targetPromise.resolve(target); + } + } + } + + /** + * An array of all open pages inside the Browser. + * + * @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 {@link Target.page}. + */ + override async pages(): Promise<Page[]> { + const contextPages = await Promise.all( + this.browserContexts().map(context => { + return context.pages(); + }) + ); + // Flatten array. + return contextPages.reduce((acc, x) => { + return acc.concat(x); + }, []); + } + + override async version(): Promise<string> { + const version = await this.#getVersion(); + return version.product; + } + + /** + * The browser's original user agent. Pages can override the browser user agent with + * {@link Page.setUserAgent}. + */ + override async userAgent(): Promise<string> { + const version = await this.#getVersion(); + return version.userAgent; + } + + override async close(): Promise<void> { + await this.#closeCallback.call(null); + this.disconnect(); + } + + override disconnect(): void { + this.#targetManager.dispose(); + this.#connection.dispose(); + this._detach(); + } + + /** + * Indicates that the browser is connected. + */ + override isConnected(): boolean { + return !this.#connection._closed; + } + + #getVersion(): Promise<Protocol.Browser.GetVersionResponse> { + return this.#connection.send('Browser.getVersion'); + } +} + +/** + * @internal + */ +export class CDPBrowserContext extends BrowserContext { + #connection: Connection; + #browser: CDPBrowser; + #id?: string; + + /** + * @internal + */ + constructor(connection: Connection, browser: CDPBrowser, contextId?: string) { + super(); + this.#connection = connection; + this.#browser = browser; + this.#id = contextId; + } + + override get id(): string | undefined { + return this.#id; + } + + /** + * An array of all active targets inside the browser context. + */ + override targets(): Target[] { + return this.#browser.targets().filter(target => { + return target.browserContext() === this; + }); + } + + /** + * This searches for a target in this specific browser context. + * + * @example + * An example of finding a target for a page opened via `window.open`: + * + * ```ts + * await page.evaluate(() => window.open('https://www.example.com/')); + * const newWindowTarget = await browserContext.waitForTarget( + * target => target.url() === 'https://www.example.com/' + * ); + * ``` + * + * @param predicate - A function to be run for every target + * @param options - An object of options. Accepts a timeout, + * which is the maximum wait time in milliseconds. + * Pass `0` to disable the timeout. Defaults to 30 seconds. + * @returns Promise which resolves to the first target found + * that matches the `predicate` function. + */ + override waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options: {timeout?: number} = {} + ): Promise<Target> { + return this.#browser.waitForTarget(target => { + return target.browserContext() === this && predicate(target); + }, options); + } + + /** + * An array of all pages inside the browser context. + * + * @returns Promise which resolves to an array of all open pages. + * Non visible pages, such as `"background_page"`, will not be listed here. + * You can find them using {@link Target.page | the target page}. + */ + override async pages(): Promise<Page[]> { + const pages = await Promise.all( + this.targets() + .filter(target => { + return ( + target.type() === 'page' || + (target.type() === 'other' && + this.#browser._getIsPageTargetCallback()?.( + target._getTargetInfo() + )) + ); + }) + .map(target => { + return target.page(); + }) + ); + return pages.filter((page): page is Page => { + return !!page; + }); + } + + /** + * Returns whether BrowserContext is incognito. + * The default browser context is the only non-incognito browser context. + * + * @remarks + * The default browser context cannot be closed. + */ + override isIncognito(): boolean { + return !!this.#id; + } + + /** + * @example + * + * ```ts + * const context = browser.defaultBrowserContext(); + * await context.overridePermissions('https://html5demos.com', [ + * 'geolocation', + * ]); + * ``` + * + * @param origin - The origin to grant permissions to, e.g. "https://example.com". + * @param permissions - An array of permissions to grant. + * All permissions that are not listed here will be automatically denied. + */ + override async overridePermissions( + origin: string, + permissions: Permission[] + ): Promise<void> { + const protocolPermissions = permissions.map(permission => { + const protocolPermission = + WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission); + if (!protocolPermission) { + throw new Error('Unknown permission: ' + permission); + } + return protocolPermission; + }); + await this.#connection.send('Browser.grantPermissions', { + origin, + browserContextId: this.#id || undefined, + permissions: protocolPermissions, + }); + } + + /** + * Clears all permission overrides for the browser context. + * + * @example + * + * ```ts + * const context = browser.defaultBrowserContext(); + * context.overridePermissions('https://example.com', ['clipboard-read']); + * // do stuff .. + * context.clearPermissionOverrides(); + * ``` + */ + override async clearPermissionOverrides(): Promise<void> { + await this.#connection.send('Browser.resetPermissions', { + browserContextId: this.#id || undefined, + }); + } + + /** + * Creates a new page in the browser context. + */ + override newPage(): Promise<Page> { + return this.#browser._createPageInContext(this.#id); + } + + /** + * The browser this browser context belongs to. + */ + override browser(): CDPBrowser { + return this.#browser; + } + + /** + * Closes the browser context. All the targets that belong to the browser context + * will be closed. + * + * @remarks + * Only incognito browser contexts can be closed. + */ + override async close(): Promise<void> { + assert(this.#id, 'Non-incognito profiles cannot be closed!'); + await this.#browser._disposeContext(this.#id); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts new file mode 100644 index 0000000000..c9916a8570 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserConnector.ts @@ -0,0 +1,176 @@ +/** + * 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 {IsPageTargetCallback, TargetFilterCallback} from '../api/Browser.js'; +import {isNode} from '../environment.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {CDPBrowser} from './Browser.js'; +import {Connection} from './Connection.js'; +import {ConnectionTransport} from './ConnectionTransport.js'; +import {getFetch} from './fetch.js'; +import type {ConnectOptions} from './Puppeteer.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {debugError} from './util.js'; +/** + * Generic browser options that can be passed when launching any browser or when + * connecting to an existing browser instance. + * @public + */ +export interface BrowserConnectOptions { + /** + * Whether to ignore HTTPS errors during navigation. + * @defaultValue `false` + */ + ignoreHTTPSErrors?: boolean; + /** + * Sets the viewport for each page. + */ + defaultViewport?: Viewport | null; + /** + * Slows down Puppeteer operations by the specified amount of milliseconds to + * aid debugging. + */ + slowMo?: number; + /** + * Callback to decide if Puppeteer should connect to a given target or not. + */ + targetFilter?: TargetFilterCallback; + /** + * @internal + */ + _isPageTarget?: IsPageTargetCallback; + /** + * @defaultValue 'cdp' + * @internal + */ + protocol?: 'cdp' | 'webDriverBiDi'; + /** + * Timeout setting for individual protocol (CDP) calls. + * + * @defaultValue `180_000` + */ + protocolTimeout?: number; +} + +const getWebSocketTransportClass = async () => { + return isNode + ? (await import('./NodeWebSocketTransport.js')).NodeWebSocketTransport + : (await import('./BrowserWebSocketTransport.js')) + .BrowserWebSocketTransport; +}; + +/** + * Users should never call this directly; it's called when calling + * `puppeteer.connect`. + * + * @internal + */ +export async function _connectToCDPBrowser( + options: BrowserConnectOptions & ConnectOptions +): Promise<CDPBrowser> { + const { + browserWSEndpoint, + browserURL, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + transport, + headers = {}, + slowMo = 0, + targetFilter, + _isPageTarget: isPageTarget, + protocolTimeout, + } = options; + + assert( + Number(!!browserWSEndpoint) + Number(!!browserURL) + Number(!!transport) === + 1, + 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' + ); + + let connection!: Connection; + if (transport) { + connection = new Connection('', transport, slowMo, protocolTimeout); + } else if (browserWSEndpoint) { + const WebSocketClass = await getWebSocketTransportClass(); + const connectionTransport: ConnectionTransport = + await WebSocketClass.create(browserWSEndpoint, headers); + connection = new Connection( + browserWSEndpoint, + connectionTransport, + slowMo, + protocolTimeout + ); + } else if (browserURL) { + const connectionURL = await getWSEndpoint(browserURL); + const WebSocketClass = await getWebSocketTransportClass(); + const connectionTransport: ConnectionTransport = + await WebSocketClass.create(connectionURL); + connection = new Connection( + connectionURL, + connectionTransport, + slowMo, + protocolTimeout + ); + } + const version = await connection.send('Browser.getVersion'); + + const product = version.product.toLowerCase().includes('firefox') + ? 'firefox' + : 'chrome'; + + const {browserContextIds} = await connection.send( + 'Target.getBrowserContexts' + ); + const browser = await CDPBrowser._create( + product || 'chrome', + connection, + browserContextIds, + ignoreHTTPSErrors, + defaultViewport, + undefined, + () => { + return connection.send('Browser.close').catch(debugError); + }, + targetFilter, + isPageTarget + ); + return browser; +} + +async function getWSEndpoint(browserURL: string): Promise<string> { + const endpointURL = new URL('/json/version', browserURL); + + const fetch = await getFetch(); + try { + const result = await fetch(endpointURL.toString(), { + method: 'GET', + }); + if (!result.ok) { + throw new Error(`HTTP ${result.statusText}`); + } + const data = await result.json(); + return data.webSocketDebuggerUrl; + } catch (error) { + if (isErrorLike(error)) { + error.message = + `Failed to fetch browser webSocket URL from ${endpointURL}: ` + + error.message; + } + throw error; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts new file mode 100644 index 0000000000..5fe32e6526 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/BrowserWebSocketTransport.ts @@ -0,0 +1,60 @@ +/** + * 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 {ConnectionTransport} from './ConnectionTransport.js'; + +/** + * @internal + */ +export class BrowserWebSocketTransport implements ConnectionTransport { + static create(url: string): Promise<BrowserWebSocketTransport> { + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + + ws.addEventListener('open', () => { + return resolve(new BrowserWebSocketTransport(ws)); + }); + ws.addEventListener('error', reject); + }); + } + + #ws: WebSocket; + onmessage?: (message: string) => void; + onclose?: () => void; + + constructor(ws: WebSocket) { + this.#ws = ws; + this.#ws.addEventListener('message', event => { + if (this.onmessage) { + this.onmessage.call(null, event.data); + } + }); + this.#ws.addEventListener('close', () => { + if (this.onclose) { + this.onclose.call(null); + } + }); + // Silently ignore all errors - we don't know what to do with them. + this.#ws.addEventListener('error', () => {}); + } + + send(message: string): void { + this.#ws.send(message); + } + + close(): void { + this.#ws.close(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ChromeTargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ChromeTargetManager.ts new file mode 100644 index 0000000000..58c8353aad --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ChromeTargetManager.ts @@ -0,0 +1,401 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; + +import {TargetFilterCallback} from '../api/Browser.js'; +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {CDPSession, Connection} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {Target} from './Target.js'; +import { + TargetInterceptor, + TargetFactory, + TargetManager, + TargetManagerEmittedEvents, +} from './TargetManager.js'; +import {debugError} from './util.js'; + +/** + * ChromeTargetManager uses the CDP's auto-attach mechanism to intercept + * new targets and allow the rest of Puppeteer to configure listeners while + * the target is paused. + * + * @internal + */ +export class ChromeTargetManager extends EventEmitter implements TargetManager { + #connection: Connection; + /** + * Keeps track of the following events: 'Target.targetCreated', + * 'Target.targetDestroyed', 'Target.targetInfoChanged'. + * + * A target becomes discovered when 'Target.targetCreated' is received. + * A target is removed from this map once 'Target.targetDestroyed' is + * received. + * + * `targetFilterCallback` has no effect on this map. + */ + #discoveredTargetsByTargetId: Map<string, Protocol.Target.TargetInfo> = + new Map(); + /** + * A target is added to this map once ChromeTargetManager has created + * a Target and attached at least once to it. + */ + #attachedTargetsByTargetId: Map<string, Target> = new Map(); + /** + * Tracks which sessions attach to which target. + */ + #attachedTargetsBySessionId: Map<string, Target> = new Map(); + /** + * If a target was filtered out by `targetFilterCallback`, we still receive + * events about it from CDP, but we don't forward them to the rest of Puppeteer. + */ + #ignoredTargets = new Set<string>(); + #targetFilterCallback: TargetFilterCallback | undefined; + #targetFactory: TargetFactory; + + #targetInterceptors: WeakMap<CDPSession | Connection, TargetInterceptor[]> = + new WeakMap(); + + #attachedToTargetListenersBySession: WeakMap< + CDPSession | Connection, + (event: Protocol.Target.AttachedToTargetEvent) => Promise<void> + > = new WeakMap(); + #detachedFromTargetListenersBySession: WeakMap< + CDPSession | Connection, + (event: Protocol.Target.DetachedFromTargetEvent) => void + > = new WeakMap(); + + #initializePromise = createDeferredPromise<void>(); + #targetsIdsForInit: Set<string> = new Set(); + + constructor( + connection: Connection, + targetFactory: TargetFactory, + targetFilterCallback?: TargetFilterCallback + ) { + super(); + this.#connection = connection; + this.#targetFilterCallback = targetFilterCallback; + this.#targetFactory = targetFactory; + + this.#connection.on('Target.targetCreated', this.#onTargetCreated); + this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed); + this.#connection.on('Target.targetInfoChanged', this.#onTargetInfoChanged); + this.#connection.on('sessiondetached', this.#onSessionDetached); + this.#setupAttachmentListeners(this.#connection); + + this.#connection + .send('Target.setDiscoverTargets', { + discover: true, + filter: [{type: 'tab', exclude: true}, {}], + }) + .then(this.#storeExistingTargetsForInit) + .catch(debugError); + } + + #storeExistingTargetsForInit = () => { + for (const [ + targetId, + targetInfo, + ] of this.#discoveredTargetsByTargetId.entries()) { + if ( + (!this.#targetFilterCallback || + this.#targetFilterCallback(targetInfo)) && + targetInfo.type !== 'browser' + ) { + this.#targetsIdsForInit.add(targetId); + } + } + }; + + async initialize(): Promise<void> { + await this.#connection.send('Target.setAutoAttach', { + waitForDebuggerOnStart: true, + flatten: true, + autoAttach: true, + }); + this.#finishInitializationIfReady(); + await this.#initializePromise; + } + + dispose(): void { + this.#connection.off('Target.targetCreated', this.#onTargetCreated); + this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed); + this.#connection.off('Target.targetInfoChanged', this.#onTargetInfoChanged); + this.#connection.off('sessiondetached', this.#onSessionDetached); + + this.#removeAttachmentListeners(this.#connection); + } + + getAvailableTargets(): Map<string, Target> { + return this.#attachedTargetsByTargetId; + } + + addTargetInterceptor( + session: CDPSession | Connection, + interceptor: TargetInterceptor + ): void { + const interceptors = this.#targetInterceptors.get(session) || []; + interceptors.push(interceptor); + this.#targetInterceptors.set(session, interceptors); + } + + removeTargetInterceptor( + client: CDPSession | Connection, + interceptor: TargetInterceptor + ): void { + const interceptors = this.#targetInterceptors.get(client) || []; + this.#targetInterceptors.set( + client, + interceptors.filter(currentInterceptor => { + return currentInterceptor !== interceptor; + }) + ); + } + + #setupAttachmentListeners(session: CDPSession | Connection): void { + const listener = (event: Protocol.Target.AttachedToTargetEvent) => { + return this.#onAttachedToTarget(session, event); + }; + assert(!this.#attachedToTargetListenersBySession.has(session)); + this.#attachedToTargetListenersBySession.set(session, listener); + session.on('Target.attachedToTarget', listener); + + const detachedListener = ( + event: Protocol.Target.DetachedFromTargetEvent + ) => { + return this.#onDetachedFromTarget(session, event); + }; + assert(!this.#detachedFromTargetListenersBySession.has(session)); + this.#detachedFromTargetListenersBySession.set(session, detachedListener); + session.on('Target.detachedFromTarget', detachedListener); + } + + #removeAttachmentListeners(session: CDPSession | Connection): void { + if (this.#attachedToTargetListenersBySession.has(session)) { + session.off( + 'Target.attachedToTarget', + this.#attachedToTargetListenersBySession.get(session)! + ); + this.#attachedToTargetListenersBySession.delete(session); + } + + if (this.#detachedFromTargetListenersBySession.has(session)) { + session.off( + 'Target.detachedFromTarget', + this.#detachedFromTargetListenersBySession.get(session)! + ); + this.#detachedFromTargetListenersBySession.delete(session); + } + } + + #onSessionDetached = (session: CDPSession) => { + this.#removeAttachmentListeners(session); + this.#targetInterceptors.delete(session); + }; + + #onTargetCreated = async (event: Protocol.Target.TargetCreatedEvent) => { + this.#discoveredTargetsByTargetId.set( + event.targetInfo.targetId, + event.targetInfo + ); + + this.emit(TargetManagerEmittedEvents.TargetDiscovered, event.targetInfo); + + // The connection is already attached to the browser target implicitly, + // therefore, no new CDPSession is created and we have special handling + // here. + if (event.targetInfo.type === 'browser' && event.targetInfo.attached) { + if (this.#attachedTargetsByTargetId.has(event.targetInfo.targetId)) { + return; + } + const target = this.#targetFactory(event.targetInfo, undefined); + this.#attachedTargetsByTargetId.set(event.targetInfo.targetId, target); + } + }; + + #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => { + const targetInfo = this.#discoveredTargetsByTargetId.get(event.targetId); + this.#discoveredTargetsByTargetId.delete(event.targetId); + this.#finishInitializationIfReady(event.targetId); + if ( + targetInfo?.type === 'service_worker' && + this.#attachedTargetsByTargetId.has(event.targetId) + ) { + // Special case for service workers: report TargetGone event when + // the worker is destroyed. + const target = this.#attachedTargetsByTargetId.get(event.targetId); + this.emit(TargetManagerEmittedEvents.TargetGone, target); + this.#attachedTargetsByTargetId.delete(event.targetId); + } + }; + + #onTargetInfoChanged = (event: Protocol.Target.TargetInfoChangedEvent) => { + this.#discoveredTargetsByTargetId.set( + event.targetInfo.targetId, + event.targetInfo + ); + + if ( + this.#ignoredTargets.has(event.targetInfo.targetId) || + !this.#attachedTargetsByTargetId.has(event.targetInfo.targetId) || + !event.targetInfo.attached + ) { + return; + } + + const target = this.#attachedTargetsByTargetId.get( + event.targetInfo.targetId + ); + this.emit(TargetManagerEmittedEvents.TargetChanged, { + target: target!, + targetInfo: event.targetInfo, + }); + }; + + #onAttachedToTarget = async ( + parentSession: Connection | CDPSession, + event: Protocol.Target.AttachedToTargetEvent + ) => { + const targetInfo = event.targetInfo; + const session = this.#connection.session(event.sessionId); + if (!session) { + throw new Error(`Session ${event.sessionId} was not created.`); + } + + const silentDetach = async () => { + await session.send('Runtime.runIfWaitingForDebugger').catch(debugError); + // We don't use `session.detach()` because that dispatches all commands on + // the connection instead of the parent session. + await parentSession + .send('Target.detachFromTarget', { + sessionId: session.id(), + }) + .catch(debugError); + }; + + if (!this.#connection.isAutoAttached(targetInfo.targetId)) { + return; + } + + // Special case for service workers: being attached to service workers will + // prevent them from ever being destroyed. Therefore, we silently detach + // from service workers unless the connection was manually created via + // `page.worker()`. To determine this, we use + // `this.#connection.isAutoAttached(targetInfo.targetId)`. In the future, we + // should determine if a target is auto-attached or not with the help of + // CDP. + if ( + targetInfo.type === 'service_worker' && + this.#connection.isAutoAttached(targetInfo.targetId) + ) { + this.#finishInitializationIfReady(targetInfo.targetId); + await silentDetach(); + if (this.#attachedTargetsByTargetId.has(targetInfo.targetId)) { + return; + } + const target = this.#targetFactory(targetInfo); + this.#attachedTargetsByTargetId.set(targetInfo.targetId, target); + this.emit(TargetManagerEmittedEvents.TargetAvailable, target); + return; + } + + if (this.#targetFilterCallback && !this.#targetFilterCallback(targetInfo)) { + this.#ignoredTargets.add(targetInfo.targetId); + this.#finishInitializationIfReady(targetInfo.targetId); + await silentDetach(); + return; + } + + const existingTarget = this.#attachedTargetsByTargetId.has( + targetInfo.targetId + ); + + const target = existingTarget + ? this.#attachedTargetsByTargetId.get(targetInfo.targetId)! + : this.#targetFactory(targetInfo, session); + + this.#setupAttachmentListeners(session); + + if (existingTarget) { + this.#attachedTargetsBySessionId.set( + session.id(), + this.#attachedTargetsByTargetId.get(targetInfo.targetId)! + ); + } else { + this.#attachedTargetsByTargetId.set(targetInfo.targetId, target); + this.#attachedTargetsBySessionId.set(session.id(), target); + } + + for (const interceptor of this.#targetInterceptors.get(parentSession) || + []) { + if (!(parentSession instanceof Connection)) { + // Sanity check: if parent session is not a connection, it should be + // present in #attachedTargetsBySessionId. + assert(this.#attachedTargetsBySessionId.has(parentSession.id())); + } + interceptor( + target, + parentSession instanceof Connection + ? null + : this.#attachedTargetsBySessionId.get(parentSession.id())! + ); + } + + this.#targetsIdsForInit.delete(target._targetId); + if (!existingTarget) { + this.emit(TargetManagerEmittedEvents.TargetAvailable, target); + } + this.#finishInitializationIfReady(); + + // TODO: the browser might be shutting down here. What do we do with the + // error? + await Promise.all([ + session.send('Target.setAutoAttach', { + waitForDebuggerOnStart: true, + flatten: true, + autoAttach: true, + }), + session.send('Runtime.runIfWaitingForDebugger'), + ]).catch(debugError); + }; + + #finishInitializationIfReady(targetId?: string): void { + targetId !== undefined && this.#targetsIdsForInit.delete(targetId); + if (this.#targetsIdsForInit.size === 0) { + this.#initializePromise.resolve(); + } + } + + #onDetachedFromTarget = ( + _parentSession: Connection | CDPSession, + event: Protocol.Target.DetachedFromTargetEvent + ) => { + const target = this.#attachedTargetsBySessionId.get(event.sessionId); + + this.#attachedTargetsBySessionId.delete(event.sessionId); + + if (!target) { + return; + } + + this.#attachedTargetsByTargetId.delete(target._targetId); + this.emit(TargetManagerEmittedEvents.TargetGone, target); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts new file mode 100644 index 0000000000..6db94c3452 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Configuration.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Product} from './Product.js'; + +/** + * Defines experiment options for Puppeteer. + * + * See individual properties for more information. + * + * @public + */ +export type ExperimentsConfiguration = Record<string, never>; + +/** + * Defines options to configure Puppeteer's behavior during installation and + * runtime. + * + * See individual properties for more information. + * + * @public + */ +export interface Configuration { + /** + * Specifies a certain version of the browser you'd like Puppeteer to use. + * + * Can be overridden by `PUPPETEER_BROWSER_REVISION`. + * + * See {@link PuppeteerNode.launch | puppeteer.launch} on how executable path + * is inferred. + * + * @defaultValue A compatible-revision of the browser. + */ + browserRevision?: string; + /** + * Defines the directory to be used by Puppeteer for caching. + * + * Can be overridden by `PUPPETEER_CACHE_DIR`. + * + * @defaultValue `path.join(os.homedir(), '.cache', 'puppeteer')` + */ + cacheDirectory?: string; + /** + * Specifies the URL prefix that is used to download the browser. + * + * Can be overridden by `PUPPETEER_DOWNLOAD_HOST`. + * + * @remarks + * This must include the protocol and may even need a path prefix. + * + * @defaultValue Either https://storage.googleapis.com or + * https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central, + * depending on the product. + */ + downloadHost?: string; + /** + * Specifies the path for the downloads folder. + * + * Can be overridden by `PUPPETEER_DOWNLOAD_PATH`. + * + * @defaultValue `<cache>/<product>` where `<cache>` is Puppeteer's cache + * directory and `<product>` is the name of the browser. + */ + downloadPath?: string; + /** + * Specifies an executable path to be used in + * {@link PuppeteerNode.launch | puppeteer.launch}. + * + * Can be overridden by `PUPPETEER_EXECUTABLE_PATH`. + * + * @defaultValue **Auto-computed.** + */ + executablePath?: string; + /** + * Specifies which browser you'd like Puppeteer to use. + * + * Can be overridden by `PUPPETEER_PRODUCT`. + * + * @defaultValue `chrome` + */ + defaultProduct?: Product; + /** + * Defines the directory to be used by Puppeteer for creating temporary files. + * + * Can be overridden by `PUPPETEER_TMP_DIR`. + * + * @defaultValue `os.tmpdir()` + */ + temporaryDirectory?: string; + /** + * Tells Puppeteer to not download during installation. + * + * Can be overridden by `PUPPETEER_SKIP_DOWNLOAD`. + */ + skipDownload?: boolean; + /** + * Tells Puppeteer to log at the given level. + * + * At the moment, any option silences logging. + * + * @defaultValue `undefined` + */ + logLevel?: 'silent' | 'error' | 'warn'; + /** + * Defines experimental options for Puppeteer. + */ + experiments?: ExperimentsConfiguration; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Connection.ts new file mode 100644 index 0000000000..8b75848347 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Connection.ts @@ -0,0 +1,615 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; +import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; + +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/util.js'; + +import {ConnectionTransport} from './ConnectionTransport.js'; +import {debug} from './Debug.js'; +import {ProtocolError} from './Errors.js'; +import {EventEmitter} from './EventEmitter.js'; + +const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); +const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀'); + +/** + * @public + */ +export {ConnectionTransport, ProtocolMapping}; + +/** + * Internal events that the Connection class emits. + * + * @internal + */ +export const ConnectionEmittedEvents = { + Disconnected: Symbol('Connection.Disconnected'), +} as const; + +/** + * @internal + */ +type GetIdFn = () => number; + +/** + * @internal + */ +function createIncrementalIdGenerator(): GetIdFn { + let id = 0; + return (): number => { + return ++id; + }; +} + +/** + * @internal + */ +class Callback { + #id: number; + #error = new ProtocolError(); + #promise = createDeferredPromise<unknown>(); + #timer?: ReturnType<typeof setTimeout>; + #label: string; + + constructor(id: number, label: string, timeout?: number) { + this.#id = id; + this.#label = label; + if (timeout) { + this.#timer = setTimeout(() => { + this.#promise.reject( + rewriteError( + this.#error, + `${label} timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.` + ) + ); + }, timeout); + } + } + + resolve(value: unknown): void { + clearTimeout(this.#timer); + this.#promise.resolve(value); + } + + reject(error: Error): void { + clearTimeout(this.#timer); + this.#promise.reject(error); + } + + get id(): number { + return this.#id; + } + + get promise(): Promise<unknown> { + return this.#promise; + } + + get error(): ProtocolError { + return this.#error; + } + + get label(): string { + return this.#label; + } +} + +/** + * Manages callbacks and their IDs for the protocol request/response communication. + * + * @internal + */ +export class CallbackRegistry { + #callbacks: Map<number, Callback> = new Map(); + #idGenerator = createIncrementalIdGenerator(); + + create( + label: string, + timeout: number | undefined, + request: (id: number) => void + ): Promise<unknown> { + const callback = new Callback(this.#idGenerator(), label, timeout); + this.#callbacks.set(callback.id, callback); + try { + request(callback.id); + } catch (error) { + // We still throw sync errors synchronously and clean up the scheduled + // callback. + callback.promise.catch(() => { + this.#callbacks.delete(callback.id); + }); + callback.reject(error as Error); + throw error; + } + // Must only have sync code up until here. + return callback.promise.finally(() => { + this.#callbacks.delete(callback.id); + }); + } + + reject(id: number, message: string, originalMessage?: string): void { + const callback = this.#callbacks.get(id); + if (!callback) { + return; + } + this._reject(callback, message, originalMessage); + } + + _reject(callback: Callback, message: string, originalMessage?: string): void { + callback.reject( + rewriteError( + callback.error, + `Protocol error (${callback.label}): ${message}`, + originalMessage + ) + ); + } + + resolve(id: number, value: unknown): void { + const callback = this.#callbacks.get(id); + if (!callback) { + return; + } + callback.resolve(value); + } + + clear(): void { + for (const callback of this.#callbacks.values()) { + // TODO: probably we can accept error messages as params. + this._reject(callback, 'Target closed'); + } + this.#callbacks.clear(); + } +} + +/** + * @public + */ +export class Connection extends EventEmitter { + #url: string; + #transport: ConnectionTransport; + #delay: number; + #timeout: number; + #sessions: Map<string, CDPSessionImpl> = new Map(); + #closed = false; + #manuallyAttached = new Set<string>(); + #callbacks = new CallbackRegistry(); + + constructor( + url: string, + transport: ConnectionTransport, + delay = 0, + timeout?: number + ) { + super(); + this.#url = url; + this.#delay = delay; + this.#timeout = timeout ?? 180_000; + + this.#transport = transport; + this.#transport.onmessage = this.onMessage.bind(this); + this.#transport.onclose = this.#onClose.bind(this); + } + + static fromSession(session: CDPSession): Connection | undefined { + return session.connection(); + } + + get timeout(): number { + return this.#timeout; + } + + /** + * @internal + */ + get _closed(): boolean { + return this.#closed; + } + + /** + * @internal + */ + get _sessions(): Map<string, CDPSession> { + return this.#sessions; + } + + /** + * @param sessionId - The session id + * @returns The current CDP session if it exists + */ + session(sessionId: string): CDPSession | null { + return this.#sessions.get(sessionId) || null; + } + + url(): string { + return this.#url; + } + + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + // There is only ever 1 param arg passed, but the Protocol defines it as an + // array of 0 or 1 items See this comment: + // https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285 + // which explains why the protocol defines the params this way for better + // type-inference. + // So now we check if there are any params or not and deal with them accordingly. + const params = paramArgs.length ? paramArgs[0] : undefined; + return this._rawSend(this.#callbacks, method, params); + } + + /** + * @internal + */ + _rawSend<T extends keyof ProtocolMapping.Commands>( + callbacks: CallbackRegistry, + method: T, + params: ProtocolMapping.Commands[T]['paramsType'][0], + sessionId?: string + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + return callbacks.create(method, this.#timeout, id => { + const stringifiedMessage = JSON.stringify({ + method, + params, + id, + sessionId, + }); + debugProtocolSend(stringifiedMessage); + this.#transport.send(stringifiedMessage); + }) as Promise<ProtocolMapping.Commands[T]['returnType']>; + } + + /** + * @internal + */ + async closeBrowser(): Promise<void> { + await this.send('Browser.close'); + } + + /** + * @internal + */ + protected async onMessage(message: string): Promise<void> { + if (this.#delay) { + await new Promise(f => { + return setTimeout(f, this.#delay); + }); + } + debugProtocolReceive(message); + const object = JSON.parse(message); + if (object.method === 'Target.attachedToTarget') { + const sessionId = object.params.sessionId; + const session = new CDPSessionImpl( + this, + object.params.targetInfo.type, + sessionId + ); + this.#sessions.set(sessionId, session); + this.emit('sessionattached', session); + const parentSession = this.#sessions.get(object.sessionId); + if (parentSession) { + parentSession.emit('sessionattached', session); + } + } else if (object.method === 'Target.detachedFromTarget') { + const session = this.#sessions.get(object.params.sessionId); + if (session) { + session._onClosed(); + this.#sessions.delete(object.params.sessionId); + this.emit('sessiondetached', session); + const parentSession = this.#sessions.get(object.sessionId); + if (parentSession) { + parentSession.emit('sessiondetached', session); + } + } + } + if (object.sessionId) { + const session = this.#sessions.get(object.sessionId); + if (session) { + session._onMessage(object); + } + } else if (object.id) { + if (object.error) { + this.#callbacks.reject( + object.id, + createProtocolErrorMessage(object), + object.error.message + ); + } else { + this.#callbacks.resolve(object.id, object.result); + } + } else { + this.emit(object.method, object.params); + } + } + + #onClose(): void { + if (this.#closed) { + return; + } + this.#closed = true; + this.#transport.onmessage = undefined; + this.#transport.onclose = undefined; + this.#callbacks.clear(); + for (const session of this.#sessions.values()) { + session._onClosed(); + } + this.#sessions.clear(); + this.emit(ConnectionEmittedEvents.Disconnected); + } + + dispose(): void { + this.#onClose(); + this.#transport.close(); + } + + /** + * @internal + */ + isAutoAttached(targetId: string): boolean { + return !this.#manuallyAttached.has(targetId); + } + + /** + * @internal + */ + async _createSession( + targetInfo: Protocol.Target.TargetInfo, + isAutoAttachEmulated = true + ): Promise<CDPSession> { + if (!isAutoAttachEmulated) { + this.#manuallyAttached.add(targetInfo.targetId); + } + const {sessionId} = await this.send('Target.attachToTarget', { + targetId: targetInfo.targetId, + flatten: true, + }); + this.#manuallyAttached.delete(targetInfo.targetId); + const session = this.#sessions.get(sessionId); + if (!session) { + throw new Error('CDPSession creation failed.'); + } + return session; + } + + /** + * @param targetInfo - The target info + * @returns The CDP session that is created + */ + async createSession( + targetInfo: Protocol.Target.TargetInfo + ): Promise<CDPSession> { + return await this._createSession(targetInfo, false); + } +} + +/** + * @public + */ +export interface CDPSessionOnMessageObject { + id?: number; + method: string; + params: Record<string, unknown>; + error: {message: string; data: any; code: number}; + result?: any; +} + +/** + * Internal events that the CDPSession class emits. + * + * @internal + */ +export const CDPSessionEmittedEvents = { + Disconnected: Symbol('CDPSession.Disconnected'), +} as const; + +/** + * The `CDPSession` instances are used to talk raw Chrome Devtools Protocol. + * + * @remarks + * + * Protocol methods can be called with {@link CDPSession.send} method and protocol + * events can be subscribed to with `CDPSession.on` method. + * + * Useful links: {@link https://chromedevtools.github.io/devtools-protocol/ | DevTools Protocol Viewer} + * and {@link https://github.com/aslushnikov/getting-started-with-cdp/blob/HEAD/README.md | Getting Started with DevTools Protocol}. + * + * @example + * + * ```ts + * const client = await page.target().createCDPSession(); + * await client.send('Animation.enable'); + * client.on('Animation.animationCreated', () => + * console.log('Animation created!') + * ); + * const response = await client.send('Animation.getPlaybackRate'); + * console.log('playback rate is ' + response.playbackRate); + * await client.send('Animation.setPlaybackRate', { + * playbackRate: response.playbackRate / 2, + * }); + * ``` + * + * @public + */ +export class CDPSession extends EventEmitter { + /** + * @internal + */ + constructor() { + super(); + } + + connection(): Connection | undefined { + throw new Error('Not implemented'); + } + + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']>; + send<T extends keyof ProtocolMapping.Commands>(): Promise< + ProtocolMapping.Commands[T]['returnType'] + > { + throw new Error('Not implemented'); + } + + /** + * Detaches the cdpSession from the target. Once detached, the cdpSession object + * won't emit any events and can't be used to send messages. + */ + async detach(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * Returns the session's id. + */ + id(): string { + throw new Error('Not implemented'); + } +} + +/** + * @internal + */ +export class CDPSessionImpl extends CDPSession { + #sessionId: string; + #targetType: string; + #callbacks = new CallbackRegistry(); + #connection?: Connection; + + /** + * @internal + */ + constructor(connection: Connection, targetType: string, sessionId: string) { + super(); + this.#connection = connection; + this.#targetType = targetType; + this.#sessionId = sessionId; + } + + override connection(): Connection | undefined { + return this.#connection; + } + + override send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + if (!this.#connection) { + return Promise.reject( + new Error( + `Protocol error (${method}): Session closed. Most likely the ${ + this.#targetType + } has been closed.` + ) + ); + } + // See the comment in Connection#send explaining why we do this. + const params = paramArgs.length ? paramArgs[0] : undefined; + return this.#connection._rawSend( + this.#callbacks, + method, + params, + this.#sessionId + ); + } + + /** + * @internal + */ + _onMessage(object: CDPSessionOnMessageObject): void { + if (object.id) { + if (object.error) { + this.#callbacks.reject( + object.id, + createProtocolErrorMessage(object), + object.error.message + ); + } else { + this.#callbacks.resolve(object.id, object.result); + } + } else { + assert(!object.id); + this.emit(object.method, object.params); + } + } + + /** + * Detaches the cdpSession from the target. Once detached, the cdpSession object + * won't emit any events and can't be used to send messages. + */ + override async detach(): Promise<void> { + if (!this.#connection) { + throw new Error( + `Session already detached. Most likely the ${ + this.#targetType + } has been closed.` + ); + } + await this.#connection.send('Target.detachFromTarget', { + sessionId: this.#sessionId, + }); + } + + /** + * @internal + */ + _onClosed(): void { + this.#callbacks.clear(); + this.#connection = undefined; + this.emit(CDPSessionEmittedEvents.Disconnected); + } + + /** + * Returns the session's id. + */ + override id(): string { + return this.#sessionId; + } +} + +function createProtocolErrorMessage(object: { + error: {message: string; data: any; code: number}; +}): string { + let message = `${object.error.message}`; + if ('data' in object.error) { + message += ` ${object.error.data}`; + } + return message; +} + +function rewriteError( + error: ProtocolError, + message: string, + originalMessage?: string +): Error { + error.message = message; + error.originalMessage = originalMessage ?? error.originalMessage; + return error; +} + +/** + * @internal + */ +export function isTargetClosedError(err: Error): boolean { + return ( + err.message.includes('Target closed') || + err.message.includes('Session closed') + ); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts new file mode 100644 index 0000000000..753379fd56 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConnectionTransport.ts @@ -0,0 +1,25 @@ +/** + * 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. + */ + +/** + * @public + */ +export interface ConnectionTransport { + send(message: string): void; + close(): void; + onmessage?: (message: string) => void; + onclose?: () => void; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts new file mode 100644 index 0000000000..9e911c8149 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ConsoleMessage.ts @@ -0,0 +1,123 @@ +/** + * 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 {JSHandle} from '../api/JSHandle.js'; + +/** + * @public + */ +export interface ConsoleMessageLocation { + /** + * URL of the resource if known or `undefined` otherwise. + */ + url?: string; + + /** + * 0-based line number in the resource if known or `undefined` otherwise. + */ + lineNumber?: number; + + /** + * 0-based column number in the resource if known or `undefined` otherwise. + */ + columnNumber?: number; +} + +/** + * The supported types for console messages. + * @public + */ +export type ConsoleMessageType = + | 'log' + | 'debug' + | 'info' + | 'error' + | 'warning' + | 'dir' + | 'dirxml' + | 'table' + | 'trace' + | 'clear' + | 'startGroup' + | 'startGroupCollapsed' + | 'endGroup' + | 'assert' + | 'profile' + | 'profileEnd' + | 'count' + | 'timeEnd' + | 'verbose'; + +/** + * ConsoleMessage objects are dispatched by page via the 'console' event. + * @public + */ +export class ConsoleMessage { + #type: ConsoleMessageType; + #text: string; + #args: JSHandle[]; + #stackTraceLocations: ConsoleMessageLocation[]; + + /** + * @public + */ + constructor( + type: ConsoleMessageType, + text: string, + args: JSHandle[], + stackTraceLocations: ConsoleMessageLocation[] + ) { + this.#type = type; + this.#text = text; + this.#args = args; + this.#stackTraceLocations = stackTraceLocations; + } + + /** + * The type of the console message. + */ + type(): ConsoleMessageType { + return this.#type; + } + + /** + * The text of the console message. + */ + text(): string { + return this.#text; + } + + /** + * An array of arguments passed to the console. + */ + args(): JSHandle[] { + return this.#args; + } + + /** + * The location of the console message. + */ + location(): ConsoleMessageLocation { + return this.#stackTraceLocations[0] ?? {}; + } + + /** + * The array of locations on the stack of the console message. + */ + stackTrace(): ConsoleMessageLocation[] { + return this.#stackTraceLocations; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Coverage.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Coverage.ts new file mode 100644 index 0000000000..b19d79e95e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Coverage.ts @@ -0,0 +1,501 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {assert} from '../util/assert.js'; + +import {CDPSession} from './Connection.js'; +import {EVALUATION_SCRIPT_URL} from './ExecutionContext.js'; +import {addEventListener, debugError, PuppeteerEventListener} from './util.js'; +import {removeEventListeners} from './util.js'; + +/** + * @internal + */ +export {PuppeteerEventListener}; + +/** + * The CoverageEntry class represents one entry of the coverage report. + * @public + */ +export interface CoverageEntry { + /** + * The URL of the style sheet or script. + */ + url: string; + /** + * The content of the style sheet or script. + */ + text: string; + /** + * The covered range as start and end positions. + */ + ranges: Array<{start: number; end: number}>; +} + +/** + * The CoverageEntry class for JavaScript + * @public + */ +export interface JSCoverageEntry extends CoverageEntry { + /** + * Raw V8 script coverage entry. + */ + rawScriptCoverage?: Protocol.Profiler.ScriptCoverage; +} + +/** + * Set of configurable options for JS coverage. + * @public + */ +export interface JSCoverageOptions { + /** + * Whether to reset coverage on every navigation. + */ + resetOnNavigation?: boolean; + /** + * Whether anonymous scripts generated by the page should be reported. + */ + reportAnonymousScripts?: boolean; + /** + * Whether the result includes raw V8 script coverage entries. + */ + includeRawScriptCoverage?: boolean; + /** + * Whether to collect coverage information at the block level. + * If true, coverage will be collected at the block level (this is the default). + * If false, coverage will be collected at the function level. + */ + useBlockCoverage?: boolean; +} + +/** + * Set of configurable options for CSS coverage. + * @public + */ +export interface CSSCoverageOptions { + /** + * Whether to reset coverage on every navigation. + */ + resetOnNavigation?: boolean; +} + +/** + * The Coverage class provides methods to gather information about parts of + * JavaScript and CSS that were used by the page. + * + * @remarks + * To output coverage in a form consumable by {@link https://github.com/istanbuljs | Istanbul}, + * see {@link https://github.com/istanbuljs/puppeteer-to-istanbul | puppeteer-to-istanbul}. + * + * @example + * An example of using JavaScript and CSS coverage to get percentage of initially + * executed code: + * + * ```ts + * // 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}%`); + * ``` + * + * @public + */ +export class Coverage { + #jsCoverage: JSCoverage; + #cssCoverage: CSSCoverage; + + constructor(client: CDPSession) { + this.#jsCoverage = new JSCoverage(client); + this.#cssCoverage = new CSSCoverage(client); + } + + /** + * @param options - Set of configurable options for coverage defaults to + * `resetOnNavigation : true, reportAnonymousScripts : false,` + * `includeRawScriptCoverage : false, useBlockCoverage : true` + * @returns 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 URL will start with `debugger://VM` (unless a magic //# sourceURL + * comment is present, in which case that will the be URL). + */ + async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { + return await this.#jsCoverage.start(options); + } + + /** + * 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. + */ + async stopJSCoverage(): Promise<JSCoverageEntry[]> { + return await this.#jsCoverage.stop(); + } + + /** + * @param options - Set of configurable options for coverage, defaults to + * `resetOnNavigation : true` + * @returns Promise that resolves when coverage is started. + */ + async startCSSCoverage(options: CSSCoverageOptions = {}): Promise<void> { + return await this.#cssCoverage.start(options); + } + + /** + * Promise that resolves to the array of coverage reports + * for all stylesheets. + * + * @remarks + * CSS Coverage doesn't include dynamically injected style tags + * without sourceURLs. + */ + async stopCSSCoverage(): Promise<CoverageEntry[]> { + return await this.#cssCoverage.stop(); + } +} + +/** + * @public + */ +export class JSCoverage { + #client: CDPSession; + #enabled = false; + #scriptURLs = new Map<string, string>(); + #scriptSources = new Map<string, string>(); + #eventListeners: PuppeteerEventListener[] = []; + #resetOnNavigation = false; + #reportAnonymousScripts = false; + #includeRawScriptCoverage = false; + + constructor(client: CDPSession) { + this.#client = client; + } + + async start( + options: { + resetOnNavigation?: boolean; + reportAnonymousScripts?: boolean; + includeRawScriptCoverage?: boolean; + useBlockCoverage?: boolean; + } = {} + ): Promise<void> { + assert(!this.#enabled, 'JSCoverage is already enabled'); + const { + resetOnNavigation = true, + reportAnonymousScripts = false, + includeRawScriptCoverage = false, + useBlockCoverage = true, + } = options; + this.#resetOnNavigation = resetOnNavigation; + this.#reportAnonymousScripts = reportAnonymousScripts; + this.#includeRawScriptCoverage = includeRawScriptCoverage; + this.#enabled = true; + this.#scriptURLs.clear(); + this.#scriptSources.clear(); + this.#eventListeners = [ + addEventListener( + this.#client, + 'Debugger.scriptParsed', + this.#onScriptParsed.bind(this) + ), + addEventListener( + this.#client, + 'Runtime.executionContextsCleared', + this.#onExecutionContextsCleared.bind(this) + ), + ]; + await Promise.all([ + this.#client.send('Profiler.enable'), + this.#client.send('Profiler.startPreciseCoverage', { + callCount: this.#includeRawScriptCoverage, + detailed: useBlockCoverage, + }), + this.#client.send('Debugger.enable'), + this.#client.send('Debugger.setSkipAllPauses', {skip: true}), + ]); + } + + #onExecutionContextsCleared(): void { + if (!this.#resetOnNavigation) { + return; + } + this.#scriptURLs.clear(); + this.#scriptSources.clear(); + } + + async #onScriptParsed( + event: Protocol.Debugger.ScriptParsedEvent + ): Promise<void> { + // Ignore puppeteer-injected scripts + if (event.url === EVALUATION_SCRIPT_URL) { + return; + } + // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. + if (!event.url && !this.#reportAnonymousScripts) { + return; + } + try { + const response = await this.#client.send('Debugger.getScriptSource', { + scriptId: event.scriptId, + }); + this.#scriptURLs.set(event.scriptId, event.url); + this.#scriptSources.set(event.scriptId, response.scriptSource); + } catch (error) { + // This might happen if the page has already navigated away. + debugError(error); + } + } + + async stop(): Promise<JSCoverageEntry[]> { + assert(this.#enabled, 'JSCoverage is not enabled'); + this.#enabled = false; + + const result = await Promise.all([ + this.#client.send('Profiler.takePreciseCoverage'), + this.#client.send('Profiler.stopPreciseCoverage'), + this.#client.send('Profiler.disable'), + this.#client.send('Debugger.disable'), + ]); + + removeEventListeners(this.#eventListeners); + + const coverage = []; + const profileResponse = result[0]; + + for (const entry of profileResponse.result) { + let url = this.#scriptURLs.get(entry.scriptId); + if (!url && this.#reportAnonymousScripts) { + url = 'debugger://VM' + entry.scriptId; + } + const text = this.#scriptSources.get(entry.scriptId); + if (text === undefined || url === undefined) { + continue; + } + const flattenRanges = []; + for (const func of entry.functions) { + flattenRanges.push(...func.ranges); + } + const ranges = convertToDisjointRanges(flattenRanges); + if (!this.#includeRawScriptCoverage) { + coverage.push({url, ranges, text}); + } else { + coverage.push({url, ranges, text, rawScriptCoverage: entry}); + } + } + return coverage; + } +} + +/** + * @public + */ +export class CSSCoverage { + #client: CDPSession; + #enabled = false; + #stylesheetURLs = new Map<string, string>(); + #stylesheetSources = new Map<string, string>(); + #eventListeners: PuppeteerEventListener[] = []; + #resetOnNavigation = false; + + constructor(client: CDPSession) { + this.#client = client; + } + + async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> { + assert(!this.#enabled, 'CSSCoverage is already enabled'); + const {resetOnNavigation = true} = options; + this.#resetOnNavigation = resetOnNavigation; + this.#enabled = true; + this.#stylesheetURLs.clear(); + this.#stylesheetSources.clear(); + this.#eventListeners = [ + addEventListener( + this.#client, + 'CSS.styleSheetAdded', + this.#onStyleSheet.bind(this) + ), + addEventListener( + this.#client, + 'Runtime.executionContextsCleared', + this.#onExecutionContextsCleared.bind(this) + ), + ]; + await Promise.all([ + this.#client.send('DOM.enable'), + this.#client.send('CSS.enable'), + this.#client.send('CSS.startRuleUsageTracking'), + ]); + } + + #onExecutionContextsCleared(): void { + if (!this.#resetOnNavigation) { + return; + } + this.#stylesheetURLs.clear(); + this.#stylesheetSources.clear(); + } + + async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> { + const header = event.header; + // Ignore anonymous scripts + if (!header.sourceURL) { + return; + } + try { + const response = await this.#client.send('CSS.getStyleSheetText', { + styleSheetId: header.styleSheetId, + }); + this.#stylesheetURLs.set(header.styleSheetId, header.sourceURL); + this.#stylesheetSources.set(header.styleSheetId, response.text); + } catch (error) { + // This might happen if the page has already navigated away. + debugError(error); + } + } + + async stop(): Promise<CoverageEntry[]> { + assert(this.#enabled, 'CSSCoverage is not enabled'); + this.#enabled = false; + const ruleTrackingResponse = await this.#client.send( + 'CSS.stopRuleUsageTracking' + ); + await Promise.all([ + this.#client.send('CSS.disable'), + this.#client.send('DOM.disable'), + ]); + removeEventListeners(this.#eventListeners); + + // aggregate by styleSheetId + const styleSheetIdToCoverage = new Map(); + for (const entry of ruleTrackingResponse.ruleUsage) { + let ranges = styleSheetIdToCoverage.get(entry.styleSheetId); + if (!ranges) { + ranges = []; + styleSheetIdToCoverage.set(entry.styleSheetId, ranges); + } + ranges.push({ + startOffset: entry.startOffset, + endOffset: entry.endOffset, + count: entry.used ? 1 : 0, + }); + } + + const coverage: CoverageEntry[] = []; + for (const styleSheetId of this.#stylesheetURLs.keys()) { + const url = this.#stylesheetURLs.get(styleSheetId); + assert( + typeof url !== 'undefined', + `Stylesheet URL is undefined (styleSheetId=${styleSheetId})` + ); + const text = this.#stylesheetSources.get(styleSheetId); + assert( + typeof text !== 'undefined', + `Stylesheet text is undefined (styleSheetId=${styleSheetId})` + ); + const ranges = convertToDisjointRanges( + styleSheetIdToCoverage.get(styleSheetId) || [] + ); + coverage.push({url, ranges, text}); + } + + return coverage; + } +} + +function convertToDisjointRanges( + nestedRanges: Array<{startOffset: number; endOffset: number; count: number}> +): Array<{start: number; end: number}> { + const points = []; + for (const range of nestedRanges) { + points.push({offset: range.startOffset, type: 0, range}); + points.push({offset: range.endOffset, type: 1, range}); + } + // Sort points to form a valid parenthesis sequence. + points.sort((a, b) => { + // Sort with increasing offsets. + if (a.offset !== b.offset) { + return a.offset - b.offset; + } + // All "end" points should go before "start" points. + if (a.type !== b.type) { + return b.type - a.type; + } + const aLength = a.range.endOffset - a.range.startOffset; + const bLength = b.range.endOffset - b.range.startOffset; + // For two "start" points, the one with longer range goes first. + if (a.type === 0) { + return bLength - aLength; + } + // For two "end" points, the one with shorter range goes first. + return aLength - bLength; + }); + + const hitCountStack = []; + const results: Array<{ + start: number; + end: number; + }> = []; + let lastOffset = 0; + // Run scanning line to intersect all ranges. + for (const point of points) { + if ( + hitCountStack.length && + lastOffset < point.offset && + hitCountStack[hitCountStack.length - 1]! > 0 + ) { + const lastResult = results[results.length - 1]; + if (lastResult && lastResult.end === lastOffset) { + lastResult.end = point.offset; + } else { + results.push({start: lastOffset, end: point.offset}); + } + } + lastOffset = point.offset; + if (point.type === 0) { + hitCountStack.push(point.range.count); + } else { + hitCountStack.pop(); + } + } + // Filter out empty ranges. + return results.filter(range => { + return range.end - range.start > 0; + }); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts new file mode 100644 index 0000000000..89ae0ca0a2 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/CustomQueryHandler.ts @@ -0,0 +1,227 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type PuppeteerUtil from '../injected/injected.js'; +import {assert} from '../util/assert.js'; +import {interpolateFunction, stringifyFunction} from '../util/Function.js'; + +import {QueryHandler, QuerySelector, QuerySelectorAll} from './QueryHandler.js'; +import {scriptInjector} from './ScriptInjector.js'; + +/** + * @public + */ +export interface CustomQueryHandler { + /** + * Searches for a {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}. + */ + queryOne?: (node: Node, selector: string) => Node | null; + /** + * Searches for some {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Nodes} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}. + */ + queryAll?: (node: Node, selector: string) => Iterable<Node>; +} + +/** + * The registry of {@link CustomQueryHandler | custom query handlers}. + * + * @example + * + * ```ts + * Puppeteer.customQueryHandlers.register('lit', { … }); + * const aHandle = await page.$('lit/…'); + * ``` + * + * @internal + */ +export class CustomQueryHandlerRegistry { + #handlers = new Map< + string, + [registerScript: string, Handler: typeof QueryHandler] + >(); + + /** + * @internal + */ + get(name: string): typeof QueryHandler | undefined { + const handler = this.#handlers.get(name); + return handler ? handler[1] : undefined; + } + + /** + * Registers a {@link CustomQueryHandler | custom query handler}. + * + * @remarks + * After registration, the handler can be used everywhere where a selector is + * expected by prepending the selection string with `<name>/`. The name is + * only allowed to consist of lower- and upper case latin letters. + * + * @example + * + * ```ts + * Puppeteer.customQueryHandlers.register('lit', { … }); + * const aHandle = await page.$('lit/…'); + * ``` + * + * @param name - Name to register under. + * @param queryHandler - {@link CustomQueryHandler | Custom query handler} to + * register. + * + * @internal + */ + register(name: string, handler: CustomQueryHandler): void { + if (this.#handlers.has(name)) { + throw new Error(`Cannot register over existing handler: ${name}`); + } + assert( + !this.#handlers.has(name), + `Cannot register over existing handler: ${name}` + ); + assert( + /^[a-zA-Z]+$/.test(name), + `Custom query handler names may only contain [a-zA-Z]` + ); + assert( + handler.queryAll || handler.queryOne, + `At least one query method must be implemented.` + ); + + const Handler = class extends QueryHandler { + static override querySelectorAll: QuerySelectorAll = interpolateFunction( + (node, selector, PuppeteerUtil) => { + return PuppeteerUtil.customQuerySelectors + .get(PLACEHOLDER('name'))! + .querySelectorAll(node, selector); + }, + {name: JSON.stringify(name)} + ); + static override querySelector: QuerySelector = interpolateFunction( + (node, selector, PuppeteerUtil) => { + return PuppeteerUtil.customQuerySelectors + .get(PLACEHOLDER('name'))! + .querySelector(node, selector); + }, + {name: JSON.stringify(name)} + ); + }; + const registerScript = interpolateFunction( + (PuppeteerUtil: PuppeteerUtil) => { + PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), { + queryAll: PLACEHOLDER('queryAll'), + queryOne: PLACEHOLDER('queryOne'), + }); + }, + { + name: JSON.stringify(name), + queryAll: handler.queryAll + ? stringifyFunction(handler.queryAll) + : String(undefined), + queryOne: handler.queryOne + ? stringifyFunction(handler.queryOne) + : String(undefined), + } + ).toString(); + + this.#handlers.set(name, [registerScript, Handler]); + scriptInjector.append(registerScript); + } + + /** + * Unregisters the {@link CustomQueryHandler | custom query handler} for the + * given name. + * + * @throws `Error` if there is no handler under the given name. + * + * @internal + */ + unregister(name: string): void { + const handler = this.#handlers.get(name); + if (!handler) { + throw new Error(`Cannot unregister unknown handler: ${name}`); + } + scriptInjector.pop(handler[0]); + this.#handlers.delete(name); + } + + /** + * Gets the names of all {@link CustomQueryHandler | custom query handlers}. + * + * @internal + */ + names(): string[] { + return [...this.#handlers.keys()]; + } + + /** + * Unregisters all custom query handlers. + * + * @internal + */ + clear(): void { + for (const [registerScript] of this.#handlers) { + scriptInjector.pop(registerScript); + } + this.#handlers.clear(); + } +} + +/** + * @internal + */ +export const customQueryHandlers = new CustomQueryHandlerRegistry(); + +/** + * @deprecated Import {@link Puppeteer} and use the static method + * {@link Puppeteer.registerCustomQueryHandler} + * + * @public + */ +export function registerCustomQueryHandler( + name: string, + handler: CustomQueryHandler +): void { + customQueryHandlers.register(name, handler); +} + +/** + * @deprecated Import {@link Puppeteer} and use the static method + * {@link Puppeteer.unregisterCustomQueryHandler} + * + * @public + */ +export function unregisterCustomQueryHandler(name: string): void { + customQueryHandlers.unregister(name); +} + +/** + * @deprecated Import {@link Puppeteer} and use the static method + * {@link Puppeteer.customQueryHandlerNames} + * + * @public + */ +export function customQueryHandlerNames(): string[] { + return customQueryHandlers.names(); +} + +/** + * @deprecated Import {@link Puppeteer} and use the static method + * {@link Puppeteer.clearCustomQueryHandlers} + * + * @public + */ +export function clearCustomQueryHandlers(): void { + customQueryHandlers.clear(); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts new file mode 100644 index 0000000000..ae9ddab90d --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Debug.ts @@ -0,0 +1,136 @@ +/** + * 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 {isNode} from '../environment.js'; + +declare global { + // eslint-disable-next-line no-var + var __PUPPETEER_DEBUG: string; +} + +/** + * @internal + */ +let debugModule: typeof import('debug') | null = null; +/** + * @internal + */ +export async function importDebug(): Promise<typeof import('debug')> { + if (!debugModule) { + debugModule = (await import('debug')).default; + } + return debugModule; +} + +/** + * A debug function that can be used in any environment. + * + * @remarks + * If used in Node, it falls back to the + * {@link https://www.npmjs.com/package/debug | debug module}. In the browser it + * uses `console.log`. + * + * In Node, use the `DEBUG` environment variable to control logging: + * + * ``` + * DEBUG=* // logs all channels + * DEBUG=foo // logs the `foo` channel + * DEBUG=foo* // logs any channels starting with `foo` + * ``` + * + * In the browser, set `window.__PUPPETEER_DEBUG` to a string: + * + * ``` + * window.__PUPPETEER_DEBUG='*'; // logs all channels + * window.__PUPPETEER_DEBUG='foo'; // logs the `foo` channel + * window.__PUPPETEER_DEBUG='foo*'; // logs any channels starting with `foo` + * ``` + * + * @example + * + * ``` + * const log = debug('Page'); + * + * log('new page created') + * // logs "Page: new page created" + * ``` + * + * @param prefix - this will be prefixed to each log. + * @returns a function that can be called to log to that debug channel. + * + * @internal + */ +export const debug = (prefix: string): ((...args: unknown[]) => void) => { + if (isNode) { + return async (...logArgs: unknown[]) => { + if (captureLogs) { + capturedLogs.push(prefix + logArgs); + } + (await importDebug())(prefix)(logArgs); + }; + } + + return (...logArgs: unknown[]): void => { + const debugLevel = (globalThis as any).__PUPPETEER_DEBUG; + if (!debugLevel) { + return; + } + + const everythingShouldBeLogged = debugLevel === '*'; + + const prefixMatchesDebugLevel = + everythingShouldBeLogged || + /** + * If the debug level is `foo*`, that means we match any prefix that + * starts with `foo`. If the level is `foo`, we match only the prefix + * `foo`. + */ + (debugLevel.endsWith('*') + ? prefix.startsWith(debugLevel) + : prefix === debugLevel); + + if (!prefixMatchesDebugLevel) { + return; + } + + // eslint-disable-next-line no-console + console.log(`${prefix}:`, ...logArgs); + }; +}; + +/** + * @internal + */ +let capturedLogs: string[] = []; +/** + * @internal + */ +let captureLogs = false; + +/** + * @internal + */ +export function setLogCapture(value: boolean): void { + capturedLogs = []; + captureLogs = value; +} + +/** + * @internal + */ +export function getCapturedLogs(): string[] { + return capturedLogs; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts new file mode 100644 index 0000000000..984418a10e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Device.ts @@ -0,0 +1,1562 @@ +/** + * 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 {Viewport} from './PuppeteerViewport.js'; + +/** + * @public + */ +export interface Device { + userAgent: string; + viewport: Viewport; +} + +const knownDevices = [ + { + name: 'Blackberry PlayBook', + userAgent: + 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+', + viewport: { + width: 600, + height: 1024, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Blackberry PlayBook landscape', + userAgent: + 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+', + viewport: { + width: 1024, + height: 600, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'BlackBerry Z30', + userAgent: + 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'BlackBerry Z30 landscape', + userAgent: + 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy Note 3', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy Note 3 landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy Note II', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy Note II landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S III', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S III landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S5', + userAgent: + 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S5 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S8', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36', + viewport: { + width: 360, + height: 740, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S8 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36', + viewport: { + width: 740, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy S9+', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36', + viewport: { + width: 320, + height: 658, + deviceScaleFactor: 4.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy S9+ landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36', + viewport: { + width: 658, + height: 320, + deviceScaleFactor: 4.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Galaxy Tab S4', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36', + viewport: { + width: 712, + height: 1138, + deviceScaleFactor: 2.25, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Galaxy Tab S4 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36', + viewport: { + width: 1138, + height: 712, + deviceScaleFactor: 2.25, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 768, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad (gen 6)', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad (gen 6) landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 1024, + height: 768, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad (gen 7)', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 810, + height: 1080, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad (gen 7) landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 1080, + height: 810, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad Mini', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 768, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad Mini landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 768, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad Pro', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1024, + height: 1366, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad Pro landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1', + viewport: { + width: 1366, + height: 1024, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPad Pro 11', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 834, + height: 1194, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPad Pro 11 landscape', + userAgent: + 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 1194, + height: 834, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 4', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53', + viewport: { + width: 320, + height: 480, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 4 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53', + viewport: { + width: 480, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 5', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 320, + height: 568, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 5 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 568, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 6', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 6 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 6 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 6 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 7', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 7 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 7 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 7 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 8', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 667, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 8 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 667, + height: 375, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 8 Plus', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 414, + height: 736, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 8 Plus landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 736, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone SE', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 320, + height: 568, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone SE landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1', + viewport: { + width: 568, + height: 320, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone X', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone X landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + viewport: { + width: 812, + height: 375, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone XR', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + viewport: { + width: 414, + height: 896, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone XR landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + viewport: { + width: 896, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 11', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 414, + height: 828, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 11 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 828, + height: 414, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 11 Pro', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 11 Pro landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 812, + height: 375, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 11 Pro Max', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 414, + height: 896, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 11 Pro Max landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1', + viewport: { + width: 896, + height: 414, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 12', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 12 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 844, + height: 390, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 12 Pro', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 12 Pro landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 844, + height: 390, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 12 Pro Max', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 428, + height: 926, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 12 Pro Max landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 926, + height: 428, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 12 Mini', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 12 Mini landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 812, + height: 375, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 13', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 13 landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 844, + height: 390, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 13 Pro', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 13 Pro landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 844, + height: 390, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 13 Pro Max', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 428, + height: 926, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 13 Pro Max landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 926, + height: 428, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'iPhone 13 Mini', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 375, + height: 812, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'iPhone 13 Mini landscape', + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1', + viewport: { + width: 812, + height: 375, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'JioPhone 2', + userAgent: + 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5', + viewport: { + width: 240, + height: 320, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'JioPhone 2 landscape', + userAgent: + 'Mozilla/5.0 (Mobile; LYF/F300B/LYF-F300B-001-01-15-130718-i;Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5', + viewport: { + width: 320, + height: 240, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Kindle Fire HDX', + userAgent: + 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true', + viewport: { + width: 800, + height: 1280, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Kindle Fire HDX landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true', + viewport: { + width: 1280, + height: 800, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'LG Optimus L70', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 384, + height: 640, + deviceScaleFactor: 1.25, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'LG Optimus L70 landscape', + userAgent: + 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 384, + deviceScaleFactor: 1.25, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Microsoft Lumia 550', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Microsoft Lumia 950', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 4, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Microsoft Lumia 950 landscape', + userAgent: + 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 4, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 10', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 800, + height: 1280, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 10 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 1280, + height: 800, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 4', + userAgent: + 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 384, + height: 640, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 4 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 384, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 5', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 5 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 5X', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 5X landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 6', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 6 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 6P', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 412, + height: 732, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 6P landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 732, + height: 412, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nexus 7', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 600, + height: 960, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nexus 7 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Safari/537.36', + viewport: { + width: 960, + height: 600, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nokia Lumia 520', + userAgent: + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)', + viewport: { + width: 320, + height: 533, + deviceScaleFactor: 1.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nokia Lumia 520 landscape', + userAgent: + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)', + viewport: { + width: 533, + height: 320, + deviceScaleFactor: 1.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Nokia N9', + userAgent: + 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13', + viewport: { + width: 480, + height: 854, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Nokia N9 landscape', + userAgent: + 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13', + viewport: { + width: 854, + height: 480, + deviceScaleFactor: 1, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 2', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 411, + height: 731, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 2 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 731, + height: 411, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 2 XL', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 411, + height: 823, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 2 XL landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3765.0 Mobile Safari/537.36', + viewport: { + width: 823, + height: 411, + deviceScaleFactor: 3.5, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 3', + userAgent: + 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36', + viewport: { + width: 393, + height: 786, + deviceScaleFactor: 2.75, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 3 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36', + viewport: { + width: 786, + height: 393, + deviceScaleFactor: 2.75, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 4', + userAgent: + 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36', + viewport: { + width: 353, + height: 745, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 4 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36', + viewport: { + width: 745, + height: 353, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 4a (5G)', + userAgent: + 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 353, + height: 745, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 4a (5G) landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 745, + height: 353, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Pixel 5', + userAgent: + 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 393, + height: 851, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Pixel 5 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 851, + height: 393, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, + { + name: 'Moto G4', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 360, + height: 640, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + }, + { + name: 'Moto G4 landscape', + userAgent: + 'Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4812.0 Mobile Safari/537.36', + viewport: { + width: 640, + height: 360, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: true, + }, + }, +] as const; + +const knownDevicesByName = {} as Record< + (typeof knownDevices)[number]['name'], + Device +>; + +for (const device of knownDevices) { + knownDevicesByName[device.name] = device; +} + +/** + * A list of devices to be used with {@link Page.emulate}. + * + * @example + * + * ```ts + * import {KnownDevices} from 'puppeteer'; + * const iPhone = KnownDevices['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(); + * })(); + * ``` + * + * @public + */ +export const KnownDevices = Object.freeze(knownDevicesByName); + +/** + * @deprecated Import {@link KnownDevices} + * + * @public + */ +export const devices = KnownDevices; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts new file mode 100644 index 0000000000..aa3b1264c1 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/DeviceRequestPrompt.ts @@ -0,0 +1,293 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Protocol from 'devtools-protocol'; + +import {WaitTimeoutOptions} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; + +import {CDPSession} from './Connection.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; + +/** + * Device in a request prompt. + * + * @public + */ +export class DeviceRequestPromptDevice { + /** + * Device id during a prompt. + */ + id: string; + + /** + * Device name as it appears in a prompt. + */ + name: string; + + /** + * @internal + */ + constructor(id: string, name: string) { + this.id = id; + this.name = name; + } +} + +/** + * Device request prompts let you respond to the page requesting for a device + * through an API like WebBluetooth. + * + * @remarks + * `DeviceRequestPrompt` instances are returned via the + * {@link Page.waitForDevicePrompt} method. + * + * @example + * + * ```ts + * const [deviceRequest] = Promise.all([ + * page.waitForDevicePrompt(), + * page.click('#connect-bluetooth'), + * ]); + * await devicePrompt.select( + * await devicePrompt.waitForDevice(({name}) => name.includes('My Device')) + * ); + * ``` + * + * @public + */ +export class DeviceRequestPrompt { + #client: CDPSession | null; + #timeoutSettings: TimeoutSettings; + #id: string; + #handled = false; + #updateDevicesHandle = this.#updateDevices.bind(this); + #waitForDevicePromises = new Set<{ + filter: (device: DeviceRequestPromptDevice) => boolean; + promise: DeferredPromise<DeviceRequestPromptDevice>; + }>(); + + /** + * Current list of selectable devices. + */ + devices: DeviceRequestPromptDevice[] = []; + + /** + * @internal + */ + constructor( + client: CDPSession, + timeoutSettings: TimeoutSettings, + firstEvent: Protocol.DeviceAccess.DeviceRequestPromptedEvent + ) { + this.#client = client; + this.#timeoutSettings = timeoutSettings; + this.#id = firstEvent.id; + + this.#client.on( + 'DeviceAccess.deviceRequestPrompted', + this.#updateDevicesHandle + ); + this.#client.on('Target.detachedFromTarget', () => { + this.#client = null; + }); + + this.#updateDevices(firstEvent); + } + + #updateDevices(event: Protocol.DeviceAccess.DeviceRequestPromptedEvent) { + if (event.id !== this.#id) { + return; + } + + for (const rawDevice of event.devices) { + if ( + this.devices.some(device => { + return device.id === rawDevice.id; + }) + ) { + continue; + } + + const newDevice = new DeviceRequestPromptDevice( + rawDevice.id, + rawDevice.name + ); + this.devices.push(newDevice); + + for (const waitForDevicePromise of this.#waitForDevicePromises) { + if (waitForDevicePromise.filter(newDevice)) { + waitForDevicePromise.promise.resolve(newDevice); + } + } + } + } + + /** + * Resolve to the first device in the prompt matching a filter. + */ + async waitForDevice( + filter: (device: DeviceRequestPromptDevice) => boolean, + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPromptDevice> { + for (const device of this.devices) { + if (filter(device)) { + return device; + } + } + + const {timeout = this.#timeoutSettings.timeout()} = options; + const promise = createDeferredPromise<DeviceRequestPromptDevice>({ + message: `Waiting for \`DeviceRequestPromptDevice\` failed: ${timeout}ms exceeded`, + timeout, + }); + const handle = {filter, promise}; + this.#waitForDevicePromises.add(handle); + try { + return await promise; + } finally { + this.#waitForDevicePromises.delete(handle); + } + } + + /** + * Select a device in the prompt's list. + */ + async select(device: DeviceRequestPromptDevice): Promise<void> { + assert( + this.#client !== null, + 'Cannot select device through detached session!' + ); + assert(this.devices.includes(device), 'Cannot select unknown device!'); + assert( + !this.#handled, + 'Cannot select DeviceRequestPrompt which is already handled!' + ); + this.#client.off( + 'DeviceAccess.deviceRequestPrompted', + this.#updateDevicesHandle + ); + this.#handled = true; + return this.#client.send('DeviceAccess.selectPrompt', { + id: this.#id, + deviceId: device.id, + }); + } + + /** + * Cancel the prompt. + */ + async cancel(): Promise<void> { + assert( + this.#client !== null, + 'Cannot cancel prompt through detached session!' + ); + assert( + !this.#handled, + 'Cannot cancel DeviceRequestPrompt which is already handled!' + ); + this.#client.off( + 'DeviceAccess.deviceRequestPrompted', + this.#updateDevicesHandle + ); + this.#handled = true; + return this.#client.send('DeviceAccess.cancelPrompt', {id: this.#id}); + } +} + +/** + * @internal + */ +export class DeviceRequestPromptManager { + #client: CDPSession | null; + #timeoutSettings: TimeoutSettings; + #deviceRequestPromptPromises = new Set< + DeferredPromise<DeviceRequestPrompt> + >(); + + /** + * @internal + */ + constructor(client: CDPSession, timeoutSettings: TimeoutSettings) { + this.#client = client; + this.#timeoutSettings = timeoutSettings; + + this.#client.on('DeviceAccess.deviceRequestPrompted', event => { + this.#onDeviceRequestPrompted(event); + }); + this.#client.on('Target.detachedFromTarget', () => { + this.#client = null; + }); + } + + /** + * Wait for device prompt created by an action like calling WebBluetooth's + * requestDevice. + */ + async waitForDevicePrompt( + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPrompt> { + assert( + this.#client !== null, + 'Cannot wait for device prompt through detached session!' + ); + const needsEnable = this.#deviceRequestPromptPromises.size === 0; + let enablePromise: Promise<void> | undefined; + if (needsEnable) { + enablePromise = this.#client.send('DeviceAccess.enable'); + } + + const {timeout = this.#timeoutSettings.timeout()} = options; + const promise = createDeferredPromise<DeviceRequestPrompt>({ + message: `Waiting for \`DeviceRequestPrompt\` failed: ${timeout}ms exceeded`, + timeout, + }); + this.#deviceRequestPromptPromises.add(promise); + + try { + const [result] = await Promise.all([promise, enablePromise]); + return result; + } finally { + this.#deviceRequestPromptPromises.delete(promise); + } + } + + /** + * @internal + */ + #onDeviceRequestPrompted( + event: Protocol.DeviceAccess.DeviceRequestPromptedEvent + ) { + if (!this.#deviceRequestPromptPromises.size) { + return; + } + + assert(this.#client !== null); + const devicePrompt = new DeviceRequestPrompt( + this.#client, + this.#timeoutSettings, + event + ); + for (const promise of this.#deviceRequestPromptPromises) { + promise.resolve(devicePrompt); + } + this.#deviceRequestPromptPromises.clear(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Dialog.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Dialog.ts new file mode 100644 index 0000000000..5ccc5e1bac --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Dialog.ts @@ -0,0 +1,117 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {assert} from '../util/assert.js'; + +import {CDPSession} from './Connection.js'; + +/** + * Dialog instances are dispatched by the {@link Page} via the `dialog` event. + * + * @remarks + * + * @example + * + * ```ts + * import puppeteer from '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')); + * })(); + * ``` + * + * @public + */ +export class Dialog { + #client: CDPSession; + #type: Protocol.Page.DialogType; + #message: string; + #defaultValue: string; + #handled = false; + + /** + * @internal + */ + constructor( + client: CDPSession, + type: Protocol.Page.DialogType, + message: string, + defaultValue = '' + ) { + this.#client = client; + this.#type = type; + this.#message = message; + this.#defaultValue = defaultValue; + } + + /** + * The type of the dialog. + */ + type(): Protocol.Page.DialogType { + return this.#type; + } + + /** + * The message displayed in the dialog. + */ + message(): string { + return this.#message; + } + + /** + * The default value of the prompt, or an empty string if the dialog + * is not a `prompt`. + */ + defaultValue(): string { + return this.#defaultValue; + } + + /** + * A promise that resolves when the dialog has been accepted. + * + * @param promptText - optional text that will be entered in the dialog + * prompt. Has no effect if the dialog's type is not `prompt`. + * + */ + async accept(promptText?: string): Promise<void> { + assert(!this.#handled, 'Cannot accept dialog which is already handled!'); + this.#handled = true; + await this.#client.send('Page.handleJavaScriptDialog', { + accept: true, + promptText: promptText, + }); + } + + /** + * A promise which will resolve once the dialog has been dismissed + */ + async dismiss(): Promise<void> { + assert(!this.#handled, 'Cannot dismiss dialog which is already handled!'); + this.#handled = true; + await this.#client.send('Page.handleJavaScriptDialog', { + accept: false, + }); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ElementHandle.ts new file mode 100644 index 0000000000..351d7057bf --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ElementHandle.ts @@ -0,0 +1,777 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import { + BoundingBox, + BoxModel, + ClickOptions, + ElementHandle, + Offset, + Point, + PressOptions, +} from '../api/ElementHandle.js'; +import {Page, ScreenshotOptions} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; + +import {CDPSession} from './Connection.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {FrameManager} from './FrameManager.js'; +import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; +import {WaitForSelectorOptions} from './IsolatedWorld.js'; +import {PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import {CDPJSHandle} from './JSHandle.js'; +import {LazyArg} from './LazyArg.js'; +import {CDPPage} from './Page.js'; +import {ElementFor, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; +import {KeyInput} from './USKeyboardLayout.js'; +import {debugError, isString} from './util.js'; + +const applyOffsetsToQuad = ( + quad: Point[], + offsetX: number, + offsetY: number +) => { + return quad.map(part => { + return {x: part.x + offsetX, y: part.y + offsetY}; + }); +}; + +/** + * The CDPElementHandle extends ElementHandle now to keep compatibility + * with `instanceof` because of that we need to have methods for + * CDPJSHandle to in this implementation as well. + * + * @internal + */ +export class CDPElementHandle< + ElementType extends Node = Element +> extends ElementHandle<ElementType> { + #frame: Frame; + declare handle: CDPJSHandle<ElementType>; + + constructor( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject, + frame: Frame + ) { + super(new CDPJSHandle(context, remoteObject)); + this.#frame = frame; + } + + /** + * @internal + */ + override executionContext(): ExecutionContext { + return this.handle.executionContext(); + } + + /** + * @internal + */ + override get client(): CDPSession { + return this.handle.client; + } + + override remoteObject(): Protocol.Runtime.RemoteObject { + return this.handle.remoteObject(); + } + + get #frameManager(): FrameManager { + return this.#frame._frameManager; + } + + get #page(): Page { + return this.#frame.page(); + } + + override get frame(): Frame { + return this.#frame; + } + + override async $<Selector extends string>( + selector: Selector + ): Promise<CDPElementHandle<NodeFor<Selector>> | null> { + const {updatedSelector, QueryHandler} = + getQueryHandlerAndSelector(selector); + return (await QueryHandler.queryOne( + this, + updatedSelector + )) as CDPElementHandle<NodeFor<Selector>> | null; + } + + override async $$<Selector extends string>( + selector: Selector + ): Promise<Array<CDPElementHandle<NodeFor<Selector>>>> { + const {updatedSelector, QueryHandler} = + getQueryHandlerAndSelector(selector); + return AsyncIterableUtil.collect( + QueryHandler.queryAll(this, updatedSelector) + ) as Promise<Array<CDPElementHandle<NodeFor<Selector>>>>; + } + + override async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const elementHandle = await this.$(selector); + if (!elementHandle) { + throw new Error( + `Error: failed to find element matching selector "${selector}"` + ); + } + const result = await elementHandle.evaluate(pageFunction, ...args); + await elementHandle.dispose(); + return result; + } + + override async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const results = await this.$$(selector); + const elements = await this.evaluateHandle((_, ...elements) => { + return elements; + }, ...results); + const [result] = await Promise.all([ + elements.evaluate(pageFunction, ...args), + ...results.map(results => { + return results.dispose(); + }), + ]); + await elements.dispose(); + return result; + } + + override async $x( + expression: string + ): Promise<Array<CDPElementHandle<Node>>> { + if (expression.startsWith('//')) { + expression = `.${expression}`; + } + return this.$$(`xpath/${expression}`); + } + + override async waitForSelector<Selector extends string>( + selector: Selector, + options: WaitForSelectorOptions = {} + ): Promise<CDPElementHandle<NodeFor<Selector>> | null> { + const {updatedSelector, QueryHandler} = + getQueryHandlerAndSelector(selector); + return (await QueryHandler.waitFor( + this, + updatedSelector, + options + )) as CDPElementHandle<NodeFor<Selector>> | null; + } + + override async waitForXPath( + xpath: string, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } = {} + ): Promise<CDPElementHandle<Node> | null> { + if (xpath.startsWith('//')) { + xpath = `.${xpath}`; + } + return this.waitForSelector(`xpath/${xpath}`, options); + } + + async #checkVisibility(visibility: boolean): Promise<boolean> { + const element = await this.frame.worlds[PUPPETEER_WORLD].adoptHandle(this); + try { + return await this.frame.worlds[PUPPETEER_WORLD].evaluate( + async (PuppeteerUtil, element, visibility) => { + return Boolean(PuppeteerUtil.checkVisibility(element, visibility)); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + element, + visibility + ); + } finally { + await element.dispose(); + } + } + + override async isVisible(): Promise<boolean> { + return this.#checkVisibility(true); + } + + override async isHidden(): Promise<boolean> { + return this.#checkVisibility(false); + } + + override async toElement< + K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap + >(tagName: K): Promise<HandleFor<ElementFor<K>>> { + const isMatchingTagName = await this.evaluate((node, tagName) => { + return node.nodeName === tagName.toUpperCase(); + }, tagName); + if (!isMatchingTagName) { + throw new Error(`Element is not a(n) \`${tagName}\` element`); + } + return this as unknown as HandleFor<ElementFor<K>>; + } + + override async contentFrame(): Promise<Frame | null> { + const nodeInfo = await this.client.send('DOM.describeNode', { + objectId: this.remoteObject().objectId, + }); + if (typeof nodeInfo.node.frameId !== 'string') { + return null; + } + return this.#frameManager.frame(nodeInfo.node.frameId); + } + + override async scrollIntoView( + this: CDPElementHandle<Element> + ): Promise<void> { + await this.assertConnectedElement(); + + try { + await this.client.send('DOM.scrollIntoViewIfNeeded', { + objectId: this.remoteObject().objectId, + }); + } catch (error) { + debugError(error); + // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported + await this.evaluate(async (element): Promise<void> => { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // @ts-expect-error Chrome still supports behavior: instant but + // it's not in the spec so TS shouts We don't want to make this + // breaking change in Puppeteer yet so we'll ignore the line. + behavior: 'instant', + }); + }); + } + } + + async #scrollIntoViewIfNeeded( + this: CDPElementHandle<Element> + ): Promise<void> { + if ( + await this.isIntersectingViewport({ + threshold: 1, + }) + ) { + return; + } + await this.scrollIntoView(); + } + + async #getOOPIFOffsets( + frame: Frame + ): Promise<{offsetX: number; offsetY: number}> { + let offsetX = 0; + let offsetY = 0; + let currentFrame: Frame | null = frame; + while (currentFrame && currentFrame.parentFrame()) { + const parent = currentFrame.parentFrame(); + if (!currentFrame.isOOPFrame() || !parent) { + currentFrame = parent; + continue; + } + const {backendNodeId} = await parent._client().send('DOM.getFrameOwner', { + frameId: currentFrame._id, + }); + const result = await parent._client().send('DOM.getBoxModel', { + backendNodeId: backendNodeId, + }); + if (!result) { + break; + } + const contentBoxQuad = result.model.content; + const topLeftCorner = this.#fromProtocolQuad(contentBoxQuad)[0]; + offsetX += topLeftCorner!.x; + offsetY += topLeftCorner!.y; + currentFrame = parent; + } + return {offsetX, offsetY}; + } + + override async clickablePoint(offset?: Offset): Promise<Point> { + const [result, layoutMetrics] = await Promise.all([ + this.client + .send('DOM.getContentQuads', { + objectId: this.remoteObject().objectId, + }) + .catch(debugError), + (this.#page as CDPPage)._client().send('Page.getLayoutMetrics'), + ]); + if (!result || !result.quads.length) { + throw new Error('Node is either not clickable or not an HTMLElement'); + } + // Filter out quads that have too small area to click into. + // Fallback to `layoutViewport` in case of using Firefox. + const {clientWidth, clientHeight} = + layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport; + const {offsetX, offsetY} = await this.#getOOPIFOffsets(this.#frame); + const quads = result.quads + .map(quad => { + return this.#fromProtocolQuad(quad); + }) + .map(quad => { + return applyOffsetsToQuad(quad, offsetX, offsetY); + }) + .map(quad => { + return this.#intersectQuadWithViewport(quad, clientWidth, clientHeight); + }) + .filter(quad => { + return computeQuadArea(quad) > 1; + }); + if (!quads.length) { + throw new Error('Node is either not clickable or not an HTMLElement'); + } + const quad = quads[0]!; + if (offset) { + // Return the point of the first quad identified by offset. + let minX = Number.MAX_SAFE_INTEGER; + let minY = Number.MAX_SAFE_INTEGER; + for (const point of quad) { + if (point.x < minX) { + minX = point.x; + } + if (point.y < minY) { + minY = point.y; + } + } + if ( + minX !== Number.MAX_SAFE_INTEGER && + minY !== Number.MAX_SAFE_INTEGER + ) { + return { + x: minX + offset.x, + y: minY + offset.y, + }; + } + } + // Return the middle point of the first quad. + let x = 0; + let y = 0; + for (const point of quad) { + x += point.x; + y += point.y; + } + return { + x: x / 4, + y: y / 4, + }; + } + + #getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> { + const params: Protocol.DOM.GetBoxModelRequest = { + objectId: this.id, + }; + return this.client.send('DOM.getBoxModel', params).catch(error => { + return debugError(error); + }); + } + + #fromProtocolQuad(quad: number[]): Point[] { + return [ + {x: quad[0]!, y: quad[1]!}, + {x: quad[2]!, y: quad[3]!}, + {x: quad[4]!, y: quad[5]!}, + {x: quad[6]!, y: quad[7]!}, + ]; + } + + #intersectQuadWithViewport( + quad: Point[], + width: number, + height: number + ): Point[] { + return quad.map(point => { + return { + x: Math.min(Math.max(point.x, 0), width), + y: Math.min(Math.max(point.y, 0), height), + }; + }); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page.mouse} to hover over the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + override async hover(this: CDPElementHandle<Element>): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.#page.mouse.move(x, y); + } + + /** + * This method scrolls element into view if needed, and then + * uses {@link Page.mouse} to click in the center of the element. + * If the element is detached from DOM, the method throws an error. + */ + override async click( + this: CDPElementHandle<Element>, + options: Readonly<ClickOptions> = {} + ): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(options.offset); + await this.#page.mouse.click(x, y, options); + } + + /** + * This method creates and captures a dragevent from the element. + */ + override async drag( + this: CDPElementHandle<Element>, + target: Point + ): Promise<Protocol.Input.DragData> { + assert( + this.#page.isDragInterceptionEnabled(), + 'Drag Interception is not enabled!' + ); + await this.#scrollIntoViewIfNeeded(); + const start = await this.clickablePoint(); + return await this.#page.mouse.drag(start, target); + } + + override async dragEnter( + this: CDPElementHandle<Element>, + data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} + ): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const target = await this.clickablePoint(); + await this.#page.mouse.dragEnter(target, data); + } + + override async dragOver( + this: CDPElementHandle<Element>, + data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} + ): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const target = await this.clickablePoint(); + await this.#page.mouse.dragOver(target, data); + } + + override async drop( + this: CDPElementHandle<Element>, + data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} + ): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const destination = await this.clickablePoint(); + await this.#page.mouse.drop(destination, data); + } + + override async dragAndDrop( + this: CDPElementHandle<Element>, + target: CDPElementHandle<Node>, + options?: {delay: number} + ): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const startPoint = await this.clickablePoint(); + const targetPoint = await target.clickablePoint(); + await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options); + } + + override async select(...values: string[]): Promise<string[]> { + for (const value of values) { + assert( + isString(value), + 'Values must be strings. Found value "' + + value + + '" of type "' + + typeof value + + '"' + ); + } + + return this.evaluate((element, vals): string[] => { + const values = new Set(vals); + if (!(element instanceof HTMLSelectElement)) { + throw new Error('Element is not a <select> element.'); + } + + const selectedValues = new Set<string>(); + if (!element.multiple) { + for (const option of element.options) { + option.selected = false; + } + for (const option of element.options) { + if (values.has(option.value)) { + option.selected = true; + selectedValues.add(option.value); + break; + } + } + } else { + for (const option of element.options) { + option.selected = values.has(option.value); + if (option.selected) { + selectedValues.add(option.value); + } + } + } + element.dispatchEvent(new Event('input', {bubbles: true})); + element.dispatchEvent(new Event('change', {bubbles: true})); + return [...selectedValues.values()]; + }, values); + } + + override async uploadFile( + this: CDPElementHandle<HTMLInputElement>, + ...filePaths: string[] + ): Promise<void> { + const isMultiple = await this.evaluate(element => { + return element.multiple; + }); + assert( + filePaths.length <= 1 || isMultiple, + 'Multiple file uploads only work with <input type=file multiple>' + ); + + // Locate all files and confirm that they exist. + let path: typeof import('path'); + try { + path = await import('path'); + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + `JSHandle#uploadFile can only be used in Node-like environments.` + ); + } + throw error; + } + const files = filePaths.map(filePath => { + if (path.win32.isAbsolute(filePath) || path.posix.isAbsolute(filePath)) { + return filePath; + } else { + return path.resolve(filePath); + } + }); + const {objectId} = this.remoteObject(); + const {node} = await this.client.send('DOM.describeNode', { + objectId, + }); + const {backendNodeId} = node; + + /* The zero-length array is a special case, it seems that + DOM.setFileInputFiles does not actually update the files in that case, + so the solution is to eval the element value to a new FileList directly. + */ + if (files.length === 0) { + await this.evaluate(element => { + element.files = new DataTransfer().files; + + // Dispatch events for this case because it should behave akin to a user action. + element.dispatchEvent(new Event('input', {bubbles: true})); + element.dispatchEvent(new Event('change', {bubbles: true})); + }); + } else { + await this.client.send('DOM.setFileInputFiles', { + objectId, + files, + backendNodeId, + }); + } + } + + override async tap(this: CDPElementHandle<Element>): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.#page.touchscreen.touchStart(x, y); + await this.#page.touchscreen.touchEnd(); + } + + override async touchStart(this: CDPElementHandle<Element>): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.#page.touchscreen.touchStart(x, y); + } + + override async touchMove(this: CDPElementHandle<Element>): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.#page.touchscreen.touchMove(x, y); + } + + override async touchEnd(this: CDPElementHandle<Element>): Promise<void> { + await this.#scrollIntoViewIfNeeded(); + await this.#page.touchscreen.touchEnd(); + } + + override async focus(): Promise<void> { + await this.evaluate(element => { + if (!(element instanceof HTMLElement)) { + throw new Error('Cannot focus non-HTMLElement'); + } + return element.focus(); + }); + } + + override async type(text: string, options?: {delay: number}): Promise<void> { + await this.focus(); + await this.#page.keyboard.type(text, options); + } + + override async press(key: KeyInput, options?: PressOptions): Promise<void> { + await this.focus(); + await this.#page.keyboard.press(key, options); + } + + override async boundingBox(): Promise<BoundingBox | null> { + const result = await this.#getBoxModel(); + + if (!result) { + return null; + } + + const {offsetX, offsetY} = await this.#getOOPIFOffsets(this.#frame); + const quad = result.model.border; + const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!); + const y = Math.min(quad[1]!, quad[3]!, quad[5]!, quad[7]!); + const width = Math.max(quad[0]!, quad[2]!, quad[4]!, quad[6]!) - x; + const height = Math.max(quad[1]!, quad[3]!, quad[5]!, quad[7]!) - y; + + return {x: x + offsetX, y: y + offsetY, width, height}; + } + + override async boxModel(): Promise<BoxModel | null> { + const result = await this.#getBoxModel(); + + if (!result) { + return null; + } + + const {offsetX, offsetY} = await this.#getOOPIFOffsets(this.#frame); + + const {content, padding, border, margin, width, height} = result.model; + return { + content: applyOffsetsToQuad( + this.#fromProtocolQuad(content), + offsetX, + offsetY + ), + padding: applyOffsetsToQuad( + this.#fromProtocolQuad(padding), + offsetX, + offsetY + ), + border: applyOffsetsToQuad( + this.#fromProtocolQuad(border), + offsetX, + offsetY + ), + margin: applyOffsetsToQuad( + this.#fromProtocolQuad(margin), + offsetX, + offsetY + ), + width, + height, + }; + } + + override async screenshot( + this: CDPElementHandle<Element>, + options: ScreenshotOptions = {} + ): Promise<string | Buffer> { + let needsViewportReset = false; + + let boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + + const viewport = this.#page.viewport(); + + if ( + viewport && + (boundingBox.width > viewport.width || + boundingBox.height > viewport.height) + ) { + const newViewport = { + width: Math.max(viewport.width, Math.ceil(boundingBox.width)), + height: Math.max(viewport.height, Math.ceil(boundingBox.height)), + }; + await this.#page.setViewport(Object.assign({}, viewport, newViewport)); + + needsViewportReset = true; + } + + await this.#scrollIntoViewIfNeeded(); + + boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); + assert(boundingBox.width !== 0, 'Node has 0 width.'); + assert(boundingBox.height !== 0, 'Node has 0 height.'); + + const layoutMetrics = await this.client.send('Page.getLayoutMetrics'); + // Fallback to `layoutViewport` in case of using Firefox. + const {pageX, pageY} = + layoutMetrics.cssVisualViewport || layoutMetrics.layoutViewport; + + const clip = Object.assign({}, boundingBox); + clip.x += pageX; + clip.y += pageY; + + const imageData = await this.#page.screenshot( + Object.assign( + {}, + { + clip, + }, + options + ) + ); + + if (needsViewportReset && viewport) { + await this.#page.setViewport(viewport); + } + + return imageData; + } +} + +function computeQuadArea(quad: Point[]): number { + /* Compute sum of all directed areas of adjacent triangles + https://en.wikipedia.org/wiki/Polygon#Simple_polygons + */ + let area = 0; + for (let i = 0; i < quad.length; ++i) { + const p1 = quad[i]!; + const p2 = quad[(i + 1) % quad.length]!; + area += (p1.x * p2.y - p2.x * p1.y) / 2; + } + return Math.abs(area); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EmulationManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EmulationManager.ts new file mode 100644 index 0000000000..27aa57581c --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EmulationManager.ts @@ -0,0 +1,63 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {CDPSession} from './Connection.js'; +import {Viewport} from './PuppeteerViewport.js'; + +/** + * @internal + */ +export class EmulationManager { + #client: CDPSession; + #emulatingMobile = false; + #hasTouch = false; + + constructor(client: CDPSession) { + this.#client = client; + } + + async emulateViewport(viewport: Viewport): Promise<boolean> { + const mobile = viewport.isMobile || false; + const width = viewport.width; + const height = viewport.height; + const deviceScaleFactor = viewport.deviceScaleFactor ?? 1; + const screenOrientation: Protocol.Emulation.ScreenOrientation = + viewport.isLandscape + ? {angle: 90, type: 'landscapePrimary'} + : {angle: 0, type: 'portraitPrimary'}; + const hasTouch = viewport.hasTouch || false; + + await Promise.all([ + this.#client.send('Emulation.setDeviceMetricsOverride', { + mobile, + width, + height, + deviceScaleFactor, + screenOrientation, + }), + this.#client.send('Emulation.setTouchEmulationEnabled', { + enabled: hasTouch, + }), + ]); + + const reloadNeeded = + this.#emulatingMobile !== mobile || this.#hasTouch !== hasTouch; + this.#emulatingMobile = mobile; + this.#hasTouch = hasTouch; + return reloadNeeded; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts new file mode 100644 index 0000000000..4d067c89ae --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Errors.ts @@ -0,0 +1,115 @@ +/** + * 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. + */ + +/** + * @deprecated Do not use. + * + * @public + */ +export class CustomError extends Error { + /** + * @internal + */ + constructor(message?: string) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * TimeoutError is emitted whenever certain operations are terminated due to + * timeout. + * + * @remarks + * Example operations are {@link Page.waitForSelector | page.waitForSelector} or + * {@link PuppeteerNode.launch | puppeteer.launch}. + * + * @public + */ +export class TimeoutError extends CustomError {} + +/** + * ProtocolError is emitted whenever there is an error from the protocol. + * + * @public + */ +export class ProtocolError extends CustomError { + #code?: number; + #originalMessage = ''; + + set code(code: number | undefined) { + this.#code = code; + } + /** + * @readonly + * @public + */ + get code(): number | undefined { + return this.#code; + } + + set originalMessage(originalMessage: string) { + this.#originalMessage = originalMessage; + } + /** + * @readonly + * @public + */ + get originalMessage(): string { + return this.#originalMessage; + } +} + +/** + * @deprecated Do not use. + * + * @public + */ +export interface PuppeteerErrors { + TimeoutError: typeof TimeoutError; + ProtocolError: typeof ProtocolError; +} + +/** + * @deprecated Import error classes directly. + * + * Puppeteer methods might throw errors if they are unable to fulfill a request. + * For example, `page.waitForSelector(selector[, 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`. + * + * @example + * An example of handling a timeout error: + * + * ```ts + * try { + * await page.waitForSelector('.foo'); + * } catch (e) { + * if (e instanceof TimeoutError) { + * // Do something if this is a timeout. + * } + * } + * ``` + * + * @public + */ +export const errors: PuppeteerErrors = Object.freeze({ + TimeoutError, + ProtocolError, +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts new file mode 100644 index 0000000000..41001a1592 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/EventEmitter.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mitt, {Emitter, EventHandlerMap} from '../../third_party/mitt/index.js'; + +/** + * @public + */ +export type EventType = string | symbol; +/** + * @public + */ +export type Handler<T = unknown> = (event: T) => void; + +/** + * @public + */ +export interface CommonEventEmitter { + on(event: EventType, handler: Handler): CommonEventEmitter; + off(event: EventType, handler: Handler): CommonEventEmitter; + /* To maintain parity with the built in NodeJS event emitter which uses removeListener + * rather than `off`. + * If you're implementing new code you should use `off`. + */ + addListener(event: EventType, handler: Handler): CommonEventEmitter; + removeListener(event: EventType, handler: Handler): CommonEventEmitter; + emit(event: EventType, eventData?: unknown): boolean; + once(event: EventType, handler: Handler): CommonEventEmitter; + listenerCount(event: string): number; + + removeAllListeners(event?: EventType): CommonEventEmitter; +} + +/** + * The EventEmitter class that many Puppeteer classes extend. + * + * @remarks + * + * This allows you to listen to events that Puppeteer classes fire and act + * accordingly. Therefore you'll mostly use {@link EventEmitter.on | on} and + * {@link EventEmitter.off | off} to bind + * and unbind to event listeners. + * + * @public + */ +export class EventEmitter implements CommonEventEmitter { + private emitter: Emitter<Record<string | symbol, any>>; + private eventsMap: EventHandlerMap<Record<string | symbol, any>> = new Map(); + + /** + * @internal + */ + constructor() { + this.emitter = mitt(this.eventsMap); + } + + /** + * Bind an event listener to fire when an event occurs. + * @param event - the event type you'd like to listen to. Can be a string or symbol. + * @param handler - the function to be called when the event occurs. + * @returns `this` to enable you to chain method calls. + */ + on(event: EventType, handler: Handler<any>): EventEmitter { + this.emitter.on(event, handler); + return this; + } + + /** + * Remove an event listener from firing. + * @param event - the event type you'd like to stop listening to. + * @param handler - the function that should be removed. + * @returns `this` to enable you to chain method calls. + */ + off(event: EventType, handler: Handler<any>): EventEmitter { + this.emitter.off(event, handler); + return this; + } + + /** + * Remove an event listener. + * @deprecated please use {@link EventEmitter.off} instead. + */ + removeListener(event: EventType, handler: Handler<any>): EventEmitter { + this.off(event, handler); + return this; + } + + /** + * Add an event listener. + * @deprecated please use {@link EventEmitter.on} instead. + */ + addListener(event: EventType, handler: Handler<any>): EventEmitter { + this.on(event, handler); + return this; + } + + /** + * Emit an event and call any associated listeners. + * + * @param event - the event you'd like to emit + * @param eventData - any data you'd like to emit with the event + * @returns `true` if there are any listeners, `false` if there are not. + */ + emit(event: EventType, eventData?: unknown): boolean { + this.emitter.emit(event, eventData); + return this.eventListenersCount(event) > 0; + } + + /** + * Like `on` but the listener will only be fired once and then it will be removed. + * @param event - the event you'd like to listen to + * @param handler - the handler function to run when the event occurs + * @returns `this` to enable you to chain method calls. + */ + once(event: EventType, handler: Handler<any>): EventEmitter { + const onceHandler: Handler<any> = eventData => { + handler(eventData); + this.off(event, onceHandler); + }; + + return this.on(event, onceHandler); + } + + /** + * Gets the number of listeners for a given event. + * + * @param event - the event to get the listener count for + * @returns the number of listeners bound to the given event + */ + listenerCount(event: EventType): number { + return this.eventListenersCount(event); + } + + /** + * Removes all listeners. If given an event argument, it will remove only + * listeners for that event. + * @param event - the event to remove listeners for. + * @returns `this` to enable you to chain method calls. + */ + removeAllListeners(event?: EventType): EventEmitter { + if (event) { + this.eventsMap.delete(event); + } else { + this.eventsMap.clear(); + } + return this; + } + + private eventListenersCount(event: EventType): number { + return this.eventsMap.get(event)?.length || 0; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts new file mode 100644 index 0000000000..e00c45b2dd --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ExecutionContext.ts @@ -0,0 +1,402 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import type {ElementHandle} from '../api/ElementHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; +import type PuppeteerUtil from '../injected/injected.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; +import {stringifyFunction} from '../util/Function.js'; + +import {ARIAQueryHandler} from './AriaQueryHandler.js'; +import {Binding} from './Binding.js'; +import {CDPSession} from './Connection.js'; +import {CDPElementHandle} from './ElementHandle.js'; +import {IsolatedWorld} from './IsolatedWorld.js'; +import {CDPJSHandle} from './JSHandle.js'; +import {LazyArg} from './LazyArg.js'; +import {scriptInjector} from './ScriptInjector.js'; +import {EvaluateFunc, HandleFor} from './types.js'; +import { + createJSHandle, + getExceptionMessage, + isString, + valueFromRemoteObject, +} from './util.js'; + +/** + * @public + */ +export const EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__'; +const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; + +/** + * Represents a context for JavaScript execution. + * + * @example + * A {@link Page} can have several execution contexts: + * + * - Each {@link Frame} of a {@link Page | page} has a "default" execution + * context that is always created after frame is attached to DOM. This context + * is returned by the {@link Frame.executionContext} method. + * - Each {@link https://developer.chrome.com/extensions | Chrome extensions} + * creates additional execution contexts to isolate their code. + * + * @remarks + * By definition, each context is isolated from one another, however they are + * all able to manipulate non-JavaScript resources (such as DOM). + * + * @remarks + * Besides pages, execution contexts can be found in + * {@link WebWorker | workers}. + * + * @internal + */ +export class ExecutionContext { + _client: CDPSession; + _world?: IsolatedWorld; + _contextId: number; + _contextName?: string; + + constructor( + client: CDPSession, + contextPayload: Protocol.Runtime.ExecutionContextDescription, + world?: IsolatedWorld + ) { + this._client = client; + this._world = world; + this._contextId = contextPayload.id; + if (contextPayload.name) { + this._contextName = contextPayload.name; + } + } + + #bindingsInstalled = false; + #puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>; + get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> { + let promise = Promise.resolve() as Promise<unknown>; + if (!this.#bindingsInstalled) { + promise = Promise.all([ + this.#installGlobalBinding( + new Binding( + '__ariaQuerySelector', + ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown + ) + ), + this.#installGlobalBinding( + new Binding('__ariaQuerySelectorAll', (async ( + element: ElementHandle<Node>, + selector: string + ): Promise<JSHandle<Node[]>> => { + const results = ARIAQueryHandler.queryAll(element, selector); + return element.executionContext().evaluateHandle((...elements) => { + return elements; + }, ...(await AsyncIterableUtil.collect(results))); + }) as (...args: unknown[]) => unknown) + ), + ]); + this.#bindingsInstalled = true; + } + scriptInjector.inject(script => { + if (this.#puppeteerUtil) { + void this.#puppeteerUtil.then(handle => { + void handle.dispose(); + }); + } + this.#puppeteerUtil = promise.then(() => { + return this.evaluateHandle(script) as Promise<JSHandle<PuppeteerUtil>>; + }); + }, !this.#puppeteerUtil); + return this.#puppeteerUtil as Promise<JSHandle<PuppeteerUtil>>; + } + + async #installGlobalBinding(binding: Binding) { + try { + if (this._world) { + this._world._bindings.set(binding.name, binding); + await this._world._addBindingToContext(this, binding.name); + } + } catch { + // If the binding cannot be added, then either the browser doesn't support + // bindings (e.g. Firefox) or the context is broken. Either breakage is + // okay, so we ignore the error. + } + } + + /** + * Evaluates the given function. + * + * @example + * + * ```ts + * const executionContext = await page.mainFrame().executionContext(); + * const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ; + * console.log(result); // prints "56" + * ``` + * + * @example + * A string can also be passed in instead of a function: + * + * ```ts + * console.log(await executionContext.evaluate('1 + 2')); // prints "3" + * ``` + * + * @example + * Handles can also be passed as `args`. They resolve to their referenced object: + * + * ```ts + * 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'. + * ``` + * + * @param pageFunction - The function to evaluate. + * @param args - Additional arguments to pass into the function. + * @returns The result of evaluating the function. If the result is an object, + * a vanilla object containing the serializable properties of the result is + * returned. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return await this.#evaluate(true, pageFunction, ...args); + } + + /** + * Evaluates the given function. + * + * Unlike {@link ExecutionContext.evaluate | evaluate}, this method returns a + * handle to the result of the function. + * + * This method may be better suited if the object cannot be serialized (e.g. + * `Map`) and requires further manipulation. + * + * @example + * + * ```ts + * const context = await page.mainFrame().executionContext(); + * const handle: JSHandle<typeof globalThis> = await context.evaluateHandle( + * () => Promise.resolve(self) + * ); + * ``` + * + * @example + * A string can also be passed in instead of a function. + * + * ```ts + * const handle: JSHandle<number> = await context.evaluateHandle('1 + 2'); + * ``` + * + * @example + * Handles can also be passed as `args`. They resolve to their referenced object: + * + * ```ts + * const bodyHandle: ElementHandle<HTMLBodyElement> = + * await context.evaluateHandle(() => { + * return document.body; + * }); + * const stringHandle: JSHandle<string> = await context.evaluateHandle( + * body => body.innerHTML, + * body + * ); + * console.log(await stringHandle.jsonValue()); // prints body's innerHTML + * // Always dispose your garbage! :) + * await bodyHandle.dispose(); + * await stringHandle.dispose(); + * ``` + * + * @param pageFunction - The function to evaluate. + * @param args - Additional arguments to pass into the function. + * @returns A {@link JSHandle | handle} to the result of evaluating the + * function. If the result is a `Node`, then this will return an + * {@link ElementHandle | element handle}. + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.#evaluate(false, pageFunction, ...args); + } + + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: true, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: false, + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: boolean, + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { + const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; + + if (isString(pageFunction)) { + const contextId = this._contextId; + const expression = pageFunction; + const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) + ? expression + : expression + '\n' + suffix; + + const {exceptionDetails, result: remoteObject} = await this._client + .send('Runtime.evaluate', { + expression: expressionWithSourceUrl, + contextId, + returnByValue, + awaitPromise: true, + userGesture: true, + }) + .catch(rewriteError); + + if (exceptionDetails) { + throw new Error( + 'Evaluation failed: ' + getExceptionMessage(exceptionDetails) + ); + } + + return returnByValue + ? valueFromRemoteObject(remoteObject) + : createJSHandle(this, remoteObject); + } + + let callFunctionOnPromise; + try { + callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { + functionDeclaration: `${stringifyFunction(pageFunction)}\n${suffix}\n`, + executionContextId: this._contextId, + arguments: await Promise.all(args.map(convertArgument.bind(this))), + returnByValue, + awaitPromise: true, + userGesture: true, + }); + } catch (error) { + if ( + error instanceof TypeError && + error.message.startsWith('Converting circular structure to JSON') + ) { + error.message += ' Recursive objects are not allowed.'; + } + throw error; + } + const {exceptionDetails, result: remoteObject} = + await callFunctionOnPromise.catch(rewriteError); + if (exceptionDetails) { + throw new Error( + 'Evaluation failed: ' + getExceptionMessage(exceptionDetails) + ); + } + return returnByValue + ? valueFromRemoteObject(remoteObject) + : createJSHandle(this, remoteObject); + + async function convertArgument( + this: ExecutionContext, + arg: unknown + ): Promise<Protocol.Runtime.CallArgument> { + if (arg instanceof LazyArg) { + arg = await arg.get(this); + } + if (typeof arg === 'bigint') { + // eslint-disable-line valid-typeof + return {unserializableValue: `${arg.toString()}n`}; + } + if (Object.is(arg, -0)) { + return {unserializableValue: '-0'}; + } + if (Object.is(arg, Infinity)) { + return {unserializableValue: 'Infinity'}; + } + if (Object.is(arg, -Infinity)) { + return {unserializableValue: '-Infinity'}; + } + if (Object.is(arg, NaN)) { + return {unserializableValue: 'NaN'}; + } + const objectHandle = + arg && (arg instanceof CDPJSHandle || arg instanceof CDPElementHandle) + ? arg + : null; + if (objectHandle) { + if (objectHandle.executionContext() !== this) { + throw new Error( + 'JSHandles can be evaluated only in the context they were created!' + ); + } + if (objectHandle.disposed) { + throw new Error('JSHandle is disposed!'); + } + if (objectHandle.remoteObject().unserializableValue) { + return { + unserializableValue: + objectHandle.remoteObject().unserializableValue, + }; + } + if (!objectHandle.remoteObject().objectId) { + return {value: objectHandle.remoteObject().value}; + } + return {objectId: objectHandle.remoteObject().objectId}; + } + return {value: arg}; + } + } +} + +const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => { + if (error.message.includes('Object reference chain is too long')) { + return {result: {type: 'undefined'}}; + } + if (error.message.includes("Object couldn't be returned by value")) { + return {result: {type: 'undefined'}}; + } + + if ( + error.message.endsWith('Cannot find context with specified id') || + error.message.endsWith('Inspected target navigated or closed') + ) { + throw new Error( + 'Execution context was destroyed, most likely because of a navigation.' + ); + } + throw error; +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts new file mode 100644 index 0000000000..96d6e6eb28 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FileChooser.ts @@ -0,0 +1,97 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {ElementHandle} from '../api/ElementHandle.js'; +import {assert} from '../util/assert.js'; + +/** + * File choosers let you react to the page requesting for a file. + * + * @remarks + * `FileChooser` instances are returned via the {@link Page.waitForFileChooser} method. + * + * 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. + * + * @example + * + * ```ts + * const [fileChooser] = await Promise.all([ + * page.waitForFileChooser(), + * page.click('#upload-file-button'), // some button that triggers file selection + * ]); + * await fileChooser.accept(['/tmp/myfile.pdf']); + * ``` + * + * @public + */ +export class FileChooser { + #element: ElementHandle<HTMLInputElement>; + #multiple: boolean; + #handled = false; + + /** + * @internal + */ + constructor( + element: ElementHandle<HTMLInputElement>, + event: Protocol.Page.FileChooserOpenedEvent + ) { + this.#element = element; + this.#multiple = event.mode !== 'selectSingle'; + } + + /** + * Whether file chooser allow for + * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple | multiple} + * file selection. + */ + isMultiple(): boolean { + return this.#multiple; + } + + /** + * Accept the file chooser request with the given file paths. + * + * @remarks This will not validate whether the file paths exists. Also, if a + * path is relative, then it is resolved against the + * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. + * For locals script connecting to remote chrome environments, paths must be + * absolute. + */ + async accept(paths: string[]): Promise<void> { + assert( + !this.#handled, + 'Cannot accept FileChooser which is already handled!' + ); + this.#handled = true; + await this.#element.uploadFile(...paths); + } + + /** + * Closes the file chooser without selecting any files. + */ + cancel(): void { + assert( + !this.#handled, + 'Cannot cancel FileChooser which is already handled!' + ); + this.#handled = true; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FirefoxTargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FirefoxTargetManager.ts new file mode 100644 index 0000000000..745c37d950 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FirefoxTargetManager.ts @@ -0,0 +1,259 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; + +import {TargetFilterCallback} from '../api/Browser.js'; +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {CDPSession, Connection} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {Target} from './Target.js'; +import { + TargetFactory, + TargetInterceptor, + TargetManagerEmittedEvents, + TargetManager, +} from './TargetManager.js'; + +/** + * FirefoxTargetManager implements target management using + * `Target.setDiscoverTargets` without using auto-attach. It, therefore, creates + * targets that lazily establish their CDP sessions. + * + * Although the approach is potentially flaky, there is no other way for Firefox + * because Firefox's CDP implementation does not support auto-attach. + * + * Firefox does not support targetInfoChanged and detachedFromTarget events: + * + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1610855 + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1636979 + * @internal + */ +export class FirefoxTargetManager + extends EventEmitter + implements TargetManager +{ + #connection: Connection; + /** + * Keeps track of the following events: 'Target.targetCreated', + * 'Target.targetDestroyed'. + * + * A target becomes discovered when 'Target.targetCreated' is received. + * A target is removed from this map once 'Target.targetDestroyed' is + * received. + * + * `targetFilterCallback` has no effect on this map. + */ + #discoveredTargetsByTargetId: Map<string, Protocol.Target.TargetInfo> = + new Map(); + /** + * Keeps track of targets that were created via 'Target.targetCreated' + * and which one are not filtered out by `targetFilterCallback`. + * + * The target is removed from here once it's been destroyed. + */ + #availableTargetsByTargetId: Map<string, Target> = new Map(); + /** + * Tracks which sessions attach to which target. + */ + #availableTargetsBySessionId: Map<string, Target> = new Map(); + /** + * If a target was filtered out by `targetFilterCallback`, we still receive + * events about it from CDP, but we don't forward them to the rest of Puppeteer. + */ + #ignoredTargets = new Set<string>(); + #targetFilterCallback: TargetFilterCallback | undefined; + #targetFactory: TargetFactory; + + #targetInterceptors: WeakMap<CDPSession | Connection, TargetInterceptor[]> = + new WeakMap(); + + #attachedToTargetListenersBySession: WeakMap< + CDPSession | Connection, + (event: Protocol.Target.AttachedToTargetEvent) => Promise<void> + > = new WeakMap(); + + #initializePromise = createDeferredPromise<void>(); + #targetsIdsForInit: Set<string> = new Set(); + + constructor( + connection: Connection, + targetFactory: TargetFactory, + targetFilterCallback?: TargetFilterCallback + ) { + super(); + this.#connection = connection; + this.#targetFilterCallback = targetFilterCallback; + this.#targetFactory = targetFactory; + + this.#connection.on('Target.targetCreated', this.#onTargetCreated); + this.#connection.on('Target.targetDestroyed', this.#onTargetDestroyed); + this.#connection.on('sessiondetached', this.#onSessionDetached); + this.setupAttachmentListeners(this.#connection); + } + + addTargetInterceptor( + client: CDPSession | Connection, + interceptor: TargetInterceptor + ): void { + const interceptors = this.#targetInterceptors.get(client) || []; + interceptors.push(interceptor); + this.#targetInterceptors.set(client, interceptors); + } + + removeTargetInterceptor( + client: CDPSession | Connection, + interceptor: TargetInterceptor + ): void { + const interceptors = this.#targetInterceptors.get(client) || []; + this.#targetInterceptors.set( + client, + interceptors.filter(currentInterceptor => { + return currentInterceptor !== interceptor; + }) + ); + } + + setupAttachmentListeners(session: CDPSession | Connection): void { + const listener = (event: Protocol.Target.AttachedToTargetEvent) => { + return this.#onAttachedToTarget(session, event); + }; + assert(!this.#attachedToTargetListenersBySession.has(session)); + this.#attachedToTargetListenersBySession.set(session, listener); + session.on('Target.attachedToTarget', listener); + } + + #onSessionDetached = (session: CDPSession) => { + this.removeSessionListeners(session); + this.#targetInterceptors.delete(session); + this.#availableTargetsBySessionId.delete(session.id()); + }; + + removeSessionListeners(session: CDPSession): void { + if (this.#attachedToTargetListenersBySession.has(session)) { + session.off( + 'Target.attachedToTarget', + this.#attachedToTargetListenersBySession.get(session)! + ); + this.#attachedToTargetListenersBySession.delete(session); + } + } + + getAvailableTargets(): Map<string, Target> { + return this.#availableTargetsByTargetId; + } + + dispose(): void { + this.#connection.off('Target.targetCreated', this.#onTargetCreated); + this.#connection.off('Target.targetDestroyed', this.#onTargetDestroyed); + } + + async initialize(): Promise<void> { + await this.#connection.send('Target.setDiscoverTargets', { + discover: true, + filter: [{}], + }); + this.#targetsIdsForInit = new Set(this.#discoveredTargetsByTargetId.keys()); + await this.#initializePromise; + } + + #onTargetCreated = async ( + event: Protocol.Target.TargetCreatedEvent + ): Promise<void> => { + if (this.#discoveredTargetsByTargetId.has(event.targetInfo.targetId)) { + return; + } + + this.#discoveredTargetsByTargetId.set( + event.targetInfo.targetId, + event.targetInfo + ); + + if (event.targetInfo.type === 'browser' && event.targetInfo.attached) { + const target = this.#targetFactory(event.targetInfo, undefined); + this.#availableTargetsByTargetId.set(event.targetInfo.targetId, target); + this.#finishInitializationIfReady(target._targetId); + return; + } + + if ( + this.#targetFilterCallback && + !this.#targetFilterCallback(event.targetInfo) + ) { + this.#ignoredTargets.add(event.targetInfo.targetId); + this.#finishInitializationIfReady(event.targetInfo.targetId); + return; + } + + const target = this.#targetFactory(event.targetInfo, undefined); + this.#availableTargetsByTargetId.set(event.targetInfo.targetId, target); + this.emit(TargetManagerEmittedEvents.TargetAvailable, target); + this.#finishInitializationIfReady(target._targetId); + }; + + #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent): void => { + this.#discoveredTargetsByTargetId.delete(event.targetId); + this.#finishInitializationIfReady(event.targetId); + const target = this.#availableTargetsByTargetId.get(event.targetId); + if (target) { + this.emit(TargetManagerEmittedEvents.TargetGone, target); + this.#availableTargetsByTargetId.delete(event.targetId); + } + }; + + #onAttachedToTarget = async ( + parentSession: Connection | CDPSession, + event: Protocol.Target.AttachedToTargetEvent + ) => { + const targetInfo = event.targetInfo; + const session = this.#connection.session(event.sessionId); + if (!session) { + throw new Error(`Session ${event.sessionId} was not created.`); + } + + const target = this.#availableTargetsByTargetId.get(targetInfo.targetId); + + assert(target, `Target ${targetInfo.targetId} is missing`); + + this.setupAttachmentListeners(session); + + this.#availableTargetsBySessionId.set( + session.id(), + this.#availableTargetsByTargetId.get(targetInfo.targetId)! + ); + + for (const hook of this.#targetInterceptors.get(parentSession) || []) { + if (!(parentSession instanceof Connection)) { + assert(this.#availableTargetsBySessionId.has(parentSession.id())); + } + await hook( + target, + parentSession instanceof Connection + ? null + : this.#availableTargetsBySessionId.get(parentSession.id())! + ); + } + }; + + #finishInitializationIfReady(targetId: string): void { + this.#targetsIdsForInit.delete(targetId); + if (this.#targetsIdsForInit.size === 0) { + this.#initializePromise.resolve(); + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Frame.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Frame.ts new file mode 100644 index 0000000000..b605e60637 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Frame.ts @@ -0,0 +1,1160 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {type ClickOptions, ElementHandle} from '../api/ElementHandle.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; +import {Page, WaitTimeoutOptions} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {CDPSession} from './Connection.js'; +import { + DeviceRequestPrompt, + DeviceRequestPromptManager, +} from './DeviceRequestPrompt.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {FrameManager} from './FrameManager.js'; +import {getQueryHandlerAndSelector} from './GetQueryHandler.js'; +import { + IsolatedWorld, + IsolatedWorldChart, + WaitForSelectorOptions, +} from './IsolatedWorld.js'; +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import {LazyArg} from './LazyArg.js'; +import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; +import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; +import {importFSPromises} from './util.js'; + +/** + * @public + */ +export interface FrameWaitForFunctionOptions { + /** + * An interval at which the `pageFunction` is executed, defaults to `raf`. If + * `polling` is a number, then it is treated as an interval in milliseconds at + * which the function would be executed. If `polling` is a string, then it can + * be one of the following values: + * + * - `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` + * callback. This is the tightest polling mode which is suitable to observe + * styling changes. + * + * - `mutation` - to execute `pageFunction` on every DOM mutation. + */ + polling?: 'raf' | 'mutation' | number; + /** + * Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds). + * Pass `0` to disable the timeout. Puppeteer's default timeout can be changed + * using {@link Page.setDefaultTimeout}. + */ + timeout?: number; + /** + * A signal object that allows you to cancel a waitForFunction call. + */ + signal?: AbortSignal; +} + +/** + * @public + */ +export interface FrameAddScriptTagOptions { + /** + * URL of the script to be added. + */ + url?: string; + /** + * Path to a JavaScript file to be injected into the frame. + * + * @remarks + * If `path` is a relative path, it is resolved relative to the current + * working directory (`process.cwd()` in Node.js). + */ + path?: string; + /** + * JavaScript to be injected into the frame. + */ + content?: string; + /** + * Sets the `type` of the script. Use `module` in order to load an ES2015 module. + */ + type?: string; + /** + * Sets the `id` of the script. + */ + id?: string; +} + +/** + * @public + */ +export interface FrameAddStyleTagOptions { + /** + * the URL of the CSS file to be added. + */ + url?: string; + /** + * The path to a CSS file to be injected into the frame. + * @remarks + * If `path` is a relative path, it is resolved relative to the current + * working directory (`process.cwd()` in Node.js). + */ + path?: string; + /** + * Raw CSS content to be injected into the frame. + */ + content?: string; +} + +/** + * Represents a DOM frame. + * + * To understand frames, you can think of frames as `<iframe>` elements. Just + * like iframes, frames can be nested, and when JavaScript is executed in a + * frame, the JavaScript does not effect frames inside the ambient frame the + * JavaScript executes in. + * + * @example + * At any point in time, {@link Page | pages} expose their current frame + * tree via the {@link Page.mainFrame} and {@link Frame.childFrames} methods. + * + * @example + * An example of dumping frame tree: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com/chrome/browser/canary.html'); + * dumpFrameTree(page.mainFrame(), ''); + * await browser.close(); + * + * function dumpFrameTree(frame, indent) { + * console.log(indent + frame.url()); + * for (const child of frame.childFrames()) { + * dumpFrameTree(child, indent + ' '); + * } + * } + * })(); + * ``` + * + * @example + * An example of getting text from an iframe element: + * + * ```ts + * const frame = page.frames().find(frame => frame.name() === 'myframe'); + * const text = await frame.$eval('.selector', element => element.textContent); + * console.log(text); + * ``` + * + * @remarks + * Frame lifecycles are controlled by three events that are all dispatched on + * the parent {@link Frame.page | page}: + * + * - {@link PageEmittedEvents.FrameAttached} + * - {@link PageEmittedEvents.FrameNavigated} + * - {@link PageEmittedEvents.FrameDetached} + * + * @public + */ +export class Frame { + #url = ''; + #detached = false; + #client!: CDPSession; + + /** + * @internal + */ + worlds!: IsolatedWorldChart; + /** + * @internal + */ + _frameManager: FrameManager; + /** + * @internal + */ + _id: string; + /** + * @internal + */ + _loaderId = ''; + /** + * @internal + */ + _name?: string; + /** + * @internal + */ + _hasStartedLoading = false; + /** + * @internal + */ + _lifecycleEvents = new Set<string>(); + /** + * @internal + */ + _parentId?: string; + + /** + * @internal + */ + constructor( + frameManager: FrameManager, + frameId: string, + parentFrameId: string | undefined, + client: CDPSession + ) { + this._frameManager = frameManager; + this.#url = ''; + this._id = frameId; + this._parentId = parentFrameId; + this.#detached = false; + + this._loaderId = ''; + + this.updateClient(client); + } + + /** + * @internal + */ + updateClient(client: CDPSession): void { + this.#client = client; + this.worlds = { + [MAIN_WORLD]: new IsolatedWorld(this), + [PUPPETEER_WORLD]: new IsolatedWorld(this), + }; + } + + /** + * The page associated with the frame. + */ + page(): Page { + return this._frameManager.page(); + } + + /** + * Is `true` if the frame is an out-of-process (OOP) frame. Otherwise, + * `false`. + */ + isOOPFrame(): boolean { + return this.#client !== this._frameManager.client; + } + + /** + * Navigates a frame to the given url. + * + * @remarks + * Navigation to `about:blank` or navigation to the same URL with a different + * hash will succeed and return `null`. + * + * :::warning + * + * Headless mode doesn't support navigation to a PDF document. See the {@link + * https://bugs.chromium.org/p/chromium/issues/detail?id=761295 | upstream + * issue}. + * + * ::: + * + * @param url - the URL to navigate the frame to. This should include the + * scheme, e.g. `https://`. + * @param options - navigation options. `waitUntil` is useful to define when + * the navigation should be considered successful - see the docs for + * {@link PuppeteerLifeCycleEvent} for more details. + * + * @returns A promise which resolves to the main resource response. In case of + * multiple redirects, the navigation will resolve with the response of the + * last redirect. + * @throws This method will throw an error if: + * + * - there's an SSL error (e.g. in case of self-signed certificates). + * - target URL is invalid. + * - the `timeout` is exceeded during navigation. + * - the remote server does not respond or is unreachable. + * - the main resource failed to load. + * + * This method will not throw an error when any valid HTTP status code is + * returned by the remote server, including 404 "Not Found" and 500 "Internal + * Server Error". The status code for such responses can be retrieved by + * calling {@link HTTPResponse.status}. + */ + async goto( + url: string, + options: { + referer?: string; + referrerPolicy?: string; + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + const { + referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'], + referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[ + 'referer-policy' + ], + waitUntil = ['load'], + timeout = this._frameManager.timeoutSettings.navigationTimeout(), + } = options; + + let ensureNewDocumentNavigation = false; + const watcher = new LifecycleWatcher( + this._frameManager, + this, + waitUntil, + timeout + ); + let error = await Promise.race([ + navigate( + this.#client, + url, + referer, + referrerPolicy as Protocol.Page.ReferrerPolicy, + this._id + ), + watcher.timeoutOrTerminationPromise(), + ]); + if (!error) { + error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + ensureNewDocumentNavigation + ? watcher.newDocumentNavigationPromise() + : watcher.sameDocumentNavigationPromise(), + ]); + } + + try { + if (error) { + throw error; + } + return await watcher.navigationResponse(); + } finally { + watcher.dispose(); + } + + async function navigate( + client: CDPSession, + url: string, + referrer: string | undefined, + referrerPolicy: Protocol.Page.ReferrerPolicy | undefined, + frameId: string + ): Promise<Error | null> { + try { + const response = await client.send('Page.navigate', { + url, + referrer, + frameId, + referrerPolicy, + }); + ensureNewDocumentNavigation = !!response.loaderId; + if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') { + return null; + } + return response.errorText + ? new Error(`${response.errorText} at ${url}`) + : null; + } catch (error) { + if (isErrorLike(error)) { + return error; + } + throw error; + } + } + } + + /** + * Waits for the frame to navigate. It is useful for when you run code which + * will indirectly cause the frame to navigate. + * + * Usage of the + * {@link https://developer.mozilla.org/en-US/docs/Web/API/History_API | History API} + * to change the URL is considered a navigation. + * + * @example + * + * ```ts + * const [response] = await Promise.all([ + * // The navigation promise resolves after navigation has finished + * frame.waitForNavigation(), + * // Clicking the link will indirectly cause a navigation + * frame.click('a.my-link'), + * ]); + * ``` + * + * @param options - options to configure when the navigation is consided + * finished. + * @returns a promise that resolves when the frame navigates to a new URL. + */ + async waitForNavigation( + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + const { + waitUntil = ['load'], + timeout = this._frameManager.timeoutSettings.navigationTimeout(), + } = options; + const watcher = new LifecycleWatcher( + this._frameManager, + this, + waitUntil, + timeout + ); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + watcher.sameDocumentNavigationPromise(), + watcher.newDocumentNavigationPromise(), + ]); + try { + if (error) { + throw error; + } + return await watcher.navigationResponse(); + } finally { + watcher.dispose(); + } + } + + /** + * @internal + */ + _client(): CDPSession { + return this.#client; + } + + /** + * @internal + */ + executionContext(): Promise<ExecutionContext> { + return this.worlds[MAIN_WORLD].executionContext(); + } + + /** + * Behaves identically to {@link Page.evaluateHandle} except it's run within + * the context of this frame. + * + * @see {@link Page.evaluateHandle} for details. + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args); + } + + /** + * Behaves identically to {@link Page.evaluate} except it's run within the + * the context of this frame. + * + * @see {@link Page.evaluate} for details. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args); + } + + /** + * Queries the frame for an element matching the given selector. + * + * @param selector - The selector to query for. + * @returns A {@link ElementHandle | element handle} to the first element + * matching the given selector. Otherwise, `null`. + */ + async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + return this.worlds[MAIN_WORLD].$(selector); + } + + /** + * Queries the frame for all elements matching the given selector. + * + * @param selector - The selector to query for. + * @returns An array of {@link ElementHandle | element handles} that point to + * elements matching the given selector. + */ + async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>> { + return this.worlds[MAIN_WORLD].$$(selector); + } + + /** + * Runs the given function on the first element matching the given selector in + * the frame. + * + * If the given function returns a promise, then this method will wait till + * the promise resolves. + * + * @example + * + * ```ts + * const searchValue = await frame.$eval('#search', el => el.value); + * ``` + * + * @param selector - The selector to query for. + * @param pageFunction - The function to be evaluated in the frame's context. + * The first element matching the selector will be passed to the function as + * its first argument. + * @param args - Additional arguments to pass to `pageFunction`. + * @returns A promise to the result of the function. + */ + async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args); + } + + /** + * Runs the given function on an array of elements matching the given selector + * in the frame. + * + * If the given function returns a promise, then this method will wait till + * the promise resolves. + * + * @example + * + * ```js + * const divsCounts = await frame.$$eval('div', divs => divs.length); + * ``` + * + * @param selector - The selector to query for. + * @param pageFunction - The function to be evaluated in the frame's context. + * An array of elements matching the given selector will be passed to the + * function as its first argument. + * @param args - Additional arguments to pass to `pageFunction`. + * @returns A promise to the result of the function. + */ + async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args); + } + + /** + * @deprecated Use {@link Frame.$$} with the `xpath` prefix. + * + * Example: `await frame.$$('xpath/' + xpathExpression)` + * + * This method evaluates the given XPath expression and returns the results. + * If `xpath` starts with `//` instead of `.//`, the dot will be appended + * automatically. + * @param expression - the XPath expression to evaluate. + */ + async $x(expression: string): Promise<Array<ElementHandle<Node>>> { + return this.worlds[MAIN_WORLD].$x(expression); + } + + /** + * Waits for an element matching the given selector to appear in the frame. + * + * This method works across navigations. + * + * @example + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .mainFrame() + * .waitForSelector('img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param selector - The selector to query and wait for. + * @param options - Options for customizing waiting behavior. + * @returns An element matching the given selector. + * @throws Throws if an element matching the given selector doesn't appear. + */ + async waitForSelector<Selector extends string>( + selector: Selector, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + const {updatedSelector, QueryHandler} = + getQueryHandlerAndSelector(selector); + return (await QueryHandler.waitFor( + this, + updatedSelector, + options + )) as ElementHandle<NodeFor<Selector>> | null; + } + + /** + * @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix. + * + * Example: `await frame.waitForSelector('xpath/' + xpathExpression)` + * + * The method evaluates the XPath expression relative to the Frame. + * If `xpath` starts with `//` instead of `.//`, the dot will be appended + * automatically. + * + * Wait for the `xpath` to appear in page. If at the moment of calling the + * method the `xpath` already exists, the method will return immediately. If + * the xpath doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * For a code example, see the example for {@link Frame.waitForSelector}. That + * function behaves identically other than taking a CSS selector rather than + * an XPath. + * + * @param xpath - the XPath expression to wait for. + * @param options - options to configure the visibility of the element and how + * long to wait before timing out. + */ + async waitForXPath( + xpath: string, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle<Node> | null> { + if (xpath.startsWith('//')) { + xpath = `.${xpath}`; + } + return this.waitForSelector(`xpath/${xpath}`, options); + } + + /** + * @example + * The `waitForFunction` can be used to observe viewport size change: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * . const browser = await puppeteer.launch(); + * . const page = await browser.newPage(); + * . const watchDog = page.mainFrame().waitForFunction('window.innerWidth < 100'); + * . page.setViewport({width: 50, height: 50}); + * . await watchDog; + * . await browser.close(); + * })(); + * ``` + * + * To pass arguments from Node.js to the predicate of `page.waitForFunction` function: + * + * ```ts + * const selector = '.foo'; + * await frame.waitForFunction( + * selector => !!document.querySelector(selector), + * {}, // empty options object + * selector + * ); + * ``` + * + * @param pageFunction - the function to evaluate in the frame context. + * @param options - options to configure the polling method and timeout. + * @param args - arguments to pass to the `pageFunction`. + * @returns the promise which resolve when the `pageFunction` returns a truthy value. + */ + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + options: FrameWaitForFunctionOptions = {}, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.worlds[MAIN_WORLD].waitForFunction( + pageFunction, + options, + ...args + ) as Promise<HandleFor<Awaited<ReturnType<Func>>>>; + } + + /** + * The full HTML contents of the frame, including the DOCTYPE. + */ + async content(): Promise<string> { + return this.worlds[PUPPETEER_WORLD].content(); + } + + /** + * Set the content of the frame. + * + * @param html - HTML markup to assign to the page. + * @param options - Options to configure how long before timing out and at + * what point to consider the content setting successful. + */ + async setContent( + html: string, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<void> { + return this.worlds[PUPPETEER_WORLD].setContent(html, options); + } + + /** + * The frame's `name` attribute as specified in the tag. + * + * @remarks + * If the name is empty, it returns the `id` attribute instead. + * + * @remarks + * This value is calculated once when the frame is created, and will not + * update if the attribute is changed later. + */ + name(): string { + return this._name || ''; + } + + /** + * The frame's URL. + */ + url(): string { + return this.#url; + } + + /** + * The parent frame, if any. Detached and main frames return `null`. + */ + parentFrame(): Frame | null { + return this._frameManager._frameTree.parentFrame(this._id) || null; + } + + /** + * An array of child frames. + */ + childFrames(): Frame[] { + return this._frameManager._frameTree.childFrames(this._id); + } + + /** + * Is`true` if the frame has been detached. Otherwise, `false`. + */ + isDetached(): boolean { + return this.#detached; + } + + /** + * Adds a `<script>` tag into the page with the desired url or content. + * + * @param options - Options for the script. + * @returns An {@link ElementHandle | element handle} to the injected + * `<script>` element. + */ + async addScriptTag( + options: FrameAddScriptTagOptions + ): Promise<ElementHandle<HTMLScriptElement>> { + let {content = '', type} = options; + const {path} = options; + if (+!!options.url + +!!path + +!!content !== 1) { + throw new Error( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + } + + if (path) { + const fs = await importFSPromises(); + content = await fs.readFile(path, 'utf8'); + content += `//# sourceURL=${path.replace(/\n/g, '')}`; + } + + type = type ?? 'text/javascript'; + + return this.worlds[MAIN_WORLD].transferHandle( + await this.worlds[PUPPETEER_WORLD].evaluateHandle( + async ({createDeferredPromise}, {url, id, type, content}) => { + const promise = createDeferredPromise<void>(); + const script = document.createElement('script'); + script.type = type; + script.text = content; + if (url) { + script.src = url; + script.addEventListener( + 'load', + () => { + return promise.resolve(); + }, + {once: true} + ); + script.addEventListener( + 'error', + event => { + promise.reject( + new Error(event.message ?? 'Could not load script') + ); + }, + {once: true} + ); + } else { + promise.resolve(); + } + if (id) { + script.id = id; + } + document.head.appendChild(script); + await promise; + return script; + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + {...options, type, content} + ) + ); + } + + /** + * Adds a `<link rel="stylesheet">` tag into the page with the desired URL or + * a `<style type="text/css">` tag with the content. + * + * @returns An {@link ElementHandle | element handle} to the loaded `<link>` + * or `<style>` element. + */ + async addStyleTag( + options: Omit<FrameAddStyleTagOptions, 'url'> + ): Promise<ElementHandle<HTMLStyleElement>>; + async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLLinkElement>>; + async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> { + let {content = ''} = options; + const {path} = options; + if (+!!options.url + +!!path + +!!content !== 1) { + throw new Error( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + } + + if (path) { + const fs = await importFSPromises(); + + content = await fs.readFile(path, 'utf8'); + content += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; + options.content = content; + } + + return this.worlds[MAIN_WORLD].transferHandle( + await this.worlds[PUPPETEER_WORLD].evaluateHandle( + async ({createDeferredPromise}, {url, content}) => { + const promise = createDeferredPromise<void>(); + let element: HTMLStyleElement | HTMLLinkElement; + if (!url) { + element = document.createElement('style'); + element.appendChild(document.createTextNode(content!)); + } else { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + element = link; + } + element.addEventListener( + 'load', + () => { + promise.resolve(); + }, + {once: true} + ); + element.addEventListener( + 'error', + event => { + promise.reject( + new Error( + (event as ErrorEvent).message ?? 'Could not load style' + ) + ); + }, + {once: true} + ); + document.head.appendChild(element); + await promise; + return element; + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + options + ) + ); + } + + /** + * Clicks the first element found that matches `selector`. + * + * @remarks + * If `click()` triggers a navigation event and there's a separate + * `page.waitForNavigation()` promise to be resolved, you may end up with a + * race condition that yields unexpected results. The correct pattern for + * click and wait for navigation is the following: + * + * ```ts + * const [response] = await Promise.all([ + * page.waitForNavigation(waitOptions), + * frame.click(selector, clickOptions), + * ]); + * ``` + * + * @param selector - The selector to query for. + */ + async click( + selector: string, + options: Readonly<ClickOptions> = {} + ): Promise<void> { + return this.worlds[PUPPETEER_WORLD].click(selector, options); + } + + /** + * Focuses the first element that matches the `selector`. + * + * @param selector - The selector to query for. + * @throws Throws if there's no element matching `selector`. + */ + async focus(selector: string): Promise<void> { + return this.worlds[PUPPETEER_WORLD].focus(selector); + } + + /** + * Hovers the pointer over the center of the first element that matches the + * `selector`. + * + * @param selector - The selector to query for. + * @throws Throws if there's no element matching `selector`. + */ + async hover(selector: string): Promise<void> { + return this.worlds[PUPPETEER_WORLD].hover(selector); + } + + /** + * Selects a set of value on the first `<select>` element that matches the + * `selector`. + * + * @example + * + * ```ts + * frame.select('select#colors', 'blue'); // single selection + * frame.select('select#colors', 'red', 'green', 'blue'); // multiple selections + * ``` + * + * @param selector - The selector to query for. + * @param values - The array of values to select. If the `<select>` has the + * `multiple` attribute, all values are considered, otherwise only the first + * one is taken into account. + * @returns the list of values that were successfully selected. + * @throws Throws if there's no `<select>` matching `selector`. + */ + select(selector: string, ...values: string[]): Promise<string[]> { + return this.worlds[PUPPETEER_WORLD].select(selector, ...values); + } + + /** + * Taps the first element that matches the `selector`. + * + * @param selector - The selector to query for. + * @throws Throws if there's no element matching `selector`. + */ + async tap(selector: string): Promise<void> { + return this.worlds[PUPPETEER_WORLD].tap(selector); + } + + /** + * Sends a `keydown`, `keypress`/`input`, and `keyup` event for each character + * in the text. + * + * @remarks + * To press a special key, like `Control` or `ArrowDown`, use + * {@link Keyboard.press}. + * + * @example + * + * ```ts + * await frame.type('#mytextarea', 'Hello'); // Types instantly + * await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a user + * ``` + * + * @param selector - the selector for the element to type into. If there are + * multiple the first will be used. + * @param text - text to type into the element + * @param options - takes one option, `delay`, which sets the time to wait + * between key presses in milliseconds. Defaults to `0`. + */ + async type( + selector: string, + text: string, + options?: {delay: number} + ): Promise<void> { + return this.worlds[PUPPETEER_WORLD].type(selector, text, options); + } + + /** + * @deprecated Replace with `new Promise(r => setTimeout(r, milliseconds));`. + * + * Causes your script to wait for the given number of milliseconds. + * + * @remarks + * It's generally recommended to not wait for a number of seconds, but instead + * use {@link Frame.waitForSelector}, {@link Frame.waitForXPath} or + * {@link Frame.waitForFunction} to wait for exactly the conditions you want. + * + * @example + * + * Wait for 1 second: + * + * ```ts + * await frame.waitForTimeout(1000); + * ``` + * + * @param milliseconds - the number of milliseconds to wait. + */ + waitForTimeout(milliseconds: number): Promise<void> { + return new Promise(resolve => { + setTimeout(resolve, milliseconds); + }); + } + + /** + * The frame's title. + */ + async title(): Promise<string> { + return this.worlds[PUPPETEER_WORLD].title(); + } + + /** + * @internal + */ + _deviceRequestPromptManager(): DeviceRequestPromptManager { + if (this.isOOPFrame()) { + return this._frameManager._deviceRequestPromptManager(this.#client); + } + const parentFrame = this.parentFrame(); + assert(parentFrame !== null); + return parentFrame._deviceRequestPromptManager(); + } + + /** + * This method is typically coupled with an action that triggers a device + * request from an api such as WebBluetooth. + * + * :::caution + * + * This must be called before the device request is made. It will not return a + * currently active device prompt. + * + * ::: + * + * @example + * + * ```ts + * const [devicePrompt] = Promise.all([ + * frame.waitForDevicePrompt(), + * frame.click('#connect-bluetooth'), + * ]); + * await devicePrompt.select( + * await devicePrompt.waitForDevice(({name}) => name.includes('My Device')) + * ); + * ``` + */ + waitForDevicePrompt( + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPrompt> { + return this._deviceRequestPromptManager().waitForDevicePrompt(options); + } + + /** + * @internal + */ + _navigated(framePayload: Protocol.Page.Frame): void { + this._name = framePayload.name; + this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`; + } + + /** + * @internal + */ + _navigatedWithinDocument(url: string): void { + this.#url = url; + } + + /** + * @internal + */ + _onLifecycleEvent(loaderId: string, name: string): void { + if (name === 'init') { + this._loaderId = loaderId; + this._lifecycleEvents.clear(); + } + this._lifecycleEvents.add(name); + } + + /** + * @internal + */ + _onLoadingStopped(): void { + this._lifecycleEvents.add('DOMContentLoaded'); + this._lifecycleEvents.add('load'); + } + + /** + * @internal + */ + _onLoadingStarted(): void { + this._hasStartedLoading = true; + } + + /** + * @internal + */ + _detach(): void { + this.#detached = true; + this.worlds[MAIN_WORLD]._detach(); + this.worlds[PUPPETEER_WORLD]._detach(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameManager.ts new file mode 100644 index 0000000000..148fe34095 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameManager.ts @@ -0,0 +1,478 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {Page} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {CDPSession, isTargetClosedError} from './Connection.js'; +import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js'; +import {EventEmitter} from './EventEmitter.js'; +import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {FrameTree} from './FrameTree.js'; +import {IsolatedWorld} from './IsolatedWorld.js'; +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import {NetworkManager} from './NetworkManager.js'; +import {Target} from './Target.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; +import {debugError} from './util.js'; + +const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; + +/** + * We use symbols to prevent external parties listening to these events. + * They are internal to Puppeteer. + * + * @internal + */ +export const FrameManagerEmittedEvents = { + FrameAttached: Symbol('FrameManager.FrameAttached'), + FrameNavigated: Symbol('FrameManager.FrameNavigated'), + FrameDetached: Symbol('FrameManager.FrameDetached'), + FrameSwapped: Symbol('FrameManager.FrameSwapped'), + LifecycleEvent: Symbol('FrameManager.LifecycleEvent'), + FrameNavigatedWithinDocument: Symbol( + 'FrameManager.FrameNavigatedWithinDocument' + ), + ExecutionContextCreated: Symbol('FrameManager.ExecutionContextCreated'), + ExecutionContextDestroyed: Symbol('FrameManager.ExecutionContextDestroyed'), +}; + +/** + * A frame manager manages the frames for a given {@link Page | page}. + * + * @internal + */ +export class FrameManager extends EventEmitter { + #page: Page; + #networkManager: NetworkManager; + #timeoutSettings: TimeoutSettings; + #contextIdToContext = new Map<string, ExecutionContext>(); + #isolatedWorlds = new Set<string>(); + #client: CDPSession; + /** + * @internal + */ + _frameTree = new FrameTree(); + + /** + * Set of frame IDs stored to indicate if a frame has received a + * frameNavigated event so that frame tree responses could be ignored as the + * frameNavigated event usually contains the latest information. + */ + #frameNavigatedReceived = new Set<string>(); + + #deviceRequestPromptManagerMap = new WeakMap< + CDPSession, + DeviceRequestPromptManager + >(); + + get timeoutSettings(): TimeoutSettings { + return this.#timeoutSettings; + } + + get networkManager(): NetworkManager { + return this.#networkManager; + } + + get client(): CDPSession { + return this.#client; + } + + constructor( + client: CDPSession, + page: Page, + ignoreHTTPSErrors: boolean, + timeoutSettings: TimeoutSettings + ) { + super(); + this.#client = client; + this.#page = page; + this.#networkManager = new NetworkManager(client, ignoreHTTPSErrors, this); + this.#timeoutSettings = timeoutSettings; + this.setupEventListeners(this.#client); + } + + private setupEventListeners(session: CDPSession) { + session.on('Page.frameAttached', event => { + this.#onFrameAttached(session, event.frameId, event.parentFrameId); + }); + session.on('Page.frameNavigated', event => { + this.#frameNavigatedReceived.add(event.frame.id); + void this.#onFrameNavigated(event.frame); + }); + session.on('Page.navigatedWithinDocument', event => { + this.#onFrameNavigatedWithinDocument(event.frameId, event.url); + }); + session.on( + 'Page.frameDetached', + (event: Protocol.Page.FrameDetachedEvent) => { + this.#onFrameDetached( + event.frameId, + event.reason as Protocol.Page.FrameDetachedEventReason + ); + } + ); + session.on('Page.frameStartedLoading', event => { + this.#onFrameStartedLoading(event.frameId); + }); + session.on('Page.frameStoppedLoading', event => { + this.#onFrameStoppedLoading(event.frameId); + }); + session.on('Runtime.executionContextCreated', event => { + this.#onExecutionContextCreated(event.context, session); + }); + session.on('Runtime.executionContextDestroyed', event => { + this.#onExecutionContextDestroyed(event.executionContextId, session); + }); + session.on('Runtime.executionContextsCleared', () => { + this.#onExecutionContextsCleared(session); + }); + session.on('Page.lifecycleEvent', event => { + this.#onLifecycleEvent(event); + }); + } + + async initialize(client: CDPSession = this.#client): Promise<void> { + try { + const result = await Promise.all([ + client.send('Page.enable'), + client.send('Page.getFrameTree'), + ]); + + const {frameTree} = result[1]; + this.#handleFrameTree(client, frameTree); + await Promise.all([ + client.send('Page.setLifecycleEventsEnabled', {enabled: true}), + client.send('Runtime.enable').then(() => { + return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME); + }), + // TODO: Network manager is not aware of OOP iframes yet. + client === this.#client + ? this.#networkManager.initialize() + : Promise.resolve(), + ]); + } catch (error) { + // The target might have been closed before the initialization finished. + if (isErrorLike(error) && isTargetClosedError(error)) { + return; + } + + throw error; + } + } + + executionContextById( + contextId: number, + session: CDPSession = this.#client + ): ExecutionContext { + const context = this.getExecutionContextById(contextId, session); + assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId); + return context; + } + + getExecutionContextById( + contextId: number, + session: CDPSession = this.#client + ): ExecutionContext | undefined { + return this.#contextIdToContext.get(`${session.id()}:${contextId}`); + } + + page(): Page { + return this.#page; + } + + mainFrame(): Frame { + const mainFrame = this._frameTree.getMainFrame(); + assert(mainFrame, 'Requesting main frame too early!'); + return mainFrame; + } + + frames(): Frame[] { + return Array.from(this._frameTree.frames()); + } + + frame(frameId: string): Frame | null { + return this._frameTree.getById(frameId) || null; + } + + onAttachedToTarget(target: Target): void { + if (target._getTargetInfo().type !== 'iframe') { + return; + } + + const frame = this.frame(target._getTargetInfo().targetId); + if (frame) { + frame.updateClient(target._session()!); + } + this.setupEventListeners(target._session()!); + void this.initialize(target._session()); + } + + /** + * @internal + */ + _deviceRequestPromptManager(client: CDPSession): DeviceRequestPromptManager { + let manager = this.#deviceRequestPromptManagerMap.get(client); + if (manager === undefined) { + manager = new DeviceRequestPromptManager(client, this.#timeoutSettings); + this.#deviceRequestPromptManagerMap.set(client, manager); + } + return manager; + } + + #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { + const frame = this.frame(event.frameId); + if (!frame) { + return; + } + frame._onLifecycleEvent(event.loaderId, event.name); + this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); + } + + #onFrameStartedLoading(frameId: string): void { + const frame = this.frame(frameId); + if (!frame) { + return; + } + frame._onLoadingStarted(); + } + + #onFrameStoppedLoading(frameId: string): void { + const frame = this.frame(frameId); + if (!frame) { + return; + } + frame._onLoadingStopped(); + this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); + } + + #handleFrameTree( + session: CDPSession, + frameTree: Protocol.Page.FrameTree + ): void { + if (frameTree.frame.parentId) { + this.#onFrameAttached( + session, + frameTree.frame.id, + frameTree.frame.parentId + ); + } + if (!this.#frameNavigatedReceived.has(frameTree.frame.id)) { + void this.#onFrameNavigated(frameTree.frame); + } else { + this.#frameNavigatedReceived.delete(frameTree.frame.id); + } + + if (!frameTree.childFrames) { + return; + } + + for (const child of frameTree.childFrames) { + this.#handleFrameTree(session, child); + } + } + + #onFrameAttached( + session: CDPSession, + frameId: string, + parentFrameId: string + ): void { + let frame = this.frame(frameId); + if (frame) { + if (session && frame.isOOPFrame()) { + // If an OOP iframes becomes a normal iframe again + // it is first attached to the parent page before + // the target is removed. + frame.updateClient(session); + } + return; + } + + frame = new Frame(this, frameId, parentFrameId, session); + this._frameTree.addFrame(frame); + this.emit(FrameManagerEmittedEvents.FrameAttached, frame); + } + + async #onFrameNavigated(framePayload: Protocol.Page.Frame): Promise<void> { + const frameId = framePayload.id; + const isMainFrame = !framePayload.parentId; + + let frame = this._frameTree.getById(frameId); + + // Detach all child frames first. + if (frame) { + for (const child of frame.childFrames()) { + this.#removeFramesRecursively(child); + } + } + + // Update or create main frame. + if (isMainFrame) { + if (frame) { + // Update frame id to retain frame identity on cross-process navigation. + this._frameTree.removeFrame(frame); + frame._id = frameId; + } else { + // Initial main frame navigation. + frame = new Frame(this, frameId, undefined, this.#client); + } + this._frameTree.addFrame(frame); + } + + frame = await this._frameTree.waitForFrame(frameId); + frame._navigated(framePayload); + this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); + } + + async #createIsolatedWorld(session: CDPSession, name: string): Promise<void> { + const key = `${session.id()}:${name}`; + + if (this.#isolatedWorlds.has(key)) { + return; + } + + await session.send('Page.addScriptToEvaluateOnNewDocument', { + source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, + worldName: name, + }); + + await Promise.all( + this.frames() + .filter(frame => { + return frame._client() === session; + }) + .map(frame => { + // Frames might be removed before we send this, so we don't want to + // throw an error. + return session + .send('Page.createIsolatedWorld', { + frameId: frame._id, + worldName: name, + grantUniveralAccess: true, + }) + .catch(debugError); + }) + ); + + this.#isolatedWorlds.add(key); + } + + #onFrameNavigatedWithinDocument(frameId: string, url: string): void { + const frame = this.frame(frameId); + if (!frame) { + return; + } + frame._navigatedWithinDocument(url); + this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame); + this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); + } + + #onFrameDetached( + frameId: string, + reason: Protocol.Page.FrameDetachedEventReason + ): void { + const frame = this.frame(frameId); + if (reason === 'remove') { + // Only remove the frame if the reason for the detached event is + // an actual removement of the frame. + // For frames that become OOP iframes, the reason would be 'swap'. + if (frame) { + this.#removeFramesRecursively(frame); + } + } else if (reason === 'swap') { + this.emit(FrameManagerEmittedEvents.FrameSwapped, frame); + } + } + + #onExecutionContextCreated( + contextPayload: Protocol.Runtime.ExecutionContextDescription, + session: CDPSession + ): void { + const auxData = contextPayload.auxData as {frameId?: string} | undefined; + const frameId = auxData && auxData.frameId; + const frame = typeof frameId === 'string' ? this.frame(frameId) : undefined; + let world: IsolatedWorld | undefined; + if (frame) { + // Only care about execution contexts created for the current session. + if (frame._client() !== session) { + return; + } + if (contextPayload.auxData && contextPayload.auxData['isDefault']) { + world = frame.worlds[MAIN_WORLD]; + } else if ( + contextPayload.name === UTILITY_WORLD_NAME && + !frame.worlds[PUPPETEER_WORLD].hasContext() + ) { + // In case of multiple sessions to the same target, there's a race between + // connections so we might end up creating multiple isolated worlds. + // We can use either. + world = frame.worlds[PUPPETEER_WORLD]; + } + } + const context = new ExecutionContext( + frame?._client() || this.#client, + contextPayload, + world + ); + if (world) { + world.setContext(context); + } + const key = `${session.id()}:${contextPayload.id}`; + this.#contextIdToContext.set(key, context); + } + + #onExecutionContextDestroyed( + executionContextId: number, + session: CDPSession + ): void { + const key = `${session.id()}:${executionContextId}`; + const context = this.#contextIdToContext.get(key); + if (!context) { + return; + } + this.#contextIdToContext.delete(key); + if (context._world) { + context._world.clearContext(); + } + } + + #onExecutionContextsCleared(session: CDPSession): void { + for (const [key, context] of this.#contextIdToContext.entries()) { + // Make sure to only clear execution contexts that belong + // to the current session. + if (context._client !== session) { + continue; + } + if (context._world) { + context._world.clearContext(); + } + this.#contextIdToContext.delete(key); + } + } + + #removeFramesRecursively(frame: Frame): void { + for (const child of frame.childFrames()) { + this.#removeFramesRecursively(child); + } + frame._detach(); + this._frameTree.removeFrame(frame); + this.emit(FrameManagerEmittedEvents.FrameDetached, frame); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameTree.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameTree.ts new file mode 100644 index 0000000000..94ee3a45e5 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/FrameTree.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; + +import type {Frame} from './Frame.js'; + +/** + * Keeps track of the page frame tree and it's is managed by + * {@link FrameManager}. FrameTree uses frame IDs to reference frame and it + * means that referenced frames might not be in the tree anymore. Thus, the tree + * structure is eventually consistent. + * @internal + */ +export class FrameTree { + #frames = new Map<string, Frame>(); + // frameID -> parentFrameID + #parentIds = new Map<string, string>(); + // frameID -> childFrameIDs + #childIds = new Map<string, Set<string>>(); + #mainFrame?: Frame; + #waitRequests = new Map<string, Set<DeferredPromise<Frame>>>(); + + getMainFrame(): Frame | undefined { + return this.#mainFrame; + } + + getById(frameId: string): Frame | undefined { + return this.#frames.get(frameId); + } + + /** + * Returns a promise that is resolved once the frame with + * the given ID is added to the tree. + */ + waitForFrame(frameId: string): Promise<Frame> { + const frame = this.getById(frameId); + if (frame) { + return Promise.resolve(frame); + } + const deferred = createDeferredPromise<Frame>(); + const callbacks = + this.#waitRequests.get(frameId) || new Set<DeferredPromise<Frame>>(); + callbacks.add(deferred); + return deferred; + } + + frames(): Frame[] { + return Array.from(this.#frames.values()); + } + + addFrame(frame: Frame): void { + this.#frames.set(frame._id, frame); + if (frame._parentId) { + this.#parentIds.set(frame._id, frame._parentId); + if (!this.#childIds.has(frame._parentId)) { + this.#childIds.set(frame._parentId, new Set()); + } + this.#childIds.get(frame._parentId)!.add(frame._id); + } else { + this.#mainFrame = frame; + } + this.#waitRequests.get(frame._id)?.forEach(request => { + return request.resolve(frame); + }); + } + + removeFrame(frame: Frame): void { + this.#frames.delete(frame._id); + this.#parentIds.delete(frame._id); + if (frame._parentId) { + this.#childIds.get(frame._parentId)?.delete(frame._id); + } else { + this.#mainFrame = undefined; + } + } + + childFrames(frameId: string): Frame[] { + const childIds = this.#childIds.get(frameId); + if (!childIds) { + return []; + } + return Array.from(childIds) + .map(id => { + return this.getById(id); + }) + .filter((frame): frame is Frame => { + return frame !== undefined; + }); + } + + parentFrame(frameId: string): Frame | undefined { + const parentId = this.#parentIds.get(frameId); + return parentId ? this.getById(parentId) : undefined; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts new file mode 100644 index 0000000000..405f09e2c5 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/GetQueryHandler.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ARIAQueryHandler} from './AriaQueryHandler.js'; +import {customQueryHandlers} from './CustomQueryHandler.js'; +import {PierceQueryHandler} from './PierceQueryHandler.js'; +import {PQueryHandler} from './PQueryHandler.js'; +import type {QueryHandler} from './QueryHandler.js'; +import {TextQueryHandler} from './TextQueryHandler.js'; +import {XPathQueryHandler} from './XPathQueryHandler.js'; + +export const BUILTIN_QUERY_HANDLERS = Object.freeze({ + aria: ARIAQueryHandler, + pierce: PierceQueryHandler, + xpath: XPathQueryHandler, + text: TextQueryHandler, +}); + +const QUERY_SEPARATORS = ['=', '/']; + +/** + * @internal + */ +export function getQueryHandlerByName( + name: string +): typeof QueryHandler | undefined { + if (name in BUILTIN_QUERY_HANDLERS) { + return BUILTIN_QUERY_HANDLERS[name as 'aria']; + } + return customQueryHandlers.get(name); +} + +/** + * @internal + */ +export function getQueryHandlerAndSelector(selector: string): { + updatedSelector: string; + QueryHandler: typeof QueryHandler; +} { + for (const handlerMap of [ + customQueryHandlers.names().map(name => { + return [name, customQueryHandlers.get(name)!] as const; + }), + Object.entries(BUILTIN_QUERY_HANDLERS), + ]) { + for (const [name, QueryHandler] of handlerMap) { + for (const separator of QUERY_SEPARATORS) { + const prefix = `${name}${separator}`; + if (selector.startsWith(prefix)) { + selector = selector.slice(prefix.length); + return {updatedSelector: selector, QueryHandler}; + } + } + } + } + return {updatedSelector: selector, QueryHandler: PQueryHandler}; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPRequest.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPRequest.ts new file mode 100644 index 0000000000..3016df7054 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPRequest.ts @@ -0,0 +1,445 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import { + ContinueRequestOverrides, + ErrorCode, + headersArray, + HTTPRequest as BaseHTTPRequest, + InterceptResolutionAction, + InterceptResolutionState, + ResourceType, + ResponseForRequest, + STATUS_TEXTS, +} from '../api/HTTPRequest.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; +import {assert} from '../util/assert.js'; + +import {CDPSession} from './Connection.js'; +import {ProtocolError} from './Errors.js'; +import {Frame} from './Frame.js'; +import {debugError, isString} from './util.js'; + +/** + * @internal + */ +export class HTTPRequest extends BaseHTTPRequest { + override _requestId: string; + override _interceptionId: string | undefined; + override _failureText: string | null = null; + override _response: HTTPResponse | null = null; + override _fromMemoryCache = false; + override _redirectChain: HTTPRequest[]; + + #client: CDPSession; + #isNavigationRequest: boolean; + #allowInterception: boolean; + #interceptionHandled = false; + #url: string; + #resourceType: ResourceType; + + #method: string; + #postData?: string; + #headers: Record<string, string> = {}; + #frame: Frame | null; + #continueRequestOverrides: ContinueRequestOverrides; + #responseForRequest: Partial<ResponseForRequest> | null = null; + #abortErrorReason: Protocol.Network.ErrorReason | null = null; + #interceptResolutionState: InterceptResolutionState = { + action: InterceptResolutionAction.None, + }; + #interceptHandlers: Array<() => void | PromiseLike<any>>; + #initiator?: Protocol.Network.Initiator; + + override get client(): CDPSession { + return this.#client; + } + + constructor( + client: CDPSession, + frame: Frame | null, + interceptionId: string | undefined, + allowInterception: boolean, + data: { + /** + * Request identifier. + */ + requestId: Protocol.Network.RequestId; + /** + * Loader identifier. Empty string if the request is fetched from worker. + */ + loaderId?: Protocol.Network.LoaderId; + /** + * URL of the document this request is loaded for. + */ + documentURL?: string; + /** + * Request data. + */ + request: Protocol.Network.Request; + /** + * Request initiator. + */ + initiator?: Protocol.Network.Initiator; + /** + * Type of this resource. + */ + type?: Protocol.Network.ResourceType; + }, + redirectChain: HTTPRequest[] + ) { + super(); + this.#client = client; + this._requestId = data.requestId; + this.#isNavigationRequest = + data.requestId === data.loaderId && data.type === 'Document'; + this._interceptionId = interceptionId; + this.#allowInterception = allowInterception; + this.#url = data.request.url; + this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType; + this.#method = data.request.method; + this.#postData = data.request.postData; + this.#frame = frame; + this._redirectChain = redirectChain; + this.#continueRequestOverrides = {}; + this.#interceptHandlers = []; + this.#initiator = data.initiator; + + for (const [key, value] of Object.entries(data.request.headers)) { + this.#headers[key.toLowerCase()] = value; + } + } + + override url(): string { + return this.#url; + } + + override continueRequestOverrides(): ContinueRequestOverrides { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#continueRequestOverrides; + } + + override responseForRequest(): Partial<ResponseForRequest> | null { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#responseForRequest; + } + + override abortErrorReason(): Protocol.Network.ErrorReason | null { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#abortErrorReason; + } + + override interceptResolutionState(): InterceptResolutionState { + if (!this.#allowInterception) { + return {action: InterceptResolutionAction.Disabled}; + } + if (this.#interceptionHandled) { + return {action: InterceptResolutionAction.AlreadyHandled}; + } + return {...this.#interceptResolutionState}; + } + + override isInterceptResolutionHandled(): boolean { + return this.#interceptionHandled; + } + + override enqueueInterceptAction( + pendingHandler: () => void | PromiseLike<unknown> + ): void { + this.#interceptHandlers.push(pendingHandler); + } + + override async finalizeInterceptions(): Promise<void> { + await this.#interceptHandlers.reduce((promiseChain, interceptAction) => { + return promiseChain.then(interceptAction); + }, Promise.resolve()); + const {action} = this.interceptResolutionState(); + switch (action) { + case 'abort': + return this.#abort(this.#abortErrorReason); + case 'respond': + if (this.#responseForRequest === null) { + throw new Error('Response is missing for the interception'); + } + return this.#respond(this.#responseForRequest); + case 'continue': + return this.#continue(this.#continueRequestOverrides); + } + } + + override resourceType(): ResourceType { + return this.#resourceType; + } + + override method(): string { + return this.#method; + } + + override postData(): string | undefined { + return this.#postData; + } + + override headers(): Record<string, string> { + return this.#headers; + } + + override response(): HTTPResponse | null { + return this._response; + } + + override frame(): Frame | null { + return this.#frame; + } + + override isNavigationRequest(): boolean { + return this.#isNavigationRequest; + } + + override initiator(): Protocol.Network.Initiator | undefined { + return this.#initiator; + } + + override redirectChain(): HTTPRequest[] { + return this._redirectChain.slice(); + } + + override failure(): {errorText: string} | null { + if (!this._failureText) { + return null; + } + return { + errorText: this._failureText, + }; + } + + override async continue( + overrides: ContinueRequestOverrides = {}, + priority?: number + ): Promise<void> { + // Request interception is not supported for data: urls. + if (this.#url.startsWith('data:')) { + return; + } + assert(this.#allowInterception, 'Request Interception is not enabled!'); + assert(!this.#interceptionHandled, 'Request is already handled!'); + if (priority === undefined) { + return this.#continue(overrides); + } + this.#continueRequestOverrides = overrides; + if ( + this.#interceptResolutionState.priority === undefined || + priority > this.#interceptResolutionState.priority + ) { + this.#interceptResolutionState = { + action: InterceptResolutionAction.Continue, + priority, + }; + return; + } + if (priority === this.#interceptResolutionState.priority) { + if ( + this.#interceptResolutionState.action === 'abort' || + this.#interceptResolutionState.action === 'respond' + ) { + return; + } + this.#interceptResolutionState.action = + InterceptResolutionAction.Continue; + } + return; + } + + async #continue(overrides: ContinueRequestOverrides = {}): Promise<void> { + const {url, method, postData, headers} = overrides; + this.#interceptionHandled = true; + + const postDataBinaryBase64 = postData + ? Buffer.from(postData).toString('base64') + : undefined; + + if (this._interceptionId === undefined) { + throw new Error( + 'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest' + ); + } + await this.#client + .send('Fetch.continueRequest', { + requestId: this._interceptionId, + url, + method, + postData: postDataBinaryBase64, + headers: headers ? headersArray(headers) : undefined, + }) + .catch(error => { + this.#interceptionHandled = false; + return handleError(error); + }); + } + + override async respond( + response: Partial<ResponseForRequest>, + priority?: number + ): Promise<void> { + // Mocking responses for dataURL requests is not currently supported. + if (this.#url.startsWith('data:')) { + return; + } + assert(this.#allowInterception, 'Request Interception is not enabled!'); + assert(!this.#interceptionHandled, 'Request is already handled!'); + if (priority === undefined) { + return this.#respond(response); + } + this.#responseForRequest = response; + if ( + this.#interceptResolutionState.priority === undefined || + priority > this.#interceptResolutionState.priority + ) { + this.#interceptResolutionState = { + action: InterceptResolutionAction.Respond, + priority, + }; + return; + } + if (priority === this.#interceptResolutionState.priority) { + if (this.#interceptResolutionState.action === 'abort') { + return; + } + this.#interceptResolutionState.action = InterceptResolutionAction.Respond; + } + } + + async #respond(response: Partial<ResponseForRequest>): Promise<void> { + this.#interceptionHandled = true; + + const responseBody: Buffer | null = + response.body && isString(response.body) + ? Buffer.from(response.body) + : (response.body as Buffer) || null; + + const responseHeaders: Record<string, string | string[]> = {}; + if (response.headers) { + for (const header of Object.keys(response.headers)) { + const value = response.headers[header]; + + responseHeaders[header.toLowerCase()] = Array.isArray(value) + ? value.map(item => { + return String(item); + }) + : String(value); + } + } + if (response.contentType) { + responseHeaders['content-type'] = response.contentType; + } + if (responseBody && !('content-length' in responseHeaders)) { + responseHeaders['content-length'] = String( + Buffer.byteLength(responseBody) + ); + } + + const status = response.status || 200; + if (this._interceptionId === undefined) { + throw new Error( + 'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest' + ); + } + await this.#client + .send('Fetch.fulfillRequest', { + requestId: this._interceptionId, + responseCode: status, + responsePhrase: STATUS_TEXTS[status], + responseHeaders: headersArray(responseHeaders), + body: responseBody ? responseBody.toString('base64') : undefined, + }) + .catch(error => { + this.#interceptionHandled = false; + return handleError(error); + }); + } + + override async abort( + errorCode: ErrorCode = 'failed', + priority?: number + ): Promise<void> { + // Request interception is not supported for data: urls. + if (this.#url.startsWith('data:')) { + return; + } + const errorReason = errorReasons[errorCode]; + assert(errorReason, 'Unknown error code: ' + errorCode); + assert(this.#allowInterception, 'Request Interception is not enabled!'); + assert(!this.#interceptionHandled, 'Request is already handled!'); + if (priority === undefined) { + return this.#abort(errorReason); + } + this.#abortErrorReason = errorReason; + if ( + this.#interceptResolutionState.priority === undefined || + priority >= this.#interceptResolutionState.priority + ) { + this.#interceptResolutionState = { + action: InterceptResolutionAction.Abort, + priority, + }; + return; + } + } + + async #abort( + errorReason: Protocol.Network.ErrorReason | null + ): Promise<void> { + this.#interceptionHandled = true; + if (this._interceptionId === undefined) { + throw new Error( + 'HTTPRequest is missing _interceptionId needed for Fetch.failRequest' + ); + } + await this.#client + .send('Fetch.failRequest', { + requestId: this._interceptionId, + errorReason: errorReason || 'Failed', + }) + .catch(handleError); + } +} + +const errorReasons: Record<ErrorCode, Protocol.Network.ErrorReason> = { + aborted: 'Aborted', + accessdenied: 'AccessDenied', + addressunreachable: 'AddressUnreachable', + blockedbyclient: 'BlockedByClient', + blockedbyresponse: 'BlockedByResponse', + connectionaborted: 'ConnectionAborted', + connectionclosed: 'ConnectionClosed', + connectionfailed: 'ConnectionFailed', + connectionrefused: 'ConnectionRefused', + connectionreset: 'ConnectionReset', + internetdisconnected: 'InternetDisconnected', + namenotresolved: 'NameNotResolved', + timedout: 'TimedOut', + failed: 'Failed', +} as const; + +async function handleError(error: ProtocolError) { + if (['Invalid header'].includes(error.originalMessage)) { + throw error; + } + // In certain cases, protocol will return error if the request was + // already canceled or the page was closed. We should tolerate these + // errors. + debugError(error); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPResponse.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPResponse.ts new file mode 100644 index 0000000000..a43aa17195 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/HTTPResponse.ts @@ -0,0 +1,188 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import { + HTTPResponse as BaseHTTPResponse, + RemoteAddress, +} from '../api/HTTPResponse.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {CDPSession} from './Connection.js'; +import {ProtocolError} from './Errors.js'; +import {Frame} from './Frame.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {SecurityDetails} from './SecurityDetails.js'; + +/** + * @internal + */ +export class HTTPResponse extends BaseHTTPResponse { + #client: CDPSession; + #request: HTTPRequest; + #contentPromise: Promise<Buffer> | null = null; + #bodyLoadedPromise = createDeferredPromise<Error | void>(); + #remoteAddress: RemoteAddress; + #status: number; + #statusText: string; + #url: string; + #fromDiskCache: boolean; + #fromServiceWorker: boolean; + #headers: Record<string, string> = {}; + #securityDetails: SecurityDetails | null; + #timing: Protocol.Network.ResourceTiming | null; + + constructor( + client: CDPSession, + request: HTTPRequest, + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null + ) { + super(); + this.#client = client; + this.#request = request; + + this.#remoteAddress = { + ip: responsePayload.remoteIPAddress, + port: responsePayload.remotePort, + }; + this.#statusText = + this.#parseStatusTextFromExtrInfo(extraInfo) || + responsePayload.statusText; + this.#url = request.url(); + this.#fromDiskCache = !!responsePayload.fromDiskCache; + this.#fromServiceWorker = !!responsePayload.fromServiceWorker; + + this.#status = extraInfo ? extraInfo.statusCode : responsePayload.status; + const headers = extraInfo ? extraInfo.headers : responsePayload.headers; + for (const [key, value] of Object.entries(headers)) { + this.#headers[key.toLowerCase()] = value; + } + + this.#securityDetails = responsePayload.securityDetails + ? new SecurityDetails(responsePayload.securityDetails) + : null; + this.#timing = responsePayload.timing || null; + } + + #parseStatusTextFromExtrInfo( + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null + ): string | undefined { + if (!extraInfo || !extraInfo.headersText) { + return; + } + const firstLine = extraInfo.headersText.split('\r', 1)[0]; + if (!firstLine) { + return; + } + const match = firstLine.match(/[^ ]* [^ ]* (.*)/); + if (!match) { + return; + } + const statusText = match[1]; + if (!statusText) { + return; + } + return statusText; + } + + override _resolveBody(err: Error | null): void { + if (err) { + return this.#bodyLoadedPromise.resolve(err); + } + return this.#bodyLoadedPromise.resolve(); + } + + override remoteAddress(): RemoteAddress { + return this.#remoteAddress; + } + + override url(): string { + return this.#url; + } + + override ok(): boolean { + // TODO: document === 0 case? + return this.#status === 0 || (this.#status >= 200 && this.#status <= 299); + } + + override status(): number { + return this.#status; + } + + override statusText(): string { + return this.#statusText; + } + + override headers(): Record<string, string> { + return this.#headers; + } + + override securityDetails(): SecurityDetails | null { + return this.#securityDetails; + } + + override timing(): Protocol.Network.ResourceTiming | null { + return this.#timing; + } + + override buffer(): Promise<Buffer> { + if (!this.#contentPromise) { + this.#contentPromise = this.#bodyLoadedPromise.then(async error => { + if (error) { + throw error; + } + try { + const response = await this.#client.send('Network.getResponseBody', { + requestId: this.#request._requestId, + }); + return Buffer.from( + response.body, + response.base64Encoded ? 'base64' : 'utf8' + ); + } catch (error) { + if ( + error instanceof ProtocolError && + error.originalMessage === 'No resource with given identifier found' + ) { + throw new ProtocolError( + 'Could not load body for this request. This might happen if the request is a preflight request.' + ); + } + + throw error; + } + }); + } + return this.#contentPromise; + } + + override request(): HTTPRequest { + return this.#request; + } + + override fromCache(): boolean { + return this.#fromDiskCache || this.#request._fromMemoryCache; + } + + override fromServiceWorker(): boolean { + return this.#fromServiceWorker; + } + + override frame(): Frame | null { + return this.#request.frame(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts new file mode 100644 index 0000000000..d5df382f88 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/HandleIterator.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {JSHandle} from '../api/JSHandle.js'; + +import {AwaitableIterable, HandleFor} from './types.js'; + +const DEFAULT_BATCH_SIZE = 20; + +/** + * This will transpose an iterator JSHandle into a fast, Puppeteer-side iterator + * of JSHandles. + * + * @param size - The number of elements to transpose. This should be something + * reasonable. + */ +async function* fastTransposeIteratorHandle<T>( + iterator: JSHandle<AwaitableIterator<T>>, + size: number +) { + const array = await iterator.evaluateHandle(async (iterator, size) => { + const results = []; + while (results.length < size) { + const result = await iterator.next(); + if (result.done) { + break; + } + results.push(result.value); + } + return results; + }, size); + const properties = (await array.getProperties()) as Map<string, HandleFor<T>>; + await array.dispose(); + yield* properties.values(); + return properties.size === 0; +} + +/** + * This will transpose an iterator JSHandle in batches based on the default size + * of {@link fastTransposeIteratorHandle}. + */ + +async function* transposeIteratorHandle<T>( + iterator: JSHandle<AwaitableIterator<T>> +) { + let size = DEFAULT_BATCH_SIZE; + try { + while (!(yield* fastTransposeIteratorHandle(iterator, size))) { + size <<= 1; + } + } finally { + await iterator.dispose(); + } +} + +type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>; + +/** + * @internal + */ +export async function* transposeIterableHandle<T>( + handle: JSHandle<AwaitableIterable<T>> +): AsyncIterableIterator<HandleFor<T>> { + yield* transposeIteratorHandle( + await handle.evaluateHandle(iterable => { + return (async function* () { + yield* iterable; + })(); + }) + ); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Input.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Input.ts new file mode 100644 index 0000000000..4af29bd520 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Input.ts @@ -0,0 +1,920 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {Point} from '../api/ElementHandle.js'; +import {assert} from '../util/assert.js'; + +import {CDPSession} from './Connection.js'; +import {_keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout.js'; + +type KeyDescription = Required< + Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'> +>; + +/** + * Keyboard provides an api for managing a virtual keyboard. + * The high level api is {@link Keyboard."type"}, + * which takes raw characters and generates proper keydown, keypress/input, + * and keyup events on your page. + * + * @remarks + * For finer control, you can use {@link Keyboard.down}, + * {@link Keyboard.up}, and {@link Keyboard.sendCharacter} + * to manually fire events as if they were generated from a real keyboard. + * + * On macOS, keyboard shortcuts like `⌘ A` -\> Select All do not work. + * See {@link https://github.com/puppeteer/puppeteer/issues/1313 | #1313}. + * + * @example + * An example of holding down `Shift` in order to select and delete some text: + * + * ```ts + * await page.keyboard.type('Hello World!'); + * await page.keyboard.press('ArrowLeft'); + * + * await page.keyboard.down('Shift'); + * for (let i = 0; i < ' World'.length; i++) + * await page.keyboard.press('ArrowLeft'); + * await page.keyboard.up('Shift'); + * + * await page.keyboard.press('Backspace'); + * // Result text will end up saying 'Hello!' + * ``` + * + * @example + * An example of pressing `A` + * + * ```ts + * await page.keyboard.down('Shift'); + * await page.keyboard.press('KeyA'); + * await page.keyboard.up('Shift'); + * ``` + * + * @public + */ +export class Keyboard { + #client: CDPSession; + #pressedKeys = new Set<string>(); + + /** + * @internal + */ + _modifiers = 0; + + /** + * @internal + */ + constructor(client: CDPSession) { + this.#client = client; + } + + /** + * Dispatches a `keydown` event. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also generated. + * The `text` option can be specified to force an input event to be generated. + * If `key` is a modifier key, `Shift`, `Meta`, `Control`, or `Alt`, + * subsequent key presses will be sent with that modifier active. + * To release the modifier key, use {@link Keyboard.up}. + * + * After the key is pressed once, subsequent calls to + * {@link Keyboard.down} will have + * {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat | repeat} + * set to true. To release the key, use {@link Keyboard.up}. + * + * Modifier keys DO influence {@link Keyboard.down}. + * Holding down `Shift` will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + * + * @param options - An object of options. Accepts text which, if specified, + * generates an input event with this text. Accepts commands which, if specified, + * is the commands of keyboard shortcuts, + * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names. + */ + async down( + key: KeyInput, + options: {text?: string; commands?: string[]} = { + text: undefined, + commands: [], + } + ): Promise<void> { + const description = this.#keyDescriptionForString(key); + + const autoRepeat = this.#pressedKeys.has(description.code); + this.#pressedKeys.add(description.code); + this._modifiers |= this.#modifierBit(description.key); + + const text = options.text === undefined ? description.text : options.text; + await this.#client.send('Input.dispatchKeyEvent', { + type: text ? 'keyDown' : 'rawKeyDown', + modifiers: this._modifiers, + windowsVirtualKeyCode: description.keyCode, + code: description.code, + key: description.key, + text: text, + unmodifiedText: text, + autoRepeat, + location: description.location, + isKeypad: description.location === 3, + commands: options.commands, + }); + } + + #modifierBit(key: string): number { + if (key === 'Alt') { + return 1; + } + if (key === 'Control') { + return 2; + } + if (key === 'Meta') { + return 4; + } + if (key === 'Shift') { + return 8; + } + return 0; + } + + #keyDescriptionForString(keyString: KeyInput): KeyDescription { + const shift = this._modifiers & 8; + const description = { + key: '', + keyCode: 0, + code: '', + text: '', + location: 0, + }; + + const definition = _keyDefinitions[keyString]; + assert(definition, `Unknown key: "${keyString}"`); + + if (definition.key) { + description.key = definition.key; + } + if (shift && definition.shiftKey) { + description.key = definition.shiftKey; + } + + if (definition.keyCode) { + description.keyCode = definition.keyCode; + } + if (shift && definition.shiftKeyCode) { + description.keyCode = definition.shiftKeyCode; + } + + if (definition.code) { + description.code = definition.code; + } + + if (definition.location) { + description.location = definition.location; + } + + if (description.key.length === 1) { + description.text = description.key; + } + + if (definition.text) { + description.text = definition.text; + } + if (shift && definition.shiftText) { + description.text = definition.shiftText; + } + + // if any modifiers besides shift are pressed, no text should be sent + if (this._modifiers & ~8) { + description.text = ''; + } + + return description; + } + + /** + * Dispatches a `keyup` event. + * + * @param key - Name of key to release, such as `ArrowLeft`. + * See {@link KeyInput | KeyInput} + * for a list of all key names. + */ + async up(key: KeyInput): Promise<void> { + const description = this.#keyDescriptionForString(key); + + this._modifiers &= ~this.#modifierBit(description.key); + this.#pressedKeys.delete(description.code); + await this.#client.send('Input.dispatchKeyEvent', { + type: 'keyUp', + modifiers: this._modifiers, + key: description.key, + windowsVirtualKeyCode: description.keyCode, + code: description.code, + location: description.location, + }); + } + + /** + * Dispatches a `keypress` and `input` event. + * This does not send a `keydown` or `keyup` event. + * + * @remarks + * Modifier keys DO NOT effect {@link Keyboard.sendCharacter | Keyboard.sendCharacter}. + * Holding down `Shift` will not type the text in upper case. + * + * @example + * + * ```ts + * page.keyboard.sendCharacter('嗨'); + * ``` + * + * @param char - Character to send into the page. + */ + async sendCharacter(char: string): Promise<void> { + await this.#client.send('Input.insertText', {text: char}); + } + + private charIsKey(char: string): char is KeyInput { + return !!_keyDefinitions[char as KeyInput]; + } + + /** + * Sends a `keydown`, `keypress`/`input`, + * and `keyup` event for each character in the text. + * + * @remarks + * To press a special key, like `Control` or `ArrowDown`, + * use {@link Keyboard.press}. + * + * Modifier keys DO NOT effect `keyboard.type`. + * Holding down `Shift` will not type the text in upper case. + * + * @example + * + * ```ts + * await page.keyboard.type('Hello'); // Types instantly + * await page.keyboard.type('World', {delay: 100}); // Types slower, like a user + * ``` + * + * @param text - A text to type into a focused element. + * @param options - An object of options. Accepts delay which, + * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. + * Defaults to 0. + */ + async type(text: string, options: {delay?: number} = {}): Promise<void> { + const delay = options.delay || undefined; + for (const char of text) { + if (this.charIsKey(char)) { + await this.press(char, {delay}); + } else { + if (delay) { + await new Promise(f => { + return setTimeout(f, delay); + }); + } + await this.sendCharacter(char); + } + } + } + + /** + * Shortcut for {@link Keyboard.down} + * and {@link Keyboard.up}. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also generated. + * The `text` option can be specified to force an input event to be generated. + * + * Modifier keys DO effect {@link Keyboard.press}. + * Holding down `Shift` will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + * + * @param options - An object of options. Accepts text which, if specified, + * generates an input event with this text. Accepts delay which, + * if specified, is the time to wait between `keydown` and `keyup` in milliseconds. + * Defaults to 0. Accepts commands which, if specified, + * is the commands of keyboard shortcuts, + * see {@link https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h | Chromium Source Code} for valid command names. + */ + async press( + key: KeyInput, + options: {delay?: number; text?: string; commands?: string[]} = {} + ): Promise<void> { + const {delay = null} = options; + await this.down(key, options); + if (delay) { + await new Promise(f => { + return setTimeout(f, options.delay); + }); + } + await this.up(key); + } +} + +/** + * @public + */ +export interface MouseOptions { + /** + * Determines which button will be pressed. + * + * @defaultValue `'left'` + */ + button?: MouseButton; + /** + * @deprecated Use {@link MouseClickOptions.count}. + * + * Determines the click count for the mouse event. This does not perform + * multiple clicks. + * + * @defaultValue `1` + */ + clickCount?: number; +} + +/** + * @public + */ +export interface MouseClickOptions extends MouseOptions { + /** + * Time (in ms) to delay the mouse release after the mouse press. + */ + delay?: number; + /** + * Number of clicks to perform. + * + * @defaultValue `1` + */ + count?: number; +} + +/** + * @public + */ +export interface MouseWheelOptions { + deltaX?: number; + deltaY?: number; +} + +/** + * @public + */ +export interface MouseMoveOptions { + /** + * Determines the number of movements to make from the current mouse position + * to the new one. + * + * @defaultValue `1` + */ + steps?: number; +} + +/** + * Enum of valid mouse buttons. + * + * @public + */ +export const MouseButton = Object.freeze({ + Left: 'left', + Right: 'right', + Middle: 'middle', + Back: 'back', + Forward: 'forward', +}) satisfies Record<string, Protocol.Input.MouseButton>; + +/** + * @public + */ +export type MouseButton = (typeof MouseButton)[keyof typeof MouseButton]; + +/** + * This must follow {@link Protocol.Input.DispatchMouseEventRequest.buttons}. + */ +const enum MouseButtonFlag { + None = 0, + Left = 1, + Right = 1 << 1, + Middle = 1 << 2, + Back = 1 << 3, + Forward = 1 << 4, +} + +const getFlag = (button: MouseButton): MouseButtonFlag => { + switch (button) { + case MouseButton.Left: + return MouseButtonFlag.Left; + case MouseButton.Right: + return MouseButtonFlag.Right; + case MouseButton.Middle: + return MouseButtonFlag.Middle; + case MouseButton.Back: + return MouseButtonFlag.Back; + case MouseButton.Forward: + return MouseButtonFlag.Forward; + } +}; + +/** + * This should match + * https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:content/browser/renderer_host/input/web_input_event_builders_mac.mm;drc=a61b95c63b0b75c1cfe872d9c8cdf927c226046e;bpv=1;bpt=1;l=221. + */ +const getButtonFromPressedButtons = ( + buttons: number +): Protocol.Input.MouseButton => { + if (buttons & MouseButtonFlag.Left) { + return MouseButton.Left; + } else if (buttons & MouseButtonFlag.Right) { + return MouseButton.Right; + } else if (buttons & MouseButtonFlag.Middle) { + return MouseButton.Middle; + } else if (buttons & MouseButtonFlag.Back) { + return MouseButton.Back; + } else if (buttons & MouseButtonFlag.Forward) { + return MouseButton.Forward; + } + return 'none'; +}; + +interface MouseState { + /** + * The current position of the mouse. + */ + position: Point; + /** + * The buttons that are currently being pressed. + */ + buttons: number; +} + +/** + * The Mouse class operates in main-frame CSS pixels + * relative to the top-left corner of the viewport. + * @remarks + * Every `page` object has its own Mouse, accessible with [`page.mouse`](#pagemouse). + * + * @example + * + * ```ts + * // Using ‘page.mouse’ to trace a 100x100 square. + * await page.mouse.move(0, 0); + * await page.mouse.down(); + * await page.mouse.move(0, 100); + * await page.mouse.move(100, 100); + * await page.mouse.move(100, 0); + * await page.mouse.move(0, 0); + * await page.mouse.up(); + * ``` + * + * **Note**: The mouse events trigger synthetic `MouseEvent`s. + * This means that it does not fully replicate the functionality of what a normal user + * would be able to do with their mouse. + * + * For example, dragging and selecting text is not possible using `page.mouse`. + * Instead, you can use the {@link https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/getSelection | `DocumentOrShadowRoot.getSelection()`} functionality implemented in the platform. + * + * @example + * For example, if you want to select all content between nodes: + * + * ```ts + * await page.evaluate( + * (from, to) => { + * const selection = from.getRootNode().getSelection(); + * const range = document.createRange(); + * range.setStartBefore(from); + * range.setEndAfter(to); + * selection.removeAllRanges(); + * selection.addRange(range); + * }, + * fromJSHandle, + * toJSHandle + * ); + * ``` + * + * If you then would want to copy-paste your selection, you can use the clipboard api: + * + * ```ts + * // The clipboard api does not allow you to copy, unless the tab is focused. + * await page.bringToFront(); + * await page.evaluate(() => { + * // Copy the selected content to the clipboard + * document.execCommand('copy'); + * // Obtain the content of the clipboard as a string + * return navigator.clipboard.readText(); + * }); + * ``` + * + * **Note**: If you want access to the clipboard API, + * you have to give it permission to do so: + * + * ```ts + * await browser + * .defaultBrowserContext() + * .overridePermissions('<your origin>', [ + * 'clipboard-read', + * 'clipboard-write', + * ]); + * ``` + * + * @public + */ +export class Mouse { + #client: CDPSession; + #keyboard: Keyboard; + + /** + * @internal + */ + constructor(client: CDPSession, keyboard: Keyboard) { + this.#client = client; + this.#keyboard = keyboard; + } + + #_state: Readonly<MouseState> = { + position: {x: 0, y: 0}, + buttons: MouseButtonFlag.None, + }; + get #state(): MouseState { + return Object.assign({...this.#_state}, ...this.#transactions); + } + + // Transactions can run in parallel, so we store each of thme in this array. + #transactions: Array<Partial<MouseState>> = []; + #createTransaction(): { + update: (updates: Partial<MouseState>) => void; + commit: () => void; + rollback: () => void; + } { + const transaction: Partial<MouseState> = {}; + this.#transactions.push(transaction); + const popTransaction = () => { + this.#transactions.splice(this.#transactions.indexOf(transaction), 1); + }; + return { + update: (updates: Partial<MouseState>) => { + Object.assign(transaction, updates); + }, + commit: () => { + this.#_state = {...this.#_state, ...transaction}; + popTransaction(); + }, + rollback: popTransaction, + }; + } + + /** + * This is a shortcut for a typical update, commit/rollback lifecycle based on + * the error of the action. + */ + async #withTransaction( + action: (update: (updates: Partial<MouseState>) => void) => Promise<unknown> + ): Promise<void> { + const {update, commit, rollback} = this.#createTransaction(); + try { + await action(update); + commit(); + } catch (error) { + rollback(); + throw error; + } + } + + /** + * Moves the mouse to the given coordinate. + * + * @param x - Horizontal position of the mouse. + * @param y - Vertical position of the mouse. + * @param options - Options to configure behavior. + */ + async move( + x: number, + y: number, + options: MouseMoveOptions = {} + ): Promise<void> { + const {steps = 1} = options; + const from = this.#state.position; + const to = {x, y}; + for (let i = 1; i <= steps; i++) { + await this.#withTransaction(updateState => { + updateState({ + position: { + x: from.x + (to.x - from.x) * (i / steps), + y: from.y + (to.y - from.y) * (i / steps), + }, + }); + const {buttons, position} = this.#state; + return this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + modifiers: this.#keyboard._modifiers, + buttons, + button: getButtonFromPressedButtons(buttons), + ...position, + }); + }); + } + } + + /** + * Presses the mouse. + * + * @param options - Options to configure behavior. + */ + async down(options: MouseOptions = {}): Promise<void> { + const {button = MouseButton.Left, clickCount = 1} = options; + const flag = getFlag(button); + if (!flag) { + throw new Error(`Unsupported mouse button: ${button}`); + } + if (this.#state.buttons & flag) { + throw new Error(`'${button}' is already pressed.`); + } + await this.#withTransaction(updateState => { + updateState({ + buttons: this.#state.buttons | flag, + }); + const {buttons, position} = this.#state; + return this.#client.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + modifiers: this.#keyboard._modifiers, + clickCount, + buttons, + button, + ...position, + }); + }); + } + + /** + * Releases the mouse. + * + * @param options - Options to configure behavior. + */ + async up(options: MouseOptions = {}): Promise<void> { + const {button = MouseButton.Left, clickCount = 1} = options; + const flag = getFlag(button); + if (!flag) { + throw new Error(`Unsupported mouse button: ${button}`); + } + if (!(this.#state.buttons & flag)) { + throw new Error(`'${button}' is not pressed.`); + } + await this.#withTransaction(updateState => { + updateState({ + buttons: this.#state.buttons & ~flag, + }); + const {buttons, position} = this.#state; + return this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + modifiers: this.#keyboard._modifiers, + clickCount, + buttons, + button, + ...position, + }); + }); + } + + /** + * Shortcut for `mouse.move`, `mouse.down` and `mouse.up`. + * + * @param x - Horizontal position of the mouse. + * @param y - Vertical position of the mouse. + * @param options - Options to configure behavior. + */ + async click( + x: number, + y: number, + options: Readonly<MouseClickOptions> = {} + ): Promise<void> { + const {delay, count = 1, clickCount = count} = options; + if (count < 1) { + throw new Error('Click must occur a positive number of times.'); + } + const actions: Array<Promise<void>> = [this.move(x, y)]; + if (clickCount === count) { + for (let i = 1; i < count; ++i) { + actions.push( + this.down({...options, clickCount: i}), + this.up({...options, clickCount: i}) + ); + } + } + actions.push(this.down({...options, clickCount})); + if (typeof delay === 'number') { + await Promise.all(actions); + actions.length = 0; + await new Promise(resolve => { + setTimeout(resolve, delay); + }); + } + actions.push(this.up({...options, clickCount})); + await Promise.all(actions); + } + + /** + * Dispatches a `mousewheel` event. + * @param options - Optional: `MouseWheelOptions`. + * + * @example + * An example of zooming into an element: + * + * ```ts + * await page.goto( + * 'https://mdn.mozillademos.org/en-US/docs/Web/API/Element/wheel_event$samples/Scaling_an_element_via_the_wheel?revision=1587366' + * ); + * + * const elem = await page.$('div'); + * const boundingBox = await elem.boundingBox(); + * await page.mouse.move( + * boundingBox.x + boundingBox.width / 2, + * boundingBox.y + boundingBox.height / 2 + * ); + * + * await page.mouse.wheel({deltaY: -100}); + * ``` + */ + async wheel(options: MouseWheelOptions = {}): Promise<void> { + const {deltaX = 0, deltaY = 0} = options; + const {position, buttons} = this.#state; + await this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + pointerType: 'mouse', + modifiers: this.#keyboard._modifiers, + deltaY, + deltaX, + buttons, + ...position, + }); + } + + /** + * Dispatches a `drag` event. + * @param start - starting point for drag + * @param target - point to drag to + */ + async drag(start: Point, target: Point): Promise<Protocol.Input.DragData> { + const promise = new Promise<Protocol.Input.DragData>(resolve => { + this.#client.once('Input.dragIntercepted', event => { + return resolve(event.data); + }); + }); + await this.move(start.x, start.y); + await this.down(); + await this.move(target.x, target.y); + return promise; + } + + /** + * Dispatches a `dragenter` event. + * @param target - point for emitting `dragenter` event + * @param data - drag data containing items and operations mask + */ + async dragEnter(target: Point, data: Protocol.Input.DragData): Promise<void> { + await this.#client.send('Input.dispatchDragEvent', { + type: 'dragEnter', + x: target.x, + y: target.y, + modifiers: this.#keyboard._modifiers, + data, + }); + } + + /** + * Dispatches a `dragover` event. + * @param target - point for emitting `dragover` event + * @param data - drag data containing items and operations mask + */ + async dragOver(target: Point, data: Protocol.Input.DragData): Promise<void> { + await this.#client.send('Input.dispatchDragEvent', { + type: 'dragOver', + x: target.x, + y: target.y, + modifiers: this.#keyboard._modifiers, + data, + }); + } + + /** + * Performs a dragenter, dragover, and drop in sequence. + * @param target - point to drop on + * @param data - drag data containing items and operations mask + */ + async drop(target: Point, data: Protocol.Input.DragData): Promise<void> { + await this.#client.send('Input.dispatchDragEvent', { + type: 'drop', + x: target.x, + y: target.y, + modifiers: this.#keyboard._modifiers, + data, + }); + } + + /** + * Performs a drag, dragenter, dragover, and drop in sequence. + * @param start - point to drag from + * @param target - point to drop on + * @param options - An object of options. Accepts delay which, + * if specified, is the time to wait between `dragover` and `drop` in milliseconds. + * Defaults to 0. + */ + async dragAndDrop( + start: Point, + target: Point, + options: {delay?: number} = {} + ): Promise<void> { + const {delay = null} = options; + const data = await this.drag(start, target); + await this.dragEnter(target, data); + await this.dragOver(target, data); + if (delay) { + await new Promise(resolve => { + return setTimeout(resolve, delay); + }); + } + await this.drop(target, data); + await this.up(); + } +} + +/** + * The Touchscreen class exposes touchscreen events. + * @public + */ +export class Touchscreen { + #client: CDPSession; + #keyboard: Keyboard; + + /** + * @internal + */ + constructor(client: CDPSession, keyboard: Keyboard) { + this.#client = client; + this.#keyboard = keyboard; + } + + /** + * Dispatches a `touchstart` and `touchend` event. + * @param x - Horizontal position of the tap. + * @param y - Vertical position of the tap. + */ + async tap(x: number, y: number): Promise<void> { + await this.touchStart(x, y); + await this.touchEnd(); + } + + /** + * Dispatches a `touchstart` event. + * @param x - Horizontal position of the tap. + * @param y - Vertical position of the tap. + */ + async touchStart(x: number, y: number): Promise<void> { + const touchPoints = [{x: Math.round(x), y: Math.round(y)}]; + await this.#client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints, + modifiers: this.#keyboard._modifiers, + }); + } + /** + * Dispatches a `touchMove` event. + * @param x - Horizontal position of the move. + * @param y - Vertical position of the move. + */ + async touchMove(x: number, y: number): Promise<void> { + const movePoints = [{x: Math.round(x), y: Math.round(y)}]; + await this.#client.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: movePoints, + modifiers: this.#keyboard._modifiers, + }); + } + /** + * Dispatches a `touchend` event. + */ + async touchEnd(): Promise<void> { + await this.#client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + modifiers: this.#keyboard._modifiers, + }); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorld.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorld.ts new file mode 100644 index 0000000000..82272ae32a --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -0,0 +1,537 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import type {ClickOptions, ElementHandle} from '../api/ElementHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {Binding} from './Binding.js'; +import {CDPSession} from './Connection.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {FrameManager} from './FrameManager.js'; +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; +import { + BindingPayload, + EvaluateFunc, + EvaluateFuncWith, + HandleFor, + InnerLazyParams, + NodeFor, +} from './types.js'; +import { + addPageBinding, + createJSHandle, + debugError, + setPageContent, +} from './util.js'; +import {TaskManager, WaitTask} from './WaitTask.js'; + +/** + * @public + */ +export interface WaitForSelectorOptions { + /** + * Wait for the selected element to be present in DOM and to be visible, i.e. + * to not have `display: none` or `visibility: hidden` CSS properties. + * + * @defaultValue `false` + */ + visible?: boolean; + /** + * Wait for the selected element to not be found in the DOM or to be hidden, + * i.e. have `display: none` or `visibility: hidden` CSS properties. + * + * @defaultValue `false` + */ + hidden?: boolean; + /** + * Maximum time to wait in milliseconds. Pass `0` to disable timeout. + * + * The default value can be changed by using {@link Page.setDefaultTimeout} + * + * @defaultValue `30_000` (30 seconds) + */ + timeout?: number; + /** + * A signal object that allows you to cancel a waitForSelector call. + */ + signal?: AbortSignal; +} + +/** + * @internal + */ +export interface PageBinding { + name: string; + pptrFunction: Function; +} + +/** + * @internal + */ +export interface IsolatedWorldChart { + [key: string]: IsolatedWorld; + [MAIN_WORLD]: IsolatedWorld; + [PUPPETEER_WORLD]: IsolatedWorld; +} + +/** + * @internal + */ +export class IsolatedWorld { + #frame: Frame; + #document?: ElementHandle<Document>; + #context = createDeferredPromise<ExecutionContext>(); + #detached = false; + + // Set of bindings that have been registered in the current context. + #contextBindings = new Set<string>(); + + // Contains mapping from functions that should be bound to Puppeteer functions. + #bindings = new Map<string, Binding>(); + #taskManager = new TaskManager(); + + get taskManager(): TaskManager { + return this.#taskManager; + } + + get _bindings(): Map<string, Binding> { + return this.#bindings; + } + + constructor(frame: Frame) { + // Keep own reference to client because it might differ from the FrameManager's + // client for OOP iframes. + this.#frame = frame; + this.#client.on('Runtime.bindingCalled', this.#onBindingCalled); + } + + get #client(): CDPSession { + return this.#frame._client(); + } + + get #frameManager(): FrameManager { + return this.#frame._frameManager; + } + + get #timeoutSettings(): TimeoutSettings { + return this.#frameManager.timeoutSettings; + } + + frame(): Frame { + return this.#frame; + } + + clearContext(): void { + this.#document = undefined; + this.#context = createDeferredPromise(); + } + + setContext(context: ExecutionContext): void { + this.#contextBindings.clear(); + this.#context.resolve(context); + void this.#taskManager.rerunAll(); + } + + hasContext(): boolean { + return this.#context.resolved(); + } + + _detach(): void { + this.#detached = true; + this.#client.off('Runtime.bindingCalled', this.#onBindingCalled); + this.#taskManager.terminateAll( + new Error('waitForFunction failed: frame got detached.') + ); + } + + executionContext(): Promise<ExecutionContext> { + if (this.#detached) { + throw new Error( + `Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)` + ); + } + if (this.#context === null) { + throw new Error(`Execution content promise is missing`); + } + return this.#context; + } + + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + const context = await this.executionContext(); + return context.evaluateHandle(pageFunction, ...args); + } + + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const context = await this.executionContext(); + return context.evaluate(pageFunction, ...args); + } + + async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + const document = await this.document(); + return document.$(selector); + } + + async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>> { + const document = await this.document(); + return document.$$(selector); + } + + async document(): Promise<ElementHandle<Document>> { + if (this.#document) { + return this.#document; + } + const context = await this.executionContext(); + this.#document = await context.evaluateHandle(() => { + return document; + }); + return this.#document; + } + + async $x(expression: string): Promise<Array<ElementHandle<Node>>> { + const document = await this.document(); + return document.$x(expression); + } + + async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const document = await this.document(); + return document.$eval(selector, pageFunction, ...args); + } + + async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const document = await this.document(); + return document.$$eval(selector, pageFunction, ...args); + } + + async content(): Promise<string> { + return await this.evaluate(() => { + let retVal = ''; + if (document.doctype) { + retVal = new XMLSerializer().serializeToString(document.doctype); + } + if (document.documentElement) { + retVal += document.documentElement.outerHTML; + } + return retVal; + }); + } + + async setContent( + html: string, + options: { + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<void> { + const { + waitUntil = ['load'], + timeout = this.#timeoutSettings.navigationTimeout(), + } = options; + + await setPageContent(this, html); + + const watcher = new LifecycleWatcher( + this.#frameManager, + this.#frame, + waitUntil, + timeout + ); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise(), + watcher.lifecyclePromise(), + ]); + watcher.dispose(); + if (error) { + throw error; + } + } + + async click( + selector: string, + options: Readonly<ClickOptions> = {} + ): Promise<void> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.click(options); + await handle.dispose(); + } + + async focus(selector: string): Promise<void> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.focus(); + await handle.dispose(); + } + + async hover(selector: string): Promise<void> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.hover(); + await handle.dispose(); + } + + async select(selector: string, ...values: string[]): Promise<string[]> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + const result = await handle.select(...values); + await handle.dispose(); + return result; + } + + async tap(selector: string): Promise<void> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.tap(); + await handle.dispose(); + } + + async type( + selector: string, + text: string, + options?: {delay: number} + ): Promise<void> { + const handle = await this.$(selector); + assert(handle, `No element found for selector: ${selector}`); + await handle.type(text, options); + await handle.dispose(); + } + + // If multiple waitFor are set up asynchronously, we need to wait for the + // first one to set up the binding in the page before running the others. + #mutex = new Mutex(); + async _addBindingToContext( + context: ExecutionContext, + name: string + ): Promise<void> { + if (this.#contextBindings.has(name)) { + return; + } + + await this.#mutex.acquire(); + try { + await context._client.send('Runtime.addBinding', { + name, + executionContextName: context._contextName, + }); + + await context.evaluate(addPageBinding, 'internal', name); + + this.#contextBindings.add(name); + } catch (error) { + // We could have tried to evaluate in a context which was already + // destroyed. This happens, for example, if the page is navigated while + // we are trying to add the binding + if (error instanceof Error) { + // Destroyed context. + if (error.message.includes('Execution context was destroyed')) { + return; + } + // Missing context. + if (error.message.includes('Cannot find context with specified id')) { + return; + } + } + + debugError(error); + } finally { + this.#mutex.release(); + } + } + + #onBindingCalled = async ( + event: Protocol.Runtime.BindingCalledEvent + ): Promise<void> => { + let payload: BindingPayload; + try { + payload = JSON.parse(event.payload); + } catch { + // The binding was either called by something in the page or it was + // called before our wrapper was initialized. + return; + } + const {type, name, seq, args, isTrivial} = payload; + if (type !== 'internal') { + return; + } + if (!this.#contextBindings.has(name)) { + return; + } + + const context = await this.#context; + if (event.executionContextId !== context._contextId) { + return; + } + + const binding = this._bindings.get(name); + await binding?.run(context, seq, args, isTrivial); + }; + + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc< + InnerLazyParams<Params> + > + >( + pageFunction: Func | string, + options: { + polling?: 'raf' | 'mutation' | number; + timeout?: number; + root?: ElementHandle<Node>; + signal?: AbortSignal; + } = {}, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + const { + polling = 'raf', + timeout = this.#timeoutSettings.timeout(), + root, + signal, + } = options; + if (typeof polling === 'number' && polling < 0) { + throw new Error('Cannot poll with non-positive interval'); + } + const waitTask = new WaitTask( + this, + { + polling, + root, + timeout, + signal, + }, + pageFunction as unknown as + | ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>) + | string, + ...args + ); + return waitTask.result; + } + + async title(): Promise<string> { + return this.evaluate(() => { + return document.title; + }); + } + + async adoptBackendNode( + backendNodeId?: Protocol.DOM.BackendNodeId + ): Promise<JSHandle<Node>> { + const executionContext = await this.executionContext(); + const {object} = await this.#client.send('DOM.resolveNode', { + backendNodeId: backendNodeId, + executionContextId: executionContext._contextId, + }); + return createJSHandle(executionContext, object) as JSHandle<Node>; + } + + async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { + const context = await this.executionContext(); + assert( + handle.executionContext() !== context, + 'Cannot adopt handle that already belongs to this execution context' + ); + const nodeInfo = await this.#client.send('DOM.describeNode', { + objectId: handle.id, + }); + return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; + } + + async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { + const context = await this.executionContext(); + if (handle.executionContext() === context) { + return handle; + } + const info = await this.#client.send('DOM.describeNode', { + objectId: handle.remoteObject().objectId, + }); + const newHandle = (await this.adoptBackendNode( + info.node.backendNodeId + )) as T; + await handle.dispose(); + return newHandle; + } +} + +class Mutex { + #locked = false; + #acquirers: Array<() => void> = []; + + // This is FIFO. + acquire(): Promise<void> { + if (!this.#locked) { + this.#locked = true; + return Promise.resolve(); + } + let resolve!: () => void; + const promise = new Promise<void>(res => { + resolve = res; + }); + this.#acquirers.push(resolve); + return promise; + } + + release(): void { + const resolve = this.#acquirers.shift(); + if (!resolve) { + this.#locked = false; + return; + } + resolve(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorlds.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorlds.ts new file mode 100644 index 0000000000..bf6ee30b11 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/IsolatedWorlds.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A unique key for {@link IsolatedWorldChart} to denote the default world. + * Execution contexts are automatically created in the default world. + * + * @internal + */ +export const MAIN_WORLD = Symbol('mainWorld'); +/** + * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world. + * This world contains all puppeteer-internal bindings/code. + * + * @internal + */ +export const PUPPETEER_WORLD = Symbol('puppeteerWorld'); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/JSHandle.ts new file mode 100644 index 0000000000..e755e9344c --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/JSHandle.ts @@ -0,0 +1,168 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {JSHandle} from '../api/JSHandle.js'; +import {assert} from '../util/assert.js'; + +import {CDPSession} from './Connection.js'; +import type {CDPElementHandle} from './ElementHandle.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {EvaluateFuncWith, HandleFor, HandleOr} from './types.js'; +import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js'; + +declare const __JSHandleSymbol: unique symbol; + +/** + * @internal + */ +export class CDPJSHandle<T = unknown> extends JSHandle<T> { + /** + * Used for nominally typing {@link JSHandle}. + */ + [__JSHandleSymbol]?: T; + + #disposed = false; + #context: ExecutionContext; + #remoteObject: Protocol.Runtime.RemoteObject; + + override get disposed(): boolean { + return this.#disposed; + } + + constructor( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject + ) { + super(); + this.#context = context; + this.#remoteObject = remoteObject; + } + + override executionContext(): ExecutionContext { + return this.#context; + } + + override get client(): CDPSession { + return this.#context._client; + } + + /** + * @see {@link ExecutionContext.evaluate} for more details. + */ + override async evaluate< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return await this.executionContext().evaluate(pageFunction, this, ...args); + } + + /** + * @see {@link ExecutionContext.evaluateHandle} for more details. + */ + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return await this.executionContext().evaluateHandle( + pageFunction, + this, + ...args + ); + } + + override async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>>; + override async getProperty(propertyName: string): Promise<JSHandle<unknown>>; + override async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>> { + return this.evaluateHandle((object, propertyName) => { + return object[propertyName as K]; + }, propertyName); + } + + override async getProperties(): Promise<Map<string, JSHandle>> { + assert(this.#remoteObject.objectId); + // We use Runtime.getProperties rather than iterative building because the + // iterative approach might create a distorted snapshot. + const response = await this.client.send('Runtime.getProperties', { + objectId: this.#remoteObject.objectId, + ownProperties: true, + }); + const result = new Map<string, JSHandle>(); + for (const property of response.result) { + if (!property.enumerable || !property.value) { + continue; + } + result.set(property.name, createJSHandle(this.#context, property.value)); + } + return result; + } + + override async jsonValue(): Promise<T> { + if (!this.#remoteObject.objectId) { + return valueFromRemoteObject(this.#remoteObject); + } + const value = await this.evaluate(object => { + return object; + }); + if (value === undefined) { + throw new Error('Could not serialize referenced object'); + } + return value; + } + + /** + * Either `null` or the handle itself if the handle is an + * instance of {@link ElementHandle}. + */ + override asElement(): CDPElementHandle<Node> | null { + return null; + } + + override async dispose(): Promise<void> { + if (this.#disposed) { + return; + } + this.#disposed = true; + await releaseObject(this.client, this.#remoteObject); + } + + override toString(): string { + if (!this.#remoteObject.objectId) { + return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); + } + const type = this.#remoteObject.subtype || this.#remoteObject.type; + return 'JSHandle@' + type; + } + + override get id(): string | undefined { + return this.#remoteObject.objectId; + } + + override remoteObject(): Protocol.Runtime.RemoteObject { + return this.#remoteObject; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts new file mode 100644 index 0000000000..7ba7dd707e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/LazyArg.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ExecutionContext} from './ExecutionContext.js'; + +/** + * @internal + */ +export class LazyArg<T> { + static create = <T>( + get: (context: ExecutionContext) => Promise<T> | T + ): T => { + // We don't want to introduce LazyArg to the type system, otherwise we would + // have to make it public. + return new LazyArg(get) as unknown as T; + }; + + #get: (context: ExecutionContext) => Promise<T> | T; + private constructor(get: (context: ExecutionContext) => Promise<T> | T) { + this.#get = get; + } + + async get(context: ExecutionContext): Promise<T> { + return this.#get(context); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/LifecycleWatcher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/LifecycleWatcher.ts new file mode 100644 index 0000000000..06899eb0e9 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/LifecycleWatcher.ts @@ -0,0 +1,304 @@ +/** + * 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 {HTTPResponse} from '../api/HTTPResponse.js'; +import {assert} from '../util/assert.js'; +import { + DeferredPromise, + createDeferredPromise, +} from '../util/DeferredPromise.js'; + +import {CDPSessionEmittedEvents} from './Connection.js'; +import {TimeoutError} from './Errors.js'; +import {Frame} from './Frame.js'; +import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {NetworkManagerEmittedEvents} from './NetworkManager.js'; +import { + addEventListener, + PuppeteerEventListener, + removeEventListeners, +} from './util.js'; +/** + * @public + */ +export type PuppeteerLifeCycleEvent = + | 'load' + | 'domcontentloaded' + | 'networkidle0' + | 'networkidle2'; + +/** + * @public + */ +export type ProtocolLifeCycleEvent = + | 'load' + | 'DOMContentLoaded' + | 'networkIdle' + | 'networkAlmostIdle'; + +const puppeteerToProtocolLifecycle = new Map< + PuppeteerLifeCycleEvent, + ProtocolLifeCycleEvent +>([ + ['load', 'load'], + ['domcontentloaded', 'DOMContentLoaded'], + ['networkidle0', 'networkIdle'], + ['networkidle2', 'networkAlmostIdle'], +]); + +const noop = (): void => {}; + +/** + * @internal + */ +export class LifecycleWatcher { + #expectedLifecycle: ProtocolLifeCycleEvent[]; + #frameManager: FrameManager; + #frame: Frame; + #timeout: number; + #navigationRequest: HTTPRequest | null = null; + #eventListeners: PuppeteerEventListener[]; + #initialLoaderId: string; + + #sameDocumentNavigationPromise = createDeferredPromise<Error | undefined>(); + #lifecyclePromise = createDeferredPromise<void>(); + #newDocumentNavigationPromise = createDeferredPromise<Error | undefined>(); + #terminationPromise = createDeferredPromise<Error | undefined>(); + + #timeoutPromise: Promise<TimeoutError | undefined>; + + #maximumTimer?: NodeJS.Timeout; + #hasSameDocumentNavigation?: boolean; + #swapped?: boolean; + + #navigationResponseReceived?: DeferredPromise<void>; + + constructor( + frameManager: FrameManager, + frame: Frame, + waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[], + timeout: number + ) { + if (Array.isArray(waitUntil)) { + waitUntil = waitUntil.slice(); + } else if (typeof waitUntil === 'string') { + waitUntil = [waitUntil]; + } + this.#initialLoaderId = frame._loaderId; + this.#expectedLifecycle = waitUntil.map(value => { + const protocolEvent = puppeteerToProtocolLifecycle.get(value); + assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value); + return protocolEvent as ProtocolLifeCycleEvent; + }); + + this.#frameManager = frameManager; + this.#frame = frame; + this.#timeout = timeout; + this.#eventListeners = [ + addEventListener( + frameManager.client, + CDPSessionEmittedEvents.Disconnected, + this.#terminate.bind( + this, + new Error('Navigation failed because browser has disconnected!') + ) + ), + addEventListener( + this.#frameManager, + FrameManagerEmittedEvents.LifecycleEvent, + this.#checkLifecycleComplete.bind(this) + ), + addEventListener( + this.#frameManager, + FrameManagerEmittedEvents.FrameNavigatedWithinDocument, + this.#navigatedWithinDocument.bind(this) + ), + addEventListener( + this.#frameManager, + FrameManagerEmittedEvents.FrameNavigated, + this.#navigated.bind(this) + ), + addEventListener( + this.#frameManager, + FrameManagerEmittedEvents.FrameSwapped, + this.#frameSwapped.bind(this) + ), + addEventListener( + this.#frameManager, + FrameManagerEmittedEvents.FrameDetached, + this.#onFrameDetached.bind(this) + ), + addEventListener( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.Request, + this.#onRequest.bind(this) + ), + addEventListener( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.Response, + this.#onResponse.bind(this) + ), + addEventListener( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.RequestFailed, + this.#onRequestFailed.bind(this) + ), + ]; + + this.#timeoutPromise = this.#createTimeoutPromise(); + this.#checkLifecycleComplete(); + } + + #onRequest(request: HTTPRequest): void { + if (request.frame() !== this.#frame || !request.isNavigationRequest()) { + return; + } + this.#navigationRequest = request; + // Resolve previous navigation response in case there are multiple + // navigation requests reported by the backend. This generally should not + // happen by it looks like it's possible. + this.#navigationResponseReceived?.resolve(); + this.#navigationResponseReceived = createDeferredPromise(); + if (request.response() !== null) { + this.#navigationResponseReceived?.resolve(); + } + } + + #onRequestFailed(request: HTTPRequest): void { + if (this.#navigationRequest?._requestId !== request._requestId) { + return; + } + this.#navigationResponseReceived?.resolve(); + } + + #onResponse(response: HTTPResponse): void { + if (this.#navigationRequest?._requestId !== response.request()._requestId) { + return; + } + this.#navigationResponseReceived?.resolve(); + } + + #onFrameDetached(frame: Frame): void { + if (this.#frame === frame) { + this.#terminationPromise.resolve( + new Error('Navigating frame was detached') + ); + return; + } + this.#checkLifecycleComplete(); + } + + async navigationResponse(): Promise<HTTPResponse | null> { + // Continue with a possibly null response. + await this.#navigationResponseReceived?.catch(() => {}); + return this.#navigationRequest ? this.#navigationRequest.response() : null; + } + + #terminate(error: Error): void { + this.#terminationPromise.resolve(error); + } + + sameDocumentNavigationPromise(): Promise<Error | undefined> { + return this.#sameDocumentNavigationPromise; + } + + newDocumentNavigationPromise(): Promise<Error | undefined> { + return this.#newDocumentNavigationPromise; + } + + lifecyclePromise(): Promise<void> { + return this.#lifecyclePromise; + } + + timeoutOrTerminationPromise(): Promise<Error | TimeoutError | undefined> { + return Promise.race([this.#timeoutPromise, this.#terminationPromise]); + } + + async #createTimeoutPromise(): Promise<TimeoutError | undefined> { + if (!this.#timeout) { + return new Promise(noop); + } + const errorMessage = + 'Navigation timeout of ' + this.#timeout + ' ms exceeded'; + await new Promise(fulfill => { + return (this.#maximumTimer = setTimeout(fulfill, this.#timeout)); + }); + return new TimeoutError(errorMessage); + } + + #navigatedWithinDocument(frame: Frame): void { + if (frame !== this.#frame) { + return; + } + this.#hasSameDocumentNavigation = true; + this.#checkLifecycleComplete(); + } + + #navigated(frame: Frame): void { + if (frame !== this.#frame) { + return; + } + this.#checkLifecycleComplete(); + } + + #frameSwapped(frame: Frame): void { + if (frame !== this.#frame) { + return; + } + this.#swapped = true; + this.#checkLifecycleComplete(); + } + + #checkLifecycleComplete(): void { + // We expect navigation to commit. + if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) { + return; + } + this.#lifecyclePromise.resolve(); + if (this.#hasSameDocumentNavigation) { + this.#sameDocumentNavigationPromise.resolve(undefined); + } + if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) { + this.#newDocumentNavigationPromise.resolve(undefined); + } + + function checkLifecycle( + frame: Frame, + expectedLifecycle: ProtocolLifeCycleEvent[] + ): boolean { + for (const event of expectedLifecycle) { + if (!frame._lifecycleEvents.has(event)) { + return false; + } + } + for (const child of frame.childFrames()) { + if ( + child._hasStartedLoading && + !checkLifecycle(child, expectedLifecycle) + ) { + return false; + } + } + return true; + } + } + + dispose(): void { + removeEventListeners(this.#eventListeners); + this.#maximumTimer !== undefined && clearTimeout(this.#maximumTimer); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkEventManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkEventManager.ts new file mode 100644 index 0000000000..da550a0068 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkEventManager.ts @@ -0,0 +1,220 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; + +import {HTTPRequest} from './HTTPRequest.js'; + +/** + * @internal + */ +export type QueuedEventGroup = { + responseReceivedEvent: Protocol.Network.ResponseReceivedEvent; + loadingFinishedEvent?: Protocol.Network.LoadingFinishedEvent; + loadingFailedEvent?: Protocol.Network.LoadingFailedEvent; +}; + +/** + * @internal + */ +export type FetchRequestId = string; + +/** + * @internal + */ +export type RedirectInfo = { + event: Protocol.Network.RequestWillBeSentEvent; + fetchRequestId?: FetchRequestId; +}; +type RedirectInfoList = RedirectInfo[]; + +/** + * @internal + */ +export type NetworkRequestId = string; + +/** + * Helper class to track network events by request ID + * + * @internal + */ +export class NetworkEventManager { + /** + * There are four possible orders of events: + * A. `_onRequestWillBeSent` + * B. `_onRequestWillBeSent`, `_onRequestPaused` + * C. `_onRequestPaused`, `_onRequestWillBeSent` + * D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused` + * (see crbug.com/1196004) + * + * For `_onRequest` we need the event from `_onRequestWillBeSent` and + * optionally the `interceptionId` from `_onRequestPaused`. + * + * If request interception is disabled, call `_onRequest` once per call to + * `_onRequestWillBeSent`. + * If request interception is enabled, call `_onRequest` once per call to + * `_onRequestPaused` (once per `interceptionId`). + * + * Events are stored to allow for subsequent events to call `_onRequest`. + * + * Note that (chains of) redirect requests have the same `requestId` (!) as + * the original request. We have to anticipate series of events like these: + * A. `_onRequestWillBeSent`, + * `_onRequestWillBeSent`, ... + * B. `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestWillBeSent`, `_onRequestPaused`, ... + * C. `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestPaused`, `_onRequestWillBeSent`, ... + * D. `_onRequestPaused`, `_onRequestWillBeSent`, + * `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`, + * `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ... + * (see crbug.com/1196004) + */ + #requestWillBeSentMap = new Map< + NetworkRequestId, + Protocol.Network.RequestWillBeSentEvent + >(); + #requestPausedMap = new Map< + NetworkRequestId, + Protocol.Fetch.RequestPausedEvent + >(); + #httpRequestsMap = new Map<NetworkRequestId, HTTPRequest>(); + + /* + * The below maps are used to reconcile Network.responseReceivedExtraInfo + * events with their corresponding request. Each response and redirect + * response gets an ExtraInfo event, and we don't know which will come first. + * This means that we have to store a Response or an ExtraInfo for each + * response, and emit the event when we get both of them. In addition, to + * handle redirects, we have to make them Arrays to represent the chain of + * events. + */ + #responseReceivedExtraInfoMap = new Map< + NetworkRequestId, + Protocol.Network.ResponseReceivedExtraInfoEvent[] + >(); + #queuedRedirectInfoMap = new Map<NetworkRequestId, RedirectInfoList>(); + #queuedEventGroupMap = new Map<NetworkRequestId, QueuedEventGroup>(); + + forget(networkRequestId: NetworkRequestId): void { + this.#requestWillBeSentMap.delete(networkRequestId); + this.#requestPausedMap.delete(networkRequestId); + this.#queuedEventGroupMap.delete(networkRequestId); + this.#queuedRedirectInfoMap.delete(networkRequestId); + this.#responseReceivedExtraInfoMap.delete(networkRequestId); + } + + responseExtraInfo( + networkRequestId: NetworkRequestId + ): Protocol.Network.ResponseReceivedExtraInfoEvent[] { + if (!this.#responseReceivedExtraInfoMap.has(networkRequestId)) { + this.#responseReceivedExtraInfoMap.set(networkRequestId, []); + } + return this.#responseReceivedExtraInfoMap.get( + networkRequestId + ) as Protocol.Network.ResponseReceivedExtraInfoEvent[]; + } + + private queuedRedirectInfo(fetchRequestId: FetchRequestId): RedirectInfoList { + if (!this.#queuedRedirectInfoMap.has(fetchRequestId)) { + this.#queuedRedirectInfoMap.set(fetchRequestId, []); + } + return this.#queuedRedirectInfoMap.get(fetchRequestId) as RedirectInfoList; + } + + queueRedirectInfo( + fetchRequestId: FetchRequestId, + redirectInfo: RedirectInfo + ): void { + this.queuedRedirectInfo(fetchRequestId).push(redirectInfo); + } + + takeQueuedRedirectInfo( + fetchRequestId: FetchRequestId + ): RedirectInfo | undefined { + return this.queuedRedirectInfo(fetchRequestId).shift(); + } + + numRequestsInProgress(): number { + return [...this.#httpRequestsMap].filter(([, request]) => { + return !request.response(); + }).length; + } + + storeRequestWillBeSent( + networkRequestId: NetworkRequestId, + event: Protocol.Network.RequestWillBeSentEvent + ): void { + this.#requestWillBeSentMap.set(networkRequestId, event); + } + + getRequestWillBeSent( + networkRequestId: NetworkRequestId + ): Protocol.Network.RequestWillBeSentEvent | undefined { + return this.#requestWillBeSentMap.get(networkRequestId); + } + + forgetRequestWillBeSent(networkRequestId: NetworkRequestId): void { + this.#requestWillBeSentMap.delete(networkRequestId); + } + + getRequestPaused( + networkRequestId: NetworkRequestId + ): Protocol.Fetch.RequestPausedEvent | undefined { + return this.#requestPausedMap.get(networkRequestId); + } + + forgetRequestPaused(networkRequestId: NetworkRequestId): void { + this.#requestPausedMap.delete(networkRequestId); + } + + storeRequestPaused( + networkRequestId: NetworkRequestId, + event: Protocol.Fetch.RequestPausedEvent + ): void { + this.#requestPausedMap.set(networkRequestId, event); + } + + getRequest(networkRequestId: NetworkRequestId): HTTPRequest | undefined { + return this.#httpRequestsMap.get(networkRequestId); + } + + storeRequest(networkRequestId: NetworkRequestId, request: HTTPRequest): void { + this.#httpRequestsMap.set(networkRequestId, request); + } + + forgetRequest(networkRequestId: NetworkRequestId): void { + this.#httpRequestsMap.delete(networkRequestId); + } + + getQueuedEventGroup( + networkRequestId: NetworkRequestId + ): QueuedEventGroup | undefined { + return this.#queuedEventGroupMap.get(networkRequestId); + } + + queueEventGroup( + networkRequestId: NetworkRequestId, + event: QueuedEventGroup + ): void { + this.#queuedEventGroupMap.set(networkRequestId, event); + } + + forgetQueuedEventGroup(networkRequestId: NetworkRequestId): void { + this.#queuedEventGroupMap.delete(networkRequestId); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManager.ts new file mode 100644 index 0000000000..d9a46be580 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/NetworkManager.ts @@ -0,0 +1,652 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {assert} from '../util/assert.js'; +import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js'; +import {DeferredPromise} from '../util/DeferredPromise.js'; + +import {CDPSession} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {FrameManager} from './FrameManager.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {HTTPResponse} from './HTTPResponse.js'; +import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js'; +import {debugError, isString} from './util.js'; + +/** + * @public + */ +export interface Credentials { + username: string; + password: string; +} + +/** + * @public + */ +export interface NetworkConditions { + // Download speed (bytes/s) + download: number; + // Upload speed (bytes/s) + upload: number; + // Latency (ms) + latency: number; +} +/** + * @public + */ +export interface InternalNetworkConditions extends NetworkConditions { + offline: boolean; +} + +/** + * We use symbols to prevent any external parties listening to these events. + * They are internal to Puppeteer. + * + * @internal + */ +export const NetworkManagerEmittedEvents = { + Request: Symbol('NetworkManager.Request'), + RequestServedFromCache: Symbol('NetworkManager.RequestServedFromCache'), + Response: Symbol('NetworkManager.Response'), + RequestFailed: Symbol('NetworkManager.RequestFailed'), + RequestFinished: Symbol('NetworkManager.RequestFinished'), +} as const; + +/** + * @internal + */ +export class NetworkManager extends EventEmitter { + #client: CDPSession; + #ignoreHTTPSErrors: boolean; + #frameManager: Pick<FrameManager, 'frame'>; + #networkEventManager = new NetworkEventManager(); + #extraHTTPHeaders: Record<string, string> = {}; + #credentials?: Credentials; + #attemptedAuthentications = new Set<string>(); + #userRequestInterceptionEnabled = false; + #protocolRequestInterceptionEnabled = false; + #userCacheDisabled = false; + #emulatedNetworkConditions: InternalNetworkConditions = { + offline: false, + upload: -1, + download: -1, + latency: 0, + }; + #deferredInitPromise?: DeferredPromise<void>; + + constructor( + client: CDPSession, + ignoreHTTPSErrors: boolean, + frameManager: Pick<FrameManager, 'frame'> + ) { + super(); + this.#client = client; + this.#ignoreHTTPSErrors = ignoreHTTPSErrors; + this.#frameManager = frameManager; + + this.#client.on('Fetch.requestPaused', this.#onRequestPaused.bind(this)); + this.#client.on('Fetch.authRequired', this.#onAuthRequired.bind(this)); + this.#client.on( + 'Network.requestWillBeSent', + this.#onRequestWillBeSent.bind(this) + ); + this.#client.on( + 'Network.requestServedFromCache', + this.#onRequestServedFromCache.bind(this) + ); + this.#client.on( + 'Network.responseReceived', + this.#onResponseReceived.bind(this) + ); + this.#client.on( + 'Network.loadingFinished', + this.#onLoadingFinished.bind(this) + ); + this.#client.on('Network.loadingFailed', this.#onLoadingFailed.bind(this)); + this.#client.on( + 'Network.responseReceivedExtraInfo', + this.#onResponseReceivedExtraInfo.bind(this) + ); + } + + /** + * Initialize calls should avoid async dependencies between CDP calls as those + * might not resolve until after the target is resumed causing a deadlock. + */ + initialize(): Promise<void> { + if (this.#deferredInitPromise) { + return this.#deferredInitPromise; + } + this.#deferredInitPromise = createDebuggableDeferredPromise( + 'NetworkManager initialization timed out' + ); + const init = Promise.all([ + this.#ignoreHTTPSErrors + ? this.#client.send('Security.setIgnoreCertificateErrors', { + ignore: true, + }) + : null, + this.#client.send('Network.enable'), + ]); + const deferredInitPromise = this.#deferredInitPromise; + init + .then(() => { + deferredInitPromise.resolve(); + }) + .catch(err => { + deferredInitPromise.reject(err); + }); + return this.#deferredInitPromise; + } + + async authenticate(credentials?: Credentials): Promise<void> { + this.#credentials = credentials; + await this.#updateProtocolRequestInterception(); + } + + async setExtraHTTPHeaders( + extraHTTPHeaders: Record<string, string> + ): Promise<void> { + this.#extraHTTPHeaders = {}; + for (const key of Object.keys(extraHTTPHeaders)) { + const value = extraHTTPHeaders[key]; + assert( + isString(value), + `Expected value of header "${key}" to be String, but "${typeof value}" is found.` + ); + this.#extraHTTPHeaders[key.toLowerCase()] = value; + } + await this.#client.send('Network.setExtraHTTPHeaders', { + headers: this.#extraHTTPHeaders, + }); + } + + extraHTTPHeaders(): Record<string, string> { + return Object.assign({}, this.#extraHTTPHeaders); + } + + numRequestsInProgress(): number { + return this.#networkEventManager.numRequestsInProgress(); + } + + async setOfflineMode(value: boolean): Promise<void> { + this.#emulatedNetworkConditions.offline = value; + await this.#updateNetworkConditions(); + } + + async emulateNetworkConditions( + networkConditions: NetworkConditions | null + ): Promise<void> { + this.#emulatedNetworkConditions.upload = networkConditions + ? networkConditions.upload + : -1; + this.#emulatedNetworkConditions.download = networkConditions + ? networkConditions.download + : -1; + this.#emulatedNetworkConditions.latency = networkConditions + ? networkConditions.latency + : 0; + + await this.#updateNetworkConditions(); + } + + async #updateNetworkConditions(): Promise<void> { + await this.#client.send('Network.emulateNetworkConditions', { + offline: this.#emulatedNetworkConditions.offline, + latency: this.#emulatedNetworkConditions.latency, + uploadThroughput: this.#emulatedNetworkConditions.upload, + downloadThroughput: this.#emulatedNetworkConditions.download, + }); + } + + async setUserAgent( + userAgent: string, + userAgentMetadata?: Protocol.Emulation.UserAgentMetadata + ): Promise<void> { + await this.#client.send('Network.setUserAgentOverride', { + userAgent: userAgent, + userAgentMetadata: userAgentMetadata, + }); + } + + async setCacheEnabled(enabled: boolean): Promise<void> { + this.#userCacheDisabled = !enabled; + await this.#updateProtocolCacheDisabled(); + } + + async setRequestInterception(value: boolean): Promise<void> { + this.#userRequestInterceptionEnabled = value; + await this.#updateProtocolRequestInterception(); + } + + async #updateProtocolRequestInterception(): Promise<void> { + const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials; + if (enabled === this.#protocolRequestInterceptionEnabled) { + return; + } + this.#protocolRequestInterceptionEnabled = enabled; + if (enabled) { + await Promise.all([ + this.#updateProtocolCacheDisabled(), + this.#client.send('Fetch.enable', { + handleAuthRequests: true, + patterns: [{urlPattern: '*'}], + }), + ]); + } else { + await Promise.all([ + this.#updateProtocolCacheDisabled(), + this.#client.send('Fetch.disable'), + ]); + } + } + + #cacheDisabled(): boolean { + return this.#userCacheDisabled; + } + + async #updateProtocolCacheDisabled(): Promise<void> { + await this.#client.send('Network.setCacheDisabled', { + cacheDisabled: this.#cacheDisabled(), + }); + } + + #onRequestWillBeSent(event: Protocol.Network.RequestWillBeSentEvent): void { + // Request interception doesn't happen for data URLs with Network Service. + if ( + this.#userRequestInterceptionEnabled && + !event.request.url.startsWith('data:') + ) { + const {requestId: networkRequestId} = event; + + this.#networkEventManager.storeRequestWillBeSent(networkRequestId, event); + + /** + * CDP may have sent a Fetch.requestPaused event already. Check for it. + */ + const requestPausedEvent = + this.#networkEventManager.getRequestPaused(networkRequestId); + if (requestPausedEvent) { + const {requestId: fetchRequestId} = requestPausedEvent; + this.#patchRequestEventHeaders(event, requestPausedEvent); + this.#onRequest(event, fetchRequestId); + this.#networkEventManager.forgetRequestPaused(networkRequestId); + } + + return; + } + this.#onRequest(event, undefined); + } + + #onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void { + let response: Protocol.Fetch.AuthChallengeResponse['response'] = 'Default'; + if (this.#attemptedAuthentications.has(event.requestId)) { + response = 'CancelAuth'; + } else if (this.#credentials) { + response = 'ProvideCredentials'; + this.#attemptedAuthentications.add(event.requestId); + } + const {username, password} = this.#credentials || { + username: undefined, + password: undefined, + }; + this.#client + .send('Fetch.continueWithAuth', { + requestId: event.requestId, + authChallengeResponse: {response, username, password}, + }) + .catch(debugError); + } + + /** + * CDP may send a Fetch.requestPaused without or before a + * Network.requestWillBeSent + * + * CDP may send multiple Fetch.requestPaused + * for the same Network.requestWillBeSent. + */ + #onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void { + if ( + !this.#userRequestInterceptionEnabled && + this.#protocolRequestInterceptionEnabled + ) { + this.#client + .send('Fetch.continueRequest', { + requestId: event.requestId, + }) + .catch(debugError); + } + + const {networkId: networkRequestId, requestId: fetchRequestId} = event; + + if (!networkRequestId) { + this.#onRequestWithoutNetworkInstrumentation(event); + return; + } + + const requestWillBeSentEvent = (() => { + const requestWillBeSentEvent = + this.#networkEventManager.getRequestWillBeSent(networkRequestId); + + // redirect requests have the same `requestId`, + if ( + requestWillBeSentEvent && + (requestWillBeSentEvent.request.url !== event.request.url || + requestWillBeSentEvent.request.method !== event.request.method) + ) { + this.#networkEventManager.forgetRequestWillBeSent(networkRequestId); + return; + } + return requestWillBeSentEvent; + })(); + + if (requestWillBeSentEvent) { + this.#patchRequestEventHeaders(requestWillBeSentEvent, event); + this.#onRequest(requestWillBeSentEvent, fetchRequestId); + } else { + this.#networkEventManager.storeRequestPaused(networkRequestId, event); + } + } + + #patchRequestEventHeaders( + requestWillBeSentEvent: Protocol.Network.RequestWillBeSentEvent, + requestPausedEvent: Protocol.Fetch.RequestPausedEvent + ): void { + requestWillBeSentEvent.request.headers = { + ...requestWillBeSentEvent.request.headers, + // includes extra headers, like: Accept, Origin + ...requestPausedEvent.request.headers, + }; + } + + #onRequestWithoutNetworkInstrumentation( + event: Protocol.Fetch.RequestPausedEvent + ): void { + // If an event has no networkId it should not have any network events. We + // still want to dispatch it for the interception by the user. + const frame = event.frameId + ? this.#frameManager.frame(event.frameId) + : null; + + const request = new HTTPRequest( + this.#client, + frame, + event.requestId, + this.#userRequestInterceptionEnabled, + event, + [] + ); + this.emit(NetworkManagerEmittedEvents.Request, request); + void request.finalizeInterceptions(); + } + + #onRequest( + event: Protocol.Network.RequestWillBeSentEvent, + fetchRequestId?: FetchRequestId + ): void { + let redirectChain: HTTPRequest[] = []; + if (event.redirectResponse) { + // We want to emit a response and requestfinished for the + // redirectResponse, but we can't do so unless we have a + // responseExtraInfo ready to pair it up with. If we don't have any + // responseExtraInfos saved in our queue, they we have to wait until + // the next one to emit response and requestfinished, *and* we should + // also wait to emit this Request too because it should come after the + // response/requestfinished. + let redirectResponseExtraInfo = null; + if (event.redirectHasExtraInfo) { + redirectResponseExtraInfo = this.#networkEventManager + .responseExtraInfo(event.requestId) + .shift(); + if (!redirectResponseExtraInfo) { + this.#networkEventManager.queueRedirectInfo(event.requestId, { + event, + fetchRequestId, + }); + return; + } + } + + const request = this.#networkEventManager.getRequest(event.requestId); + // If we connect late to the target, we could have missed the + // requestWillBeSent event. + if (request) { + this.#handleRequestRedirect( + request, + event.redirectResponse, + redirectResponseExtraInfo + ); + redirectChain = request._redirectChain; + } + } + const frame = event.frameId + ? this.#frameManager.frame(event.frameId) + : null; + + const request = new HTTPRequest( + this.#client, + frame, + fetchRequestId, + this.#userRequestInterceptionEnabled, + event, + redirectChain + ); + this.#networkEventManager.storeRequest(event.requestId, request); + this.emit(NetworkManagerEmittedEvents.Request, request); + void request.finalizeInterceptions(); + } + + #onRequestServedFromCache( + event: Protocol.Network.RequestServedFromCacheEvent + ): void { + const request = this.#networkEventManager.getRequest(event.requestId); + if (request) { + request._fromMemoryCache = true; + } + this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request); + } + + #handleRequestRedirect( + request: HTTPRequest, + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null + ): void { + const response = new HTTPResponse( + this.#client, + request, + responsePayload, + extraInfo + ); + request._response = response; + request._redirectChain.push(request); + response._resolveBody( + new Error('Response body is unavailable for redirect responses') + ); + this.#forgetRequest(request, false); + this.emit(NetworkManagerEmittedEvents.Response, response); + this.emit(NetworkManagerEmittedEvents.RequestFinished, request); + } + + #emitResponseEvent( + responseReceived: Protocol.Network.ResponseReceivedEvent, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null + ): void { + const request = this.#networkEventManager.getRequest( + responseReceived.requestId + ); + // FileUpload sends a response without a matching request. + if (!request) { + return; + } + + const extraInfos = this.#networkEventManager.responseExtraInfo( + responseReceived.requestId + ); + if (extraInfos.length) { + debugError( + new Error( + 'Unexpected extraInfo events for request ' + + responseReceived.requestId + ) + ); + } + + // Chromium sends wrong extraInfo events for responses served from cache. + // See https://github.com/puppeteer/puppeteer/issues/9965 and + // https://crbug.com/1340398. + if (responseReceived.response.fromDiskCache) { + extraInfo = null; + } + + const response = new HTTPResponse( + this.#client, + request, + responseReceived.response, + extraInfo + ); + request._response = response; + this.emit(NetworkManagerEmittedEvents.Response, response); + } + + #onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { + const request = this.#networkEventManager.getRequest(event.requestId); + let extraInfo = null; + if (request && !request._fromMemoryCache && event.hasExtraInfo) { + extraInfo = this.#networkEventManager + .responseExtraInfo(event.requestId) + .shift(); + if (!extraInfo) { + // Wait until we get the corresponding ExtraInfo event. + this.#networkEventManager.queueEventGroup(event.requestId, { + responseReceivedEvent: event, + }); + return; + } + } + this.#emitResponseEvent(event, extraInfo); + } + + #onResponseReceivedExtraInfo( + event: Protocol.Network.ResponseReceivedExtraInfoEvent + ): void { + // We may have skipped a redirect response/request pair due to waiting for + // this ExtraInfo event. If so, continue that work now that we have the + // request. + const redirectInfo = this.#networkEventManager.takeQueuedRedirectInfo( + event.requestId + ); + if (redirectInfo) { + this.#networkEventManager.responseExtraInfo(event.requestId).push(event); + this.#onRequest(redirectInfo.event, redirectInfo.fetchRequestId); + return; + } + + // We may have skipped response and loading events because we didn't have + // this ExtraInfo event yet. If so, emit those events now. + const queuedEvents = this.#networkEventManager.getQueuedEventGroup( + event.requestId + ); + if (queuedEvents) { + this.#networkEventManager.forgetQueuedEventGroup(event.requestId); + this.#emitResponseEvent(queuedEvents.responseReceivedEvent, event); + if (queuedEvents.loadingFinishedEvent) { + this.#emitLoadingFinished(queuedEvents.loadingFinishedEvent); + } + if (queuedEvents.loadingFailedEvent) { + this.#emitLoadingFailed(queuedEvents.loadingFailedEvent); + } + return; + } + + // Wait until we get another event that can use this ExtraInfo event. + this.#networkEventManager.responseExtraInfo(event.requestId).push(event); + } + + #forgetRequest(request: HTTPRequest, events: boolean): void { + const requestId = request._requestId; + const interceptionId = request._interceptionId; + + this.#networkEventManager.forgetRequest(requestId); + interceptionId !== undefined && + this.#attemptedAuthentications.delete(interceptionId); + + if (events) { + this.#networkEventManager.forget(requestId); + } + } + + #onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { + // If the response event for this request is still waiting on a + // corresponding ExtraInfo event, then wait to emit this event too. + const queuedEvents = this.#networkEventManager.getQueuedEventGroup( + event.requestId + ); + if (queuedEvents) { + queuedEvents.loadingFinishedEvent = event; + } else { + this.#emitLoadingFinished(event); + } + } + + #emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { + const request = this.#networkEventManager.getRequest(event.requestId); + // For certain requestIds we never receive requestWillBeSent event. + // @see https://crbug.com/750469 + if (!request) { + return; + } + + // Under certain conditions we never get the Network.responseReceived + // event from protocol. @see https://crbug.com/883475 + if (request.response()) { + request.response()?._resolveBody(null); + } + this.#forgetRequest(request, true); + this.emit(NetworkManagerEmittedEvents.RequestFinished, request); + } + + #onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { + // If the response event for this request is still waiting on a + // corresponding ExtraInfo event, then wait to emit this event too. + const queuedEvents = this.#networkEventManager.getQueuedEventGroup( + event.requestId + ); + if (queuedEvents) { + queuedEvents.loadingFailedEvent = event; + } else { + this.#emitLoadingFailed(event); + } + } + + #emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { + const request = this.#networkEventManager.getRequest(event.requestId); + // For certain requestIds we never receive requestWillBeSent event. + // @see https://crbug.com/750469 + if (!request) { + return; + } + request._failureText = event.errorText; + const response = request.response(); + if (response) { + response._resolveBody(null); + } + this.#forgetRequest(request, true); + this.emit(NetworkManagerEmittedEvents.RequestFailed, request); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/NodeWebSocketTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/NodeWebSocketTransport.ts new file mode 100644 index 0000000000..2a864df651 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/NodeWebSocketTransport.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 NodeWebSocket from 'ws'; + +import {ConnectionTransport} from '../common/ConnectionTransport.js'; +import {packageVersion} from '../generated/version.js'; + +/** + * @internal + */ +export class NodeWebSocketTransport implements ConnectionTransport { + static create( + url: string, + headers?: Record<string, string> + ): Promise<NodeWebSocketTransport> { + return new Promise((resolve, reject) => { + const ws = new NodeWebSocket(url, [], { + followRedirects: true, + perMessageDeflate: false, + maxPayload: 256 * 1024 * 1024, // 256Mb + headers: { + 'User-Agent': `Puppeteer ${packageVersion}`, + ...headers, + }, + }); + + ws.addEventListener('open', () => { + return resolve(new NodeWebSocketTransport(ws)); + }); + ws.addEventListener('error', reject); + }); + } + + #ws: NodeWebSocket; + onmessage?: (message: NodeWebSocket.Data) => void; + onclose?: () => void; + + constructor(ws: NodeWebSocket) { + this.#ws = ws; + this.#ws.addEventListener('message', event => { + if (this.onmessage) { + this.onmessage.call(null, event.data); + } + }); + this.#ws.addEventListener('close', () => { + if (this.onclose) { + this.onclose.call(null); + } + }); + // Silently ignore all errors - we don't know what to do with them. + this.#ws.addEventListener('error', () => {}); + } + + send(message: string): void { + this.#ws.send(message); + } + + close(): void { + this.#ws.close(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts new file mode 100644 index 0000000000..7b9eed7872 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PDFOptions.ts @@ -0,0 +1,221 @@ +/** + * 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. + */ + +/** + * @public + */ +export interface PDFMargin { + top?: string | number; + bottom?: string | number; + left?: string | number; + right?: string | number; +} + +/** + * @public + */ +export type LowerCasePaperFormat = + | 'letter' + | 'legal' + | 'tabloid' + | 'ledger' + | 'a0' + | 'a1' + | 'a2' + | 'a3' + | 'a4' + | 'a5' + | 'a6'; + +/** + * All the valid paper format types when printing a PDF. + * + * @remarks + * + * The sizes of each format are as follows: + * + * - `Letter`: 8.5in x 11in + * + * - `Legal`: 8.5in x 14in + * + * - `Tabloid`: 11in x 17in + * + * - `Ledger`: 17in x 11in + * + * - `A0`: 33.1in x 46.8in + * + * - `A1`: 23.4in x 33.1in + * + * - `A2`: 16.54in x 23.4in + * + * - `A3`: 11.7in x 16.54in + * + * - `A4`: 8.27in x 11.7in + * + * - `A5`: 5.83in x 8.27in + * + * - `A6`: 4.13in x 5.83in + * + * @public + */ +export type PaperFormat = + | Uppercase<LowerCasePaperFormat> + | Capitalize<LowerCasePaperFormat> + | LowerCasePaperFormat; + +/** + * Valid options to configure PDF generation via {@link Page.pdf}. + * @public + */ +export interface PDFOptions { + /** + * Scales the rendering of the web page. Amount must be between `0.1` and `2`. + * @defaultValue `1` + */ + scale?: number; + /** + * Whether to show the header and footer. + * @defaultValue `false` + */ + displayHeaderFooter?: boolean; + /** + * HTML template for the print header. Should be valid HTML with the following + * classes used to inject values into them: + * + * - `date` formatted print date + * + * - `title` document title + * + * - `url` document location + * + * - `pageNumber` current page number + * + * - `totalPages` total pages in the document + */ + headerTemplate?: string; + /** + * HTML template for the print footer. Has the same constraints and support + * for special classes as {@link PDFOptions | PDFOptions.headerTemplate}. + */ + footerTemplate?: string; + /** + * Set to `true` to print background graphics. + * @defaultValue `false` + */ + printBackground?: boolean; + /** + * Whether to print in landscape orientation. + * @defaultValue `false` + */ + landscape?: boolean; + /** + * Paper ranges to print, e.g. `1-5, 8, 11-13`. + * @defaultValue The empty string, which means all pages are printed. + */ + pageRanges?: string; + /** + * @remarks + * If set, this takes priority over the `width` and `height` options. + * @defaultValue `letter`. + */ + format?: PaperFormat; + /** + * Sets the width of paper. You can pass in a number or a string with a unit. + */ + width?: string | number; + /** + * Sets the height of paper. You can pass in a number or a string with a unit. + */ + height?: string | number; + /** + * Give any CSS `@page` size declared in the page priority over what is + * declared in the `width` or `height` or `format` option. + * @defaultValue `false`, which will scale the content to fit the paper size. + */ + preferCSSPageSize?: boolean; + /** + * Set the PDF margins. + * @defaultValue `undefined` no margins are set. + */ + margin?: PDFMargin; + /** + * The path to save the file to. + * + * @remarks + * + * If the path is relative, it's resolved relative to the current working directory. + * + * @defaultValue `undefined`, which means the PDF will not be written to disk. + */ + path?: string; + /** + * Hides default white background and allows generating pdfs with transparency. + * @defaultValue `false` + */ + omitBackground?: boolean; + /** + * Timeout in milliseconds. Pass `0` to disable timeout. + * @defaultValue `30_000` + */ + timeout?: number; +} + +/** + * @internal + */ +export interface PaperFormatDimensions { + width: number; + height: number; +} + +/** + * @internal + */ +export interface ParsedPDFOptionsInterface { + width: number; + height: number; + margin: { + top: number; + bottom: number; + left: number; + right: number; + }; +} + +/** + * @internal + */ +export type ParsedPDFOptions = Required< + Omit<PDFOptions, 'path' | 'format'> & ParsedPDFOptionsInterface +>; + +/** + * @internal + */ +export const paperFormats: Record<LowerCasePaperFormat, PaperFormatDimensions> = + { + letter: {width: 8.5, height: 11}, + legal: {width: 8.5, height: 14}, + tabloid: {width: 11, height: 17}, + ledger: {width: 17, height: 11}, + a0: {width: 33.1, height: 46.8}, + a1: {width: 23.4, height: 33.1}, + a2: {width: 16.54, height: 23.4}, + a3: {width: 11.7, height: 16.54}, + a4: {width: 8.27, height: 11.7}, + a5: {width: 5.83, height: 8.27}, + a6: {width: 4.13, height: 5.83}, + } as const; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts new file mode 100644 index 0000000000..4044c4ba4d --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PQueryHandler.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {QueryHandler, QuerySelector, QuerySelectorAll} from './QueryHandler.js'; + +/** + * @internal + */ +export class PQueryHandler extends QueryHandler { + static override querySelectorAll: QuerySelectorAll = ( + element, + selector, + {pQuerySelectorAll} + ) => { + return pQuerySelectorAll(element, selector); + }; + static override querySelector: QuerySelector = ( + element, + selector, + {pQuerySelector} + ) => { + return pQuerySelector(element, selector); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Page.ts new file mode 100644 index 0000000000..543065597e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Page.ts @@ -0,0 +1,1656 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {Readable} from 'stream'; + +import {Protocol} from 'devtools-protocol'; + +import type {Browser} from '../api/Browser.js'; +import type {BrowserContext} from '../api/BrowserContext.js'; +import {ClickOptions, ElementHandle} from '../api/ElementHandle.js'; +import {HTTPRequest} from '../api/HTTPRequest.js'; +import {HTTPResponse} from '../api/HTTPResponse.js'; +import {JSHandle} from '../api/JSHandle.js'; +import { + GeolocationOptions, + MediaFeature, + Metrics, + Page, + PageEmittedEvents, + ScreenshotClip, + ScreenshotOptions, + WaitForOptions, + WaitTimeoutOptions, +} from '../api/Page.js'; +import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {Accessibility} from './Accessibility.js'; +import {Binding} from './Binding.js'; +import { + CDPSession, + CDPSessionEmittedEvents, + isTargetClosedError, +} from './Connection.js'; +import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; +import {Coverage} from './Coverage.js'; +import {DeviceRequestPrompt} from './DeviceRequestPrompt.js'; +import {Dialog} from './Dialog.js'; +import {EmulationManager} from './EmulationManager.js'; +import {FileChooser} from './FileChooser.js'; +import { + Frame, + FrameAddScriptTagOptions, + FrameAddStyleTagOptions, + FrameWaitForFunctionOptions, +} from './Frame.js'; +import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; +import {Keyboard, Mouse, Touchscreen} from './Input.js'; +import {WaitForSelectorOptions} from './IsolatedWorld.js'; +import {MAIN_WORLD} from './IsolatedWorlds.js'; +import { + Credentials, + NetworkConditions, + NetworkManagerEmittedEvents, +} from './NetworkManager.js'; +import {PDFOptions} from './PDFOptions.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {Target} from './Target.js'; +import {TargetManagerEmittedEvents} from './TargetManager.js'; +import {TaskQueue} from './TaskQueue.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; +import {Tracing} from './Tracing.js'; +import { + BindingPayload, + EvaluateFunc, + EvaluateFuncWith, + HandleFor, + NodeFor, +} from './types.js'; +import { + createJSHandle, + debugError, + evaluationString, + getExceptionMessage, + getReadableAsBuffer, + getReadableFromProtocolStream, + isString, + pageBindingInitString, + releaseObject, + valueFromRemoteObject, + waitForEvent, + waitWithTimeout, +} from './util.js'; +import {WebWorker} from './WebWorker.js'; + +/** + * @internal + */ +export class CDPPage extends Page { + /** + * @internal + */ + static async _create( + client: CDPSession, + target: Target, + ignoreHTTPSErrors: boolean, + defaultViewport: Viewport | null, + screenshotTaskQueue: TaskQueue + ): Promise<CDPPage> { + const page = new CDPPage( + client, + target, + ignoreHTTPSErrors, + screenshotTaskQueue + ); + await page.#initialize(); + if (defaultViewport) { + try { + await page.setViewport(defaultViewport); + } catch (err) { + if (isErrorLike(err) && isTargetClosedError(err)) { + debugError(err); + } else { + throw err; + } + } + } + return page; + } + + #closed = false; + #client: CDPSession; + #target: Target; + #keyboard: Keyboard; + #mouse: Mouse; + #timeoutSettings = new TimeoutSettings(); + #touchscreen: Touchscreen; + #accessibility: Accessibility; + #frameManager: FrameManager; + #emulationManager: EmulationManager; + #tracing: Tracing; + #bindings = new Map<string, Binding>(); + #coverage: Coverage; + #javascriptEnabled = true; + #viewport: Viewport | null; + #screenshotTaskQueue: TaskQueue; + #workers = new Map<string, WebWorker>(); + #fileChooserPromises = new Set<DeferredPromise<FileChooser>>(); + + #disconnectPromise?: Promise<Error>; + #userDragInterceptionEnabled = false; + + /** + * @internal + */ + constructor( + client: CDPSession, + target: Target, + ignoreHTTPSErrors: boolean, + screenshotTaskQueue: TaskQueue + ) { + super(); + this.#client = client; + this.#target = target; + this.#keyboard = new Keyboard(client); + this.#mouse = new Mouse(client, this.#keyboard); + this.#touchscreen = new Touchscreen(client, this.#keyboard); + this.#accessibility = new Accessibility(client); + this.#frameManager = new FrameManager( + client, + this, + ignoreHTTPSErrors, + this.#timeoutSettings + ); + this.#emulationManager = new EmulationManager(client); + this.#tracing = new Tracing(client); + this.#coverage = new Coverage(client); + this.#screenshotTaskQueue = screenshotTaskQueue; + this.#viewport = null; + + this.#target + ._targetManager() + .addTargetInterceptor(this.#client, this.#onAttachedToTarget); + + this.#target + ._targetManager() + .on(TargetManagerEmittedEvents.TargetGone, this.#onDetachedFromTarget); + + this.#frameManager.on(FrameManagerEmittedEvents.FrameAttached, event => { + return this.emit(PageEmittedEvents.FrameAttached, event); + }); + this.#frameManager.on(FrameManagerEmittedEvents.FrameDetached, event => { + return this.emit(PageEmittedEvents.FrameDetached, event); + }); + this.#frameManager.on(FrameManagerEmittedEvents.FrameNavigated, event => { + return this.emit(PageEmittedEvents.FrameNavigated, event); + }); + + const networkManager = this.#frameManager.networkManager; + networkManager.on(NetworkManagerEmittedEvents.Request, event => { + return this.emit(PageEmittedEvents.Request, event); + }); + networkManager.on( + NetworkManagerEmittedEvents.RequestServedFromCache, + event => { + return this.emit(PageEmittedEvents.RequestServedFromCache, event); + } + ); + networkManager.on(NetworkManagerEmittedEvents.Response, event => { + return this.emit(PageEmittedEvents.Response, event); + }); + networkManager.on(NetworkManagerEmittedEvents.RequestFailed, event => { + return this.emit(PageEmittedEvents.RequestFailed, event); + }); + networkManager.on(NetworkManagerEmittedEvents.RequestFinished, event => { + return this.emit(PageEmittedEvents.RequestFinished, event); + }); + + client.on('Page.domContentEventFired', () => { + return this.emit(PageEmittedEvents.DOMContentLoaded); + }); + client.on('Page.loadEventFired', () => { + return this.emit(PageEmittedEvents.Load); + }); + client.on('Runtime.consoleAPICalled', event => { + return this.#onConsoleAPI(event); + }); + client.on('Runtime.bindingCalled', event => { + return this.#onBindingCalled(event); + }); + client.on('Page.javascriptDialogOpening', event => { + return this.#onDialog(event); + }); + client.on('Runtime.exceptionThrown', exception => { + return this.#handleException(exception.exceptionDetails); + }); + client.on('Inspector.targetCrashed', () => { + return this.#onTargetCrashed(); + }); + client.on('Performance.metrics', event => { + return this.#emitMetrics(event); + }); + client.on('Log.entryAdded', event => { + return this.#onLogEntryAdded(event); + }); + client.on('Page.fileChooserOpened', event => { + return this.#onFileChooser(event); + }); + void this.#target._isClosedPromise.then(() => { + this.#target + ._targetManager() + .removeTargetInterceptor(this.#client, this.#onAttachedToTarget); + + this.#target + ._targetManager() + .off(TargetManagerEmittedEvents.TargetGone, this.#onDetachedFromTarget); + this.emit(PageEmittedEvents.Close); + this.#closed = true; + }); + } + + #onDetachedFromTarget = (target: Target) => { + const sessionId = target._session()?.id(); + const worker = this.#workers.get(sessionId!); + if (!worker) { + return; + } + this.#workers.delete(sessionId!); + this.emit(PageEmittedEvents.WorkerDestroyed, worker); + }; + + #onAttachedToTarget = (createdTarget: Target) => { + this.#frameManager.onAttachedToTarget(createdTarget); + if (createdTarget._getTargetInfo().type === 'worker') { + const session = createdTarget._session(); + assert(session); + const worker = new WebWorker( + session, + createdTarget.url(), + this.#addConsoleMessage.bind(this), + this.#handleException.bind(this) + ); + this.#workers.set(session.id(), worker); + this.emit(PageEmittedEvents.WorkerCreated, worker); + } + if (createdTarget._session()) { + this.#target + ._targetManager() + .addTargetInterceptor( + createdTarget._session()!, + this.#onAttachedToTarget + ); + } + }; + + async #initialize(): Promise<void> { + try { + await Promise.all([ + this.#frameManager.initialize(), + this.#client.send('Performance.enable'), + this.#client.send('Log.enable'), + ]); + } catch (err) { + if (isErrorLike(err) && isTargetClosedError(err)) { + debugError(err); + } else { + throw err; + } + } + } + + async #onFileChooser( + event: Protocol.Page.FileChooserOpenedEvent + ): Promise<void> { + if (!this.#fileChooserPromises.size) { + return; + } + + const frame = this.#frameManager.frame(event.frameId); + assert(frame, 'This should never happen.'); + + // This is guaranteed to be an HTMLInputElement handle by the event. + const handle = (await frame.worlds[MAIN_WORLD].adoptBackendNode( + event.backendNodeId + )) as ElementHandle<HTMLInputElement>; + + const fileChooser = new FileChooser(handle, event); + for (const promise of this.#fileChooserPromises) { + promise.resolve(fileChooser); + } + this.#fileChooserPromises.clear(); + } + + /** + * @internal + */ + _client(): CDPSession { + return this.#client; + } + + override isDragInterceptionEnabled(): boolean { + return this.#userDragInterceptionEnabled; + } + + override isJavaScriptEnabled(): boolean { + return this.#javascriptEnabled; + } + + override waitForFileChooser( + options: WaitTimeoutOptions = {} + ): Promise<FileChooser> { + const needsEnable = this.#fileChooserPromises.size === 0; + const {timeout = this.#timeoutSettings.timeout()} = options; + const promise = createDeferredPromise<FileChooser>({ + message: `Waiting for \`FileChooser\` failed: ${timeout}ms exceeded`, + timeout, + }); + this.#fileChooserPromises.add(promise); + let enablePromise: Promise<void> | undefined; + if (needsEnable) { + enablePromise = this.#client.send('Page.setInterceptFileChooserDialog', { + enabled: true, + }); + } + return Promise.all([promise, enablePromise]) + .then(([result]) => { + return result; + }) + .catch(error => { + this.#fileChooserPromises.delete(promise); + throw error; + }); + } + + override async setGeolocation(options: GeolocationOptions): Promise<void> { + const {longitude, latitude, accuracy = 0} = options; + if (longitude < -180 || longitude > 180) { + throw new Error( + `Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.` + ); + } + if (latitude < -90 || latitude > 90) { + throw new Error( + `Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.` + ); + } + if (accuracy < 0) { + throw new Error( + `Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.` + ); + } + await this.#client.send('Emulation.setGeolocationOverride', { + longitude, + latitude, + accuracy, + }); + } + + override target(): Target { + return this.#target; + } + + override browser(): Browser { + return this.#target.browser(); + } + + override browserContext(): BrowserContext { + return this.#target.browserContext(); + } + + #onTargetCrashed(): void { + this.emit('error', new Error('Page crashed!')); + } + + #onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void { + const {level, text, args, source, url, lineNumber} = event.entry; + if (args) { + args.map(arg => { + return releaseObject(this.#client, arg); + }); + } + if (source !== 'worker') { + this.emit( + PageEmittedEvents.Console, + new ConsoleMessage(level, text, [], [{url, lineNumber}]) + ); + } + } + + override mainFrame(): Frame { + return this.#frameManager.mainFrame(); + } + + override get keyboard(): Keyboard { + return this.#keyboard; + } + + override get touchscreen(): Touchscreen { + return this.#touchscreen; + } + + override get coverage(): Coverage { + return this.#coverage; + } + + override get tracing(): Tracing { + return this.#tracing; + } + + override get accessibility(): Accessibility { + return this.#accessibility; + } + + override frames(): Frame[] { + return this.#frameManager.frames(); + } + + override workers(): WebWorker[] { + return Array.from(this.#workers.values()); + } + + override async setRequestInterception(value: boolean): Promise<void> { + return this.#frameManager.networkManager.setRequestInterception(value); + } + + override async setDragInterception(enabled: boolean): Promise<void> { + this.#userDragInterceptionEnabled = enabled; + return this.#client.send('Input.setInterceptDrags', {enabled}); + } + + override setOfflineMode(enabled: boolean): Promise<void> { + return this.#frameManager.networkManager.setOfflineMode(enabled); + } + + override emulateNetworkConditions( + networkConditions: NetworkConditions | null + ): Promise<void> { + return this.#frameManager.networkManager.emulateNetworkConditions( + networkConditions + ); + } + + override setDefaultNavigationTimeout(timeout: number): void { + this.#timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + override setDefaultTimeout(timeout: number): void { + this.#timeoutSettings.setDefaultTimeout(timeout); + } + + override getDefaultTimeout(): number { + return this.#timeoutSettings.timeout(); + } + + override async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + return this.mainFrame().$(selector); + } + + override async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>> { + return this.mainFrame().$$(selector); + } + + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + const context = await this.mainFrame().executionContext(); + return context.evaluateHandle(pageFunction, ...args); + } + + override async queryObjects<Prototype>( + prototypeHandle: JSHandle<Prototype> + ): Promise<JSHandle<Prototype[]>> { + const context = await this.mainFrame().executionContext(); + assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); + assert( + prototypeHandle.id, + 'Prototype JSHandle must not be referencing primitive value' + ); + const response = await context._client.send('Runtime.queryObjects', { + prototypeObjectId: prototypeHandle.id, + }); + return createJSHandle(context, response.objects) as HandleFor<Prototype[]>; + } + + override async $eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith< + NodeFor<Selector>, + Params + > + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.mainFrame().$eval(selector, pageFunction, ...args); + } + + override async $$eval< + Selector extends string, + Params extends unknown[], + Func extends EvaluateFuncWith< + Array<NodeFor<Selector>>, + Params + > = EvaluateFuncWith<Array<NodeFor<Selector>>, Params> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.mainFrame().$$eval(selector, pageFunction, ...args); + } + + override async $x(expression: string): Promise<Array<ElementHandle<Node>>> { + return this.mainFrame().$x(expression); + } + + override async cookies( + ...urls: string[] + ): Promise<Protocol.Network.Cookie[]> { + const originalCookies = ( + await this.#client.send('Network.getCookies', { + urls: urls.length ? urls : [this.url()], + }) + ).cookies; + + const unsupportedCookieAttributes = ['priority']; + const filterUnsupportedAttributes = ( + cookie: Protocol.Network.Cookie + ): Protocol.Network.Cookie => { + for (const attr of unsupportedCookieAttributes) { + delete (cookie as unknown as Record<string, unknown>)[attr]; + } + return cookie; + }; + return originalCookies.map(filterUnsupportedAttributes); + } + + override async deleteCookie( + ...cookies: Protocol.Network.DeleteCookiesRequest[] + ): Promise<void> { + const pageURL = this.url(); + for (const cookie of cookies) { + const item = Object.assign({}, cookie); + if (!cookie.url && pageURL.startsWith('http')) { + item.url = pageURL; + } + await this.#client.send('Network.deleteCookies', item); + } + } + + override async setCookie( + ...cookies: Protocol.Network.CookieParam[] + ): Promise<void> { + const pageURL = this.url(); + const startsWithHTTP = pageURL.startsWith('http'); + const items = cookies.map(cookie => { + const item = Object.assign({}, cookie); + if (!item.url && startsWithHTTP) { + item.url = pageURL; + } + assert( + item.url !== 'about:blank', + `Blank page can not have cookie "${item.name}"` + ); + assert( + !String.prototype.startsWith.call(item.url || '', 'data:'), + `Data URL page can not have cookie "${item.name}"` + ); + return item; + }); + await this.deleteCookie(...items); + if (items.length) { + await this.#client.send('Network.setCookies', {cookies: items}); + } + } + + override async addScriptTag( + options: FrameAddScriptTagOptions + ): Promise<ElementHandle<HTMLScriptElement>> { + return this.mainFrame().addScriptTag(options); + } + + override async addStyleTag( + options: Omit<FrameAddStyleTagOptions, 'url'> + ): Promise<ElementHandle<HTMLStyleElement>>; + override async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLLinkElement>>; + override async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> { + return this.mainFrame().addStyleTag(options); + } + + override async exposeFunction( + name: string, + pptrFunction: Function | {default: Function} + ): Promise<void> { + if (this.#bindings.has(name)) { + throw new Error( + `Failed to add page binding with name ${name}: window['${name}'] already exists!` + ); + } + + let binding: Binding; + switch (typeof pptrFunction) { + case 'function': + binding = new Binding( + name, + pptrFunction as (...args: unknown[]) => unknown + ); + break; + default: + binding = new Binding( + name, + pptrFunction.default as (...args: unknown[]) => unknown + ); + break; + } + + this.#bindings.set(name, binding); + + const expression = pageBindingInitString('exposedFun', name); + await this.#client.send('Runtime.addBinding', {name: name}); + await this.#client.send('Page.addScriptToEvaluateOnNewDocument', { + source: expression, + }); + await Promise.all( + this.frames().map(frame => { + return frame.evaluate(expression).catch(debugError); + }) + ); + } + + override async authenticate(credentials: Credentials): Promise<void> { + return this.#frameManager.networkManager.authenticate(credentials); + } + + override async setExtraHTTPHeaders( + headers: Record<string, string> + ): Promise<void> { + return this.#frameManager.networkManager.setExtraHTTPHeaders(headers); + } + + override async setUserAgent( + userAgent: string, + userAgentMetadata?: Protocol.Emulation.UserAgentMetadata + ): Promise<void> { + return this.#frameManager.networkManager.setUserAgent( + userAgent, + userAgentMetadata + ); + } + + override async metrics(): Promise<Metrics> { + const response = await this.#client.send('Performance.getMetrics'); + return this.#buildMetricsObject(response.metrics); + } + + #emitMetrics(event: Protocol.Performance.MetricsEvent): void { + this.emit(PageEmittedEvents.Metrics, { + title: event.title, + metrics: this.#buildMetricsObject(event.metrics), + }); + } + + #buildMetricsObject(metrics?: Protocol.Performance.Metric[]): Metrics { + const result: Record< + Protocol.Performance.Metric['name'], + Protocol.Performance.Metric['value'] + > = {}; + for (const metric of metrics || []) { + if (supportedMetrics.has(metric.name)) { + result[metric.name] = metric.value; + } + } + return result; + } + + #handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails): void { + const message = getExceptionMessage(exceptionDetails); + const err = new Error(message); + err.stack = ''; // Don't report clientside error with a node stack attached + this.emit(PageEmittedEvents.PageError, err); + } + + async #onConsoleAPI( + event: Protocol.Runtime.ConsoleAPICalledEvent + ): Promise<void> { + if (event.executionContextId === 0) { + // DevTools protocol stores the last 1000 console messages. These + // messages are always reported even for removed execution contexts. In + // this case, they are marked with executionContextId = 0 and are + // reported upon enabling Runtime agent. + // + // Ignore these messages since: + // - there's no execution context we can use to operate with message + // arguments + // - these messages are reported before Puppeteer clients can subscribe + // to the 'console' + // page event. + // + // @see https://github.com/puppeteer/puppeteer/issues/3865 + return; + } + const context = this.#frameManager.getExecutionContextById( + event.executionContextId, + this.#client + ); + if (!context) { + debugError( + new Error( + `ExecutionContext not found for a console message: ${JSON.stringify( + event + )}` + ) + ); + return; + } + const values = event.args.map(arg => { + return createJSHandle(context, arg); + }); + this.#addConsoleMessage(event.type, values, event.stackTrace); + } + + async #onBindingCalled( + event: Protocol.Runtime.BindingCalledEvent + ): Promise<void> { + let payload: BindingPayload; + try { + payload = JSON.parse(event.payload); + } catch { + // The binding was either called by something in the page or it was + // called before our wrapper was initialized. + return; + } + const {type, name, seq, args, isTrivial} = payload; + if (type !== 'exposedFun') { + return; + } + + const context = this.#frameManager.executionContextById( + event.executionContextId, + this.#client + ); + if (!context) { + return; + } + + const binding = this.#bindings.get(name); + await binding?.run(context, seq, args, isTrivial); + } + + #addConsoleMessage( + eventType: ConsoleMessageType, + args: JSHandle[], + stackTrace?: Protocol.Runtime.StackTrace + ): void { + if (!this.listenerCount(PageEmittedEvents.Console)) { + args.forEach(arg => { + return arg.dispose(); + }); + return; + } + const textTokens = []; + for (const arg of args) { + const remoteObject = arg.remoteObject(); + if (remoteObject.objectId) { + textTokens.push(arg.toString()); + } else { + textTokens.push(valueFromRemoteObject(remoteObject)); + } + } + const stackTraceLocations = []; + if (stackTrace) { + for (const callFrame of stackTrace.callFrames) { + stackTraceLocations.push({ + url: callFrame.url, + lineNumber: callFrame.lineNumber, + columnNumber: callFrame.columnNumber, + }); + } + } + const message = new ConsoleMessage( + eventType, + textTokens.join(' '), + args, + stackTraceLocations + ); + this.emit(PageEmittedEvents.Console, message); + } + + #onDialog(event: Protocol.Page.JavascriptDialogOpeningEvent): void { + let dialogType = null; + const validDialogTypes = new Set<Protocol.Page.DialogType>([ + 'alert', + 'confirm', + 'prompt', + 'beforeunload', + ]); + + if (validDialogTypes.has(event.type)) { + dialogType = event.type as Protocol.Page.DialogType; + } + assert(dialogType, 'Unknown javascript dialog type: ' + event.type); + + const dialog = new Dialog( + this.#client, + dialogType, + event.message, + event.defaultPrompt + ); + this.emit(PageEmittedEvents.Dialog, dialog); + } + + /** + * Resets default white background + */ + async #resetDefaultBackgroundColor() { + await this.#client.send('Emulation.setDefaultBackgroundColorOverride'); + } + + /** + * Hides default white background + */ + async #setTransparentBackgroundColor(): Promise<void> { + await this.#client.send('Emulation.setDefaultBackgroundColorOverride', { + color: {r: 0, g: 0, b: 0, a: 0}, + }); + } + + override url(): string { + return this.mainFrame().url(); + } + + override async content(): Promise<string> { + return await this.#frameManager.mainFrame().content(); + } + + override async setContent( + html: string, + options: WaitForOptions = {} + ): Promise<void> { + await this.#frameManager.mainFrame().setContent(html, options); + } + + override async goto( + url: string, + options: WaitForOptions & {referer?: string; referrerPolicy?: string} = {} + ): Promise<HTTPResponse | null> { + return await this.#frameManager.mainFrame().goto(url, options); + } + + override async reload( + options?: WaitForOptions + ): Promise<HTTPResponse | null> { + const result = await Promise.all([ + this.waitForNavigation(options), + this.#client.send('Page.reload'), + ]); + + return result[0]; + } + + override async waitForNavigation( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return await this.#frameManager.mainFrame().waitForNavigation(options); + } + + #sessionClosePromise(): Promise<Error> { + if (!this.#disconnectPromise) { + this.#disconnectPromise = new Promise(fulfill => { + return this.#client.once(CDPSessionEmittedEvents.Disconnected, () => { + return fulfill(new Error('Target closed')); + }); + }); + } + return this.#disconnectPromise; + } + + override async waitForRequest( + urlOrPredicate: string | ((req: HTTPRequest) => boolean | Promise<boolean>), + options: {timeout?: number} = {} + ): Promise<HTTPRequest> { + const {timeout = this.#timeoutSettings.timeout()} = options; + return waitForEvent( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.Request, + async request => { + if (isString(urlOrPredicate)) { + return urlOrPredicate === request.url(); + } + if (typeof urlOrPredicate === 'function') { + return !!(await urlOrPredicate(request)); + } + return false; + }, + timeout, + this.#sessionClosePromise() + ); + } + + override async waitForResponse( + urlOrPredicate: + | string + | ((res: HTTPResponse) => boolean | Promise<boolean>), + options: {timeout?: number} = {} + ): Promise<HTTPResponse> { + const {timeout = this.#timeoutSettings.timeout()} = options; + return waitForEvent( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.Response, + async response => { + if (isString(urlOrPredicate)) { + return urlOrPredicate === response.url(); + } + if (typeof urlOrPredicate === 'function') { + return !!(await urlOrPredicate(response)); + } + return false; + }, + timeout, + this.#sessionClosePromise() + ); + } + + override async waitForNetworkIdle( + options: {idleTime?: number; timeout?: number} = {} + ): Promise<void> { + const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options; + + const networkManager = this.#frameManager.networkManager; + + const idlePromise = createDeferredPromise<void>(); + + let abortRejectCallback: (error: Error) => void; + const abortPromise = new Promise<Error>((_, reject) => { + abortRejectCallback = reject; + }); + + let idleTimer: NodeJS.Timeout; + const cleanup = () => { + idleTimer && clearTimeout(idleTimer); + abortRejectCallback(new Error('abort')); + }; + + const evaluate = () => { + idleTimer && clearTimeout(idleTimer); + if (networkManager.numRequestsInProgress() === 0) { + idleTimer = setTimeout(idlePromise.resolve, idleTime); + } + }; + + evaluate(); + + const eventHandler = () => { + evaluate(); + return false; + }; + + const listenToEvent = (event: symbol) => { + return waitForEvent( + networkManager, + event, + eventHandler, + timeout, + abortPromise + ); + }; + + const eventPromises = [ + listenToEvent(NetworkManagerEmittedEvents.Request), + listenToEvent(NetworkManagerEmittedEvents.Response), + listenToEvent(NetworkManagerEmittedEvents.RequestFailed), + ]; + + await Promise.race([ + idlePromise, + ...eventPromises, + this.#sessionClosePromise(), + ]).then( + r => { + cleanup(); + return r; + }, + error => { + cleanup(); + throw error; + } + ); + } + + override async waitForFrame( + urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>), + options: {timeout?: number} = {} + ): Promise<Frame> { + const {timeout = this.#timeoutSettings.timeout()} = options; + + let predicate: (frame: Frame) => Promise<boolean>; + if (isString(urlOrPredicate)) { + predicate = (frame: Frame) => { + return Promise.resolve(urlOrPredicate === frame.url()); + }; + } else { + predicate = (frame: Frame) => { + const value = urlOrPredicate(frame); + if (typeof value === 'boolean') { + return Promise.resolve(value); + } + return value; + }; + } + + const eventRace: Promise<Frame> = Promise.race([ + waitForEvent( + this.#frameManager, + FrameManagerEmittedEvents.FrameAttached, + predicate, + timeout, + this.#sessionClosePromise() + ), + waitForEvent( + this.#frameManager, + FrameManagerEmittedEvents.FrameNavigated, + predicate, + timeout, + this.#sessionClosePromise() + ), + ...this.frames().map(async frame => { + if (await predicate(frame)) { + return frame; + } + return await eventRace; + }), + ]); + + return eventRace; + } + + override async goBack( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return this.#go(-1, options); + } + + override async goForward( + options: WaitForOptions = {} + ): Promise<HTTPResponse | null> { + return this.#go(+1, options); + } + + async #go( + delta: number, + options: WaitForOptions + ): Promise<HTTPResponse | null> { + const history = await this.#client.send('Page.getNavigationHistory'); + const entry = history.entries[history.currentIndex + delta]; + if (!entry) { + return null; + } + const result = await Promise.all([ + this.waitForNavigation(options), + this.#client.send('Page.navigateToHistoryEntry', {entryId: entry.id}), + ]); + return result[0]; + } + + override async bringToFront(): Promise<void> { + await this.#client.send('Page.bringToFront'); + } + + override async setJavaScriptEnabled(enabled: boolean): Promise<void> { + if (this.#javascriptEnabled === enabled) { + return; + } + this.#javascriptEnabled = enabled; + await this.#client.send('Emulation.setScriptExecutionDisabled', { + value: !enabled, + }); + } + + override async setBypassCSP(enabled: boolean): Promise<void> { + await this.#client.send('Page.setBypassCSP', {enabled}); + } + + override async emulateMediaType(type?: string): Promise<void> { + assert( + type === 'screen' || + type === 'print' || + (type ?? undefined) === undefined, + 'Unsupported media type: ' + type + ); + await this.#client.send('Emulation.setEmulatedMedia', { + media: type || '', + }); + } + + override async emulateCPUThrottling(factor: number | null): Promise<void> { + assert( + factor === null || factor >= 1, + 'Throttling rate should be greater or equal to 1' + ); + await this.#client.send('Emulation.setCPUThrottlingRate', { + rate: factor !== null ? factor : 1, + }); + } + + override async emulateMediaFeatures( + features?: MediaFeature[] + ): Promise<void> { + if (!features) { + await this.#client.send('Emulation.setEmulatedMedia', {}); + } + if (Array.isArray(features)) { + for (const mediaFeature of features) { + const name = mediaFeature.name; + assert( + /^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test( + name + ), + 'Unsupported media feature: ' + name + ); + } + await this.#client.send('Emulation.setEmulatedMedia', { + features: features, + }); + } + } + + override async emulateTimezone(timezoneId?: string): Promise<void> { + try { + await this.#client.send('Emulation.setTimezoneOverride', { + timezoneId: timezoneId || '', + }); + } catch (error) { + if (isErrorLike(error) && error.message.includes('Invalid timezone')) { + throw new Error(`Invalid timezone ID: ${timezoneId}`); + } + throw error; + } + } + + override async emulateIdleState(overrides?: { + isUserActive: boolean; + isScreenUnlocked: boolean; + }): Promise<void> { + if (overrides) { + await this.#client.send('Emulation.setIdleOverride', { + isUserActive: overrides.isUserActive, + isScreenUnlocked: overrides.isScreenUnlocked, + }); + } else { + await this.#client.send('Emulation.clearIdleOverride'); + } + } + + override async emulateVisionDeficiency( + type?: Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + ): Promise<void> { + const visionDeficiencies = new Set< + Protocol.Emulation.SetEmulatedVisionDeficiencyRequest['type'] + >([ + 'none', + 'achromatopsia', + 'blurredVision', + 'deuteranopia', + 'protanopia', + 'tritanopia', + ]); + try { + assert( + !type || visionDeficiencies.has(type), + `Unsupported vision deficiency: ${type}` + ); + await this.#client.send('Emulation.setEmulatedVisionDeficiency', { + type: type || 'none', + }); + } catch (error) { + throw error; + } + } + + override async setViewport(viewport: Viewport): Promise<void> { + const needsReload = await this.#emulationManager.emulateViewport(viewport); + this.#viewport = viewport; + if (needsReload) { + await this.reload(); + } + } + + override viewport(): Viewport | null { + return this.#viewport; + } + + override async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.#frameManager.mainFrame().evaluate(pageFunction, ...args); + } + + override async evaluateOnNewDocument< + Params extends unknown[], + Func extends (...args: Params) => unknown = (...args: Params) => unknown + >(pageFunction: Func | string, ...args: Params): Promise<void> { + const source = evaluationString(pageFunction, ...args); + await this.#client.send('Page.addScriptToEvaluateOnNewDocument', { + source, + }); + } + + override async setCacheEnabled(enabled = true): Promise<void> { + await this.#frameManager.networkManager.setCacheEnabled(enabled); + } + + override screenshot( + options: ScreenshotOptions & {encoding: 'base64'} + ): Promise<string>; + override screenshot( + options?: ScreenshotOptions & {encoding?: 'binary'} + ): Promise<Buffer>; + override async screenshot( + options: ScreenshotOptions = {} + ): Promise<Buffer | string> { + let screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png; + // options.type takes precedence over inferring the type from options.path + // because it may be a 0-length file with no extension created beforehand + // (i.e. as a temp file). + if (options.type) { + screenshotType = + options.type as Protocol.Page.CaptureScreenshotRequestFormat; + } else if (options.path) { + const filePath = options.path; + const extension = filePath + .slice(filePath.lastIndexOf('.') + 1) + .toLowerCase(); + switch (extension) { + case 'png': + screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png; + break; + case 'jpeg': + case 'jpg': + screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Jpeg; + break; + case 'webp': + screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Webp; + break; + default: + throw new Error( + `Unsupported screenshot type for extension \`.${extension}\`` + ); + } + } + + if (options.quality) { + assert( + screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Jpeg || + screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Webp, + 'options.quality is unsupported for the ' + + screenshotType + + ' screenshots' + ); + assert( + typeof options.quality === 'number', + 'Expected options.quality to be a number but found ' + + typeof options.quality + ); + assert( + Number.isInteger(options.quality), + 'Expected options.quality to be an integer' + ); + assert( + options.quality >= 0 && options.quality <= 100, + 'Expected options.quality to be between 0 and 100 (inclusive), got ' + + options.quality + ); + } + assert( + !options.clip || !options.fullPage, + 'options.clip and options.fullPage are exclusive' + ); + if (options.clip) { + assert( + typeof options.clip.x === 'number', + 'Expected options.clip.x to be a number but found ' + + typeof options.clip.x + ); + assert( + typeof options.clip.y === 'number', + 'Expected options.clip.y to be a number but found ' + + typeof options.clip.y + ); + assert( + typeof options.clip.width === 'number', + 'Expected options.clip.width to be a number but found ' + + typeof options.clip.width + ); + assert( + typeof options.clip.height === 'number', + 'Expected options.clip.height to be a number but found ' + + typeof options.clip.height + ); + assert( + options.clip.width !== 0, + 'Expected options.clip.width not to be 0.' + ); + assert( + options.clip.height !== 0, + 'Expected options.clip.height not to be 0.' + ); + } + return this.#screenshotTaskQueue.postTask(() => { + return this.#screenshotTask(screenshotType, options); + }); + } + + async #screenshotTask( + format: Protocol.Page.CaptureScreenshotRequestFormat, + options: ScreenshotOptions = {} + ): Promise<Buffer | string> { + await this.#client.send('Target.activateTarget', { + targetId: this.#target._targetId, + }); + let clip = options.clip ? processClip(options.clip) : undefined; + let captureBeyondViewport = options.captureBeyondViewport ?? true; + const fromSurface = options.fromSurface; + + if (options.fullPage) { + // Overwrite clip for full page. + clip = undefined; + + if (!captureBeyondViewport) { + const metrics = await this.#client.send('Page.getLayoutMetrics'); + // Fallback to `contentSize` in case of using Firefox. + const {width, height} = metrics.cssContentSize || metrics.contentSize; + const { + isMobile = false, + deviceScaleFactor = 1, + isLandscape = false, + } = this.#viewport || {}; + const screenOrientation: Protocol.Emulation.ScreenOrientation = + isLandscape + ? {angle: 90, type: 'landscapePrimary'} + : {angle: 0, type: 'portraitPrimary'}; + await this.#client.send('Emulation.setDeviceMetricsOverride', { + mobile: isMobile, + width, + height, + deviceScaleFactor, + screenOrientation, + }); + } + } else if (!clip) { + captureBeyondViewport = false; + } + + const shouldSetDefaultBackground = + options.omitBackground && (format === 'png' || format === 'webp'); + if (shouldSetDefaultBackground) { + await this.#setTransparentBackgroundColor(); + } + + const result = await this.#client.send('Page.captureScreenshot', { + format, + quality: options.quality, + clip: clip && { + ...clip, + scale: clip.scale ?? 1, + }, + captureBeyondViewport, + fromSurface, + }); + if (shouldSetDefaultBackground) { + await this.#resetDefaultBackgroundColor(); + } + + if (options.fullPage && this.#viewport) { + await this.setViewport(this.#viewport); + } + + if (options.encoding === 'base64') { + return result.data; + } + + const buffer = Buffer.from(result.data, 'base64'); + await this._maybeWriteBufferToFile(options.path, buffer); + + return buffer; + + function processClip(clip: ScreenshotClip): ScreenshotClip { + const x = Math.round(clip.x); + const y = Math.round(clip.y); + const width = Math.round(clip.width + clip.x - x); + const height = Math.round(clip.height + clip.y - y); + return {x, y, width, height, scale: clip.scale}; + } + } + + override async createPDFStream(options: PDFOptions = {}): Promise<Readable> { + const { + landscape, + displayHeaderFooter, + headerTemplate, + footerTemplate, + printBackground, + scale, + width: paperWidth, + height: paperHeight, + margin, + pageRanges, + preferCSSPageSize, + omitBackground, + timeout, + } = this._getPDFOptions(options); + + if (omitBackground) { + await this.#setTransparentBackgroundColor(); + } + + const printCommandPromise = this.#client.send('Page.printToPDF', { + transferMode: 'ReturnAsStream', + landscape, + displayHeaderFooter, + headerTemplate, + footerTemplate, + printBackground, + scale, + paperWidth, + paperHeight, + marginTop: margin.top, + marginBottom: margin.bottom, + marginLeft: margin.left, + marginRight: margin.right, + pageRanges, + preferCSSPageSize, + }); + + const result = await waitWithTimeout( + printCommandPromise, + 'Page.printToPDF', + timeout + ); + + if (omitBackground) { + await this.#resetDefaultBackgroundColor(); + } + + assert(result.stream, '`stream` is missing from `Page.printToPDF'); + return getReadableFromProtocolStream(this.#client, result.stream); + } + + override async pdf(options: PDFOptions = {}): Promise<Buffer> { + const {path = undefined} = options; + const readable = await this.createPDFStream(options); + const buffer = await getReadableAsBuffer(readable, path); + assert(buffer, 'Could not create buffer'); + return buffer; + } + + override async title(): Promise<string> { + return this.mainFrame().title(); + } + + override async close( + options: {runBeforeUnload?: boolean} = {runBeforeUnload: undefined} + ): Promise<void> { + const connection = this.#client.connection(); + assert( + connection, + 'Protocol error: Connection closed. Most likely the page has been closed.' + ); + const runBeforeUnload = !!options.runBeforeUnload; + if (runBeforeUnload) { + await this.#client.send('Page.close'); + } else { + await connection.send('Target.closeTarget', { + targetId: this.#target._targetId, + }); + await this.#target._isClosedPromise; + } + } + + override isClosed(): boolean { + return this.#closed; + } + + override get mouse(): Mouse { + return this.#mouse; + } + + override click( + selector: string, + options: Readonly<ClickOptions> = {} + ): Promise<void> { + return this.mainFrame().click(selector, options); + } + + override focus(selector: string): Promise<void> { + return this.mainFrame().focus(selector); + } + + override hover(selector: string): Promise<void> { + return this.mainFrame().hover(selector); + } + + override select(selector: string, ...values: string[]): Promise<string[]> { + return this.mainFrame().select(selector, ...values); + } + + override tap(selector: string): Promise<void> { + return this.mainFrame().tap(selector); + } + + override type( + selector: string, + text: string, + options?: {delay: number} + ): Promise<void> { + return this.mainFrame().type(selector, text, options); + } + + override waitForTimeout(milliseconds: number): Promise<void> { + return this.mainFrame().waitForTimeout(milliseconds); + } + + override async waitForSelector<Selector extends string>( + selector: Selector, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + return await this.mainFrame().waitForSelector(selector, options); + } + + override waitForXPath( + xpath: string, + options: WaitForSelectorOptions = {} + ): Promise<ElementHandle<Node> | null> { + return this.mainFrame().waitForXPath(xpath, options); + } + + override waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + options: FrameWaitForFunctionOptions = {}, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.mainFrame().waitForFunction(pageFunction, options, ...args); + } + + /** + * This method is typically coupled with an action that triggers a device + * request from an api such as WebBluetooth. + * + * :::caution + * + * This must be called before the device request is made. It will not return a + * currently active device prompt. + * + * ::: + * + * @example + * + * ```ts + * const [devicePrompt] = Promise.all([ + * page.waitForDevicePrompt(), + * page.click('#connect-bluetooth'), + * ]); + * await devicePrompt.select( + * await devicePrompt.waitForDevice(({name}) => name.includes('My Device')) + * ); + * ``` + */ + override waitForDevicePrompt( + options: WaitTimeoutOptions = {} + ): Promise<DeviceRequestPrompt> { + return this.mainFrame().waitForDevicePrompt(options); + } +} + +const supportedMetrics = new Set<string>([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', +]); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts new file mode 100644 index 0000000000..941f762c82 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PierceQueryHandler.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type PuppeteerUtil from '../injected/injected.js'; + +import {QueryHandler} from './QueryHandler.js'; + +/** + * @internal + */ +export class PierceQueryHandler extends QueryHandler { + static override querySelector = ( + element: Node, + selector: string, + {pierceQuerySelector}: PuppeteerUtil + ): Node | null => { + return pierceQuerySelector(element, selector); + }; + static override querySelectorAll = ( + element: Node, + selector: string, + {pierceQuerySelectorAll}: PuppeteerUtil + ): Iterable<Node> => { + return pierceQuerySelectorAll(element, selector); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PredefinedNetworkConditions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PredefinedNetworkConditions.ts new file mode 100644 index 0000000000..69ee56ed68 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PredefinedNetworkConditions.ts @@ -0,0 +1,59 @@ +/** + * 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 {NetworkConditions} from './NetworkManager.js'; + +/** + * A list of network conditions to be used with + * {@link Page.emulateNetworkConditions}. + * + * @example + * + * ```ts + * import {PredefinedNetworkConditions} from 'puppeteer'; + * const slow3G = PredefinedNetworkConditions['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(); + * })(); + * ``` + * + * @public + */ +export const PredefinedNetworkConditions = Object.freeze({ + 'Slow 3G': { + download: ((500 * 1000) / 8) * 0.8, + upload: ((500 * 1000) / 8) * 0.8, + latency: 400 * 5, + } as NetworkConditions, + 'Fast 3G': { + download: ((1.6 * 1000 * 1000) / 8) * 0.9, + upload: ((750 * 1000) / 8) * 0.9, + latency: 150 * 3.75, + } as NetworkConditions, +}); + +/** + * @deprecated Import {@link PredefinedNetworkConditions}. + * + * @public + */ +export const networkConditions = PredefinedNetworkConditions; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts new file mode 100644 index 0000000000..58a62fad3e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Product.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ + +/** + * Supported products. + * @public + */ +export type Product = 'chrome' | 'firefox'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts new file mode 100644 index 0000000000..068ec173f0 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Puppeteer.ts @@ -0,0 +1,147 @@ +/** + * 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 {Browser} from '../api/Browser.js'; + +import { + BrowserConnectOptions, + _connectToCDPBrowser, +} from './BrowserConnector.js'; +import {ConnectionTransport} from './ConnectionTransport.js'; +import {CustomQueryHandler, customQueryHandlers} from './CustomQueryHandler.js'; + +/** + * Settings that are common to the Puppeteer class, regardless of environment. + * + * @internal + */ +export interface CommonPuppeteerSettings { + isPuppeteerCore: boolean; +} +/** + * @public + */ +export interface ConnectOptions extends BrowserConnectOptions { + browserWSEndpoint?: string; + browserURL?: string; + transport?: ConnectionTransport; + /** + * Headers to use for the web socket connection. + * @remarks + * Only works in the Node.js environment. + */ + headers?: Record<string, string>; +} + +/** + * The main Puppeteer class. + * + * IMPORTANT: if you are using Puppeteer in a Node environment, you will get an + * instance of {@link PuppeteerNode} when you import or require `puppeteer`. + * That class extends `Puppeteer`, so has all the methods documented below as + * well as all that are defined on {@link PuppeteerNode}. + * + * @public + */ +export class Puppeteer { + /** + * Operations for {@link CustomQueryHandler | custom query handlers}. See + * {@link CustomQueryHandlerRegistry}. + * + * @internal + */ + static customQueryHandlers = customQueryHandlers; + + /** + * Registers a {@link CustomQueryHandler | custom query handler}. + * + * @remarks + * After registration, the handler can be used everywhere where a selector is + * expected by prepending the selection string with `<name>/`. The name is only + * allowed to consist of lower- and upper case latin letters. + * + * @example + * + * ``` + * puppeteer.registerCustomQueryHandler('text', { … }); + * const aHandle = await page.$('text/…'); + * ``` + * + * @param name - The name that the custom query handler will be registered + * under. + * @param queryHandler - The {@link CustomQueryHandler | custom query handler} + * to register. + * + * @public + */ + static registerCustomQueryHandler( + name: string, + queryHandler: CustomQueryHandler + ): void { + return this.customQueryHandlers.register(name, queryHandler); + } + + /** + * Unregisters a custom query handler for a given name. + */ + static unregisterCustomQueryHandler(name: string): void { + return this.customQueryHandlers.unregister(name); + } + + /** + * Gets the names of all custom query handlers. + */ + static customQueryHandlerNames(): string[] { + return this.customQueryHandlers.names(); + } + + /** + * Unregisters all custom query handlers. + */ + static clearCustomQueryHandlers(): void { + return this.customQueryHandlers.clear(); + } + + /** + * @internal + */ + _isPuppeteerCore: boolean; + /** + * @internal + */ + protected _changedProduct = false; + + /** + * @internal + */ + constructor(settings: CommonPuppeteerSettings) { + this._isPuppeteerCore = settings.isPuppeteerCore; + + this.connect = this.connect.bind(this); + } + + /** + * This method attaches Puppeteer to an existing browser instance. + * + * @remarks + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + connect(options: ConnectOptions): Promise<Browser> { + return _connectToCDPBrowser(options); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/PuppeteerViewport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/PuppeteerViewport.ts new file mode 100644 index 0000000000..953b327c01 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/PuppeteerViewport.ts @@ -0,0 +1,55 @@ +/** + * 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. + */ +/** + * + * Sets the viewport of the page. + * @public + */ +export interface Viewport { + /** + * The page width in pixels. + */ + width: number; + /** + * The page height in pixels. + */ + height: number; + /** + * Specify device scale factor. + * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio | devicePixelRatio} for more info. + * + * @remarks + * Setting this value to `0` will set the deviceScaleFactor to the system default. + * + * @defaultValue `1` + */ + deviceScaleFactor?: number; + /** + * Whether the `meta viewport` tag is taken into account. + * @defaultValue `false` + */ + isMobile?: boolean; + /** + * Specifies if the viewport is in landscape mode. + * @defaultValue `false` + */ + isLandscape?: boolean; + /** + * Specify if the viewport supports touch events. + * @defaultValue `false` + */ + hasTouch?: boolean; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts new file mode 100644 index 0000000000..975bee4530 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/QueryHandler.ts @@ -0,0 +1,226 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ElementHandle} from '../api/ElementHandle.js'; +import type PuppeteerUtil from '../injected/injected.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {interpolateFunction, stringifyFunction} from '../util/Function.js'; + +import type {Frame} from './Frame.js'; +import {transposeIterableHandle} from './HandleIterator.js'; +import type {WaitForSelectorOptions} from './IsolatedWorld.js'; +import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; +import {LazyArg} from './LazyArg.js'; +import type {Awaitable, AwaitableIterable} from './types.js'; + +/** + * @internal + */ +export type QuerySelectorAll = ( + node: Node, + selector: string, + PuppeteerUtil: PuppeteerUtil +) => AwaitableIterable<Node>; + +/** + * @internal + */ +export type QuerySelector = ( + node: Node, + selector: string, + PuppeteerUtil: PuppeteerUtil +) => Awaitable<Node | null>; + +/** + * @internal + */ +export class QueryHandler { + // Either one of these may be implemented, but at least one must be. + static querySelectorAll?: QuerySelectorAll; + static querySelector?: QuerySelector; + + static get _querySelector(): QuerySelector { + if (this.querySelector) { + return this.querySelector; + } + if (!this.querySelectorAll) { + throw new Error('Cannot create default `querySelector`.'); + } + + return (this.querySelector = interpolateFunction( + async (node, selector, PuppeteerUtil) => { + const querySelectorAll: QuerySelectorAll = + PLACEHOLDER('querySelectorAll'); + const results = querySelectorAll(node, selector, PuppeteerUtil); + for await (const result of results) { + return result; + } + return null; + }, + { + querySelectorAll: stringifyFunction(this.querySelectorAll), + } + )); + } + + static get _querySelectorAll(): QuerySelectorAll { + if (this.querySelectorAll) { + return this.querySelectorAll; + } + if (!this.querySelector) { + throw new Error('Cannot create default `querySelectorAll`.'); + } + + return (this.querySelectorAll = interpolateFunction( + async function* (node, selector, PuppeteerUtil) { + const querySelector: QuerySelector = PLACEHOLDER('querySelector'); + const result = await querySelector(node, selector, PuppeteerUtil); + if (result) { + yield result; + } + }, + { + querySelector: stringifyFunction(this.querySelector), + } + )); + } + + /** + * Queries for multiple nodes given a selector and {@link ElementHandle}. + * + * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll | Document.querySelectorAll()}. + */ + static async *queryAll( + element: ElementHandle<Node>, + selector: string + ): AwaitableIterable<ElementHandle<Node>> { + const world = element.executionContext()._world; + assert(world); + const handle = await element.evaluateHandle( + this._querySelectorAll, + selector, + LazyArg.create(context => { + return context.puppeteerUtil; + }) + ); + yield* transposeIterableHandle(handle); + } + + /** + * Queries for a single node given a selector and {@link ElementHandle}. + * + * Akin to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector}. + */ + static async queryOne( + element: ElementHandle<Node>, + selector: string + ): Promise<ElementHandle<Node> | null> { + const world = element.executionContext()._world; + assert(world); + const result = await element.evaluateHandle( + this._querySelector, + selector, + LazyArg.create(context => { + return context.puppeteerUtil; + }) + ); + if (!(result instanceof ElementHandle)) { + await result.dispose(); + return null; + } + return result; + } + + /** + * Waits until a single node appears for a given selector and + * {@link ElementHandle}. + * + * This will always query the handle in the Puppeteer world and migrate the + * result to the main world. + */ + static async waitFor( + elementOrFrame: ElementHandle<Node> | Frame, + selector: string, + options: WaitForSelectorOptions + ): Promise<ElementHandle<Node> | null> { + let frame: Frame; + let element: ElementHandle<Node> | undefined; + if (!(elementOrFrame instanceof ElementHandle)) { + frame = elementOrFrame; + } else { + frame = elementOrFrame.frame; + element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); + } + + const {visible = false, hidden = false, timeout, signal} = options; + + try { + signal?.throwIfAborted(); + + const handle = await frame.worlds[PUPPETEER_WORLD].waitForFunction( + async (PuppeteerUtil, query, selector, root, visible) => { + const querySelector = PuppeteerUtil.createFunction( + query + ) as QuerySelector; + const node = await querySelector( + root ?? document, + selector, + PuppeteerUtil + ); + return PuppeteerUtil.checkVisibility(node, visible); + }, + { + polling: visible || hidden ? 'raf' : 'mutation', + root: element, + timeout, + signal, + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + stringifyFunction(this._querySelector), + selector, + element, + visible ? true : hidden ? false : undefined + ); + + if (signal?.aborted) { + await handle.dispose(); + throw signal.reason; + } + + if (!(handle instanceof ElementHandle)) { + await handle.dispose(); + return null; + } + return frame.worlds[MAIN_WORLD].transferHandle(handle); + } catch (error) { + if (!isErrorLike(error)) { + throw error; + } + if (error.name === 'AbortError') { + throw error; + } + error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`; + throw error; + } finally { + if (element) { + await element.dispose(); + } + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts new file mode 100644 index 0000000000..cb0c039530 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/ScriptInjector.ts @@ -0,0 +1,49 @@ +import {source as injectedSource} from '../generated/injected.js'; + +class ScriptInjector { + #updated = false; + #amendments = new Set<string>(); + + // Appends a statement of the form `(PuppeteerUtil) => {...}`. + append(statement: string): void { + this.#update(() => { + this.#amendments.add(statement); + }); + } + + pop(statement: string): void { + this.#update(() => { + this.#amendments.delete(statement); + }); + } + + inject(inject: (script: string) => void, force = false) { + if (this.#updated || force) { + inject(this.#get()); + } + this.#updated = false; + } + + #update(callback: () => void): void { + callback(); + this.#updated = true; + } + + #get(): string { + return `(() => { + const module = {}; + ${injectedSource} + ${[...this.#amendments] + .map(statement => { + return `(${statement})(module.exports.default);`; + }) + .join('')} + return module.exports.default; + })()`; + } +} + +/** + * @internal + */ +export const scriptInjector = new ScriptInjector(); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts new file mode 100644 index 0000000000..4dbb71046e --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/SecurityDetails.ts @@ -0,0 +1,88 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +/** + * The SecurityDetails class represents the security details of a + * response that was received over a secure connection. + * + * @public + */ +export class SecurityDetails { + #subjectName: string; + #issuer: string; + #validFrom: number; + #validTo: number; + #protocol: string; + #sanList: string[]; + + /** + * @internal + */ + constructor(securityPayload: Protocol.Network.SecurityDetails) { + this.#subjectName = securityPayload.subjectName; + this.#issuer = securityPayload.issuer; + this.#validFrom = securityPayload.validFrom; + this.#validTo = securityPayload.validTo; + this.#protocol = securityPayload.protocol; + this.#sanList = securityPayload.sanList; + } + + /** + * The name of the issuer of the certificate. + */ + issuer(): string { + return this.#issuer; + } + + /** + * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the start of the certificate's validity. + */ + validFrom(): number { + return this.#validFrom; + } + + /** + * {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the end of the certificate's validity. + */ + validTo(): number { + return this.#validTo; + } + + /** + * The security protocol being used, e.g. "TLS 1.2". + */ + protocol(): string { + return this.#protocol; + } + + /** + * The name of the subject to which the certificate was issued. + */ + subjectName(): string { + return this.#subjectName; + } + + /** + * The list of {@link https://en.wikipedia.org/wiki/Subject_Alternative_Name | subject alternative names (SANs)} of the certificate. + */ + subjectAlternativeNames(): string[] { + return this.#sanList; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Target.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Target.ts new file mode 100644 index 0000000000..fd9b5f9f27 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Target.ts @@ -0,0 +1,285 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import type {Browser, IsPageTargetCallback} from '../api/Browser.js'; +import type {BrowserContext} from '../api/BrowserContext.js'; +import {Page, PageEmittedEvents} from '../api/Page.js'; + +import {CDPSession} from './Connection.js'; +import {CDPPage} from './Page.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {TargetManager} from './TargetManager.js'; +import {TaskQueue} from './TaskQueue.js'; +import {WebWorker} from './WebWorker.js'; + +/** + * Target represents a + * {@link https://chromedevtools.github.io/devtools-protocol/tot/Target/ | CDP target}. + * In CDP a target is something that can be debugged such a frame, a page or a + * worker. + * + * @public + */ +export class Target { + #browserContext: BrowserContext; + #session?: CDPSession; + #targetInfo: Protocol.Target.TargetInfo; + #sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CDPSession>; + #ignoreHTTPSErrors: boolean; + #defaultViewport?: Viewport; + #pagePromise?: Promise<Page>; + #workerPromise?: Promise<WebWorker>; + #screenshotTaskQueue: TaskQueue; + + /** + * @internal + */ + _initializedPromise: Promise<boolean>; + /** + * @internal + */ + _initializedCallback!: (x: boolean) => void; + /** + * @internal + */ + _isClosedPromise: Promise<void>; + /** + * @internal + */ + _closedCallback!: () => void; + /** + * @internal + */ + _isInitialized: boolean; + /** + * @internal + */ + _targetId: string; + /** + * @internal + */ + _isPageTargetCallback: IsPageTargetCallback; + + #targetManager: TargetManager; + + /** + * @internal + */ + constructor( + targetInfo: Protocol.Target.TargetInfo, + session: CDPSession | undefined, + browserContext: BrowserContext, + targetManager: TargetManager, + sessionFactory: (isAutoAttachEmulated: boolean) => Promise<CDPSession>, + ignoreHTTPSErrors: boolean, + defaultViewport: Viewport | null, + screenshotTaskQueue: TaskQueue, + isPageTargetCallback: IsPageTargetCallback + ) { + this.#session = session; + this.#targetManager = targetManager; + this.#targetInfo = targetInfo; + this.#browserContext = browserContext; + this._targetId = targetInfo.targetId; + this.#sessionFactory = sessionFactory; + this.#ignoreHTTPSErrors = ignoreHTTPSErrors; + this.#defaultViewport = defaultViewport ?? undefined; + this.#screenshotTaskQueue = screenshotTaskQueue; + this._isPageTargetCallback = isPageTargetCallback; + this._initializedPromise = new Promise<boolean>(fulfill => { + return (this._initializedCallback = fulfill); + }).then(async success => { + if (!success) { + return false; + } + const opener = this.opener(); + if (!opener || !opener.#pagePromise || this.type() !== 'page') { + return true; + } + const openerPage = await opener.#pagePromise; + if (!openerPage.listenerCount(PageEmittedEvents.Popup)) { + return true; + } + const popupPage = await this.page(); + openerPage.emit(PageEmittedEvents.Popup, popupPage); + return true; + }); + this._isClosedPromise = new Promise<void>(fulfill => { + return (this._closedCallback = fulfill); + }); + this._isInitialized = + !this._isPageTargetCallback(this.#targetInfo) || + this.#targetInfo.url !== ''; + if (this._isInitialized) { + this._initializedCallback(true); + } + } + + /** + * @internal + */ + _session(): CDPSession | undefined { + return this.#session; + } + + /** + * Creates a Chrome Devtools Protocol session attached to the target. + */ + createCDPSession(): Promise<CDPSession> { + return this.#sessionFactory(false); + } + + /** + * @internal + */ + _targetManager(): TargetManager { + return this.#targetManager; + } + + /** + * @internal + */ + _getTargetInfo(): Protocol.Target.TargetInfo { + return this.#targetInfo; + } + + /** + * If the target is not of type `"page"` or `"background_page"`, returns `null`. + */ + async page(): Promise<Page | null> { + if (this._isPageTargetCallback(this.#targetInfo) && !this.#pagePromise) { + this.#pagePromise = ( + this.#session + ? Promise.resolve(this.#session) + : this.#sessionFactory(true) + ).then(client => { + return CDPPage._create( + client, + this, + this.#ignoreHTTPSErrors, + this.#defaultViewport ?? null, + this.#screenshotTaskQueue + ); + }); + } + return (await this.#pagePromise) ?? null; + } + + /** + * If the target is not of type `"service_worker"` or `"shared_worker"`, returns `null`. + */ + async worker(): Promise<WebWorker | null> { + if ( + this.#targetInfo.type !== 'service_worker' && + this.#targetInfo.type !== 'shared_worker' + ) { + return null; + } + if (!this.#workerPromise) { + // TODO(einbinder): Make workers send their console logs. + this.#workerPromise = ( + this.#session + ? Promise.resolve(this.#session) + : this.#sessionFactory(false) + ).then(client => { + return new WebWorker( + client, + this.#targetInfo.url, + () => {} /* consoleAPICalled */, + () => {} /* exceptionThrown */ + ); + }); + } + return this.#workerPromise; + } + + url(): string { + return this.#targetInfo.url; + } + + /** + * Identifies what kind of target this is. + * + * @remarks + * + * See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages. + */ + type(): + | 'page' + | 'background_page' + | 'service_worker' + | 'shared_worker' + | 'other' + | 'browser' + | 'webview' { + const type = this.#targetInfo.type; + if ( + type === 'page' || + type === 'background_page' || + type === 'service_worker' || + type === 'shared_worker' || + type === 'browser' || + type === 'webview' + ) { + return type; + } + return 'other'; + } + + /** + * Get the browser the target belongs to. + */ + browser(): Browser { + return this.#browserContext.browser(); + } + + /** + * Get the browser context the target belongs to. + */ + browserContext(): BrowserContext { + return this.#browserContext; + } + + /** + * Get the target that opened this target. Top-level targets return `null`. + */ + opener(): Target | undefined { + const {openerId} = this.#targetInfo; + if (!openerId) { + return; + } + return this.browser()._targets.get(openerId); + } + + /** + * @internal + */ + _targetInfoChanged(targetInfo: Protocol.Target.TargetInfo): void { + this.#targetInfo = targetInfo; + + if ( + !this._isInitialized && + (!this._isPageTargetCallback(this.#targetInfo) || + this.#targetInfo.url !== '') + ) { + this._isInitialized = true; + this._initializedCallback(true); + return; + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TargetManager.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TargetManager.ts new file mode 100644 index 0000000000..4f69b72ba9 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TargetManager.ts @@ -0,0 +1,72 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; + +import {CDPSession} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {Target} from './Target.js'; + +/** + * @internal + */ +export type TargetFactory = ( + targetInfo: Protocol.Target.TargetInfo, + session?: CDPSession +) => Target; + +/** + * @internal + */ +export type TargetInterceptor = ( + createdTarget: Target, + parentTarget: Target | null +) => void; + +/** + * TargetManager encapsulates all interactions with CDP targets and is + * responsible for coordinating the configuration of targets with the rest of + * Puppeteer. Code outside of this class should not subscribe `Target.*` events + * and only use the TargetManager events. + * + * There are two implementations: one for Chrome that uses CDP's auto-attach + * mechanism and one for Firefox because Firefox does not support auto-attach. + * + * @internal + */ +export interface TargetManager extends EventEmitter { + getAvailableTargets(): Map<string, Target>; + initialize(): Promise<void>; + dispose(): void; + addTargetInterceptor( + session: CDPSession, + interceptor: TargetInterceptor + ): void; + removeTargetInterceptor( + session: CDPSession, + interceptor: TargetInterceptor + ): void; +} + +/** + * @internal + */ +export const enum TargetManagerEmittedEvents { + TargetDiscovered = 'targetDiscovered', + TargetAvailable = 'targetAvailable', + TargetGone = 'targetGone', + TargetChanged = 'targetChanged', +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts new file mode 100644 index 0000000000..97cfe7c769 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TaskQueue.ts @@ -0,0 +1,39 @@ +/** + * 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. + */ + +/** + * @internal + */ +export class TaskQueue { + #chain: Promise<void>; + + constructor() { + this.#chain = Promise.resolve(); + } + + postTask<T>(task: () => Promise<T>): Promise<T> { + const result = this.#chain.then(task); + this.#chain = result.then( + () => { + return undefined; + }, + () => { + return undefined; + } + ); + return result; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts new file mode 100644 index 0000000000..02ecdddca8 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TextQueryHandler.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {QueryHandler, QuerySelectorAll} from './QueryHandler.js'; + +/** + * @internal + */ +export class TextQueryHandler extends QueryHandler { + static override querySelectorAll: QuerySelectorAll = ( + element, + selector, + {textQuerySelectorAll} + ) => { + return textQuerySelectorAll(element, selector); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts new file mode 100644 index 0000000000..97acc70147 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/TimeoutSettings.ts @@ -0,0 +1,55 @@ +/** + * 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. + */ + +const DEFAULT_TIMEOUT = 30000; + +/** + * @internal + */ +export class TimeoutSettings { + #defaultTimeout: number | null; + #defaultNavigationTimeout: number | null; + + constructor() { + this.#defaultTimeout = null; + this.#defaultNavigationTimeout = null; + } + + setDefaultTimeout(timeout: number): void { + this.#defaultTimeout = timeout; + } + + setDefaultNavigationTimeout(timeout: number): void { + this.#defaultNavigationTimeout = timeout; + } + + navigationTimeout(): number { + if (this.#defaultNavigationTimeout !== null) { + return this.#defaultNavigationTimeout; + } + if (this.#defaultTimeout !== null) { + return this.#defaultTimeout; + } + return DEFAULT_TIMEOUT; + } + + timeout(): number { + if (this.#defaultTimeout !== null) { + return this.#defaultTimeout; + } + return DEFAULT_TIMEOUT; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/Tracing.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/Tracing.ts new file mode 100644 index 0000000000..e76f3a15d4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/Tracing.ts @@ -0,0 +1,144 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import {CDPSession} from './Connection.js'; +import {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js'; + +/** + * @public + */ +export interface TracingOptions { + path?: string; + screenshots?: boolean; + categories?: string[]; +} + +/** + * The Tracing class exposes the tracing audit interface. + * @remarks + * You can use `tracing.start` and `tracing.stop` to create a trace file + * which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}. + * + * @example + * + * ```ts + * await page.tracing.start({path: 'trace.json'}); + * await page.goto('https://www.google.com'); + * await page.tracing.stop(); + * ``` + * + * @public + */ +export class Tracing { + #client: CDPSession; + #recording = false; + #path?: string; + + /** + * @internal + */ + constructor(client: CDPSession) { + this.#client = client; + } + + /** + * Starts a trace for the current page. + * @remarks + * Only one trace can be active at a time per browser. + * + * @param options - Optional `TracingOptions`. + */ + async start(options: TracingOptions = {}): Promise<void> { + assert( + !this.#recording, + 'Cannot start recording trace while already recording trace.' + ); + + const defaultCategories = [ + '-*', + 'devtools.timeline', + 'v8.execute', + 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', + 'toplevel', + 'blink.console', + 'blink.user_timing', + 'latencyInfo', + 'disabled-by-default-devtools.timeline.stack', + 'disabled-by-default-v8.cpu_profiler', + ]; + const {path, screenshots = false, categories = defaultCategories} = options; + + if (screenshots) { + categories.push('disabled-by-default-devtools.screenshot'); + } + + const excludedCategories = categories + .filter(cat => { + return cat.startsWith('-'); + }) + .map(cat => { + return cat.slice(1); + }); + const includedCategories = categories.filter(cat => { + return !cat.startsWith('-'); + }); + + this.#path = path; + this.#recording = true; + await this.#client.send('Tracing.start', { + transferMode: 'ReturnAsStream', + traceConfig: { + excludedCategories, + includedCategories, + }, + }); + } + + /** + * Stops a trace started with the `start` method. + * @returns Promise which resolves to buffer with trace data. + */ + async stop(): Promise<Buffer | undefined> { + let resolve: (value: Buffer | undefined) => void; + let reject: (err: Error) => void; + const contentPromise = new Promise<Buffer | undefined>((x, y) => { + resolve = x; + reject = y; + }); + this.#client.once('Tracing.tracingComplete', async event => { + try { + const readable = await getReadableFromProtocolStream( + this.#client, + event.stream + ); + const buffer = await getReadableAsBuffer(readable, this.#path); + resolve(buffer ?? undefined); + } catch (error) { + if (isErrorLike(error)) { + reject(error); + } else { + reject(new Error(`Unknown error: ${error}`)); + } + } + }); + await this.#client.send('Tracing.end'); + this.#recording = false; + return contentPromise; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts new file mode 100644 index 0000000000..f6a042e5ce --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/USKeyboardLayout.ts @@ -0,0 +1,681 @@ +/** + * 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. + */ + +/** + * @internal + */ +export interface KeyDefinition { + keyCode?: number; + shiftKeyCode?: number; + key?: string; + shiftKey?: string; + code?: string; + text?: string; + shiftText?: string; + location?: number; +} + +/** + * All the valid keys that can be passed to functions that take user input, such + * as {@link Keyboard.press | keyboard.press } + * + * @public + */ +export type KeyInput = + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | 'Power' + | 'Eject' + | 'Abort' + | 'Help' + | 'Backspace' + | 'Tab' + | 'Numpad5' + | 'NumpadEnter' + | 'Enter' + | '\r' + | '\n' + | 'ShiftLeft' + | 'ShiftRight' + | 'ControlLeft' + | 'ControlRight' + | 'AltLeft' + | 'AltRight' + | 'Pause' + | 'CapsLock' + | 'Escape' + | 'Convert' + | 'NonConvert' + | 'Space' + | 'Numpad9' + | 'PageUp' + | 'Numpad3' + | 'PageDown' + | 'End' + | 'Numpad1' + | 'Home' + | 'Numpad7' + | 'ArrowLeft' + | 'Numpad4' + | 'Numpad8' + | 'ArrowUp' + | 'ArrowRight' + | 'Numpad6' + | 'Numpad2' + | 'ArrowDown' + | 'Select' + | 'Open' + | 'PrintScreen' + | 'Insert' + | 'Numpad0' + | 'Delete' + | 'NumpadDecimal' + | 'Digit0' + | 'Digit1' + | 'Digit2' + | 'Digit3' + | 'Digit4' + | 'Digit5' + | 'Digit6' + | 'Digit7' + | 'Digit8' + | 'Digit9' + | 'KeyA' + | 'KeyB' + | 'KeyC' + | 'KeyD' + | 'KeyE' + | 'KeyF' + | 'KeyG' + | 'KeyH' + | 'KeyI' + | 'KeyJ' + | 'KeyK' + | 'KeyL' + | 'KeyM' + | 'KeyN' + | 'KeyO' + | 'KeyP' + | 'KeyQ' + | 'KeyR' + | 'KeyS' + | 'KeyT' + | 'KeyU' + | 'KeyV' + | 'KeyW' + | 'KeyX' + | 'KeyY' + | 'KeyZ' + | 'MetaLeft' + | 'MetaRight' + | 'ContextMenu' + | 'NumpadMultiply' + | 'NumpadAdd' + | 'NumpadSubtract' + | 'NumpadDivide' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12' + | 'F13' + | 'F14' + | 'F15' + | 'F16' + | 'F17' + | 'F18' + | 'F19' + | 'F20' + | 'F21' + | 'F22' + | 'F23' + | 'F24' + | 'NumLock' + | 'ScrollLock' + | 'AudioVolumeMute' + | 'AudioVolumeDown' + | 'AudioVolumeUp' + | 'MediaTrackNext' + | 'MediaTrackPrevious' + | 'MediaStop' + | 'MediaPlayPause' + | 'Semicolon' + | 'Equal' + | 'NumpadEqual' + | 'Comma' + | 'Minus' + | 'Period' + | 'Slash' + | 'Backquote' + | 'BracketLeft' + | 'Backslash' + | 'BracketRight' + | 'Quote' + | 'AltGraph' + | 'Props' + | 'Cancel' + | 'Clear' + | 'Shift' + | 'Control' + | 'Alt' + | 'Accept' + | 'ModeChange' + | ' ' + | 'Print' + | 'Execute' + | '\u0000' + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'k' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z' + | 'Meta' + | '*' + | '+' + | '-' + | '/' + | ';' + | '=' + | ',' + | '.' + | '`' + | '[' + | '\\' + | ']' + | "'" + | 'Attn' + | 'CrSel' + | 'ExSel' + | 'EraseEof' + | 'Play' + | 'ZoomOut' + | ')' + | '!' + | '@' + | '#' + | '$' + | '%' + | '^' + | '&' + | '(' + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z' + | ':' + | '<' + | '_' + | '>' + | '?' + | '~' + | '{' + | '|' + | '}' + | '"' + | 'SoftLeft' + | 'SoftRight' + | 'Camera' + | 'Call' + | 'EndCall' + | 'VolumeDown' + | 'VolumeUp'; + +/** + * @internal + */ +export const _keyDefinitions: Readonly<Record<KeyInput, KeyDefinition>> = { + '0': {keyCode: 48, key: '0', code: 'Digit0'}, + '1': {keyCode: 49, key: '1', code: 'Digit1'}, + '2': {keyCode: 50, key: '2', code: 'Digit2'}, + '3': {keyCode: 51, key: '3', code: 'Digit3'}, + '4': {keyCode: 52, key: '4', code: 'Digit4'}, + '5': {keyCode: 53, key: '5', code: 'Digit5'}, + '6': {keyCode: 54, key: '6', code: 'Digit6'}, + '7': {keyCode: 55, key: '7', code: 'Digit7'}, + '8': {keyCode: 56, key: '8', code: 'Digit8'}, + '9': {keyCode: 57, key: '9', code: 'Digit9'}, + Power: {key: 'Power', code: 'Power'}, + Eject: {key: 'Eject', code: 'Eject'}, + Abort: {keyCode: 3, code: 'Abort', key: 'Cancel'}, + Help: {keyCode: 6, code: 'Help', key: 'Help'}, + Backspace: {keyCode: 8, code: 'Backspace', key: 'Backspace'}, + Tab: {keyCode: 9, code: 'Tab', key: 'Tab'}, + Numpad5: { + keyCode: 12, + shiftKeyCode: 101, + key: 'Clear', + code: 'Numpad5', + shiftKey: '5', + location: 3, + }, + NumpadEnter: { + keyCode: 13, + code: 'NumpadEnter', + key: 'Enter', + text: '\r', + location: 3, + }, + Enter: {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'}, + '\r': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'}, + '\n': {keyCode: 13, code: 'Enter', key: 'Enter', text: '\r'}, + ShiftLeft: {keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1}, + ShiftRight: {keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2}, + ControlLeft: { + keyCode: 17, + code: 'ControlLeft', + key: 'Control', + location: 1, + }, + ControlRight: { + keyCode: 17, + code: 'ControlRight', + key: 'Control', + location: 2, + }, + AltLeft: {keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1}, + AltRight: {keyCode: 18, code: 'AltRight', key: 'Alt', location: 2}, + Pause: {keyCode: 19, code: 'Pause', key: 'Pause'}, + CapsLock: {keyCode: 20, code: 'CapsLock', key: 'CapsLock'}, + Escape: {keyCode: 27, code: 'Escape', key: 'Escape'}, + Convert: {keyCode: 28, code: 'Convert', key: 'Convert'}, + NonConvert: {keyCode: 29, code: 'NonConvert', key: 'NonConvert'}, + Space: {keyCode: 32, code: 'Space', key: ' '}, + Numpad9: { + keyCode: 33, + shiftKeyCode: 105, + key: 'PageUp', + code: 'Numpad9', + shiftKey: '9', + location: 3, + }, + PageUp: {keyCode: 33, code: 'PageUp', key: 'PageUp'}, + Numpad3: { + keyCode: 34, + shiftKeyCode: 99, + key: 'PageDown', + code: 'Numpad3', + shiftKey: '3', + location: 3, + }, + PageDown: {keyCode: 34, code: 'PageDown', key: 'PageDown'}, + End: {keyCode: 35, code: 'End', key: 'End'}, + Numpad1: { + keyCode: 35, + shiftKeyCode: 97, + key: 'End', + code: 'Numpad1', + shiftKey: '1', + location: 3, + }, + Home: {keyCode: 36, code: 'Home', key: 'Home'}, + Numpad7: { + keyCode: 36, + shiftKeyCode: 103, + key: 'Home', + code: 'Numpad7', + shiftKey: '7', + location: 3, + }, + ArrowLeft: {keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft'}, + Numpad4: { + keyCode: 37, + shiftKeyCode: 100, + key: 'ArrowLeft', + code: 'Numpad4', + shiftKey: '4', + location: 3, + }, + Numpad8: { + keyCode: 38, + shiftKeyCode: 104, + key: 'ArrowUp', + code: 'Numpad8', + shiftKey: '8', + location: 3, + }, + ArrowUp: {keyCode: 38, code: 'ArrowUp', key: 'ArrowUp'}, + ArrowRight: {keyCode: 39, code: 'ArrowRight', key: 'ArrowRight'}, + Numpad6: { + keyCode: 39, + shiftKeyCode: 102, + key: 'ArrowRight', + code: 'Numpad6', + shiftKey: '6', + location: 3, + }, + Numpad2: { + keyCode: 40, + shiftKeyCode: 98, + key: 'ArrowDown', + code: 'Numpad2', + shiftKey: '2', + location: 3, + }, + ArrowDown: {keyCode: 40, code: 'ArrowDown', key: 'ArrowDown'}, + Select: {keyCode: 41, code: 'Select', key: 'Select'}, + Open: {keyCode: 43, code: 'Open', key: 'Execute'}, + PrintScreen: {keyCode: 44, code: 'PrintScreen', key: 'PrintScreen'}, + Insert: {keyCode: 45, code: 'Insert', key: 'Insert'}, + Numpad0: { + keyCode: 45, + shiftKeyCode: 96, + key: 'Insert', + code: 'Numpad0', + shiftKey: '0', + location: 3, + }, + Delete: {keyCode: 46, code: 'Delete', key: 'Delete'}, + NumpadDecimal: { + keyCode: 46, + shiftKeyCode: 110, + code: 'NumpadDecimal', + key: '\u0000', + shiftKey: '.', + location: 3, + }, + Digit0: {keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0'}, + Digit1: {keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1'}, + Digit2: {keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2'}, + Digit3: {keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3'}, + Digit4: {keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4'}, + Digit5: {keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5'}, + Digit6: {keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6'}, + Digit7: {keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7'}, + Digit8: {keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8'}, + Digit9: {keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9'}, + KeyA: {keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a'}, + KeyB: {keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b'}, + KeyC: {keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c'}, + KeyD: {keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd'}, + KeyE: {keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e'}, + KeyF: {keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f'}, + KeyG: {keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g'}, + KeyH: {keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h'}, + KeyI: {keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i'}, + KeyJ: {keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j'}, + KeyK: {keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k'}, + KeyL: {keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l'}, + KeyM: {keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm'}, + KeyN: {keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n'}, + KeyO: {keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o'}, + KeyP: {keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p'}, + KeyQ: {keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q'}, + KeyR: {keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r'}, + KeyS: {keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's'}, + KeyT: {keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't'}, + KeyU: {keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u'}, + KeyV: {keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v'}, + KeyW: {keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w'}, + KeyX: {keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x'}, + KeyY: {keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y'}, + KeyZ: {keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z'}, + MetaLeft: {keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1}, + MetaRight: {keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2}, + ContextMenu: {keyCode: 93, code: 'ContextMenu', key: 'ContextMenu'}, + NumpadMultiply: { + keyCode: 106, + code: 'NumpadMultiply', + key: '*', + location: 3, + }, + NumpadAdd: {keyCode: 107, code: 'NumpadAdd', key: '+', location: 3}, + NumpadSubtract: { + keyCode: 109, + code: 'NumpadSubtract', + key: '-', + location: 3, + }, + NumpadDivide: {keyCode: 111, code: 'NumpadDivide', key: '/', location: 3}, + F1: {keyCode: 112, code: 'F1', key: 'F1'}, + F2: {keyCode: 113, code: 'F2', key: 'F2'}, + F3: {keyCode: 114, code: 'F3', key: 'F3'}, + F4: {keyCode: 115, code: 'F4', key: 'F4'}, + F5: {keyCode: 116, code: 'F5', key: 'F5'}, + F6: {keyCode: 117, code: 'F6', key: 'F6'}, + F7: {keyCode: 118, code: 'F7', key: 'F7'}, + F8: {keyCode: 119, code: 'F8', key: 'F8'}, + F9: {keyCode: 120, code: 'F9', key: 'F9'}, + F10: {keyCode: 121, code: 'F10', key: 'F10'}, + F11: {keyCode: 122, code: 'F11', key: 'F11'}, + F12: {keyCode: 123, code: 'F12', key: 'F12'}, + F13: {keyCode: 124, code: 'F13', key: 'F13'}, + F14: {keyCode: 125, code: 'F14', key: 'F14'}, + F15: {keyCode: 126, code: 'F15', key: 'F15'}, + F16: {keyCode: 127, code: 'F16', key: 'F16'}, + F17: {keyCode: 128, code: 'F17', key: 'F17'}, + F18: {keyCode: 129, code: 'F18', key: 'F18'}, + F19: {keyCode: 130, code: 'F19', key: 'F19'}, + F20: {keyCode: 131, code: 'F20', key: 'F20'}, + F21: {keyCode: 132, code: 'F21', key: 'F21'}, + F22: {keyCode: 133, code: 'F22', key: 'F22'}, + F23: {keyCode: 134, code: 'F23', key: 'F23'}, + F24: {keyCode: 135, code: 'F24', key: 'F24'}, + NumLock: {keyCode: 144, code: 'NumLock', key: 'NumLock'}, + ScrollLock: {keyCode: 145, code: 'ScrollLock', key: 'ScrollLock'}, + AudioVolumeMute: { + keyCode: 173, + code: 'AudioVolumeMute', + key: 'AudioVolumeMute', + }, + AudioVolumeDown: { + keyCode: 174, + code: 'AudioVolumeDown', + key: 'AudioVolumeDown', + }, + AudioVolumeUp: {keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp'}, + MediaTrackNext: { + keyCode: 176, + code: 'MediaTrackNext', + key: 'MediaTrackNext', + }, + MediaTrackPrevious: { + keyCode: 177, + code: 'MediaTrackPrevious', + key: 'MediaTrackPrevious', + }, + MediaStop: {keyCode: 178, code: 'MediaStop', key: 'MediaStop'}, + MediaPlayPause: { + keyCode: 179, + code: 'MediaPlayPause', + key: 'MediaPlayPause', + }, + Semicolon: {keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';'}, + Equal: {keyCode: 187, code: 'Equal', shiftKey: '+', key: '='}, + NumpadEqual: {keyCode: 187, code: 'NumpadEqual', key: '=', location: 3}, + Comma: {keyCode: 188, code: 'Comma', shiftKey: '<', key: ','}, + Minus: {keyCode: 189, code: 'Minus', shiftKey: '_', key: '-'}, + Period: {keyCode: 190, code: 'Period', shiftKey: '>', key: '.'}, + Slash: {keyCode: 191, code: 'Slash', shiftKey: '?', key: '/'}, + Backquote: {keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`'}, + BracketLeft: {keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '['}, + Backslash: {keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\'}, + BracketRight: {keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']'}, + Quote: {keyCode: 222, code: 'Quote', shiftKey: '"', key: "'"}, + AltGraph: {keyCode: 225, code: 'AltGraph', key: 'AltGraph'}, + Props: {keyCode: 247, code: 'Props', key: 'CrSel'}, + Cancel: {keyCode: 3, key: 'Cancel', code: 'Abort'}, + Clear: {keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3}, + Shift: {keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1}, + Control: {keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1}, + Alt: {keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1}, + Accept: {keyCode: 30, key: 'Accept'}, + ModeChange: {keyCode: 31, key: 'ModeChange'}, + ' ': {keyCode: 32, key: ' ', code: 'Space'}, + Print: {keyCode: 42, key: 'Print'}, + Execute: {keyCode: 43, key: 'Execute', code: 'Open'}, + '\u0000': {keyCode: 46, key: '\u0000', code: 'NumpadDecimal', location: 3}, + a: {keyCode: 65, key: 'a', code: 'KeyA'}, + b: {keyCode: 66, key: 'b', code: 'KeyB'}, + c: {keyCode: 67, key: 'c', code: 'KeyC'}, + d: {keyCode: 68, key: 'd', code: 'KeyD'}, + e: {keyCode: 69, key: 'e', code: 'KeyE'}, + f: {keyCode: 70, key: 'f', code: 'KeyF'}, + g: {keyCode: 71, key: 'g', code: 'KeyG'}, + h: {keyCode: 72, key: 'h', code: 'KeyH'}, + i: {keyCode: 73, key: 'i', code: 'KeyI'}, + j: {keyCode: 74, key: 'j', code: 'KeyJ'}, + k: {keyCode: 75, key: 'k', code: 'KeyK'}, + l: {keyCode: 76, key: 'l', code: 'KeyL'}, + m: {keyCode: 77, key: 'm', code: 'KeyM'}, + n: {keyCode: 78, key: 'n', code: 'KeyN'}, + o: {keyCode: 79, key: 'o', code: 'KeyO'}, + p: {keyCode: 80, key: 'p', code: 'KeyP'}, + q: {keyCode: 81, key: 'q', code: 'KeyQ'}, + r: {keyCode: 82, key: 'r', code: 'KeyR'}, + s: {keyCode: 83, key: 's', code: 'KeyS'}, + t: {keyCode: 84, key: 't', code: 'KeyT'}, + u: {keyCode: 85, key: 'u', code: 'KeyU'}, + v: {keyCode: 86, key: 'v', code: 'KeyV'}, + w: {keyCode: 87, key: 'w', code: 'KeyW'}, + x: {keyCode: 88, key: 'x', code: 'KeyX'}, + y: {keyCode: 89, key: 'y', code: 'KeyY'}, + z: {keyCode: 90, key: 'z', code: 'KeyZ'}, + Meta: {keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1}, + '*': {keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3}, + '+': {keyCode: 107, key: '+', code: 'NumpadAdd', location: 3}, + '-': {keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3}, + '/': {keyCode: 111, key: '/', code: 'NumpadDivide', location: 3}, + ';': {keyCode: 186, key: ';', code: 'Semicolon'}, + '=': {keyCode: 187, key: '=', code: 'Equal'}, + ',': {keyCode: 188, key: ',', code: 'Comma'}, + '.': {keyCode: 190, key: '.', code: 'Period'}, + '`': {keyCode: 192, key: '`', code: 'Backquote'}, + '[': {keyCode: 219, key: '[', code: 'BracketLeft'}, + '\\': {keyCode: 220, key: '\\', code: 'Backslash'}, + ']': {keyCode: 221, key: ']', code: 'BracketRight'}, + "'": {keyCode: 222, key: "'", code: 'Quote'}, + Attn: {keyCode: 246, key: 'Attn'}, + CrSel: {keyCode: 247, key: 'CrSel', code: 'Props'}, + ExSel: {keyCode: 248, key: 'ExSel'}, + EraseEof: {keyCode: 249, key: 'EraseEof'}, + Play: {keyCode: 250, key: 'Play'}, + ZoomOut: {keyCode: 251, key: 'ZoomOut'}, + ')': {keyCode: 48, key: ')', code: 'Digit0'}, + '!': {keyCode: 49, key: '!', code: 'Digit1'}, + '@': {keyCode: 50, key: '@', code: 'Digit2'}, + '#': {keyCode: 51, key: '#', code: 'Digit3'}, + $: {keyCode: 52, key: '$', code: 'Digit4'}, + '%': {keyCode: 53, key: '%', code: 'Digit5'}, + '^': {keyCode: 54, key: '^', code: 'Digit6'}, + '&': {keyCode: 55, key: '&', code: 'Digit7'}, + '(': {keyCode: 57, key: '(', code: 'Digit9'}, + A: {keyCode: 65, key: 'A', code: 'KeyA'}, + B: {keyCode: 66, key: 'B', code: 'KeyB'}, + C: {keyCode: 67, key: 'C', code: 'KeyC'}, + D: {keyCode: 68, key: 'D', code: 'KeyD'}, + E: {keyCode: 69, key: 'E', code: 'KeyE'}, + F: {keyCode: 70, key: 'F', code: 'KeyF'}, + G: {keyCode: 71, key: 'G', code: 'KeyG'}, + H: {keyCode: 72, key: 'H', code: 'KeyH'}, + I: {keyCode: 73, key: 'I', code: 'KeyI'}, + J: {keyCode: 74, key: 'J', code: 'KeyJ'}, + K: {keyCode: 75, key: 'K', code: 'KeyK'}, + L: {keyCode: 76, key: 'L', code: 'KeyL'}, + M: {keyCode: 77, key: 'M', code: 'KeyM'}, + N: {keyCode: 78, key: 'N', code: 'KeyN'}, + O: {keyCode: 79, key: 'O', code: 'KeyO'}, + P: {keyCode: 80, key: 'P', code: 'KeyP'}, + Q: {keyCode: 81, key: 'Q', code: 'KeyQ'}, + R: {keyCode: 82, key: 'R', code: 'KeyR'}, + S: {keyCode: 83, key: 'S', code: 'KeyS'}, + T: {keyCode: 84, key: 'T', code: 'KeyT'}, + U: {keyCode: 85, key: 'U', code: 'KeyU'}, + V: {keyCode: 86, key: 'V', code: 'KeyV'}, + W: {keyCode: 87, key: 'W', code: 'KeyW'}, + X: {keyCode: 88, key: 'X', code: 'KeyX'}, + Y: {keyCode: 89, key: 'Y', code: 'KeyY'}, + Z: {keyCode: 90, key: 'Z', code: 'KeyZ'}, + ':': {keyCode: 186, key: ':', code: 'Semicolon'}, + '<': {keyCode: 188, key: '<', code: 'Comma'}, + _: {keyCode: 189, key: '_', code: 'Minus'}, + '>': {keyCode: 190, key: '>', code: 'Period'}, + '?': {keyCode: 191, key: '?', code: 'Slash'}, + '~': {keyCode: 192, key: '~', code: 'Backquote'}, + '{': {keyCode: 219, key: '{', code: 'BracketLeft'}, + '|': {keyCode: 220, key: '|', code: 'Backslash'}, + '}': {keyCode: 221, key: '}', code: 'BracketRight'}, + '"': {keyCode: 222, key: '"', code: 'Quote'}, + SoftLeft: {key: 'SoftLeft', code: 'SoftLeft', location: 4}, + SoftRight: {key: 'SoftRight', code: 'SoftRight', location: 4}, + Camera: {keyCode: 44, key: 'Camera', code: 'Camera', location: 4}, + Call: {key: 'Call', code: 'Call', location: 4}, + EndCall: {keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4}, + VolumeDown: { + keyCode: 182, + key: 'VolumeDown', + code: 'VolumeDown', + location: 4, + }, + VolumeUp: {keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4}, +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts new file mode 100644 index 0000000000..30155d4a50 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/WaitTask.ts @@ -0,0 +1,260 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ElementHandle} from '../api/ElementHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; +import type {Poller} from '../injected/Poller.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; +import {stringifyFunction} from '../util/Function.js'; + +import {TimeoutError} from './Errors.js'; +import {IsolatedWorld} from './IsolatedWorld.js'; +import {LazyArg} from './LazyArg.js'; +import {HandleFor} from './types.js'; + +/** + * @internal + */ +export interface WaitTaskOptions { + polling: 'raf' | 'mutation' | number; + root?: ElementHandle<Node>; + timeout: number; + signal?: AbortSignal; +} + +/** + * @internal + */ +export class WaitTask<T = unknown> { + #world: IsolatedWorld; + #polling: 'raf' | 'mutation' | number; + #root?: ElementHandle<Node>; + + #fn: string; + #args: unknown[]; + + #timeout?: NodeJS.Timeout; + + #result = createDeferredPromise<HandleFor<T>>(); + + #poller?: JSHandle<Poller<T>>; + #signal?: AbortSignal; + + constructor( + world: IsolatedWorld, + options: WaitTaskOptions, + fn: ((...args: unknown[]) => Promise<T>) | string, + ...args: unknown[] + ) { + this.#world = world; + this.#polling = options.polling; + this.#root = options.root; + this.#signal = options.signal; + this.#signal?.addEventListener( + 'abort', + () => { + void this.terminate(this.#signal?.reason); + }, + { + once: true, + } + ); + + switch (typeof fn) { + case 'string': + this.#fn = `() => {return (${fn});}`; + break; + default: + this.#fn = stringifyFunction(fn); + break; + } + this.#args = args; + + this.#world.taskManager.add(this); + + if (options.timeout) { + this.#timeout = setTimeout(() => { + void this.terminate( + new TimeoutError(`Waiting failed: ${options.timeout}ms exceeded`) + ); + }, options.timeout); + } + + void this.rerun(); + } + + get result(): Promise<HandleFor<T>> { + return this.#result; + } + + async rerun(): Promise<void> { + try { + switch (this.#polling) { + case 'raf': + this.#poller = await this.#world.evaluateHandle( + ({RAFPoller, createFunction}, fn, ...args) => { + const fun = createFunction(fn); + return new RAFPoller(() => { + return fun(...args) as Promise<T>; + }); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + this.#fn, + ...this.#args + ); + break; + case 'mutation': + this.#poller = await this.#world.evaluateHandle( + ({MutationPoller, createFunction}, root, fn, ...args) => { + const fun = createFunction(fn); + return new MutationPoller(() => { + return fun(...args) as Promise<T>; + }, root || document); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + this.#root, + this.#fn, + ...this.#args + ); + break; + default: + this.#poller = await this.#world.evaluateHandle( + ({IntervalPoller, createFunction}, ms, fn, ...args) => { + const fun = createFunction(fn); + return new IntervalPoller(() => { + return fun(...args) as Promise<T>; + }, ms); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + this.#polling, + this.#fn, + ...this.#args + ); + break; + } + + await this.#poller.evaluate(poller => { + void poller.start(); + }); + + const result = await this.#poller.evaluateHandle(poller => { + return poller.result(); + }); + this.#result.resolve(result); + + await this.terminate(); + } catch (error) { + const badError = this.getBadError(error); + if (badError) { + await this.terminate(badError); + } + } + } + + async terminate(error?: unknown): Promise<void> { + this.#world.taskManager.delete(this); + + if (this.#timeout) { + clearTimeout(this.#timeout); + } + + if (error && !this.#result.finished()) { + this.#result.reject(error); + } + + if (this.#poller) { + try { + await this.#poller.evaluateHandle(async poller => { + await poller.stop(); + }); + if (this.#poller) { + await this.#poller.dispose(); + this.#poller = undefined; + } + } catch { + // Ignore errors since they most likely come from low-level cleanup. + } + } + } + + /** + * Not all errors lead to termination. They usually imply we need to rerun the task. + */ + getBadError(error: unknown): unknown { + if (error instanceof Error) { + // When frame is detached the task should have been terminated by the IsolatedWorld. + // This can fail if we were adding this task while the frame was detached, + // so we terminate here instead. + if ( + error.message.includes( + 'Execution context is not available in detached frame' + ) + ) { + return new Error('Waiting failed: Frame detached'); + } + + // When the page is navigated, the promise is rejected. + // We will try again in the new execution context. + if (error.message.includes('Execution context was destroyed')) { + return; + } + + // We could have tried to evaluate in a context which was already + // destroyed. + if (error.message.includes('Cannot find context with specified id')) { + return; + } + } + + return error; + } +} + +/** + * @internal + */ +export class TaskManager { + #tasks: Set<WaitTask> = new Set<WaitTask>(); + + add(task: WaitTask<any>): void { + this.#tasks.add(task); + } + + delete(task: WaitTask<any>): void { + this.#tasks.delete(task); + } + + terminateAll(error?: Error): void { + for (const task of this.#tasks) { + void task.terminate(error); + } + this.#tasks.clear(); + } + + async rerunAll(): Promise<void> { + await Promise.all( + [...this.#tasks].map(task => { + return task.rerun(); + }) + ); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/WebWorker.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/WebWorker.ts new file mode 100644 index 0000000000..fface119ad --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/WebWorker.ts @@ -0,0 +1,179 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; + +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +import {CDPSession} from './Connection.js'; +import {ConsoleMessageType} from './ConsoleMessage.js'; +import {EventEmitter} from './EventEmitter.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {CDPJSHandle} from './JSHandle.js'; +import {EvaluateFunc, HandleFor} from './types.js'; +import {debugError} from './util.js'; + +/** + * @internal + */ +export type ConsoleAPICalledCallback = ( + eventType: ConsoleMessageType, + handles: CDPJSHandle[], + trace: Protocol.Runtime.StackTrace +) => void; + +/** + * @internal + */ +export type ExceptionThrownCallback = ( + details: Protocol.Runtime.ExceptionDetails +) => void; + +/** + * This class represents a + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API | WebWorker}. + * + * @remarks + * The events `workercreated` and `workerdestroyed` are emitted on the page + * object to signal the worker lifecycle. + * + * @example + * + * ```ts + * page.on('workercreated', worker => + * console.log('Worker created: ' + worker.url()) + * ); + * page.on('workerdestroyed', worker => + * console.log('Worker destroyed: ' + worker.url()) + * ); + * + * console.log('Current workers:'); + * for (const worker of page.workers()) { + * console.log(' ' + worker.url()); + * } + * ``` + * + * @public + */ +export class WebWorker extends EventEmitter { + #executionContext = createDeferredPromise<ExecutionContext>(); + + #client: CDPSession; + #url: string; + + /** + * @internal + */ + constructor( + client: CDPSession, + url: string, + consoleAPICalled: ConsoleAPICalledCallback, + exceptionThrown: ExceptionThrownCallback + ) { + super(); + this.#client = client; + this.#url = url; + + this.#client.once('Runtime.executionContextCreated', async event => { + const context = new ExecutionContext(client, event.context); + this.#executionContext.resolve(context); + }); + this.#client.on('Runtime.consoleAPICalled', async event => { + const context = await this.#executionContext; + return consoleAPICalled( + event.type, + event.args.map((object: Protocol.Runtime.RemoteObject) => { + return new CDPJSHandle(context, object); + }), + event.stackTrace + ); + }); + this.#client.on('Runtime.exceptionThrown', exception => { + return exceptionThrown(exception.exceptionDetails); + }); + + // This might fail if the target is closed before we receive all execution contexts. + this.#client.send('Runtime.enable').catch(debugError); + } + + /** + * @internal + */ + async executionContext(): Promise<ExecutionContext> { + return this.#executionContext; + } + + /** + * The URL of this web worker. + */ + url(): string { + return this.#url; + } + + /** + * The CDP session client the WebWorker belongs to. + */ + get client(): CDPSession { + return this.#client; + } + + /** + * If the function passed to the `worker.evaluate` returns a Promise, then + * `worker.evaluate` would wait for the promise to resolve and return its + * value. If the function passed to the `worker.evaluate` returns a + * non-serializable value, then `worker.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. + * Shortcut for `await worker.executionContext()).evaluate(pageFunction, ...args)`. + * + * @param pageFunction - Function to be evaluated in the worker context. + * @param args - Arguments to pass to `pageFunction`. + * @returns Promise which resolves to the return value of `pageFunction`. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const context = await this.#executionContext; + return context.evaluate(pageFunction, ...args); + } + + /** + * The only difference between `worker.evaluate` and `worker.evaluateHandle` + * is that `worker.evaluateHandle` returns in-page object (JSHandle). If the + * function passed to the `worker.evaluateHandle` returns a `Promise`, then + * `worker.evaluateHandle` would wait for the promise to resolve and return + * its value. Shortcut for + * `await worker.executionContext()).evaluateHandle(pageFunction, ...args)` + * + * @param pageFunction - Function to be evaluated in the page context. + * @param args - Arguments to pass to `pageFunction`. + * @returns Promise which resolves to the return value of `pageFunction`. + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + const context = await this.#executionContext; + return context.evaluateHandle(pageFunction, ...args); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts new file mode 100644 index 0000000000..34f824d542 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/XPathQueryHandler.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {QueryHandler, QuerySelectorAll} from './QueryHandler.js'; + +/** + * @internal + */ +export class XPathQueryHandler extends QueryHandler { + static override querySelectorAll: QuerySelectorAll = ( + element, + selector, + {xpathQuerySelectorAll} + ) => { + return xpathQuerySelectorAll(element, selector); + }; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts new file mode 100644 index 0000000000..1f965c56ab --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BidiOverCDP.ts @@ -0,0 +1,190 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js'; +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; + +import {CDPSession, Connection as CDPPPtrConnection} from '../Connection.js'; +import {Handler} from '../EventEmitter.js'; + +import {Connection as BidiPPtrConnection} from './Connection.js'; + +type CdpEvents = { + [Property in keyof ProtocolMapping.Events]: ProtocolMapping.Events[Property][0]; +}; + +/** + * @internal + */ +export async function connectBidiOverCDP( + cdp: CDPPPtrConnection +): Promise<BidiPPtrConnection> { + const transportBiDi = new NoOpTransport(); + const cdpConnectionAdapter = new CDPConnectionAdapter(cdp); + const pptrTransport = { + send(message: string): void { + // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer. + transportBiDi.emitMessage(JSON.parse(message)); + }, + close(): void { + bidiServer.close(); + cdpConnectionAdapter.close(); + }, + onmessage(_message: string): void { + // The method is overridden by the Connection. + }, + }; + transportBiDi.on('bidiResponse', (message: object) => { + // Forwards a BiDi event sent by BidiServer to Puppeteer. + pptrTransport.onmessage(JSON.stringify(message)); + }); + const pptrBiDiConnection = new BidiPPtrConnection(pptrTransport); + const bidiServer = await BidiMapper.BidiServer.createAndStart( + transportBiDi, + cdpConnectionAdapter, + '' + ); + return pptrBiDiConnection; +} + +/** + * Manages CDPSessions for BidiServer. + * @internal + */ +class CDPConnectionAdapter { + #cdp: CDPPPtrConnection; + #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>(); + #browser: CDPClientAdapter<CDPPPtrConnection>; + + constructor(cdp: CDPPPtrConnection) { + this.#cdp = cdp; + this.#browser = new CDPClientAdapter(cdp); + } + + browserClient(): CDPClientAdapter<CDPPPtrConnection> { + return this.#browser; + } + + getCdpClient(id: string) { + const session = this.#cdp.session(id); + if (!session) { + throw new Error('Unknown CDP session with id' + id); + } + if (!this.#adapters.has(session)) { + const adapter = new CDPClientAdapter(session); + this.#adapters.set(session, adapter); + return adapter; + } + return this.#adapters.get(session)!; + } + + close() { + this.#browser.close(); + for (const adapter of this.#adapters.values()) { + adapter.close(); + } + } +} + +/** + * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that + * BidiServer needs. + * + * @internal + */ +class CDPClientAdapter<T extends Pick<CDPPPtrConnection, 'send' | 'on' | 'off'>> + extends BidiMapper.EventEmitter<CdpEvents> + implements BidiMapper.CdpClient +{ + #closed = false; + #client: T; + + constructor(client: T) { + super(); + this.#client = client; + this.#client.on('*', this.#forwardMessage as Handler<any>); + } + + #forwardMessage = <T extends keyof CdpEvents>( + method: T, + event: CdpEvents[T] + ) => { + this.emit(method, event); + }; + + async sendCommand<T extends keyof ProtocolMapping.Commands>( + method: T, + ...params: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + if (this.#closed) { + return; + } + try { + return await this.#client.send(method, ...params); + } catch (err) { + if (this.#closed) { + return; + } + throw err; + } + } + + close() { + this.#client.off('*', this.#forwardMessage as Handler<any>); + this.#closed = true; + } +} + +/** + * This transport is given to the BiDi server instance and allows Puppeteer + * to send and receive commands to the BiDiServer. + * @internal + */ +class NoOpTransport + extends BidiMapper.EventEmitter<any> + implements BidiMapper.BidiTransport +{ + #onMessage: ( + message: Bidi.Message.RawCommandRequest + ) => Promise<void> | void = async ( + _m: Bidi.Message.RawCommandRequest + ): Promise<void> => { + return; + }; + + emitMessage(message: Bidi.Message.RawCommandRequest) { + void this.#onMessage(message); + } + + setOnMessage( + onMessage: (message: Bidi.Message.RawCommandRequest) => Promise<void> | void + ): void { + this.#onMessage = onMessage; + } + + async sendMessage(message: Bidi.Message.OutgoingMessage): Promise<void> { + this.emit('bidiResponse', message); + } + + close() { + this.#onMessage = async ( + _m: Bidi.Message.RawCommandRequest + ): Promise<void> => { + return; + }; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts new file mode 100644 index 0000000000..9741ce7129 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Browser.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ChildProcess} from 'child_process'; + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import { + Browser as BrowserBase, + BrowserCloseCallback, + BrowserContextOptions, +} from '../../api/Browser.js'; +import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js'; +import {Viewport} from '../PuppeteerViewport.js'; + +import {BrowserContext} from './BrowserContext.js'; +import {Connection} from './Connection.js'; + +/** + * @internal + */ +export class Browser extends BrowserBase { + static async create(opts: Options): Promise<Browser> { + // TODO: await until the connection is established. + try { + await opts.connection.send('session.new', {}); + } catch {} + await opts.connection.send('session.subscribe', { + events: [ + 'browsingContext.contextCreated', + ] as Bidi.Session.SubscribeParametersEvent[], + }); + return new Browser(opts); + } + + #process?: ChildProcess; + #closeCallback?: BrowserCloseCallback; + #connection: Connection; + #defaultViewport: Viewport | null; + + constructor(opts: Options) { + super(); + this.#process = opts.process; + this.#closeCallback = opts.closeCallback; + this.#connection = opts.connection; + this.#defaultViewport = opts.defaultViewport; + } + + override async close(): Promise<void> { + this.#connection.dispose(); + await this.#closeCallback?.call(null); + } + + override isConnected(): boolean { + return !this.#connection.closed; + } + + override process(): ChildProcess | null { + return this.#process ?? null; + } + + override async createIncognitoBrowserContext( + _options?: BrowserContextOptions + ): Promise<BrowserContextBase> { + return new BrowserContext(this.#connection, { + defaultViewport: this.#defaultViewport, + }); + } +} + +interface Options { + process?: ChildProcess; + closeCallback?: BrowserCloseCallback; + connection: Connection; + defaultViewport: Viewport | null; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts new file mode 100644 index 0000000000..92950b87b0 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/BrowserContext.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js'; +import {Page as PageBase} from '../../api/Page.js'; +import {Viewport} from '../PuppeteerViewport.js'; + +import {Connection} from './Connection.js'; +import {Context} from './Context.js'; +import {Page} from './Page.js'; + +interface BrowserContextOptions { + defaultViewport: Viewport | null; +} + +/** + * @internal + */ +export class BrowserContext extends BrowserContextBase { + #connection: Connection; + #defaultViewport: Viewport | null; + + constructor(connection: Connection, options: BrowserContextOptions) { + super(); + this.#connection = connection; + this.#defaultViewport = options.defaultViewport; + } + + override async newPage(): Promise<PageBase> { + const {result} = await this.#connection.send('browsingContext.create', { + type: 'tab', + }); + const context = this.#connection.context(result.context) as Context; + const page = new Page(context); + if (this.#defaultViewport) { + try { + await page.setViewport(this.#defaultViewport); + } catch { + // No support for setViewport in Firefox. + } + } + return page; + } + + override async close(): Promise<void> {} +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts new file mode 100644 index 0000000000..5f26ee00fb --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Connection.ts @@ -0,0 +1,215 @@ +/** + * 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {CallbackRegistry} from '../Connection.js'; +import {ConnectionTransport} from '../ConnectionTransport.js'; +import {debug} from '../Debug.js'; +import {EventEmitter} from '../EventEmitter.js'; + +import {Context} from './Context.js'; + +const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►'); +const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀'); + +/** + * @internal + */ +interface Commands { + 'script.evaluate': { + params: Bidi.Script.EvaluateParameters; + returnType: Bidi.Script.EvaluateResult; + }; + 'script.callFunction': { + params: Bidi.Script.CallFunctionParameters; + returnType: Bidi.Script.CallFunctionResult; + }; + 'script.disown': { + params: Bidi.Script.DisownParameters; + returnType: Bidi.Script.DisownResult; + }; + + 'browsingContext.create': { + params: Bidi.BrowsingContext.CreateParameters; + returnType: Bidi.BrowsingContext.CreateResult; + }; + 'browsingContext.close': { + params: Bidi.BrowsingContext.CloseParameters; + returnType: Bidi.BrowsingContext.CloseResult; + }; + 'browsingContext.navigate': { + params: Bidi.BrowsingContext.NavigateParameters; + returnType: Bidi.BrowsingContext.NavigateResult; + }; + 'browsingContext.print': { + params: Bidi.BrowsingContext.PrintParameters; + returnType: Bidi.BrowsingContext.PrintResult; + }; + 'browsingContext.captureScreenshot': { + params: Bidi.BrowsingContext.CaptureScreenshotParameters; + returnType: Bidi.BrowsingContext.CaptureScreenshotResult; + }; + + 'session.new': { + params: {capabilities?: Record<any, unknown>}; // TODO: Update Types in chromium bidi + returnType: {sessionId: string}; + }; + 'session.status': { + params: object; + returnType: Bidi.Session.StatusResult; + }; + 'session.subscribe': { + params: Bidi.Session.SubscribeParameters; + returnType: Bidi.Session.SubscribeResult; + }; + 'session.unsubscribe': { + params: Bidi.Session.SubscribeParameters; + returnType: Bidi.Session.UnsubscribeResult; + }; + 'cdp.sendCommand': { + params: Bidi.CDP.SendCommandParams; + returnType: Bidi.CDP.SendCommandResult; + }; + 'cdp.getSession': { + params: Bidi.CDP.GetSessionParams; + returnType: Bidi.CDP.GetSessionResult; + }; +} + +/** + * @internal + */ +export class Connection extends EventEmitter { + #transport: ConnectionTransport; + #delay: number; + #timeout? = 0; + #closed = false; + #callbacks = new CallbackRegistry(); + #contexts: Map<string, Context> = new Map(); + + constructor(transport: ConnectionTransport, delay = 0, timeout?: number) { + super(); + this.#delay = delay; + this.#timeout = timeout ?? 180_000; + + this.#transport = transport; + this.#transport.onmessage = this.onMessage.bind(this); + this.#transport.onclose = this.#onClose.bind(this); + } + + get closed(): boolean { + return this.#closed; + } + + context(contextId: string): Context | null { + return this.#contexts.get(contextId) || null; + } + + send<T extends keyof Commands>( + method: T, + params: Commands[T]['params'] + ): Promise<Commands[T]['returnType']> { + return this.#callbacks.create(method, this.#timeout, id => { + const stringifiedMessage = JSON.stringify({ + id, + method, + params, + } as Bidi.Message.CommandRequest); + debugProtocolSend(stringifiedMessage); + this.#transport.send(stringifiedMessage); + }) as Promise<Commands[T]['returnType']>; + } + + /** + * @internal + */ + protected async onMessage(message: string): Promise<void> { + if (this.#delay) { + await new Promise(f => { + return setTimeout(f, this.#delay); + }); + } + debugProtocolReceive(message); + const object = JSON.parse(message) as + | Bidi.Message.CommandResponse + | Bidi.Message.EventMessage; + + if ('id' in object) { + if ('error' in object) { + this.#callbacks.reject( + object.id, + createProtocolError(object), + object.message + ); + } else { + this.#callbacks.resolve(object.id, object); + } + } else { + this.#handleSpecialEvents(object); + this.#maybeEmitOnContext(object); + this.emit(object.method, object.params); + } + } + + #maybeEmitOnContext(event: Bidi.Message.EventMessage) { + let context: Context | undefined; + // Context specific events + if ('context' in event.params && event.params.context) { + context = this.#contexts.get(event.params.context); + // `log.entryAdded` specific context + } else if ('source' in event.params && event.params.source.context) { + context = this.#contexts.get(event.params.source.context); + } + context?.emit(event.method, event.params); + } + + #handleSpecialEvents(event: Bidi.Message.EventMessage) { + switch (event.method) { + case 'browsingContext.contextCreated': + this.#contexts.set( + event.params.context, + new Context(this, event.params) + ); + } + } + + #onClose(): void { + if (this.#closed) { + return; + } + this.#closed = true; + this.#transport.onmessage = undefined; + this.#transport.onclose = undefined; + this.#callbacks.clear(); + } + + dispose(): void { + this.#onClose(); + this.#transport.close(); + } +} + +/** + * @internal + */ +function createProtocolError(object: Bidi.Message.ErrorResult): string { + let message = `${object.error} ${object.message}`; + if (object.stacktrace) { + message += ` ${object.stacktrace}`; + } + return message; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts new file mode 100644 index 0000000000..4d3711d6aa --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Context.ts @@ -0,0 +1,282 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {HTTPResponse} from '../../api/HTTPResponse.js'; +import {WaitForOptions} from '../../api/Page.js'; +import {assert} from '../../util/assert.js'; +import {stringifyFunction} from '../../util/Function.js'; +import {ProtocolMapping} from '../Connection.js'; +import {ProtocolError, TimeoutError} from '../Errors.js'; +import {EventEmitter} from '../EventEmitter.js'; +import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js'; +import {TimeoutSettings} from '../TimeoutSettings.js'; +import {EvaluateFunc, HandleFor} from '../types.js'; +import {isString, setPageContent, waitWithTimeout} from '../util.js'; + +import {Connection} from './Connection.js'; +import {ElementHandle} from './ElementHandle.js'; +import {JSHandle} from './JSHandle.js'; +import {BidiSerializer} from './Serializer.js'; + +/** + * @internal + */ +const lifeCycleToReadinessState = new Map< + PuppeteerLifeCycleEvent, + Bidi.BrowsingContext.ReadinessState +>([ + ['load', 'complete'], + ['domcontentloaded', 'interactive'], +]); + +/** + * @internal + */ +const lifeCycleToSubscribedEvent = new Map<PuppeteerLifeCycleEvent, string>([ + ['load', 'browsingContext.load'], + ['domcontentloaded', 'browsingContext.domContentLoaded'], +]); + +/** + * @internal + */ +export class Context extends EventEmitter { + #connection: Connection; + #url: string; + _contextId: string; + _timeoutSettings = new TimeoutSettings(); + + constructor(connection: Connection, result: Bidi.BrowsingContext.Info) { + super(); + this.#connection = connection; + this._contextId = result.context; + this.#url = result.url; + } + + get connection(): Connection { + return this.#connection; + } + + get id(): string { + return this._contextId; + } + + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.#evaluate(false, pageFunction, ...args); + } + + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.#evaluate(true, pageFunction, ...args); + } + + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: true, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: false, + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + returnByValue: boolean, + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { + let responsePromise; + const resultOwnership = returnByValue ? 'none' : 'root'; + if (isString(pageFunction)) { + responsePromise = this.#connection.send('script.evaluate', { + expression: pageFunction, + target: {context: this._contextId}, + resultOwnership, + awaitPromise: true, + }); + } else { + responsePromise = this.#connection.send('script.callFunction', { + functionDeclaration: stringifyFunction(pageFunction), + arguments: await Promise.all( + args.map(arg => { + return BidiSerializer.serialize(arg, this); + }) + ), + target: {context: this._contextId}, + resultOwnership, + awaitPromise: true, + }); + } + + const {result} = await responsePromise; + + if ('type' in result && result.type === 'exception') { + throw new Error(result.exceptionDetails.text); + } + + return returnByValue + ? BidiSerializer.deserialize(result.result) + : getBidiHandle(this, result.result); + } + + async goto( + url: string, + options: WaitForOptions & { + referer?: string | undefined; + referrerPolicy?: string | undefined; + } = {} + ): Promise<HTTPResponse | null> { + const { + waitUntil = 'load', + timeout = this._timeoutSettings.navigationTimeout(), + } = options; + + const readinessState = lifeCycleToReadinessState.get( + getWaitUntilSingle(waitUntil) + ) as Bidi.BrowsingContext.ReadinessState; + + try { + const response = await waitWithTimeout( + this.connection.send('browsingContext.navigate', { + url: url, + context: this.id, + wait: readinessState, + }), + 'Navigation', + timeout + ); + this.#url = response.result.url; + + return null; + } catch (error) { + if (error instanceof ProtocolError) { + error.message += ` at ${url}`; + } else if (error instanceof TimeoutError) { + error.message = 'Navigation timeout of ' + timeout + ' ms exceeded'; + } + throw error; + } + } + + url(): string { + return this.#url; + } + + async setContent( + html: string, + options: WaitForOptions | undefined = {} + ): Promise<void> { + const { + waitUntil = 'load', + timeout = this._timeoutSettings.navigationTimeout(), + } = options; + + const waitUntilCommand = lifeCycleToSubscribedEvent.get( + getWaitUntilSingle(waitUntil) + ) as string; + + await Promise.all([ + setPageContent(this, html), + waitWithTimeout( + new Promise<void>(resolve => { + this.once(waitUntilCommand, () => { + resolve(); + }); + }), + waitUntilCommand, + timeout + ), + ]); + } + + async sendCDPCommand( + method: keyof ProtocolMapping.Commands, + params: object = {} + ): Promise<unknown> { + const session = await this.#connection.send('cdp.getSession', { + context: this._contextId, + }); + // TODO: remove any once chromium-bidi types are updated. + const sessionId = (session.result as any).cdpSession; + return await this.#connection.send('cdp.sendCommand', { + cdpMethod: method, + cdpParams: params, + cdpSession: sessionId, + }); + } +} + +/** + * @internal + */ +function getWaitUntilSingle( + event: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[] +): Extract<PuppeteerLifeCycleEvent, 'load' | 'domcontentloaded'> { + if (Array.isArray(event) && event.length > 1) { + throw new Error('BiDi support only single `waitUntil` argument'); + } + const waitUntilSingle = Array.isArray(event) + ? (event.find(lifecycle => { + return lifecycle === 'domcontentloaded' || lifecycle === 'load'; + }) as PuppeteerLifeCycleEvent) + : event; + + if ( + waitUntilSingle === 'networkidle0' || + waitUntilSingle === 'networkidle2' + ) { + throw new Error(`BiDi does not support 'waitUntil' ${waitUntilSingle}`); + } + + assert(waitUntilSingle, `Invalid waitUntil option ${waitUntilSingle}`); + + return waitUntilSingle; +} + +/** + * @internal + */ +export function getBidiHandle( + context: Context, + result: Bidi.CommonDataTypes.RemoteValue +): JSHandle | ElementHandle<Node> { + if (result.type === 'node' || result.type === 'window') { + return new ElementHandle(context, result); + } + return new JSHandle(context, result); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts new file mode 100644 index 0000000000..21e69e3e9b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/ElementHandle.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {ElementHandle as BaseElementHandle} from '../../api/ElementHandle.js'; + +import {Connection} from './Connection.js'; +import {Context} from './Context.js'; +import {JSHandle} from './JSHandle.js'; + +/** + * @internal + */ +export class ElementHandle< + ElementType extends Node = Element +> extends BaseElementHandle<ElementType> { + declare handle: JSHandle<ElementType>; + + constructor(context: Context, remoteValue: Bidi.CommonDataTypes.RemoteValue) { + super(new JSHandle(context, remoteValue)); + } + + context(): Context { + return this.handle.context(); + } + + get connection(): Connection { + return this.handle.connection; + } + + get isPrimitiveValue(): boolean { + return this.handle.isPrimitiveValue; + } + + remoteValue(): Bidi.CommonDataTypes.RemoteValue { + return this.handle.remoteValue(); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts new file mode 100644 index 0000000000..2cd2876622 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/JSHandle.ts @@ -0,0 +1,159 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {ElementHandle} from '../../api/ElementHandle.js'; +import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js'; +import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js'; + +import {Connection} from './Connection.js'; +import {Context} from './Context.js'; +import {BidiSerializer} from './Serializer.js'; +import {releaseReference} from './utils.js'; + +export class JSHandle<T = unknown> extends BaseJSHandle<T> { + #disposed = false; + #context; + #remoteValue; + + constructor(context: Context, remoteValue: Bidi.CommonDataTypes.RemoteValue) { + super(); + this.#context = context; + this.#remoteValue = remoteValue; + } + + context(): Context { + return this.#context; + } + + get connection(): Connection { + return this.#context.connection; + } + + override get disposed(): boolean { + return this.#disposed; + } + + override async evaluate< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return await this.context().evaluate(pageFunction, this, ...args); + } + + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return await this.context().evaluateHandle(pageFunction, this, ...args); + } + + override async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>>; + override async getProperty(propertyName: string): Promise<HandleFor<unknown>>; + override async getProperty<K extends keyof T>( + propertyName: HandleOr<K> + ): Promise<HandleFor<T[K]>> { + return await this.evaluateHandle((object, propertyName) => { + return object[propertyName as K]; + }, propertyName); + } + + override async getProperties(): Promise<Map<string, BaseJSHandle>> { + // TODO(lightning00blade): Either include return of depth Handles in RemoteValue + // or new BiDi command that returns array of remote value + const keys = await this.evaluate(object => { + return Object.getOwnPropertyNames(object); + }); + const map: Map<string, BaseJSHandle> = new Map(); + const results = await Promise.all( + keys.map(key => { + return this.getProperty(key); + }) + ); + + for (const [key, value] of Object.entries(keys)) { + const handle = results[key as any]; + if (handle) { + map.set(value, handle); + } + } + + return map; + } + + override async jsonValue(): Promise<T> { + const value = BidiSerializer.deserialize(this.#remoteValue); + + if (this.#remoteValue.type !== 'undefined' && value === undefined) { + throw new Error('Could not serialize referenced object'); + } + return value; + } + + override asElement(): ElementHandle<Node> | null { + return null; + } + + override async dispose(): Promise<void> { + if (this.#disposed) { + return; + } + this.#disposed = true; + if ('handle' in this.#remoteValue) { + await releaseReference(this.#context, this.#remoteValue); + } + } + + get isPrimitiveValue(): boolean { + switch (this.#remoteValue.type) { + case 'string': + case 'number': + case 'bigint': + case 'boolean': + case 'undefined': + case 'null': + return true; + + default: + return false; + } + } + + override toString(): string { + if (this.isPrimitiveValue) { + return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue); + } + + return 'JSHandle@' + this.#remoteValue.type; + } + + override get id(): string | undefined { + return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined; + } + + remoteValue(): Bidi.CommonDataTypes.RemoteValue { + return this.#remoteValue; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts new file mode 100644 index 0000000000..524f5ed122 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Page.ts @@ -0,0 +1,345 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {Readable} from 'stream'; + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {HTTPResponse} from '../../api/HTTPResponse.js'; +import { + Page as PageBase, + PageEmittedEvents, + ScreenshotOptions, + WaitForOptions, +} from '../../api/Page.js'; +import {isErrorLike} from '../../util/ErrorLike.js'; +import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js'; +import {Handler} from '../EventEmitter.js'; +import {PDFOptions} from '../PDFOptions.js'; +import {Viewport} from '../PuppeteerViewport.js'; +import {EvaluateFunc, HandleFor} from '../types.js'; +import {debugError, waitWithTimeout} from '../util.js'; + +import {Context, getBidiHandle} from './Context.js'; +import {BidiSerializer} from './Serializer.js'; + +/** + * @internal + */ +export class Page extends PageBase { + #context: Context; + #subscribedEvents = new Map<string, Handler<any>>([ + ['log.entryAdded', this.#onLogEntryAdded.bind(this)], + ['browsingContext.load', this.#onLoad.bind(this)], + ['browsingContext.domContentLoaded', this.#onDOMLoad.bind(this)], + ]) as Map<Bidi.Session.SubscribeParametersEvent, Handler>; + #viewport: Viewport | null = null; + + constructor(context: Context) { + super(); + this.#context = context; + + this.#context.connection + .send('session.subscribe', { + events: [ + ...this.#subscribedEvents.keys(), + ] as Bidi.Session.SubscribeParameters['events'], + contexts: [this.#context.id], + }) + .catch(error => { + if (isErrorLike(error) && !error.message.includes('Target closed')) { + throw error; + } + }); + + for (const [event, subscriber] of this.#subscribedEvents) { + this.#context.on(event, subscriber); + } + } + + #onLogEntryAdded(event: Bidi.Log.LogEntry): void { + if (isConsoleLogEntry(event)) { + const args = event.args.map(arg => { + return getBidiHandle(this.#context, arg); + }); + + const text = args + .reduce((value, arg) => { + const parsedValue = arg.isPrimitiveValue + ? BidiSerializer.deserialize(arg.remoteValue()) + : arg.toString(); + return `${value} ${parsedValue}`; + }, '') + .slice(1); + + this.emit( + PageEmittedEvents.Console, + new ConsoleMessage( + event.method as any, + text, + args, + getStackTraceLocations(event.stackTrace) + ) + ); + } else if (isJavaScriptLogEntry(event)) { + let message = event.text ?? ''; + + if (event.stackTrace) { + for (const callFrame of event.stackTrace.callFrames) { + const location = + callFrame.url + + ':' + + callFrame.lineNumber + + ':' + + callFrame.columnNumber; + const functionName = callFrame.functionName || '<anonymous>'; + message += `\n at ${functionName} (${location})`; + } + } + + const error = new Error(message); + error.stack = ''; // Don't capture Puppeteer stacktrace. + + this.emit(PageEmittedEvents.PageError, error); + } else { + debugError( + `Unhandled LogEntry with type "${event.type}", text "${event.text}" and level "${event.level}"` + ); + } + } + + #onLoad(_event: Bidi.BrowsingContext.NavigationInfo): void { + this.emit(PageEmittedEvents.Load); + } + + #onDOMLoad(_event: Bidi.BrowsingContext.NavigationInfo): void { + this.emit(PageEmittedEvents.DOMContentLoaded); + } + + override async close(): Promise<void> { + await this.#context.connection.send('session.unsubscribe', { + events: [...this.#subscribedEvents.keys()], + contexts: [this.#context.id], + }); + + await this.#context.connection.send('browsingContext.close', { + context: this.#context.id, + }); + + for (const [event, subscriber] of this.#subscribedEvents) { + this.#context.off(event, subscriber); + } + } + + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.#context.evaluateHandle(pageFunction, ...args); + } + + override async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.#context.evaluate(pageFunction, ...args); + } + + override async goto( + url: string, + options?: WaitForOptions & { + referer?: string | undefined; + referrerPolicy?: string | undefined; + } + ): Promise<HTTPResponse | null> { + return this.#context.goto(url, options); + } + + override url(): string { + return this.#context.url(); + } + + override setDefaultNavigationTimeout(timeout: number): void { + this.#context._timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + override setDefaultTimeout(timeout: number): void { + this.#context._timeoutSettings.setDefaultTimeout(timeout); + } + + override async setContent( + html: string, + options: WaitForOptions = {} + ): Promise<void> { + await this.#context.setContent(html, options); + } + + override async content(): Promise<string> { + return await this.evaluate(() => { + let retVal = ''; + if (document.doctype) { + retVal = new XMLSerializer().serializeToString(document.doctype); + } + if (document.documentElement) { + retVal += document.documentElement.outerHTML; + } + return retVal; + }); + } + + override async setViewport(viewport: Viewport): Promise<void> { + // TODO: use BiDi commands when available. + const mobile = false; + const width = viewport.width; + const height = viewport.height; + const deviceScaleFactor = 1; + const screenOrientation = {angle: 0, type: 'portraitPrimary'}; + + await this.#context.sendCDPCommand('Emulation.setDeviceMetricsOverride', { + mobile, + width, + height, + deviceScaleFactor, + screenOrientation, + }); + + this.#viewport = viewport; + } + + override viewport(): Viewport | null { + return this.#viewport; + } + + override async pdf(options: PDFOptions = {}): Promise<Buffer> { + const {path = undefined} = options; + const { + printBackground: background, + margin, + landscape, + width, + height, + pageRanges, + scale, + preferCSSPageSize, + timeout, + } = this._getPDFOptions(options, 'cm'); + const {result} = await waitWithTimeout( + this.#context.connection.send('browsingContext.print', { + context: this.#context._contextId, + background, + margin, + orientation: landscape ? 'landscape' : 'portrait', + page: { + width, + height, + }, + pageRanges: pageRanges.split(', '), + scale, + shrinkToFit: !preferCSSPageSize, + }), + 'browsingContext.print', + timeout + ); + + const buffer = Buffer.from(result.data, 'base64'); + + await this._maybeWriteBufferToFile(path, buffer); + + return buffer; + } + + override async createPDFStream( + options?: PDFOptions | undefined + ): Promise<Readable> { + const buffer = await this.pdf(options); + try { + const {Readable} = await import('stream'); + return Readable.from(buffer); + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Can only pass a file path in a Node-like environment.' + ); + } + throw error; + } + } + + override screenshot( + options: ScreenshotOptions & {encoding: 'base64'} + ): Promise<string>; + override screenshot( + options?: ScreenshotOptions & {encoding?: 'binary'} + ): never; + override async screenshot( + options: ScreenshotOptions = {} + ): Promise<Buffer | string> { + const {path = undefined, encoding, ...args} = options; + if (Object.keys(args).length >= 1) { + throw new Error('BiDi only supports "encoding" and "path" options'); + } + + const {result} = await this.#context.connection.send( + 'browsingContext.captureScreenshot', + { + context: this.#context._contextId, + } + ); + + if (encoding === 'base64') { + return result.data; + } + + const buffer = Buffer.from(result.data, 'base64'); + await this._maybeWriteBufferToFile(path, buffer); + + return buffer; + } +} + +function isConsoleLogEntry( + event: Bidi.Log.LogEntry +): event is Bidi.Log.ConsoleLogEntry { + return event.type === 'console'; +} + +function isJavaScriptLogEntry( + event: Bidi.Log.LogEntry +): event is Bidi.Log.JavascriptLogEntry { + return event.type === 'javascript'; +} + +function getStackTraceLocations( + stackTrace?: Bidi.Script.StackTrace +): ConsoleMessageLocation[] { + const stackTraceLocations: ConsoleMessageLocation[] = []; + if (stackTrace) { + for (const callFrame of stackTrace.callFrames) { + stackTraceLocations.push({ + url: callFrame.url, + lineNumber: callFrame.lineNumber, + columnNumber: callFrame.columnNumber, + }); + } + } + return stackTraceLocations; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts new file mode 100644 index 0000000000..f28b0e7318 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/Serializer.ts @@ -0,0 +1,273 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {debugError, isDate, isPlainObject, isRegExp} from '../util.js'; + +import {Context} from './Context.js'; +import {ElementHandle} from './ElementHandle.js'; +import {JSHandle} from './JSHandle.js'; + +/** + * @internal + */ +class UnserializableError extends Error {} + +/** + * @internal + */ +export class BidiSerializer { + static serializeNumber(arg: number): Bidi.CommonDataTypes.LocalOrRemoteValue { + let value: Bidi.CommonDataTypes.SpecialNumber | number; + if (Object.is(arg, -0)) { + value = '-0'; + } else if (Object.is(arg, Infinity)) { + value = 'Infinity'; + } else if (Object.is(arg, -Infinity)) { + value = '-Infinity'; + } else if (Object.is(arg, NaN)) { + value = 'NaN'; + } else { + value = arg; + } + return { + type: 'number', + value, + }; + } + + static serializeObject( + arg: object | null + ): Bidi.CommonDataTypes.LocalOrRemoteValue { + if (arg === null) { + return { + type: 'null', + }; + } else if (Array.isArray(arg)) { + const parsedArray = arg.map(subArg => { + return BidiSerializer.serializeRemoveValue(subArg); + }); + + return { + type: 'array', + value: parsedArray, + }; + } else if (isPlainObject(arg)) { + try { + JSON.stringify(arg); + } catch (error) { + if ( + error instanceof TypeError && + error.message.startsWith('Converting circular structure to JSON') + ) { + error.message += ' Recursive objects are not allowed.'; + } + throw error; + } + + const parsedObject: Bidi.CommonDataTypes.MappingLocalValue = []; + for (const key in arg) { + parsedObject.push([ + BidiSerializer.serializeRemoveValue(key), + BidiSerializer.serializeRemoveValue(arg[key]), + ]); + } + + return { + type: 'object', + value: parsedObject, + }; + } else if (isRegExp(arg)) { + return { + type: 'regexp', + value: { + pattern: arg.source, + flags: arg.flags, + }, + }; + } else if (isDate(arg)) { + return { + type: 'date', + value: arg.toISOString(), + }; + } + + throw new UnserializableError( + 'Custom object sterilization not possible. Use plain objects instead.' + ); + } + + static serializeRemoveValue( + arg: unknown + ): Bidi.CommonDataTypes.LocalOrRemoteValue { + switch (typeof arg) { + case 'symbol': + case 'function': + throw new UnserializableError(`Unable to serializable ${typeof arg}`); + case 'object': + return BidiSerializer.serializeObject(arg); + + case 'undefined': + return { + type: 'undefined', + }; + case 'number': + return BidiSerializer.serializeNumber(arg); + case 'bigint': + return { + type: 'bigint', + value: arg.toString(), + }; + case 'string': + return { + type: 'string', + value: arg, + }; + case 'boolean': + return { + type: 'boolean', + value: arg, + }; + } + } + + static serialize( + arg: unknown, + context: Context + ): Bidi.CommonDataTypes.LocalOrRemoteValue { + // TODO: See use case of LazyArgs + const objectHandle = + arg && (arg instanceof JSHandle || arg instanceof ElementHandle) + ? arg + : null; + if (objectHandle) { + if (objectHandle.context() !== context) { + throw new Error( + 'JSHandles can be evaluated only in the context they were created!' + ); + } + if (objectHandle.disposed) { + throw new Error('JSHandle is disposed!'); + } + return objectHandle.remoteValue(); + } + + return BidiSerializer.serializeRemoveValue(arg); + } + + static deserializeNumber( + value: Bidi.CommonDataTypes.SpecialNumber | number + ): number { + switch (value) { + case '-0': + return -0; + case 'NaN': + return NaN; + case 'Infinity': + case '+Infinity': + return Infinity; + case '-Infinity': + return -Infinity; + default: + return value; + } + } + + static deserializeLocalValue( + result: Bidi.CommonDataTypes.RemoteValue + ): unknown { + switch (result.type) { + case 'array': + // TODO: Check expected output when value is undefined + return result.value?.map(value => { + return BidiSerializer.deserializeLocalValue(value); + }); + case 'set': + // TODO: Check expected output when value is undefined + return result.value.reduce((acc: Set<unknown>, value) => { + return acc.add(BidiSerializer.deserializeLocalValue(value)); + }, new Set()); + case 'object': + if (result.value) { + return result.value.reduce((acc: Record<any, unknown>, tuple) => { + const {key, value} = BidiSerializer.deserializeTuple(tuple); + acc[key as any] = value; + return acc; + }, {}); + } + break; + case 'map': + return result.value.reduce((acc: Map<unknown, unknown>, tuple) => { + const {key, value} = BidiSerializer.deserializeTuple(tuple); + return acc.set(key, value); + }, new Map()); + case 'promise': + return {}; + case 'regexp': + return new RegExp(result.value.pattern, result.value.flags); + case 'date': + return new Date(result.value); + + case 'undefined': + return undefined; + case 'null': + return null; + case 'number': + return BidiSerializer.deserializeNumber(result.value); + case 'bigint': + return BigInt(result.value); + case 'boolean': + return Boolean(result.value); + case 'string': + return result.value; + } + + throw new UnserializableError( + `Deserialization of type ${result.type} not supported.` + ); + } + + static deserializeTuple([serializedKey, serializedValue]: [ + Bidi.CommonDataTypes.RemoteValue | string, + Bidi.CommonDataTypes.RemoteValue + ]): {key: unknown; value: unknown} { + const key = + typeof serializedKey === 'string' + ? serializedKey + : BidiSerializer.deserializeLocalValue(serializedKey); + const value = BidiSerializer.deserializeLocalValue(serializedValue); + + return {key, value}; + } + + static deserialize(result: Bidi.CommonDataTypes.RemoteValue): any { + if (!result) { + debugError('Service did not produce a result.'); + return undefined; + } + + try { + return BidiSerializer.deserializeLocalValue(result); + } catch (error) { + if (error instanceof UnserializableError) { + debugError(error.message); + return undefined; + } + throw error; + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts new file mode 100644 index 0000000000..c980168aaa --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/bidi.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Browser.js'; +export * from './BrowserContext.js'; +export * from './Page.js'; +export * from './Connection.js'; +export * from './BidiOverCDP.js'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts new file mode 100644 index 0000000000..ad4a590c5a --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/bidi/utils.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +import {debug} from '../Debug.js'; + +import {Context} from './Context.js'; + +/** + * @internal + */ +export const debugError = debug('puppeteer:error'); +/** + * @internal + */ +export async function releaseReference( + client: Context, + remoteReference: Bidi.CommonDataTypes.RemoteReference +): Promise<void> { + if (!remoteReference.handle) { + return; + } + await client.connection + .send('script.disown', { + target: {context: client._contextId}, + handles: [remoteReference.handle], + }) + .catch((error: any) => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts new file mode 100644 index 0000000000..ef4f2de99b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/common.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Accessibility.js'; +export * from './AriaQueryHandler.js'; +export * from './Browser.js'; +export * from './BrowserConnector.js'; +export * from './BrowserWebSocketTransport.js'; +export * from './ChromeTargetManager.js'; +export * from './Configuration.js'; +export * from './Connection.js'; +export * from './ConnectionTransport.js'; +export * from './ConsoleMessage.js'; +export * from './Coverage.js'; +export * from './CustomQueryHandler.js'; +export * from './Debug.js'; +export * from './Device.js'; +export * from './DeviceRequestPrompt.js'; +export * from './Dialog.js'; +export * from './ElementHandle.js'; +export * from './EmulationManager.js'; +export * from './Errors.js'; +export * from './EventEmitter.js'; +export * from './ExecutionContext.js'; +export * from './fetch.js'; +export * from './FileChooser.js'; +export * from './FirefoxTargetManager.js'; +export * from './Frame.js'; +export * from './FrameManager.js'; +export * from './FrameTree.js'; +export * from './Input.js'; +export * from './IsolatedWorld.js'; +export * from './IsolatedWorlds.js'; +export * from './JSHandle.js'; +export * from './LazyArg.js'; +export * from './LifecycleWatcher.js'; +export * from './NetworkEventManager.js'; +export * from './NetworkManager.js'; +export * from './NodeWebSocketTransport.js'; +export * from './Page.js'; +export * from './PDFOptions.js'; +export * from './PredefinedNetworkConditions.js'; +export * from './Product.js'; +export * from './Puppeteer.js'; +export * from './PuppeteerViewport.js'; +export * from './SecurityDetails.js'; +export * from './Target.js'; +export * from './TargetManager.js'; +export * from './TaskQueue.js'; +export * from './TimeoutSettings.js'; +export * from './Tracing.js'; +export * from './types.js'; +export * from './USKeyboardLayout.js'; +export * from './util.js'; +export * from './WaitTask.js'; +export * from './WebWorker.js'; +export * from './QueryHandler.js'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts new file mode 100644 index 0000000000..5f13a35288 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/fetch.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ + +/** + * Gets the global version if we're in the browser, else loads the node-fetch module. + * + * @internal + */ +export const getFetch = async (): Promise<typeof fetch> => { + return (globalThis as any).fetch || (await import('cross-fetch')).fetch; +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts new file mode 100644 index 0000000000..ac05a58ac9 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/types.ts @@ -0,0 +1,213 @@ +/** + * 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 type {ElementHandle} from '../api/ElementHandle.js'; +import type {JSHandle} from '../api/JSHandle.js'; + +import type {LazyArg} from './LazyArg.js'; + +/** + * @internal + */ +export type BindingPayload = { + type: string; + name: string; + seq: number; + args: unknown[]; + /** + * Determines whether the arguments of the payload are trivial. + */ + isTrivial: boolean; +}; + +/** + * @internal + */ +export type AwaitableIterator<T> = Iterator<T> | AsyncIterator<T>; + +/** + * @public + */ +export type AwaitableIterable<T> = Iterable<T> | AsyncIterable<T>; + +/** + * @public + */ +export type Awaitable<T> = T | PromiseLike<T>; + +/** + * @public + */ +export type HandleFor<T> = T extends Node ? ElementHandle<T> : JSHandle<T>; + +/** + * @public + */ +export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T; + +/** + * @public + */ +export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never; + +/** + * @internal + */ +export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T; + +/** + * @internal + */ +export type InnerLazyParams<T extends unknown[]> = { + [K in keyof T]: FlattenLazyArg<T[K]>; +}; + +/** + * @public + */ +export type InnerParams<T extends unknown[]> = { + [K in keyof T]: FlattenHandle<T[K]>; +}; + +/** + * @public + */ +export type ElementFor< + TagName extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap +> = TagName extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[TagName] + : TagName extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[TagName] + : never; + +/** + * @public + */ +export type EvaluateFunc<T extends unknown[]> = ( + ...params: InnerParams<T> +) => Awaitable<unknown>; + +/** + * @public + */ +export type EvaluateFuncWith<V, T extends unknown[]> = ( + ...params: [V, ...InnerParams<T>] +) => Awaitable<unknown>; + +/** + * @public + */ +export type NodeFor<ComplexSelector extends string> = + TypeSelectorOfComplexSelector<ComplexSelector> extends infer TypeSelector + ? TypeSelector extends + | keyof HTMLElementTagNameMap + | keyof SVGElementTagNameMap + ? ElementFor<TypeSelector> + : Element + : never; + +type TypeSelectorOfComplexSelector<ComplexSelector extends string> = + CompoundSelectorsOfComplexSelector<ComplexSelector> extends infer CompoundSelectors + ? CompoundSelectors extends NonEmptyReadonlyArray<string> + ? Last<CompoundSelectors> extends infer LastCompoundSelector + ? LastCompoundSelector extends string + ? TypeSelectorOfCompoundSelector<LastCompoundSelector> + : never + : never + : unknown + : never; + +type TypeSelectorOfCompoundSelector<CompoundSelector extends string> = + SplitWithDelemiters< + CompoundSelector, + BeginSubclassSelectorTokens + > extends infer CompoundSelectorTokens + ? CompoundSelectorTokens extends [infer TypeSelector, ...any[]] + ? TypeSelector extends '' + ? unknown + : TypeSelector + : never + : never; + +type Last<Arr extends NonEmptyReadonlyArray<unknown>> = Arr extends [ + infer Head, + ...infer Tail +] + ? Tail extends NonEmptyReadonlyArray<unknown> + ? Last<Tail> + : Head + : never; + +type NonEmptyReadonlyArray<T> = [T, ...(readonly T[])]; + +type CompoundSelectorsOfComplexSelector<ComplexSelector extends string> = + SplitWithDelemiters< + ComplexSelector, + CombinatorTokens + > extends infer IntermediateTokens + ? IntermediateTokens extends readonly string[] + ? Drop<IntermediateTokens, ''> + : never + : never; + +type SplitWithDelemiters< + Input extends string, + Delemiters extends readonly string[] +> = Delemiters extends [infer FirstDelemiter, ...infer RestDelemiters] + ? FirstDelemiter extends string + ? RestDelemiters extends readonly string[] + ? FlatmapSplitWithDelemiters<Split<Input, FirstDelemiter>, RestDelemiters> + : never + : never + : [Input]; + +type BeginSubclassSelectorTokens = ['.', '#', '[', ':']; + +type CombinatorTokens = [' ', '>', '+', '~', '|', '|']; + +type Drop< + Arr extends readonly unknown[], + Remove, + Acc extends unknown[] = [] +> = Arr extends [infer Head, ...infer Tail] + ? Head extends Remove + ? Drop<Tail, Remove> + : Drop<Tail, Remove, [...Acc, Head]> + : Acc; + +type FlatmapSplitWithDelemiters< + Inputs extends readonly string[], + Delemiters extends readonly string[], + Acc extends string[] = [] +> = Inputs extends [infer FirstInput, ...infer RestInputs] + ? FirstInput extends string + ? RestInputs extends readonly string[] + ? FlatmapSplitWithDelemiters< + RestInputs, + Delemiters, + [...Acc, ...SplitWithDelemiters<FirstInput, Delemiters>] + > + : Acc + : Acc + : Acc; + +type Split< + Input extends string, + Delimiter extends string, + Acc extends string[] = [] +> = Input extends `${infer Prefix}${Delimiter}${infer Suffix}` + ? Split<Suffix, Delimiter, [...Acc, Prefix]> + : [...Acc, Input]; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts new file mode 100644 index 0000000000..d9b66934de --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/common/util.ts @@ -0,0 +1,472 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {Readable} from 'stream'; + +import type {Protocol} from 'devtools-protocol'; + +import type {ElementHandle} from '../api/ElementHandle.js'; +import type {JSHandle} from '../api/JSHandle.js'; +import {Page} from '../api/Page.js'; +import {isNode} from '../environment.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; + +import type {CDPSession} from './Connection.js'; +import {debug} from './Debug.js'; +import {CDPElementHandle} from './ElementHandle.js'; +import {TimeoutError} from './Errors.js'; +import type {CommonEventEmitter} from './EventEmitter.js'; +import type {ExecutionContext} from './ExecutionContext.js'; +import {CDPJSHandle} from './JSHandle.js'; + +/** + * @internal + */ +export const debugError = debug('puppeteer:error'); + +/** + * @internal + */ +export function getExceptionMessage( + exceptionDetails: Protocol.Runtime.ExceptionDetails +): string { + if (exceptionDetails.exception) { + return ( + exceptionDetails.exception.description || exceptionDetails.exception.value + ); + } + let message = exceptionDetails.text; + if (exceptionDetails.stackTrace) { + for (const callframe of exceptionDetails.stackTrace.callFrames) { + const location = + callframe.url + + ':' + + callframe.lineNumber + + ':' + + callframe.columnNumber; + const functionName = callframe.functionName || '<anonymous>'; + message += `\n at ${functionName} (${location})`; + } + } + return message; +} + +/** + * @internal + */ +export function valueFromRemoteObject( + remoteObject: Protocol.Runtime.RemoteObject +): any { + assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); + if (remoteObject.unserializableValue) { + if (remoteObject.type === 'bigint') { + return BigInt(remoteObject.unserializableValue.replace('n', '')); + } + switch (remoteObject.unserializableValue) { + case '-0': + return -0; + case 'NaN': + return NaN; + case 'Infinity': + return Infinity; + case '-Infinity': + return -Infinity; + default: + throw new Error( + 'Unsupported unserializable value: ' + + remoteObject.unserializableValue + ); + } + } + return remoteObject.value; +} + +/** + * @internal + */ +export async function releaseObject( + client: CDPSession, + remoteObject: Protocol.Runtime.RemoteObject +): Promise<void> { + if (!remoteObject.objectId) { + return; + } + await client + .send('Runtime.releaseObject', {objectId: remoteObject.objectId}) + .catch(error => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); +} + +/** + * @internal + */ +export interface PuppeteerEventListener { + emitter: CommonEventEmitter; + eventName: string | symbol; + handler: (...args: any[]) => void; +} + +/** + * @internal + */ +export function addEventListener( + emitter: CommonEventEmitter, + eventName: string | symbol, + handler: (...args: any[]) => void +): PuppeteerEventListener { + emitter.on(eventName, handler); + return {emitter, eventName, handler}; +} + +/** + * @internal + */ +export function removeEventListeners( + listeners: Array<{ + emitter: CommonEventEmitter; + eventName: string | symbol; + handler: (...args: any[]) => void; + }> +): void { + for (const listener of listeners) { + listener.emitter.removeListener(listener.eventName, listener.handler); + } + listeners.length = 0; +} + +/** + * @internal + */ +export const isString = (obj: unknown): obj is string => { + return typeof obj === 'string' || obj instanceof String; +}; + +/** + * @internal + */ +export const isNumber = (obj: unknown): obj is number => { + return typeof obj === 'number' || obj instanceof Number; +}; + +/** + * @internal + */ +export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => { + return typeof obj === 'object' && obj?.constructor === Object; +}; + +/** + * @internal + */ +export const isRegExp = (obj: unknown): obj is RegExp => { + return typeof obj === 'object' && obj?.constructor === RegExp; +}; + +/** + * @internal + */ +export const isDate = (obj: unknown): obj is Date => { + return typeof obj === 'object' && obj?.constructor === Date; +}; + +/** + * @internal + */ +export async function waitForEvent<T>( + emitter: CommonEventEmitter, + eventName: string | symbol, + predicate: (event: T) => Promise<boolean> | boolean, + timeout: number, + abortPromise: Promise<Error> +): Promise<T> { + let eventTimeout: NodeJS.Timeout; + let resolveCallback: (value: T | PromiseLike<T>) => void; + let rejectCallback: (value: Error) => void; + const promise = new Promise<T>((resolve, reject) => { + resolveCallback = resolve; + rejectCallback = reject; + }); + const listener = addEventListener(emitter, eventName, async event => { + if (!(await predicate(event))) { + return; + } + resolveCallback(event); + }); + if (timeout) { + eventTimeout = setTimeout(() => { + rejectCallback( + new TimeoutError('Timeout exceeded while waiting for event') + ); + }, timeout); + } + function cleanup(): void { + removeEventListeners([listener]); + clearTimeout(eventTimeout); + } + const result = await Promise.race([promise, abortPromise]).then( + r => { + cleanup(); + return r; + }, + error => { + cleanup(); + throw error; + } + ); + if (isErrorLike(result)) { + throw result; + } + + return result; +} + +/** + * @internal + */ +export function createJSHandle( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject +): JSHandle | ElementHandle<Node> { + if (remoteObject.subtype === 'node' && context._world) { + return new CDPElementHandle(context, remoteObject, context._world.frame()); + } + return new CDPJSHandle(context, remoteObject); +} + +/** + * @internal + */ +export function evaluationString( + fun: Function | string, + ...args: unknown[] +): string { + if (isString(fun)) { + assert(args.length === 0, 'Cannot evaluate a string with arguments'); + return fun; + } + + function serializeArgument(arg: unknown): string { + if (Object.is(arg, undefined)) { + return 'undefined'; + } + return JSON.stringify(arg); + } + + return `(${fun})(${args.map(serializeArgument).join(',')})`; +} + +/** + * @internal + */ +export function addPageBinding(type: string, name: string): void { + // This is the CDP binding. + // @ts-expect-error: In a different context. + const callCDP = globalThis[name]; + + // We replace the CDP binding with a Puppeteer binding. + Object.assign(globalThis, { + [name](...args: unknown[]): Promise<unknown> { + // This is the Puppeteer binding. + // @ts-expect-error: In a different context. + const callPuppeteer = globalThis[name]; + callPuppeteer.args ??= new Map(); + callPuppeteer.callbacks ??= new Map(); + + const seq = (callPuppeteer.lastSeq ?? 0) + 1; + callPuppeteer.lastSeq = seq; + callPuppeteer.args.set(seq, args); + + callCDP( + JSON.stringify({ + type, + name, + seq, + args, + isTrivial: !args.some(value => { + return value instanceof Node; + }), + }) + ); + + return new Promise((resolve, reject) => { + callPuppeteer.callbacks.set(seq, { + resolve(value: unknown) { + callPuppeteer.args.delete(seq); + resolve(value); + }, + reject(value?: unknown) { + callPuppeteer.args.delete(seq); + reject(value); + }, + }); + }); + }, + }); +} + +/** + * @internal + */ +export function pageBindingInitString(type: string, name: string): string { + return evaluationString(addPageBinding, type, name); +} + +/** + * @internal + */ +export async function waitWithTimeout<T>( + promise: Promise<T>, + taskName: string, + timeout: number +): Promise<T> { + let reject: (reason?: Error) => void; + const timeoutError = new TimeoutError( + `waiting for ${taskName} failed: timeout ${timeout}ms exceeded` + ); + const timeoutPromise = new Promise<never>((_, rej) => { + return (reject = rej); + }); + let timeoutTimer = null; + if (timeout) { + timeoutTimer = setTimeout(() => { + return reject(timeoutError); + }, timeout); + } + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timeoutTimer) { + clearTimeout(timeoutTimer); + } + } +} + +/** + * @internal + */ +let fs: typeof import('fs/promises') | null = null; +/** + * @internal + */ +export async function importFSPromises(): Promise< + typeof import('fs/promises') +> { + if (!fs) { + try { + fs = await import('fs/promises'); + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Cannot write to a path outside of a Node-like environment.' + ); + } + throw error; + } + } + return fs; +} + +/** + * @internal + */ +export async function getReadableAsBuffer( + readable: Readable, + path?: string +): Promise<Buffer | null> { + const buffers = []; + if (path) { + const fs = await importFSPromises(); + const fileHandle = await fs.open(path, 'w+'); + try { + for await (const chunk of readable) { + buffers.push(chunk); + await fileHandle.writeFile(chunk); + } + } finally { + await fileHandle.close(); + } + } else { + for await (const chunk of readable) { + buffers.push(chunk); + } + } + try { + return Buffer.concat(buffers); + } catch (error) { + return null; + } +} + +/** + * @internal + */ +export async function getReadableFromProtocolStream( + client: CDPSession, + handle: string +): Promise<Readable> { + // TODO: Once Node 18 becomes the lowest supported version, we can migrate to + // ReadableStream. + if (!isNode) { + throw new Error('Cannot create a stream outside of Node.js environment.'); + } + + const {Readable} = await import('stream'); + + let eof = false; + return new Readable({ + async read(size: number) { + if (eof) { + return; + } + + try { + const response = await client.send('IO.read', {handle, size}); + this.push(response.data, response.base64Encoded ? 'base64' : undefined); + if (response.eof) { + eof = true; + await client.send('IO.close', {handle}); + this.push(null); + } + } catch (error) { + if (isErrorLike(error)) { + this.destroy(error); + return; + } + throw error; + } + }, + }); +} + +/** + * @internal + */ +export async function setPageContent( + page: Pick<Page, 'evaluate'>, + content: string +): Promise<void> { + // We rely upon the fact that document.open() will reset frame lifecycle with "init" + // lifecycle event. @see https://crrev.com/608658 + return page.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, content); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts b/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts new file mode 100644 index 0000000000..80258c67fe --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/environment.ts @@ -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. + */ + +/** + * @internal + */ +export const isNode = !!(typeof process !== 'undefined' && process.version); + +/** + * @internal + */ +export const DEFERRED_PROMISE_DEBUG_TIMEOUT = + typeof process !== 'undefined' && + typeof process.env['PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT'] !== 'undefined' + ? Number(process.env['PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT']) + : -1; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/generated/injected.ts b/remote/test/puppeteer/packages/puppeteer-core/src/generated/injected.ts new file mode 100644 index 0000000000..1a3afff23b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/generated/injected.ts @@ -0,0 +1,8 @@ +/** + * JavaScript code that provides the puppeteer utilities. See the + * [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md) + * for injection for more information. + * + * @internal + */ +export const source = "\"use strict\";var C=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var oe=Object.getOwnPropertyNames;var se=Object.prototype.hasOwnProperty;var u=(e,t)=>{for(var n in t)C(e,n,{get:t[n],enumerable:!0})},ie=(e,t,n,r)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let o of oe(t))!se.call(e,o)&&o!==n&&C(e,o,{get:()=>t[o],enumerable:!(r=ne(t,o))||r.enumerable});return e};var le=e=>ie(C({},\"__esModule\",{value:!0}),e);var Oe={};u(Oe,{default:()=>Re});module.exports=le(Oe);var P=class extends Error{constructor(t){super(t),this.name=this.constructor.name,Error.captureStackTrace(this,this.constructor)}},S=class extends P{},I=class extends P{#e;#r=\"\";set code(t){this.#e=t}get code(){return this.#e}set originalMessage(t){this.#r=t}get originalMessage(){return this.#r}},De=Object.freeze({TimeoutError:S,ProtocolError:I});function p(e){let t=!1,n=!1,r,o,i=new Promise((l,a)=>{r=l,o=a}),s=e&&e.timeout>0?setTimeout(()=>{n=!0,o(new S(e.message))},e.timeout):void 0;return Object.assign(i,{resolved:()=>t,finished:()=>t||n,resolve:l=>{s&&clearTimeout(s),t=!0,r(l)},reject:l=>{clearTimeout(s),n=!0,o(l)}})}var G=new Map,X=e=>{let t=G.get(e);return t||(t=new Function(`return ${e}`)(),G.set(e,t),t)};var R={};u(R,{ariaQuerySelector:()=>ae,ariaQuerySelectorAll:()=>k});var ae=(e,t)=>window.__ariaQuerySelector(e,t),k=async function*(e,t){yield*await window.__ariaQuerySelectorAll(e,t)};var D={};u(D,{customQuerySelectors:()=>_});var O=class{#e=new Map;register(t,n){if(!n.queryOne&&n.queryAll){let r=n.queryAll;n.queryOne=(o,i)=>{for(let s of r(o,i))return s;return null}}else if(n.queryOne&&!n.queryAll){let r=n.queryOne;n.queryAll=(o,i)=>{let s=r(o,i);return s?[s]:[]}}else if(!n.queryOne||!n.queryAll)throw new Error(\"At least one query method must be defined.\");this.#e.set(t,{querySelector:n.queryOne,querySelectorAll:n.queryAll})}unregister(t){this.#e.delete(t)}get(t){return this.#e.get(t)}clear(){this.#e.clear()}},_=new O;var M={};u(M,{pierceQuerySelector:()=>ce,pierceQuerySelectorAll:()=>ue});var ce=(e,t)=>{let n=null,r=o=>{let i=document.createTreeWalker(o,NodeFilter.SHOW_ELEMENT);do{let s=i.currentNode;s.shadowRoot&&r(s.shadowRoot),!(s instanceof ShadowRoot)&&s!==o&&!n&&s.matches(t)&&(n=s)}while(!n&&i.nextNode())};return e instanceof Document&&(e=e.documentElement),r(e),n},ue=(e,t)=>{let n=[],r=o=>{let i=document.createTreeWalker(o,NodeFilter.SHOW_ELEMENT);do{let s=i.currentNode;s.shadowRoot&&r(s.shadowRoot),!(s instanceof ShadowRoot)&&s!==o&&s.matches(t)&&n.push(s)}while(i.nextNode())};return e instanceof Document&&(e=e.documentElement),r(e),n};var m=(e,t)=>{if(!e)throw new Error(t)};var T=class{#e;#r;#n;#t;constructor(t,n){this.#e=t,this.#r=n}async start(){let t=this.#t=p(),n=await this.#e();if(n){t.resolve(n);return}this.#n=new MutationObserver(async()=>{let r=await this.#e();r&&(t.resolve(r),await this.stop())}),this.#n.observe(this.#r,{childList:!0,subtree:!0,attributes:!0})}async stop(){m(this.#t,\"Polling never started.\"),this.#t.finished()||this.#t.reject(new Error(\"Polling stopped\")),this.#n&&(this.#n.disconnect(),this.#n=void 0)}result(){return m(this.#t,\"Polling never started.\"),this.#t}},x=class{#e;#r;constructor(t){this.#e=t}async start(){let t=this.#r=p(),n=await this.#e();if(n){t.resolve(n);return}let r=async()=>{if(t.finished())return;let o=await this.#e();if(!o){window.requestAnimationFrame(r);return}t.resolve(o),await this.stop()};window.requestAnimationFrame(r)}async stop(){m(this.#r,\"Polling never started.\"),this.#r.finished()||this.#r.reject(new Error(\"Polling stopped\"))}result(){return m(this.#r,\"Polling never started.\"),this.#r}},E=class{#e;#r;#n;#t;constructor(t,n){this.#e=t,this.#r=n}async start(){let t=this.#t=p(),n=await this.#e();if(n){t.resolve(n);return}this.#n=setInterval(async()=>{let r=await this.#e();r&&(t.resolve(r),await this.stop())},this.#r)}async stop(){m(this.#t,\"Polling never started.\"),this.#t.finished()||this.#t.reject(new Error(\"Polling stopped\")),this.#n&&(clearInterval(this.#n),this.#n=void 0)}result(){return m(this.#t,\"Polling never started.\"),this.#t}};var H={};u(H,{pQuerySelector:()=>Ie,pQuerySelectorAll:()=>re});var c=class{static async*map(t,n){for await(let r of t)yield await n(r)}static async*flatMap(t,n){for await(let r of t)yield*n(r)}static async collect(t){let n=[];for await(let r of t)n.push(r);return n}static async first(t){for await(let n of t)return n}};var h={attribute:/\\[\\s*(?:(?<namespace>\\*|[-\\w\\P{ASCII}]*)\\|)?(?<name>[-\\w\\P{ASCII}]+)\\s*(?:(?<operator>\\W?=)\\s*(?<value>.+?)\\s*(\\s(?<caseSensitive>[iIsS]))?\\s*)?\\]/gu,id:/#(?<name>[-\\w\\P{ASCII}]+)/gu,class:/\\.(?<name>[-\\w\\P{ASCII}]+)/gu,comma:/\\s*,\\s*/g,combinator:/\\s*[\\s>+~]\\s*/g,\"pseudo-element\":/::(?<name>[-\\w\\P{ASCII}]+)(?:\\((?<argument>¶+)\\))?/gu,\"pseudo-class\":/:(?<name>[-\\w\\P{ASCII}]+)(?:\\((?<argument>¶+)\\))?/gu,universal:/(?:(?<namespace>\\*|[-\\w\\P{ASCII}]*)\\|)?\\*/gu,type:/(?:(?<namespace>\\*|[-\\w\\P{ASCII}]*)\\|)?(?<name>[-\\w\\P{ASCII}]+)/gu},fe=new Set([\"combinator\",\"comma\"]);var me=e=>{switch(e){case\"pseudo-element\":case\"pseudo-class\":return new RegExp(h[e].source.replace(\"(?<argument>\\xB6+)\",\"(?<argument>.+)\"),\"gu\");default:return h[e]}};function de(e,t){let n=0,r=\"\";for(;t<e.length;t++){let o=e[t];switch(o){case\"(\":++n;break;case\")\":--n;break}if(r+=o,n===0)return r}return r}function pe(e,t=h){if(!e)return[];let n=[e];for(let[o,i]of Object.entries(t))for(let s=0;s<n.length;s++){let l=n[s];if(typeof l!=\"string\")continue;i.lastIndex=0;let a=i.exec(l);if(!a)continue;let d=a.index-1,f=[],V=a[0],B=l.slice(0,d+1);B&&f.push(B),f.push({...a.groups,type:o,content:V});let z=l.slice(d+V.length+1);z&&f.push(z),n.splice(s,1,...f)}let r=0;for(let o of n)switch(typeof o){case\"string\":throw new Error(`Unexpected sequence ${o} found at index ${r}`);case\"object\":r+=o.content.length,o.pos=[r-o.content.length,r],fe.has(o.type)&&(o.content=o.content.trim()||\" \");break}return n}var he=/(['\"])([^\\\\\\n]+?)\\1/g,ge=/\\\\./g;function K(e,t=h){if(e=e.trim(),e===\"\")return[];let n=[];e=e.replace(ge,(i,s)=>(n.push({value:i,offset:s}),\"\\uE000\".repeat(i.length))),e=e.replace(he,(i,s,l,a)=>(n.push({value:i,offset:a}),`${s}${\"\\uE001\".repeat(l.length)}${s}`));{let i=0,s;for(;(s=e.indexOf(\"(\",i))>-1;){let l=de(e,s);n.push({value:l,offset:s}),e=`${e.substring(0,s)}(${\"\\xB6\".repeat(l.length-2)})${e.substring(s+l.length)}`,i=s+l.length}}let r=pe(e,t),o=new Set;for(let i of n.reverse())for(let s of r){let{offset:l,value:a}=i;if(!(s.pos[0]<=l&&l+a.length<=s.pos[1]))continue;let{content:d}=s,f=l-s.pos[0];s.content=d.slice(0,f)+a+d.slice(f+a.length),s.content!==d&&o.add(s)}for(let i of o){let s=me(i.type);if(!s)throw new Error(`Unknown token type: ${i.type}`);s.lastIndex=0;let l=s.exec(i.content);if(!l)throw new Error(`Unable to parse content for ${i.type}: ${i.content}`);Object.assign(i,l.groups)}return r}function*N(e,t){switch(e.type){case\"list\":for(let n of e.list)yield*N(n,e);break;case\"complex\":yield*N(e.left,e),yield*N(e.right,e);break;case\"compound\":yield*e.list.map(n=>[n,e]);break;default:yield[e,t]}}function g(e){let t;return Array.isArray(e)?t=e:t=[...N(e)].map(([n])=>n),t.map(n=>n.content).join(\"\")}h.combinator=/\\s*(>>>>?|[\\s>+~])\\s*/g;var ye=/\\\\[\\s\\S]/g,we=e=>{if(e.length>1){for(let t of['\"',\"'\"])if(!(!e.startsWith(t)||!e.endsWith(t)))return e.slice(t.length,-t.length).replace(ye,n=>n.slice(1))}return e};function Y(e){let t=!0,n=K(e);if(n.length===0)return[[],t];let r=[],o=[r],i=[o],s=[];for(let l of n){switch(l.type){case\"combinator\":switch(l.content){case\">>>\":t=!1,s.length&&(r.push(g(s)),s.splice(0)),r=[],o.push(\">>>\"),o.push(r);continue;case\">>>>\":t=!1,s.length&&(r.push(g(s)),s.splice(0)),r=[],o.push(\">>>>\"),o.push(r);continue}break;case\"pseudo-element\":if(!l.name.startsWith(\"-p-\"))break;t=!1,s.length&&(r.push(g(s)),s.splice(0)),r.push({name:l.name.slice(3),value:we(l.argument??\"\")});continue;case\"comma\":s.length&&(r.push(g(s)),s.splice(0)),r=[],o=[r],i.push(o);continue}s.push(l)}return s.length&&r.push(g(s)),[i,t]}var Q={};u(Q,{textQuerySelectorAll:()=>b});var Se=new Set([\"checkbox\",\"image\",\"radio\"]),be=e=>e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement||e instanceof HTMLInputElement&&!Se.has(e.type),Pe=new Set([\"SCRIPT\",\"STYLE\"]),w=e=>!Pe.has(e.nodeName)&&!document.head?.contains(e),q=new WeakMap,Z=e=>{for(;e;)q.delete(e),e instanceof ShadowRoot?e=e.host:e=e.parentNode},J=new WeakSet,Te=new MutationObserver(e=>{for(let t of e)Z(t.target)}),y=e=>{let t=q.get(e);if(t||(t={full:\"\",immediate:[]},!w(e)))return t;let n=\"\";if(be(e))t.full=e.value,t.immediate.push(e.value),e.addEventListener(\"input\",r=>{Z(r.target)},{once:!0,capture:!0});else{for(let r=e.firstChild;r;r=r.nextSibling){if(r.nodeType===Node.TEXT_NODE){t.full+=r.nodeValue??\"\",n+=r.nodeValue??\"\";continue}n&&t.immediate.push(n),n=\"\",r.nodeType===Node.ELEMENT_NODE&&(t.full+=y(r).full)}n&&t.immediate.push(n),e instanceof Element&&e.shadowRoot&&(t.full+=y(e.shadowRoot).full),J.has(e)||(Te.observe(e,{childList:!0,characterData:!0}),J.add(e))}return q.set(e,t),t};var b=function*(e,t){let n=!1;for(let r of e.childNodes)if(r instanceof Element&&w(r)){let o;r.shadowRoot?o=b(r.shadowRoot,t):o=b(r,t);for(let i of o)yield i,n=!0}n||e instanceof Element&&w(e)&&y(e).full.includes(t)&&(yield e)};var $={};u($,{checkVisibility:()=>Ee,pierce:()=>A,pierceAll:()=>L});var xe=[\"hidden\",\"collapse\"],Ee=(e,t)=>{if(!e)return t===!1;if(t===void 0)return e;let n=e.nodeType===Node.TEXT_NODE?e.parentElement:e,r=window.getComputedStyle(n),o=r&&!xe.includes(r.visibility)&&!Ne(n);return t===o?e:!1};function Ne(e){let t=e.getBoundingClientRect();return t.width===0||t.height===0}var Ae=e=>\"shadowRoot\"in e&&e.shadowRoot instanceof ShadowRoot;function*A(e){Ae(e)?yield e.shadowRoot:yield e}function*L(e){e=A(e).next().value,yield e;let t=[document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT)];for(let n of t){let r;for(;r=n.nextNode();)r.shadowRoot&&(yield r.shadowRoot,t.push(document.createTreeWalker(r.shadowRoot,NodeFilter.SHOW_ELEMENT)))}}var U={};u(U,{xpathQuerySelectorAll:()=>j});var j=function*(e,t){let r=(e.ownerDocument||document).evaluate(t,e,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),o;for(;o=r.iterateNext();)yield o};var ve=/[-\\w\\P{ASCII}*]/,ee=e=>\"querySelectorAll\"in e,v=class extends Error{constructor(t,n){super(`${t} is not a valid selector: ${n}`)}},F=class{#e;#r;#n=[];#t=void 0;elements;constructor(t,n,r){this.elements=[t],this.#e=n,this.#r=r,this.#o()}async run(){if(typeof this.#t==\"string\")switch(this.#t.trimStart()){case\":scope\":this.#o();break}for(;this.#t!==void 0;this.#o()){let t=this.#t,n=this.#e;typeof t==\"string\"?t[0]&&ve.test(t[0])?this.elements=c.flatMap(this.elements,async function*(r){ee(r)&&(yield*r.querySelectorAll(t))}):this.elements=c.flatMap(this.elements,async function*(r){if(!r.parentElement){if(!ee(r))return;yield*r.querySelectorAll(t);return}let o=0;for(let i of r.parentElement.children)if(++o,i===r)break;yield*r.parentElement.querySelectorAll(`:scope>:nth-child(${o})${t}`)}):this.elements=c.flatMap(this.elements,async function*(r){switch(t.name){case\"text\":yield*b(r,t.value);break;case\"xpath\":yield*j(r,t.value);break;case\"aria\":yield*k(r,t.value);break;default:let o=_.get(t.name);if(!o)throw new v(n,`Unknown selector type: ${t.name}`);yield*o.querySelectorAll(r,t.value)}})}}#o(){if(this.#n.length!==0){this.#t=this.#n.shift();return}if(this.#r.length===0){this.#t=void 0;return}let t=this.#r.shift();switch(t){case\">>>>\":{this.elements=c.flatMap(this.elements,A),this.#o();break}case\">>>\":{this.elements=c.flatMap(this.elements,L),this.#o();break}default:this.#n=t,this.#o();break}}},W=class{#e=new WeakMap;calculate(t,n=[]){if(t===null)return n;t instanceof ShadowRoot&&(t=t.host);let r=this.#e.get(t);if(r)return[...r,...n];let o=0;for(let s=t.previousSibling;s;s=s.previousSibling)++o;let i=this.calculate(t.parentNode,[o]);return this.#e.set(t,i),[...i,...n]}},te=(e,t)=>{if(e.length+t.length===0)return 0;let[n=-1,...r]=e,[o=-1,...i]=t;return n===o?te(r,i):n<o?-1:1},Ce=async function*(e){let t=new Set;for await(let r of e)t.add(r);let n=new W;yield*[...t.values()].map(r=>[r,n.calculate(r)]).sort(([,r],[,o])=>te(r,o)).map(([r])=>r)},re=function(e,t){let n,r;try{[n,r]=Y(t)}catch{return e.querySelectorAll(t)}if(r)return e.querySelectorAll(t);if(n.some(o=>{let i=0;return o.some(s=>(typeof s==\"string\"?++i:i=0,i>1))}))throw new v(t,\"Multiple deep combinators found in sequence.\");return Ce(c.flatMap(n,o=>{let i=new F(e,t,o);return i.run(),i.elements}))},Ie=async function(e,t){for await(let n of re(e,t))return n;return null};var ke=Object.freeze({...R,...D,...M,...H,...Q,...$,...U,createDeferredPromise:p,createFunction:X,createTextContent:y,IntervalPoller:E,isSuitableNodeForTextMatching:w,MutationPoller:T,RAFPoller:x}),Re=ke;\n"; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/generated/version.ts b/remote/test/puppeteer/packages/puppeteer-core/src/generated/version.ts new file mode 100644 index 0000000000..a2c4aa3ba7 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/generated/version.ts @@ -0,0 +1,4 @@ +/** + * @internal + */ +export const packageVersion = '20.1.0'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts new file mode 100644 index 0000000000..90168e2dd2 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/ARIAQuerySelector.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare global { + interface Window { + /** + * @internal + */ + __ariaQuerySelector(root: Node, selector: string): Promise<Node | null>; + /** + * @internal + */ + __ariaQuerySelectorAll(root: Node, selector: string): Promise<Node[]>; + } +} + +export const ariaQuerySelector = ( + root: Node, + selector: string +): Promise<Node | null> => { + return window.__ariaQuerySelector(root, selector); +}; +export const ariaQuerySelectorAll = async function* ( + root: Node, + selector: string +): AsyncIterable<Node> { + yield* await window.__ariaQuerySelectorAll(root, selector); +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts new file mode 100644 index 0000000000..41bb1a8408 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/CustomQuerySelector.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {CustomQueryHandler} from '../common/CustomQueryHandler.js'; +import type {Awaitable, AwaitableIterable} from '../common/types.js'; + +export interface CustomQuerySelector { + querySelector(root: Node, selector: string): Awaitable<Node | null>; + querySelectorAll(root: Node, selector: string): AwaitableIterable<Node>; +} + +/** + * This class mimics the injected {@link CustomQuerySelectorRegistry}. + */ +class CustomQuerySelectorRegistry { + #selectors = new Map<string, CustomQuerySelector>(); + + register(name: string, handler: CustomQueryHandler): void { + if (!handler.queryOne && handler.queryAll) { + const querySelectorAll = handler.queryAll; + handler.queryOne = (node, selector) => { + for (const result of querySelectorAll(node, selector)) { + return result; + } + return null; + }; + } else if (handler.queryOne && !handler.queryAll) { + const querySelector = handler.queryOne; + handler.queryAll = (node, selector) => { + const result = querySelector(node, selector); + return result ? [result] : []; + }; + } else if (!handler.queryOne || !handler.queryAll) { + throw new Error('At least one query method must be defined.'); + } + + this.#selectors.set(name, { + querySelector: handler.queryOne, + querySelectorAll: handler.queryAll!, + }); + } + + unregister(name: string): void { + this.#selectors.delete(name); + } + + get(name: string): CustomQuerySelector | undefined { + return this.#selectors.get(name); + } + + clear() { + this.#selectors.clear(); + } +} + +export const customQuerySelectors = new CustomQuerySelectorRegistry(); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts new file mode 100644 index 0000000000..f345442f5a --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PQuerySelector.ts @@ -0,0 +1,308 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type {AwaitableIterable} from '../common/types.js'; +import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; + +import {ariaQuerySelectorAll} from './ARIAQuerySelector.js'; +import {customQuerySelectors} from './CustomQuerySelector.js'; +import { + ComplexPSelector, + ComplexPSelectorList, + CompoundPSelector, + CSSSelector, + parsePSelectors, + PCombinator, + PPseudoSelector, +} from './PSelectorParser.js'; +import {textQuerySelectorAll} from './TextQuerySelector.js'; +import {pierce, pierceAll} from './util.js'; +import {xpathQuerySelectorAll} from './XPathQuerySelector.js'; + +const IDENT_TOKEN_START = /[-\w\P{ASCII}*]/; + +interface QueryableNode extends Node { + querySelectorAll: typeof Document.prototype.querySelectorAll; +} + +const isQueryableNode = (node: Node): node is QueryableNode => { + return 'querySelectorAll' in node; +}; + +class SelectorError extends Error { + constructor(selector: string, message: string) { + super(`${selector} is not a valid selector: ${message}`); + } +} + +class PQueryEngine { + #input: string; + + #complexSelector: ComplexPSelector; + #compoundSelector: CompoundPSelector = []; + #selector: CSSSelector | PPseudoSelector | undefined = undefined; + + elements: AwaitableIterable<Node>; + + constructor(element: Node, input: string, complexSelector: ComplexPSelector) { + this.elements = [element]; + this.#input = input; + this.#complexSelector = complexSelector; + this.#next(); + } + + async run(): Promise<void> { + if (typeof this.#selector === 'string') { + switch (this.#selector.trimStart()) { + case ':scope': + // `:scope` has some special behavior depending on the node. It always + // represents the current node within a compound selector, but by + // itself, it depends on the node. For example, Document is + // represented by `<html>`, but any HTMLElement is not represented by + // itself (i.e. `null`). This can be troublesome if our combinators + // are used right after so we treat this selector specially. + this.#next(); + break; + } + } + + for (; this.#selector !== undefined; this.#next()) { + const selector = this.#selector; + const input = this.#input; + if (typeof selector === 'string') { + // The regular expression tests if the selector is a type/universal + // selector. Any other case means we want to apply the selector onto + // the element itself (e.g. `element.class`, `element>div`, + // `element:hover`, etc.). + if (selector[0] && IDENT_TOKEN_START.test(selector[0])) { + this.elements = AsyncIterableUtil.flatMap( + this.elements, + async function* (element) { + if (isQueryableNode(element)) { + yield* element.querySelectorAll(selector); + } + } + ); + } else { + this.elements = AsyncIterableUtil.flatMap( + this.elements, + async function* (element) { + if (!element.parentElement) { + if (!isQueryableNode(element)) { + return; + } + yield* element.querySelectorAll(selector); + return; + } + + let index = 0; + for (const child of element.parentElement.children) { + ++index; + if (child === element) { + break; + } + } + yield* element.parentElement.querySelectorAll( + `:scope>:nth-child(${index})${selector}` + ); + } + ); + } + } else { + this.elements = AsyncIterableUtil.flatMap( + this.elements, + async function* (element) { + switch (selector.name) { + case 'text': + yield* textQuerySelectorAll(element, selector.value); + break; + case 'xpath': + yield* xpathQuerySelectorAll(element, selector.value); + break; + case 'aria': + yield* ariaQuerySelectorAll(element, selector.value); + break; + default: + const querySelector = customQuerySelectors.get(selector.name); + if (!querySelector) { + throw new SelectorError( + input, + `Unknown selector type: ${selector.name}` + ); + } + yield* querySelector.querySelectorAll(element, selector.value); + } + } + ); + } + } + } + + #next() { + if (this.#compoundSelector.length !== 0) { + this.#selector = this.#compoundSelector.shift(); + return; + } + if (this.#complexSelector.length === 0) { + this.#selector = undefined; + return; + } + const selector = this.#complexSelector.shift(); + switch (selector) { + case PCombinator.Child: { + this.elements = AsyncIterableUtil.flatMap(this.elements, pierce); + this.#next(); + break; + } + case PCombinator.Descendent: { + this.elements = AsyncIterableUtil.flatMap(this.elements, pierceAll); + this.#next(); + break; + } + default: + this.#compoundSelector = selector as CompoundPSelector; + this.#next(); + break; + } + } +} + +class DepthCalculator { + #cache = new WeakMap<Node, number[]>(); + + calculate(node: Node | null, depth: number[] = []): number[] { + if (node === null) { + return depth; + } + if (node instanceof ShadowRoot) { + node = node.host; + } + + const cachedDepth = this.#cache.get(node); + if (cachedDepth) { + return [...cachedDepth, ...depth]; + } + + let index = 0; + for ( + let prevSibling = node.previousSibling; + prevSibling; + prevSibling = prevSibling.previousSibling + ) { + ++index; + } + + const value = this.calculate(node.parentNode, [index]); + this.#cache.set(node, value); + return [...value, ...depth]; + } +} + +const compareDepths = (a: number[], b: number[]): -1 | 0 | 1 => { + if (a.length + b.length === 0) { + return 0; + } + const [i = -1, ...otherA] = a; + const [j = -1, ...otherB] = b; + if (i === j) { + return compareDepths(otherA, otherB); + } + return i < j ? -1 : 1; +}; + +const domSort = async function* (elements: AwaitableIterable<Node>) { + const results = new Set<Node>(); + for await (const element of elements) { + results.add(element); + } + const calculator = new DepthCalculator(); + yield* [...results.values()] + .map(result => { + return [result, calculator.calculate(result)] as const; + }) + .sort(([, a], [, b]) => { + return compareDepths(a, b); + }) + .map(([result]) => { + return result; + }); +}; + +/** + * Queries the given node for all nodes matching the given text selector. + * + * @internal + */ +export const pQuerySelectorAll = function ( + root: Node, + selector: string +): AwaitableIterable<Node> { + let selectors: ComplexPSelectorList; + let isPureCSS: boolean; + try { + [selectors, isPureCSS] = parsePSelectors(selector); + } catch (error) { + return (root as unknown as QueryableNode).querySelectorAll(selector); + } + + if (isPureCSS) { + return (root as unknown as QueryableNode).querySelectorAll(selector); + } + // If there are any empty elements, then this implies the selector has + // contiguous combinators (e.g. `>>> >>>>`) or starts/ends with one which we + // treat as illegal, similar to existing behavior. + if ( + selectors.some(parts => { + let i = 0; + return parts.some(parts => { + if (typeof parts === 'string') { + ++i; + } else { + i = 0; + } + return i > 1; + }); + }) + ) { + throw new SelectorError( + selector, + 'Multiple deep combinators found in sequence.' + ); + } + + return domSort( + AsyncIterableUtil.flatMap(selectors, selectorParts => { + const query = new PQueryEngine(root, selector, selectorParts); + void query.run(); + return query.elements; + }) + ); +}; + +/** + * Queries the given node for all nodes matching the given text selector. + * + * @internal + */ +export const pQuerySelector = async function ( + root: Node, + selector: string +): Promise<Node | null> { + for await (const element of pQuerySelectorAll(root, selector)) { + return element; + } + return null; +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts new file mode 100644 index 0000000000..19bb9e3000 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PSelectorParser.ts @@ -0,0 +1,119 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Token, tokenize, TOKENS, stringify} from 'parsel-js'; + +export type CSSSelector = string; +export type PPseudoSelector = { + name: string; + value: string; +}; +export const enum PCombinator { + Descendent = '>>>', + Child = '>>>>', +} +export type CompoundPSelector = Array<CSSSelector | PPseudoSelector>; +export type ComplexPSelector = Array<CompoundPSelector | PCombinator>; +export type ComplexPSelectorList = ComplexPSelector[]; + +TOKENS['combinator'] = /\s*(>>>>?|[\s>+~])\s*/g; + +const ESCAPE_REGEXP = /\\[\s\S]/g; +const unquote = (text: string): string => { + if (text.length > 1) { + for (const char of ['"', "'"]) { + if (!text.startsWith(char) || !text.endsWith(char)) { + continue; + } + return text + .slice(char.length, -char.length) + .replace(ESCAPE_REGEXP, match => { + return match.slice(1); + }); + } + } + return text; +}; + +export function parsePSelectors( + selector: string +): [selector: ComplexPSelectorList, isPureCSS: boolean] { + let isPureCSS = true; + const tokens = tokenize(selector); + if (tokens.length === 0) { + return [[], isPureCSS]; + } + let compoundSelector: CompoundPSelector = []; + let complexSelector: ComplexPSelector = [compoundSelector]; + const selectors: ComplexPSelectorList = [complexSelector]; + const storage: Token[] = []; + for (const token of tokens) { + switch (token.type) { + case 'combinator': + switch (token.content) { + case PCombinator.Descendent: + isPureCSS = false; + if (storage.length) { + compoundSelector.push(stringify(storage)); + storage.splice(0); + } + compoundSelector = []; + complexSelector.push(PCombinator.Descendent); + complexSelector.push(compoundSelector); + continue; + case PCombinator.Child: + isPureCSS = false; + if (storage.length) { + compoundSelector.push(stringify(storage)); + storage.splice(0); + } + compoundSelector = []; + complexSelector.push(PCombinator.Child); + complexSelector.push(compoundSelector); + continue; + } + break; + case 'pseudo-element': + if (!token.name.startsWith('-p-')) { + break; + } + isPureCSS = false; + if (storage.length) { + compoundSelector.push(stringify(storage)); + storage.splice(0); + } + compoundSelector.push({ + name: token.name.slice(3), + value: unquote(token.argument ?? ''), + }); + continue; + case 'comma': + if (storage.length) { + compoundSelector.push(stringify(storage)); + storage.splice(0); + } + compoundSelector = []; + complexSelector = [compoundSelector]; + selectors.push(complexSelector); + continue; + } + storage.push(token); + } + if (storage.length) { + compoundSelector.push(stringify(storage)); + } + return [selectors, isPureCSS]; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts new file mode 100644 index 0000000000..d72d8913bb --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/PierceQuerySelector.ts @@ -0,0 +1,73 @@ +// Copyright 2022 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @internal + */ +export const pierceQuerySelector = ( + root: Node, + selector: string +): Element | null => { + let found: Node | null = null; + const search = (root: Node) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as Element; + if (currentNode.shadowRoot) { + search(currentNode.shadowRoot); + } + if (currentNode instanceof ShadowRoot) { + continue; + } + if (currentNode !== root && !found && currentNode.matches(selector)) { + found = currentNode; + } + } while (!found && iter.nextNode()); + }; + if (root instanceof Document) { + root = root.documentElement; + } + search(root); + return found; +}; + +/** + * @internal + */ +export const pierceQuerySelectorAll = ( + element: Node, + selector: string +): Element[] => { + const result: Element[] = []; + const collect = (root: Node) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as Element; + if (currentNode.shadowRoot) { + collect(currentNode.shadowRoot); + } + if (currentNode instanceof ShadowRoot) { + continue; + } + if (currentNode !== root && currentNode.matches(selector)) { + result.push(currentNode); + } + } while (iter.nextNode()); + }; + if (element instanceof Document) { + element = element.documentElement; + } + collect(element); + return result; +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts new file mode 100644 index 0000000000..d7f4eb398b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/Poller.ts @@ -0,0 +1,181 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; + +/** + * @internal + */ +export interface Poller<T> { + start(): Promise<void>; + stop(): Promise<void>; + result(): Promise<T>; +} + +/** + * @internal + */ +export class MutationPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + + #root: Node; + + #observer?: MutationObserver; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>, root: Node) { + this.#fn = fn; + this.#root = root; + } + + async start(): Promise<void> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return; + } + + this.#observer = new MutationObserver(async () => { + const result = await this.#fn(); + if (!result) { + return; + } + promise.resolve(result); + await this.stop(); + }); + this.#observer.observe(this.#root, { + childList: true, + subtree: true, + attributes: true, + }); + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + if (this.#observer) { + this.#observer.disconnect(); + this.#observer = undefined; + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} + +/** + * @internal + */ +export class RAFPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>) { + this.#fn = fn; + } + + async start(): Promise<void> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return; + } + + const poll = async () => { + if (promise.finished()) { + return; + } + const result = await this.#fn(); + if (!result) { + window.requestAnimationFrame(poll); + return; + } + promise.resolve(result); + await this.stop(); + }; + window.requestAnimationFrame(poll); + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} + +/** + * @internal + */ + +export class IntervalPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + #ms: number; + + #interval?: NodeJS.Timer; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>, ms: number) { + this.#fn = fn; + this.#ms = ms; + } + + async start(): Promise<void> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return; + } + + this.#interval = setInterval(async () => { + const result = await this.#fn(); + if (!result) { + return; + } + promise.resolve(result); + await this.stop(); + }, this.#ms); + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + if (this.#interval) { + clearInterval(this.#interval); + this.#interval = undefined; + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts new file mode 100644 index 0000000000..8c09bbc6d5 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts @@ -0,0 +1,155 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface NonTrivialValueNode extends Node { + value: string; +} + +const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']); + +/** + * Determines if the node has a non-trivial value property. + * + * @internal + */ +const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => { + if (node instanceof HTMLSelectElement) { + return true; + } + if (node instanceof HTMLTextAreaElement) { + return true; + } + if ( + node instanceof HTMLInputElement && + !TRIVIAL_VALUE_INPUT_TYPES.has(node.type) + ) { + return true; + } + return false; +}; + +const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']); + +/** + * Determines whether a given node is suitable for text matching. + * + * @internal + */ +export const isSuitableNodeForTextMatching = (node: Node): boolean => { + return ( + !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node) + ); +}; + +/** + * @internal + */ +export type TextContent = { + // Contains the full text of the node. + full: string; + // Contains the text immediately beneath the node. + immediate: string[]; +}; + +/** + * Maps {@link Node}s to their computed {@link TextContent}. + */ +const textContentCache = new WeakMap<Node, TextContent>(); +const eraseFromCache = (node: Node | null) => { + while (node) { + textContentCache.delete(node); + if (node instanceof ShadowRoot) { + node = node.host; + } else { + node = node.parentNode; + } + } +}; + +/** + * Erases the cache when the tree has mutated text. + */ +const observedNodes = new WeakSet<Node>(); +const textChangeObserver = new MutationObserver(mutations => { + for (const mutation of mutations) { + eraseFromCache(mutation.target); + } +}); + +/** + * Builds the text content of a node using some custom logic. + * + * @remarks + * The primary reason this function exists is due to {@link ShadowRoot}s not having + * text content. + * + * @internal + */ +export const createTextContent = (root: Node): TextContent => { + let value = textContentCache.get(root); + if (value) { + return value; + } + value = {full: '', immediate: []}; + if (!isSuitableNodeForTextMatching(root)) { + return value; + } + + let currentImmediate = ''; + if (isNonTrivialValueNode(root)) { + value.full = root.value; + value.immediate.push(root.value); + + root.addEventListener( + 'input', + event => { + eraseFromCache(event.target as HTMLInputElement); + }, + {once: true, capture: true} + ); + } else { + for (let child = root.firstChild; child; child = child.nextSibling) { + if (child.nodeType === Node.TEXT_NODE) { + value.full += child.nodeValue ?? ''; + currentImmediate += child.nodeValue ?? ''; + continue; + } + if (currentImmediate) { + value.immediate.push(currentImmediate); + } + currentImmediate = ''; + if (child.nodeType === Node.ELEMENT_NODE) { + value.full += createTextContent(child).full; + } + } + if (currentImmediate) { + value.immediate.push(currentImmediate); + } + if (root instanceof Element && root.shadowRoot) { + value.full += createTextContent(root.shadowRoot).full; + } + + if (!observedNodes.has(root)) { + textChangeObserver.observe(root, { + childList: true, + characterData: true, + }); + observedNodes.add(root); + } + } + textContentCache.set(root, value); + return value; +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts new file mode 100644 index 0000000000..eebd59f675 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextQuerySelector.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createTextContent, + isSuitableNodeForTextMatching, +} from './TextContent.js'; + +/** + * Queries the given node for all nodes matching the given text selector. + * + * @internal + */ +export const textQuerySelectorAll = function* ( + root: Node, + selector: string +): Generator<Element> { + let yielded = false; + for (const node of root.childNodes) { + if (node instanceof Element && isSuitableNodeForTextMatching(node)) { + let matches: Generator<Element, boolean>; + if (!node.shadowRoot) { + matches = textQuerySelectorAll(node, selector); + } else { + matches = textQuerySelectorAll(node.shadowRoot, selector); + } + for (const match of matches) { + yield match; + yielded = true; + } + } + } + if (yielded) { + return; + } + + if (root instanceof Element && isSuitableNodeForTextMatching(root)) { + const textContent = createTextContent(root); + if (textContent.full.includes(selector)) { + yield root; + } + } +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts new file mode 100644 index 0000000000..787e3afaec --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/XPathQuerySelector.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @internal + */ +export const xpathQuerySelectorAll = function* ( + root: Node, + selector: string +): Iterable<Node> { + const doc = root.ownerDocument || document; + const iterator = doc.evaluate( + selector, + root, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + let item; + while ((item = iterator.iterateNext())) { + yield item; + } +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts new file mode 100644 index 0000000000..0f6aac78ac --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/injected.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createDeferredPromise} from '../util/DeferredPromise.js'; +import {createFunction} from '../util/Function.js'; + +import * as ARIAQuerySelector from './ARIAQuerySelector.js'; +import * as CustomQuerySelectors from './CustomQuerySelector.js'; +import * as PierceQuerySelector from './PierceQuerySelector.js'; +import {IntervalPoller, MutationPoller, RAFPoller} from './Poller.js'; +import * as PQuerySelector from './PQuerySelector.js'; +import { + createTextContent, + isSuitableNodeForTextMatching, +} from './TextContent.js'; +import * as TextQuerySelector from './TextQuerySelector.js'; +import * as util from './util.js'; +import * as XPathQuerySelector from './XPathQuerySelector.js'; + +/** + * @internal + */ +const PuppeteerUtil = Object.freeze({ + ...ARIAQuerySelector, + ...CustomQuerySelectors, + ...PierceQuerySelector, + ...PQuerySelector, + ...TextQuerySelector, + ...util, + ...XPathQuerySelector, + createDeferredPromise, + createFunction, + createTextContent, + IntervalPoller, + isSuitableNodeForTextMatching, + MutationPoller, + RAFPoller, +}); + +/** + * @internal + */ +type PuppeteerUtil = typeof PuppeteerUtil; + +/** + * @internal + */ +export default PuppeteerUtil; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts new file mode 100644 index 0000000000..34fe8f7748 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/util.ts @@ -0,0 +1,67 @@ +const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse']; + +/** + * @internal + */ +export const checkVisibility = ( + node: Node | null, + visible?: boolean +): Node | boolean => { + if (!node) { + return visible === false; + } + if (visible === undefined) { + return node; + } + const element = ( + node.nodeType === Node.TEXT_NODE ? node.parentElement : node + ) as Element; + + const style = window.getComputedStyle(element); + const isVisible = + style && + !HIDDEN_VISIBILITY_VALUES.includes(style.visibility) && + !isBoundingBoxEmpty(element); + return visible === isVisible ? node : false; +}; + +function isBoundingBoxEmpty(element: Element): boolean { + const rect = element.getBoundingClientRect(); + return rect.width === 0 || rect.height === 0; +} + +const hasShadowRoot = (node: Node): node is Node & {shadowRoot: ShadowRoot} => { + return 'shadowRoot' in node && node.shadowRoot instanceof ShadowRoot; +}; + +/** + * @internal + */ +export function* pierce(root: Node): IterableIterator<Node | ShadowRoot> { + if (hasShadowRoot(root)) { + yield root.shadowRoot; + } else { + yield root; + } +} + +/** + * @internal + */ +export function* pierceAll(root: Node): IterableIterator<Node | ShadowRoot> { + root = pierce(root).next().value; + yield root; + const walkers = [document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)]; + for (const walker of walkers) { + let node: Element | null; + while ((node = walker.nextNode() as Element | null)) { + if (!node.shadowRoot) { + continue; + } + yield node.shadowRoot; + walkers.push( + document.createTreeWalker(node.shadowRoot, NodeFilter.SHOW_ELEMENT) + ); + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts new file mode 100644 index 0000000000..9594ed33db --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ChromeLauncher.ts @@ -0,0 +1,257 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {mkdtemp} from 'fs/promises'; +import path from 'path'; + +import { + computeSystemExecutablePath, + Browser as SupportedBrowsers, + ChromeReleaseChannel as BrowsersChromeReleaseChannel, +} from '@puppeteer/browsers'; + +import {debugError} from '../common/util.js'; +import {Browser} from '../puppeteer-core.js'; +import {assert} from '../util/assert.js'; + +import { + BrowserLaunchArgumentOptions, + ChromeReleaseChannel, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; +import {ProductLauncher, ResolvedLaunchArgs} from './ProductLauncher.js'; +import {PuppeteerNode} from './PuppeteerNode.js'; +import {rm} from './util/fs.js'; + +/** + * @internal + */ +export class ChromeLauncher extends ProductLauncher { + constructor(puppeteer: PuppeteerNode) { + super(puppeteer, 'chrome'); + } + + override launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { + const headless = options.headless ?? true; + if ( + headless === true && + (!this.puppeteer.configuration.logLevel || + this.puppeteer.configuration.logLevel === 'warn') && + !Boolean(process.env['PUPPETEER_DISABLE_HEADLESS_WARNING']) + ) { + console.warn( + [ + '\x1B[1m\x1B[43m\x1B[30m', + 'Puppeteer old Headless deprecation warning:\x1B[0m\x1B[33m', + ' In the near feature `headless: true` will default to the new Headless mode', + ' for Chrome instead of the old Headless implementation. For more', + ' information, please see https://developer.chrome.com/articles/new-headless/.', + ' Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`', + ' If you encounter any bugs, please report them to https://github.com/puppeteer/puppeteer/issues/new/choose.\x1B[0m\n', + ].join('\n ') + ); + } + + return super.launch(options); + } + + /** + * @internal + */ + override async computeLaunchArguments( + options: PuppeteerNodeLaunchOptions = {} + ): Promise<ResolvedLaunchArgs> { + const { + ignoreDefaultArgs = false, + args = [], + pipe = false, + debuggingPort, + channel, + executablePath, + } = options; + + const chromeArguments = []; + if (!ignoreDefaultArgs) { + chromeArguments.push(...this.defaultArgs(options)); + } else if (Array.isArray(ignoreDefaultArgs)) { + chromeArguments.push( + ...this.defaultArgs(options).filter(arg => { + return !ignoreDefaultArgs.includes(arg); + }) + ); + } else { + chromeArguments.push(...args); + } + + if ( + !chromeArguments.some(argument => { + return argument.startsWith('--remote-debugging-'); + }) + ) { + if (pipe) { + assert( + !debuggingPort, + 'Browser should be launched with either pipe or debugging port - not both.' + ); + chromeArguments.push('--remote-debugging-pipe'); + } else { + chromeArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); + } + } + + let isTempUserDataDir = false; + + // Check for the user data dir argument, which will always be set even + // with a custom directory specified via the userDataDir option. + let userDataDirIndex = chromeArguments.findIndex(arg => { + return arg.startsWith('--user-data-dir'); + }); + if (userDataDirIndex < 0) { + isTempUserDataDir = true; + chromeArguments.push( + `--user-data-dir=${await mkdtemp(this.getProfilePath())}` + ); + userDataDirIndex = chromeArguments.length - 1; + } + + const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1]; + assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed'); + + let chromeExecutable = executablePath; + if (!chromeExecutable) { + assert( + channel || !this.puppeteer._isPuppeteerCore, + `An \`executablePath\` or \`channel\` must be specified for \`puppeteer-core\`` + ); + chromeExecutable = this.executablePath(channel); + } + + return { + executablePath: chromeExecutable, + args: chromeArguments, + isTempUserDataDir, + userDataDir, + }; + } + + /** + * @internal + */ + override async cleanUserDataDir( + path: string, + opts: {isTemp: boolean} + ): Promise<void> { + if (opts.isTemp) { + try { + await rm(path); + } catch (error) { + debugError(error); + throw error; + } + } + } + + override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { + // See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md + const chromeArguments = [ + '--allow-pre-commit-input', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-component-update', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + // AcceptCHFrame disabled because of crbug.com/1348106. + // DIPS is disabled because of crbug.com/1439578. TODO: enable after M115. + '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,DIPS', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--enable-automation', + // TODO(sadym): remove '--enable-blink-features=IdleDetection' once + // IdleDetection is turned on by default. + '--enable-blink-features=IdleDetection', + '--enable-features=NetworkServiceInProcess2', + '--export-tagged-pdf', + '--force-color-profile=srgb', + '--metrics-recording-only', + '--no-first-run', + '--password-store=basic', + '--use-mock-keychain', + ]; + const { + devtools = false, + headless = !devtools, + args = [], + userDataDir, + } = options; + if (userDataDir) { + chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`); + } + if (devtools) { + chromeArguments.push('--auto-open-devtools-for-tabs'); + } + if (headless) { + chromeArguments.push( + headless === 'new' ? '--headless=new' : '--headless', + '--hide-scrollbars', + '--mute-audio' + ); + } + if ( + args.every(arg => { + return arg.startsWith('-'); + }) + ) { + chromeArguments.push('about:blank'); + } + chromeArguments.push(...args); + return chromeArguments; + } + + override executablePath(channel?: ChromeReleaseChannel): string { + if (channel) { + return computeSystemExecutablePath({ + browser: SupportedBrowsers.CHROME, + channel: convertPuppeteerChannelToBrowsersChannel(channel), + }); + } else { + return this.resolveExecutablePath(); + } + } +} + +function convertPuppeteerChannelToBrowsersChannel( + channel: ChromeReleaseChannel +): BrowsersChromeReleaseChannel { + switch (channel) { + case 'chrome': + return BrowsersChromeReleaseChannel.STABLE; + case 'chrome-dev': + return BrowsersChromeReleaseChannel.DEV; + case 'chrome-beta': + return BrowsersChromeReleaseChannel.BETA; + case 'chrome-canary': + return BrowsersChromeReleaseChannel.CANARY; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts new file mode 100644 index 0000000000..004d78bd7f --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/FirefoxLauncher.ts @@ -0,0 +1,225 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import {rename, unlink, mkdtemp} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import { + Browser as SupportedBrowsers, + createProfile, + Cache, + detectBrowserPlatform, + Browser, +} from '@puppeteer/browsers'; + +import {debugError} from '../common/util.js'; +import {assert} from '../util/assert.js'; + +import { + BrowserLaunchArgumentOptions, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; +import {ProductLauncher, ResolvedLaunchArgs} from './ProductLauncher.js'; +import {PuppeteerNode} from './PuppeteerNode.js'; +import {rm} from './util/fs.js'; + +/** + * @internal + */ +export class FirefoxLauncher extends ProductLauncher { + constructor(puppeteer: PuppeteerNode) { + super(puppeteer, 'firefox'); + } + /** + * @internal + */ + override async computeLaunchArguments( + options: PuppeteerNodeLaunchOptions = {} + ): Promise<ResolvedLaunchArgs> { + const { + ignoreDefaultArgs = false, + args = [], + executablePath, + pipe = false, + extraPrefsFirefox = {}, + debuggingPort = null, + } = options; + + const firefoxArguments = []; + if (!ignoreDefaultArgs) { + firefoxArguments.push(...this.defaultArgs(options)); + } else if (Array.isArray(ignoreDefaultArgs)) { + firefoxArguments.push( + ...this.defaultArgs(options).filter(arg => { + return !ignoreDefaultArgs.includes(arg); + }) + ); + } else { + firefoxArguments.push(...args); + } + + if ( + !firefoxArguments.some(argument => { + return argument.startsWith('--remote-debugging-'); + }) + ) { + if (pipe) { + assert( + debuggingPort === null, + 'Browser should be launched with either pipe or debugging port - not both.' + ); + } + firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); + } + + let userDataDir: string | undefined; + let isTempUserDataDir = true; + + // Check for the profile argument, which will always be set even + // with a custom directory specified via the userDataDir option. + const profileArgIndex = firefoxArguments.findIndex(arg => { + return ['-profile', '--profile'].includes(arg); + }); + + if (profileArgIndex !== -1) { + userDataDir = firefoxArguments[profileArgIndex + 1]; + if (!userDataDir || !fs.existsSync(userDataDir)) { + throw new Error(`Firefox profile not found at '${userDataDir}'`); + } + + // When using a custom Firefox profile it needs to be populated + // with required preferences. + isTempUserDataDir = false; + } else { + userDataDir = await mkdtemp(this.getProfilePath()); + firefoxArguments.push('--profile'); + firefoxArguments.push(userDataDir); + } + + await createProfile(SupportedBrowsers.FIREFOX, { + path: userDataDir, + preferences: extraPrefsFirefox, + }); + + let firefoxExecutable: string; + if (this.puppeteer._isPuppeteerCore || executablePath) { + assert( + executablePath, + `An \`executablePath\` must be specified for \`puppeteer-core\`` + ); + firefoxExecutable = executablePath; + } else { + firefoxExecutable = this.executablePath(); + } + + return { + isTempUserDataDir, + userDataDir, + args: firefoxArguments, + executablePath: firefoxExecutable, + }; + } + + /** + * @internal + */ + override async cleanUserDataDir( + userDataDir: string, + opts: {isTemp: boolean} + ): Promise<void> { + if (opts.isTemp) { + try { + await rm(userDataDir); + } catch (error) { + debugError(error); + throw error; + } + } else { + try { + // When an existing user profile has been used remove the user + // preferences file and restore possibly backuped preferences. + await unlink(path.join(userDataDir, 'user.js')); + + const prefsBackupPath = path.join(userDataDir, 'prefs.js.puppeteer'); + if (fs.existsSync(prefsBackupPath)) { + const prefsPath = path.join(userDataDir, 'prefs.js'); + await unlink(prefsPath); + await rename(prefsBackupPath, prefsPath); + } + } catch (error) { + debugError(error); + } + } + } + + override executablePath(): string { + // replace 'latest' placeholder with actual downloaded revision + if (this.puppeteer.browserRevision === 'latest') { + const cache = new Cache(this.puppeteer.defaultDownloadPath!); + const installedFirefox = cache.getInstalledBrowsers().find(browser => { + return ( + browser.platform === detectBrowserPlatform() && + browser.browser === Browser.FIREFOX + ); + }); + if (installedFirefox) { + this.actualBrowserRevision = installedFirefox.buildId; + } + } + return this.resolveExecutablePath(); + } + + override defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { + const { + devtools = false, + headless = !devtools, + args = [], + userDataDir = null, + } = options; + + const firefoxArguments = ['--no-remote']; + + switch (os.platform()) { + case 'darwin': + firefoxArguments.push('--foreground'); + break; + case 'win32': + firefoxArguments.push('--wait-for-browser'); + break; + } + if (userDataDir) { + firefoxArguments.push('--profile'); + firefoxArguments.push(userDataDir); + } + if (headless) { + firefoxArguments.push('--headless'); + } + if (devtools) { + firefoxArguments.push('--devtools'); + } + if ( + args.every(arg => { + return arg.startsWith('-'); + }) + ) { + firefoxArguments.push('about:blank'); + } + firefoxArguments.push(...args); + return firefoxArguments; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts new file mode 100644 index 0000000000..b7f97ad9c0 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/LaunchOptions.ts @@ -0,0 +1,150 @@ +/** + * 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 {BrowserConnectOptions} from '../common/BrowserConnector.js'; +import {Product} from '../common/Product.js'; + +/** + * Launcher options that only apply to Chrome. + * + * @public + */ +export interface BrowserLaunchArgumentOptions { + /** + * Whether to run the browser in headless mode. + * + * @remarks + * In the future `headless: true` will be equivalent to `headless: 'new'`. + * You can read more about the change {@link https://developer.chrome.com/articles/new-headless/ | here}. + * Consider opting in early by setting the value to `"new"`. + * + * @defaultValue `true` + */ + headless?: boolean | 'new'; + /** + * Path to a user data directory. + * {@link https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/user_data_dir.md | see the Chromium docs} + * for more info. + */ + userDataDir?: string; + /** + * Whether to auto-open a DevTools panel for each tab. If this is set to + * `true`, then `headless` will be forced to `false`. + * @defaultValue `false` + */ + devtools?: boolean; + /** + * Specify the debugging port number to use + */ + debuggingPort?: number; + /** + * Additional command line arguments to pass to the browser instance. + */ + args?: string[]; +} +/** + * @public + */ +export type ChromeReleaseChannel = + | 'chrome' + | 'chrome-beta' + | 'chrome-canary' + | 'chrome-dev'; + +/** + * Generic launch options that can be passed when launching any browser. + * @public + */ +export interface LaunchOptions { + /** + * Chrome Release Channel + */ + channel?: ChromeReleaseChannel; + /** + * Path to a browser executable to use instead of the bundled Chromium. Note + * that Puppeteer is only guaranteed to work with the bundled Chromium, so use + * this setting at your own risk. + */ + executablePath?: string; + /** + * If `true`, do not use `puppeteer.defaultArgs()` when creating a browser. If + * an array is provided, these args will be filtered out. Use this with care - + * you probably want the default arguments Puppeteer uses. + * @defaultValue `false` + */ + ignoreDefaultArgs?: boolean | string[]; + /** + * Close the browser process on `Ctrl+C`. + * @defaultValue `true` + */ + handleSIGINT?: boolean; + /** + * Close the browser process on `SIGTERM`. + * @defaultValue `true` + */ + handleSIGTERM?: boolean; + /** + * Close the browser process on `SIGHUP`. + * @defaultValue `true` + */ + handleSIGHUP?: boolean; + /** + * Maximum time in milliseconds to wait for the browser to start. + * Pass `0` to disable the timeout. + * @defaultValue `30_000` (30 seconds). + */ + timeout?: number; + /** + * If true, pipes the browser process stdout and stderr to `process.stdout` + * and `process.stderr`. + * @defaultValue `false` + */ + dumpio?: boolean; + /** + * Specify environment variables that will be visible to the browser. + * @defaultValue The contents of `process.env`. + */ + env?: Record<string, string | undefined>; + /** + * Connect to a browser over a pipe instead of a WebSocket. + * @defaultValue `false` + */ + pipe?: boolean; + /** + * Which browser to launch. + * @defaultValue `chrome` + */ + product?: Product; + /** + * {@link https://searchfox.org/mozilla-release/source/modules/libpref/init/all.js | Additional preferences } that can be passed when launching with Firefox. + */ + extraPrefsFirefox?: Record<string, unknown>; + /** + * Whether to wait for the initial page to be ready. + * Useful when a user explicitly disables that (e.g. `--no-startup-window` for Chrome). + * @defaultValue `true` + */ + waitForInitialPage?: boolean; +} + +/** + * Utility type exposed to enable users to define options that can be passed to + * `puppeteer.launch` without having to list the set of all types. + * @public + */ +export type PuppeteerNodeLaunchOptions = BrowserLaunchArgumentOptions & + LaunchOptions & + BrowserConnectOptions; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts new file mode 100644 index 0000000000..830825e6f7 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/PipeTransport.ts @@ -0,0 +1,93 @@ +/** + * 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 {ConnectionTransport} from '../common/ConnectionTransport.js'; +import { + addEventListener, + debugError, + PuppeteerEventListener, + removeEventListeners, +} from '../common/util.js'; +import {assert} from '../util/assert.js'; + +/** + * @internal + */ +export class PipeTransport implements ConnectionTransport { + #pipeWrite: NodeJS.WritableStream; + #eventListeners: PuppeteerEventListener[]; + + #isClosed = false; + #pendingMessage = ''; + + onclose?: () => void; + onmessage?: (value: string) => void; + + constructor( + pipeWrite: NodeJS.WritableStream, + pipeRead: NodeJS.ReadableStream + ) { + this.#pipeWrite = pipeWrite; + this.#eventListeners = [ + addEventListener(pipeRead, 'data', buffer => { + return this.#dispatch(buffer); + }), + addEventListener(pipeRead, 'close', () => { + if (this.onclose) { + this.onclose.call(null); + } + }), + addEventListener(pipeRead, 'error', debugError), + addEventListener(pipeWrite, 'error', debugError), + ]; + } + + send(message: string): void { + assert(!this.#isClosed, '`PipeTransport` is closed.'); + + this.#pipeWrite.write(message); + this.#pipeWrite.write('\0'); + } + + #dispatch(buffer: Buffer): void { + assert(!this.#isClosed, '`PipeTransport` is closed.'); + + let end = buffer.indexOf('\0'); + if (end === -1) { + this.#pendingMessage += buffer.toString(); + return; + } + const message = this.#pendingMessage + buffer.toString(undefined, 0, end); + if (this.onmessage) { + this.onmessage.call(null, message); + } + + let start = end + 1; + end = buffer.indexOf('\0', start); + while (end !== -1) { + if (this.onmessage) { + this.onmessage.call(null, buffer.toString(undefined, start, end)); + } + start = end + 1; + end = buffer.indexOf('\0', start); + } + this.#pendingMessage = buffer.toString(undefined, start); + } + + close(): void { + this.#isClosed = true; + removeEventListeners(this.#eventListeners); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts new file mode 100644 index 0000000000..9b92772fab --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/ProductLauncher.ts @@ -0,0 +1,448 @@ +/** + * 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 {existsSync} from 'fs'; +import {tmpdir} from 'os'; +import {join} from 'path'; + +import { + Browser as InstalledBrowser, + CDP_WEBSOCKET_ENDPOINT_REGEX, + launch, + TimeoutError as BrowsersTimeoutError, + WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, + computeExecutablePath, +} from '@puppeteer/browsers'; + +import {Browser, BrowserCloseCallback} from '../api/Browser.js'; +import {CDPBrowser} from '../common/Browser.js'; +import {Connection} from '../common/Connection.js'; +import {TimeoutError} from '../common/Errors.js'; +import {NodeWebSocketTransport as WebSocketTransport} from '../common/NodeWebSocketTransport.js'; +import {Product} from '../common/Product.js'; +import {Viewport} from '../common/PuppeteerViewport.js'; +import {debugError} from '../common/util.js'; + +import { + BrowserLaunchArgumentOptions, + ChromeReleaseChannel, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; +import {PipeTransport} from './PipeTransport.js'; +import {PuppeteerNode} from './PuppeteerNode.js'; + +/** + * @internal + */ +export type ResolvedLaunchArgs = { + isTempUserDataDir: boolean; + userDataDir: string; + executablePath: string; + args: string[]; +}; + +/** + * Describes a launcher - a class that is able to create and launch a browser instance. + * + * @public + */ +export class ProductLauncher { + #product: Product; + + /** + * @internal + */ + puppeteer: PuppeteerNode; + + /** + * @internal + */ + protected actualBrowserRevision?: string; + + /** + * @internal + */ + constructor(puppeteer: PuppeteerNode, product: Product) { + this.puppeteer = puppeteer; + this.#product = product; + } + + get product(): Product { + return this.#product; + } + + async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { + const { + dumpio = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + slowMo = 0, + timeout = 30000, + waitForInitialPage = true, + protocol, + protocolTimeout, + } = options; + + const launchArgs = await this.computeLaunchArguments(options); + + const usePipe = launchArgs.args.includes('--remote-debugging-pipe'); + + const onProcessExit = async () => { + await this.cleanUserDataDir(launchArgs.userDataDir, { + isTemp: launchArgs.isTempUserDataDir, + }); + }; + + const browserProcess = launch({ + executablePath: launchArgs.executablePath, + args: launchArgs.args, + handleSIGHUP, + handleSIGTERM, + handleSIGINT, + dumpio, + env, + pipe: usePipe, + onExit: onProcessExit, + }); + + let browser: Browser; + let connection: Connection; + let closing = false; + + const browserCloseCallback = async () => { + if (closing) { + return; + } + closing = true; + await this.closeBrowser(browserProcess, connection); + }; + + try { + if (this.#product === 'firefox' && protocol === 'webDriverBiDi') { + browser = await this.createBiDiBrowser( + browserProcess, + browserCloseCallback, + { + timeout, + protocolTimeout, + slowMo, + defaultViewport, + } + ); + } else { + if (usePipe) { + connection = await this.createCDPPipeConnection(browserProcess, { + timeout, + protocolTimeout, + slowMo, + }); + } else { + connection = await this.createCDPSocketConnection(browserProcess, { + timeout, + protocolTimeout, + slowMo, + }); + } + if (protocol === 'webDriverBiDi') { + browser = await this.createBiDiOverCDPBrowser( + browserProcess, + connection, + browserCloseCallback, + { + timeout, + protocolTimeout, + slowMo, + defaultViewport, + } + ); + } else { + browser = await CDPBrowser._create( + this.product, + connection, + [], + ignoreHTTPSErrors, + defaultViewport, + browserProcess.nodeProcess, + browserCloseCallback, + options.targetFilter + ); + } + } + } catch (error) { + void browserCloseCallback(); + if (error instanceof BrowsersTimeoutError) { + throw new TimeoutError(error.message); + } + throw error; + } + + if (waitForInitialPage && protocol !== 'webDriverBiDi') { + await this.waitForPageTarget(browser, timeout); + } + + return browser; + } + + executablePath(channel?: ChromeReleaseChannel): string; + executablePath(): string { + throw new Error('Not implemented'); + } + + defaultArgs(object: BrowserLaunchArgumentOptions): string[]; + defaultArgs(): string[] { + throw new Error('Not implemented'); + } + + /** + * Set only for Firefox, after the launcher resolves the `latest` revision to + * the actual revision. + * @internal + */ + getActualBrowserRevision(): string | undefined { + return this.actualBrowserRevision; + } + + /** + * @internal + */ + protected async computeLaunchArguments( + options: PuppeteerNodeLaunchOptions + ): Promise<ResolvedLaunchArgs>; + protected async computeLaunchArguments(): Promise<ResolvedLaunchArgs> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + protected async cleanUserDataDir( + path: string, + opts: {isTemp: boolean} + ): Promise<void>; + protected async cleanUserDataDir(): Promise<void> { + throw new Error('Not implemented'); + } + + /** + * @internal + */ + protected async closeBrowser( + browserProcess: ReturnType<typeof launch>, + connection?: Connection + ): Promise<void> { + if (connection) { + // Attempt to close the browser gracefully + try { + await connection.closeBrowser(); + await browserProcess.hasClosed(); + } catch (error) { + debugError(error); + await browserProcess.close(); + } + } else { + await browserProcess.close(); + } + } + + /** + * @internal + */ + protected async waitForPageTarget( + browser: Browser, + timeout: number + ): Promise<void> { + try { + await browser.waitForTarget( + t => { + return t.type() === 'page'; + }, + {timeout} + ); + } catch (error) { + await browser.close(); + throw error; + } + } + + /** + * @internal + */ + protected async createCDPSocketConnection( + browserProcess: ReturnType<typeof launch>, + opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number} + ): Promise<Connection> { + const browserWSEndpoint = await browserProcess.waitForLineOutput( + CDP_WEBSOCKET_ENDPOINT_REGEX, + opts.timeout + ); + const transport = await WebSocketTransport.create(browserWSEndpoint); + return new Connection( + browserWSEndpoint, + transport, + opts.slowMo, + opts.protocolTimeout + ); + } + + /** + * @internal + */ + protected async createCDPPipeConnection( + browserProcess: ReturnType<typeof launch>, + opts: {timeout: number; protocolTimeout: number | undefined; slowMo: number} + ): Promise<Connection> { + // stdio was assigned during start(), and the 'pipe' option there adds the + // 4th and 5th items to stdio array + const {3: pipeWrite, 4: pipeRead} = browserProcess.nodeProcess.stdio; + const transport = new PipeTransport( + pipeWrite as NodeJS.WritableStream, + pipeRead as NodeJS.ReadableStream + ); + return new Connection('', transport, opts.slowMo, opts.protocolTimeout); + } + + /** + * @internal + */ + protected async createBiDiOverCDPBrowser( + browserProcess: ReturnType<typeof launch>, + connection: Connection, + closeCallback: BrowserCloseCallback, + opts: { + timeout: number; + protocolTimeout: number | undefined; + slowMo: number; + defaultViewport: Viewport | null; + } + ): Promise<Browser> { + // TODO: use other options too. + const BiDi = await import( + /* webpackIgnore: true */ '../common/bidi/bidi.js' + ); + const bidiConnection = await BiDi.connectBidiOverCDP(connection); + return await BiDi.Browser.create({ + connection: bidiConnection, + closeCallback, + process: browserProcess.nodeProcess, + defaultViewport: opts.defaultViewport, + }); + } + + /** + * @internal + */ + protected async createBiDiBrowser( + browserProcess: ReturnType<typeof launch>, + closeCallback: BrowserCloseCallback, + opts: { + timeout: number; + protocolTimeout: number | undefined; + slowMo: number; + defaultViewport: Viewport | null; + } + ): Promise<Browser> { + const browserWSEndpoint = + (await browserProcess.waitForLineOutput( + WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_REGEX, + opts.timeout + )) + '/session'; + const transport = await WebSocketTransport.create(browserWSEndpoint); + const BiDi = await import( + /* webpackIgnore: true */ '../common/bidi/bidi.js' + ); + const bidiConnection = new BiDi.Connection( + transport, + opts.slowMo, + opts.protocolTimeout + ); + // TODO: use other options too. + return await BiDi.Browser.create({ + connection: bidiConnection, + closeCallback, + process: browserProcess.nodeProcess, + defaultViewport: opts.defaultViewport, + }); + } + + /** + * @internal + */ + protected getProfilePath(): string { + return join( + this.puppeteer.configuration.temporaryDirectory ?? tmpdir(), + `puppeteer_dev_${this.product}_profile-` + ); + } + + /** + * @internal + */ + protected resolveExecutablePath(): string { + let executablePath = this.puppeteer.configuration.executablePath; + if (executablePath) { + if (!existsSync(executablePath)) { + throw new Error( + `Tried to find the browser at the configured path (${executablePath}), but no executable was found.` + ); + } + return executablePath; + } + + function productToBrowser(product?: Product) { + switch (product) { + case 'chrome': + return InstalledBrowser.CHROME; + case 'firefox': + return InstalledBrowser.FIREFOX; + } + return InstalledBrowser.CHROME; + } + + executablePath = computeExecutablePath({ + cacheDir: this.puppeteer.defaultDownloadPath!, + browser: productToBrowser(this.product), + buildId: this.puppeteer.browserRevision, + }); + + if (!existsSync(executablePath)) { + if (this.puppeteer.configuration.browserRevision) { + throw new Error( + `Tried to find the browser at the configured path (${executablePath}) for revision ${this.puppeteer.browserRevision}, but no executable was found.` + ); + } + switch (this.product) { + case 'chrome': + throw new Error( + `Could not find Chrome (ver. ${this.puppeteer.browserRevision}). This can occur if either\n` + + ' 1. you did not perform an installation before running the script (e.g. `npm install`) or\n' + + ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` + + 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.' + ); + case 'firefox': + throw new Error( + `Could not find Firefox (rev. ${this.puppeteer.browserRevision}). This can occur if either\n` + + ' 1. you did not perform an installation for Firefox before running the script (e.g. `PUPPETEER_PRODUCT=firefox npm install`) or\n' + + ` 2. your cache path is incorrectly configured (which is: ${this.puppeteer.configuration.cacheDirectory}).\n` + + 'For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.' + ); + } + } + return executablePath; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts new file mode 100644 index 0000000000..c6667eb28f --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/PuppeteerNode.ts @@ -0,0 +1,267 @@ +/** + * 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 {Browser} from '../api/Browser.js'; +import {BrowserConnectOptions} from '../common/BrowserConnector.js'; +import {Configuration} from '../common/Configuration.js'; +import {Product} from '../common/Product.js'; +import { + CommonPuppeteerSettings, + ConnectOptions, + Puppeteer, +} from '../common/Puppeteer.js'; +import {PUPPETEER_REVISIONS} from '../revisions.js'; + +import {ChromeLauncher} from './ChromeLauncher.js'; +import {FirefoxLauncher} from './FirefoxLauncher.js'; +import { + BrowserLaunchArgumentOptions, + ChromeReleaseChannel, + LaunchOptions, +} from './LaunchOptions.js'; +import {ProductLauncher} from './ProductLauncher.js'; + +/** + * @public + */ +export interface PuppeteerLaunchOptions + extends LaunchOptions, + BrowserLaunchArgumentOptions, + BrowserConnectOptions { + product?: Product; + extraPrefsFirefox?: Record<string, unknown>; +} + +/** + * Extends the main {@link Puppeteer} class with Node specific behaviour for + * fetching and downloading browsers. + * + * If you're using Puppeteer in a Node environment, this is the class you'll get + * when you run `require('puppeteer')` (or the equivalent ES `import`). + * + * @remarks + * The most common method to use is {@link PuppeteerNode.launch | launch}, which + * is used to launch and connect to a new browser instance. + * + * See {@link Puppeteer | the main Puppeteer class} for methods common to all + * environments, such as {@link Puppeteer.connect}. + * + * @example + * The following is a typical example of using Puppeteer to drive automation: + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * Once you have created a `page` you have access to a large API to interact + * with the page, navigate, or find certain elements in that page. + * The {@link Page | `page` documentation} lists all the available methods. + * + * @public + */ +export class PuppeteerNode extends Puppeteer { + #_launcher?: ProductLauncher; + #lastLaunchedProduct?: Product; + + /** + * @internal + */ + defaultBrowserRevision: string; + + /** + * @internal + */ + configuration: Configuration = {}; + + /** + * @internal + */ + constructor( + settings: { + configuration?: Configuration; + } & CommonPuppeteerSettings + ) { + const {configuration, ...commonSettings} = settings; + super(commonSettings); + if (configuration) { + this.configuration = configuration; + } + switch (this.configuration.defaultProduct) { + case 'firefox': + this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox; + break; + default: + this.configuration.defaultProduct = 'chrome'; + this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome; + break; + } + + this.connect = this.connect.bind(this); + this.launch = this.launch.bind(this); + this.executablePath = this.executablePath.bind(this); + this.defaultArgs = this.defaultArgs.bind(this); + } + + /** + * This method attaches Puppeteer to an existing browser instance. + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + override connect(options: ConnectOptions): Promise<Browser> { + return super.connect(options); + } + + /** + * Launches a browser instance with given arguments and options when + * specified. + * + * When using with `puppeteer-core`, + * {@link LaunchOptions | options.executablePath} or + * {@link LaunchOptions | options.channel} must be provided. + * + * @example + * You can use {@link LaunchOptions | options.ignoreDefaultArgs} + * to filter out `--mute-audio` from default arguments: + * + * ```ts + * const browser = await puppeteer.launch({ + * ignoreDefaultArgs: ['--mute-audio'], + * }); + * ``` + * + * @remarks + * Puppeteer can also be used to control the Chrome browser, but it works best + * with the version of Chrome for Testing downloaded by default. + * There is no guarantee it will work with any other version. If Google Chrome + * (rather than Chrome for Testing) is preferred, a + * {@link https://www.google.com/chrome/browser/canary.html | Chrome Canary} + * or + * {@link https://www.chromium.org/getting-involved/dev-channel | Dev Channel} + * build is suggested. See + * {@link https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/ | this article} + * for a description of the differences between Chromium and Chrome. + * {@link https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md | This article} + * describes some differences for Linux users. See + * {@link https://goo.gle/chrome-for-testing | this doc} for the description + * of Chrome for Testing. + * + * @param options - Options to configure launching behavior. + */ + launch(options: PuppeteerLaunchOptions = {}): Promise<Browser> { + const {product = this.defaultProduct} = options; + this.#lastLaunchedProduct = product; + return this.#launcher.launch(options); + } + + /** + * @internal + */ + get #launcher(): ProductLauncher { + if ( + this.#_launcher && + this.#_launcher.product === this.lastLaunchedProduct + ) { + return this.#_launcher; + } + switch (this.lastLaunchedProduct) { + case 'chrome': + this.defaultBrowserRevision = PUPPETEER_REVISIONS.chrome; + this.#_launcher = new ChromeLauncher(this); + break; + case 'firefox': + this.defaultBrowserRevision = PUPPETEER_REVISIONS.firefox; + this.#_launcher = new FirefoxLauncher(this); + break; + default: + throw new Error(`Unknown product: ${this.#lastLaunchedProduct}`); + } + return this.#_launcher; + } + + /** + * The default executable path. + */ + executablePath(channel?: ChromeReleaseChannel): string { + return this.#launcher.executablePath(channel); + } + + /** + * @internal + */ + get browserRevision(): string { + return ( + this.#_launcher?.getActualBrowserRevision() ?? + this.configuration.browserRevision ?? + this.defaultBrowserRevision! + ); + } + + /** + * The default download path for puppeteer. For puppeteer-core, this + * code should never be called as it is never defined. + * + * @internal + */ + get defaultDownloadPath(): string | undefined { + return this.configuration.downloadPath ?? this.configuration.cacheDirectory; + } + + /** + * The name of the browser that was last launched. + */ + get lastLaunchedProduct(): Product { + return this.#lastLaunchedProduct ?? this.defaultProduct; + } + + /** + * The name of the browser that will be launched by default. For + * `puppeteer`, this is influenced by your configuration. Otherwise, it's + * `chrome`. + */ + get defaultProduct(): Product { + return this.configuration.defaultProduct ?? 'chrome'; + } + + /** + * @deprecated Do not use as this field as it does not take into account + * multiple browsers of different types. Use + * {@link PuppeteerNode.defaultProduct | defaultProduct} or + * {@link PuppeteerNode.lastLaunchedProduct | lastLaunchedProduct}. + * + * @returns The name of the browser that is under automation. + */ + get product(): string { + return this.#launcher.product; + } + + /** + * @param options - Set of configurable options to set on the browser. + * + * @returns The default flags that Chromium will be launched with. + */ + defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { + return this.#launcher.defaultArgs(options); + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts new file mode 100644 index 0000000000..da815faf16 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/node.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ChromeLauncher.js'; +export * from './FirefoxLauncher.js'; +export * from './LaunchOptions.js'; +export * from './PipeTransport.js'; +export * from './ProductLauncher.js'; +export * from './PuppeteerNode.js'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts b/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts new file mode 100644 index 0000000000..ae0419a91d --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/node/util/fs.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +const rmOptions = { + force: true, + recursive: true, + maxRetries: 5, +}; + +/** + * @internal + */ +export async function rm(path: string): Promise<void> { + await fs.promises.rm(path, rmOptions); +} + +/** + * @internal + */ +export function rmSync(path: string): void { + fs.rmSync(path, rmOptions); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts b/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts new file mode 100644 index 0000000000..08cb8092a5 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/puppeteer-core.ts @@ -0,0 +1,58 @@ +/** + * 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. + */ + +export {Protocol} from 'devtools-protocol'; + +export * from './api/api.js'; +export * from './common/common.js'; +export * from './node/node.js'; +export * from './revisions.js'; +export * from './util/util.js'; + +/** + * @deprecated Use the query handler API defined on {@link Puppeteer} + */ +export * from './common/CustomQueryHandler.js'; + +import {PuppeteerNode} from './node/PuppeteerNode.js'; + +/** + * @public + */ +const puppeteer = new PuppeteerNode({ + isPuppeteerCore: true, +}); + +export const { + /** + * @public + */ + connect, + /** + * @public + */ + defaultArgs, + /** + * @public + */ + executablePath, + /** + * @public + */ + launch, +} = puppeteer; + +export default puppeteer; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts new file mode 100644 index 0000000000..dd30692b6a --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/revisions.ts @@ -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. + */ + +/** + * @internal + */ +export const PUPPETEER_REVISIONS = Object.freeze({ + chrome: '113.0.5672.63', + firefox: 'latest', +}); diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl b/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl new file mode 100644 index 0000000000..aa799e9fdb --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/templates/injected.ts.tmpl @@ -0,0 +1,8 @@ +/** + * JavaScript code that provides the puppeteer utilities. See the + * [README](https://github.com/puppeteer/puppeteer/blob/main/src/injected/README.md) + * for injection for more information. + * + * @internal + */ +export const source = SOURCE_CODE; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl b/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl new file mode 100644 index 0000000000..73b984d2ff --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/templates/version.ts.tmpl @@ -0,0 +1,4 @@ +/** + * @internal + */ +export const packageVersion = 'PACKAGE_VERSION'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json new file mode 100644 index 0000000000..0fabc5470c --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "../lib/cjs/puppeteer" + }, + "references": [{"path": "../third_party/tsconfig.cjs.json"}] +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json new file mode 100644 index 0000000000..2cd2ab579f --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../lib/esm/puppeteer" + }, + "references": [{"path": "../third_party/tsconfig.json"}] +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts new file mode 100644 index 0000000000..5b06b3ab30 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/AsyncIterableUtil.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {AwaitableIterable} from '../common/types.js'; + +/** + * @internal + */ +export class AsyncIterableUtil { + static async *map<T, U>( + iterable: AwaitableIterable<T>, + map: (item: T) => Promise<U> + ): AsyncIterable<U> { + for await (const value of iterable) { + yield await map(value); + } + } + + static async *flatMap<T, U>( + iterable: AwaitableIterable<T>, + map: (item: T) => AwaitableIterable<U> + ): AsyncIterable<U> { + for await (const value of iterable) { + yield* map(value); + } + } + + static async collect<T>(iterable: AwaitableIterable<T>): Promise<T[]> { + const result = []; + for await (const value of iterable) { + result.push(value); + } + return result; + } + + static async first<T>( + iterable: AwaitableIterable<T> + ): Promise<T | undefined> { + for await (const value of iterable) { + return value; + } + return; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/DebuggableDeferredPromise.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/DebuggableDeferredPromise.ts new file mode 100644 index 0000000000..0632fd5e88 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/DebuggableDeferredPromise.ts @@ -0,0 +1,21 @@ +import {DEFERRED_PROMISE_DEBUG_TIMEOUT} from '../environment.js'; + +import {DeferredPromise, createDeferredPromise} from './DeferredPromise.js'; + +/** + * Creates and returns a deferred promise using DEFERRED_PROMISE_DEBUG_TIMEOUT + * if it's specified or a normal deferred promise otherwise. + * + * @internal + */ +export function createDebuggableDeferredPromise<T>( + message: string +): DeferredPromise<T> { + if (DEFERRED_PROMISE_DEBUG_TIMEOUT > 0) { + return createDeferredPromise({ + message, + timeout: DEFERRED_PROMISE_DEBUG_TIMEOUT, + }); + } + return createDeferredPromise(); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/DeferredPromise.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/DeferredPromise.ts new file mode 100644 index 0000000000..8c7945e359 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/DeferredPromise.ts @@ -0,0 +1,68 @@ +import {TimeoutError} from '../common/Errors.js'; + +/** + * @internal + */ +export interface DeferredPromise<T> extends Promise<T> { + finished: () => boolean; + resolved: () => boolean; + resolve: (value: T) => void; + reject: (reason?: unknown) => void; +} + +/** + * @internal + */ +export interface DeferredPromiseOptions { + message: string; + timeout: number; +} + +/** + * Creates and returns a promise along with the resolve/reject functions. + * + * If the promise has not been resolved/rejected within the `timeout` period, + * the promise gets rejected with a timeout error. `timeout` has to be greater than 0 or + * it is ignored. + * + * @internal + */ +export function createDeferredPromise<T>( + opts?: DeferredPromiseOptions +): DeferredPromise<T> { + let isResolved = false; + let isRejected = false; + let resolver: (value: T) => void; + let rejector: (reason?: unknown) => void; + const taskPromise = new Promise<T>((resolve, reject) => { + resolver = resolve; + rejector = reject; + }); + const timeoutId = + opts && opts.timeout > 0 + ? setTimeout(() => { + isRejected = true; + rejector(new TimeoutError(opts.message)); + }, opts.timeout) + : undefined; + return Object.assign(taskPromise, { + resolved: () => { + return isResolved; + }, + finished: () => { + return isResolved || isRejected; + }, + resolve: (value: T) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + isResolved = true; + resolver(value); + }, + reject: (err?: unknown) => { + clearTimeout(timeoutId); + isRejected = true; + rejector(err); + }, + }); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts new file mode 100644 index 0000000000..e5659ce3e3 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/ErrorLike.ts @@ -0,0 +1,27 @@ +/** + * @internal + */ + +export interface ErrorLike extends Error { + name: string; + message: string; +} +/** + * @internal + */ + +export function isErrorLike(obj: unknown): obj is ErrorLike { + return ( + typeof obj === 'object' && obj !== null && 'name' in obj && 'message' in obj + ); +} +/** + * @internal + */ + +export function isErrnoException(obj: unknown): obj is NodeJS.ErrnoException { + return ( + isErrorLike(obj) && + ('errno' in obj || 'code' in obj || 'path' in obj || 'syscall' in obj) + ); +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts new file mode 100644 index 0000000000..cdf09ba195 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/Function.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const createdFunctions = new Map<string, (...args: unknown[]) => unknown>(); + +/** + * Creates a function from a string. + * + * @internal + */ +export const createFunction = ( + functionValue: string +): ((...args: unknown[]) => unknown) => { + let fn = createdFunctions.get(functionValue); + if (fn) { + return fn; + } + fn = new Function(`return ${functionValue}`)() as ( + ...args: unknown[] + ) => unknown; + createdFunctions.set(functionValue, fn); + return fn; +}; + +/** + * @internal + */ +export function stringifyFunction(fn: (...args: never) => unknown): string { + let value = fn.toString(); + try { + new Function(`(${value})`); + } catch { + // This means we might have a function shorthand (e.g. `test(){}`). Let's + // try prefixing. + let prefix = 'function '; + if (value.startsWith('async ')) { + prefix = `async ${prefix}`; + value = value.substring('async '.length); + } + value = `${prefix}${value}`; + try { + new Function(`(${value})`); + } catch { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function cannot be serialized!'); + } + } + return value; +} + +/** + * Replaces `PLACEHOLDER`s with the given replacements. + * + * All replacements must be valid JS code. + * + * @example + * + * ```ts + * interpolateFunction(() => PLACEHOLDER('test'), {test: 'void 0'}); + * // Equivalent to () => void 0 + * ``` + * + * @internal + */ +export const interpolateFunction = <T extends (...args: never[]) => unknown>( + fn: T, + replacements: Record<string, string> +): T => { + let value = stringifyFunction(fn); + for (const [name, jsValue] of Object.entries(replacements)) { + value = value.replace( + new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'), + jsValue + ); + } + return createFunction(value) as unknown as T; +}; + +declare global { + /** + * Used for interpolation with {@link interpolateFunction}. + * + * @internal + */ + function PLACEHOLDER<T>(name: string): T; +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts new file mode 100644 index 0000000000..bd8b10e731 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/assert.ts @@ -0,0 +1,31 @@ +/** + * 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. + */ + +/** + * Asserts that the given value is truthy. + * @param value - some conditional statement + * @param message - the error message to throw if the value is not truthy. + * + * @internal + */ +export const assert: (value: unknown, message?: string) => asserts value = ( + value, + message +) => { + if (!value) { + throw new Error(message); + } +}; diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts new file mode 100644 index 0000000000..d316075794 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/util/util.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './assert.js'; +export * from './DebuggableDeferredPromise.js'; +export * from './DeferredPromise.js'; +export * from './ErrorLike.js'; +export * from './AsyncIterableUtil.js'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/index.ts b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/index.ts new file mode 100644 index 0000000000..b4833b79e4 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/mitt/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from 'mitt'; +export {default as default} from 'mitt'; diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json new file mode 100644 index 0000000000..a169b93816 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "declarationMap": false, + "outDir": "../lib/cjs/third_party", + "sourceMap": false + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json new file mode 100644 index 0000000000..cfe3a26f4c --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/third_party/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "declarationMap": false, + "outDir": "../lib/esm/third_party", + "sourceMap": false + } +} diff --git a/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts b/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts new file mode 100644 index 0000000000..5c0d74cbcd --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/tools/ensure-correct-devtools-protocol-package.ts @@ -0,0 +1,97 @@ +/** + * 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. + */ + +/** + * This script ensures that the pinned version of devtools-protocol in + * package.json is the right version for the current revision of Chrome that + * Puppeteer ships with. + * + * The devtools-protocol package publisher runs every hour and checks if there + * are protocol changes. If there are, it will be versioned with the revision + * number of the commit that last changed the .pdl files. + * + * Chrome branches/releases are figured out at a later point in time, so it's + * not true that each Chrome revision will have an exact matching revision + * version of devtools-protocol. To ensure we're using a devtools-protocol that + * is aligned with our revision, we want to find the largest package number + * that's \<= the revision that Puppeteer is using. + * + * This script uses npm's `view` function to list all versions in a range and + * find the one closest to our Chrome revision. + */ + +// eslint-disable-next-line import/extensions +import {execSync} from 'child_process'; + +import packageJson from '../package.json'; +import {PUPPETEER_REVISIONS} from '../src/revisions.js'; + +async function main() { + const currentProtocolPackageInstalledVersion = + packageJson.dependencies['devtools-protocol']; + + /** + * Ensure that the devtools-protocol version is pinned. + */ + if (/^[^0-9]/.test(currentProtocolPackageInstalledVersion)) { + console.log( + `ERROR: devtools-protocol package is not pinned to a specific version.\n` + ); + process.exit(1); + } + + const chromeVersion = PUPPETEER_REVISIONS.chrome; + // find the right revision for our Chrome version. + const req = await fetch( + `https://chromiumdash.appspot.com/fetch_releases?channel=stable` + ); + const stableReleases = await req.json(); + const chromeRevision = stableReleases.find(release => { + return release.version === chromeVersion; + }).chromium_main_branch_position; + console.log(`Revisions for ${chromeVersion}: ${chromeRevision}`); + + const command = `npm view "devtools-protocol@<=0.0.${chromeRevision}" version | tail -1`; + + console.log( + 'Checking npm for devtools-protocol revisions:\n', + `'${command}'`, + '\n' + ); + + const output = execSync(command, { + encoding: 'utf8', + }); + + const bestRevisionFromNpm = output.split(' ')[1]!.replace(/'|\n/g, ''); + + if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) { + console.log(`ERROR: bad devtools-protocol revision detected: + + Current Puppeteer Chrome revision: ${chromeRevision} + Current devtools-protocol version in package.json: ${currentProtocolPackageInstalledVersion} + Expected devtools-protocol version: ${bestRevisionFromNpm}`); + + process.exit(1); + } + + console.log( + `Correct devtools-protocol version found (${bestRevisionFromNpm}).` + ); + process.exit(0); +} + +void main(); diff --git a/remote/test/puppeteer/packages/puppeteer-core/tools/generate_sources.ts b/remote/test/puppeteer/packages/puppeteer-core/tools/generate_sources.ts new file mode 100644 index 0000000000..70922c6aad --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/tools/generate_sources.ts @@ -0,0 +1,88 @@ +#!/usr/bin/env node +import {mkdir, mkdtemp, readFile, rm, writeFile} from 'fs/promises'; +import path, {join, resolve} from 'path'; +import {chdir} from 'process'; + +import esbuild from 'esbuild'; + +import {job} from '../../../tools/internal/job.js'; + +const packageRoot = resolve(join(__dirname, '..')); +chdir(packageRoot); + +(async () => { + await job('', async ({outputs}) => { + await Promise.all( + outputs.map(outputs => { + return mkdir(outputs, {recursive: true}); + }) + ); + }) + .outputs(['src/generated']) + .build(); + + const versionJob = job('', async ({inputs, outputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + outputs[0]!, + (await readFile(inputs[1]!, 'utf8')).replace('PACKAGE_VERSION', version) + ); + }) + .inputs(['package.json', 'src/templates/version.ts.tmpl']) + .outputs(['src/generated/version.ts']) + .build(); + + const injectedJob = job('', async ({name, inputs, outputs}) => { + const input = inputs.find(input => { + return input.endsWith('injected.ts'); + })!; + const template = await readFile( + inputs.find(input => { + return input.includes('injected.ts.tmpl'); + })!, + 'utf8' + ); + const tmp = await mkdtemp(name); + await esbuild.build({ + entryPoints: [input], + bundle: true, + outdir: tmp, + format: 'cjs', + platform: 'browser', + target: 'ES2022', + minify: true, + }); + const baseName = path.basename(input); + const content = await readFile( + path.join(tmp, baseName.replace('.ts', '.js')), + 'utf-8' + ); + const scriptContent = template.replace( + 'SOURCE_CODE', + JSON.stringify(content) + ); + await writeFile(outputs[0]!, scriptContent); + await rm(tmp, {recursive: true, force: true}); + }) + .inputs(['src/templates/injected.ts.tmpl', 'src/injected/**/*.ts']) + .outputs(['src/generated/injected.ts']) + .build(); + + await Promise.all([versionJob, injectedJob]); + + if (process.env['PUBLISH']) { + await job('', async ({inputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + inputs[1]!, + ( + await readFile(inputs[1]!, { + encoding: 'utf-8', + }) + ).replace("'NEXT'", `'v${version}'`) + ); + }) + .inputs(['package.json', '../../versions.js']) + .build(); + } +})(); diff --git a/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json b/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json new file mode 100644 index 0000000000..a219f8b704 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "references": [ + {"path": "src/tsconfig.esm.json"}, + {"path": "src/tsconfig.cjs.json"} + ] +} diff --git a/remote/test/puppeteer/packages/puppeteer/.gitignore b/remote/test/puppeteer/packages/puppeteer/.gitignore new file mode 100644 index 0000000000..42061c01a1 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/.gitignore @@ -0,0 +1 @@ +README.md
\ No newline at end of file diff --git a/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md b/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md new file mode 100644 index 0000000000..e7018aef46 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/CHANGELOG.md @@ -0,0 +1,1457 @@ +# 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. + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @puppeteer/browsers bumped from 0.3.0 to 0.3.1 + +## [20.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v20.0.0...puppeteer-v20.1.0) (2023-05-03) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 20.0.0 to 20.1.0 + +## [20.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.11.1...puppeteer-v20.0.0) (2023-05-02) + + +### ⚠ BREAKING CHANGES + +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) + +### Features + +* switch to Chrome for Testing instead of Chromium ([#10054](https://github.com/puppeteer/puppeteer/issues/10054)) ([df4d60c](https://github.com/puppeteer/puppeteer/commit/df4d60c187aa11c4ad783827242e9511f4ec2aab)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.11.1 to 20.0.0 + * @puppeteer/browsers bumped from 0.5.0 to 1.0.0 + +## [19.11.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.11.0...puppeteer-v19.11.1) (2023-04-25) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.11.0 to 19.11.1 + +## [19.11.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.10.1...puppeteer-v19.11.0) (2023-04-24) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.10.1 to 19.11.0 + +## [19.10.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.10.0...puppeteer-v19.10.1) (2023-04-21) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.10.0 to 19.10.1 + * @puppeteer/browsers bumped from 0.4.1 to 0.5.0 + +## [19.10.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.9.1...puppeteer-v19.10.0) (2023-04-20) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.9.1 to 19.10.0 + +## [19.9.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.9.0...puppeteer-v19.9.1) (2023-04-17) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.9.0 to 19.9.1 + +## [19.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.5...puppeteer-v19.9.0) (2023-04-13) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.8.5 to 19.9.0 + * @puppeteer/browsers bumped from 0.4.0 to 0.4.1 + +## [19.8.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.4...puppeteer-v19.8.5) (2023-04-06) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.8.4 to 19.8.5 + * @puppeteer/browsers bumped from 0.3.3 to 0.4.0 + +## [19.8.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.3...puppeteer-v19.8.4) (2023-04-06) + + +### Bug Fixes + +* consider downloadHost as baseUrl ([#9973](https://github.com/puppeteer/puppeteer/issues/9973)) ([05a44af](https://github.com/puppeteer/puppeteer/commit/05a44afe5affcac9fe0f0a2e83f17807c99b2f0c)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.8.3 to 19.8.4 + * @puppeteer/browsers bumped from 0.3.2 to 0.3.3 + +## [19.8.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.2...puppeteer-v19.8.3) (2023-04-03) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.8.1 to 19.8.3 + * @puppeteer/browsers bumped from 0.3.1 to 0.3.2 + +## [19.8.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.8.0...puppeteer-v19.8.1) (2023-03-28) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.8.0 to 19.8.1 + +## [19.8.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.5...puppeteer-v19.8.0) (2023-03-24) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.5 to 19.8.0 + +## [19.7.5](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.4...puppeteer-v19.7.5) (2023-03-14) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.4 to 19.7.5 + +## [19.7.4](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.3...puppeteer-v19.7.4) (2023-03-10) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.3 to 19.7.4 + +## [19.7.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.2...puppeteer-v19.7.3) (2023-03-06) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.2 to 19.7.3 + +## [19.7.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.1...puppeteer-v19.7.2) (2023-02-20) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.1 to 19.7.2 + +## [19.7.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.7.0...puppeteer-v19.7.1) (2023-02-15) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.7.0 to 19.7.1 + +## [19.7.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.3...puppeteer-v19.7.0) (2023-02-13) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.6.3 to 19.7.0 + +## [19.6.3](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.2...puppeteer-v19.6.3) (2023-02-01) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.6.2 to 19.6.3 + +## [19.6.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.1...puppeteer-v19.6.2) (2023-01-27) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.6.1 to 19.6.2 + +## [19.6.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.6.0...puppeteer-v19.6.1) (2023-01-26) + + +### Bug Fixes + +* don't clean up previous browser versions ([#9568](https://github.com/puppeteer/puppeteer/issues/9568)) ([344bc2a](https://github.com/puppeteer/puppeteer/commit/344bc2af62e4068fe2cb8162d4b6c8242aac843b)), closes [#9533](https://github.com/puppeteer/puppeteer/issues/9533) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.6.0 to 19.6.1 + +## [19.6.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.2...puppeteer-v19.6.0) (2023-01-23) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.5.2 to 19.6.0 + +## [19.5.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.1...puppeteer-v19.5.2) (2023-01-11) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.5.1 to 19.5.2 + +## [19.5.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.5.0...puppeteer-v19.5.1) (2023-01-11) + + +### Bug Fixes + +* use puppeteer node for installation script ([#9489](https://github.com/puppeteer/puppeteer/issues/9489)) ([9bf90d9](https://github.com/puppeteer/puppeteer/commit/9bf90d9f4b5aeab06f8b433714712cad3259d36e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.5.0 to 19.5.1 + +## [19.5.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.4.1...puppeteer-v19.5.0) (2023-01-05) + + +### Features + +* Default to not downloading if explicit browser path is set ([#9440](https://github.com/puppeteer/puppeteer/issues/9440)) ([d2536d7](https://github.com/puppeteer/puppeteer/commit/d2536d7cf5fa731250bbfd0d18959cacc8afffac)), closes [#9419](https://github.com/puppeteer/puppeteer/issues/9419) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.4.1 to 19.5.0 + +## [19.4.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.4.0...puppeteer-v19.4.1) (2022-12-16) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.4.0 to 19.4.1 + +## [19.4.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.3.0...puppeteer-v19.4.0) (2022-12-07) + + +### Features + +* **chromium:** roll to Chromium 109.0.5412.0 (r1069273) ([#9364](https://github.com/puppeteer/puppeteer/issues/9364)) ([1875da6](https://github.com/puppeteer/puppeteer/commit/1875da61916df1fbcf98047858c01075bd9af189)), closes [#9233](https://github.com/puppeteer/puppeteer/issues/9233) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.3.0 to 19.4.0 + +## [19.3.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v19.2.2...puppeteer-v19.3.0) (2022-11-23) + + +### Miscellaneous Chores + +* **puppeteer:** Synchronize puppeteer versions + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.2.2 to 19.3.0 + +## [19.2.2](https://github.com/puppeteer/puppeteer/compare/v19.2.1...v19.2.2) (2022-11-03) + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.2.1 to ^19.2.2 + +## [19.2.1](https://github.com/puppeteer/puppeteer/compare/v19.2.0...v19.2.1) (2022-10-28) + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.2.0 to ^19.2.1 + +## [19.2.0](https://github.com/puppeteer/puppeteer/compare/v19.1.2...v19.2.0) (2022-10-26) + + +### Features + +* **chromium:** roll to Chromium 108.0.5351.0 (r1056772) ([#9153](https://github.com/puppeteer/puppeteer/issues/9153)) ([e78a4e8](https://github.com/puppeteer/puppeteer/commit/e78a4e89c22bb1180e72d180c16b39673ff9125e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.1.1 to ^19.2.0 + +## [19.1.2](https://github.com/puppeteer/puppeteer/compare/v19.1.1...v19.1.2) (2022-10-25) + + +### Bug Fixes + +* skip browser download ([#9160](https://github.com/puppeteer/puppeteer/issues/9160)) ([2245d7d](https://github.com/puppeteer/puppeteer/commit/2245d7d6ed0630ee1ad985dcbd48354772924750)) + +## [19.1.1](https://github.com/puppeteer/puppeteer/compare/v19.1.0...v19.1.1) (2022-10-21) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.1.0 to ^19.1.1 + +## [19.1.0](https://github.com/puppeteer/puppeteer/compare/v19.0.0...v19.1.0) (2022-10-21) + + +### Features + +* use configuration files ([#9140](https://github.com/puppeteer/puppeteer/issues/9140)) ([ec20174](https://github.com/puppeteer/puppeteer/commit/ec201744f077987b288e3dff52c0906fe700f6fb)), closes [#9128](https://github.com/puppeteer/puppeteer/issues/9128) + + +### Bug Fixes + +* update `BrowserFetcher` deprecation message ([#9141](https://github.com/puppeteer/puppeteer/issues/9141)) ([efcbc97](https://github.com/puppeteer/puppeteer/commit/efcbc97c60e4cfd49a9ed25a900f6133d06b290b)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 19.0.0 to ^19.1.0 + +## [19.0.0](https://github.com/puppeteer/puppeteer/compare/v18.2.1...v19.0.0) (2022-10-14) + + +### ⚠ BREAKING CHANGES + +* use `~/.cache/puppeteer` for browser downloads (#9095) +* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` (#9079) +* refactor custom query handler API (#9078) +* remove `puppeteer.devices` in favor of `KnownDevices` (#9075) +* deprecate indirect network condition imports (#9074) + +### Features + +* deprecate `createBrowserFetcher` in favor of `BrowserFetcher` ([#9079](https://github.com/puppeteer/puppeteer/issues/9079)) ([7294dfe](https://github.com/puppeteer/puppeteer/commit/7294dfe9c6c3b224f95ba6d59b5ef33d379fd09a)), closes [#8999](https://github.com/puppeteer/puppeteer/issues/8999) +* use `~/.cache/puppeteer` for browser downloads ([#9095](https://github.com/puppeteer/puppeteer/issues/9095)) ([3df375b](https://github.com/puppeteer/puppeteer/commit/3df375baedad64b8773bb1e1e6f81b604ed18989)) + + +### Bug Fixes + +* deprecate indirect network condition imports ([#9074](https://github.com/puppeteer/puppeteer/issues/9074)) ([41d0122](https://github.com/puppeteer/puppeteer/commit/41d0122b94f41b308536c48ced345dec8c272a49)) +* refactor custom query handler API ([#9078](https://github.com/puppeteer/puppeteer/issues/9078)) ([1847704](https://github.com/puppeteer/puppeteer/commit/1847704789e2888c755de8c739d567364b8ad645)) +* remove `puppeteer.devices` in favor of `KnownDevices` ([#9075](https://github.com/puppeteer/puppeteer/issues/9075)) ([87c08fd](https://github.com/puppeteer/puppeteer/commit/87c08fd86a79b63308ad8d46c5f7acd1927505f8)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 18.2.1 to ^19.0.0 + +## [18.2.1](https://github.com/puppeteer/puppeteer/compare/v18.2.0...v18.2.1) (2022-10-06) + + +### Bug Fixes + +* add README to package during prepack ([#9057](https://github.com/puppeteer/puppeteer/issues/9057)) ([9374e23](https://github.com/puppeteer/puppeteer/commit/9374e23d3da5e40378461ed08db24649730a445a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 18.2.0 to ^18.2.1 + +## [18.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-v18.1.0...puppeteer-v18.2.0) (2022-10-05) + + +### Features + +* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * puppeteer-core bumped from 18.1.0 to ^18.2.0 + +## [18.1.0](https://github.com/puppeteer/puppeteer/compare/v18.0.5...v18.1.0) (2022-10-05) + + +### Features + +* **chromium:** roll to Chromium 107.0.5296.0 (r1045629) ([#9039](https://github.com/puppeteer/puppeteer/issues/9039)) ([022fbde](https://github.com/puppeteer/puppeteer/commit/022fbde85e067e8c419cf42dd571f9a1187c343c)) + +## [18.0.5](https://github.com/puppeteer/puppeteer/compare/v18.0.4...v18.0.5) (2022-09-22) + + +### Bug Fixes + +* add missing npm config environment variable ([#8996](https://github.com/puppeteer/puppeteer/issues/8996)) ([7c1be20](https://github.com/puppeteer/puppeteer/commit/7c1be20aef46aaf5029732a580ec65aa8008aa9c)) + +## [18.0.4](https://github.com/puppeteer/puppeteer/compare/v18.0.3...v18.0.4) (2022-09-21) + + +### Bug Fixes + +* hardcode binding names ([#8993](https://github.com/puppeteer/puppeteer/issues/8993)) ([7e20554](https://github.com/puppeteer/puppeteer/commit/7e2055433e79ef20f6dcdf02f92e1d64564b7d33)) + +## [18.0.3](https://github.com/puppeteer/puppeteer/compare/v18.0.2...v18.0.3) (2022-09-20) + + +### Bug Fixes + +* change injected.ts imports ([#8987](https://github.com/puppeteer/puppeteer/issues/8987)) ([10a114d](https://github.com/puppeteer/puppeteer/commit/10a114d36f2add90860950f61b3f8b93258edb5c)) + +## [18.0.2](https://github.com/puppeteer/puppeteer/compare/v18.0.1...v18.0.2) (2022-09-19) + + +### Bug Fixes + +* mark internal objects ([#8984](https://github.com/puppeteer/puppeteer/issues/8984)) ([181a148](https://github.com/puppeteer/puppeteer/commit/181a148269fce1575f5e37056929ecdec0517586)) + +## [18.0.1](https://github.com/puppeteer/puppeteer/compare/v18.0.0...v18.0.1) (2022-09-19) + + +### Bug Fixes + +* internal lazy params ([#8982](https://github.com/puppeteer/puppeteer/issues/8982)) ([d504597](https://github.com/puppeteer/puppeteer/commit/d5045976a6dd321bbd265b84c2474ff1ad5d0b77)) + +## [18.0.0](https://github.com/puppeteer/puppeteer/compare/v17.1.3...v18.0.0) (2022-09-19) + + +### ⚠ BREAKING CHANGES + +* fix bounding box visibility conditions (#8954) + +### Features + +* add text query handler ([#8956](https://github.com/puppeteer/puppeteer/issues/8956)) ([633e7cf](https://github.com/puppeteer/puppeteer/commit/633e7cfdf99d42f420d0af381394bd1f6ac7bcd1)) + + +### Bug Fixes + +* fix bounding box visibility conditions ([#8954](https://github.com/puppeteer/puppeteer/issues/8954)) ([ac9929d](https://github.com/puppeteer/puppeteer/commit/ac9929d80f6f7d4905a39183ae235500e29b4f53)) +* suppress init errors if the target is closed ([#8947](https://github.com/puppeteer/puppeteer/issues/8947)) ([cfaaa5e](https://github.com/puppeteer/puppeteer/commit/cfaaa5e2c07e5f98baeb7de99e303aa840a351e8)) +* use win64 version of chromium when on arm64 windows ([#8927](https://github.com/puppeteer/puppeteer/issues/8927)) ([64843b8](https://github.com/puppeteer/puppeteer/commit/64843b88853210314677ab1b434729513ce615a7)) + +## [17.1.3](https://github.com/puppeteer/puppeteer/compare/v17.1.2...v17.1.3) (2022-09-08) + + +### Bug Fixes + +* FirefoxLauncher should not use BrowserFetcher in puppeteer-core ([#8920](https://github.com/puppeteer/puppeteer/issues/8920)) ([f2e8de7](https://github.com/puppeteer/puppeteer/commit/f2e8de777fc5d547778fdc6cac658add84ed4082)), closes [#8919](https://github.com/puppeteer/puppeteer/issues/8919) +* linux arm64 check on windows arm ([#8917](https://github.com/puppeteer/puppeteer/issues/8917)) ([f02b926](https://github.com/puppeteer/puppeteer/commit/f02b926245e28b5671087c051dbdbb3165696f08)), closes [#8915](https://github.com/puppeteer/puppeteer/issues/8915) + +## [17.1.2](https://github.com/puppeteer/puppeteer/compare/v17.1.1...v17.1.2) (2022-09-07) + + +### Bug Fixes + +* add missing code coverage ranges that span only a single character ([#8911](https://github.com/puppeteer/puppeteer/issues/8911)) ([0c577b9](https://github.com/puppeteer/puppeteer/commit/0c577b9bf8855dc0ccb6098cd43a25c528f6d7f5)) +* add Page.getDefaultTimeout getter ([#8903](https://github.com/puppeteer/puppeteer/issues/8903)) ([3240095](https://github.com/puppeteer/puppeteer/commit/32400954c50cbddc48468ad118c3f8a47653b9d3)), closes [#8901](https://github.com/puppeteer/puppeteer/issues/8901) +* don't detect project root for puppeteer-core ([#8907](https://github.com/puppeteer/puppeteer/issues/8907)) ([b4f5ea1](https://github.com/puppeteer/puppeteer/commit/b4f5ea1167a60c870194c70d22f5372ada5b7c4c)), closes [#8896](https://github.com/puppeteer/puppeteer/issues/8896) +* support scale for screenshot clips ([#8908](https://github.com/puppeteer/puppeteer/issues/8908)) ([260e428](https://github.com/puppeteer/puppeteer/commit/260e4282275ab1d05c86e5643e2a02c01f269a9c)), closes [#5329](https://github.com/puppeteer/puppeteer/issues/5329) +* work around a race in waitForFileChooser ([#8905](https://github.com/puppeteer/puppeteer/issues/8905)) ([053d960](https://github.com/puppeteer/puppeteer/commit/053d960fb593e514e7914d7da9af436afc39a12f)), closes [#6040](https://github.com/puppeteer/puppeteer/issues/6040) + +## [17.1.1](https://github.com/puppeteer/puppeteer/compare/v17.1.0...v17.1.1) (2022-09-05) + + +### Bug Fixes + +* restore deferred promise debugging ([#8895](https://github.com/puppeteer/puppeteer/issues/8895)) ([7b42250](https://github.com/puppeteer/puppeteer/commit/7b42250c7bb91ac873307acda493726ffc4c54a8)) + +## [17.1.0](https://github.com/puppeteer/puppeteer/compare/v17.0.0...v17.1.0) (2022-09-02) + + +### Features + +* **chromium:** roll to Chromium 106.0.5249.0 (r1036745) ([#8869](https://github.com/puppeteer/puppeteer/issues/8869)) ([6e9a47a](https://github.com/puppeteer/puppeteer/commit/6e9a47a6faa06d241dec0bcf7bcdf49370517008)) + + +### Bug Fixes + +* allow getting a frame from an elementhandle ([#8875](https://github.com/puppeteer/puppeteer/issues/8875)) ([3732757](https://github.com/puppeteer/puppeteer/commit/3732757450b4363041ccbacc3b236289a156abb0)) +* typos in documentation ([#8858](https://github.com/puppeteer/puppeteer/issues/8858)) ([8d95a9b](https://github.com/puppeteer/puppeteer/commit/8d95a9bc920b98820aa655ad4eb2d8fd9b2b893a)) +* use the timeout setting in waitForFileChooser ([#8856](https://github.com/puppeteer/puppeteer/issues/8856)) ([f477b46](https://github.com/puppeteer/puppeteer/commit/f477b46f212da9206102da695697760eea539f05)) + +## [17.0.0](https://github.com/puppeteer/puppeteer/compare/v16.2.0...v17.0.0) (2022-08-26) + + +### ⚠ BREAKING CHANGES + +* remove `root` from `WaitForSelectorOptions` (#8848) +* internalize execution context (#8844) + +### Bug Fixes + +* allow multiple navigations to happen in LifecycleWatcher ([#8826](https://github.com/puppeteer/puppeteer/issues/8826)) ([341b669](https://github.com/puppeteer/puppeteer/commit/341b669a5e45ecbb9ffb0f28c45b520660f27ad2)), closes [#8811](https://github.com/puppeteer/puppeteer/issues/8811) +* internalize execution context ([#8844](https://github.com/puppeteer/puppeteer/issues/8844)) ([2f33237](https://github.com/puppeteer/puppeteer/commit/2f33237d0443de77d58dca4454b0c9a1d2b57d03)) +* remove `root` from `WaitForSelectorOptions` ([#8848](https://github.com/puppeteer/puppeteer/issues/8848)) ([1155c8e](https://github.com/puppeteer/puppeteer/commit/1155c8eac85b176c3334cc3d98adfe7d943dfbe6)) +* remove deferred promise timeouts ([#8835](https://github.com/puppeteer/puppeteer/issues/8835)) ([202ffce](https://github.com/puppeteer/puppeteer/commit/202ffce0aa4f34dba35fbb8e7d740af16efee35f)), closes [#8832](https://github.com/puppeteer/puppeteer/issues/8832) + +## [16.2.0](https://github.com/puppeteer/puppeteer/compare/v16.1.1...v16.2.0) (2022-08-18) + + +### Features + +* add Khmer (Cambodian) language support ([#8809](https://github.com/puppeteer/puppeteer/issues/8809)) ([34f8737](https://github.com/puppeteer/puppeteer/commit/34f873721804d57a5faf3eab8ef50340c69ed180)) + + +### Bug Fixes + +* handle service workers in extensions ([#8807](https://github.com/puppeteer/puppeteer/issues/8807)) ([2a0eefb](https://github.com/puppeteer/puppeteer/commit/2a0eefb99f0ae00dacc9e768a253308c0d18a4c3)), closes [#8800](https://github.com/puppeteer/puppeteer/issues/8800) + +## [16.1.1](https://github.com/puppeteer/puppeteer/compare/v16.1.0...v16.1.1) (2022-08-16) + + +### Bug Fixes + +* custom sessions should not emit targetcreated events ([#8788](https://github.com/puppeteer/puppeteer/issues/8788)) ([3fad05d](https://github.com/puppeteer/puppeteer/commit/3fad05d333b79f41a7b58582c4ca493200bb5a79)), closes [#8787](https://github.com/puppeteer/puppeteer/issues/8787) +* deprecate `ExecutionContext` ([#8792](https://github.com/puppeteer/puppeteer/issues/8792)) ([b5da718](https://github.com/puppeteer/puppeteer/commit/b5da718e2e4a2004a36cf23cad555e1fc3b50333)) +* deprecate `root` in `WaitForSelectorOptions` ([#8795](https://github.com/puppeteer/puppeteer/issues/8795)) ([65a5ce8](https://github.com/puppeteer/puppeteer/commit/65a5ce8464c56fcc55e5ac3ed490f31311bbe32a)) +* deprecate `waitForTimeout` ([#8793](https://github.com/puppeteer/puppeteer/issues/8793)) ([8f612d5](https://github.com/puppeteer/puppeteer/commit/8f612d5ff855d48ae4b38bdaacf2a8fbda8e9ce8)) +* make sure there is a check for targets when timeout=0 ([#8765](https://github.com/puppeteer/puppeteer/issues/8765)) ([c23cdb7](https://github.com/puppeteer/puppeteer/commit/c23cdb73a7b113c1dd29f7e4a7a61326422c4080)), closes [#8763](https://github.com/puppeteer/puppeteer/issues/8763) +* resolve navigation flakiness ([#8768](https://github.com/puppeteer/puppeteer/issues/8768)) ([2580347](https://github.com/puppeteer/puppeteer/commit/2580347b50091d172b2a5591138a2e41ede072fe)), closes [#8644](https://github.com/puppeteer/puppeteer/issues/8644) +* specify Puppeteer version for Chromium 105.0.5173.0 ([#8766](https://github.com/puppeteer/puppeteer/issues/8766)) ([b5064b7](https://github.com/puppeteer/puppeteer/commit/b5064b7b8bd3bd9eb481b6807c65d9d06d23b9dd)) +* use targetFilter in puppeteer.launch ([#8774](https://github.com/puppeteer/puppeteer/issues/8774)) ([ee2540b](https://github.com/puppeteer/puppeteer/commit/ee2540baefeced44f6b336f2b979af5c3a4cb040)), closes [#8772](https://github.com/puppeteer/puppeteer/issues/8772) + +## [16.1.0](https://github.com/puppeteer/puppeteer/compare/v16.0.0...v16.1.0) (2022-08-06) + + +### Features + +* use an `xpath` query handler ([#8730](https://github.com/puppeteer/puppeteer/issues/8730)) ([5cf9b4d](https://github.com/puppeteer/puppeteer/commit/5cf9b4de8d50bd056db82bcaa23279b72c9313c5)) + + +### Bug Fixes + +* resolve target manager init if no existing targets detected ([#8748](https://github.com/puppeteer/puppeteer/issues/8748)) ([8cb5043](https://github.com/puppeteer/puppeteer/commit/8cb5043868f69cdff7f34f1cfe0c003ff09e281b)), closes [#8747](https://github.com/puppeteer/puppeteer/issues/8747) +* specify the target filter in setDiscoverTargets ([#8742](https://github.com/puppeteer/puppeteer/issues/8742)) ([49193cb](https://github.com/puppeteer/puppeteer/commit/49193cbf1c17f16f0ca59a9fd2ebf306f812f52b)) + +## [16.0.0](https://github.com/puppeteer/puppeteer/compare/v15.5.0...v16.0.0) (2022-08-02) + + +### ⚠ BREAKING CHANGES + +* With Chromium, Puppeteer will now attach to page/iframe targets immediately to allow reliable configuration of targets. + +### Features + +* add Dockerfile ([#8315](https://github.com/puppeteer/puppeteer/issues/8315)) ([936ed86](https://github.com/puppeteer/puppeteer/commit/936ed8607ec0c3798d2b22b590d0be0ad361a888)) +* detect Firefox in connect() automatically ([#8718](https://github.com/puppeteer/puppeteer/issues/8718)) ([2abd772](https://github.com/puppeteer/puppeteer/commit/2abd772c9c3d2b86deb71541eaac41aceef94356)) +* use CDP's auto-attach mechanism ([#8520](https://github.com/puppeteer/puppeteer/issues/8520)) ([2cbfdeb](https://github.com/puppeteer/puppeteer/commit/2cbfdeb0ca388a45cedfae865266230e1291bd29)) + + +### Bug Fixes + +* address flakiness in frame handling ([#8688](https://github.com/puppeteer/puppeteer/issues/8688)) ([6f81b23](https://github.com/puppeteer/puppeteer/commit/6f81b23728a511f7b89eaa2b8f850b22d6c4ab24)) +* disable AcceptCHFrame ([#8706](https://github.com/puppeteer/puppeteer/issues/8706)) ([96d9608](https://github.com/puppeteer/puppeteer/commit/96d9608d1de17877414a649a0737661894dd96c8)), closes [#8479](https://github.com/puppeteer/puppeteer/issues/8479) +* use loaderId to reduce test flakiness ([#8717](https://github.com/puppeteer/puppeteer/issues/8717)) ([d2f6db2](https://github.com/puppeteer/puppeteer/commit/d2f6db20735342bb3f419e85adbd51ed10470044)) + +## [15.5.0](https://github.com/puppeteer/puppeteer/compare/v15.4.2...v15.5.0) (2022-07-21) + + +### Features + +* **chromium:** roll to Chromium 105.0.5173.0 (r1022525) ([#8682](https://github.com/puppeteer/puppeteer/issues/8682)) ([f1b8ad3](https://github.com/puppeteer/puppeteer/commit/f1b8ad3269286800d31818ea4b6b3ee23f7437c3)) + +## [15.4.2](https://github.com/puppeteer/puppeteer/compare/v15.4.1...v15.4.2) (2022-07-21) + + +### Bug Fixes + +* taking a screenshot with null viewport should be possible ([#8680](https://github.com/puppeteer/puppeteer/issues/8680)) ([2abb9f0](https://github.com/puppeteer/puppeteer/commit/2abb9f0c144779d555ecbf337a759440d0282cba)), closes [#8673](https://github.com/puppeteer/puppeteer/issues/8673) + +## [15.4.1](https://github.com/puppeteer/puppeteer/compare/v15.4.0...v15.4.1) (2022-07-21) + + +### Bug Fixes + +* import URL ([#8670](https://github.com/puppeteer/puppeteer/issues/8670)) ([34ab5ca](https://github.com/puppeteer/puppeteer/commit/34ab5ca50353ffb6a6345a8984b724a6f42fb726)) + +## [15.4.0](https://github.com/puppeteer/puppeteer/compare/v15.3.2...v15.4.0) (2022-07-13) + + +### Features + +* expose the page getter on Frame ([#8657](https://github.com/puppeteer/puppeteer/issues/8657)) ([af08c5c](https://github.com/puppeteer/puppeteer/commit/af08c5c90380c853e8257a51298bfed4b0635779)) + + +### Bug Fixes + +* ignore *.tsbuildinfo ([#8662](https://github.com/puppeteer/puppeteer/issues/8662)) ([edcdf21](https://github.com/puppeteer/puppeteer/commit/edcdf217cefbf31aee5a2f571abac429dd81f3a0)) + +## [15.3.2](https://github.com/puppeteer/puppeteer/compare/v15.3.1...v15.3.2) (2022-07-08) + + +### Bug Fixes + +* cache dynamic imports ([#8652](https://github.com/puppeteer/puppeteer/issues/8652)) ([1de0383](https://github.com/puppeteer/puppeteer/commit/1de0383abf6be31cf06faede3e59b087a2958227)) +* expose a RemoteObject getter ([#8642](https://github.com/puppeteer/puppeteer/issues/8642)) ([d0c4291](https://github.com/puppeteer/puppeteer/commit/d0c42919956bd36ad7993a0fc1de86e886e39f62)), closes [#8639](https://github.com/puppeteer/puppeteer/issues/8639) +* **page:** fix page.#scrollIntoViewIfNeeded method ([#8631](https://github.com/puppeteer/puppeteer/issues/8631)) ([b47f066](https://github.com/puppeteer/puppeteer/commit/b47f066c2c068825e3b65cfe17b6923c77ad30b9)) + +## [15.3.1](https://github.com/puppeteer/puppeteer/compare/v15.3.0...v15.3.1) (2022-07-06) + + +### Bug Fixes + +* extends `ElementHandle` to `Node`s ([#8552](https://github.com/puppeteer/puppeteer/issues/8552)) ([5ff205d](https://github.com/puppeteer/puppeteer/commit/5ff205dc8b659eb8864b4b1862105d21dd334c8f)) + +## [15.3.0](https://github.com/puppeteer/puppeteer/compare/v15.2.0...v15.3.0) (2022-07-01) + + +### Features + +* add documentation ([#8593](https://github.com/puppeteer/puppeteer/issues/8593)) ([066f440](https://github.com/puppeteer/puppeteer/commit/066f440ba7bdc9aca9423d7205adf36f2858bd78)) + + +### Bug Fixes + +* remove unused imports ([#8613](https://github.com/puppeteer/puppeteer/issues/8613)) ([0cf4832](https://github.com/puppeteer/puppeteer/commit/0cf4832878731ffcfc84570315f326eb851d7629)) + +## [15.2.0](https://github.com/puppeteer/puppeteer/compare/v15.1.1...v15.2.0) (2022-06-29) + + +### Features + +* add fromSurface option to page.screenshot ([#8496](https://github.com/puppeteer/puppeteer/issues/8496)) ([79e1198](https://github.com/puppeteer/puppeteer/commit/79e11985ba44b72b1ad6b8cd861fe316f1945e64)) +* export public types only ([#8584](https://github.com/puppeteer/puppeteer/issues/8584)) ([7001322](https://github.com/puppeteer/puppeteer/commit/7001322cd1cf9f77ee2c370d50a6707e7aaad72d)) + + +### Bug Fixes + +* clean up tmp profile dirs when browser is closed ([#8580](https://github.com/puppeteer/puppeteer/issues/8580)) ([9787a1d](https://github.com/puppeteer/puppeteer/commit/9787a1d8df7768017b36d42327faab402695c4bb)) + +## [15.1.1](https://github.com/puppeteer/puppeteer/compare/v15.1.0...v15.1.1) (2022-06-25) + + +### Bug Fixes + +* export `ElementHandle` ([e0198a7](https://github.com/puppeteer/puppeteer/commit/e0198a79e06c8bb72dde554db0246a3db5fec4c2)) + +## [15.1.0](https://github.com/puppeteer/puppeteer/compare/v15.0.2...v15.1.0) (2022-06-24) + + +### Features + +* **chromium:** roll to Chromium 104.0.5109.0 (r1011831) ([#8569](https://github.com/puppeteer/puppeteer/issues/8569)) ([fb7d31e](https://github.com/puppeteer/puppeteer/commit/fb7d31e3698428560e1f654d33782d241192f48f)) + +## [15.0.2](https://github.com/puppeteer/puppeteer/compare/v15.0.1...v15.0.2) (2022-06-24) + + +### Bug Fixes + +* CSS coverage should work with empty stylesheets ([#8570](https://github.com/puppeteer/puppeteer/issues/8570)) ([383e855](https://github.com/puppeteer/puppeteer/commit/383e8558477fae7708734ab2160ef50f385e2983)), closes [#8535](https://github.com/puppeteer/puppeteer/issues/8535) + +## [15.0.1](https://github.com/puppeteer/puppeteer/compare/v15.0.0...v15.0.1) (2022-06-24) + + +### Bug Fixes + +* infer unioned handles ([#8562](https://github.com/puppeteer/puppeteer/issues/8562)) ([8100cbb](https://github.com/puppeteer/puppeteer/commit/8100cbb29569541541f61001983efb9a80d89890)) + +## [15.0.0](https://github.com/puppeteer/puppeteer/compare/v14.4.1...v15.0.0) (2022-06-23) + + +### ⚠ BREAKING CHANGES + +* type inference for evaluation types (#8547) + +### Features + +* add experimental `client` to `HTTPRequest` ([#8556](https://github.com/puppeteer/puppeteer/issues/8556)) ([ec79f3a](https://github.com/puppeteer/puppeteer/commit/ec79f3a58a44c9ea60a82f9cd2df4c8f19e82ab8)) +* type inference for evaluation types ([#8547](https://github.com/puppeteer/puppeteer/issues/8547)) ([26c3acb](https://github.com/puppeteer/puppeteer/commit/26c3acbb0795eb66f29479f442e156832f794f01)) + +## [14.4.1](https://github.com/puppeteer/puppeteer/compare/v14.4.0...v14.4.1) (2022-06-17) + + +### Bug Fixes + +* avoid `instanceof Object` check in `isErrorLike` ([#8527](https://github.com/puppeteer/puppeteer/issues/8527)) ([6cd5cd0](https://github.com/puppeteer/puppeteer/commit/6cd5cd043997699edca6e3458f90adc1118cf4a5)) +* export `devices`, `errors`, and more ([cba58a1](https://github.com/puppeteer/puppeteer/commit/cba58a12c4e2043f6a5acf7d4754e4a7b7f6e198)) + +## [14.4.0](https://github.com/puppeteer/puppeteer/compare/v14.3.0...v14.4.0) (2022-06-13) + + +### Features + +* export puppeteer methods ([#8493](https://github.com/puppeteer/puppeteer/issues/8493)) ([465a7c4](https://github.com/puppeteer/puppeteer/commit/465a7c405f01fcef99380ffa69d86042a1f5618f)) +* support node-like environments ([#8490](https://github.com/puppeteer/puppeteer/issues/8490)) ([f64ec20](https://github.com/puppeteer/puppeteer/commit/f64ec2051b9b2d12225abba6ffe9551da9751bf7)) + + +### Bug Fixes + +* parse empty options in \<select\> ([#8489](https://github.com/puppeteer/puppeteer/issues/8489)) ([b30f3f4](https://github.com/puppeteer/puppeteer/commit/b30f3f44cdabd9545c4661cd755b9d49e5c144cd)) +* use error-like ([#8504](https://github.com/puppeteer/puppeteer/issues/8504)) ([4d35990](https://github.com/puppeteer/puppeteer/commit/4d359906a44e4ddd5ec54a523cfd9076048d3433)) +* use OS-independent abs. path check ([#8505](https://github.com/puppeteer/puppeteer/issues/8505)) ([bfd4e68](https://github.com/puppeteer/puppeteer/commit/bfd4e68f25bec6e00fd5cbf261813f8297d362ee)) + +## [14.3.0](https://github.com/puppeteer/puppeteer/compare/v14.2.1...v14.3.0) (2022-06-07) + + +### Features + +* use absolute URL for EVALUATION_SCRIPT_URL ([#8481](https://github.com/puppeteer/puppeteer/issues/8481)) ([e142560](https://github.com/puppeteer/puppeteer/commit/e14256010d2d84d613cd3c6e7999b0705115d4bf)), closes [#8424](https://github.com/puppeteer/puppeteer/issues/8424) + + +### Bug Fixes + +* don't throw on bad access ([#8472](https://github.com/puppeteer/puppeteer/issues/8472)) ([e837866](https://github.com/puppeteer/puppeteer/commit/e8378666c671e5703aec4f52912de2aac94e1828)) +* Kill browser process when killing process group fails ([#8477](https://github.com/puppeteer/puppeteer/issues/8477)) ([7dc8e37](https://github.com/puppeteer/puppeteer/commit/7dc8e37a23d025bb2c31efb9c060c7f6e00179b4)) +* only lookup `localhost` for DNS lookups ([1b025b4](https://github.com/puppeteer/puppeteer/commit/1b025b4c8466fe64da0fa2050eaa02b7764770b1)) +* robustly check for launch executable ([#8468](https://github.com/puppeteer/puppeteer/issues/8468)) ([b54dc55](https://github.com/puppeteer/puppeteer/commit/b54dc55f7622ee2b75afd3bd9fe118dd2f144f40)) + +## [14.2.1](https://github.com/puppeteer/puppeteer/compare/v14.2.0...v14.2.1) (2022-06-02) + + +### Bug Fixes + +* use isPageTargetCallback in Browser::pages() ([#8460](https://github.com/puppeteer/puppeteer/issues/8460)) ([5c9050a](https://github.com/puppeteer/puppeteer/commit/5c9050aea0fe8d57114130fe38bd33ed2b4955d6)) + +## [14.2.0](https://github.com/puppeteer/puppeteer/compare/v14.1.2...v14.2.0) (2022-06-01) + + +### 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)) +* support node 18 ([#8447](https://github.com/puppeteer/puppeteer/issues/8447)) ([f2d8276](https://github.com/puppeteer/puppeteer/commit/f2d8276d6e745a7547b8ce54c3f50934bb70de0b)) +* use strict typescript ([#8401](https://github.com/puppeteer/puppeteer/issues/8401)) ([b4e751f](https://github.com/puppeteer/puppeteer/commit/b4e751f29cb6fd4c3cc41fe702de83721f0eb6dc)) + + +### Bug Fixes + +* multiple same request event listener ([#8404](https://github.com/puppeteer/puppeteer/issues/8404)) ([9211015](https://github.com/puppeteer/puppeteer/commit/92110151d9a33f26abc07bc805f4f2f3943697a0)) +* NodeNext incompatibility in package.json ([#8445](https://github.com/puppeteer/puppeteer/issues/8445)) ([c4898a7](https://github.com/puppeteer/puppeteer/commit/c4898a7a2e69681baac55366848da6688f0d8790)) +* 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/remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json b/remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json new file mode 100644 index 0000000000..88fcdbfd38 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/api-extractor.docs.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer.d.ts", + + "extends": "./api-extractor.json", + + "dtsRollup": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "<projectFolder>/../../docs/<unscopedPackageName>.api.json" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/api-extractor.json b/remote/test/puppeteer/packages/puppeteer/api-extractor.json new file mode 100644 index 0000000000..486b3929e7 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/api-extractor.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/puppeteer.d.ts", + "bundledPackages": ["puppeteer-core"], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": false + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "alphaTrimmedFilePath": "lib/types.d.ts" + }, + + "tsdocMetadata": { + "enabled": false + }, + + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + + "extractorMessageReporting": { + "ae-wrong-input-file-type": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + }, + "default": { + "logLevel": "warning" + } + }, + + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/install.js b/remote/test/puppeteer/packages/puppeteer/install.js new file mode 100644 index 0000000000..c00cbeb273 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/install.js @@ -0,0 +1,43 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file is part of public API. + * + * By default, the `puppeteer` package runs this script during the installation + * process unless one of the env flags is provided. + * `puppeteer-core` package doesn't include this step at all. However, it's + * still possible to install a supported browser using this script when + * necessary. + */ + +const fs = require('fs'); +const path = require('path'); + +// Need to ensure TS is compiled before loading the installer +if (!fs.existsSync(path.join(__dirname, 'lib'))) { + console.warn( + 'Skipping browser installation because the Puppeteer build is not available. Run `npm install` again after you have re-built Puppeteer.' + ); + process.exit(0); +} + +try { + const {downloadBrowser} = require('puppeteer/internal/node/install.js'); + downloadBrowser(); +} catch (err) { + console.warn('Browser download failed', err); +} diff --git a/remote/test/puppeteer/packages/puppeteer/package.json b/remote/test/puppeteer/packages/puppeteer/package.json new file mode 100644 index 0000000000..fac27c2b19 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/package.json @@ -0,0 +1,126 @@ +{ + "name": "puppeteer", + "version": "20.1.0", + "description": "A high-level API to control headless Chrome over the DevTools Protocol", + "keywords": [ + "puppeteer", + "chrome", + "headless", + "automation" + ], + "type": "commonjs", + "main": "./lib/cjs/puppeteer/puppeteer.js", + "types": "./lib/types.d.ts", + "exports": { + ".": { + "types": "./lib/types.d.ts", + "import": "./lib/esm/puppeteer/puppeteer.js", + "require": "./lib/cjs/puppeteer/puppeteer.js" + }, + "./internal/*": { + "import": "./lib/esm/puppeteer/*", + "require": "./lib/cjs/puppeteer/*" + }, + "./*": { + "import": "./*", + "require": "./*" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer" + }, + "scripts": { + "build:docs": "wireit", + "build:tsc": "wireit", + "build:types": "wireit", + "build": "wireit", + "clean": "tsc -b --clean && rm -rf lib", + "generate:package-json": "wireit", + "postinstall": "node install.js", + "prepack": "wireit" + }, + "wireit": { + "prepack": { + "command": "tsx ../../tools/cp.ts ../../README.md README.md", + "files": [ + "../../README.md" + ], + "output": [ + "README.md" + ] + }, + "build": { + "dependencies": [ + "build:tsc", + "build:types" + ] + }, + "generate:package-json": { + "command": "tsx ../../tools/generate_module_package_json.ts lib/esm/package.json", + "files": [ + "../../tools/generate_module_package_json.ts" + ], + "output": [ + "lib/esm/package.json" + ] + }, + "build:docs": { + "command": "api-extractor run --local --config \"./api-extractor.docs.json\"", + "files": [ + "api-extractor.docs.json", + "lib/esm/puppeteer/puppeteer-core.d.ts", + "tsconfig.json" + ], + "dependencies": [ + "build:tsc" + ] + }, + "build:tsc": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "../puppeteer-core:build", + "../browsers:build", + "generate:package-json" + ], + "files": [ + "src/**" + ], + "output": [ + "lib/{cjs,esm}/**", + "!lib/esm/package.json" + ] + }, + "build:types": { + "command": "api-extractor run --local && eslint --cache-location .eslintcache --cache --ext=ts --no-ignore --no-eslintrc -c=../../.eslintrc.types.cjs --fix lib/types.d.ts", + "files": [ + "../../.eslintrc.types.cjs", + "api-extractor.json", + "lib/esm/puppeteer/types.d.ts", + "tsconfig.json" + ], + "output": [ + "lib/types.d.ts" + ], + "dependencies": [ + "build:tsc" + ] + } + }, + "files": [ + "lib", + "install.js", + "!*.tsbuildinfo" + ], + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "cosmiconfig": "8.1.3", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "20.1.0", + "@puppeteer/browsers": "1.0.0" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts b/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts new file mode 100644 index 0000000000..d682bac4de --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/src/getConfiguration.ts @@ -0,0 +1,97 @@ +import {homedir} from 'os'; +import {join} from 'path'; + +import {cosmiconfigSync} from 'cosmiconfig'; +import {Configuration, Product} from 'puppeteer-core'; + +/** + * @internal + */ +function isSupportedProduct(product: unknown): product is Product { + switch (product) { + case 'chrome': + case 'firefox': + return true; + default: + return false; + } +} + +/** + * @internal + */ +export const getConfiguration = (): Configuration => { + const result = cosmiconfigSync('puppeteer').search(); + const configuration: Configuration = result ? result.config : {}; + + // Merging environment variables. + configuration.defaultProduct = (process.env['PUPPETEER_PRODUCT'] ?? + process.env['npm_config_puppeteer_product'] ?? + process.env['npm_package_config_puppeteer_product'] ?? + configuration.defaultProduct ?? + 'chrome') as Product; + + configuration.executablePath = + process.env['PUPPETEER_EXECUTABLE_PATH'] ?? + process.env['npm_config_puppeteer_executable_path'] ?? + process.env['npm_package_config_puppeteer_executable_path'] ?? + configuration.executablePath; + + // Default to skipDownload if executablePath is set + if (configuration.executablePath) { + configuration.skipDownload = true; + } + + // Set skipDownload explicitly or from default + configuration.skipDownload = Boolean( + process.env['PUPPETEER_SKIP_DOWNLOAD'] ?? + process.env['npm_config_puppeteer_skip_download'] ?? + process.env['npm_package_config_puppeteer_skip_download'] ?? + configuration.skipDownload + ); + + // Prepare variables used in browser downloading + if (!configuration.skipDownload) { + configuration.browserRevision = + process.env['PUPPETEER_BROWSER_REVISION'] ?? + process.env['npm_config_puppeteer_browser_revision'] ?? + process.env['npm_package_config_puppeteer_browser_revision'] ?? + configuration.browserRevision; + configuration.downloadHost = + process.env['PUPPETEER_DOWNLOAD_HOST'] ?? + process.env['npm_config_puppeteer_download_host'] ?? + process.env['npm_package_config_puppeteer_download_host'] ?? + configuration.downloadHost; + configuration.downloadPath = + process.env['PUPPETEER_DOWNLOAD_PATH'] ?? + process.env['npm_config_puppeteer_download_path'] ?? + process.env['npm_package_config_puppeteer_download_path'] ?? + configuration.downloadPath; + } + + configuration.cacheDirectory = + process.env['PUPPETEER_CACHE_DIR'] ?? + process.env['npm_config_puppeteer_cache_dir'] ?? + process.env['npm_package_config_puppeteer_cache_dir'] ?? + configuration.cacheDirectory ?? + join(homedir(), '.cache', 'puppeteer'); + configuration.temporaryDirectory = + process.env['PUPPETEER_TMP_DIR'] ?? + process.env['npm_config_puppeteer_tmp_dir'] ?? + process.env['npm_package_config_puppeteer_tmp_dir'] ?? + configuration.temporaryDirectory; + + configuration.experiments ??= {}; + + configuration.logLevel = (process.env['PUPPETEER_LOGLEVEL'] ?? + process.env['npm_config_LOGLEVEL'] ?? + process.env['npm_package_config_LOGLEVEL'] ?? + configuration.logLevel) as 'silent' | 'error' | 'warn'; + + // Validate configuration. + if (!isSupportedProduct(configuration.defaultProduct)) { + throw new Error(`Unsupported product ${configuration.defaultProduct}`); + } + + return configuration; +}; diff --git a/remote/test/puppeteer/packages/puppeteer/src/node/install.ts b/remote/test/puppeteer/packages/puppeteer/src/node/install.ts new file mode 100644 index 0000000000..f3cd7587b2 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/src/node/install.ts @@ -0,0 +1,131 @@ +/** + * 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 { + install, + Browser, + resolveBuildId, + makeProgressCallback, + detectBrowserPlatform, +} from '@puppeteer/browsers'; +import {Product} from 'puppeteer-core'; +import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js'; + +import {getConfiguration} from '../getConfiguration.js'; + +/** + * @internal + */ +const supportedProducts = { + chrome: 'Chrome', + firefox: 'Firefox Nightly', +} as const; + +/** + * @internal + */ +export async function downloadBrowser(): Promise<void> { + overrideProxy(); + + const configuration = getConfiguration(); + if (configuration.skipDownload) { + logPolitely('**INFO** Skipping browser download as instructed.'); + return; + } + + const downloadHost = configuration.downloadHost; + + const platform = detectBrowserPlatform(); + if (!platform) { + throw new Error('The current platform is not supported.'); + } + + const product = configuration.defaultProduct!; + const browser = productToBrowser(product); + + const unresolvedBuildId = + configuration.browserRevision || PUPPETEER_REVISIONS[product] || 'latest'; + + const buildId = await resolveBuildId(browser, platform, unresolvedBuildId); + + try { + const result = await install({ + browser, + cacheDir: configuration.cacheDirectory!, + platform, + buildId, + downloadProgressCallback: makeProgressCallback(browser, buildId), + // TODO: remove downloadHost in favour of baseDownloadUrl. The "host" of + // Firefox is already a URL and not a host. This would be a breaking change. + baseUrl: downloadHost, + }); + + logPolitely( + `${supportedProducts[product]} (${result.buildId}) downloaded to ${result.path}` + ); + } catch (error) { + console.error( + `ERROR: Failed to set up ${supportedProducts[product]} r${buildId}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.` + ); + console.error(error); + process.exit(1); + } +} + +function productToBrowser(product?: Product) { + switch (product) { + case 'chrome': + return Browser.CHROME; + case 'firefox': + return Browser.FIREFOX; + } + return Browser.CHROME; +} + +/** + * @internal + */ +function logPolitely(toBeLogged: unknown): void { + const logLevel = process.env['npm_config_loglevel'] || ''; + const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; + + // eslint-disable-next-line no-console + if (!logLevelDisplay) { + console.log(toBeLogged); + } +} + +/** + * @internal + */ +function overrideProxy() { + // Override current environment proxy settings with npm configuration, if any. + const NPM_HTTPS_PROXY = + process.env['npm_config_https_proxy'] || process.env['npm_config_proxy']; + const NPM_HTTP_PROXY = + process.env['npm_config_http_proxy'] || process.env['npm_config_proxy']; + const NPM_NO_PROXY = process.env['npm_config_no_proxy']; + + if (NPM_HTTPS_PROXY) { + process.env['HTTPS_PROXY'] = NPM_HTTPS_PROXY; + } + if (NPM_HTTP_PROXY) { + process.env['HTTP_PROXY'] = NPM_HTTP_PROXY; + } + if (NPM_NO_PROXY) { + process.env['NO_PROXY'] = NPM_NO_PROXY; + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts b/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts new file mode 100644 index 0000000000..1ba37feab0 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/src/puppeteer.ts @@ -0,0 +1,54 @@ +/** + * 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. + */ + +export {Protocol} from 'puppeteer-core'; + +export * from 'puppeteer-core/internal/puppeteer-core.js'; + +import {PuppeteerNode} from 'puppeteer-core/internal/node/PuppeteerNode.js'; + +import {getConfiguration} from './getConfiguration.js'; + +const configuration = getConfiguration(); + +/** + * @public + */ +const puppeteer = new PuppeteerNode({ + isPuppeteerCore: false, + configuration, +}); + +export const { + /** + * @public + */ + connect, + /** + * @public + */ + defaultArgs, + /** + * @public + */ + executablePath, + /** + * @public + */ + launch, +} = puppeteer; + +export default puppeteer; diff --git a/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json new file mode 100644 index 0000000000..ce5e013027 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "NodeNext", + "outDir": "../lib/cjs/puppeteer" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json new file mode 100644 index 0000000000..82c67449bb --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/src/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../lib/esm/puppeteer", + "moduleResolution": "NodeNext" + } +} diff --git a/remote/test/puppeteer/packages/puppeteer/tsconfig.json b/remote/test/puppeteer/packages/puppeteer/tsconfig.json new file mode 100644 index 0000000000..405998de4b --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "compilerOptions": { + // API extractor doesn't work well with NodeNext module resolution, so we + // just stick with ol'fashion path resolution. + "baseUrl": ".", + "paths": { + "puppeteer-core/internal/*": ["../puppeteer-core/lib/esm/puppeteer/*"] + } + }, + "references": [ + {"path": "src/tsconfig.esm.json"}, + {"path": "src/tsconfig.cjs.json"} + ] +} diff --git a/remote/test/puppeteer/packages/testserver/CHANGELOG.md b/remote/test/puppeteer/packages/testserver/CHANGELOG.md new file mode 100644 index 0000000000..bb971ef46d --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [0.6.0](https://github.com/puppeteer/puppeteer/compare/testserver-v0.5.0...testserver-v0.6.0) (2022-10-05) + + +### Features + +* separate puppeteer and puppeteer-core ([#9023](https://github.com/puppeteer/puppeteer/issues/9023)) ([f42336c](https://github.com/puppeteer/puppeteer/commit/f42336cf83982332829ca7e14ee48d8676e11545)) diff --git a/remote/test/puppeteer/packages/testserver/LICENSE b/remote/test/puppeteer/packages/testserver/LICENSE new file mode 100644 index 0000000000..afdfe50e72 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://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 + + 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. diff --git a/remote/test/puppeteer/packages/testserver/README.md b/remote/test/puppeteer/packages/testserver/README.md new file mode 100644 index 0000000000..d22b2da449 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/README.md @@ -0,0 +1,18 @@ +# TestServer + +This test server is used internally by Puppeteer to test Puppeteer itself. + +### Example + +```ts +const {TestServer} = require('@pptr/testserver'); + +(async(() => { + const httpServer = await TestServer.create(__dirname, 8000), + const httpsServer = await TestServer.createHTTPS(__dirname, 8001) + httpServer.setRoute('/hello', (req, res) => { + res.end('Hello, world!'); + }); + console.log('HTTP and HTTPS servers are running!'); +})(); +``` diff --git a/remote/test/puppeteer/packages/testserver/cert.pem b/remote/test/puppeteer/packages/testserver/cert.pem new file mode 100644 index 0000000000..fd3838535a --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWDCCAkCgAwIBAgIUM8Tmw+D1j+eVz9x9So4zRVqFsKowDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMB4XDTIwMDUxMzA4MDQyOVoX +DTMwMDUxMTA4MDQyOVowGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWbbhgc6CnWywd8xGETT1mfLi3wi +KIbpAUHghLF4sj0jXz8vLh/4oicpQ12d6bsz+IAi7qrdXNh11P5nEej6/Gx4fWzB +gGdrJFGPqsvXuhYdzZAmy6xOaWcLIJeQ543bXv3YeST7EGRXJBc/ocTo2jIGTGjq +hksFaid910VQlX3KGOLTDMUCk00TeEYBTTUx47PWoIsxVqbl2RzVXRSWL5hlPWlW +29/BQtBGmsXxZyWtqqHudiUulGBSr4LcPyicZLI8nqCqD0ioS0TEmGh61nRBuwBa +xmLCvPmpt0+sDuOU+1bme3w8juvTVToBIFxGB86rADd3ys+8NeZzXqi+bQIDAQAB +o4GVMIGSMB0GA1UdDgQWBBT/m3vdkZpQyVQFdYrKHVoAHXDFODAfBgNVHSMEGDAW +gBT/m3vdkZpQyVQFdYrKHVoAHXDFODAPBgNVHRMBAf8EBTADAQH/MD8GA1UdEQQ4 +MDaCGHd3dy5wdXBwZXRlZXItdGVzdHMudGVzdIIad3d3LnB1cHBldGVlci10ZXN0 +cy0xLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAI1qp5ZppV1R3e8XxzwwkFDPFN8W +Pe3AoqhAKyJnJl1NUn9q3sroEeSQRhODWUHCd7lENzhsT+3mzonNNkN9B/hq0rpK +KHHczXILDqdyuxH3LxQ1VHGE8VN2NbdkfobtzAsA3woiJxOuGeusXJnKB4kJQeIP +V+BMEZWeaSDC2PREkG7GOezmE1/WDUCYaorPw2whdCA5wJvTW3zXpJjYhfsld+5z +KuErx4OCxRJij73/BD9SpLxDEY1cdl819F1IvxsRGhmTIaSly2hQLrhOgo1jgZtV +FGCa6DSlXnQGLaV+N+ssR0lkCksNrNBVDfA1bP5bT/4VCcwUWwm9TUeF0Qo= +-----END CERTIFICATE----- diff --git a/remote/test/puppeteer/packages/testserver/key.pem b/remote/test/puppeteer/packages/testserver/key.pem new file mode 100644 index 0000000000..cbc3acb229 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZtuGBzoKdbLB +3zEYRNPWZ8uLfCIohukBQeCEsXiyPSNfPy8uH/iiJylDXZ3puzP4gCLuqt1c2HXU +/mcR6Pr8bHh9bMGAZ2skUY+qy9e6Fh3NkCbLrE5pZwsgl5Dnjdte/dh5JPsQZFck +Fz+hxOjaMgZMaOqGSwVqJ33XRVCVfcoY4tMMxQKTTRN4RgFNNTHjs9agizFWpuXZ +HNVdFJYvmGU9aVbb38FC0EaaxfFnJa2qoe52JS6UYFKvgtw/KJxksjyeoKoPSKhL +RMSYaHrWdEG7AFrGYsK8+am3T6wO45T7VuZ7fDyO69NVOgEgXEYHzqsAN3fKz7w1 +5nNeqL5tAgMBAAECggEAKPveo0xBHnxhidZzBM9xKixX7D0a/a3IKI6ZQmfzPz8U +97HhT+2OHyfS+qVEzribPRULEtZ1uV7Ne7R5958iKc/63yFGpTl6++nVzn1p++sl +AV2Zr1gHqehlgnLr7eRhmh0OOZ5nM32ZdhDorH3tMLu6gc5xZktKkS4t6Vx8hj3a +Docx+rbawp8GRd0p7I6vzIE3bsDab8hC+RTRO63q2G0BqgKwV9ZNtJxQgcDJ5L8N +6gtM2z5nKXAIOCbCQYa1PsrDh3IRA/ZNxEeA9G3YQjwlZYCWmdRRplgDraYxcTBO +oQGjaLwICNdcprMacPD6cCSgrI+PadzyMsAuk9SgpQKBgQDO9PT4gK40Pm+Damxv ++tWYBFmvn3vasmyolc1zVDltsxQbQTjKhVpTLXTTGmrIhDXEIIV9I4rg164WptQs +6Brp2EwYR7ZJIrjvXs/9i2QTW1ZXvhdiWpB3s+RXD5VHGovHUadcI6wOgw2Cl+Jk +zXjSIgyXKM99N1MAonuR7DyzTwKBgQDMmPX+9vWZMpS/gc6JLQiPPoGszE6tYjXg +W3LpRUNqmO0/bDDjslbebDgrGAmhlkJlxzH6gz96VmGm1evEGPEet3euy8S9zuM3 +LCgEM9Ulqa3JbInwtKupmKv76Im+XWLLSxAXbfiel1zFRRwxI99A3ad0QRZ6Bov5 +3cHJBwvzgwKBgAU5HW2gIcVjxgC1EOOKmxVpFrJd/gw48JEYpsTAXWqtWFaPwNUr +pGnw/b/OLN++pnS6tWPBH+Ioz1X3A+fWO8enE9SRCsKxw6UW6XzmpbHvXjB8ta5f +xsGeoqan2AahXuG659RlehQrro2bM7WDkgcLoPG3r/TjDo83ipLWOXn1AoGAKWiL +4R56dpcWI+xRsNG8ecFc3Ww8QDswTEg16aBrFJf+7GcpPexKSJn+hDpJOLsAlTjL +lLgbkNcKzIlfPkEOC/l175quJvxIYFI/hxo2eXjuA2ZERMNMOvb7V/CocC7WX+7B +Qvyu5OodjI+ANTHdbXNvAMhrlCbfDaMkJVuXv6ECgYBzvY4aYmVoFsr+72/EfLls +Dz9pi55tUUWc61w6ovd+iliawvXeGi4wibtTH4iGj/C2sJIaMmOD99NQ7Oi/x89D +oMgSUemkoFL8FGsZGyZ7szqxyON1jP42Bm2MQrW5kIf7Y4yaIGhoak5JNxn2JUyV +gupVbY1mQ1GTPByxHeLh1w== +-----END PRIVATE KEY----- diff --git a/remote/test/puppeteer/packages/testserver/package.json b/remote/test/puppeteer/packages/testserver/package.json new file mode 100644 index 0000000000..e21cf44b45 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/package.json @@ -0,0 +1,33 @@ +{ + "name": "@pptr/testserver", + "version": "0.6.0", + "description": "testing server", + "main": "lib/index.js", + "scripts": { + "build": "wireit", + "clean": "tsc -b --clean && rm -rf lib" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "files": [ + "src/**" + ], + "output": [ + "lib/**", + "tsconfig.tsbuildinfo" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/packages/testserver" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "mime": "3.0.0", + "ws": "8.13.0" + } +} diff --git a/remote/test/puppeteer/packages/testserver/src/index.ts b/remote/test/puppeteer/packages/testserver/src/index.ts new file mode 100644 index 0000000000..5304e91e09 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/src/index.ts @@ -0,0 +1,303 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {readFile, readFileSync} from 'fs'; +import { + createServer as createHttpServer, + IncomingMessage, + RequestListener, + Server as HttpServer, + ServerResponse, +} from 'http'; +import { + createServer as createHttpsServer, + Server as HttpsServer, + ServerOptions as HttpsServerOptions, +} from 'https'; +import {AddressInfo} from 'net'; +import {join} from 'path'; +import {Duplex} from 'stream'; +import {gzip} from 'zlib'; + +import {getType as getMimeType} from 'mime'; +import {Server as WebSocketServer, WebSocket} from 'ws'; + +interface Subscriber { + resolve: (msg: IncomingMessage) => void; + reject: (err?: Error) => void; + promise: Promise<IncomingMessage>; +} + +type TestIncomingMessage = IncomingMessage & {postBody?: Promise<string>}; + +export class TestServer { + PORT!: number; + PREFIX!: string; + CROSS_PROCESS_PREFIX!: string; + EMPTY_PAGE!: string; + + #dirPath: string; + #server: HttpsServer | HttpServer; + #wsServer: WebSocketServer; + + #startTime = new Date(); + #cachedPathPrefix?: string; + + #connections = new Set<Duplex>(); + #routes = new Map< + string, + (msg: IncomingMessage, res: ServerResponse) => void + >(); + #auths = new Map<string, {username: string; password: string}>(); + #csp = new Map<string, string>(); + #gzipRoutes = new Set<string>(); + #requestSubscribers = new Map<string, Subscriber>(); + + static async create(dirPath: string): Promise<TestServer> { + let res!: (value: unknown) => void; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath); + server.#server.once('listening', res); + server.#server.listen(0); + await promise; + return server; + } + + static async createHTTPS(dirPath: string): Promise<TestServer> { + let res!: (value: unknown) => void; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath, { + key: readFileSync(join(__dirname, '..', 'key.pem')), + cert: readFileSync(join(__dirname, '..', 'cert.pem')), + passphrase: 'aaaa', + }); + server.#server.once('listening', res); + server.#server.listen(0); + await promise; + return server; + } + + constructor(dirPath: string, sslOptions?: HttpsServerOptions) { + this.#dirPath = dirPath; + + if (sslOptions) { + this.#server = createHttpsServer(sslOptions, this.#onRequest); + } else { + this.#server = createHttpServer(this.#onRequest); + } + this.#server.on('connection', this.#onServerConnection); + this.#wsServer = new WebSocketServer({server: this.#server}); + this.#wsServer.on('connection', this.#onWebSocketConnection); + } + + #onServerConnection = (connection: Duplex): void => { + this.#connections.add(connection); + // ECONNRESET is a legit error given + // that tab closing simply kills process. + connection.on('error', error => { + if ((error as NodeJS.ErrnoException).code !== 'ECONNRESET') { + throw error; + } + }); + connection.once('close', () => { + return this.#connections.delete(connection); + }); + }; + + get port(): number { + return (this.#server.address() as AddressInfo).port; + } + + enableHTTPCache(pathPrefix: string): void { + this.#cachedPathPrefix = pathPrefix; + } + + setAuth(path: string, username: string, password: string): void { + this.#auths.set(path, {username, password}); + } + + enableGzip(path: string): void { + this.#gzipRoutes.add(path); + } + + setCSP(path: string, csp: string): void { + this.#csp.set(path, csp); + } + + async stop(): Promise<void> { + this.reset(); + for (const socket of this.#connections) { + socket.destroy(); + } + this.#connections.clear(); + await new Promise(x => { + return this.#server.close(x); + }); + } + + setRoute( + path: string, + handler: (req: IncomingMessage, res: ServerResponse) => void + ): void { + this.#routes.set(path, handler); + } + + setRedirect(from: string, to: string): void { + this.setRoute(from, (_, res) => { + res.writeHead(302, {location: to}); + res.end(); + }); + } + + waitForRequest(path: string): Promise<TestIncomingMessage> { + const subscriber = this.#requestSubscribers.get(path); + if (subscriber) { + return subscriber.promise; + } + let resolve!: (value: IncomingMessage) => void; + let reject!: (reason?: Error) => void; + const promise = new Promise<IncomingMessage>((res, rej) => { + resolve = res; + reject = rej; + }); + this.#requestSubscribers.set(path, {resolve, reject, promise}); + return promise; + } + + reset(): void { + this.#routes.clear(); + this.#auths.clear(); + this.#csp.clear(); + this.#gzipRoutes.clear(); + const error = new Error('Static Server has been reset'); + for (const subscriber of this.#requestSubscribers.values()) { + subscriber.reject.call(undefined, error); + } + this.#requestSubscribers.clear(); + } + + #onRequest: RequestListener = ( + request: TestIncomingMessage, + response + ): void => { + request.on('error', (error: {code: string}) => { + if (error.code === 'ECONNRESET') { + response.end(); + } else { + throw error; + } + }); + request.postBody = new Promise(resolve => { + let body = ''; + request.on('data', (chunk: string) => { + return (body += chunk); + }); + request.on('end', () => { + return resolve(body); + }); + }); + assert(request.url); + const url = new URL(request.url, `https://${request.headers.host}`); + const path = url.pathname + url.search; + const auth = this.#auths.get(path); + if (auth) { + const credentials = Buffer.from( + (request.headers.authorization || '').split(' ')[1] || '', + 'base64' + ).toString(); + if (credentials !== `${auth.username}:${auth.password}`) { + response.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Secure Area"', + }); + response.end('HTTP Error 401 Unauthorized: Access is denied'); + return; + } + } + const subscriber = this.#requestSubscribers.get(path); + if (subscriber) { + subscriber.resolve.call(undefined, request); + this.#requestSubscribers.delete(path); + } + const handler = this.#routes.get(path); + if (handler) { + handler.call(undefined, request, response); + } else { + this.serveFile(request, response, path); + } + }; + + serveFile( + request: IncomingMessage, + response: ServerResponse, + pathName: string + ): void { + if (pathName === '/') { + pathName = '/index.html'; + } + const filePath = join(this.#dirPath, pathName.substring(1)); + + if (this.#cachedPathPrefix && filePath.startsWith(this.#cachedPathPrefix)) { + if (request.headers['if-modified-since']) { + response.statusCode = 304; // not modified + response.end(); + return; + } + response.setHeader('Cache-Control', 'public, max-age=31536000'); + response.setHeader('Last-Modified', this.#startTime.toISOString()); + } else { + response.setHeader('Cache-Control', 'no-cache, no-store'); + } + const csp = this.#csp.get(pathName); + if (csp) { + response.setHeader('Content-Security-Policy', csp); + } + + readFile(filePath, (err, data) => { + if (err) { + response.statusCode = 404; + response.end(`File not found: ${filePath}`); + return; + } + const mimeType = getMimeType(filePath); + if (mimeType) { + const isTextEncoding = /^text\/|^application\/(javascript|json)/.test( + mimeType + ); + const contentType = isTextEncoding + ? `${mimeType}; charset=utf-8` + : mimeType; + response.setHeader('Content-Type', contentType); + } + if (this.#gzipRoutes.has(pathName)) { + response.setHeader('Content-Encoding', 'gzip'); + gzip(data, (_, result) => { + response.end(result); + }); + } else { + response.end(data); + } + }); + } + + #onWebSocketConnection = (socket: WebSocket): void => { + socket.send('opened'); + }; +} diff --git a/remote/test/puppeteer/packages/testserver/tsconfig.json b/remote/test/puppeteer/packages/testserver/tsconfig.json new file mode 100644 index 0000000000..c2576c2564 --- /dev/null +++ b/remote/test/puppeteer/packages/testserver/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "composite": true, + "module": "CommonJS", + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/release-please-config.json b/remote/test/puppeteer/release-please-config.json new file mode 100644 index 0000000000..5ae14b8cc3 --- /dev/null +++ b/remote/test/puppeteer/release-please-config.json @@ -0,0 +1,29 @@ +{ + "last-release-sha": "30e5b1a58edb8b1d94acdff00d64c76e76cf02a3", + "prerelease": true, + "packages": { + "packages/puppeteer": { + "component": "puppeteer" + }, + "packages/puppeteer-core": { + "component": "puppeteer-core" + }, + "packages/testserver": {}, + "packages/ng-schematics": { + "release-as": "0.2.0" + }, + "packages/browsers": {} + }, + "plugins": [ + { + "type": "node-workspace", + "merge": false + }, + { + "type": "linked-versions", + "group-name": "puppeteer", + "groupName": "puppeteer", + "components": ["puppeteer", "puppeteer-core"] + } + ] +} diff --git a/remote/test/puppeteer/test-d/ElementHandle.test-d.ts b/remote/test/puppeteer/test-d/ElementHandle.test-d.ts new file mode 100644 index 0000000000..e23e66678c --- /dev/null +++ b/remote/test/puppeteer/test-d/ElementHandle.test-d.ts @@ -0,0 +1,1024 @@ +import {ElementHandle} from 'puppeteer'; +import {expectNotType, expectType} from 'tsd'; + +declare const handle: ElementHandle; + +{ + { + { + expectType<ElementHandle<HTMLAnchorElement> | null>(await handle.$('a')); + expectNotType<ElementHandle<Element> | null>(await handle.$('a')); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('a#id') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('a#id')); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('a.class') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('a.class')); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('a[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('a[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('a:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('a:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('a:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('a:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<HTMLDivElement> | null>(await handle.$('div')); + expectNotType<ElementHandle<Element> | null>(await handle.$('div')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div#id') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div#id')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div.class') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div.class')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>(await handle.$('some-custom')); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('some-custom#id') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('some-custom.class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('some-custom[attr=value]') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('some-custom:pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('some-custom:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>(await handle.$('')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('#id')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('.class')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('[attr=value]')); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$(':pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>(await handle.$(':func(arg)')); + } + } + { + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div > a')); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a#id') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a#id') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a.class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a.class') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div > div')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div#id') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div#id') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div.class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div.class') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom#id') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom.class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom[attr=value]') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom:pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>(await handle.$('div > #id')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('div > .class')); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > [attr=value]') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > :pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > :func(arg)') + ); + } + } + { + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div > a')); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a#id') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a#id') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a.class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a.class') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.$('div > a:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > a:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div') + ); + expectNotType<ElementHandle<Element> | null>(await handle.$('div > div')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div#id') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div#id') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div.class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div.class') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div[attr=value]') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div[attr=value]') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div:psuedo-class') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div:pseudo-class') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.$('div > div:func(arg)') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.$('div > div:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom#id') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom.class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom[attr=value]') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom:pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType<ElementHandle<Element> | null>(await handle.$('div > #id')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('div > .class')); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > [attr=value]') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > :pseudo-class') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.$('div > :func(arg)') + ); + } + } +} + +{ + { + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>(await handle.$$('a')); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a')); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('a#id') + ); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a#id')); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('a.class') + ); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a.class')); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('a[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('a[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('a:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('a:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('a:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('a:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<HTMLDivElement>>>(await handle.$$('div')); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div')); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div#id') + ); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div#id')); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div.class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div.class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('some-custom')); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('some-custom#id') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('some-custom.class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('some-custom[attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('some-custom:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('some-custom:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('')); + } + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('#id')); + } + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('.class')); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('[attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$(':pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>(await handle.$$(':func(arg)')); + } + } + { + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a') + ); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div > a')); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a#id') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a#id') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a.class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a.class') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div#id') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div#id') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div.class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div.class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom#id') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom.class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom[attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('div > #id')); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > .class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > [attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > :pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > :func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a') + ); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div > a')); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a#id') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a#id') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a.class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a.class') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>( + await handle.$$('div > a:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > a:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div#id') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div#id') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div.class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div.class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div[attr=value]') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div[attr=value]') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div:psuedo-class') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>( + await handle.$$('div > div:func(arg)') + ); + expectNotType<Array<ElementHandle<Element>>>( + await handle.$$('div > div:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom#id') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom.class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom[attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom:pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > some-custom:func(arg)') + ); + } + } + { + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('div > #id')); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > .class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > [attr=value]') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > :pseudo-class') + ); + } + { + expectType<Array<ElementHandle<Element>>>( + await handle.$$('div > :func(arg)') + ); + } + } +} + +{ + expectType<void>( + await handle.$eval( + 'a', + (element, int) => { + expectType<HTMLAnchorElement>(element); + expectType<number>(int); + }, + 1 + ) + ); + expectType<void>( + await handle.$eval( + 'div', + (element, int, str) => { + expectType<HTMLDivElement>(element); + expectType<number>(int); + expectType<string>(str); + }, + 1, + '' + ) + ); + expectType<number>( + await handle.$eval( + 'a', + (element, value) => { + expectType<HTMLAnchorElement>(element); + return value; + }, + 1 + ) + ); + expectType<number>( + await handle.$eval( + 'some-element', + (element, value) => { + expectType<Element>(element); + return value; + }, + 1 + ) + ); + expectType<HTMLAnchorElement>( + await handle.$eval('a', element => { + return element; + }) + ); + expectType<unknown>(await handle.$eval('a', 'document')); +} + +{ + expectType<void>( + await handle.$$eval( + 'a', + (elements, int) => { + expectType<HTMLAnchorElement[]>(elements); + expectType<number>(int); + }, + 1 + ) + ); + expectType<void>( + await handle.$$eval( + 'div', + (elements, int, str) => { + expectType<HTMLDivElement[]>(elements); + expectType<number>(int); + expectType<string>(str); + }, + 1, + '' + ) + ); + expectType<number>( + await handle.$$eval( + 'a', + (elements, value) => { + expectType<HTMLAnchorElement[]>(elements); + return value; + }, + 1 + ) + ); + expectType<number>( + await handle.$$eval( + 'some-element', + (elements, value) => { + expectType<Element[]>(elements); + return value; + }, + 1 + ) + ); + expectType<HTMLAnchorElement[]>( + await handle.$$eval('a', elements => { + return elements; + }) + ); + expectType<unknown>(await handle.$$eval('a', 'document')); +} + +{ + { + expectType<ElementHandle<HTMLAnchorElement> | null>( + await handle.waitForSelector('a') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.waitForSelector('a') + ); + } + { + expectType<ElementHandle<HTMLDivElement> | null>( + await handle.waitForSelector('div') + ); + expectNotType<ElementHandle<Element> | null>( + await handle.waitForSelector('div') + ); + } + { + expectType<ElementHandle<Element> | null>( + await handle.waitForSelector('some-custom') + ); + } +} diff --git a/remote/test/puppeteer/test-d/JSHandle.test-d.ts b/remote/test/puppeteer/test-d/JSHandle.test-d.ts new file mode 100644 index 0000000000..0e1b6385d8 --- /dev/null +++ b/remote/test/puppeteer/test-d/JSHandle.test-d.ts @@ -0,0 +1,83 @@ +import {ElementHandle, JSHandle} from 'puppeteer'; +import {expectNotAssignable, expectNotType, expectType} from 'tsd'; + +declare const handle: JSHandle; + +{ + expectType<unknown>(await handle.evaluate('document')); + expectType<number>( + await handle.evaluate(() => { + return 1; + }) + ); + expectType<HTMLElement>( + await handle.evaluate(() => { + return document.body; + }) + ); + expectType<string>( + await handle.evaluate(() => { + return ''; + }) + ); + expectType<string>( + await handle.evaluate((value, str) => { + expectNotAssignable<never>(value); + expectType<string>(str); + return ''; + }, '') + ); +} + +{ + expectType<JSHandle>(await handle.evaluateHandle('document')); + expectType<JSHandle<number>>( + await handle.evaluateHandle(() => { + return 1; + }) + ); + expectType<JSHandle<string>>( + await handle.evaluateHandle(() => { + return ''; + }) + ); + expectType<JSHandle<string>>( + await handle.evaluateHandle((value, str) => { + expectNotAssignable<never>(value); + expectType<string>(str); + return ''; + }, '') + ); + expectType<ElementHandle<HTMLElement>>( + await handle.evaluateHandle(() => { + return document.body; + }) + ); +} + +declare const handle2: JSHandle<{test: number}>; + +{ + { + expectType<JSHandle<number>>(await handle2.getProperty('test')); + expectNotType<JSHandle<unknown>>(await handle2.getProperty('test')); + } + { + expectType<JSHandle<unknown>>( + await handle2.getProperty('key-doesnt-exist') + ); + expectNotType<JSHandle<string>>( + await handle2.getProperty('key-doesnt-exist') + ); + expectNotType<JSHandle<number>>( + await handle2.getProperty('key-doesnt-exist') + ); + } +} + +{ + void handle.evaluate((value, other) => { + expectType<unknown>(value); + expectType<{test: number}>(other); + }, handle2); +} diff --git a/remote/test/puppeteer/test-d/NodeFor.test-d.ts b/remote/test/puppeteer/test-d/NodeFor.test-d.ts new file mode 100644 index 0000000000..a478b5952c --- /dev/null +++ b/remote/test/puppeteer/test-d/NodeFor.test-d.ts @@ -0,0 +1,156 @@ +import type {NodeFor} from 'puppeteer'; +import {expectType, expectNotType} from 'tsd'; + +declare const nodeFor: <Selector extends string>( + selector: Selector +) => NodeFor<Selector>; + +{ + { + expectType<HTMLTableRowElement>( + nodeFor( + '[data-testid="my-component"] div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div tbody tr' + ) + ); + expectNotType<Element>( + nodeFor( + '[data-testid="my-component"] div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div div tbody tr' + ) + ); + } + { + expectType<HTMLAnchorElement>(nodeFor('a')); + expectNotType<Element>(nodeFor('a')); + } + { + expectType<HTMLAnchorElement>(nodeFor('a#ignored')); + expectNotType<Element>(nodeFor('a#ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('a.ignored')); + expectNotType<Element>(nodeFor('a.ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('a[ignored')); + expectNotType<Element>(nodeFor('a[ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('a:ignored')); + expectNotType<Element>(nodeFor('a:ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored a')); + expectNotType<Element>(nodeFor('ignored a')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored a#ignored')); + expectNotType<Element>(nodeFor('ignored a#ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored a.ignored')); + expectNotType<Element>(nodeFor('ignored a.ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored a[ignored')); + expectNotType<Element>(nodeFor('ignored a[ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored a:ignored')); + expectNotType<Element>(nodeFor('ignored a:ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored > a')); + expectNotType<Element>(nodeFor('ignored > a')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored > a#ignored')); + expectNotType<Element>(nodeFor('ignored > a#ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored > a.ignored')); + expectNotType<Element>(nodeFor('ignored > a.ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored > a[ignored')); + expectNotType<Element>(nodeFor('ignored > a[ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored > a:ignored')); + expectNotType<Element>(nodeFor('ignored > a:ignored')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored + a')); + expectNotType<Element>(nodeFor('ignored + a')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored ~ a')); + expectNotType<Element>(nodeFor('ignored ~ a')); + } + { + expectType<HTMLAnchorElement>(nodeFor('ignored | a')); + expectNotType<Element>(nodeFor('ignored | a')); + } + { + expectType<HTMLAnchorElement>( + nodeFor('ignored ignored > ignored + ignored | a#ignore') + ); + expectNotType<Element>( + nodeFor('ignored ignored > ignored + ignored | a#ignore') + ); + } +} +{ + { + expectType<Element>(nodeFor('')); + } + { + expectType<Element>(nodeFor('#ignored')); + } + { + expectType<Element>(nodeFor('.ignored')); + } + { + expectType<Element>(nodeFor('[ignored')); + } + { + expectType<Element>(nodeFor(':ignored')); + } + { + expectType<Element>(nodeFor('ignored #ignored')); + } + { + expectType<Element>(nodeFor('ignored .ignored')); + } + { + expectType<Element>(nodeFor('ignored [ignored')); + } + { + expectType<Element>(nodeFor('ignored :ignored')); + } + { + expectType<Element>(nodeFor('ignored > #ignored')); + } + { + expectType<Element>(nodeFor('ignored > .ignored')); + } + { + expectType<Element>(nodeFor('ignored > [ignored')); + } + { + expectType<Element>(nodeFor('ignored > :ignored')); + } + { + expectType<Element>(nodeFor('ignored + #ignored')); + } + { + expectType<Element>(nodeFor('ignored ~ #ignored')); + } + { + expectType<Element>(nodeFor('ignored | #ignored')); + } + { + expectType<Element>( + nodeFor('ignored ignored > ignored ~ ignored + ignored | #ignored') + ); + } +} diff --git a/remote/test/puppeteer/test-d/puppeteer.test-d.ts b/remote/test/puppeteer/test-d/puppeteer.test-d.ts new file mode 100644 index 0000000000..11128f15f7 --- /dev/null +++ b/remote/test/puppeteer/test-d/puppeteer.test-d.ts @@ -0,0 +1,12 @@ +import puppeteer, { + connect, + defaultArgs, + executablePath, + launch, +} from 'puppeteer'; +import {expectType} from 'tsd'; + +expectType<typeof launch>(puppeteer.launch); +expectType<typeof connect>(puppeteer.connect); +expectType<typeof defaultArgs>(puppeteer.defaultArgs); +expectType<typeof executablePath>(puppeteer.executablePath); diff --git a/remote/test/puppeteer/test/.eslintrc.js b/remote/test/puppeteer/test/.eslintrc.js new file mode 100644 index 0000000000..9d86da20e1 --- /dev/null +++ b/remote/test/puppeteer/test/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + rules: { + 'no-restricted-imports': [ + 'error', + { + /** The mocha tests run on the compiled output in the /lib directory + * so we should avoid importing from src. + */ + patterns: ['*src*'], + }, + ], + }, +}; diff --git a/remote/test/puppeteer/test/README.md b/remote/test/puppeteer/test/README.md new file mode 100644 index 0000000000..cc4fe3b5ae --- /dev/null +++ b/remote/test/puppeteer/test/README.md @@ -0,0 +1,95 @@ +# Puppeteer tests + +Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library. + +## Test state + +We have some common setup that runs before each test and is defined in `mocha-utils.js`. + +You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you: + +- `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`. +- `puppeteerPath`: the path to the root source file for Puppeteer. +- `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required. +- `server`: a dummy test server instance (see `packages/testserver` for more). +- `httpsServer`: a dummy test server HTTPS instance (see `packages/testserver` for more). +- `isFirefox`: true if running in Firefox. +- `isChrome`: true if running Chromium. +- `isHeadless`: true if the test is in headless mode. + +If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`. + +If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this. + +The best place to look is an existing test to see how they use the helpers. + +## Skipping tests in specific conditions + +To skip tests edit the [TestExpectations](https://github.com/puppeteer/puppeteer/blob/main/test/TestExpectations.json) file. See [test runner documentation](https://github.com/puppeteer/puppeteer/tree/main/tools/mochaRunner) for more details. + +## Running tests + +- To run all tests applicable for your platform: + +```bash +npm test +``` + +- **Important**: don't forget to first build the code if you're testing local changes: + +```bash +npm run build --workspace=@puppeteer-test/test && npm test +``` + +### CLI options + +| Description | Option | Type | +| ----------------------------------------------------------------- | ---------------- | ------- | +| Do not generate coverage report | --no-coverage | boolean | +| Do not generate suggestion for updating TestExpectation.json file | --no-suggestions | boolean | +| Specify a file to which to save run data | --save-stats-to | string | +| Specify a file with a custom Mocha reporter | --reporter | string | +| Number of times to retry failed tests. | --retries | number | +| Timeout threshold value. | --timeout | number | +| Tell Mocha to not run test files in parallel | --no-parallel | boolean | +| Generate full stacktrace upon failure | --fullTrace | boolean | +| Name of the Test suit defined in TestSuites.json | --test-suite | string | + +### Helpful information + +- To run a specific test, substitute the `it` with `it.only`: + +```ts + ... + it.only('should work', async function() { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `it.skip`: + +```ts + ... + it.skip('should work', async function({server, page}) { + const {server, page} = getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run Chrome headful tests: + +```bash +npm run test:chrome:headful +``` + +- To run tests with custom browser executable: + +```bash +BINARY=<path-to-executable> npm run test:chrome:headless # Or npm run test:firefox +``` + +[mocha]: https://mochajs.org/ +[expect]: https://www.npmjs.com/package/expect diff --git a/remote/test/puppeteer/test/TestExpectations.json b/remote/test/puppeteer/test/TestExpectations.json new file mode 100644 index 0000000000..a96bbe9709 --- /dev/null +++ b/remote/test/puppeteer/test/TestExpectations.json @@ -0,0 +1,2234 @@ +[ + { + "testIdPattern": "*", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP", "TIMEOUT"] + }, + { + "testIdPattern": "[DeviceRequestPrompt.spec] DeviceRequestPrompt *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[EventEmitter.spec] EventEmitter *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[jshandle.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goBack *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[NetworkManager.spec] NetworkManager *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.DOMContentLoaded *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.PageError *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page Page.pdf *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setContent *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[accessibility.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP", "TIMEOUT"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[chromiumonly.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[Connection.spec] WebDriver BiDi Connection should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[coverage.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[drag-and-drop.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs Custom queries should throw with invalid query names", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isVisible and ElementHandle.isHidden should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should detect touch when applying viewport with touches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL", "PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should execute after cross-site navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have correct execution contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have different execution contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should accept element handle as an argument", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should be able to throw a tricky error", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should evaluate in the page context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should not throw an error when evaluation does a navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should not throw an error when evaluation does a navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw a nice error after a navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if underlying element was disposed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work right after framenavigated", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument should evaluate before anything else on the page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument should work with CSP", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[headful.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[idle_override.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[input.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with domcontentloaded", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle2", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not throw an error for a 404 response with an empty body", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not throw an error for a 500 response with an empty body", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should return last response in redirect chain", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should wait for network idle to succeed navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to 404", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to valid url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with self requesting page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.reload should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[oopif.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should *not* run beforeunload by default", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[proxy.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[requestinterception.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[TargetManager.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[tracing.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[worker.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should fire target events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should isolate localStorage and cookies", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should provide a context id", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should work across sessions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button if window.Node is removed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[click.spec] Page.click should click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should select the text by triple clicking", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.cookies should get cookies from multiple urls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should default to setting secure cookie for HTTPS websites", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie on a different domain", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie with a path", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookie with reasonable defaults", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookies from a frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set multiple cookies", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set secure same-site cookies from a frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.deleteCookie() should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.setCookie() should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should return null for invisible elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boxModel should return null for invisible elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulate should support clicking", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateCPUThrottling should change the CPU throttling rate successfully", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaType should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateNetworkConditions should change navigator.connection.effectiveType", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should throw for invalid timezone IDs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateVisionDeficiency should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should detect touch when applying viewport with touches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should get the proper viewport size", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should support landscape emulation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should support mobile emulation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs \"after each\" hook for \"should transfer 100Mb of data from page to node.js\"", + "platforms": ["darwin"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw a nice error after a navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument should evaluate before anything else on the page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument should work with CSP", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[fixtures.spec] Fixtures dumpio option should work with pipe option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame from-inside shadow DOM", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.name()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.parent()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame.evaluate should throw for detached frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame.executionContext should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[headful.spec] headful tests HEADFUL target.page() should return a background_page", + "platforms": ["darwin", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with request interception", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard ElementHandle.press should support |text| option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should press the meta key", + "platforms": ["linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should press the metaKey", + "platforms": ["linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should report shiftKey", + "platforms": ["darwin"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should specify location", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should specify repeat property", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should trigger commands of keyboard shortcuts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type all kinds of characters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type emoji", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type emoji into an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Browser target events should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Browser.Events.disconnected should be emitted when: browser gets closed, disconnected or underlying websocket gets closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to a browser with no page targets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to the same page simultaneously", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to reconnect to a disconnected browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support targetFilter option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath returns executablePath for channel", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Chrome", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default argument in Firefox", + "platforms": ["linux"], + "parameters": ["firefox", "headful"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default arguments in Chrome", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should have custom URL when launching browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should launch Chrome properly with --no-startup-window and waitForInitialPage=false", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir argument with non-existent dir", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should select the text with mouse", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should send mouse wheel events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should trigger hover state", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should trigger hover state with removed window.Node", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should tween mouse movement", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL after redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle2", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation of 11 pages", + "platforms": ["darwin"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should wait for network idle to succeed navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network \"after all\" hook in \"network\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network \"after each\" hook for \"should wait until response completes\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFailed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Response", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should fire events in proper order", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should fire events in proper order", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events should support redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should support redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events should support redirects", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should allow disable authentication", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should fail if wrong credentials", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should not disable caching", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Page.setExtraHTTPHeaders should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.headers should define Chrome as user agent header", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Request.headers should define Firefox as user agent header", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Request.initiator should return the initiator", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work with request interception", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.postData should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer should throw if the response does not have a body", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer should work with compression", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.fromCache should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.json should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should return uncompressed text", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should throw when requesting body of redirected response", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.text should wait until response completes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.timing returns timing information", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF-debug OOPIF should support wait for navigation for transitions from local to OOPIF", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page \"after all\" hook in \"Page\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[page.spec] Page \"before each\" hook for \"should include sourcemap when path is provided\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[page.spec] Page \"before each\" hook for \"should return the page title\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[page.spec] Page \"before each\" hook for \"should throw an error if loading from url fail\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should deny permission when not listed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should grant permission when listed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should grant persistent-storage", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should isolate permissions between browser contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should reset permissions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page BrowserContext.overridePermissions should trigger permission onchange", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page ExecutionContext.queryObjects should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page ExecutionContext.queryObjects should work for non-trivial page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.addStyleTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should terminate network waiters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location when fetch fails", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should not fail for window object", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should trigger correct Log", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with timing functions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.error should throw when page crashes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and rel=noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and with rel=opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and without rel=opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with fake-clicking target=_blank and rel=noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should await returned promise", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should be callable from-inside evaluateOnNewDocument", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should fallback to default export when passed a module object", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should not throw when frames detach", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should support throwing \"null\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should survive navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should throw exception in page context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should work on frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should work on frames before navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should work with complex objects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.metrics metrics event fired on console.timeStamp", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.metrics should get metrics from a page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.select should work when re-defining top-level Event class", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass after cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP header", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP in iframes as well", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP meta tag", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should enable or disable the cache based on the state passed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should stay disabled when toggling request interception on/off", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setContent should work with accents", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setContent should work with emojis", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setContent should work with newline", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setContent should work with tricky content", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setGeolocation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should emulate navigator.onLine", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setUserAgent should work with additional userAgentMetdata", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.url should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level", + "platforms": ["linux"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work with deep combinators", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should load fonts if cache enabled", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should fail to screenshot a detached element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should allow transparency", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip rect", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should render white background on jpeg file", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should use scale for clip", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work with webp", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should create a worker from a service worker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should create a worker from a shared worker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should have an opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a new page is created and closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a service worker is created and destroyed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a target url changes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[touchscreen.spec] Touchscreen should report touches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[touchscreen.spec] Touchscreen should report touchMove", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[touchscreen.spec] Touchscreen should tap the button", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work when resolved right before execution context disposal", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work with strict CSP policy", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector Page.waitForSelector is shortcut for main frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should work with removed MutationObserver", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[fixtures.spec] Fixtures dumpio option should work with pipe option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[fixtures.spec] Fixtures dumpio option should work with pipe option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[headful.spec] headful tests HEADFUL OOPIF: should expose events within OOPIFs", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation \"after all\" hook in \"navigation\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "headless", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network \"after each\" hook for \"Same-origin set-cookie subresource\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network \"after each\" hook for \"Same-origin set-cookie subresource\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should be abortable", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should return base64", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work in \"fromSurface: false\" mode", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should work in \"fromSurface: false\" mode", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[tracing.spec] Tracing \"after each\" hook for \"should output a trace\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[tracing.spec] Tracing \"after each\" hook for \"should output a trace\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[worker.spec] Workers Page.workers", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "headless", "webDriverBiDi"], + "expectations": ["PASS", "TIMEOUT"] + } +] diff --git a/remote/test/puppeteer/test/TestSuites.json b/remote/test/puppeteer/test/TestSuites.json new file mode 100644 index 0000000000..75fcc1dd0d --- /dev/null +++ b/remote/test/puppeteer/test/TestSuites.json @@ -0,0 +1,68 @@ +{ + "testSuites": [ + { + "id": "chrome-headless", + "platforms": ["linux", "win32", "darwin"], + "parameters": ["chrome", "headless", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "chrome-headful", + "platforms": ["linux"], + "parameters": ["chrome", "headful", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "chrome-new-headless", + "platforms": ["linux"], + "parameters": ["chrome", "new-headless", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "firefox-headless", + "platforms": ["linux", "darwin"], + "parameters": ["firefox", "headless", "cdp"], + "expectedLineCoverage": 80 + }, + { + "id": "firefox-headful", + "platforms": ["linux"], + "parameters": ["firefox", "headful", "cdp"], + "expectedLineCoverage": 80 + }, + { + "id": "firefox-bidi", + "platforms": ["linux"], + "parameters": ["firefox", "headless", "webDriverBiDi"], + "expectedLineCoverage": 56 + }, + { + "id": "chrome-bidi", + "platforms": ["linux"], + "parameters": ["chrome", "headless", "webDriverBiDi"], + "expectedLineCoverage": 56 + } + ], + "parameterDefinitions": { + "chrome": { + "PUPPETEER_PRODUCT": "chrome" + }, + "firefox": { + "PUPPETEER_PRODUCT": "firefox" + }, + "headless": { + "HEADLESS": "true", + "PUPPETEER_LOGLEVEL": "silent" + }, + "headful": { + "HEADLESS": "false" + }, + "new-headless": { + "HEADLESS": "new" + }, + "webDriverBiDi": { + "PUPPETEER_PROTOCOL": "webDriverBiDi" + }, + "cdp": {} + } +} diff --git a/remote/test/puppeteer/test/assets/abort-request.html b/remote/test/puppeteer/test/assets/abort-request.html new file mode 100644 index 0000000000..77c056a422 --- /dev/null +++ b/remote/test/puppeteer/test/assets/abort-request.html @@ -0,0 +1,13 @@ +<button id="abort"></button> + +<script> + const button = document.getElementById('abort'); + button.addEventListener('click', getJson) + async function getJson() { + const abort = new AbortController(); + const result = fetch("/simple.json", { + signal: abort.signal + }); + abort.abort(); + } +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/beforeunload.html b/remote/test/puppeteer/test/assets/beforeunload.html new file mode 100644 index 0000000000..3cef6763f3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/beforeunload.html @@ -0,0 +1,10 @@ +<div>beforeunload demo.</div> +<script> +window.addEventListener('beforeunload', event => { + // Chrome way. + event.returnValue = 'Leave?'; + // Firefox way. + event.preventDefault(); +}); +</script> + diff --git a/remote/test/puppeteer/test/assets/cached/one-style-font.css b/remote/test/puppeteer/test/assets/cached/one-style-font.css new file mode 100644 index 0000000000..6178de0350 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/cached/one-style-font.html b/remote/test/puppeteer/test/assets/cached/one-style-font.html new file mode 100644 index 0000000000..8e7236dfb3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style-font.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style-font.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/cached/one-style.css b/remote/test/puppeteer/test/assets/cached/one-style.css new file mode 100644 index 0000000000..04e7110b41 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/cached/one-style.html b/remote/test/puppeteer/test/assets/cached/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/chromium-linux.zip b/remote/test/puppeteer/test/assets/chromium-linux.zip Binary files differnew file mode 100644 index 0000000000..9c00ec080d --- /dev/null +++ b/remote/test/puppeteer/test/assets/chromium-linux.zip diff --git a/remote/test/puppeteer/test/assets/consolelog.html b/remote/test/puppeteer/test/assets/consolelog.html new file mode 100644 index 0000000000..4a27803aa9 --- /dev/null +++ b/remote/test/puppeteer/test/assets/consolelog.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <title>console.log test</title> + </head> + <body> + <script> + function foo() { + console.log('yellow') + } + function bar() { + foo(); + } + bar(); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/csp.html b/remote/test/puppeteer/test/assets/csp.html new file mode 100644 index 0000000000..34fc1fc1a5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csp.html @@ -0,0 +1 @@ +<meta http-equiv="Content-Security-Policy" content="default-src 'self'"> diff --git a/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf Binary files differnew file mode 100644 index 0000000000..4b208624e8 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf diff --git a/remote/test/puppeteer/test/assets/csscoverage/OFL.txt b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000000..a9b3c8b34e --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/csscoverage/empty.html b/remote/test/puppeteer/test/assets/csscoverage/empty.html new file mode 100644 index 0000000000..b3845c366d --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/empty.html @@ -0,0 +1,3 @@ +<style></style> +<div>empty style tag</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/involved.html b/remote/test/puppeteer/test/assets/csscoverage/involved.html new file mode 100644 index 0000000000..bcd9845b93 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ +<style> +@charset "utf-8"; +@namespace svg url(http://www.w3.org/2000/svg); +@font-face { + font-family: "Example Font"; + src: url("./Dosis-Regular.ttf"); +} + +#fluffy { + border: 1px solid black; + z-index: 1; + /* -webkit-disabled-property: rgb(1, 2, 3) */ + -lol-cats: "dogs" /* non-existing property */ +} + +@media (min-width: 1px) { + span { + -webkit-border-radius: 10px; + font-family: "Example Font"; + animation: 1s identifier; + } +} +</style> +<div id="fluffy">woof!</div> +<span>fancy text</span> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/media.html b/remote/test/puppeteer/test/assets/csscoverage/media.html new file mode 100644 index 0000000000..bfb89f8f75 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ +<style> +@media screen { div { color: green; } } </style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/multiple.html b/remote/test/puppeteer/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000000..0fd97e962a --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ +<link rel="stylesheet" href="stylesheet1.css"> +<link rel="stylesheet" href="stylesheet2.css"> +<script> +window.addEventListener('DOMContentLoaded', () => { + // Force stylesheets to load. + console.log(window.getComputedStyle(document.body).color); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/csscoverage/simple.html b/remote/test/puppeteer/test/assets/csscoverage/simple.html new file mode 100644 index 0000000000..3beae21829 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ +<style> +div { color: green; } +a { color: blue; } +</style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000000..df4e9c276c --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ +<style> +body { + padding: 10px; +} +/*# sourceURL=nicename.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000000..60f1eab971 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000000..a87defb098 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/unused.html b/remote/test/puppeteer/test/assets/csscoverage/unused.html new file mode 100644 index 0000000000..5b8186a3bf --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ +<style> +@media screen { + a { color: green; } +} +/*# sourceURL=unused.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/detect-touch.html b/remote/test/puppeteer/test/assets/detect-touch.html new file mode 100644 index 0000000000..80a4123fbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/detect-touch.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <title>Detect Touch Test</title> + <script src='modernizr.js'></script> + </head> + <body style="font-size:30vmin"> + <script> + document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO'; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/digits/0.png b/remote/test/puppeteer/test/assets/digits/0.png Binary files differnew file mode 100644 index 0000000000..ac3c4768ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/0.png diff --git a/remote/test/puppeteer/test/assets/digits/1.png b/remote/test/puppeteer/test/assets/digits/1.png Binary files differnew file mode 100644 index 0000000000..6768222729 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/1.png diff --git a/remote/test/puppeteer/test/assets/digits/2.png b/remote/test/puppeteer/test/assets/digits/2.png Binary files differnew file mode 100644 index 0000000000..b1daa4735d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/2.png diff --git a/remote/test/puppeteer/test/assets/digits/3.png b/remote/test/puppeteer/test/assets/digits/3.png Binary files differnew file mode 100644 index 0000000000..6eca99b21b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/3.png diff --git a/remote/test/puppeteer/test/assets/digits/4.png b/remote/test/puppeteer/test/assets/digits/4.png Binary files differnew file mode 100644 index 0000000000..a721071e2c --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/4.png diff --git a/remote/test/puppeteer/test/assets/digits/5.png b/remote/test/puppeteer/test/assets/digits/5.png Binary files differnew file mode 100644 index 0000000000..15cb19932a --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/5.png diff --git a/remote/test/puppeteer/test/assets/digits/6.png b/remote/test/puppeteer/test/assets/digits/6.png Binary files differnew file mode 100644 index 0000000000..639f38439d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/6.png diff --git a/remote/test/puppeteer/test/assets/digits/7.png b/remote/test/puppeteer/test/assets/digits/7.png Binary files differnew file mode 100644 index 0000000000..5c1150b005 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/7.png diff --git a/remote/test/puppeteer/test/assets/digits/8.png b/remote/test/puppeteer/test/assets/digits/8.png Binary files differnew file mode 100644 index 0000000000..abb8b48b0b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/8.png diff --git a/remote/test/puppeteer/test/assets/digits/9.png b/remote/test/puppeteer/test/assets/digits/9.png Binary files differnew file mode 100644 index 0000000000..6a40a21c6f --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/9.png diff --git a/remote/test/puppeteer/test/assets/dynamic-oopif.html b/remote/test/puppeteer/test/assets/dynamic-oopif.html new file mode 100644 index 0000000000..38614d0289 --- /dev/null +++ b/remote/test/puppeteer/test/assets/dynamic-oopif.html @@ -0,0 +1,10 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; + url.pathname = '/oopif.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/empty.html b/remote/test/puppeteer/test/assets/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/empty.html diff --git a/remote/test/puppeteer/test/assets/error.html b/remote/test/puppeteer/test/assets/error.html new file mode 100644 index 0000000000..130400c006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/error.html @@ -0,0 +1,15 @@ +<script> +a(); + +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + throw new Error('Fancy error!'); +} +</script> diff --git a/remote/test/puppeteer/test/assets/es6/.eslintrc b/remote/test/puppeteer/test/assets/es6/.eslintrc new file mode 100644 index 0000000000..1903e176f5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +}
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/es6/es6import.js b/remote/test/puppeteer/test/assets/es6/es6import.js new file mode 100644 index 0000000000..9aac2d4d64 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/es6/es6module.js b/remote/test/puppeteer/test/assets/es6/es6module.js new file mode 100644 index 0000000000..7a4e8a723a --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; diff --git a/remote/test/puppeteer/test/assets/es6/es6pathimport.js b/remote/test/puppeteer/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000..eb17a9a3d1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/favicon.ico b/remote/test/puppeteer/test/assets/favicon.ico Binary files differnew file mode 100644 index 0000000000..d4edd50799 --- /dev/null +++ b/remote/test/puppeteer/test/assets/favicon.ico diff --git a/remote/test/puppeteer/test/assets/file-to-upload.txt b/remote/test/puppeteer/test/assets/file-to-upload.txt new file mode 100644 index 0000000000..b4ad118489 --- /dev/null +++ b/remote/test/puppeteer/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 Binary files differnew file mode 100644 index 0000000000..be6d188027 --- /dev/null +++ b/remote/test/puppeteer/test/assets/firefox-75.0a1.en-US.linux-x86_64.tar.bz2 diff --git a/remote/test/puppeteer/test/assets/frames/frame.html b/remote/test/puppeteer/test/assets/frames/frame.html new file mode 100644 index 0000000000..8f20d2da9f --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frame.html @@ -0,0 +1,8 @@ +<link rel='stylesheet' href='./style.css'> +<script src='./script.js' type='text/javascript'></script> +<style> +div { + line-height: 18px; +} +</style> +<div>Hi, I'm frame</div> diff --git a/remote/test/puppeteer/test/assets/frames/frameset.html b/remote/test/puppeteer/test/assets/frames/frameset.html new file mode 100644 index 0000000000..4d56f88839 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ +<frameset> + <frameset> + <frame src='./frame.html'></frame> + <frame src='about:blank'></frame> + </frameset> + <frame src='/empty.html'></frame> + <frame></frame> +</frameset> diff --git a/remote/test/puppeteer/test/assets/frames/lazy-frame.html b/remote/test/puppeteer/test/assets/frames/lazy-frame.html new file mode 100644 index 0000000000..4821cd76cd --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/lazy-frame.html @@ -0,0 +1,3 @@ +<iframe width="100%" height="300" src="about:blank"></iframe> +<div style="height: 800vh"></div> +<iframe width="100%" height="300" src='./frame.html' loading="lazy"></iframe>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/frames/nested-frames.html b/remote/test/puppeteer/test/assets/frames/nested-frames.html new file mode 100644 index 0000000000..de1987586f --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/nested-frames.html @@ -0,0 +1,25 @@ +<style> +body { + display: flex; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +::-webkit-scrollbar{ + display: none; +} +</style> +<script> +async function attachFrame(frameId, url) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => frame.onload = x); + return 'kazakh'; +} +</script> +<iframe src='./two-frames.html' name='2frames'></iframe> +<iframe src='./frame.html' name='aframe'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html new file mode 100644 index 0000000000..d1462641ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html @@ -0,0 +1 @@ +<iframe src='./frame.html?param=value#fragment'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame.html b/remote/test/puppeteer/test/assets/frames/one-frame.html new file mode 100644 index 0000000000..e941d795a2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame.html @@ -0,0 +1 @@ +<iframe src='./frame.html'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/script.js b/remote/test/puppeteer/test/assets/frames/script.js new file mode 100644 index 0000000000..be22256d16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/remote/test/puppeteer/test/assets/frames/style.css b/remote/test/puppeteer/test/assets/frames/style.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/remote/test/puppeteer/test/assets/frames/two-frames.html b/remote/test/puppeteer/test/assets/frames/two-frames.html new file mode 100644 index 0000000000..b2ee853eda --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ +<style> +body { + display: flex; + flex-direction: column; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +</style> +<iframe src='./frame.html' name='uno'></iframe> +<iframe src='./frame.html' name='dos'></iframe> diff --git a/remote/test/puppeteer/test/assets/global-var.html b/remote/test/puppeteer/test/assets/global-var.html new file mode 100644 index 0000000000..b6be975038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/global-var.html @@ -0,0 +1,3 @@ +<script> +var globalVar = 123; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/grid.html b/remote/test/puppeteer/test/assets/grid.html new file mode 100644 index 0000000000..0bdbb1220e --- /dev/null +++ b/remote/test/puppeteer/test/assets/grid.html @@ -0,0 +1,52 @@ +<script> +document.addEventListener('DOMContentLoaded', function() { + function generatePalette(amount) { + var result = []; + var hueStep = 360 / amount; + for (var i = 0; i < amount; ++i) + result.push('hsl(' + (hueStep * i) + ', 100%, 90%)'); + return result; + } + + var palette = generatePalette(100); + for (var i = 0; i < 200; ++i) { + var box = document.createElement('div'); + box.classList.add('box'); + box.style.setProperty('background-color', palette[i % palette.length]); + var x = i; + do { + var digit = x % 10; + x = (x / 10)|0; + var img = document.createElement('img'); + img.src = `./digits/${digit}.png`; + box.insertBefore(img, box.firstChild); + } while (x); + document.body.appendChild(box); + } +}); +</script> + +<style> + +body { + margin: 0; + padding: 0; +} + +.box { + font-family: arial; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + width: 50px; + height: 50px; + box-sizing: border-box; + border: 1px solid darkgray; +} + +::-webkit-scrollbar { + display: none; +} +</style> diff --git a/remote/test/puppeteer/test/assets/historyapi.html b/remote/test/puppeteer/test/assets/historyapi.html new file mode 100644 index 0000000000..bacaf9e9a0 --- /dev/null +++ b/remote/test/puppeteer/test/assets/historyapi.html @@ -0,0 +1,5 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + history.pushState({}, '', '#1'); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/idle-detector.html b/remote/test/puppeteer/test/assets/idle-detector.html new file mode 100644 index 0000000000..83b496c03d --- /dev/null +++ b/remote/test/puppeteer/test/assets/idle-detector.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<div id="state"></div> +<script> + const elState = document.querySelector('#state'); + function setState(msg) { + elState.textContent = msg; + } + async function main() { + const controller = new AbortController(); + const signal = controller.signal; + const idleDetector = new IdleDetector({ + threshold: 60000, + signal, + }); + idleDetector.addEventListener('change', () => { + const userState = idleDetector.userState; + const screenState = idleDetector.screenState; + setState(`Idle state: ${userState}, ${screenState}.`); + }); + idleDetector.start(); + } + main(); +</script> diff --git a/remote/test/puppeteer/test/assets/initiator.html b/remote/test/puppeteer/test/assets/initiator.html new file mode 100644 index 0000000000..12889d3242 --- /dev/null +++ b/remote/test/puppeteer/test/assets/initiator.html @@ -0,0 +1,2 @@ +<iframe src="./frames/frame.html"></iframe> +<script src="./initiator.js"></script> diff --git a/remote/test/puppeteer/test/assets/initiator.js b/remote/test/puppeteer/test/assets/initiator.js new file mode 100644 index 0000000000..642e775f31 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/injectedfile.js b/remote/test/puppeteer/test/assets/injectedfile.js new file mode 100644 index 0000000000..c211b62c16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); diff --git a/remote/test/puppeteer/test/assets/injectedstyle.css b/remote/test/puppeteer/test/assets/injectedstyle.css new file mode 100644 index 0000000000..aa1634c255 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/remote/test/puppeteer/test/assets/inline-svg.html b/remote/test/puppeteer/test/assets/inline-svg.html new file mode 100644 index 0000000000..20023ecc79 --- /dev/null +++ b/remote/test/puppeteer/test/assets/inline-svg.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <body> + <svg> + <circle cx="10" cy="10" r="10" /> + </svg> + + <div style="margin-top: 5000px;"> + <svg> + <circle cx="10" cy="10" r="10" /> + </svg> + </div> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/inner-frame1.html b/remote/test/puppeteer/test/assets/inner-frame1.html new file mode 100644 index 0000000000..00f19ec166 --- /dev/null +++ b/remote/test/puppeteer/test/assets/inner-frame1.html @@ -0,0 +1,10 @@ +<script> + window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = 'inner-frame2.test'; + url.pathname = '/inner-frame2.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); + }, false); +</script> diff --git a/remote/test/puppeteer/test/assets/inner-frame2.html b/remote/test/puppeteer/test/assets/inner-frame2.html new file mode 100644 index 0000000000..9a236cc48f --- /dev/null +++ b/remote/test/puppeteer/test/assets/inner-frame2.html @@ -0,0 +1 @@ +<button>click</button> diff --git a/remote/test/puppeteer/test/assets/input/button.html b/remote/test/puppeteer/test/assets/input/button.html new file mode 100644 index 0000000000..d4c6e13fd2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/button.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/checkbox.html b/remote/test/puppeteer/test/assets/input/checkbox.html new file mode 100644 index 0000000000..ca56762e2b --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <label for="agree">Remember Me</label> + <input id="agree" type="checkbox"> + <script> + window.result = { + check: null, + events: [], + }; + + let checkbox = document.querySelector('input'); + + const events = [ + 'change', + 'click', + 'dblclick', + 'input', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + ]; + + for (let event of events) { + checkbox.addEventListener(event, () => { + if (['change', 'click', 'dblclick', 'input'].includes(event) === true) { + result.check = checkbox.checked; + } + + result.events.push(event); + }, false); + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/drag-and-drop.html b/remote/test/puppeteer/test/assets/input/drag-and-drop.html new file mode 100644 index 0000000000..bc376a5045 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/drag-and-drop.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> + <head> + <title>Drag-and-drop test</title> + <style> + #drop { + width: 5em; + height: 5em; + border: 1px solid black; + } + </style> + </head> + <body> + <div id="drag" draggable="true">drag me</div> + <div id="drop"></div> + <script> + window.didDragStart = false; + window.didDragEnter = false; + window.didDragOver = false; + window.didDrop = false; + const drag = document.getElementById('drag'); + const drop = document.getElementById('drop'); + drag.addEventListener('dragstart', function(event) { + event.dataTransfer.setData('id', event.target.id); + window.didDragStart = true; + }); + drop.addEventListener('dragenter', function(event) { + event.preventDefault(); + window.didDragEnter = true; + }); + drop.addEventListener('dragover', function(event) { + event.preventDefault(); + window.didDragOver = true; + }); + drop.addEventListener('drop', function(event) { + event.preventDefault(); + const id = event.dataTransfer.getData('id'); + const el = document.getElementById(id); + if (el) { + event.target.appendChild(el); + window.didDrop = true; + } + }); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/fileupload.html b/remote/test/puppeteer/test/assets/input/fileupload.html new file mode 100644 index 0000000000..55fd7c5006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>File upload test</title> + </head> + <body> + <input type="file"> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/keyboard.html b/remote/test/puppeteer/test/assets/input/keyboard.html new file mode 100644 index 0000000000..fd962c7518 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Keyboard test</title> + </head> + <body> + <textarea></textarea> + <script> + window.result = ""; + let textarea = document.querySelector('textarea'); + textarea.focus(); + textarea.addEventListener('keydown', event => { + log('Keydown:', event.key, event.code, event.which, modifiers(event)); + }); + textarea.addEventListener('keypress', event => { + log('Keypress:', event.key, event.code, event.which, event.charCode, modifiers(event)); + }); + textarea.addEventListener('keyup', event => { + log('Keyup:', event.key, event.code, event.which, modifiers(event)); + }); + function modifiers(event) { + let m = []; + if (event.altKey) + m.push('Alt') + if (event.ctrlKey) + m.push('Control'); + if (event.shiftKey) + m.push('Shift') + return '[' + m.join(' ') + ']'; + } + function log(...args) { + console.log.apply(console, args); + result += args.join(' ') + '\n'; + } + function getResult() { + let temp = result.trim(); + result = ""; + return temp; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/mouse-helper.js b/remote/test/puppeteer/test/assets/input/mouse-helper.js new file mode 100644 index 0000000000..97a764aa80 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/input/rotatedButton.html b/remote/test/puppeteer/test/assets/input/rotatedButton.html new file mode 100644 index 0000000000..1bce66cf5e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <title>Rotated button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <style> + button { + transform: rotateY(180deg); + } + </style> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/scrollable.html b/remote/test/puppeteer/test/assets/input/scrollable.html new file mode 100644 index 0000000000..75757824a4 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/scrollable.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + <head> + <title>Scrollable test</title> + </head> + <body> + <script src='mouse-helper.js'></script> + <script> + for (let i = 0; i < 100; i++) { + let button = document.createElement('button'); + button.textContent = i + ': not clicked'; + button.id = 'button-' + i; + button.onclick = () => button.textContent = 'clicked'; + button.oncontextmenu = event => { + if (![2].includes(event.button)) { + return; + } + event.preventDefault(); + button.textContent = 'context menu'; + } + button.onmouseup = event => { + if (![1,3,4].includes(event.button)) { + return; + } + event.preventDefault(); + button.textContent = { + 3: 'back click', + 4: 'forward click', + 1: 'aux click', + }[event.button]; + } + document.body.appendChild(button); + document.body.appendChild(document.createElement('br')); + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/select.html b/remote/test/puppeteer/test/assets/input/select.html new file mode 100644 index 0000000000..026d48e328 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/select.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <select> + <option value="">Empty</option> + <option value="black">Black</option> + <option value="blue">Blue</option> + <option value="brown">Brown</option> + <option value="cyan">Cyan</option> + <option value="gray">Gray</option> + <option value="green">Green</option> + <option value="indigo">Indigo</option> + <option value="magenta">Magenta</option> + <option value="orange">Orange</option> + <option value="pink">Pink</option> + <option value="purple">Purple</option> + <option value="red">Red</option> + <option value="violet">Violet</option> + <option value="white">White</option> + <option value="yellow">Yellow</option> + </select> + <script> + window.result = { + onInput: null, + onChange: null, + onBubblingChange: null, + onBubblingInput: null, + }; + + let select = document.querySelector('select'); + + function makeEmpty() { + for (let i = select.options.length - 1; i >= 0; --i) { + select.remove(i); + } + } + + function makeMultiple() { + select.setAttribute('multiple', true); + } + + select.addEventListener('input', () => { + result.onInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + select.addEventListener('change', () => { + result.onChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('input', () => { + result.onBubblingInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('change', () => { + result.onBubblingChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/textarea.html b/remote/test/puppeteer/test/assets/input/textarea.html new file mode 100644 index 0000000000..6d77f3106d --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/textarea.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Textarea test</title> + </head> + <body> + <textarea></textarea> + <script src='mouse-helper.js'></script> + <script> + globalThis.result = ''; + globalThis.textarea = document.querySelector('textarea'); + textarea.addEventListener('input', () => result = textarea.value, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/touches-move.html b/remote/test/puppeteer/test/assets/input/touches-move.html new file mode 100644 index 0000000000..212330d46c --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touches-move.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Drag-and-drop test</title> + <style> + #drop { + width: 5em; + height: 5em; + border: 1px solid black; + position: absolute; + top: 30px; + left: 0px; + } + + #drag { + position: absolute; + left: 0; + top: 0; + } + </style> +</head> + +<body> + <div id="touch" draggable="true">touch me</div> + <script> + window.result = []; + function log(...args) { + console.log.apply(console, args); + result.push(args.join(' ')); + } + window.didTouchStart = false; + window.didDragEnter = false; + window.didTouchMove = false; + window.didTouchEnd = false; + const drag = document.getElementById('touch'); + drag.addEventListener('touchstart', function (event) { + console.log("touchstart") + window.didTouchStart = true; + }); + drag.addEventListener('touchmove', function (event) { + event.preventDefault(); + log('Touchmove:', ...Array.from(event.changedTouches).map(touch => "x: "+ Math.round(touch.clientX) +" y: "+ Math.round(touch.clientY))); + window.didTouchMove = true; + var touchLocation = event.targetTouches[0]; + var moveLoction = event.changedTouches[0]; + drag.style.left = touchLocation.pageX + 'px'; + drag.style.top = touchLocation.pageY + 'px'; + }); + drag.addEventListener('touchend', function (event) { + console.log("touch end") + log('Touchend:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + if(Array.from(event.changedTouches).map((touch) => { + console.log("x: "+ Math.round(touch.clientX) +" y: "+ Math.round(touch.clientY)) + window.touchX = touch.clientX; + window.touchY = touch.clientY; + })) + event.preventDefault(); + window.didTouchEnd = true; + + }); + </script> +</body> + +</html> diff --git a/remote/test/puppeteer/test/assets/input/touches.html b/remote/test/puppeteer/test/assets/input/touches.html new file mode 100644 index 0000000000..4392cfacbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touches.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> + <head> + <title>Touch test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <script> + window.result = []; + const button = document.querySelector('button'); + button.style.height = '200px'; + button.style.width = '200px'; + button.focus(); + button.addEventListener('touchstart', event => { + log('Touchstart:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + button.addEventListener('touchend', event => { + log('Touchend:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + button.addEventListener('touchmove', event => { + log('Touchmove:', ...Array.from(event.changedTouches).map(touch => touch.identifier)); + }); + function log(...args) { + console.log.apply(console, args); + result.push(args.join(' ')); + } + function getResult() { + let temp = result; + result = []; + return temp; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/wheel.html b/remote/test/puppeteer/test/assets/input/wheel.html new file mode 100644 index 0000000000..3d093a993e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/wheel.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + body { + min-height: 100vh; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + } + + div { + width: 105px; + height: 105px; + background: #cdf; + padding: 5px; + } + </style> + <title>Element: wheel event - Scaling_an_element_via_the_wheel - code sample</title> + </head> + <body> + <div>Scale me with your mouse wheel.</div> + <script> + function zoom(event) { + event.preventDefault(); + + scale += event.deltaY * -0.01; + + // Restrict scale + scale = Math.min(Math.max(.125, scale), 4); + + // Apply scale transform + el.style.transform = `scale(${scale})`; + } + + let scale = 1; + const el = document.querySelector('div'); + el.onwheel = zoom; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/jscoverage/eval.html b/remote/test/puppeteer/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000..838ae28763 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ +<script>eval('console.log("foo")')</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/involved.html b/remote/test/puppeteer/test/assets/jscoverage/involved.html new file mode 100644 index 0000000000..fcc32ba2ca --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/involved.html @@ -0,0 +1,16 @@ +<script> +function foo() { + if (1 > 2) + console.log(1); + if (1 < 2) + console.log(2); + let x = 1 > 2 ? 'foo' : 'bar'; + let y = 1 < 2 ? 'foo' : 'bar'; + let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}}; + let z = () => {}; + let q = () => {}; + q(); +} + +foo(); +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/multiple.html b/remote/test/puppeteer/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000000..bdef59885b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ +<script src='script1.js'></script> +<script src='script2.js'></script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/ranges.html b/remote/test/puppeteer/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000000..3d02670aea --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ +<script> +function unused(){}console.log('used!');if(true===false)console.log('unused!');</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/script1.js b/remote/test/puppeteer/test/assets/jscoverage/script1.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/script2.js b/remote/test/puppeteer/test/assets/jscoverage/script2.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/simple.html b/remote/test/puppeteer/test/assets/jscoverage/simple.html new file mode 100644 index 0000000000..49eeeea6ae --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ +<script> +function foo() {function bar() { } console.log(1); } foo(); </script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000000..e477750320 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ +<script> +console.log(1); +//# sourceURL=nicename.js +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/unused.html b/remote/test/puppeteer/test/assets/jscoverage/unused.html new file mode 100644 index 0000000000..59c4a5a70b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ +<script>function foo() { }</script> diff --git a/remote/test/puppeteer/test/assets/lazy-oopif-frame.html b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html new file mode 100644 index 0000000000..7c259b6673 --- /dev/null +++ b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html @@ -0,0 +1,3 @@ +<iframe width="100%" height="300" src="about:blank"></iframe> +<div style="height: 800vh"></div> +<iframe width="100%" height="300" src='www.example.com' loading="lazy"></iframe>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/main-frame.html b/remote/test/puppeteer/test/assets/main-frame.html new file mode 100644 index 0000000000..0c50feff85 --- /dev/null +++ b/remote/test/puppeteer/test/assets/main-frame.html @@ -0,0 +1,10 @@ +<script> + window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = 'inner-frame1.test'; + url.pathname = '/inner-frame1.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); + }, false); +</script> diff --git a/remote/test/puppeteer/test/assets/mobile.html b/remote/test/puppeteer/test/assets/mobile.html new file mode 100644 index 0000000000..8e94b2fe29 --- /dev/null +++ b/remote/test/puppeteer/test/assets/mobile.html @@ -0,0 +1 @@ +<meta name = "viewport" content = "initial-scale = 1, user-scalable = no"> diff --git a/remote/test/puppeteer/test/assets/modernizr.js b/remote/test/puppeteer/test/assets/modernizr.js new file mode 100644 index 0000000000..7991a4ec40 --- /dev/null +++ b/remote/test/puppeteer/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<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document); diff --git a/remote/test/puppeteer/test/assets/networkidle.html b/remote/test/puppeteer/test/assets/networkidle.html new file mode 100644 index 0000000000..910ae1736d --- /dev/null +++ b/remote/test/puppeteer/test/assets/networkidle.html @@ -0,0 +1,19 @@ +<script> + 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(); +</script> diff --git a/remote/test/puppeteer/test/assets/offscreenbuttons.html b/remote/test/puppeteer/test/assets/offscreenbuttons.html new file mode 100644 index 0000000000..e487caf4d3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/offscreenbuttons.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<style> + button { + position: absolute; + width: 100px; + height: 20px; + } + + #btn0 { right: 0px; top: 0; } + #btn1 { right: -10px; top: 25px; } + #btn2 { right: -20px; top: 50px; } + #btn3 { right: -30px; top: 75px; } + #btn4 { right: -40px; top: 100px; } + #btn5 { right: -50px; top: 125px; } + #btn6 { right: -60px; top: 150px; } + #btn7 { right: -70px; top: 175px; } + #btn8 { right: -80px; top: 200px; } + #btn9 { right: -90px; top: 225px; } + #btn10 { right: -100px; top: 250px; } + #btn11 { right: -99.999px; top: 275px; } +</style> +<button id=btn0>0</button> +<button id=btn1>1</button> +<button id=btn2>2</button> +<button id=btn3>3</button> +<button id=btn4>4</button> +<button id=btn5>5</button> +<button id=btn6>6</button> +<button id=btn7>7</button> +<button id=btn8>8</button> +<button id=btn9>9</button> +<button id=btn10>10</button> +<button id=btn11>11</button> +<script> + for (const button of document.querySelectorAll('button')) { + button.addEventListener('click', () => { + console.log(`button #${button.textContent} clicked`); + }); + } +</script> diff --git a/remote/test/puppeteer/test/assets/one-style.css b/remote/test/puppeteer/test/assets/one-style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/one-style.html b/remote/test/puppeteer/test/assets/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/oopif.html b/remote/test/puppeteer/test/assets/oopif.html new file mode 100644 index 0000000000..0761e8ab11 --- /dev/null +++ b/remote/test/puppeteer/test/assets/oopif.html @@ -0,0 +1,2 @@ +<a id="navigate-within-document" href="#nav">Navigate within document</a> +<a name="nav"></a>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/p-selectors.html b/remote/test/puppeteer/test/assets/p-selectors.html new file mode 100644 index 0000000000..52b8ca9020 --- /dev/null +++ b/remote/test/puppeteer/test/assets/p-selectors.html @@ -0,0 +1,13 @@ +<div id="a">hello <button id="b">world</button> + <span id="f"></span> + <div id="c"> + <template shadowrootmode="open"> + shadow dom + <div id="d"> + <template shadowrootmode="open"> + <a id="e">deep text</a> + </template> + </div> + </template> + </div> +</div>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/pdf.html b/remote/test/puppeteer/test/assets/pdf.html new file mode 100644 index 0000000000..987df27ebe --- /dev/null +++ b/remote/test/puppeteer/test/assets/pdf.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>PDF</title> + </head> + <body> + <div>PDF Content</div> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/playground.html b/remote/test/puppeteer/test/assets/playground.html new file mode 100644 index 0000000000..828cfb1c70 --- /dev/null +++ b/remote/test/puppeteer/test/assets/playground.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Playground</title> + </head> + <body> + <button>A button</button> + <textarea>A text area</textarea> + <div id="first">First div</div> + <div id="second"> + Second div + <span class="inner">Inner span</span> + </div> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/popup/popup.html b/remote/test/puppeteer/test/assets/popup/popup.html new file mode 100644 index 0000000000..b855162c25 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/popup.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup</title> + </head> + <body> + I am a popup + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/popup/window-open.html b/remote/test/puppeteer/test/assets/popup/window-open.html new file mode 100644 index 0000000000..d138be1d22 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup test</title> + </head> + <body> + <script> + window.open('./popup.html'); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/pptr.png b/remote/test/puppeteer/test/assets/pptr.png Binary files differnew file mode 100644 index 0000000000..65d87c68e6 --- /dev/null +++ b/remote/test/puppeteer/test/assets/pptr.png diff --git a/remote/test/puppeteer/test/assets/resetcss.html b/remote/test/puppeteer/test/assets/resetcss.html new file mode 100644 index 0000000000..e4e04b1f8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/resetcss.html @@ -0,0 +1,50 @@ +<style> +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +</style> diff --git a/remote/test/puppeteer/test/assets/self-request.html b/remote/test/puppeteer/test/assets/self-request.html new file mode 100644 index 0000000000..88aff620ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/self-request.html @@ -0,0 +1,5 @@ +<script> +var req = new XMLHttpRequest(); +req.open('GET', '/self-request.html'); +req.send(null); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000000..bef85d985b --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js new file mode 100644 index 0000000000..8b1a393741 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js @@ -0,0 +1 @@ +// empty diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json new file mode 100644 index 0000000000..25828b6d2b --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "service_worker": "background.js" + }, + "permissions": ["background", "activeTab"], + "manifest_version": 3 +} diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000000..a9d28acb09 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ +<link rel="stylesheet" href="./style.css"> +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); + window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000000..21381484b6 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/shadow.html b/remote/test/puppeteer/test/assets/shadow.html new file mode 100644 index 0000000000..3796ca768c --- /dev/null +++ b/remote/test/puppeteer/test/assets/shadow.html @@ -0,0 +1,17 @@ +<script> + +let h1 = null; +window.button = null; +window.clicked = false; + +window.addEventListener('DOMContentLoaded', () => { + const shadowRoot = document.body.attachShadow({mode: 'open'}); + h1 = document.createElement('h1'); + h1.textContent = 'Hellow Shadow DOM v1'; + button = document.createElement('button'); + button.textContent = 'Click'; + button.addEventListener('click', () => clicked = true); + shadowRoot.appendChild(h1); + shadowRoot.appendChild(button); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/simple-extension/content-script.js b/remote/test/puppeteer/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000000..0fd83b90f1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/content-script.js @@ -0,0 +1,2 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; diff --git a/remote/test/puppeteer/test/assets/simple-extension/index.js b/remote/test/puppeteer/test/assets/simple-extension/index.js new file mode 100644 index 0000000000..a0bb3f4eae --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/remote/test/puppeteer/test/assets/simple-extension/manifest.json b/remote/test/puppeteer/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000000..da2cd082ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "scripts": ["index.js"] + }, + "content_scripts": [{ + "matches": ["<all_urls>"], + "css": [], + "js": ["content-script.js"] + }], + "permissions": ["background", "activeTab"], + "manifest_version": 2 +} diff --git a/remote/test/puppeteer/test/assets/simple.json b/remote/test/puppeteer/test/assets/simple.json new file mode 100644 index 0000000000..6d95903051 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/remote/test/puppeteer/test/assets/tamperable.html b/remote/test/puppeteer/test/assets/tamperable.html new file mode 100644 index 0000000000..d027e97038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/tamperable.html @@ -0,0 +1,3 @@ +<script> + window.result = window.injected; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/title.html b/remote/test/puppeteer/test/assets/title.html new file mode 100644 index 0000000000..88a86ce412 --- /dev/null +++ b/remote/test/puppeteer/test/assets/title.html @@ -0,0 +1 @@ +<title>Woof-Woof</title> diff --git a/remote/test/puppeteer/test/assets/worker/worker.html b/remote/test/puppeteer/test/assets/worker/worker.html new file mode 100644 index 0000000000..7de2d9fd9e --- /dev/null +++ b/remote/test/puppeteer/test/assets/worker/worker.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <title>Worker test</title> + </head> + <body> + <script> + var worker = new Worker('worker.js'); + worker.onmessage = function(message) { + console.log(message.data); + }; + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/worker/worker.js b/remote/test/puppeteer/test/assets/worker/worker.js new file mode 100644 index 0000000000..0626f13e58 --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/assets/wrappedlink.html b/remote/test/puppeteer/test/assets/wrappedlink.html new file mode 100644 index 0000000000..429b6e9156 --- /dev/null +++ b/remote/test/puppeteer/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ +<style> +:root { + font-family: monospace; +} + +body { + display: flex; + align-items: center; + justify-content: center; +} + +div { + width: 10ch; + word-wrap: break-word; + border: 1px solid blue; + transform: rotate(33deg); + line-height: 8ch; + padding: 2ch; +} + +a { + margin-left: 7ch; +} +</style> +<div> + <a href='#clicked'>123321</a> +</div> +<script> + document.querySelector('a').addEventListener('click', () => { + window.__clicked = true; + }); +</script> diff --git a/remote/test/puppeteer/test/fixtures/closeme.js b/remote/test/puppeteer/test/fixtures/closeme.js new file mode 100644 index 0000000000..dbe798f70d --- /dev/null +++ b/remote/test/puppeteer/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/remote/test/puppeteer/test/fixtures/dumpio.js b/remote/test/puppeteer/test/fixtures/dumpio.js new file mode 100644 index 0000000000..a16cf4d633 --- /dev/null +++ b/remote/test/puppeteer/test/fixtures/dumpio.js @@ -0,0 +1,10 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => { + return console.error('message from dumpio'); + }); + await page.close(); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt new file mode 100644 index 0000000000..189ae05f97 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:<PORT>/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 306, + "end": 435 + } + ], + "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/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..ff282e989b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..91a1cb8510 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png Binary files differnew file mode 100644 index 0000000000..7b01753b6a --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png Binary files differnew file mode 100644 index 0000000000..b9b8b2922b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png diff --git a/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt new file mode 100644 index 0000000000..016b30bde8 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt @@ -0,0 +1,36 @@ +[ + { + "url": "http://localhost:<PORT>/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 168 + }, + { + "start": 203, + "end": 204 + }, + { + "start": 238, + "end": 251 + }, + { + "start": 259, + "end": 298 + } + ], + "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png Binary files differnew file mode 100644 index 0000000000..8595e0598e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..b010d1f87f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png Binary files differnew file mode 100644 index 0000000000..d713d27943 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..ac23b7de50 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..32e05bf05b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..cc8669d598 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..35c53377f9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..5fcdb92355 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..52e2a0f6d3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..d6d38217f7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..e503f801ec --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..ecab61fe17 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-chrome/transparent.png b/remote/test/puppeteer/test/golden-chrome/transparent.png Binary files differnew file mode 100644 index 0000000000..1cf45d8688 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/transparent.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png Binary files differnew file mode 100644 index 0000000000..4d74aac44c --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png Binary files differnew file mode 100644 index 0000000000..78979425a9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png Binary files differnew file mode 100644 index 0000000000..79b4b0fa1b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png Binary files differnew file mode 100644 index 0000000000..bede7c1ed0 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png Binary files differnew file mode 100644 index 0000000000..d5f6bbec2e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/white.jpg b/remote/test/puppeteer/test/golden-chrome/white.jpg Binary files differnew file mode 100644 index 0000000000..fb9070def3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/white.jpg diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..4677bdbc4f --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..532dc8db65 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..8e86dc9017 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png Binary files differnew file mode 100644 index 0000000000..d713d27943 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..7a74457869 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..f4e059c300 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..f554b1d62c --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..d1431bd91d --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..6d28cddcea --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..2b72c7528b --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..0a78fb1ae7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..2b72c7528b --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..ac47ec83b1 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..846b810386 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..07890a04b3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/installation/.mocharc.cjs b/remote/test/puppeteer/test/installation/.mocharc.cjs new file mode 100644 index 0000000000..f84e852fd8 --- /dev/null +++ b/remote/test/puppeteer/test/installation/.mocharc.cjs @@ -0,0 +1,21 @@ +// Copyright 2022 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @type {import('mocha').MochaOptions} + */ +module.exports = { + spec: ['build/**/*.spec.js'], + timeout: '240000ms', +}; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js new file mode 100644 index 0000000000..0446da820a --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js @@ -0,0 +1,19 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'puppeteer-core'; +import 'puppeteer-core/internal/revisions.js'; +import 'puppeteer-core/lib/esm/puppeteer/revisions.js'; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js new file mode 100644 index 0000000000..c635de59c4 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js @@ -0,0 +1,32 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import puppeteer from 'puppeteer-core'; + +(async () => { + try { + await puppeteer.launch({ + product: '${product}', + executablePath: 'node', + }); + } catch (error) { + if (error.message.includes('Failed to launch the browser process')) { + process.exit(0); + } + console.error(error); + process.exit(1); + } +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs new file mode 100644 index 0000000000..a82551b8d1 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs @@ -0,0 +1,19 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +require('puppeteer-core'); +require('puppeteer-core/internal/revisions.js'); +require('puppeteer-core/lib/cjs/puppeteer/revisions.js'); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js new file mode 100644 index 0000000000..475adc5248 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js @@ -0,0 +1,25 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import puppeteer from 'puppeteer'; +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.$('aria/example'); + await page.screenshot({path: 'example.png'}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs new file mode 100644 index 0000000000..8be5af0037 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs @@ -0,0 +1,8 @@ +const {join} = require('path'); + +/** + * @type {import("puppeteer").PuppeteerConfiguration} + */ +module.exports = { + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js new file mode 100644 index 0000000000..7416522ba3 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'puppeteer'; + +// Should still be reachable. +import 'puppeteer-core/internal/revisions.js'; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs new file mode 100644 index 0000000000..e44f5aeabd --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs @@ -0,0 +1,20 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +require('puppeteer'); + +// Should still be reachable. +require('puppeteer-core/internal/revisions.js'); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js new file mode 100644 index 0000000000..63ab015ef8 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js @@ -0,0 +1,25 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default { + mode: 'production', + entry: './index.js', + target: 'node', + output: { + path: process.cwd(), + filename: 'bundle.js', + }, +}; diff --git a/remote/test/puppeteer/test/installation/package.json b/remote/test/puppeteer/test/installation/package.json new file mode 100644 index 0000000000..3176dd7053 --- /dev/null +++ b/remote/test/puppeteer/test/installation/package.json @@ -0,0 +1,54 @@ +{ + "name": "@puppeteer-test/installation", + "version": "latest", + "type": "module", + "private": true, + "scripts": { + "build": "wireit", + "build:packages": "wireit", + "clean": "tsc -b --clean && rm -rf build", + "test": "mocha" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "build:packages" + ], + "files": [ + "tsconfig.json", + "src/**" + ], + "output": [ + "build/**", + "tsconfig.tsbuildinfo" + ] + }, + "build:packages": { + "command": "npm pack --quiet --workspace puppeteer --workspace puppeteer-core --workspace @puppeteer/browsers", + "dependencies": [ + "../../packages/puppeteer:build", + "../../packages/puppeteer-core:build", + "../../packages/browsers:build" + ], + "files": [], + "output": [ + "puppeteer-*.tgz" + ] + } + }, + "files": [ + ".mocharc.cjs", + "puppeteer-*.tgz", + "build", + "assets" + ], + "dependencies": { + "glob": "8.1.0", + "mocha": "10.2.0" + }, + "devDependencies": { + "@types/glob": "8.1.0" + } +} diff --git a/remote/test/puppeteer/test/installation/src/browsers.spec.ts b/remote/test/puppeteer/test/installation/src/browsers.spec.ts new file mode 100644 index 0000000000..83111f3ad3 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/browsers.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {spawnSync} from 'child_process'; + +import {configureSandbox} from './sandbox.js'; + +describe('`@puppeteer/browsers`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers'], + }); + + it('can launch CLI', async function () { + const result = spawnSync('npx', ['@puppeteer/browsers', '--help'], { + // npx is not found without the shell flag on Windows. + shell: process.platform === 'win32', + }); + assert.strictEqual(result.status, 0); + assert.ok( + result.stdout + .toString('utf-8') + .startsWith('@puppeteer/browsers <command>') + ); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/constants.ts b/remote/test/puppeteer/test/installation/src/constants.ts new file mode 100644 index 0000000000..e78c93b159 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/constants.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {dirname, join, resolve} from 'path'; +import {fileURLToPath} from 'url'; + +import glob from 'glob'; + +export const PUPPETEER_CORE_PACKAGE_PATH = resolve( + glob.sync('puppeteer-core-*.tgz')[0]! +); +export const PUPPETEER_BROWSERS_PACKAGE_PATH = resolve( + glob.sync('puppeteer-browsers-[0-9]*.tgz')[0]! +); +export const PUPPETEER_PACKAGE_PATH = resolve( + glob.sync('puppeteer-[0-9]*.tgz')[0]! +); +export const ASSETS_DIR = join( + dirname(fileURLToPath(import.meta.url)), + '..', + 'assets' +); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts new file mode 100644 index 0000000000..f74922844d --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {readdir, writeFile} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer` with configuration', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + before: async cwd => { + await writeFile( + join(cwd, '.puppeteerrc.cjs'), + await readAsset('puppeteer', 'configuration', '.puppeteerrc.cjs') + ); + }, + }); + + it('evaluates', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 1); + assert.equal(files[0], 'chrome'); + + const script = await readAsset('puppeteer', 'basic.js'); + await this.runScript(script, 'mjs'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts new file mode 100644 index 0000000000..03a57d5437 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer-core`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core'], + }); + + it('evaluates CommonJS', async function () { + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); + + for (const product of ['firefox', 'chrome']) { + it(`\`launch\` for \`${product}\` with a bad \`executablePath\``, async function () { + const script = (await readAsset('puppeteer-core', 'launch.js')).replace( + '${product}', + product + ); + await this.runScript(script, 'mjs'); + }); + } +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts new file mode 100644 index 0000000000..edefbd3afe --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {readdir} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer` with Firefox', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + PUPPETEER_PRODUCT: 'firefox', + }; + }, + }); + + it('evaluates CommonJS', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 1); + assert.equal(files[0], 'firefox'); + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts new file mode 100644 index 0000000000..d0b97bf75f --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {readFile, rm, writeFile} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {execFile, readAsset} from './util.js'; + +describe('`puppeteer` with Webpack', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + devDependencies: ['webpack', 'webpack-cli'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates WebPack Bundles', async function () { + // Write a Webpack configuration. + await writeFile( + join(this.sandbox, 'webpack.config.mjs'), + await readAsset('puppeteer', 'webpack', 'webpack.config.js') + ); + + // Write the source code. + await writeFile( + join(this.sandbox, 'index.js'), + await readAsset('puppeteer', 'basic.js') + ); + + // Bundle. + await execFile('npx', ['webpack'], {cwd: this.sandbox, shell: true}); + + // Remove `node_modules` to test independence. + await rm('node_modules', {recursive: true, force: true}); + + const script = await readFile(join(this.sandbox, 'bundle.js'), 'utf-8'); + + await this.runScript(script, 'cjs'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts new file mode 100644 index 0000000000..6633bdb2e2 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; +import {readdir} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates CommonJS', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 1); + assert.equal(files[0], 'chrome'); + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/sandbox.ts b/remote/test/puppeteer/test/installation/src/sandbox.ts new file mode 100644 index 0000000000..7bb9dda194 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/sandbox.ts @@ -0,0 +1,137 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import crypto from 'crypto'; +import {mkdtemp, rm, writeFile} from 'fs/promises'; +import {tmpdir} from 'os'; +import {join} from 'path'; + +import { + PUPPETEER_CORE_PACKAGE_PATH, + PUPPETEER_PACKAGE_PATH, + PUPPETEER_BROWSERS_PACKAGE_PATH, +} from './constants.js'; +import {execFile} from './util.js'; + +const PKG_MANAGER = process.env['PKG_MANAGER'] || 'npm'; + +let ADD_PKG_SUBCOMMAND = 'install'; +if (PKG_MANAGER !== 'npm') { + ADD_PKG_SUBCOMMAND = 'add'; +} + +export interface ItEvaluatesOptions { + commonjs?: boolean; +} + +export interface ItEvaluatesFn { + ( + title: string, + options: ItEvaluatesOptions, + getScriptContent: (cwd: string) => Promise<string> + ): void; + (title: string, getScriptContent: (cwd: string) => Promise<string>): void; +} + +export interface SandboxOptions { + dependencies?: string[]; + devDependencies?: string[]; + /** + * This should be idempotent. + */ + env?: ((cwd: string) => NodeJS.ProcessEnv) | NodeJS.ProcessEnv; + before?: (cwd: string) => Promise<void>; +} + +declare module 'mocha' { + export interface Context { + /** + * The path to the root of the sandbox folder. + */ + sandbox: string; + env: NodeJS.ProcessEnv | undefined; + runScript: (content: string, type: 'cjs' | 'mjs') => Promise<void>; + } +} + +/** + * Configures mocha before/after hooks to create a temp folder and install + * specified dependencies. + */ +export const configureSandbox = (options: SandboxOptions): void => { + before(async function (): Promise<void> { + const sandbox = await mkdtemp(join(tmpdir(), 'puppeteer-')); + const dependencies = (options.dependencies ?? []).map(module => { + switch (module) { + case 'puppeteer': + return PUPPETEER_PACKAGE_PATH; + case 'puppeteer-core': + return PUPPETEER_CORE_PACKAGE_PATH; + case '@puppeteer/browsers': + return PUPPETEER_BROWSERS_PACKAGE_PATH; + default: + return module; + } + }); + const devDependencies = options.devDependencies ?? []; + + let getEnv: (cwd: string) => NodeJS.ProcessEnv | undefined; + if (typeof options.env === 'function') { + getEnv = options.env; + } else { + const env = options.env; + getEnv = () => { + return env; + }; + } + const env = {...process.env, ...getEnv(sandbox)}; + + await options.before?.(sandbox); + if (dependencies.length > 0) { + await execFile(PKG_MANAGER, [ADD_PKG_SUBCOMMAND, ...dependencies], { + cwd: sandbox, + env, + shell: true, + }); + } + if (devDependencies.length > 0) { + await execFile( + PKG_MANAGER, + [ADD_PKG_SUBCOMMAND, '-D', ...devDependencies], + { + cwd: sandbox, + env, + shell: true, + } + ); + } + + this.sandbox = sandbox; + this.env = env; + this.runScript = async (content: string, type: 'cjs' | 'mjs') => { + const script = join(sandbox, `script-${crypto.randomUUID()}.${type}`); + await writeFile(script, content); + await execFile('node', [script], {cwd: sandbox, env}); + }; + }); + + after(async function () { + if (!process.env['KEEP_SANDBOX']) { + await rm(this.sandbox, {recursive: true, force: true, maxRetries: 5}); + } else { + console.log('sandbox saved in', this.sandbox); + } + }); +}; diff --git a/remote/test/puppeteer/test/installation/src/util.ts b/remote/test/puppeteer/test/installation/src/util.ts new file mode 100644 index 0000000000..404691840a --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/util.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {execFile as execFileAsync} from 'child_process'; +import {readFile} from 'fs/promises'; +import {join} from 'path'; +import {promisify} from 'util'; + +import {ASSETS_DIR} from './constants.js'; + +export const execFile = promisify(execFileAsync); +export const readAsset = (...components: string[]): Promise<string> => { + return readFile(join(ASSETS_DIR, ...components), 'utf8'); +}; diff --git a/remote/test/puppeteer/test/installation/tsconfig.json b/remote/test/puppeteer/test/installation/tsconfig.json new file mode 100644 index 0000000000..f749dd8a07 --- /dev/null +++ b/remote/test/puppeteer/test/installation/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "build", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/test/package.json b/remote/test/puppeteer/test/package.json new file mode 100644 index 0000000000..276b0cddc8 --- /dev/null +++ b/remote/test/puppeteer/test/package.json @@ -0,0 +1,30 @@ +{ + "name": "@puppeteer-test/test", + "version": "latest", + "private": true, + "scripts": { + "build": "wireit", + "clean": "tsc -b --clean && rm -rf build" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "../packages/puppeteer:build", + "../packages/testserver:build" + ], + "files": [ + "../tools/mochaRunner/**", + "src/**" + ], + "output": [ + "build/**", + "tsconfig.tsbuildinfo" + ] + } + }, + "dependencies": { + "puppeteer": "file:../packages/puppeteer" + } +} diff --git a/remote/test/puppeteer/test/src/CDPSession.spec.ts b/remote/test/puppeteer/test/src/CDPSession.spec.ts new file mode 100644 index 0000000000..b059e72723 --- /dev/null +++ b/remote/test/puppeteer/test/src/CDPSession.spec.ts @@ -0,0 +1,139 @@ +/** + * 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 {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Target.createCDPSession', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + + await Promise.all([ + client.send('Runtime.enable'), + client.send('Runtime.evaluate', {expression: 'window.foo = "bar"'}), + ]); + const foo = await page.evaluate(() => { + return (globalThis as any).foo; + }); + expect(foo).toBe('bar'); + }); + + it('should not report created targets for custom CDP sessions', async () => { + const {browser} = getTestState(); + let called = 0; + browser.browserContexts()[0]!.on('targetcreated', async target => { + called++; + if (called > 1) { + throw new Error('Too many targets created'); + } + await target.createCDPSession(); + }); + await browser.newPage(); + }); + + it('should send events', async () => { + const {page, server} = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + const events: unknown[] = []; + client.on('Network.requestWillBeSent', event => { + return events.push(event); + }); + await Promise.all([ + waitEvent(client, 'Network.requestWillBeSent'), + page.goto(server.EMPTY_PAGE), + ]); + expect(events).toHaveLength(1); + }); + it('should enable and disable domains independently', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Runtime.enable'); + await client.send('Debugger.enable'); + // JS coverage enables and then disables Debugger domain. + await page.coverage.startJSCoverage(); + await page.coverage.stopJSCoverage(); + // generate a script in page and wait for the event. + const [event] = await Promise.all([ + waitEvent(client, 'Debugger.scriptParsed'), + page.evaluate('//# sourceURL=foo.js'), + ]); + // expect events to be dispatched. + expect(event.url).toBe('foo.js'); + }); + it('should be able to detach session', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + await client.send('Runtime.enable'); + const evalResponse = await client.send('Runtime.evaluate', { + expression: '1 + 2', + returnByValue: true, + }); + expect(evalResponse.result.value).toBe(3); + await client.detach(); + let error!: Error; + try { + await client.send('Runtime.evaluate', { + expression: '3 + 1', + returnByValue: true, + }); + } catch (error_) { + if (isErrorLike(error_)) { + error = error_ as Error; + } + } + expect(error.message).toContain('Session closed.'); + }); + it('should throw nice errors', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + const error = await theSourceOfTheProblems().catch(error => { + return error; + }); + expect(error.stack).toContain('theSourceOfTheProblems'); + expect(error.message).toContain('ThisCommand.DoesNotExist'); + + async function theSourceOfTheProblems() { + // @ts-expect-error This fails in TS as it knows that command does not + // exist but we want to have this tests for our users who consume in JS + // not TS. + await client.send('ThisCommand.DoesNotExist'); + } + }); + + it('should expose the underlying connection', async () => { + const {page} = getTestState(); + + const client = await page.target().createCDPSession(); + expect(client.connection()).toBeTruthy(); + }); +}); diff --git a/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts b/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts new file mode 100644 index 0000000000..b898b2090f --- /dev/null +++ b/remote/test/puppeteer/test/src/DeviceRequestPrompt.spec.ts @@ -0,0 +1,457 @@ +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import { + DeviceRequestPrompt, + DeviceRequestPromptDevice, + DeviceRequestPromptManager, +} from 'puppeteer-core/internal/common/DeviceRequestPrompt.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {TimeoutSettings} from 'puppeteer-core/internal/common/TimeoutSettings.js'; + +class MockCDPSession extends EventEmitter { + async send(): Promise<any> {} + connection() { + return undefined; + } + async detach() {} + id() { + return '1'; + } +} + +describe('DeviceRequestPrompt', function () { + describe('waitForDevicePrompt', function () { + it('should return prompt', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + const [prompt] = await Promise.all([ + manager.waitForDevicePrompt(), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [], + }); + })(), + ]); + expect(prompt).toBeTruthy(); + }); + + it('should respect timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + await expect( + manager.waitForDevicePrompt({timeout: 1}) + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it('should respect default timeout when there is no custom timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + timeoutSettings.setDefaultTimeout(1); + await expect(manager.waitForDevicePrompt()).rejects.toBeInstanceOf( + TimeoutError + ); + }); + + it('should prioritize exact timeout over default timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + timeoutSettings.setDefaultTimeout(0); + await expect( + manager.waitForDevicePrompt({timeout: 1}) + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it('should work with no timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + const [prompt] = await Promise.all([ + manager.waitForDevicePrompt({timeout: 0}), + (async () => { + await new Promise(resolve => { + setTimeout(resolve, 50); + }); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [], + }); + })(), + ]); + expect(prompt).toBeTruthy(); + }); + + it('should return the same prompt when there are many watchdogs simultaneously', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + const [prompt1, prompt2] = await Promise.all([ + manager.waitForDevicePrompt(), + manager.waitForDevicePrompt(), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [], + }); + })(), + ]); + expect(prompt1 === prompt2).toBeTruthy(); + }); + + it('should listen and shortcut when there are no watchdogs', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const manager = new DeviceRequestPromptManager(client, timeoutSettings); + + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [], + }); + + expect(manager).toBeTruthy(); + }); + }); + + describe('DeviceRequestPrompt.devices', function () { + it('lists devices as they arrive', function () { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + expect(prompt.devices).toHaveLength(0); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [{id: '00000000', name: 'Device 0'}], + }); + expect(prompt.devices).toHaveLength(1); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + expect(prompt.devices).toHaveLength(2); + expect(prompt.devices[0]).toBeInstanceOf(DeviceRequestPromptDevice); + expect(prompt.devices[1]).toBeInstanceOf(DeviceRequestPromptDevice); + }); + + it('does not list devices from events of another prompt', function () { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + expect(prompt.devices).toHaveLength(0); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '88888888888888888888888888888888', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + expect(prompt.devices).toHaveLength(0); + }); + }); + + describe('DeviceRequestPrompt.waitForDevice', function () { + it('should return first matching device', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device] = await Promise.all([ + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [{id: '00000000', name: 'Device 0'}], + }); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + expect(device).toBeInstanceOf(DeviceRequestPromptDevice); + }); + + it('should return first matching device from already known devices', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + + const device = await prompt.waitForDevice(({name}) => { + return name.includes('1'); + }); + expect(device).toBeInstanceOf(DeviceRequestPromptDevice); + }); + + it('should return device in the devices list', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device] = await Promise.all([ + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + expect(prompt.devices).toContain(device); + }); + + it('should respect timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + await expect( + prompt.waitForDevice( + ({name}) => { + return name.includes('Device'); + }, + {timeout: 1} + ) + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it('should respect default timeout when there is no custom timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + timeoutSettings.setDefaultTimeout(1); + await expect( + prompt.waitForDevice( + ({name}) => { + return name.includes('Device'); + }, + {timeout: 1} + ) + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it('should prioritize exact timeout over default timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + timeoutSettings.setDefaultTimeout(0); + await expect( + prompt.waitForDevice( + ({name}) => { + return name.includes('Device'); + }, + {timeout: 1} + ) + ).rejects.toBeInstanceOf(TimeoutError); + }); + + it('should work with no timeout', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device] = await Promise.all([ + prompt.waitForDevice( + ({name}) => { + return name.includes('1'); + }, + {timeout: 0} + ), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [{id: '00000000', name: 'Device 0'}], + }); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + expect(device).toBeInstanceOf(DeviceRequestPromptDevice); + }); + + it('should return same device from multiple watchdogs', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device1, device2] = await Promise.all([ + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [{id: '00000000', name: 'Device 0'}], + }); + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + expect(device1 === device2).toBeTruthy(); + }); + }); + + describe('DeviceRequestPrompt.select', function () { + it('should succeed with listed device', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device] = await Promise.all([ + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + await prompt.select(device); + }); + + it('should error for device not listed in devices', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + await expect( + prompt.select(new DeviceRequestPromptDevice('11111111', 'Device 1')) + ).rejects.toThrowError('Cannot select unknown device!'); + }); + + it('should fail when selecting prompt twice', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + + const [device] = await Promise.all([ + prompt.waitForDevice(({name}) => { + return name.includes('1'); + }), + (() => { + client.emit('DeviceAccess.deviceRequestPrompted', { + id: '00000000000000000000000000000000', + devices: [ + {id: '00000000', name: 'Device 0'}, + {id: '11111111', name: 'Device 1'}, + ], + }); + })(), + ]); + await prompt.select(device); + await expect(prompt.select(device)).rejects.toThrowError( + 'Cannot select DeviceRequestPrompt which is already handled!' + ); + }); + }); + + describe('DeviceRequestPrompt.cancel', function () { + it('should succeed on first call', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + await prompt.cancel(); + }); + + it('should fail when canceling prompt twice', async () => { + const client = new MockCDPSession(); + const timeoutSettings = new TimeoutSettings(); + const prompt = new DeviceRequestPrompt(client, timeoutSettings, { + id: '00000000000000000000000000000000', + devices: [], + }); + await prompt.cancel(); + await expect(prompt.cancel()).rejects.toThrowError( + 'Cannot cancel DeviceRequestPrompt which is already handled!' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/EventEmitter.spec.ts b/remote/test/puppeteer/test/src/EventEmitter.spec.ts new file mode 100644 index 0000000000..ef5a6cc065 --- /dev/null +++ b/remote/test/puppeteer/test/src/EventEmitter.spec.ts @@ -0,0 +1,170 @@ +/** + * 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 {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import sinon from 'sinon'; + +describe('EventEmitter', () => { + let emitter: EventEmitter; + + beforeEach(() => { + emitter = new EventEmitter(); + }); + + describe('on', () => { + const onTests = (methodName: 'on' | 'addListener'): void => { + it(`${methodName}: adds an event listener that is fired when the event is emitted`, () => { + const listener = sinon.spy(); + emitter[methodName]('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it(`${methodName} sends the event data to the handler`, () => { + const listener = sinon.spy(); + const data = {}; + emitter[methodName]('foo', listener); + emitter.emit('foo', data); + expect(listener.callCount).toEqual(1); + expect(listener.firstCall.args[0]).toBe(data); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + const returnValue = emitter[methodName]('foo', listener); + expect(returnValue).toBe(emitter); + }); + }; + onTests('on'); + // we support addListener for legacy reasons + onTests('addListener'); + }); + + describe('off', () => { + const offTests = (methodName: 'off' | 'removeListener'): void => { + it(`${methodName}: removes the listener so it is no longer called`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + emitter.off('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it(`${methodName}: supports chaining`, () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const returnValue = emitter.off('foo', listener); + expect(returnValue).toBe(emitter); + }); + }; + offTests('off'); + // we support removeListener for legacy reasons + offTests('removeListener'); + }); + + describe('once', () => { + it('only calls the listener once and then removes it', () => { + const listener = sinon.spy(); + emitter.once('foo', listener); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + emitter.emit('foo'); + expect(listener.callCount).toEqual(1); + }); + + it('supports chaining', () => { + const listener = sinon.spy(); + const returnValue = emitter.once('foo', listener); + expect(returnValue).toBe(emitter); + }); + }); + + describe('emit', () => { + it('calls all the listeners for an event', () => { + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + emitter.on('foo', listener1).on('foo', listener2).on('bar', listener3); + + emitter.emit('foo'); + + expect(listener1.callCount).toEqual(1); + expect(listener2.callCount).toEqual(1); + expect(listener3.callCount).toEqual(0); + }); + + it('passes data through to the listener', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + const data = {}; + + emitter.emit('foo', data); + expect(listener.callCount).toEqual(1); + expect(listener.firstCall.args[0]).toBe(data); + }); + + it('returns true if the event has listeners', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + expect(emitter.emit('foo')).toBe(true); + }); + + it('returns false if the event has listeners', () => { + const listener = sinon.spy(); + emitter.on('foo', listener); + expect(emitter.emit('notFoo')).toBe(false); + }); + }); + + describe('listenerCount', () => { + it('returns the number of listeners for the given event', () => { + emitter.on('foo', () => {}); + emitter.on('foo', () => {}); + emitter.on('bar', () => {}); + expect(emitter.listenerCount('foo')).toEqual(2); + expect(emitter.listenerCount('bar')).toEqual(1); + expect(emitter.listenerCount('noListeners')).toEqual(0); + }); + }); + + describe('removeAllListeners', () => { + it('removes every listener from all events by default', () => { + emitter.on('foo', () => {}).on('bar', () => {}); + + emitter.removeAllListeners(); + expect(emitter.emit('foo')).toBe(false); + expect(emitter.emit('bar')).toBe(false); + }); + + it('returns the emitter for chaining', () => { + expect(emitter.removeAllListeners()).toBe(emitter); + }); + + it('can filter to remove only listeners for a given event name', () => { + emitter + .on('foo', () => {}) + .on('bar', () => {}) + .on('bar', () => {}); + + emitter.removeAllListeners('bar'); + expect(emitter.emit('foo')).toBe(true); + expect(emitter.emit('bar')).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/NetworkManager.spec.ts b/remote/test/puppeteer/test/src/NetworkManager.spec.ts new file mode 100644 index 0000000000..db73f6e601 --- /dev/null +++ b/remote/test/puppeteer/test/src/NetworkManager.spec.ts @@ -0,0 +1,1537 @@ +/** + * 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 {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; +import { + NetworkManager, + NetworkManagerEmittedEvents, +} from 'puppeteer-core/internal/common/NetworkManager.js'; + +// TODO: develop a helper to generate fake network events for attributes that +// are not relevant for the network manager to make tests shorter. + +class MockCDPSession extends EventEmitter { + async send(): Promise<any> {} + connection() { + return undefined; + } + async detach() {} + id() { + return '1'; + } +} + +describe('NetworkManager', () => { + it('should process extra info on multiple redirects', async () => { + const mockCDPSession = new MockCDPSession(); + new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/1.html', + request: { + url: 'http://localhost:8907/redirect/1.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.55635, + wallTime: 1637315638.473634, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: {requestTime: 2111.557593}, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: '/redirect/2.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: /redirect/2.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/2.html', + request: { + url: 'http://localhost:8907/redirect/2.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.559124, + wallTime: 1637315638.47642, + initiator: {type: 'other'}, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/1.html', + status: 302, + statusText: 'Found', + headers: { + location: '/redirect/2.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: false, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 162, + timing: { + requestTime: 2111.557593, + proxyStart: -1, + proxyEnd: -1, + dnsStart: 0.241, + dnsEnd: 0.251, + connectStart: 0.251, + connectEnd: 0.47, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.537, + sendEnd: 0.611, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.939, + }, + responseTime: 1.637315638475744e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: {requestTime: 2111.559346}, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/3.html', + request: { + url: 'http://localhost:8907/redirect/3.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.560249, + wallTime: 1637315638.477543, + initiator: {type: 'other'}, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/2.html', + status: 302, + statusText: 'Found', + headers: { + location: '/redirect/3.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 162, + timing: { + requestTime: 2111.559346, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.15, + sendEnd: 0.196, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.507, + }, + responseTime: 1.637315638477063e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: '/redirect/3.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: /redirect/3.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: {requestTime: 2111.560482}, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/empty.html', + request: { + url: 'http://localhost:8907/empty.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.561542, + wallTime: 1637315638.478837, + initiator: {type: 'other'}, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/3.html', + status: 302, + statusText: 'Found', + headers: { + location: 'http://localhost:8907/empty.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 178, + timing: { + requestTime: 2111.560482, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.149, + sendEnd: 0.198, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.478, + }, + responseTime: 1.637315638478184e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: 'http://localhost:8907/empty.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: http://localhost:8907/empty.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: {requestTime: 2111.561759}, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Content-Length': '0', + }, + resourceIPAddressSpace: 'Local', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n', + }); + mockCDPSession.emit('Network.responseReceived', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + timestamp: 2111.563565, + type: 'Document', + response: { + url: 'http://localhost:8907/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Content-Length': '0', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 2111.561759, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.148, + sendEnd: 0.19, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.925, + }, + responseTime: 1.637315638479928e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: '099A5216AF03AAFEC988F214B024DF08', + }); + }); + it(`should handle "double pause" (crbug.com/1196004) Fetch.requestPaused events for the same Network.requestWillBeSent event`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + manager.setRequestInterception(true); + + const requests: HTTPRequest[] = []; + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + request.continue(); + requests.push(request); + }); + + /** + * This sequence was taken from an actual CDP session produced by the following + * test script: + * + * ```ts + * const browser = await puppeteer.launch({headless: false}); + * const page = await browser.newPage(); + * await page.setCacheEnabled(false); + * + * await page.setRequestInterception(true); + * page.on('request', interceptedRequest => { + * interceptedRequest.continue(); + * }); + * + * await page.goto('https://www.google.com'); + * await browser.close(); + * ``` + */ + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '11ACE9783588040D644B905E8B55285B', + loaderId: '11ACE9783588040D644B905E8B55285B', + documentURL: 'https://www.google.com/', + request: { + url: 'https://www.google.com/', + method: 'GET', + headers: [Object], + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 224604.980827, + wallTime: 1637955746.786191, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '84AC261A351B86932B775B76D1DD79F8', + hasUserGesture: false, + }); + mockCDPSession.emit('Fetch.requestPaused', { + requestId: 'interception-job-1.0', + request: { + url: 'https://www.google.com/', + method: 'GET', + headers: [Object], + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + }, + frameId: '84AC261A351B86932B775B76D1DD79F8', + resourceType: 'Document', + networkId: '11ACE9783588040D644B905E8B55285B', + }); + mockCDPSession.emit('Fetch.requestPaused', { + requestId: 'interception-job-2.0', + request: { + url: 'https://www.google.com/', + method: 'GET', + headers: [Object], + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + }, + frameId: '84AC261A351B86932B775B76D1DD79F8', + resourceType: 'Document', + networkId: '11ACE9783588040D644B905E8B55285B', + }); + + expect(requests).toHaveLength(2); + }); + it(`should handle Network.responseReceivedExtraInfo event after Network.responseReceived event (github.com/puppeteer/puppeteer/issues/8234)`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.RequestFinished, + (request: HTTPRequest) => { + requests.push(request); + } + ); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '1360.2', + loaderId: '9E86B0282CC98B77FB0ABD49156DDFDD', + documentURL: 'http://this.is.the.start.page.com/', + request: { + url: 'http://this.is.a.test.com:1080/test.js', + method: 'GET', + headers: { + 'Accept-Language': 'en-US,en;q=0.9', + Referer: 'http://this.is.the.start.page.com/', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'High', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: false, + }, + timestamp: 10959.020087, + wallTime: 1649712607.861365, + initiator: { + type: 'parser', + url: 'http://this.is.the.start.page.com/', + lineNumber: 9, + columnNumber: 80, + }, + redirectHasExtraInfo: false, + type: 'Script', + frameId: '60E6C35E7E519F28E646056820095498', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceived', { + requestId: '1360.2', + loaderId: '9E86B0282CC98B77FB0ABD49156DDFDD', + timestamp: 10959.042529, + type: 'Script', + response: { + url: 'http://this.is.a.test.com:1080', + status: 200, + statusText: 'OK', + headers: { + connection: 'keep-alive', + 'content-length': '85862', + }, + mimeType: 'text/plain', + connectionReused: false, + connectionId: 119, + remoteIPAddress: '127.0.0.1', + remotePort: 1080, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 66, + timing: { + requestTime: 10959.023904, + proxyStart: -1, + proxyEnd: -1, + dnsStart: 0.328, + dnsEnd: 2.183, + connectStart: 2.183, + connectEnd: 2.798, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 2.982, + sendEnd: 3.757, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 16.373, + }, + responseTime: 1649712607880.971, + protocol: 'http/1.1', + securityState: 'insecure', + }, + hasExtraInfo: true, + frameId: '60E6C35E7E519F28E646056820095498', + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '1360.2', + blockedCookies: [], + headers: { + connection: 'keep-alive', + 'content-length': '85862', + }, + resourceIPAddressSpace: 'Private', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\r\nconnection: keep-alive\r\ncontent-length: 85862\r\n\r\n', + }); + mockCDPSession.emit('Network.loadingFinished', { + requestId: '1360.2', + timestamp: 10959.060708, + encodedDataLength: 85928, + shouldReportCorbBlocking: false, + }); + + expect(requests).toHaveLength(1); + }); + + it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const finishedRequests: HTTPRequest[] = []; + const pendingRequests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.RequestFinished, + (request: HTTPRequest) => { + finishedRequests.push(request); + } + ); + + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + pendingRequests.push(request); + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: 'LOADERID', + loaderId: 'LOADERID', + documentURL: 'http://10.1.0.39:42915/empty.html', + request: { + url: 'http://10.1.0.39:42915/empty.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 671.229856, + wallTime: 1660121157.913774, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: 'FRAMEID', + hasUserGesture: false, + }); + + mockCDPSession.emit('Network.responseReceived', { + requestId: 'LOADERID', + loaderId: 'LOADERID', + timestamp: 671.236025, + type: 'Document', + response: { + url: 'http://10.1.0.39:42915/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 08:45:57 GMT', + 'Keep-Alive': 'timeout=5', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 18, + remoteIPAddress: '10.1.0.39', + remotePort: 42915, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 671.232585, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.308, + sendEnd: 0.364, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 1.554, + }, + responseTime: 1.660121157917951e12, + protocol: 'http/1.1', + securityState: 'insecure', + }, + hasExtraInfo: true, + frameId: 'FRAMEID', + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: 'LOADERID', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.9', + Connection: 'keep-alive', + Host: '10.1.0.39:42915', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', + }, + connectTiming: {requestTime: 671.232585}, + }); + + mockCDPSession.emit('Network.loadingFinished', { + requestId: 'LOADERID', + timestamp: 671.234448, + encodedDataLength: 197, + shouldReportCorbBlocking: false, + }); + + expect(pendingRequests).toHaveLength(1); + expect(finishedRequests).toHaveLength(0); + expect(pendingRequests[0]!.response()).toEqual(null); + + // The extra info might arrive late. + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: 'LOADERID', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 09:04:39 GMT', + 'Keep-Alive': 'timeout=5', + }, + resourceIPAddressSpace: 'Private', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\\r\\nCache-Control: no-cache, no-store\\r\\nContent-Type: text/html; charset=utf-8\\r\\nDate: Wed, 10 Aug 2022 09:04:39 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nContent-Length: 0\\r\\n\\r\\n', + }); + + expect(pendingRequests).toHaveLength(1); + expect(finishedRequests).toHaveLength(1); + expect(pendingRequests[0]!.response()).not.toEqual(null); + }); + + it(`should send responses for iframe that don't receive loadingFinished event`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + requests.push(request); + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '94051D839ACF29E53A3D1273FB20B4C4', + loaderId: '94051D839ACF29E53A3D1273FB20B4C4', + documentURL: 'http://127.0.0.1:54590/empty.html', + request: { + url: 'http://127.0.0.1:54590/empty.html', + method: 'GET', + headers: { + Referer: 'http://localhost:54590/', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: false, + }, + timestamp: 504903.99901, + wallTime: 1660125092.026021, + initiator: { + type: 'script', + stack: { + callFrames: [ + { + functionName: 'navigateFrame', + scriptId: '8', + url: 'pptr://__puppeteer_evaluation_script__', + lineNumber: 2, + columnNumber: 18, + }, + ], + }, + }, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '07D18B8630A8161C72B6079B74123D60', + hasUserGesture: true, + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '94051D839ACF29E53A3D1273FB20B4C4', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + Connection: 'keep-alive', + Host: '127.0.0.1:54590', + Referer: 'http://localhost:54590/', + 'Sec-Fetch-Dest': 'iframe', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'cross-site', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36', + }, + connectTiming: {requestTime: 504904.000422}, + clientSecurityState: { + initiatorIsSecureContext: true, + initiatorIPAddressSpace: 'Local', + privateNetworkRequestPolicy: 'Allow', + }, + }); + + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '94051D839ACF29E53A3D1273FB20B4C4', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 09:51:32 GMT', + 'Keep-Alive': 'timeout=5', + }, + resourceIPAddressSpace: 'Local', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Wed, 10 Aug 2022 09:51:32 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n', + }); + + mockCDPSession.emit('Network.responseReceived', { + requestId: '94051D839ACF29E53A3D1273FB20B4C4', + loaderId: '94051D839ACF29E53A3D1273FB20B4C4', + timestamp: 504904.00338, + type: 'Document', + response: { + url: 'http://127.0.0.1:54590/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 09:51:32 GMT', + 'Keep-Alive': 'timeout=5', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 13, + remoteIPAddress: '127.0.0.1', + remotePort: 54590, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 504904.000422, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.338, + sendEnd: 0.413, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 1.877, + }, + responseTime: 1.660125092029241e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: '07D18B8630A8161C72B6079B74123D60', + }); + + expect(requests).toHaveLength(1); + expect(responses).toHaveLength(1); + expect(requests[0]!.response()).not.toEqual(null); + }); + + it(`should send responses for iframe that don't receive loadingFinished event`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + requests.push(request); + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: 'E18BEB94B486CA8771F9AFA2030FEA37', + loaderId: 'E18BEB94B486CA8771F9AFA2030FEA37', + documentURL: 'http://localhost:56295/empty.html', + request: { + url: 'http://localhost:56295/empty.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 510294.105656, + wallTime: 1660130482.230591, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: 'F9C89A517341F1EFFE63310141630189', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceived', { + requestId: 'E18BEB94B486CA8771F9AFA2030FEA37', + loaderId: 'E18BEB94B486CA8771F9AFA2030FEA37', + timestamp: 510294.119816, + type: 'Document', + response: { + url: 'http://localhost:56295/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 11:21:22 GMT', + 'Keep-Alive': 'timeout=5', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 13, + remoteIPAddress: '[::1]', + remotePort: 56295, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 510294.106734, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 2.195, + sendEnd: 2.29, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 6.493, + }, + responseTime: 1.660130482238109e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: 'F9C89A517341F1EFFE63310141630189', + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: 'E18BEB94B486CA8771F9AFA2030FEA37', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + Connection: 'keep-alive', + Host: 'localhost:56295', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/105.0.5173.0 Safari/537.36', + }, + connectTiming: {requestTime: 510294.106734}, + }); + mockCDPSession.emit('Network.loadingFinished', { + requestId: 'E18BEB94B486CA8771F9AFA2030FEA37', + timestamp: 510294.113383, + encodedDataLength: 197, + shouldReportCorbBlocking: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: 'E18BEB94B486CA8771F9AFA2030FEA37', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 11:21:22 GMT', + 'Keep-Alive': 'timeout=5', + }, + resourceIPAddressSpace: 'Local', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\r\nCache-Control: no-cache, no-store\r\nContent-Type: text/html; charset=utf-8\r\nDate: Wed, 10 Aug 2022 11:21:22 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n', + }); + + expect(requests).toHaveLength(1); + expect(responses).toHaveLength(1); + expect(requests[0]!.response()).not.toEqual(null); + }); + + it(`should handle cached redirects`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const responses: HTTPResponse[] = []; + const requests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.Response, + (response: HTTPResponse) => { + responses.push(response); + } + ); + + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + requests.push(request); + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '6D76C8ACAECE880C722FA515AD380015', + loaderId: '6D76C8ACAECE880C722FA515AD380015', + documentURL: 'http://localhost:3000/', + request: { + url: 'http://localhost:3000/', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 31949.95878, + wallTime: 1680698353.570949, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '6D76C8ACAECE880C722FA515AD380015', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', + Connection: 'keep-alive', + Host: 'localhost:3000', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + 'sec-ch-ua-mobile': '?0', + }, + connectTiming: {requestTime: 31949.959838}, + siteHasCookieInOtherPartition: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '6D76C8ACAECE880C722FA515AD380015', + blockedCookies: [], + headers: { + 'Cache-Control': 'max-age=5', + Connection: 'keep-alive', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\\r\\nContent-Type: text/html; charset=utf-8\\r\\nCache-Control: max-age=5\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n', + cookiePartitionKey: 'http://localhost', + cookiePartitionKeyOpaque: false, + }); + + mockCDPSession.emit('Network.responseReceived', { + requestId: '6D76C8ACAECE880C722FA515AD380015', + loaderId: '6D76C8ACAECE880C722FA515AD380015', + timestamp: 31949.965149, + type: 'Document', + response: { + url: 'http://localhost:3000/', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'max-age=5', + Connection: 'keep-alive', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 34, + remoteIPAddress: '127.0.0.1', + remotePort: 3000, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 31949.959838, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.613, + sendEnd: 0.665, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 3.619, + }, + responseTime: 1.680698353573552e12, + protocol: 'http/1.1', + alternateProtocolUsage: 'unspecifiedReason', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4', + }); + mockCDPSession.emit('Network.loadingFinished', { + requestId: '6D76C8ACAECE880C722FA515AD380015', + timestamp: 31949.963861, + encodedDataLength: 847, + shouldReportCorbBlocking: false, + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + documentURL: 'http://localhost:3000/redirect', + request: { + url: 'http://localhost:3000/redirect', + method: 'GET', + headers: { + Referer: 'http://localhost:3000/', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + 'sec-ch-ua-mobile': '?0', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 31949.982895, + wallTime: 1680698353.595079, + initiator: { + type: 'script', + stack: { + callFrames: [ + { + functionName: '', + scriptId: '5', + url: 'http://localhost:3000/', + lineNumber: 8, + columnNumber: 32, + }, + ], + }, + }, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4', + hasUserGesture: false, + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', + Connection: 'keep-alive', + Host: 'localhost:3000', + Referer: 'http://localhost:3000/', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'same-origin', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + 'sec-ch-ua-mobile': '?0', + }, + connectTiming: {requestTime: 31949.983605}, + siteHasCookieInOtherPartition: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + blockedCookies: [], + headers: { + Connection: 'keep-alive', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + 'Keep-Alive': 'timeout=5', + Location: 'http://localhost:3000/#from-redirect', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\\r\\nLocation: http://localhost:3000/#from-redirect\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n', + cookiePartitionKey: 'http://localhost', + cookiePartitionKeyOpaque: false, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + documentURL: 'http://localhost:3000/', + request: { + url: 'http://localhost:3000/', + urlFragment: '#from-redirect', + method: 'GET', + headers: { + Referer: 'http://localhost:3000/', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + 'sec-ch-ua-mobile': '?0', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 31949.988506, + wallTime: 1680698353.60069, + initiator: { + type: 'script', + stack: { + callFrames: [ + { + functionName: '', + scriptId: '5', + url: 'http://localhost:3000/', + lineNumber: 8, + columnNumber: 32, + }, + ], + }, + }, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:3000/redirect', + status: 302, + statusText: 'Found', + headers: { + Connection: 'keep-alive', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + 'Keep-Alive': 'timeout=5', + Location: 'http://localhost:3000/#from-redirect', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: true, + connectionId: 34, + remoteIPAddress: '127.0.0.1', + remotePort: 3000, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 182, + timing: { + requestTime: 31949.983605, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.364, + sendEnd: 0.401, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 4.085, + }, + responseTime: 1.680698353596548e12, + protocol: 'http/1.1', + alternateProtocolUsage: 'unspecifiedReason', + securityState: 'secure', + }, + type: 'Document', + frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + associatedCookies: [], + headers: {}, + connectTiming: {requestTime: 31949.988855}, + siteHasCookieInOtherPartition: false, + }); + + mockCDPSession.emit('Network.responseReceived', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + loaderId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + timestamp: 31949.991319, + type: 'Document', + response: { + url: 'http://localhost:3000/', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'max-age=5', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + }, + mimeType: 'text/html', + connectionReused: false, + connectionId: 0, + remoteIPAddress: '127.0.0.1', + remotePort: 3000, + fromDiskCache: true, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 0, + timing: { + requestTime: 31949.988855, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.069, + sendEnd: 0.069, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.321, + }, + responseTime: 1.680698353573552e12, + protocol: 'http/1.1', + alternateProtocolUsage: 'unspecifiedReason', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: '4A6E05B1781795F1B586C1F8F8B2CBE4', + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + blockedCookies: [], + headers: { + Connection: 'keep-alive', + Date: 'Wed, 05 Apr 2023 12:39:13 GMT', + 'Keep-Alive': 'timeout=5', + Location: 'http://localhost:3000/#from-redirect', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\\r\\nLocation: http://localhost:3000/#from-redirect\\r\\nDate: Wed, 05 Apr 2023 12:39:13 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n', + cookiePartitionKey: 'http://localhost', + cookiePartitionKeyOpaque: false, + }); + mockCDPSession.emit('Network.loadingFinished', { + requestId: '4C2CC44FB6A6CAC5BE2780BCC9313105', + timestamp: 31949.989412, + encodedDataLength: 0, + shouldReportCorbBlocking: false, + }); + expect( + responses.map(r => { + return r.status(); + }) + ).toEqual([200, 302, 200]); + }); +}); diff --git a/remote/test/puppeteer/test/src/TargetManager.spec.ts b/remote/test/puppeteer/test/src/TargetManager.spec.ts new file mode 100644 index 0000000000..282086eda8 --- /dev/null +++ b/remote/test/puppeteer/test/src/TargetManager.spec.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + CDPBrowser, + CDPBrowserContext, +} from 'puppeteer-core/internal/common/Browser.js'; + +import {getTestState} from './mocha-utils'; // eslint-disable-line import/extensions +import {attachFrame} from './utils.js'; + +describe('TargetManager', () => { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser: CDPBrowser; + let context: CDPBrowserContext; + + 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', + ]), + }) + )) as CDPBrowser; + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + }); + + afterEach(async () => { + await context.close(); + }); + + after(async () => { + await browser.close(); + }); + + it('should handle targets', async () => { + const {server} = getTestState(); + + const targetManager = browser._targetManager(); + expect(targetManager.getAvailableTargets().size).toBe(2); + + expect(await context.pages()).toHaveLength(0); + expect(targetManager.getAvailableTargets().size).toBe(2); + + const page = await context.newPage(); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(3); + + await page.goto(server.EMPTY_PAGE); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(3); + + // attach a local iframe. + let framePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/empty.html'); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(3); + expect(page.frames()).toHaveLength(2); + + // // attach a remote frame iframe. + framePromise = page.waitForFrame(frame => { + return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + await attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(4); + expect(page.frames()).toHaveLength(3); + + framePromise = page.waitForFrame(frame => { + return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + await attachFrame( + page, + 'frame3', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(5); + expect(page.frames()).toHaveLength(4); + }); +}); diff --git a/remote/test/puppeteer/test/src/accessibility.spec.ts b/remote/test/puppeteer/test/src/accessibility.spec.ts new file mode 100644 index 0000000000..3ab518e703 --- /dev/null +++ b/remote/test/puppeteer/test/src/accessibility.spec.ts @@ -0,0 +1,582 @@ +/** + * 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 assert from 'assert'; + +import expect from 'expect'; +import {SerializedAXNode} from 'puppeteer-core/internal/common/Accessibility.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Accessibility', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <head> + <title>Accessibility Test</title> + </head> + <body> + <div>Hello World</div> + <h1>Inputs</h1> + <input placeholder="Empty input" autofocus /> + <input placeholder="readonly input" readonly /> + <input placeholder="disabled input" disabled /> + <input aria-label="Input with whitespace" value=" " /> + <input value="value only" /> + <input aria-placeholder="placeholder" value="and a value" /> + <div aria-hidden="true" id="desc">This is a description!</div> + <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" /> + <select> + <option>First Option</option> + <option>Second Option</option> + </select> + </body>`); + + await page.focus('[placeholder="Empty input"]'); + const golden = isFirefox + ? { + role: 'document', + name: 'Accessibility Test', + children: [ + {role: 'text leaf', name: 'Hello World'}, + {role: 'heading', name: 'Inputs', level: 1}, + {role: 'entry', name: 'Empty input', focused: true}, + {role: 'entry', name: 'readonly input', readonly: true}, + {role: 'entry', name: 'disabled input', disabled: true}, + {role: 'entry', name: 'Input with whitespace', value: ' '}, + {role: 'entry', name: '', value: 'value only'}, + {role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name + { + role: 'entry', + name: '', + value: 'and a value', + description: 'This is a description!', + }, // and here + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: true, + children: [ + { + role: 'combobox option', + name: 'First Option', + selected: true, + }, + {role: 'combobox option', name: 'Second Option'}, + ], + }, + ], + } + : { + role: 'RootWebArea', + name: 'Accessibility Test', + children: [ + {role: 'StaticText', name: 'Hello World'}, + {role: 'heading', name: 'Inputs', level: 1}, + {role: 'textbox', name: 'Empty input', focused: true}, + {role: 'textbox', name: 'readonly input', readonly: true}, + {role: 'textbox', name: 'disabled input', disabled: true}, + {role: 'textbox', name: 'Input with whitespace', value: ' '}, + {role: 'textbox', name: '', value: 'value only'}, + {role: 'textbox', name: 'placeholder', value: 'and a value'}, + { + role: 'textbox', + name: 'placeholder', + value: 'and a value', + description: 'This is a description!', + }, + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: 'menu', + children: [ + {role: 'menuitem', name: 'First Option', selected: true}, + {role: 'menuitem', name: 'Second Option'}, + ], + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('should report uninteresting nodes', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(`<textarea>hi</textarea>`); + await page.focus('textarea'); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'text leaf', + name: 'hi', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'generic', + name: '', + children: [ + { + role: 'StaticText', + name: 'hi', + }, + ], + }, + ], + }; + expect( + findFocusedNode( + await page.accessibility.snapshot({interestingOnly: false}) + ) + ).toEqual(golden); + }); + it('get snapshots while the tree is re-calculated', async () => { + // see https://github.com/puppeteer/puppeteer/issues/9404 + const {page} = getTestState(); + + await page.setContent( + `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Accessible name + aria-expanded puppeteer bug</title> + <style> + [aria-expanded="false"] + * { + display: none; + } + </style> + </head> + <body> + <button hidden>Show</button> + <p>Some content</p> + <script> + const button = document.querySelector('button'); + button.removeAttribute('hidden') + button.setAttribute('aria-expanded', 'false'); + button.addEventListener('click', function() { + button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') !== 'true') + if (button.getAttribute('aria-expanded') == 'true') { + button.textContent = 'Hide' + } else { + button.textContent = 'Show' + } + }) + </script> + </body> + </html>` + ); + async function getAccessibleName(page: any, element: any) { + return (await page.accessibility.snapshot({root: element})).name; + } + const button = await page.$('button'); + expect(await getAccessibleName(page, button)).toEqual('Show'); + await button?.click(); + await page.waitForSelector('aria/Hide'); + }); + it('roledescription', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div tabIndex=-1 aria-roledescription="foo">Hi</div>' + ); + const snapshot = await page.accessibility.snapshot(); + // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862 + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.roledescription).toBeUndefined(); + }); + it('orientation', async () => { + const {page} = getTestState(); + + await page.setContent( + '<a href="" role="slider" aria-orientation="vertical">11</a>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.orientation).toEqual('vertical'); + }); + it('autocomplete', async () => { + const {page} = getTestState(); + + await page.setContent('<input type="number" aria-autocomplete="list" />'); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.autocomplete).toEqual('list'); + }); + it('multiselectable', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.multiselectable).toEqual(true); + }); + it('keyshortcuts', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.keyshortcuts).toEqual('foo'); + }); + describe('filtering children of leaf nodes', function () { + it('should not report text nodes inside controls', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="tablist"> + <div role="tab" aria-selected="true"><b>Tab1</b></div> + <div role="tab">Tab2</div> + </div>`); + const golden = isFirefox + ? { + role: 'document', + name: '', + children: [ + { + role: 'pagetab', + name: 'Tab1', + selected: true, + }, + { + role: 'pagetab', + name: 'Tab2', + }, + ], + } + : { + role: 'RootWebArea', + name: '', + children: [ + { + role: 'tab', + name: 'Tab1', + selected: true, + }, + { + role: 'tab', + name: 'Tab2', + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('rich text editable fields should have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div contenteditable="true"> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + const golden = isFirefox + ? { + role: 'section', + name: '', + children: [ + { + role: 'text leaf', + name: 'Edit this image:', + }, + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'generic', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('rich text editable fields with role should have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div contenteditable="true" role='textbox'> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + // Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392. + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'Edit this image: my fake image', + children: [ + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'Edit this image: ', + multiline: true, + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + + // Firefox does not support contenteditable="plaintext-only". + describe('plaintext contenteditable', function () { + it('plain text field with role should not have children', async () => { + const {page} = getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual({ + role: 'textbox', + name: '', + value: 'Edit this image:', + multiline: true, + }); + }); + }); + it('non editable textbox with role and tabIndex and label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'entry', + name: 'my favorite textbox', + value: 'this is the inner content yo', + } + : { + role: 'textbox', + name: 'my favorite textbox', + value: 'this is the inner content ', + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox with and tabIndex and label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'my favorite checkbox', + checked: true, + } + : { + role: 'checkbox', + name: 'my favorite checkbox', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox without label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="checkbox" aria-checked="true"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'this is the inner content yo', + checked: true, + } + : { + role: 'checkbox', + name: 'this is the inner content yo', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + + describe('root option', function () { + it('should work a button', async () => { + const {page} = getTestState(); + + await page.setContent(`<button>My Button</button>`); + + const button = (await page.$('button'))!; + expect(await page.accessibility.snapshot({root: button})).toEqual({ + role: 'button', + name: 'My Button', + }); + }); + it('should work an input', async () => { + const {page} = getTestState(); + + await page.setContent(`<input title="My Input" value="My Value">`); + + const input = (await page.$('input'))!; + expect(await page.accessibility.snapshot({root: input})).toEqual({ + role: 'textbox', + name: 'My Input', + value: 'My Value', + }); + }); + it('should work a menu', async () => { + const {page} = getTestState(); + + await page.setContent(` + <div role="menu" title="My Menu"> + <div role="menuitem">First Item</div> + <div role="menuitem">Second Item</div> + <div role="menuitem">Third Item</div> + </div> + `); + + const menu = (await page.$('div[role="menu"]'))!; + expect(await page.accessibility.snapshot({root: menu})).toEqual({ + role: 'menu', + name: 'My Menu', + children: [ + {role: 'menuitem', name: 'First Item'}, + {role: 'menuitem', name: 'Second Item'}, + {role: 'menuitem', name: 'Third Item'}, + ], + orientation: 'vertical', + }); + }); + it('should return null when the element is no longer in DOM', async () => { + const {page} = getTestState(); + + await page.setContent(`<button>My Button</button>`); + const button = (await page.$('button'))!; + await page.$eval('button', button => { + return button.remove(); + }); + expect(await page.accessibility.snapshot({root: button})).toEqual(null); + }); + it('should support the interestingOnly option', async () => { + const {page} = getTestState(); + + await page.setContent(`<div><button>My Button</button></div>`); + const div = (await page.$('div'))!; + expect(await page.accessibility.snapshot({root: div})).toEqual(null); + expect( + await page.accessibility.snapshot({ + root: div, + interestingOnly: false, + }) + ).toEqual({ + role: 'generic', + name: '', + children: [ + { + role: 'button', + name: 'My Button', + children: [{role: 'StaticText', name: 'My Button'}], + }, + ], + }); + }); + }); + }); + + function findFocusedNode( + node: SerializedAXNode | null + ): SerializedAXNode | null { + if (node?.focused) { + return node; + } + for (const child of node?.children || []) { + const focusedChild = findFocusedNode(child); + if (focusedChild) { + return focusedChild; + } + } + return null; + } +}); diff --git a/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts new file mode 100644 index 0000000000..52fe99997b --- /dev/null +++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts @@ -0,0 +1,698 @@ +/** + * 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 assert from 'assert'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame} from './utils.js'; + +describe('AriaQueryHandler', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('parseAriaSelector', () => { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + '<button id="btn" role="button"> Submit button and some spaces </button>' + ); + }); + it('should find button', async () => { + const {page} = getTestState(); + const expectFound = async (button: ElementHandle | null) => { + assert(button); + const id = await button.evaluate((button: Element) => { + return button.id; + }); + expect(id).toBe('btn'); + }; + let button = await page.$( + 'aria/Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + "aria/Submit button and some spaces[role='button']" + ); + await expectFound(button); + button = await page.$( + 'aria/ Submit button and some spaces[role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [role="button"]' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button and some spaces [ role = "button" ] ' + ); + await expectFound(button); + button = await page.$( + 'aria/[role="button"]Submit button and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/Submit button [role="button"]and some spaces' + ); + await expectFound(button); + button = await page.$( + 'aria/[name=" Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + button = await page.$( + "aria/[name=' Submit button and some spaces'][role='button']" + ); + await expectFound(button); + button = await page.$( + 'aria/ignored[name="Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow( + 'Unknown aria attribute "smth" in selector' + ); + }); + }); + + describe('queryOne', () => { + it('should find button by role', async () => { + const {page} = getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + const button = (await page.$( + 'aria/[role="button"]' + )) as ElementHandle<HTMLButtonElement>; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find button by name and role', async () => { + const {page} = getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + const button = (await page.$( + 'aria/Submit[role="button"]' + )) as ElementHandle<HTMLButtonElement>; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find first matching element', async () => { + const {page} = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + const div = (await page.$( + 'aria/menu div' + )) as ElementHandle<HTMLDivElement>; + const id = await div!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + const menu = (await page.$( + 'aria/menu-label1' + )) as ElementHandle<HTMLDivElement>; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + const menu = (await page.$( + 'aria/menu-label2' + )) as ElementHandle<HTMLDivElement>; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu2'); + }); + }); + + describe('queryAll', () => { + it('should find menu by name', async () => { + const {page} = getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + const divs = (await page.$$('aria/menu div')) as Array< + ElementHandle<HTMLDivElement> + >; + const ids = await Promise.all( + divs.map(n => { + return n.evaluate(div => { + return div.id; + }); + }) + ); + expect(ids.join(', ')).toBe('mnu1, mnu2'); + }); + }); + describe('queryAllArray', () => { + it('$$eval should handle many elements', async function () { + this.timeout(25_000); + + const {page} = getTestState(); + await page.setContent(''); + await page.evaluate( + ` + for (var i = 0; i <= 10000; i++) { + const button = document.createElement('button'); + button.textContent = i; + document.body.appendChild(button); + } + ` + ); + const sum = await page.$$eval('aria/[role="button"]', buttons => { + return buttons.reduce((acc, button) => { + return acc + Number(button.textContent); + }, 0); + }); + expect(sum).toBe(50005000); + }); + }); + + describe('waitForSelector (aria)', function () { + const addElement = (tag: string) => { + return 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); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should work for ElementHandle.waitForSelector', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (document.body.innerHTML = `<div><button>test</button></div>`); + }); + const element = (await page.$('div'))!; + await element!.waitForSelector('aria/test'); + }); + + it('should persist query handler bindings across reloads', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + await page.reload(); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should persist query handler bindings across navigations', async () => { + const {page, server} = getTestState(); + + // Reset page but make sure that execution context ids start with 1. + await page.goto('data:text/html,'); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + + // Reset page but again make sure that execution context ids start with 1. + await page.goto('data:text/html,'); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should work independently of `exposeFunction`', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('ariaQuerySelector', (a: number, b: number) => { + return a + b; + }); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)'); + expect(result).toBe(10); + }); + + it('should work with removed MutationObserver', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + // @ts-expect-error This is the point of the test. + return delete window.MutationObserver; + }); + const [handle] = await Promise.all([ + page.waitForSelector('aria/anything'), + page.setContent(`<h1>anything</h1>`), + ]); + assert(handle); + expect( + await page.evaluate(x => { + return 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('aria/[role="heading"]'); + await frame.evaluate(addElement, 'br'); + await frame.evaluate(addElement, 'h1'); + const elementHandle = (await watchdog)!; + const tagName = await ( + await elementHandle.getProperty('tagName') + ).jsonValue(); + expect(tagName).toBe('H1'); + }); + + it('should work when node is added through innerHTML', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const watchdog = page.waitForSelector('aria/name'); + await page.evaluate(addElement, 'span'); + await page.evaluate(() => { + return (document.querySelector('span')!.innerHTML = + '<h3><div aria-label="name"></div></h3>'); + }); + await watchdog; + }); + + it('Page.waitForSelector is shortcut for main frame', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const otherFrame = page.frames()[1]; + const watchdog = page.waitForSelector('aria/[role="button"]'); + await otherFrame!.evaluate(addElement, 'button'); + await page.evaluate(addElement, 'button'); + const elementHandle = await watchdog; + expect(elementHandle!.frame).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForSelectorPromise = frame2!.waitForSelector( + 'aria/[role="button"]' + ); + await frame1!.evaluate(addElement, 'button'); + await frame2!.evaluate(addElement, 'button'); + const elementHandle = await waitForSelectorPromise; + expect(elementHandle!.frame).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]; + let waitError!: Error; + const waitPromise = frame! + .waitForSelector('aria/does-not-exist') + .catch(error => { + return (waitError = error); + }); + await 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 imgFound = false; + const waitForSelector = page + .waitForSelector('aria/[role="img"]') + .then(() => { + return (imgFound = true); + }); + await page.goto(server.EMPTY_PAGE); + expect(imgFound).toBe(false); + await page.reload(); + expect(imgFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + await waitForSelector; + expect(imgFound).toBe(true); + }); + + it('should wait for visible', async () => { + const {page} = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/name', {visible: true}) + .then(() => { + return (divFound = true); + }); + await page.setContent( + `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>` + ); + expect(divFound).toBe(false); + await page.evaluate(() => { + return document.querySelector('div')!.style.removeProperty('display'); + }); + expect(divFound).toBe(false); + await page.evaluate(() => { + return 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('aria/inner', {visible: true}) + .then(() => { + return (divVisible = true); + }); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => { + return document.querySelector('div')!.style.removeProperty('display'); + }); + expect(divVisible).toBe(false); + await page.evaluate(() => { + return 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( + `<div role='button' style='display: block;'>text</div>` + ); + const waitForSelector = page + .waitForSelector('aria/[role="button"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + await page.waitForSelector('aria/[role="button"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => { + return 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( + `<div role='main' style='display: block;'>text</div>` + ); + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => { + return 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(`<div role='main'>text</div>`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divRemoved = true); + }); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => { + return 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('aria/non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + + it('should respect timeout', async () => { + const {page} = getTestState(); + + const error = await page + .waitForSelector('aria/[role="button"]', { + timeout: 10, + }) + .catch(error => { + return error; + }); + expect(error.message).toContain( + 'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded' + ); + expect(error).toBeInstanceOf(TimeoutError); + }); + + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const {page} = getTestState(); + + await page.setContent(`<div role='main'>text</div>`); + const promise = page.waitForSelector('aria/[role="main"]', { + hidden: true, + timeout: 10, + }); + await expect(promise).rejects.toMatchObject({ + message: + 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded', + }); + }); + + it('should respond to node attribute mutation', async () => { + const {page} = getTestState(); + + let divFound = false; + const waitForSelector = page.waitForSelector('aria/zombo').then(() => { + return (divFound = true); + }); + await page.setContent(`<div aria-label='notZombo'></div>`); + expect(divFound).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div')! + .setAttribute('aria-label', 'zombo'); + }); + expect(await waitForSelector).toBe(true); + }); + + it('should return the element handle', async () => { + const {page} = getTestState(); + + const waitForSelector = page.waitForSelector('aria/zombo'); + await page.setContent(`<div aria-label='zombo'>anything</div>`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForSelector) + ).toBe('anything'); + }); + + it('should have correct stack trace for timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error!.stack).toContain( + 'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded' + ); + }); + }); + + describe('queryOne (Chromium web test)', async () => { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + ` + <h2 id="shown">title</h2> + <h2 id="hidden" aria-hidden="true">title</h2> + <div id="node1" aria-labeledby="node2"></div> + <div id="node2" aria-label="bar"></div> + <div id="node3" aria-label="foo"></div> + <div id="node4" class="container"> + <div id="node5" role="button" aria-label="foo"></div> + <div id="node6" role="button" aria-label="foo"></div> + <!-- Accessible name not available when element is hidden --> + <div id="node7" hidden role="button" aria-label="foo"></div> + <div id="node8" role="button" aria-label="bar"></div> + </div> + <button id="node10">text content</button> + <h1 id="node11">text content</h1> + <!-- Accessible name not available when role is "presentation" --> + <h1 id="node12" role="presentation">text content</h1> + <!-- Elements inside shadow dom should be found --> + <script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const h1 = document.createElement('h1'); + h1.textContent = 'text content'; + h1.id = 'node13'; + shadowRoot.appendChild(h1); + document.documentElement.appendChild(div); + </script> + <img id="node20" src="" alt="Accessible Name"> + <input id="node21" type="submit" value="Accessible Name"> + <label id="node22" for="node23">Accessible Name</label> + <!-- Accessible name for the <input> is "Accessible Name" --> + <input id="node23"> + <div id="node24" title="Accessible Name"></div> + <div role="treeitem" id="node30"> + <div role="treeitem" id="node31"> + <div role="treeitem" id="node32">item1</div> + <div role="treeitem" id="node33">item2</div> + </div> + <div role="treeitem" id="node34">item3</div> + </div> + <!-- Accessible name for the <div> is "item1 item2 item3" --> + <div aria-describedby="node30"></div> + ` + ); + }); + const getIds = async (elements: ElementHandle[]) => { + return Promise.all( + elements.map(element => { + return element.evaluate((element: Element) => { + return 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"]')) as Array< + ElementHandle<HTMLButtonElement> + >; + 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']); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/bidi/Connection.spec.ts b/remote/test/puppeteer/test/src/bidi/Connection.spec.ts new file mode 100644 index 0000000000..e5cd9de0a1 --- /dev/null +++ b/remote/test/puppeteer/test/src/bidi/Connection.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import {Connection} from 'puppeteer-core/internal/common/bidi/Connection.js'; +import {ConnectionTransport} from 'puppeteer-core/internal/common/ConnectionTransport.js'; + +describe('WebDriver BiDi', () => { + describe('Connection', () => { + class TestConnectionTransport implements ConnectionTransport { + sent: string[] = []; + closed = false; + + send(message: string) { + this.sent.push(message); + } + + close(): void { + this.closed = true; + } + } + + it('should work', async () => { + const transport = new TestConnectionTransport(); + const connection = new Connection(transport); + const responsePromise = connection.send('session.new', { + capabilities: { + proxy: {}, + }, + }); + expect(transport.sent).toEqual([ + `{"id":1,"method":"session.new","params":{"capabilities":{"proxy":{}}}}`, + ]); + const id = JSON.parse(transport.sent[0]!).id; + const rawResponse = { + id, + result: {ready: false, message: 'already connected'}, + }; + (transport as ConnectionTransport).onmessage?.( + JSON.stringify(rawResponse) + ); + const response = await responsePromise; + expect(response).toEqual(rawResponse); + connection.dispose(); + expect(transport.closed).toBeTruthy(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts new file mode 100644 index 0000000000..2023da0f59 --- /dev/null +++ b/remote/test/puppeteer/test/src/browser.spec.ts @@ -0,0 +1,91 @@ +/** + * 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.js'; + +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 !== 'new' + ); + }); + }); + + 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/remote/test/puppeteer/test/src/browsercontext.spec.ts b/remote/test/puppeteer/test/src/browsercontext.spec.ts new file mode 100644 index 0000000000..d17d77457e --- /dev/null +++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts @@ -0,0 +1,243 @@ +/** + * 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 {TimeoutError} from 'puppeteer'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const {browser} = getTestState(); + expect(browser.browserContexts()).toHaveLength(1); + const defaultContext = browser.browserContexts()[0]!; + expect(defaultContext!.isIncognito()).toBe(false); + let error!: Error; + await defaultContext!.close().catch(error_ => { + return (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()).toHaveLength(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts()).toHaveLength(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts()).toHaveLength(1); + }); + it('should close all belonging targets once closing context', async () => { + const {browser} = getTestState(); + + expect(await browser.pages()).toHaveLength(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect(await browser.pages()).toHaveLength(2); + expect(await context.pages()).toHaveLength(1); + + await context.close(); + expect(await browser.pages()).toHaveLength(1); + }); + it('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([ + waitEvent(browser, 'targetcreated'), + page.evaluate(url => { + return window.open(url); + }, server.EMPTY_PAGE), + ]); + expect(popupTarget.browserContext()).toBe(context); + await context.close(); + }); + it('should fire target events', async () => { + const {browser, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const events: any[] = []; + context.on('targetcreated', target => { + return events.push('CREATED: ' + target.url()); + }); + context.on('targetchanged', target => { + return events.push('CHANGED: ' + target.url()); + }); + context.on('targetdestroyed', target => { + return 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(); + }); + it('should wait for a target', async () => { + const {browser, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + let resolved = false; + + const targetPromise = context.waitForTarget(target => { + return target.url() === server.EMPTY_PAGE; + }); + targetPromise + .then(() => { + return (resolved = true); + }) + .catch(error => { + resolved = true; + if (error instanceof 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 TimeoutError) { + console.error(error); + } else { + throw error; + } + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const {browser, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return error_; + }); + expect(error).toBeInstanceOf(TimeoutError); + await context.close(); + }); + + it('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()).toHaveLength(0); + expect(context2.targets()).toHaveLength(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()).toHaveLength(1); + expect(context2.targets()).toHaveLength(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()).toHaveLength(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets()).toHaveLength(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect( + await page1.evaluate(() => { + return localStorage.getItem('name'); + }) + ).toBe('page1'); + expect( + await page1.evaluate(() => { + return document.cookie; + }) + ).toBe('name=page1'); + expect( + await page2.evaluate(() => { + return localStorage.getItem('name'); + }) + ).toBe('page2'); + expect( + await page2.evaluate(() => { + return document.cookie; + }) + ).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([context1.close(), context2.close()]); + expect(browser.browserContexts()).toHaveLength(1); + }); + + it('should work across sessions', async () => { + const {browser, puppeteer} = getTestState(); + + expect(browser.browserContexts()).toHaveLength(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts()).toHaveLength(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts).toHaveLength(2); + remoteBrowser.disconnect(); + await context.close(); + }); + + it('should provide a context id', async () => { + const {browser} = getTestState(); + + expect(browser.browserContexts()).toHaveLength(1); + expect(browser.browserContexts()[0]!.id).toBeUndefined(); + + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts()).toHaveLength(2); + expect(browser.browserContexts()[1]!.id).toBeDefined(); + await context.close(); + }); +}); diff --git a/remote/test/puppeteer/test/src/chromiumonly.spec.ts b/remote/test/puppeteer/test/src/chromiumonly.spec.ts new file mode 100644 index 0000000000..8ddcfe3302 --- /dev/null +++ b/remote/test/puppeteer/test/src/chromiumonly.spec.ts @@ -0,0 +1,172 @@ +/** + * 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 {IncomingMessage} from 'http'; + +import expect from 'expect'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('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(() => { + return 7 * 8; + }) + ).toBe(56); + browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect( + await page2.evaluate(() => { + return 8 * 7; + }) + ).toBe(56); + browser2.disconnect(); + await 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!: Error; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: originalBrowser.wsEndpoint(), + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + + await 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!: Error; + await puppeteer.connect({browserURL}).catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Failed to fetch browser webSocket URL from' + ); + await 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()).toHaveLength(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 = waitEvent(browser, 'disconnected'); + // Emulate user exiting browser. + browser.process()!.kill(); + await disconnectedEventPromise; + }); + }); +}); + +describe('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) => { + return res.end(` + <script> + document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>'); + </script> + `); + }); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest: IncomingMessage | undefined; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', request => { + return 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/remote/test/puppeteer/test/src/click.spec.ts b/remote/test/puppeteer/test/src/click.spec.ts new file mode 100644 index 0000000000..8e253f3085 --- /dev/null +++ b/remote/test/puppeteer/test/src/click.spec.ts @@ -0,0 +1,492 @@ +/** + * 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 {KnownDevices} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame} 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(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should click svg', async () => { + const {page} = getTestState(); + + await page.setContent(` + <svg height="100" width="100"> + <circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /> + </svg> + `); + await page.click('circle'); + expect( + await page.evaluate(() => { + return (globalThis as any).__CLICKED; + }) + ).toBe(42); + }); + it('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(() => { + // @ts-expect-error Expected. + return delete window.Node; + }); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).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(` + <style> + span::before { + content: 'q'; + } + </style> + <span onclick='javascript:window.CLICKED=42'></span> + `); + await page.click('span'); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('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 scroll and click with disabled javascript', async () => { + const {page, server} = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + const body = await page.waitForSelector('body'); + await body!.evaluate(el => { + el.style.paddingTop = '3000px'; + }); + 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(` + <style> + i { + position: absolute; + top: -1000px; + } + </style> + <span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span> + `); + await page.click('span'); + expect( + await page.evaluate(() => { + return (globalThis as any).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.evaluate(() => { + (window as any).clicks = []; + window.addEventListener('click', event => { + return (window as any).clicks.push(event.detail); + }); + }); + await page.click('textarea', {count: 3}); + expect( + await page.evaluate(() => { + return (window as any).clicks; + }) + ).toMatchObject({0: 1, 1: 2, 2: 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: any[] = []; + page.on('console', msg => { + if (msg.type() === 'log') { + return messages.push(msg.text()); + } + return; + }); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => { + return 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(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).result.check; + }) + ).toBe(null); + await page.click('input#agree'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).result.events; + }) + ).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(false); + }); + + it('should click on checkbox label and toggle', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(null); + await page.click('label[for="agree"]'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).result.events; + }) + ).toEqual(['click', 'input', 'change']); + await page.click('label[for="agree"]'); + expect( + await page.evaluate(() => { + return (globalThis as any).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!: Error; + await page.click('button.does-not-exist').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'No element 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} = getTestState(); + + await page.setViewport(KnownDevices['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(() => { + return document.querySelector('#button-5')!.textContent; + }) + ).toBe('clicked'); + await page.click('#button-80'); + expect( + await page.evaluate(() => { + return 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 as any).double = false; + const button = document.querySelector('button'); + button!.addEventListener('dblclick', () => { + (globalThis as any).double = true; + }); + }); + const button = (await page.$('button'))!; + await button!.click({count: 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(() => { + return (globalThis as any).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(() => { + return (globalThis as any).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(() => { + return 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(() => { + return 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(() => { + return 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(() => { + return 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(`<a href="${server.EMPTY_PAGE}">empty.html</a>`); + // 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('<div style="width:100px;height:100px">spacer</div>'); + await 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(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + it('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( + '<div style="width:100px;height:2000px">spacer</div>' + ); + await attachFrame( + page, + 'button-test', + server.CROSS_PROCESS_PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + await frame!.$eval('button', (button: Element) => { + return (button as HTMLElement).style.setProperty('position', 'fixed'); + }); + await frame!.click('button'); + expect( + await frame!.evaluate(() => { + return (globalThis as any).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(() => { + return window.devicePixelRatio; + }) + ).toBe(5); + await page.setContent('<div style="width:100px;height:100px">spacer</div>'); + await 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(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); +}); diff --git a/remote/test/puppeteer/test/src/cookies.spec.ts b/remote/test/puppeteer/test/src/cookies.spec.ts new file mode 100644 index 0000000000..2a048bd008 --- /dev/null +++ b/remote/test/puppeteer/test/src/cookies.spec.ts @@ -0,0 +1,570 @@ +/** + * 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, +} from './mocha-utils.js'; + +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, + 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).toHaveLength(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).toHaveLength(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).toHaveLength(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) => { + return 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, + sourceScheme: 'NonSecure', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('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) => { + return 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 () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + expect( + await page.evaluate(() => { + return document.cookie; + }) + ).toEqual('password=123456'); + }); + it('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).toHaveLength(1); + expect(cookies2).toHaveLength(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(); + }); + it('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 => { + return 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); + }); + it('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) => { + return 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', + }, + ] + ); + }); + it('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!: Error; + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (error_) { + error = error_ as 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!: Error; + 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_ as 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!: Error; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it('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 insecure 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); + }); + it('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', + }, + ]); + }); + it('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 => { + let fulfill!: () => void; + const promise = new Promise<void>(x => { + return (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', + }, + ]); + }); + it('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 => { + let fulfill!: () => void; + const promise = new Promise<void>(x => { + return (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 () { + it('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/remote/test/puppeteer/test/src/coverage.spec.ts b/remote/test/puppeteer/test/src/coverage.spec.ts new file mode 100644 index 0000000000..7c93fc73ba --- /dev/null +++ b/remote/test/puppeteer/test/src/coverage.spec.ts @@ -0,0 +1,356 @@ +/** + * 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, +} from './mocha-utils.js'; + +describe('Coverage specs', function () { + describe('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).toHaveLength(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).toHaveLength(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).toHaveLength(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 => { + return entry.url.startsWith('debugger://'); + }) + ).not.toBe(null); + expect(coverage).toHaveLength(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(() => { + return console.log('bar'); + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(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).toHaveLength(2); + coverage.sort((a, b) => { + return 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).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.ranges).toHaveLength(2); + const range1 = entry.ranges[0]!; + expect(entry.text.substring(range1.start, range1.end)).toBe('\n'); + const range2 = entry.ranges[1]!; + expect(entry.text.substring(range2.start, range2.end)).toBe( + `console.log('used!');if(true===false)` + ); + }); + it('should report right ranges for "per function" scope', async () => { + const {page, server} = getTestState(); + + const coverageOptions = { + useBlockCoverage: false, + }; + + await page.coverage.startJSCoverage(coverageOptions); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.ranges).toHaveLength(2); + const range1 = entry.ranges[0]!; + expect(entry.text.substring(range1.start, range1.end)).toBe('\n'); + const range2 = entry.ranges[1]!; + expect(entry.text.substring(range2.start, range2.end)).toBe( + `console.log('used!');if(true===false)console.log('unused!');` + ); + }); + 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).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges).toHaveLength(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,5}\//g, ':<PORT>/') + ).toBeGolden('jscoverage-involved.txt'); + }); + // @see https://crbug.com/990945 + it.skip('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).toHaveLength(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).toHaveLength(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).toHaveLength(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).toHaveLength(1); + expect(coverage[0]!.rawScriptCoverage).toBeTruthy(); + }); + }); + // @see https://crbug.com/990945 + it.skip('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('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).toHaveLength(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).toHaveLength(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).toHaveLength(2); + coverage.sort((a, b) => { + return 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).toHaveLength(1); + expect(coverage[0]!.url).toBe('unused.css'); + expect(coverage[0]!.ranges).toHaveLength(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).toHaveLength(1); + expect(coverage[0]!.url).toContain('/csscoverage/media.html'); + expect(coverage[0]!.ranges).toEqual([{start: 8, end: 40}]); + }); + 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,5}\//g, ':<PORT>/') + ).toBeGolden('csscoverage-involved.txt'); + }); + it('should work with empty stylesheets', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/empty.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.text).toEqual(''); + }); + 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(() => { + return window.getComputedStyle(document.body).margin; + }); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(0); + }); + it('should work with a recently loaded stylesheet', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.evaluate(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 => { + return (link.onload = x); + }); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(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).toHaveLength(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).toHaveLength(0); + }); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000..73c31d73c4 --- /dev/null +++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts @@ -0,0 +1,115 @@ +/** + * 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, +} from './mocha-utils.js'; + +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, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('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(() => { + return 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', + }, + ]); + }); + it('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/remote/test/puppeteer/test/src/dialog.spec.ts b/remote/test/puppeteer/test/src/dialog.spec.ts new file mode 100644 index 0000000000..e4489004cd --- /dev/null +++ b/remote/test/puppeteer/test/src/dialog.spec.ts @@ -0,0 +1,79 @@ +/** + * 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, +} from './mocha-utils.js'; + +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(() => { + return 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'); + }); + + it('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(() => { + return 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(() => { + return prompt('question?'); + }); + expect(result).toBe(null); + }); +}); diff --git a/remote/test/puppeteer/test/src/diffstyle.css b/remote/test/puppeteer/test/src/diffstyle.css new file mode 100644 index 0000000000..202e85f41a --- /dev/null +++ b/remote/test/puppeteer/test/src/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/remote/test/puppeteer/test/src/drag-and-drop.spec.ts b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts new file mode 100644 index 0000000000..cb46b5e60c --- /dev/null +++ b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts @@ -0,0 +1,197 @@ +/** + * 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, +} from './mocha-utils.js'; + +describe('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 as 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).toHaveLength(1); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragOver; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).didDragStart; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragEnter; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).didDragOver; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).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 as Error).message).toContain( + 'Drag Interception is not enabled!' + ); + } + }); +}); diff --git a/remote/test/puppeteer/test/src/elementhandle.spec.ts b/remote/test/puppeteer/test/src/elementhandle.spec.ts new file mode 100644 index 0000000000..704c262d8c --- /dev/null +++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts @@ -0,0 +1,802 @@ +/** + * 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 {Puppeteer} from 'puppeteer'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + shortWaitForArrayToHaveAtLeastNElements, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('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('<div style="display:none">hi</div>'); + 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( + '<div style="width: 100px; height: 100px">hello</div>' + ); + const elementHandle = (await page.$('div'))!; + await page.evaluate((element: HTMLElement) => { + return (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(` + <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> + <rect id="theRect" x="30" y="50" width="200" height="300"></rect> + </svg> + `); + const element = (await page.$( + '#therect' + )) as ElementHandle<SVGRectElement>; + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate(e => { + const rect = e.getBoundingClientRect(); + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describe('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 attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html'); + await page.evaluate(() => { + const frame = document.querySelector<HTMLElement>('#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('<div style="display:none">hi</div>'); + const element = (await page.$('div'))!; + expect(await element.boxModel()).toBe(null); + }); + }); + + describe('ElementHandle.contentFrame', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = (await page.$('#frame1'))!; + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.isVisible and ElementHandle.isHidden', function () { + it('should work', async () => { + const {page} = getTestState(); + await page.setContent('<div style="display: none">text</div>'); + const element = (await page.waitForSelector('div'))!; + await expect(element.isVisible()).resolves.toBeFalsy(); + await expect(element.isHidden()).resolves.toBeTruthy(); + await element.evaluate(e => { + e.style.removeProperty('display'); + }); + await expect(element.isVisible()).resolves.toBeTruthy(); + await expect(element.isHidden()).resolves.toBeFalsy(); + }); + }); + + describe('ElementHandle.click', function () { + it('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(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should return Point data', async () => { + const {page} = getTestState(); + + const clicks: Array<[x: number, y: number]> = []; + + 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 = ` + <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div> + `; + 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 + ]); + }); + 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. + return button as HTMLButtonElement; + }); + await buttonHandle.click(); + expect( + await page.evaluate(() => { + // @ts-expect-error clicked is expected to be in the page's scope. + return clicked; + }) + ).toBe(true); + }); + it('should not work for TextNodes', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + const buttonTextNode = await page.evaluateHandle(() => { + return document.querySelector('button')!.firstChild as HTMLElement; + }); + let error!: Error; + await buttonTextNode.click().catch(error_ => { + return (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) => { + return button.remove(); + }, button); + let error!: Error; + await button.click().catch(error_ => { + return (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) => { + return (button.style.display = 'none'); + }, button); + const error = await button.click().catch(error_ => { + return 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) => { + return (button.parentElement!.style.display = 'none'); + }, button); + const error = await button.click().catch(error_ => { + return error_; + }); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + it('should throw for <br> elements', async () => { + const {page} = getTestState(); + + await page.setContent('hello<br>goodbye'); + const br = (await page.$('br'))!; + const error = await br.click().catch(error_ => { + return error_; + }); + expect(error.message).toBe( + 'Node is either not clickable or not an HTMLElement' + ); + }); + }); + + describe('ElementHandle.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 = ` + <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div> + `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return 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 = ` + <iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe> + `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return 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('Element.waitForSelector', () => { + it('should wait correctly with waitForSelector on an element', async () => { + const {page} = getTestState(); + const waitFor = page.waitForSelector('.foo') as Promise< + ElementHandle<HTMLDivElement> + >; + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>' + ); + let element = (await waitFor)!; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('.bar') as Promise< + ElementHandle<HTMLDivElement> + >; + await element.evaluate(el => { + el.innerHTML = '<div class="bar">bar1</div>'; + }); + element = (await innerWaitFor)!; + expect(element).toBeDefined(); + expect( + await element.evaluate(el => { + return (el as HTMLElement).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( + `<div id=el1> + el1 + <div id=el2> + el2 + </div> + </div> + <div id=el3> + el3 + </div>` + ); + + const el1 = (await page.waitForSelector( + '#el1' + )) as ElementHandle<HTMLDivElement>; + + for (const path of ['//div', './/div']) { + const e = (await el1.waitForXPath( + path + )) as ElementHandle<HTMLDivElement>; + expect( + await e.evaluate(el => { + return 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(() => { + return 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); + }); + it('should work with svg elements', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/inline-svg.html'); + const visibleCircle = await page.$('circle'); + const visibleSvg = await page.$('svg'); + expect( + await visibleCircle!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + expect( + await visibleCircle!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(true); + expect( + await visibleSvg!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + expect( + await visibleSvg!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(true); + + const invisibleCircle = await page.$('div circle'); + const invisibleSvg = await page.$('div svg'); + expect( + await invisibleCircle!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(false); + expect( + await invisibleCircle!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(false); + expect( + await invisibleSvg!.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(false); + expect( + await invisibleSvg!.isIntersectingViewport({ + threshold: 0, + }) + ).toBe(false); + }); + }); + + describe('Custom queries', function () { + afterEach(() => { + Puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const {page} = getTestState(); + await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); + + // Register. + Puppeteer.registerCustomQueryHandler('getById', { + queryOne: (_element, selector) => { + return document.querySelector(`[id="${selector}"]`); + }, + }); + const element = (await page.$( + 'getById/foo' + )) as ElementHandle<HTMLDivElement>; + expect( + await page.evaluate(element => { + return 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).not.toStrictEqual( + new Error('Custom query handler name not set - throw expected') + ); + } + const handlerNamesAfterUnregistering = + Puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); + }); + it('should throw with invalid query names', () => { + try { + Puppeteer.registerCustomQueryHandler('1/2/3', { + queryOne: () => { + return 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} = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (_element, selector) => { + return [...document.querySelectorAll(`.${selector}`)]; + }, + }); + const elements = (await page.$$('getByClass/foo')) as Array< + ElementHandle<HTMLDivElement> + >; + const classNames = await Promise.all( + elements.map(async element => { + return await page.evaluate(element => { + return element.className; + }, element); + }) + ); + + expect(classNames).toStrictEqual(['foo', 'foo baz']); + }); + it('should eval correctly', async () => { + const {page} = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (_element, selector) => { + return [...document.querySelectorAll(`.${selector}`)]; + }, + }); + const elements = await page.$$eval('getByClass/foo', divs => { + return divs.length; + }); + + expect(elements).toBe(2); + }); + it('should wait correctly with waitForSelector', async () => { + const {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + + it('should wait correctly with waitForSelector on an element', async () => { + const {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo') as Promise< + ElementHandle<HTMLElement> + >; + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>' + ); + let element = (await waitFor)!; + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('getByClass/bar') as Promise< + ElementHandle<HTMLElement> + >; + + await element.evaluate(el => { + el.innerHTML = '<div class="bar">bar1</div>'; + }); + + element = (await innerWaitFor)!; + expect(element).toBeDefined(); + expect( + await element.evaluate(el => { + return 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} = getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo'); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + expect(element).toBeDefined(); + }); + it('should work when both queryOne and queryAll are registered', async () => { + const {page} = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(`.${selector}`)]; + }, + }); + + const element = (await page.$('getByClass/foo'))!; + expect(element).toBeDefined(); + + const elements = await page.$$('getByClass/foo'); + expect(elements).toHaveLength(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const {page} = getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(`.${selector}`)]; + }, + }); + + const txtContent = await page.$eval('getByClass/foo', div => { + return div.textContent; + }); + expect(txtContent).toBe('text'); + + const txtContents = await page.$$eval('getByClass/foo', divs => { + return divs + .map(d => { + return d.textContent; + }) + .join(''); + }); + expect(txtContents).toBe('textcontent'); + }); + + it('should work with function shorthands', async () => { + const {page} = getTestState(); + await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); + + Puppeteer.registerCustomQueryHandler('getById', { + // This is a function shorthand + queryOne(_element, selector) { + return document.querySelector(`[id="${selector}"]`); + }, + }); + + const element = (await page.$( + 'getById/foo' + )) as ElementHandle<HTMLDivElement>; + expect( + await page.evaluate(element => { + return element.id; + }, element) + ).toBe('foo'); + }); + }); + + describe('Element.toElement', () => { + it('should work', async () => { + const {page} = getTestState(); + await page.setContent('<div class="foo">Foo1</div>'); + const element = await page.$('.foo'); + const div = await element?.toElement('div'); + expect(div).toBeDefined(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/emulation.spec.ts b/remote/test/puppeteer/test/src/emulation.spec.ts new file mode 100644 index 0000000000..43ea9bf59c --- /dev/null +++ b/remote/test/puppeteer/test/src/emulation.spec.ts @@ -0,0 +1,516 @@ +/** + * 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 {KnownDevices, PredefinedNetworkConditions} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +const iPhone = KnownDevices['iPhone 6']; +const iPhoneLandscape = KnownDevices['iPhone 6 landscape']; + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + 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(() => { + return window.innerWidth; + }) + ).toBe(800); + await page.setViewport(iPhone.viewport); + expect( + await page.evaluate(() => { + return window.innerWidth; + }) + ).toBe(375); + await page.setViewport({width: 400, height: 300}); + expect( + await page.evaluate(() => { + return 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(() => { + return 'ontouchstart' in window; + }) + ).toBe(false); + await page.setViewport(iPhone.viewport); + expect( + await page.evaluate(() => { + return 'ontouchstart' in window; + }) + ).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({width: 100, height: 100}); + expect( + await page.evaluate(() => { + return 'ontouchstart' in window; + }) + ).toBe(false); + + function dispatchTouch() { + let fulfill!: (value: string) => void; + 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(() => { + return document.body.textContent!.trim(); + }) + ).toBe('NO'); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect( + await page.evaluate(() => { + return 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(() => { + return (globalThis as any).Modernizr.touchevents; + }) + ).toBe(true); + }); + it('should support landscape emulation', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return screen.orientation.type; + }) + ).toBe('portrait-primary'); + await page.setViewport(iPhoneLandscape.viewport); + expect( + await page.evaluate(() => { + return screen.orientation.type; + }) + ).toBe('landscape-primary'); + await page.setViewport({width: 100, height: 100}); + expect( + await page.evaluate(() => { + return 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(() => { + return window.innerWidth; + }) + ).toBe(375); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('iPhone'); + }); + it('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) => { + return (button.style.marginTop = '200px'); + }, button); + await button.click(); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + }); + + describe('Page.emulateMediaType', function () { + it('should work', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(false); + await page.emulateMediaType('print'); + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(true); + await page.emulateMediaType(); + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(false); + }); + it('should throw in case of bad argument', async () => { + const {page} = getTestState(); + + let error!: Error; + await page.emulateMediaType('bad').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe('Page.emulateMediaFeatures', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.emulateMediaFeatures([ + {name: 'prefers-reduced-motion', value: 'reduce'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: reduce)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: no-preference)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'prefers-color-scheme', value: 'light'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: light)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'prefers-color-scheme', value: 'dark'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return 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(() => { + return matchMedia('(prefers-reduced-motion: reduce)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: no-preference)').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: light)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([{name: 'color-gamut', value: 'srgb'}]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'color-gamut', value: 'rec2020'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(true); + }); + it('should throw in case of bad argument', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .emulateMediaFeatures([{name: 'bad', value: ''}]) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describe('Page.emulateTimezone', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + (globalThis as any).date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect( + await page.evaluate(() => { + return (globalThis as any).date.toString(); + }) + ).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + + await page.emulateTimezone('Pacific/Honolulu'); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).date.toString(); + }) + ).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + + await page.emulateTimezone('Europe/Berlin'); + expect( + await page.evaluate(() => { + return (globalThis as any).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!: Error; + await page.emulateTimezone('Foo/Bar').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + + describe('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!: Error; + await page + // @ts-expect-error deliberately passing invalid deficiency + .emulateVisionDeficiency('invalid') + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported vision deficiency: invalid'); + }); + }); + + describe('Page.emulateNetworkConditions', function () { + it('should change navigator.connection.effectiveType', async () => { + const {page} = getTestState(); + + const slow3G = PredefinedNetworkConditions['Slow 3G']!; + const fast3G = PredefinedNetworkConditions['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); + }); + }); + + describe('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/remote/test/puppeteer/test/src/evaluation.spec.ts b/remote/test/puppeteer/test/src/evaluation.spec.ts new file mode 100644 index 0000000000..f8992b051e --- /dev/null +++ b/remote/test/puppeteer/test/src/evaluation.spec.ts @@ -0,0 +1,600 @@ +/** + * 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.js'; +import {attachFrame} from './utils.js'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return 7 * 3; + }); + expect(result).toBe(21); + }); + it('should transfer BigInt', async () => { + const {page} = getTestState(); + + const result = await page.evaluate((a: bigint) => { + return a; + }, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const {page} = getTestState(); + + const result = await page.evaluate( + a => { + return 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 => { + return Array.isArray(a); + }, + [1, 2, 3] + ); + expect(result).toBe(true); + }); + it('should modify global environment', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + return ((globalThis as any).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); + }); + it('should return undefined for objects with symbols', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return [Symbol('foo4')]; + }) + ).toBe(undefined); + }); + it('should work with function shorthands', async () => { + const {page} = getTestState(); + + const a = { + sum(a: number, b: number) { + return a + b; + }, + + async mult(a: number, b: number) { + 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 => { + return a['中文字符']; + }, + { + 中文字符: 42, + } + ); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + location.reload(); + return new Promise(() => {}); + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return 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(() => { + return 6 * 7; + }); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + it('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: number, b: number) { + return await page.evaluate( + (a: number, b: number): number => { + return a * b; + }, + a, + b + ); + } + ); + const result = await page.evaluate(async function () { + return await (globalThis as any).callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + // @ts-expect-error we know the object doesn't exist + return notExistingObject.property; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain('notExistingObject'); + }); + it('should support thrown strings as error messages', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 100500; + }) + .catch(error_ => { + return (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 => { + return a; + }, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + it('should return BigInt', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return BigInt(42); + }); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return NaN; + }); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return -0; + }); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return Infinity; + }); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return -Infinity; + }); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "null" as one of multiple parameters', async () => { + const {page} = getTestState(); + + const result = await page.evaluate( + (a, b) => { + return Object.is(a, null) && Object.is(b, 'foo'); + }, + null, + 'foo' + ); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return {a: undefined}; + }) + ).toEqual({}); + }); + it('should return undefined for non-serializable objects', async () => { + const {page} = getTestState(); + + expect( + await page.evaluate(() => { + return window; + }) + ).toBe(undefined); + }); + it('should return promise as empty object', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return { + promise: new Promise(resolve => { + setTimeout(resolve, 1000); + }), + }; + }); + expect(result).toEqual({ + promise: {}, + }); + }); + it('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); + }); + it('should be able to throw a tricky error', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + const errorText = await windowHandle.jsonValue().catch(error_ => { + return error_.message; + }); + const error = await page + .evaluate(errorText => { + throw new Error(errorText); + }, errorText) + .catch(error_ => { + return 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('<section>42</section>'); + const element = (await page.$('section'))!; + const text = await page.evaluate(e => { + return e.textContent; + }, element); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async () => { + const {page} = getTestState(); + + await page.setContent('<section>39</section>'); + const element = (await page.$('section'))!; + expect(element).toBeTruthy(); + await element.dispose(); + let error!: Error; + await page + .evaluate((e: HTMLElement) => { + return e.textContent; + }, element) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('JSHandle is disposed'); + }); + it('should throw if elementHandles are from other frames', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const bodyHandle = await page.frames()[1]!.$('body'); + let error!: Error; + await page + .evaluate(body => { + return body?.innerHTML; + }, bodyHandle) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'JSHandles can be evaluated only in the context they were created' + ); + }); + it('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); + }); + it('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(() => { + return window.location.reload(); + }), + ]); + const error = await executionContext + .evaluate(() => { + return null; + }) + .catch(error_ => { + return error_; + }); + expect((error as Error).message).toContain('navigation'); + }); + it('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 () { + this.timeout(25_000); + const {page} = getTestState(); + + const a = await page.evaluate(() => { + return 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!: Error; + await page + .evaluate(() => { + return new Promise(() => { + throw new Error('Error in promise'); + }); + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Error in promise'); + }); + }); + + describe('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const {page, server} = getTestState(); + + await page.evaluateOnNewDocument(function () { + (globalThis as any).injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).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 as any).injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).injected; + }) + ).toBe(123); + + // Make sure CSP works. + await page.addScriptTag({content: 'window.e = 10;'}).catch(error => { + return void error; + }); + expect( + await page.evaluate(() => { + return (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 attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(2); + await page.frames()[0]!.evaluate(() => { + return ((globalThis as any).FOO = 'foo'); + }); + await page.frames()[1]!.evaluate(() => { + return ((globalThis as any).FOO = 'bar'); + }); + expect( + await page.frames()[0]!.evaluate(() => { + return (globalThis as any).FOO; + }) + ).toBe('foo'); + expect( + await page.frames()[1]!.evaluate(() => { + return (globalThis as any).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()).toHaveLength(2); + expect( + await page.frames()[0]!.evaluate(() => { + return document.body.textContent!.trim(); + }) + ).toBe(''); + expect( + await page.frames()[1]!.evaluate(() => { + return 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(() => { + return window.location.href; + }) + ).toContain('localhost'); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect( + await mainFrame.evaluate(() => { + return window.location.href; + }) + ).toContain('127'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/fixtures.spec.ts b/remote/test/puppeteer/test/src/fixtures.spec.ts new file mode 100644 index 0000000000..4065b867fc --- /dev/null +++ b/remote/test/puppeteer/test/src/fixtures.spec.ts @@ -0,0 +1,112 @@ +/** + * 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 path from 'path'; + +import expect from 'expect'; + +import {getTestState} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Fixtures', function () { + it('dumpio option should work with pipe option', async () => { + const {defaultBrowserOptions, puppeteerPath, headless} = getTestState(); + if (headless === 'new') { + // This test only works in the old headless mode. + return; + } + + let dumpioData = ''; + const {spawn} = await import('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 => { + return (dumpioData += data.toString('utf8')); + }); + await new Promise(resolve => { + return 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} = await import('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 => { + return (dumpioData += data.toString('utf8')); + }); + await new Promise(resolve => { + return 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} = await import('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: (value: string) => void; + const wsEndPointPromise = new Promise<string>(x => { + return (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 = [ + waitEvent(browser, 'disconnected'), + new Promise(resolve => { + return 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/remote/test/puppeteer/test/src/frame.spec.ts b/remote/test/puppeteer/test/src/frame.spec.ts new file mode 100644 index 0000000000..4d4d05cce6 --- /dev/null +++ b/remote/test/puppeteer/test/src/frame.spec.ts @@ -0,0 +1,350 @@ +/** + * 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 {CDPSession} from 'puppeteer-core/internal/common/Connection.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import { + attachFrame, + detachFrame, + dumpFrames, + navigateFrame, + waitEvent, +} from './utils.js'; + +describe('Frame specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.executionContext', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(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._world?.frame()).toBe(frame1); + expect(context2._world?.frame()).toBe(frame2); + + await Promise.all([ + context1.evaluate(() => { + return ((globalThis as any).a = 1); + }), + context2.evaluate(() => { + return ((globalThis as any).a = 2); + }), + ]); + const [a1, a2] = await Promise.all([ + context1.evaluate(() => { + return (globalThis as any).a; + }), + context2.evaluate(() => { + return (globalThis as any).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(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + it('should throw for detached frames', async () => { + const {page, server} = getTestState(); + + const frame1 = (await attachFrame(page, 'frame1', server.EMPTY_PAGE))!; + await detachFrame(page, 'frame1'); + let error!: Error; + await frame1 + .evaluate(() => { + return 7 * 8; + }) + .catch(error_ => { + return (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 => { + return arr; + }, readonlyArray); + }); + }); + + describe('Frame.page', function () { + it('should retrieve the page from a frame', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect(mainFrame.page()).toEqual(page); + }); + }); + + describe('Frame Management', function () { + it('should handle nested frames', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + }); + it('should send events when frames are manipulated dynamically', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + await attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames).toHaveLength(1); + expect(attachedFrames[0]!.url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames: Frame[] = []; + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames).toHaveLength(1); + expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames: Frame[] = []; + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + await detachFrame(page, 'frame1'); + expect(detachedFrames).toHaveLength(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'), + 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', () => { + return (hasEvents = true); + }); + page.on('framedetached', () => { + return (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: Frame[] = []; + let detachedFrames: Frame[] = []; + let navigatedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(attachedFrames).toHaveLength(4); + expect(detachedFrames).toHaveLength(0); + expect(navigatedFrames).toHaveLength(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames).toHaveLength(0); + expect(detachedFrames).toHaveLength(4); + expect(navigatedFrames).toHaveLength(1); + }); + it('should support framesets', async () => { + const {page, server} = getTestState(); + + let attachedFrames: Frame[] = []; + let detachedFrames: Frame[] = []; + let navigatedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames).toHaveLength(4); + expect(detachedFrames).toHaveLength(0); + expect(navigatedFrames).toHaveLength(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames).toHaveLength(0); + expect(detachedFrames).toHaveLength(4); + expect(navigatedFrames).toHaveLength(1); + }); + it('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 => { + return (frame.onload = x); + }); + }, server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(2); + expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should report frame.name()', async () => { + const {page, server} = getTestState(); + + await 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 => { + return (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'); + }); + it('should report frame.parent()', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await 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()); + }); + it('should report different frame instance when frame re-attaches', async () => { + const {page, server} = getTestState(); + + const frame1 = await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.evaluate(() => { + (globalThis as any).frame = document.querySelector('#frame1'); + (globalThis as any).frame.remove(); + }); + expect(frame1!.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + waitEvent(page, 'frameattached'), + page.evaluate(() => { + return document.body.appendChild((globalThis as any).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()).toHaveLength(2); + expect(page.frames()[1]!.url()).toBe( + server.PREFIX + '/frames/frame.html?param=value#fragment' + ); + }); + it('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 => { + return 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/remote/test/puppeteer/test/src/golden-utils.ts b/remote/test/puppeteer/test/src/golden-utils.ts new file mode 100644 index 0000000000..3ebe534aae --- /dev/null +++ b/remote/test/puppeteer/test/src/golden-utils.ts @@ -0,0 +1,173 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; + +import {diffLines} from 'diff'; +import jpeg from 'jpeg-js'; +import mime from 'mime'; +import pixelmatch from 'pixelmatch'; +import {PNG} from 'pngjs'; + +interface DiffFile { + diff: string | Buffer; + ext?: string; +} + +const GoldenComparators = new Map< + string, + ( + actualBuffer: string | Buffer, + expectedBuffer: string | Buffer, + mimeType: string + ) => DiffFile | undefined +>(); + +const addSuffix = ( + filePath: string, + suffix: string, + customExtension?: string +): string => { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +}; + +const compareImages = ( + actualBuffer: string | Buffer, + expectedBuffer: string | Buffer, + mimeType: string +): DiffFile | undefined => { + assert(typeof actualBuffer !== 'string'); + assert(typeof expectedBuffer !== 'string'); + + 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) { + throw new Error( + `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)} : undefined; +}; + +const compareText = ( + actual: string | Buffer, + expectedBuffer: string | Buffer +): DiffFile | undefined => { + assert(typeof actual === 'string'); + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) { + return; + } + const result = diffLines(expected, actual); + const html = result.reduce((text, change) => { + text += change.added + ? `<span class='ins'>${change.value}</span>` + : change.removed + ? `<span class='del'>${change.value}</span>` + : change.value; + return text; + }, `<link rel="stylesheet" href="file://${path.join(__dirname, 'diffstyle.css')}">`); + return { + diff: html, + ext: '.html', + }; +}; + +GoldenComparators.set('image/png', compareImages); +GoldenComparators.set('image/jpeg', compareImages); +GoldenComparators.set('text/plain', compareText); + +export const compare = ( + goldenPath: string, + outputPath: string, + actual: string | Buffer, + goldenName: string +): {pass: true} | {pass: false; message: string} => { + 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); + assert(mimeType); + const comparator = GoldenComparators.get(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) { + const diffPath = addSuffix(actualPath, '-diff', result.ext); + fs.writeFileSync(diffPath, result.diff); + } + + return { + pass: false, + message: `${goldenName} mismatch! ${messageSuffix}`, + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath); + } + } +}; diff --git a/remote/test/puppeteer/test/src/headful.spec.ts b/remote/test/puppeteer/test/src/headful.spec.ts new file mode 100644 index 0000000000..f75654f1e1 --- /dev/null +++ b/remote/test/puppeteer/test/src/headful.spec.ts @@ -0,0 +1,438 @@ +/** + * 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 {mkdtemp} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import expect from 'expect'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; + +import {getTestState} from './mocha-utils.js'; + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, '../assets', 'simple-extension'); +const serviceWorkerExtensionPath = path.join( + __dirname, + '..', + 'assets', + 'serviceworkers', + 'extension' +); + +describe('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions: PuppeteerLaunchOptions | undefined; + let headlessOptions: PuppeteerLaunchOptions & {headless: boolean}; + let extensionOptions: PuppeteerLaunchOptions & { + headless: boolean; + args: string[]; + }; + let forcedOopifOptions: PuppeteerLaunchOptions & { + headless: boolean; + devtools: boolean; + args: string[]; + }; + let devtoolsOptions: PuppeteerLaunchOptions & { + headless: boolean; + devtools: boolean; + }; + const browsers: any[] = []; + + 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: PuppeteerNode, options: any) { + 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 => { + return target.type() === 'background_page'; + } + ); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + it('service_worker target type should be available', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const browserWithExtension = await launchBrowser(puppeteer, { + ...defaultBrowserOptions, + headless: false, + args: [ + `--disable-extensions-except=${serviceWorkerExtensionPath}`, + `--load-extension=${serviceWorkerExtensionPath}`, + ], + }); + const page = await browserWithExtension.newPage(); + const serviceWorkerTarget = await browserWithExtension.waitForTarget( + target => { + return target.type() === 'service_worker'; + } + ); + await page.close(); + await browserWithExtension.close(); + expect(serviceWorkerTarget).toBeTruthy(); + }); + it('target.page() should return a background_page', async function () { + const {puppeteer} = getTestState(); + const browserWithExtension = await launchBrowser( + puppeteer, + extensionOptions + ); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + target => { + return target.type() === 'background_page'; + } + ); + const page = (await backgroundPageTarget.page())!; + expect( + await page.evaluate(() => { + return 2 * 3; + }) + ).toBe(6); + expect( + await page.evaluate(() => { + return (globalThis as any).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 => { + return target.type() === 'other'; + }); + const page = (await devtoolsPageTarget.page())!; + expect( + await page.evaluate(() => { + return 2 * 3; + }) + ).toBe(6); + expect(await browser.pages()).toContainEqual(page); + 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: {url: () => any}) => { + return page.url(); + }); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it('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 mkdtemp(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(() => { + return (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(() => { + return document.cookie; + }); + await headlessBrowser.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + expect(cookie).toBe('foo=true'); + }); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + it.skip('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: {respond: (arg0: {body: string}) => any}) => { + return 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 => { + return (frame.onload = x); + }); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page + .frames() + .map((frame: {url: () => any}) => { + return 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: any[] = []; + const otherSessions: any[] = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.on( + 'sessionattached', + async (session: { + on: (arg0: string, arg1: (params: any) => number) => void; + send: (arg0: string) => any; + }) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params: any) => { + return 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: string) => { + 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 => { + return 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: {url: () => string | string[]}) => { + return 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(() => { + return document.visibilityState; + }) + ).toBe('visible'); + expect( + await page2.evaluate(() => { + return document.visibilityState; + }) + ).toBe('hidden'); + + await page2.bringToFront(); + expect( + await page1.evaluate(() => { + return document.visibilityState; + }) + ).toBe('hidden'); + expect( + await page2.evaluate(() => { + return 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 => { + return page.close(); + }) + ); + + await browser.close(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/idle_override.spec.ts b/remote/test/puppeteer/test/src/idle_override.spec.ts new file mode 100644 index 0000000000..1b2dbb894a --- /dev/null +++ b/remote/test/puppeteer/test/src/idle_override.spec.ts @@ -0,0 +1,95 @@ +/** + * 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 {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Emulate idle state', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + async function getIdleState() { + const {page} = getTestState(); + + const stateElement = (await page.$('#state')) as ElementHandle<HTMLElement>; + return await page.evaluate(element => { + 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/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts new file mode 100644 index 0000000000..8217092e22 --- /dev/null +++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts @@ -0,0 +1,143 @@ +/** + * 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 {TLSSocket} from 'tls'; + +import expect from 'expect'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState} from './mocha-utils.js'; + +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!: Browser; + let context: BrowserContext; + let page!: Page; + + before(async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign( + {ignoreHTTPSErrors: true}, + defaultBrowserOptions + ); + browser = await puppeteer.launch(options); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async () => { + context = await browser.createIncognitoBrowserContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + }); + + describe('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 as TLSSocket) + .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: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + const [serverRequest] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect'), + ]); + expect(responses).toHaveLength(2); + expect(responses[0]!.status()).toBe(302); + const securityDetails = responses[0]!.securityDetails()!; + const protocol = (serverRequest.socket as TLSSocket) + .getProtocol()! + .replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async () => { + const {httpsServer} = getTestState(); + + let error!: Error; + const response = await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + expect(response.ok()).toBe(true); + }); + it('should work with request interception', async () => { + const {httpsServer} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const response = (await page.goto(httpsServer.EMPTY_PAGE))!; + expect(response.status()).toBe(200); + }); + it('should work with mixed content', async () => { + const {server, httpsServer} = getTestState(); + + httpsServer.setRoute('/mixedcontent.html', (_req, res) => { + res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`); + }); + await page.goto(httpsServer.PREFIX + '/mixedcontent.html', { + waitUntil: 'load', + }); + expect(page.frames()).toHaveLength(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/remote/test/puppeteer/test/src/injected.spec.ts b/remote/test/puppeteer/test/src/injected.spec.ts new file mode 100644 index 0000000000..836ee479a1 --- /dev/null +++ b/remote/test/puppeteer/test/src/injected.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import {PUPPETEER_WORLD} from 'puppeteer-core/internal/common/IsolatedWorlds.js'; +import {LazyArg} from 'puppeteer-core/internal/common/LazyArg.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('PuppeteerUtil tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page} = getTestState(); + + const world = page.mainFrame().worlds[PUPPETEER_WORLD]; + const value = await world.evaluate( + PuppeteerUtil => { + return typeof PuppeteerUtil === 'object'; + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }) + ); + expect(value).toBeTruthy(); + }); + + describe('createFunction tests', function () { + it('should work', async () => { + const {page} = getTestState(); + + const world = page.mainFrame().worlds[PUPPETEER_WORLD]; + const value = await world.evaluate( + ({createFunction}, fnString) => { + return createFunction(fnString)(4); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + (() => { + return 4; + }).toString() + ); + expect(value).toBe(4); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/input.spec.ts b/remote/test/puppeteer/test/src/input.spec.ts new file mode 100644 index 0000000000..5519fca4c1 --- /dev/null +++ b/remote/test/puppeteer/test/src/input.spec.ts @@ -0,0 +1,409 @@ +/** + * 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 {TimeoutError} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('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 as any)._inputEvents = []; + e.addEventListener('change', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + e.addEventListener('input', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + }, input); + await input.uploadFile(filePath); + expect( + await page.evaluate((e: HTMLInputElement) => { + return e.files![0]!.name; + }, input) + ).toBe('file-to-upload.txt'); + expect( + await page.evaluate((e: HTMLInputElement) => { + return e.files![0]!.type; + }, input) + ).toBe('text/plain'); + expect( + await page.evaluate(() => { + return (globalThis as any)._inputEvents; + }) + ).toEqual(['input', 'change']); + expect( + await page.evaluate((e: HTMLInputElement) => { + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onload = fulfill); + }); + reader.readAsText(e.files![0]!); + return promise.then(() => { + return reader.result; + }); + }, input) + ).toBe('contents of the file'); + }); + }); + + describe('Page.waitForFileChooser', function () { + it('should work when file input is attached to DOM', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + 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} = getTestState(); + + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const {page} = getTestState(); + + page.setDefaultTimeout(1); + let error!: Error; + await page.waitForFileChooser().catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const {page} = getTestState(); + + page.setDefaultTimeout(0); + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with no timeout', async () => { + const {page} = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({timeout: 0}), + page.evaluate(() => { + return 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(`<input type=file>`); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describe('FileChooser.accept', function () { + it('should accept single file', async () => { + const {page} = getTestState(); + + await page.setContent( + `<input type=file oninput='javascript:console.timeStamp()'>` + ); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + waitEvent(page, 'metrics'), + ]); + expect( + await page.$eval('input', input => { + return (input as HTMLInputElement).files!.length; + }) + ).toBe(1); + expect( + await page.$eval('input', input => { + return (input as HTMLInputElement).files![0]!.name; + }) + ).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + page.waitForFileChooser().then(chooser => { + return chooser.accept([FILE_TO_UPLOAD]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onload = fulfill); + }); + reader.readAsText(pick.files![0]!); + return promise.then(() => { + return 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(`<input type=file>`); + page.waitForFileChooser().then(chooser => { + return chooser.accept([FILE_TO_UPLOAD]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + return pick.files!.length; + }) + ).toBe(1); + page.waitForFileChooser().then(chooser => { + return chooser.accept([]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + return pick.files!.length; + }) + ).toBe(0); + }); + it('should not accept multiple files for single-file input', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error!: Error; + await chooser + .accept([ + path.relative( + process.cwd(), + __dirname + '/../assets/file-to-upload.txt' + ), + path.relative(process.cwd(), __dirname + '/../assets/pptr.png'), + ]) + .catch(error_ => { + return (error = error_); + }); + expect(error).not.toBe(null); + }); + it('should succeed even for non-existent files', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error!: Error; + await chooser.accept(['file-does-not-exist.txt']).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should error on read of non-existent files', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + page.waitForFileChooser().then(chooser => { + return chooser.accept(['file-does-not-exist.txt']); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onerror = fulfill); + }); + reader.readAsText(pick.files![0]!); + return promise.then(() => { + return false; + }); + }) + ).toBeFalsy(); + }); + it('should fail when accepting file chooser twice', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + await fileChooser.accept([]); + let error!: Error; + await fileChooser.accept([]).catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'Cannot accept FileChooser which is already handled!' + ); + }); + }); + + describe('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(`<input type=file>`); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + }); + it('should fail when canceling file chooser twice', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLElement).click(); + }), + ]); + await fileChooser.cancel(); + let error!: Error; + + try { + fileChooser.cancel(); + } catch (error_) { + error = error_ as Error; + } + + expect(error.message).toBe( + 'Cannot cancel FileChooser which is already handled!' + ); + }); + }); + + describe('FileChooser.isMultiple', () => { + it('should work for single file pick', async () => { + const {page} = getTestState(); + + await page.setContent(`<input type=file>`); + 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(`<input multiple type=file>`); + 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(`<input multiple webkitdirectory type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/jshandle.spec.ts b/remote/test/puppeteer/test/src/jshandle.spec.ts new file mode 100644 index 0000000000..3d307a0523 --- /dev/null +++ b/remote/test/puppeteer/test/src/jshandle.spec.ts @@ -0,0 +1,339 @@ +/** + * 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.js'; + +describe('JSHandle', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + it('should return the RemoteObject', async () => { + const {page} = getTestState(); + + const windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle.remoteObject()).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const {page} = getTestState(); + + const navigatorHandle = await page.evaluateHandle(() => { + return navigator; + }); + const text = await page.evaluate(e => { + return e.userAgent; + }, navigatorHandle); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return 5; + }); + const isFive = await page.evaluate(e => { + return Object.is(e, 5); + }, aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn about recursive objects', async () => { + const {page} = getTestState(); + + const test: {obj?: unknown} = {}; + test.obj = test; + let error!: Error; + await page + .evaluateHandle(opts => { + return opts; + }, test) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Recursive objects are not allowed.'); + }); + it('should accept object handle to unserializable value', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return Infinity; + }); + expect( + await page.evaluate(e => { + return Object.is(e, Infinity); + }, aHandle) + ).toBe(true); + }); + it('should use the same JS wrappers', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + (globalThis as any).FOO = 123; + return window; + }); + expect( + await page.evaluate(e => { + return (e as any).FOO; + }, aHandle) + ).toBe(123); + }); + }); + + describe('JSHandle.getProperty', function () { + it('should work', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return { + one: 1, + two: 2, + three: 3, + }; + }); + const twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + }); + + describe('JSHandle.jsonValue', function () { + it('should work', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return {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(() => { + return ['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(() => { + return 'foo'; + }); + expect(await aHandle.jsonValue()).toEqual('foo'); + + const bHandle = await page.evaluateHandle(() => { + return undefined; + }); + expect(await bHandle.jsonValue()).toEqual(undefined); + }); + + it('should not work with dates', async () => { + const {page} = getTestState(); + + const dateHandle = await page.evaluateHandle(() => { + return 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} = getTestState(); + + const handle = await page.evaluateHandle(() => { + const t: {t?: unknown; g: number} = {g: 1}; + t.t = t; + return t; + }); + let error!: Error; + await handle.jsonValue().catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Could not serialize referenced object'); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return { + 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(() => { + return 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(() => { + return 2; + }); + const element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const {page} = getTestState(); + + await page.setContent('<div>ee!</div>'); + const aHandle = await page.evaluateHandle(() => { + return document.querySelector('div')!.firstChild; + }); + const element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect( + await page.evaluate(e => { + return e?.nodeType === Node.TEXT_NODE; + }, element) + ); + }); + }); + + describe('JSHandle.toString', function () { + it('should work for primitives', async () => { + const {page} = getTestState(); + + const numberHandle = await page.evaluateHandle(() => { + return 2; + }); + expect(numberHandle.toString()).toBe('JSHandle:2'); + const stringHandle = await page.evaluateHandle(() => { + return 'a'; + }); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + return 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' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/keyboard.spec.ts b/remote/test/puppeteer/test/src/keyboard.spec.ts new file mode 100644 index 0000000000..1ae6db22dd --- /dev/null +++ b/remote/test/puppeteer/test/src/keyboard.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 os from 'os'; + +import expect from 'expect'; +import {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame} from './utils.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(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe(text); + }); + it('should press the metaKey', async () => { + const {page, isFirefox} = getTestState(); + + await page.evaluate(() => { + (window as any).keyPromise = new Promise(resolve => { + return document.addEventListener('keydown', event => { + return 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(() => { + return 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(() => { + return 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(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('Hello World!'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/1313 + it('should trigger commands of keyboard shortcuts', async () => { + const {page, server} = getTestState(); + const cmdKey = os.platform() !== 'darwin' ? 'Meta' : 'Control'; + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'hello'); + + await page.keyboard.down(cmdKey); + await page.keyboard.press('a', {commands: ['SelectAll']}); + await page.keyboard.up(cmdKey); + + await page.keyboard.down(cmdKey); + await page.keyboard.down('c', {commands: ['Copy']}); + await page.keyboard.up('c'); + await page.keyboard.up(cmdKey); + + await page.keyboard.down(cmdKey); + await page.keyboard.press('v', {commands: ['Paste']}); + await page.keyboard.up(cmdKey); + await page.keyboard.down(cmdKey); + await page.keyboard.press('v', {commands: ['Paste']}); + await page.keyboard.up(cmdKey); + + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('hellohello'); + }); + 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(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('a'); + + await page.evaluate(() => { + return window.addEventListener( + 'keydown', + e => { + return e.preventDefault(); + }, + true + ); + }); + + await textarea.press('b'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('a'); + }); + it('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(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('ё'); + }); + it('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(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('嗨'); + await page.evaluate(() => { + return window.addEventListener( + 'keydown', + e => { + return e.preventDefault(); + }, + true + ); + }); + await page.keyboard.sendCharacter('a'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('嗨a'); + }); + it('should report shiftKey', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = new Map<KeyInput, number>([ + ['Shift', 16], + ['Alt', 18], + ['Control', 17], + ]); + for (const [modifierKey, modifierCode] of codeForKey) { + await keyboard.down(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + 'Keydown: ' + + modifierKey + + ' ' + + modifierKey + + 'Left ' + + modifierCode + + ' [' + + modifierKey + + ']' + ); + await keyboard.down('!'); + // Shift+! will generate a keypress + if (modifierKey === 'Shift') { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + 'Keydown: ! Digit1 49 [' + + modifierKey + + ']\nKeypress: ! Digit1 33 33 [' + + modifierKey + + ']' + ); + } else { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: ! Digit1 49 [' + modifierKey + ']'); + } + + await keyboard.up('!'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: ! Digit1 49 [' + modifierKey + ']'); + await keyboard.up(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: Control ControlLeft 17 [Control]'); + await keyboard.down('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: Alt AltLeft 18 [Alt Control]'); + await keyboard.down(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: ; Semicolon 186 [Alt Control]'); + await keyboard.up(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: ; Semicolon 186 [Alt Control]'); + await keyboard.up('Control'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: Control ControlLeft 17 [Alt]'); + await keyboard.up('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + [ + 'Keydown: ! Digit1 49 []', + 'Keypress: ! Digit1 33 33 []', + 'Keyup: ! Digit1 49 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).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(() => { + return (globalThis as any).textarea.value; + }) + ).toBe('He Wrd!'); + }); + it('should specify repeat property', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + return document.querySelector('textarea')!.addEventListener( + 'keydown', + e => { + return ((globalThis as any).lastEvent = e); + }, + true + ); + }); + await page.keyboard.down('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + await page.keyboard.press('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(true); + + await page.keyboard.down('b'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + await page.keyboard.down('b'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + }); + it('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); + }); + it('should specify location', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + event => { + return ((globalThis as any).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_ => { + return error_; + }); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('ё').catch(error_ => { + return error_; + }); + expect(error && error.message).toBe('Unknown key: "ё"'); + + // @ts-expect-error bad input + error = await page.keyboard.press('😊').catch(error_ => { + return error_; + }); + expect(error && error.message).toBe('Unknown key: "😊"'); + }); + it('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 => { + return textarea.value; + }) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should type emoji into an iframe', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await 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 => { + return textarea.value; + }) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should press the meta key', async () => { + const {page, isFirefox} = getTestState(); + + await page.evaluate(() => { + (globalThis as any).result = null; + document.addEventListener('keydown', event => { + (globalThis as any).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/remote/test/puppeteer/test/src/launcher.spec.ts b/remote/test/puppeteer/test/src/launcher.spec.ts new file mode 100644 index 0000000000..61249c0e07 --- /dev/null +++ b/remote/test/puppeteer/test/src/launcher.spec.ts @@ -0,0 +1,897 @@ +/** + * 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 {mkdtemp, readFile, writeFile} from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import {TLSSocket} from 'tls'; + +import {Protocol} from 'devtools-protocol'; +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import sinon from 'sinon'; + +import {getTestState, itOnlyRegularInstall} from './mocha-utils.js'; +import {dumpFrames, waitEvent} from './utils.js'; + +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('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_ => { + return error_; + }); + await server.waitForRequest('/one-style.css'); + remote.disconnect(); + const error = await navigationPromise; + expect( + [ + 'Navigation failed because browser has disconnected!', + 'Protocol error (Page.navigate): Target closed.', + ].includes(error.message) + ).toBeTruthy(); + 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_ => { + return 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 => { + return error; + }), + newPage.waitForResponse(server.EMPTY_PAGE).catch(error => { + return 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('can launch and close the browser', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + await browser.close(); + }); + 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!: Error; + const neverResolves = page + .evaluate(() => { + return new Promise(() => {}); + }) + .catch(error_ => { + return (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!: Error; + const options = Object.assign({}, defaultBrowserOptions, { + executablePath: 'random-invalid-path', + }); + await puppeteer.launch(options).catch(error => { + return (waitError = error); + }); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const userDataDir = await mkdtemp(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 + try { + rmSync(userDataDir); + } catch {} + }); + it('tmp profile should be cleaned up', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + // Set a custom test tmp dir so that we can validate that + // the profile dir is created and then cleaned up. + const testTmpDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'puppeteer_test_chrome_profile-') + ); + const oldTmpDir = puppeteer.configuration.temporaryDirectory; + puppeteer.configuration.temporaryDirectory = testTmpDir; + + // Path should be empty before starting the browser. + expect(fs.readdirSync(testTmpDir)).toHaveLength(0); + const browser = await puppeteer.launch(defaultBrowserOptions); + + // One profile folder should have been created at this moment. + const profiles = fs.readdirSync(testTmpDir); + expect(profiles).toHaveLength(1); + expect(profiles[0]?.startsWith('puppeteer_dev_chrome_profile-')).toBe( + true + ); + + // Open a page to make sure its functional. + await browser.newPage(); + await browser.close(); + // Profile should be deleted after closing the browser + expect(fs.readdirSync(testTmpDir)).toHaveLength(0); + + // Restore env var + puppeteer.configuration.temporaryDirectory = oldTmpDir; + }); + it('userDataDir option restores preferences', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + + const prefsJSPath = path.join(userDataDir, 'prefs.js'); + const prefsJSContent = 'user_pref("browser.warnOnQuit", true)'; + await writeFile(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 readFile(prefsJSPath, 'utf8')).toBe(prefsJSContent); + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir argument', async () => { + const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(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 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir argument with non-existent dir', async () => { + const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(TMP_FOLDER); + rmSync(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 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir option should restore state', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(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(() => { + return (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(() => { + return localStorage['hey']; + }) + ).toBe('hello'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir option should restore cookies', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtemp(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(() => { + return (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(() => { + return document.cookie; + }) + ).toBe('doSomethingOnlyOnce=true'); + await browser2.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(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); + try { + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + } finally { + await browser.close(); + } + }); + it('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(); + }); + it('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 => { + return page.url(); + }); + expect(pages).toEqual(['about:blank']); + await browser.close(); + }); + it('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).toHaveLength(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!: Error; + await puppeteer.launch(options).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with timeout = 0', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + timeout: 0, + }); + const browser = await puppeteer.launch(options); + await browser.close(); + }); + 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!: Error; + await puppeteer.launch(options).catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('either pipe or debugging port'); + }); + it('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).toHaveLength(0); + await browser.close(); + }); + }); + + describe('Puppeteer.launch', function () { + 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'); + }); + + it('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(() => { + return 7 * 8; + }) + ).toBe(56); + otherBrowser.disconnect(); + + const secondPage = await originalBrowser.newPage(); + expect( + await secondPage.evaluate(() => { + return 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([ + waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should be able to connect to a browser with no page targets', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const pages = await originalBrowser.pages(); + await Promise.all( + pages.map(page => { + return page.close(); + }) + ); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: originalBrowser.wsEndpoint(), + }); + await Promise.all([ + 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!: Error; + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }), + ]); + expect(error).toBeUndefined(); + expect(response.ok()).toBe(true); + expect(response.securityDetails()).toBeTruthy(); + const protocol = (serverRequest.socket as TLSSocket) + .getProtocol()! + .replace('v', ' '); + expect(response.securityDetails().protocol()).toBe(protocol); + await page.close(); + await browser.close(); + }); + + it('should support targetFilter option in puppeteer.launch', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + targetFilter: target => { + return target.type !== 'page'; + }, + waitForInitialPage: false, + }); + try { + const targets = browser.targets(); + expect(targets).toHaveLength(1); + expect( + targets.find(target => { + return target.type() === 'page'; + }) + ).toBeUndefined(); + } finally { + await browser.close(); + } + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4197 + it('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) => { + return !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) => { + return p.url(); + }) + .sort() + ).toEqual(['about:blank', server.EMPTY_PAGE]); + }); + it('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 => { + return page.url() === server.PREFIX + '/frames/nested-frames.html'; + })!; + expect(dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + expect( + await restoredPage.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + await browser.close(); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 + it('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<Page>(x => { + return browserOne.once('targetcreated', target => { + return x(target.page()); + }); + }), + browserTwo.newPage(), + ]); + expect( + await page1.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + expect( + await page2.evaluate(() => { + return 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 => { + return 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 executable path is configured', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + const {puppeteer} = getTestState(); + sandbox + .stub(puppeteer.configuration, 'executablePath') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('its value is used', async () => { + const {puppeteer} = getTestState(); + try { + puppeteer.executablePath(); + } catch (error) { + expect((error as Error).message).toContain( + 'SOME_CUSTOM_EXECUTABLE' + ); + } + }); + }); + }); + }); + + describe('Browser target events', function () { + it('should work', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch(defaultBrowserOptions); + const events: string[] = []; + browser.on('targetcreated', () => { + return events.push('CREATED'); + }); + browser.on('targetchanged', () => { + return events.push('CHANGED'); + }); + browser.on('targetdestroyed', () => { + return 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', () => { + return ++disconnectedOriginal; + }); + remoteBrowser1.on('disconnected', () => { + return ++disconnectedRemote1; + }); + remoteBrowser2.on('disconnected', () => { + return ++disconnectedRemote2; + }); + + await Promise.all([ + waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + waitEvent(remoteBrowser1, 'disconnected'), + waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/mocha-utils.ts b/remote/test/puppeteer/test/src/mocha-utils.ts new file mode 100644 index 0000000000..70d9ab505e --- /dev/null +++ b/remote/test/puppeteer/test/src/mocha-utils.ts @@ -0,0 +1,330 @@ +/** + * 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 fs from 'fs'; +import path from 'path'; + +import {TestServer} from '@pptr/testserver'; +import {Protocol} from 'devtools-protocol'; +import expect from 'expect'; +import * as Mocha from 'mocha'; +import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js'; +import {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import { + setLogCapture, + getCapturedLogs, +} from 'puppeteer-core/internal/common/Debug.js'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; +import sinon from 'sinon'; + +import {extendExpectWithToBeGolden} from './utils.js'; + +const setupServer = async () => { + const assetsPath = path.join(__dirname, '../assets'); + const cachedPath = path.join(__dirname, '../assets', 'cached'); + + const server = await TestServer.create(assetsPath); + const port = server.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 httpsServer = await TestServer.createHTTPS(assetsPath); + const httpsPort = httpsServer.port; + 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 => { + return state as PuppeteerTestState; +}; + +const product = + process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome'; + +const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false; + +const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase() as + | 'true' + | 'false' + | 'new'; +const isHeadless = headless === 'true' || headless === 'new'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'chrome'; +const protocol = process.env['PUPPETEER_PROTOCOL'] || 'cdp'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}'); +} catch (error) { + if (isErrorLike(error)) { + console.warn( + `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` + ); + } else { + throw error; + } +} + +const defaultBrowserOptions = Object.assign( + { + handleSIGINT: true, + executablePath: process.env['BINARY'], + headless: headless === 'new' ? ('new' as const) : isHeadless, + dumpio: !!process.env['DUMPIO'], + protocol: protocol as 'cdp' | 'webDriverBiDi', + }, + extraLaunchOptions +); + +(async (): Promise<void> => { + if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); + } else { + 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` + ); + } + } +})(); + +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)) { + rmSync(OUTPUT_DIR); + } + extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); +}; + +setupGoldenAssertions(); + +interface PuppeteerTestState { + browser: Browser; + context: BrowserContext; + page: Page; + puppeteer: PuppeteerNode; + defaultBrowserOptions: PuppeteerLaunchOptions; + server: TestServer; + httpsServer: TestServer; + isFirefox: boolean; + isChrome: boolean; + isHeadless: boolean; + headless: 'true' | 'false' | 'new'; + puppeteerPath: string; +} +const state: Partial<PuppeteerTestState> = {}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.AsyncFunc +): Mocha.Test => { + if (alternativeInstall || process.env['BINARY']) { + return it.skip(description, body); + } else { + return it(description, body); + } +}; + +if ( + process.env['MOCHA_WORKER_ID'] === undefined || + process.env['MOCHA_WORKER_ID'] === '0' +) { + console.log( + `Running unit tests with: + -> product: ${product} + -> binary: ${ + defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + } + -> mode: ${ + isHeadless + ? headless === 'new' + ? '--headless=new' + : '--headless' + : 'headful' + }` + ); +} + +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 = undefined; + }); +}; + +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 = undefined; + state.page = undefined; + }); +}; + +export const mochaHooks = { + beforeAll: [ + async (): Promise<void> => { + 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, '..', '..', 'packages', 'puppeteer') + ); + }, + ], + + beforeEach: async (): Promise<void> => { + state.server!.reset(); + state.httpsServer!.reset(); + }, + + afterAll: [ + async (): Promise<void> => { + await state.server!.stop(); + state.server = undefined; + await state.httpsServer!.stop(); + state.httpsServer = undefined; + }, + ], + + afterEach: (): void => { + sinon.restore(); + }, +}; + +export const expectCookieEquals = ( + cookies: Protocol.Network.Cookie[], + expectedCookies: Array<Partial<Protocol.Network.Cookie>> +): 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.length).toBe(expectedCookies.length); + for (let i = 0; i < cookies.length; i++) { + expect(cookies[i]).toMatchObject(expectedCookies[i]!); + } +}; + +/** + * Use it if you want to capture debug logs for a specitic test suite in CI. + * This describe function enables capturing of debug logs and would print them + * only if a test fails to reduce the amount of output. + */ +export const describeWithDebugLogs = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite | void => { + describe(description + '-debug', () => { + beforeEach(() => { + setLogCapture(true); + }); + + afterEach(function () { + if (this.currentTest?.state === 'failed') { + console.log( + `\n"${this.currentTest.fullTitle()}" failed. Here is a debug log:` + ); + console.log(getCapturedLogs().join('\n') + '\n'); + } + setLogCapture(false); + }); + + describe(description, body); + }); +}; + +export const shortWaitForArrayToHaveAtLeastNElements = async ( + data: unknown[], + minLength: number, + attempts = 3, + timeout = 50 +): Promise<void> => { + for (let i = 0; i < attempts; i++) { + if (data.length >= minLength) { + break; + } + await new Promise(resolve => { + return setTimeout(resolve, timeout); + }); + } +}; + +export const createTimeout = <T>( + n: number, + value?: T +): Promise<T | undefined> => { + return new Promise(resolve => { + setTimeout(() => { + return resolve(value); + }, n); + }); +}; diff --git a/remote/test/puppeteer/test/src/mouse.spec.ts b/remote/test/puppeteer/test/src/mouse.spec.ts new file mode 100644 index 0000000000..6d7aec47ee --- /dev/null +++ b/remote/test/puppeteer/test/src/mouse.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 os from 'os'; + +import expect from 'expect'; +import {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.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 as any).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(() => { + return (globalThis as any).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); + 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); + 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(() => { + return new Promise(requestAnimationFrame); + }); + await page.evaluate(() => { + return (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); + }); + it('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(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-6'); + await page.hover('#button-2'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-2'); + await page.hover('#button-91'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-91'); + }); + it('should trigger hover state with removed window.Node', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => { + // @ts-expect-error Expected. + return delete window.Node; + }); + await page.hover('#button-6'); + expect( + await page.evaluate(() => { + return 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(() => { + return document.querySelector('#button-3')!.addEventListener( + 'mousedown', + e => { + return ((globalThis as any).lastEvent = e); + }, + true + ); + }); + const modifiers = new Map<KeyInput, string>([ + ['Shift', 'shiftKey'], + ['Control', 'ctrlKey'], + ['Alt', 'altKey'], + ['Meta', 'metaKey'], + ]); + // In Firefox, the Meta modifier only exists on Mac + if (isFirefox && os.platform() !== 'darwin') { + modifiers.delete('Meta'); + } + for (const [modifier, key] of modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if ( + !(await page.evaluate((mod: string) => { + return (globalThis as any).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) => { + return (globalThis as any).lastEvent[mod]; + }, key) + ) { + throw new Error(modifiers.get(modifier) + ' should be false'); + } + } + }); + it('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, + }); + }); + it('should tween mouse movement', async () => { + const {page} = getTestState(); + + await page.mouse.move(100, 100); + await page.evaluate(() => { + (globalThis as any).result = []; + document.addEventListener('mousemove', event => { + (globalThis as any).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 as any).result = {x: event.clientX, y: event.clientY}; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); + }); + it('should throw if buttons are pressed incorrectly', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.mouse.down(); + await expect(page.mouse.down()).rejects.toBeInstanceOf(Error); + }); + it('should not throw if clicking in parallel', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + interface ClickData { + type: string; + detail: number; + clientX: number; + clientY: number; + isTrusted: boolean; + button: number; + buttons: number; + } + + await page.evaluate(() => { + const clicks: ClickData[] = []; + const mouseEventListener = (event: MouseEvent) => { + clicks.push({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button, + buttons: event.buttons, + }); + }; + document.addEventListener('mousedown', mouseEventListener); + document.addEventListener('mouseup', mouseEventListener); + document.addEventListener('click', mouseEventListener); + (window as unknown as {clicks: ClickData[]}).clicks = clicks; + }); + + await Promise.all([page.mouse.click(0, 5), page.mouse.click(6, 10)]); + + const data = await page.evaluate(() => { + return (window as unknown as {clicks: ClickData[]}).clicks; + }); + const commonAttrs = { + isTrusted: true, + detail: 1, + clientY: 5, + clientX: 0, + button: 0, + }; + expect(data.splice(0, 3)).toMatchObject({ + 0: { + type: 'mousedown', + buttons: 1, + ...commonAttrs, + }, + 1: { + type: 'mouseup', + buttons: 0, + ...commonAttrs, + }, + 2: { + type: 'click', + buttons: 0, + ...commonAttrs, + }, + }); + Object.assign(commonAttrs, { + clientX: 6, + clientY: 10, + }); + expect(data).toMatchObject({ + 0: { + type: 'mousedown', + buttons: 1, + ...commonAttrs, + }, + 1: { + type: 'mouseup', + buttons: 0, + ...commonAttrs, + }, + 2: { + type: 'click', + buttons: 0, + ...commonAttrs, + }, + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/navigation.spec.ts b/remote/test/puppeteer/test/src/navigation.spec.ts new file mode 100644 index 0000000000..0f2c17e36f --- /dev/null +++ b/remote/test/puppeteer/test/src/navigation.spec.ts @@ -0,0 +1,850 @@ +/** + * 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 {ServerResponse} from 'http'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +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); + }); + it('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!: Error; + await page + .goto(server.PREFIX + '/frames/one-frame.html') + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('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!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (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', + () => { + return history.replaceState(null, 'initial', window.location.href); + }, + false + ); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response!.status()).toBe(200); + }); + it('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); + }); + it('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); + }); + it('should fail when navigating to bad url', async () => { + const {page, isChrome} = getTestState(); + + let error!: Error; + await page.goto('asdfasdf').catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toContain('Cannot navigate to invalid URL'); + } else { + expect(error.message).toContain('Invalid url'); + } + }); + + const EXPECTED_SSL_CERT_MESSAGE_REGEX = + /net::ERR_CERT_INVALID|net::ERR_CERT_AUTHORITY_INVALID/; + + it('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: string[] = []; + page.on('request', () => { + return requests.push('request'); + }); + page.on('requestfinished', () => { + return requests.push('requestfinished'); + }); + page.on('requestfailed', () => { + return requests.push('requestfailed'); + }); + + let error!: Error; + await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX); + } else { + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + } + + expect(requests).toHaveLength(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!: Error; + await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX); + } else { + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + } + }); + it('should fail when main resources failed to load', async () => { + const {page, isChrome} = getTestState(); + + let error!: Error; + await page + .goto('http://localhost:44123/non-existing-url') + .catch(error_ => { + return (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} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + await page + .goto(server.PREFIX + '/empty.html', {timeout: 1}) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const {page, server} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const {page, server} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const {page, server} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should disable timeout when its set to 0', async () => { + const {page, server} = getTestState(); + + let error!: Error; + let loaded = false; + page.once('load', () => { + return (loaded = true); + }); + await page + .goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + 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); + }); + it('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 not throw an error for a 404 response with an empty body', async () => { + const {page, server} = getTestState(); + + server.setRoute('/404-error', (_, res) => { + res.statusCode = 404; + res.end(); + }); + + const response = (await page.goto(server.PREFIX + '/404-error'))!; + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should not throw an error for a 500 response with an empty body', async () => { + const {page, server} = getTestState(); + + server.setRoute('/500-error', (_, res) => { + res.statusCode = 500; + res.end(); + }); + + const response = (await page.goto(server.PREFIX + '/500-error'))!; + expect(response.ok()).toBe(false); + expect(response.status()).toBe(500); + }); + 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); + }); + it('should wait for network idle to succeed navigation', async () => { + const {page, server} = getTestState(); + + let responses: ServerResponse[] = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-b.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-c.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-d.js', (_req, res) => { + return 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'), + ]).catch(() => { + // Ignore Error that arise from test server during hooks + }); + const secondFetchResourceRequested = server + .waitForRequest('/fetch-request-d.js') + .catch(() => { + // Ignore Error that arise from test server during hooks + }); + + // 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(() => { + return (navigationFinished = true); + }); + + // Wait for the page's 'load' event. + await waitEvent(page, 'load'); + 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 function () { + this.timeout(25_000); + + const {page, server} = getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (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 function () { + this.timeout(25_000); + + const {page} = getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (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 function () { + this.timeout(25_000); + + const {context, server} = getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (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); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const {page} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = (await page.goto(dataURL))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !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).toHaveLength(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!: Error; + try { + await page.goto(url); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain(url); + }); + it('should send referer', async () => { + const {page, server} = getTestState(); + + const requests = Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]).catch(() => { + return []; + }); + + const [request1, request2] = await requests; + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + + it('should send referer policy', 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', { + referrerPolicy: 'no-referer', + }), + ]).catch(() => { + return []; + }); + expect(request1.headers['referer']).toBeUndefined(); + 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) => { + return (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!: ServerResponse; + server.setRoute('/one-style.css', (_req, res) => { + return (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(() => { + return (bothFired = true); + }); + + await server.waitForRequest('/one-style.css').catch(() => {}); + 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(`<a href='#foobar'>foobar</a>`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + it('should work with history.pushState()', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:pushState()'>SPA</a> + <script> + function pushState() { history.pushState({}, '', 'wow.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + it('should work with history.replaceState()', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:replaceState()'>SPA</a> + <script> + function replaceState() { history.replaceState({}, '', '/replaced.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + it('should work with DOM history.back()/history.forward()', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a id=back onclick='javascript:goBack()'>back</a> + <a id=forward onclick='javascript:goForward()'>forward</a> + <script> + function goBack() { history.back(); } + function goForward() { history.forward(); } + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + </script> + `); + 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'); + }); + it('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 waitEvent(page, 'frameattached'); + await waitEvent(page, 'framenavigated', f => { + return f === frame; + }); + await Promise.all([ + frame.evaluate(() => { + return 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); + }); + it('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'); + }); + }); + + describe('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_ => { + return error_; + }); + await server.waitForRequest('/empty.html').catch(() => {}); + + await page.$eval('iframe', frame => { + return 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, the browser will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + attachFrame(page, 'frame1', server.EMPTY_PAGE), + attachFrame(page, 'frame2', server.EMPTY_PAGE), + attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses: ServerResponse[] = []; + server.setRoute('/one-style.html', (_req, res) => { + return 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]); + } + }); + }); + + describe('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) => { + return (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!: Error; + const navigationPromise = frame.waitForNavigation().catch(error_ => { + return (error = error_); + }); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => { + return ((window as any).location = '/empty.html'); + }), + ]); + await page.$eval('iframe', frame => { + return 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(() => { + return ((globalThis as any)._foo = 10); + }); + await page.reload(); + expect( + await page.evaluate(() => { + return (globalThis as any)._foo; + }) + ).toBe(undefined); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/network.spec.ts b/remote/test/puppeteer/test/src/network.spec.ts new file mode 100644 index 0000000000..f3899a5ff7 --- /dev/null +++ b/remote/test/puppeteer/test/src/network.spec.ts @@ -0,0 +1,863 @@ +/** + * 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 {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('network', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + }); + it('should fire for iframes', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests).toHaveLength(2); + }); + it('should fire for fetches', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return fetch('/empty.html'); + }); + expect(requests).toHaveLength(2); + }); + }); + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.frame()).toBe(page.mainFrame()); + }); + it('should work for subframe navigation request', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests).toHaveLength(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: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.evaluate(() => { + return fetch('/digits/1.png'); + }); + requests = requests.filter(request => { + return !request.url().includes('favicon'); + }); + expect(requests).toHaveLength(1); + expect(requests[0]!.frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + it('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'); + }); + + it('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'); + }); + }); + + describe('Request.initiator', () => { + it('should return the initiator', async () => { + const {page, server} = getTestState(); + + const initiators = new Map(); + page.on('request', request => { + return 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' + ); + }); + }); + + describe('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 => { + return ( + !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); + }); + }); + + describe('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 => { + return !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 () => { + return await (globalThis as any).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); + }); + }); + + describe('Request.postData', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (_req, res) => { + return res.end(); + }); + + const [request] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'request', r => { + return !isFavicon(r); + }), + page.evaluate(() => { + return 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); + }); + }); + + describe('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).toHaveLength(1); + const redirected = redirectChain[0]!.response()!; + expect(redirected.status()).toBe(302); + let error!: Error; + await redirected.text().catch(error_ => { + return (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!: ServerResponse; + 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 => { + return (requestFinished = requestFinished || r.url().includes('/get')); + }); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse(r => { + return !isFavicon(r.request()); + }), + page.evaluate(() => { + return 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 => { + return serverResponse.write('wor', x); + }); + // Finish response. + await new Promise<void>(x => { + serverResponse.end('ld!', () => { + return x(); + }); + }); + expect(await responseText).toBe('hello world!'); + }); + }); + + describe('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'}); + }); + }); + + describe('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 = waitEvent<HTTPResponse>( + page, + 'response', + response => { + // Get the preflight response. + return ( + response.request().method() === 'OPTIONS' && response.url() === url + ); + } + ); + + // Trigger a request with a preflight. + await page.evaluate(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(''); + }); + }); + + describe('Response.timing', function () { + it('returns timing information', async () => { + const {page, server} = getTestState(); + const responses: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + await page.goto(server.EMPTY_PAGE); + expect(responses).toHaveLength(1); + expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0); + }); + }); + + describe('Network Events', function () { + it('Page.Events.Request', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(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: string[] = []; + page.on('requestservedfromcache', r => { + return 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: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + await page.goto(server.EMPTY_PAGE); + expect(responses).toHaveLength(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: HTTPRequest[] = []; + page.on('requestfailed', request => { + return failedRequests.push(request); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests).toHaveLength(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: HTTPRequest[] = []; + page.on('requestfinished', request => { + return requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(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: string[] = []; + page.on('request', () => { + return events.push('request'); + }); + page.on('response', () => { + return events.push('response'); + }); + page.on('requestfinished', () => { + return 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: string[] = []; + page.on('request', request => { + return events.push(`${request.method()} ${request.url()}`); + }); + page.on('response', response => { + return events.push(`${response.status()} ${response.url()}`); + }); + page.on('requestfinished', request => { + return events.push(`DONE ${request.url()}`); + }); + page.on('requestfailed', request => { + return 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).toHaveLength(1); + expect(redirectChain[0]!.url()).toContain('/foo.html'); + expect(redirectChain[0]!.response()!.remoteAddress().port).toBe( + server.PORT + ); + }); + }); + + describe('Request.isNavigationRequest', () => { + it('should work', async () => { + const {page, server} = getTestState(); + + const requests = new Map(); + page.on('request', request => { + return 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); + }); + it('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); + }); + it('should work when navigating to image', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return requests.push(request); + }); + await page.goto(server.PREFIX + '/pptr.png'); + expect(requests[0]!.isNavigationRequest()).toBe(true); + }); + }); + + describe('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!: Error; + try { + // @ts-expect-error purposeful bad input + await page.setExtraHTTPHeaders({foo: 1}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Expected value of header "foo" to be String, but "number" is found.' + ); + }); + }); + + describe('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 as 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({ + username: '', + password: '', + }); + // 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 as 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 => { + return 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); + }); + }); + + describe('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 = waitEvent<HTTPResponse>(page, '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 url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; + const [response] = await Promise.all([ + waitEvent<HTTPResponse>(page, 'response', response => { + return response.url() === url; + }), + page.evaluate(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/remote/test/puppeteer/test/src/oopif.spec.ts b/remote/test/puppeteer/test/src/oopif.spec.ts new file mode 100644 index 0000000000..744b31082f --- /dev/null +++ b/remote/test/puppeteer/test/src/oopif.spec.ts @@ -0,0 +1,454 @@ +/** + * 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 {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {describeWithDebugLogs, getTestState} from './mocha-utils.js'; +import {attachFrame, detachFrame, navigateFrame} from './utils.js'; + +describeWithDebugLogs('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser: Browser; + let context: BrowserContext; + let page: 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(); + }); + + after(async () => { + await browser.close(); + }); + + 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 => { + return frame.url().endsWith('/empty.html'); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await 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 attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await 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 attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await 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 attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html' + ); + + const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]); + + expect( + await frame1.evaluate(() => { + return document.location.href; + }) + ).toMatch(/one-frame\.html$/); + expect( + await frame2.evaluate(() => { + return 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 attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await 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 attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + const nav = frame.waitForNavigation(); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await nav; + expect(frame.isOOPFrame()).toBe(true); + await 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 attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await 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 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, isHeadless, headless} = getTestState(); + + if (!isHeadless || headless === 'new') { + // TODO: this test is partially blocked on crbug.com/1334119. Enable test once + // the upstream is fixed. + // TLDR: when we dispatch events to the frame the compositor might + // not be up-to-date yet resulting in a misclick (the iframe element + // becomes the event target instead of the content inside the iframe). + // The solution is to use InsertVisualCallback on the backend but that causes + // another issue that events cannot be dispatched to inactive tabs as the + // visual callback is never invoked. + // The old headless mode does not have this issue since it operates with + // special scheduling settings that keep even inactive tabs updating. + return; + } + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await 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 => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(1); + expect(page.frames()).toHaveLength(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 => { + return frame.url().endsWith('inner-frame2.html'); + }); + expect(oopifs(context)).toHaveLength(2); + expect( + page.frames().filter(frame => { + return frame.isOOPFrame(); + }) + ).toHaveLength(2); + expect( + await frame2.evaluate(() => { + return 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 => { + return frame.url().endsWith('/oopif.html'); + }); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(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 attachFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame1 = oopIframe.childFrames()[0]!; + expect(frame1.url()).toMatch(/empty.html$/); + await 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 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 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 => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(1); + expect(page.frames()).toHaveLength(2); + + const browserURL = 'http://127.0.0.1:21222'; + const browser1 = await puppeteer.connect({browserURL}); + const target = await browser1.waitForTarget(target => { + return target.url().endsWith('dynamic-oopif.html'); + }); + await target.page(); + browser1.disconnect(); + }); + + it('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 => { + return 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 attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + await page.waitForFrame(frame => { + return frame.url().endsWith('/empty.html'); + }); + }); + }); +}); + +function oopifs(context: BrowserContext) { + return context.targets().filter(target => { + return target._getTargetInfo().type === 'iframe'; + }); +} diff --git a/remote/test/puppeteer/test/src/page.spec.ts b/remote/test/puppeteer/test/src/page.spec.ts new file mode 100644 index 0000000000..6bf28e10b1 --- /dev/null +++ b/remote/test/puppeteer/test/src/page.spec.ts @@ -0,0 +1,2330 @@ +/** + * 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 {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import {KnownDevices, TimeoutError} from 'puppeteer'; +import {Metrics, Page} from 'puppeteer-core/internal/api/Page.js'; +import {CDPSession} from 'puppeteer-core/internal/common/Connection.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; +import {CDPPage} from 'puppeteer-core/internal/common/Page.js'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame, waitEvent} from './utils.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!: Error; + await Promise.all([ + newPage + .evaluate(() => { + return new Promise(() => {}); + }) + .catch(error_ => { + return (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); + }); + it('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; + }); + it('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); + }); + it('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 => { + return error; + }), + newPage.waitForResponse(server.EMPTY_PAGE).catch(error => { + return 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'), 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: {url: () => string}) => { + // 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: {url: () => string}) => { + // 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); + }); + }); + + describe('Page.Events.error', function () { + it('should throw when page crashes', async () => { + const {page, isChrome} = getTestState(); + + let navigate: Promise<unknown>; + if (isChrome) { + navigate = page.goto('chrome://crash').catch(() => {}); + } else { + navigate = page.goto('about:crashcontent').catch(() => {}); + } + const [error] = await Promise.all([ + waitEvent<Error>(page, 'error'), + navigate, + ]); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe('Page.Events.Popup', function () { + it('should work', async () => { + const {page} = getTestState(); + + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.evaluate(() => { + return window.open('about:blank'); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(true); + }); + it('should work with noopener', async () => { + const {page} = getTestState(); + + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.evaluate(() => { + return window.open('about:blank', undefined, 'noopener'); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!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('<a target=_blank href="/one-style.html">yo</a>'); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!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( + '<a target=_blank rel=opener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!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( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.$eval('a', a => { + return (a as HTMLAnchorElement).click(); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!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( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + }); + + describe('BrowserContext.overridePermissions', function () { + function getPermission(page: Page, name: PermissionName) { + return page.evaluate(name => { + return navigator.permissions.query({name}).then(result => { + return 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'); + }); + it('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!: Error; + await context + // @ts-expect-error purposeful bad input for test + .overridePermissions(server.EMPTY_PAGE, ['foo']) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unknown permission: foo'); + }); + it('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'); + }); + it('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'); + }); + it('should trigger permission onchange', async () => { + const {page, server, context} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + (globalThis as any).events = []; + return navigator.permissions + .query({name: 'geolocation'}) + .then(function (result) { + (globalThis as any).events.push(result.state); + result.onchange = function () { + (globalThis as any).events.push(result.state); + }; + }); + }); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied']); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied', 'granted']); + await context.clearPermissionOverrides(); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied', 'granted', 'prompt']); + }); + it('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(); + }); + it('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 () { + it('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(() => { + return new Promise(resolve => { + return 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!: Error; + try { + await page.setGeolocation({longitude: 200, latitude: 10}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describe('Page.setOfflineMode', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.setOfflineMode(true); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (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(() => { + return window.navigator.onLine; + }) + ).toBe(true); + await page.setOfflineMode(true); + expect( + await page.evaluate(() => { + return window.navigator.onLine; + }) + ).toBe(false); + await page.setOfflineMode(false); + expect( + await page.evaluate(() => { + return window.navigator.onLine; + }) + ).toBe(true); + }); + }); + + describe('ExecutionContext.queryObjects', function () { + it('should work', async () => { + const {page} = getTestState(); + + // Create a custom class + const classHandle = await page.evaluateHandle(() => { + return class CustomClass {}; + }); + + // Create an instance. + await page.evaluate(CustomClass => { + // @ts-expect-error: Different context. + self.customClass = new CustomClass(); + }, classHandle); + + // Validate only one has been added. + const prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + const objectsHandle = await page.queryObjects(prototypeHandle); + await expect( + page.evaluate(objects => { + return objects.length; + }, objectsHandle) + ).resolves.toBe(1); + + // Check that instances. + await expect( + page.evaluate(objects => { + // @ts-expect-error: Different context. + return objects[0] === self.customClass; + }, objectsHandle) + ).resolves.toBeTruthy(); + }); + it('should work for non-trivial page', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + + // Create a custom class + const classHandle = await page.evaluateHandle(() => { + return class CustomClass {}; + }); + + // Create an instance. + await page.evaluate(CustomClass => { + // @ts-expect-error: Different context. + self.customClass = new CustomClass(); + }, classHandle); + + // Validate only one has been added. + const prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + const objectsHandle = await page.queryObjects(prototypeHandle); + await expect( + page.evaluate(objects => { + return objects.length; + }, objectsHandle) + ).resolves.toBe(1); + + // Check that instances. + await expect( + page.evaluate(objects => { + // @ts-expect-error: Different context. + return objects[0] === self.customClass; + }, objectsHandle) + ).resolves.toBeTruthy(); + }); + it('should fail for disposed handles', async () => { + const {page} = getTestState(); + + const prototypeHandle = await page.evaluateHandle(() => { + return HTMLBodyElement.prototype; + }); + await prototypeHandle.dispose(); + let error!: Error; + await page.queryObjects(prototypeHandle).catch(error_ => { + return (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(() => { + return 42; + }); + let error!: Error; + await page.queryObjects(prototypeHandle).catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'Prototype JSHandle must not be referencing primitive value' + ); + }); + }); + + describe('Page.Events.Console', function () { + it('should work', async () => { + const {page} = getTestState(); + + const [message] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.evaluate(() => { + return console.log('hello', 5, {foo: 'bar'}); + }), + ]); + 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 with logging functions', async () => { + const {page} = getTestState(); + + const messages: any[] = []; + page.on('console', msg => { + return messages.push(msg); + }); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + 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 => { + return msg.type(); + }) + ).toEqual(['trace', 'dir', 'warning', 'error', 'log']); + expect( + messages.map(msg => { + return msg.text(); + }) + ).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should work for different console API calls with timing functions', async () => { + const {page} = getTestState(); + + const messages: any[] = []; + page.on('console', msg => { + return 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'); + }); + expect( + messages.map(msg => { + return msg.type(); + }) + ).toEqual(['timeEnd']); + expect(messages[0]!.text()).toContain('calling console.time'); + }); + it('should not fail for window object', async () => { + const {page} = getTestState(); + + const [message] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.evaluate(() => { + return console.error(window); + }), + ]); + 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) => { + return 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(`<script>fetch('http://wat');</script>`), + ]); + 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 => { + return (win.onload = x); + }); + // 2. In this popup, create an iframe that console.logs a message. + win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`; + const frame = win.document.querySelector('iframe')!; + await new Promise(x => { + return (frame.onload = x); + }); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page + .browserContext() + .targets() + .find(target => { + return 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'); + }); + }); + + describe('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 = waitEvent<{metrics: Metrics; title: string}>( + page, + 'metrics' + ); + + await page.evaluate(() => { + return console.timeStamp('test42'); + }); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(metrics: 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 as keyof Metrics]).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(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void 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 => { + return request.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(request.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 [request] = await Promise.all([ + page.waitForRequest(async request => { + return request.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .waitForRequest( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(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(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void 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(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .waitForResponse( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(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 => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void 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(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void 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(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void 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(() => { + return (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise(resolve => { + return setTimeout(resolve, 200); + }); + await fetch('/digits/3.png'); + await new Promise(resolve => { + return setTimeout(resolve, 200); + }); + await fetch('/digits/4.png'); + })(); + }) + .then(() => { + return Date.now(); + }), + ]); + expect(res).toBe(undefined); + expect(t1).toBeGreaterThan(t2); + expect(t1 - t2).toBeGreaterThanOrEqual(400); + }); + it('should respect timeout', async () => { + const {page} = getTestState(); + let error!: Error; + await page.waitForNetworkIdle({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(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(() => { + return Date.now(); + }), + page + .evaluate(() => { + return (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise(resolve => { + return setTimeout(resolve, 250); + }); + })(); + }) + .then(() => { + return 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(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(result).toBe(undefined); + }); + it('should work with aborted requests', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/abort-request.html'); + + const element = await page.$(`#abort`); + await element!.click(); + + let error = false; + await page.waitForNetworkIdle().catch(() => { + return (error = true); + }); + + expect(error).toBe(false); + }); + }); + + describe('Page.exposeFunction', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const {page} = getTestState(); + + await page.exposeFunction('woof', () => { + throw new Error('WOOF WOOF'); + }); + const {message, stack} = await page.evaluate(async () => { + try { + return await (globalThis as any).woof(); + } catch (error) { + return { + message: (error as Error).message, + stack: (error as Error).stack, + }; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain('page.spec.ts'); + }); + 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 as any).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(() => { + return (globalThis as any).woof(); + }); + await page.reload(); + expect(called).toBe(true); + }); + it('should survive navigation', async () => { + const {page, server} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const {page} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const {page, server} = getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + 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 as any).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: number, b: number) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]!; + const result = await frame.evaluate(async function () { + return await (globalThis as any).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 attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + await detachFrame(page, 'frame1'); + + await expect( + page.evaluate(async function () { + return await (globalThis as any).compute(3, 5); + }) + ).resolves.toEqual(15); + }); + it('should work with complex objects', async () => { + const {page} = getTestState(); + + await page.exposeFunction( + 'complexObject', + function (a: {x: any}, b: {x: any}) { + return {x: a.x + b.x}; + } + ); + const result = await page.evaluate(async () => { + return (globalThis as any).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: number, b: number) { + return a * b; + }, + }; + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('compute', moduleObject); + const result = await page.evaluate(async function () { + return await (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + }); + + describe('Page.Events.PageError', function () { + it('should fire', async () => { + const {page, server} = getTestState(); + + const [error] = await Promise.all([ + waitEvent<Error>(page, 'pageerror'), + page.goto(server.PREFIX + '/error.html'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + expect( + await page.evaluate(() => { + return 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(() => { + return navigator.userAgent; + }) + ).toContain('Mozilla'); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).not.toContain('iPhone'); + await page.setUserAgent(KnownDevices['iPhone 6'].userAgent); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('iPhone'); + }); + it('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 = + '<html><head></head><body><div>hello</div></body></html>'; + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent('<div>hello</div>'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async () => { + const {page} = getTestState(); + + const doctype = '<!DOCTYPE html>'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async () => { + const {page} = getTestState(); + + const doctype = + '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ' + + '"http://www.w3.org/TR/html4/strict.dtd">'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should respect timeout', async () => { + const {page, server} = getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, { + timeout: 1, + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const {page, server} = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should await resources to load', async () => { + const {page, server} = getTestState(); + + const imgPath = '/img.png'; + let imgResponse!: ServerResponse; + server.setRoute(imgPath, (_req, res) => { + return (imgResponse = res); + }); + let loaded = false; + const contentPromise = page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .then(() => { + return (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('<div>yo</div>'); + } + }); + it('should work with tricky content', async () => { + const {page} = getTestState(); + + await page.setContent('<div>hello world</div>' + '\x7F'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('hello world'); + }); + it('should work with accents', async () => { + const {page} = getTestState(); + + await page.setContent('<div>aberración</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('aberración'); + }); + it('should work with emojis', async () => { + const {page} = getTestState(); + + await page.setContent('<div>🐥</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('🐥'); + }); + it('should work with newline', async () => { + const {page} = getTestState(); + + await page.setContent('<div>\n</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('\n'); + }); + }); + + describe('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 => { + return void error; + }); + expect( + await page.evaluate(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).__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 => { + return void error; + }); + expect( + await page.evaluate(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect( + await page.evaluate(() => { + return (globalThis as any).__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 attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ))!; + await frame + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await frame.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(undefined); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = (await attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ))!; + await frame + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await frame.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function () { + it('should throw an error if no options are provided', async () => { + const {page} = getTestState(); + + let error!: Error; + try { + // @ts-expect-error purposefully passing bad options + await page.addScriptTag('/injectedfile.js'); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + }); + + 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(() => { + return (globalThis as any).__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(() => { + return (window as unknown as {__es6injected: number}).__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(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }); + expect( + await page.evaluate(() => { + return (window as unknown as {__es6injected: number}).__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(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }); + expect( + await page.evaluate(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }) + ).toBe(42); + }); + + it('should throw an error if loading from url fail', async () => { + const {page, server, isFirefox} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error!: Error; + try { + await page.addScriptTag({url: '/nonexistfile.js'}); + } catch (error_) { + error = error_ as Error; + } + if (isFirefox) { + expect(error.message).toBeTruthy(); + } else { + expect(error.message).toContain('Could not load script'); + } + }); + + 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(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).__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(() => { + return (globalThis as any).__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 + it('should throw when added with content to the CSP page', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addScriptTag({content: 'window.__injected = 35;'}) + .catch(error_ => { + return (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!: Error; + await page + .addScriptTag({url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js'}) + .catch(error_ => { + return (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!: Error; + try { + // @ts-expect-error purposefully passing bad input + await page.addStyleTag('/injectedstyle.css'); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + }); + + 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, isFirefox} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error!: Error; + try { + await page.addStyleTag({url: '/nonexistfile.js'}); + } catch (error_) { + error = error_ as Error; + } + if (isFirefox) { + expect(error.message).toBeTruthy(); + } else { + expect(error.message).toContain('Could not load style'); + } + }); + + 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) => { + return 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)'); + }); + + it('should throw when added with content to the CSP page', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addStyleTag({content: 'body { background-color: green; }'}) + .catch(error_ => { + return (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!: Error; + await page + .addStyleTag({ + url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css', + }) + .catch(error_ => { + return (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); + }); + }); + + describe('Page.setJavaScriptEnabled', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + let error!: Error; + await page.evaluate('something').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('something is not defined'); + + await page.setJavaScriptEnabled(true); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + 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); + }); + it('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('Page.pdf', function () { + it('can print to PDF and save to file', async () => { + const {page, server} = getTestState(); + + const outputFile = __dirname + '/../assets/output.pdf'; + await page.goto(server.PREFIX + '/pdf.html'); + 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 () => { + const {page} = getTestState(); + + 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 {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/pdf.html'); + + let error!: Error; + await page.pdf({timeout: 1}).catch(_error => { + return (error = _error); + }); + expect(error).toBeInstanceOf(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(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).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 => { + return select.addEventListener('input', () => { + return ((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(() => { + return (globalThis as any).makeMultiple(); + }); + await page.select('select', 'blue', 'green', 'red'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue', 'green', 'red']); + expect( + await page.evaluate(() => { + return (globalThis as any).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(() => { + return (globalThis as any).result.onBubblingInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onBubblingChange; + }) + ).toEqual(['blue']); + }); + it('should throw when element is not a <select>', async () => { + const {page, server} = getTestState(); + + let error!: Error; + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('body', '').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Element is not a <select> 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(() => { + return (globalThis as any).makeMultiple(); + }); + const result = await page.select('select', 'blue', 'black', 'magenta'); + expect( + result.reduce((accumulator, current) => { + return ['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).toHaveLength(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(() => { + return (globalThis as any).makeMultiple(); + }); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', select => { + return Array.from((select as HTMLSelectElement).options).every( + option => { + return !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 => { + return Array.from((select as HTMLSelectElement).options).filter( + option => { + return option.selected; + } + )[0]!.value; + }) + ).toEqual(''); + }); + it('should throw if passed in non-strings', async () => { + const {page} = getTestState(); + + await page.setContent('<select><option value="12"/></select>'); + let error!: Error; + try { + // @ts-expect-error purposefully passing bad input + await page.select('select', 12); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3327 + it('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(() => { + // @ts-expect-error Expected. + return (window.Event = undefined); + }); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onChange; + }) + ).toEqual(['blue']); + }); + }); + + describe('Page.Events.Close', function () { + it('should work with window.close', async () => { + const {page, context} = getTestState(); + + const newPagePromise = new Promise<Page>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target.page()); + }); + }); + await page.evaluate(() => { + return ((window as any)['newPage'] = window.open('about:blank')); + }); + const newPage = await newPagePromise; + const closedPromise = waitEvent(newPage, 'close'); + await page.evaluate(() => { + return (window as any)['newPage'].close(); + }); + await closedPromise; + }); + it('should work with page.close', async () => { + const {context} = getTestState(); + + const newPage = await context.newPage(); + const closedPromise = waitEvent(newPage, 'close'); + 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 as CDPPage)._client()).toBeInstanceOf(CDPSession); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/proxy.spec.ts b/remote/test/puppeteer/test/src/proxy.spec.ts new file mode 100644 index 0000000000..de0e9f543a --- /dev/null +++ b/remote/test/puppeteer/test/src/proxy.spec.ts @@ -0,0 +1,236 @@ +/** + * 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 http from 'http'; +import type {Server, IncomingMessage, ServerResponse} from 'http'; +import type {AddressInfo} from 'net'; +import os from 'os'; + +import {TestServer} from '@pptr/testserver'; +import expect from 'expect'; +import type {Browser} from 'puppeteer-core/internal/api/Browser.js'; + +import {getTestState} from './mocha-utils.js'; + +let HOSTNAME = os.hostname(); + +// Hostname might not be always accessible in environments other than GitHub +// Actions. Therefore, we try to find an external IPv4 address to be used as a +// hostname in these tests. +const networkInterfaces = os.networkInterfaces(); +for (const key of Object.keys(networkInterfaces)) { + const interfaces = networkInterfaces[key]; + for (const net of interfaces || []) { + if (net.family === 'IPv4' && !net.internal) { + HOSTNAME = net.address; + break; + } + } +} + +/** + * Requests to localhost do not get proxied by default. Create a URL using the hostname + * instead. + */ +function getEmptyPageUrl(server: TestServer): string { + const emptyPagePath = new URL(server.EMPTY_PAGE).pathname; + + return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`; +} + +describe('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. + */ + it('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/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts new file mode 100644 index 0000000000..195a9e7e6f --- /dev/null +++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts @@ -0,0 +1,594 @@ +/** + * 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 assert from 'assert'; + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer-core'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Query handler tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Pierce selectors', function () { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + `<script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const div1 = document.createElement('div'); + div1.textContent = 'Hello'; + div1.className = 'foo'; + const div2 = document.createElement('div'); + div2.textContent = 'World'; + div2.className = 'foo'; + shadowRoot.appendChild(div1); + shadowRoot.appendChild(div2); + document.documentElement.appendChild(div); + </script>` + ); + }); + it('should find first element in shadow', async () => { + const {page} = getTestState(); + const div = (await page.$('pierce/.foo')) as ElementHandle<HTMLElement>; + const text = await div.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const {page} = getTestState(); + const divs = (await page.$$('pierce/.foo')) as Array< + ElementHandle<HTMLElement> + >; + const text = await Promise.all( + divs.map(div => { + return div.evaluate(element => { + return 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' + )) as ElementHandle<HTMLElement>; + const text = await childElement.evaluate(element => { + return 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')) as Array< + ElementHandle<HTMLElement> + >; + const text = await Promise.all( + childElements.map(div => { + return div.evaluate(element => { + return element.textContent; + }); + }) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + }); + + describe('Text selectors', function () { + describe('in Page', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<section>test</section>'); + + expect(await page.$('text/test')).toBeTruthy(); + expect(await page.$$('text/test')).toHaveLength(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = getTestState(); + + expect(await page.$('text/test')).toBeFalsy(); + expect(await page.$$('text/test')).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = getTestState(); + + await page.setContent('<div id="1">a</div><div>a</div>'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.id; + }) + ).toBe('1'); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div>a</div><div>a</div>'); + + const elements = await page.$$('text/a'); + expect(elements).toHaveLength(2); + }); + it('should pierce shadow DOM', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + const div = document.createElement('div'); + const shadow = div.attachShadow({mode: 'open'}); + const diva = document.createElement('div'); + shadow.append(diva); + const divb = document.createElement('div'); + shadow.append(divb); + diva.innerHTML = 'a'; + divb.innerHTML = 'b'; + document.body.append(div); + }); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query deeply nested text', async () => { + const {page} = getTestState(); + + await page.setContent('<div><div>a</div><div>b</div></div>'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query inputs', async () => { + const {page} = getTestState(); + + await page.setContent('<input value="a">'); + + const element = (await page.$( + 'text/a' + )) as ElementHandle<HTMLInputElement>; + expect( + await element?.evaluate(e => { + return e.value; + }) + ).toBe('a'); + }); + it('should not query radio', async () => { + const {page} = getTestState(); + + await page.setContent('<radio value="a">'); + + expect(await page.$('text/a')).toBeNull(); + }); + it('should query text spanning multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div><span>a</span> <span>b</span><div>'); + + const element = await page.$('text/a b'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a b'); + }); + it('should clear caches', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>' + ); + const div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>; + const input = (await page.$( + '#target2' + )) as ElementHandle<HTMLInputElement>; + + await div.evaluate(div => { + div.textContent = 'text'; + }); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target1'); + await div.evaluate(div => { + div.textContent = 'foo'; + }); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target2'); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('foo'); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target3'); + + await div.evaluate(div => { + div.textContent = 'text'; + }); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('text'); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(3); + await div.evaluate(div => { + div.textContent = 'foo'; + }); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(2); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('foo'); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(1); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<div class="a"><span>a</span></div>'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`text/a`)).toBeTruthy(); + expect(await elementHandle.$$(`text/a`)).toHaveLength(1); + }); + + it('should return null for non-existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<div class="a"></div>'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`text/a`)).toBeFalsy(); + expect(await elementHandle.$$(`text/a`)).toHaveLength(0); + }); + }); + }); + + describe('XPath selectors', function () { + describe('in Page', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<section>test</section>'); + + expect(await page.$('xpath/html/body/section')).toBeTruthy(); + expect(await page.$$('xpath/html/body/section')).toHaveLength(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = getTestState(); + + expect( + await page.$('xpath/html/body/non-existing-element') + ).toBeFalsy(); + expect( + await page.$$('xpath/html/body/non-existing-element') + ).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = getTestState(); + + await page.setContent('<div>a</div><div></div>'); + + const element = await page.$('xpath/html/body/div'); + expect( + await element?.evaluate(e => { + return e.textContent === 'a'; + }) + ).toBeTruthy(); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div></div><div></div>'); + + const elements = await page.$$('xpath/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<div class="a">a<span></span></div>'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeTruthy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(1); + }); + + it('should return null for non-existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<div class="a">a</div>'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeFalsy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0); + }); + }); + }); + + describe('P selectors', () => { + beforeEach(async () => { + const {page, server} = getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + Puppeteer.clearCustomQueryHandlers(); + }); + + it('should work with CSS selectors', async () => { + const {page} = getTestState(); + const element = await page.$('div > button'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + + // Should parse more complex CSS selectors. Listing a few problematic + // cases from bug reports. + for (const selector of [ + '.user_row[data-user-id="\\38 "]:not(.deactivated_user)', + `input[value='Search']:not([class='hidden'])`, + `[data-test-id^="test-"]:not([data-test-id^="test-foo"])`, + ]) { + await page.$$(selector); + } + }); + + it('should work with deep combinators', async () => { + const {page} = getTestState(); + { + const element = await page.$('div >>>> div'); + console.log({element}) + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'c'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('div >>> div'); + assert(elements[1], 'Could not find element'); + expect( + await elements[1]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('#c >>>> div'); + assert(elements[0], 'Could not find element'); + expect( + await elements[0]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('#c >>> div'); + assert(elements[0], 'Could not find element'); + expect( + await elements[0]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + }); + + it('should work with text selectors', async () => { + const {page} = getTestState(); + const element = await page.$('div ::-p-text(world)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work ARIA selectors', async () => { + const {page} = getTestState(); + const element = await page.$('div ::-p-aria(world)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work XPath selectors', async () => { + const {page} = getTestState(); + const element = await page.$('div ::-p-xpath(//button)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work with custom selectors', async () => { + Puppeteer.registerCustomQueryHandler('div', { + queryOne() { + return document.querySelector('div'); + }, + }); + + const {page} = getTestState(); + const element = await page.$('::-p-div'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + }); + + it('should work with custom selectors with args', async () => { + const {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('div', { + queryOne(_, selector) { + if (selector === 'true') { + return document.querySelector('div'); + } else { + return document.querySelector('button'); + } + }, + }); + + { + const element = await page.$('::-p-div(true)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$('::-p-div("true")'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$("::-p-div('true')"); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$('::-p-div'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + } + }); + + it('should work with :hover', async () => { + const {page} = getTestState(); + let button = await page.$('div ::-p-text(world)'); + assert(button, 'Could not find element'); + await button.hover(); + await button.dispose(); + + button = await page.$('div ::-p-text(world):hover'); + assert(button, 'Could not find element'); + const value = await button.evaluate(span => { + return {textContent: span.textContent, tagName: span.tagName}; + }); + expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'}); + }); + + it('should work with selector lists', async () => { + const {page} = getTestState(); + const elements = await page.$$('div, ::-p-text(world)'); + expect(elements).toHaveLength(3); + }); + + const permute = <T>(inputs: T[]): T[][] => { + const results: T[][] = []; + for (let i = 0; i < inputs.length; ++i) { + const permutation = permute( + inputs.slice(0, i).concat(inputs.slice(i + 1)) + ); + const value = inputs[i] as T; + if (permutation.length === 0) { + results.push([value]); + continue; + } + for (const part of permutation) { + results.push([value].concat(part)); + } + } + return results; + }; + + it('should match querySelector* ordering', async () => { + const {page} = getTestState(); + for (const list of permute(['div', 'button', 'span'])) { + const elements = await page.$$( + list + .map(selector => { + return selector === 'button' ? '::-p-text(world)' : selector; + }) + .join(',') + ); + const actual = await Promise.all( + elements.map(element => { + return element.evaluate(element => { + return element.id; + }); + }) + ); + expect(actual.join()).toStrictEqual('a,b,f,c'); + } + }); + + it('should not have duplicate elements from selector lists', async () => { + const {page} = getTestState(); + const elements = await page.$$('::-p-text(world), button'); + expect(elements).toHaveLength(1); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/queryselector.spec.ts b/remote/test/puppeteer/test/src/queryselector.spec.ts new file mode 100644 index 0000000000..190578bd59 --- /dev/null +++ b/remote/test/puppeteer/test/src/queryselector.spec.ts @@ -0,0 +1,505 @@ +/** + * 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 {Puppeteer} from 'puppeteer'; +import type {CustomQueryHandler} from 'puppeteer-core/internal/common/CustomQueryHandler.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('querySelector', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.$eval', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent('<section id="testAttribute">43543</section>'); + const idAttribute = await page.$eval('section', e => { + return e.id; + }); + expect(idAttribute).toBe('testAttribute'); + }); + it('should accept arguments', async () => { + const {page} = getTestState(); + + await page.setContent('<section>hello</section>'); + const text = await page.$eval( + 'section', + (e, suffix) => { + return e.textContent! + suffix; + }, + ' world!' + ); + expect(text).toBe('hello world!'); + }); + it('should accept ElementHandles as arguments', async () => { + const {page} = getTestState(); + + await page.setContent('<section>hello</section><div> world</div>'); + const divHandle = (await page.$('div'))!; + const text = await page.$eval( + 'section', + (e, div) => { + return e.textContent! + (div as HTMLElement).textContent!; + }, + divHandle + ); + expect(text).toBe('hello world'); + }); + it('should throw error if no element is found', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .$eval('section', e => { + return e.id; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'failed to find element matching selector "section"' + ); + }); + }); + + // 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<Element>. + describe('Page.$$eval', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval('div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('should accept extra arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'div', + (divs, two, three) => { + return divs.length + (two as number) + (three as number); + }, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('should accept ElementHandles as arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + const divHandle = (await page.$('div'))!; + const sum = await page.$$eval( + 'section', + (sections, div) => { + return ( + sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0) + Number((div as HTMLElement).textContent) + ); + }, + divHandle + ); + expect(sum).toBe(8); + }); + it('should handle many elements', async function () { + this.timeout(25_000); + + 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 => { + return sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0); + }); + expect(sum).toBe(500500); + }); + }); + + describe('Page.$', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<section>test</section>'); + 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('<div>A</div><br/><div>B</div>'); + const elements = await page.$$('div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate((e: HTMLElement) => { + return 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).toHaveLength(0); + }); + }); + + describe('Page.$x', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<section>test</section>'); + const elements = await page.$x('/html/body/section'); + expect(elements[0]).toBeTruthy(); + expect(elements).toHaveLength(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('<div></div><div></div>'); + const elements = await page.$x('/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + const html = (await page.$('html'))!; + const second = (await html.$('.second'))!; + const inner = await second.$('.inner'); + const content = await page.evaluate(e => { + return e?.textContent; + }, inner); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const {page} = getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + 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( + '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>' + ); + const tweet = (await page.$('.tweet'))!; + const content = await tweet.$eval('.like', node => { + return (node as HTMLElement).innerText; + }); + expect(content).toBe('100'); + }); + + it('should retrieve content from subtree', async () => { + const {page} = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>'; + await page.setContent(htmlContent); + const elementHandle = (await page.$('#myId'))!; + const content = await elementHandle.$eval('.a', node => { + return (node as HTMLElement).innerText; + }); + expect(content).toBe('a-child-div'); + }); + + it('should throw in case of missing selector', async () => { + const {page} = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + const elementHandle = (await page.$('#myId'))!; + const errorMessage = await elementHandle + .$eval('.a', node => { + return (node as HTMLElement).innerText; + }) + .catch(error => { + return 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( + '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>' + ); + const tweet = (await page.$('.tweet'))!; + const content = await tweet.$$eval('.like', nodes => { + return (nodes as HTMLElement[]).map(n => { + return n.innerText; + }); + }); + expect(content).toEqual(['100', '10']); + }); + + it('should retrieve content from subtree', async () => { + const {page} = getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>'; + await page.setContent(htmlContent); + const elementHandle = (await page.$('#myId'))!; + const content = await elementHandle.$$eval('.a', nodes => { + return (nodes as HTMLElement[]).map(n => { + return 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 = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + const elementHandle = (await page.$('#myId'))!; + const nodesLength = await elementHandle.$$eval('.a', nodes => { + return nodes.length; + }); + expect(nodesLength).toBe(0); + }); + }); + + describe('ElementHandle.$$', function () { + it('should query existing elements', async () => { + const {page} = getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate((e: HTMLElement) => { + return 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( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('div'); + expect(elements).toHaveLength(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( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + 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 => { + return e.textContent; + }, inner[0]!); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const {page} = getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + 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, selector) => { + return [...(element as Element).querySelectorAll(selector)]; + }, + }; + before(() => { + Puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + expect( + Puppeteer.customQueryHandlerNames().includes('allArray') + ).toBeTruthy(); + }); + it('$$ should query existing elements', async () => { + const {page} = getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate(e => { + return 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( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements).toHaveLength(0); + }); + it('$$eval should work', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval('allArray/div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('$$eval should accept extra arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'allArray/div', + (divs, two, three) => { + return divs.length + (two as number) + (three as number); + }, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('$$eval should accept ElementHandles as arguments', async () => { + const {page} = getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + const divHandle = (await page.$('div'))!; + const sum = await page.$$eval( + 'allArray/section', + (sections, div) => { + return ( + sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0) + Number((div as HTMLElement).textContent) + ); + }, + divHandle + ); + expect(sum).toBe(8); + }); + it('$$eval should handle many elements', async function () { + this.timeout(25_000); + + 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 => { + return sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0); + }); + expect(sum).toBe(500500); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts new file mode 100644 index 0000000000..62f0cb640e --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts @@ -0,0 +1,983 @@ +/** + * 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 expect from 'expect'; +import { + ActionResult, + HTTPRequest, + InterceptResolutionAction, +} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('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).toHaveLength(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 (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 => { + return request.continue({}, 0); + }); + await page.setContent(` + <form action='/rredirect' method='post'> + <input type="hidden" id="foo" name="foo" value="FOOBAR"> + </form> + `); + await Promise.all([ + page.$eval('form', form => { + return (form as HTMLFormElement).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: HTTPRequest[] = []; + page.on('request', request => { + if (!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 => { + return 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 => { + return 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 => { + return 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', () => { + return ++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); + }); + + const [failedRequest] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'requestfailed'), + 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 => { + return 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 => { + return request.abort('failed', 0); + }); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (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: HTTPRequest[] = []; + 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).toHaveLength(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response!.request().redirectChain(); + expect(redirectChain).toHaveLength(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: HTTPRequest[] = []; + page.on('request', request => { + request.continue({}, 0); + if (!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) => { + return 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).toHaveLength(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain).toHaveLength(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 { + return await fetch('/non-existing.json'); + } catch (error) { + return (error as 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) => { + return res.end(responseCount++ * 11 + ''); + }); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', request => { + if (isFavicon(request)) { + request.continue({}, 0); + return; + } + spinner ? request.abort('failed', 0) : request.continue({}, 0); + spinner = !spinner; + }); + const results = await page.evaluate(() => { + return Promise.all([ + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return '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: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = await page.goto(dataURL); + expect(response!.status()).toBe(200); + expect(requests).toHaveLength(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: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + request.continue({}, 0); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('<div>yo</div>'); + expect(requests).toHaveLength(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: HTTPRequest[] = []; + 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).toHaveLength(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 => { + return 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) => { + return res.end(); + }); + page.on('request', request => { + return 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: HTTPRequest[] = []; + page.on('request', request => { + request.continue({}, 0); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>` + ); + expect(response!.status()).toBe(200); + expect(requests).toHaveLength(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('<iframe></iframe>'); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + page.$eval( + 'iframe', + (frame, url) => { + return ((frame as HTMLIFrameElement).src = url as string); + }, + server.EMPTY_PAGE + ), + // Wait for request interception. + await waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', frame => { + return frame.remove(); + }); + let error!: Error; + await request.continue({}, 0).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should throw if interception is not enabled', async () => { + const {page, server} = getTestState(); + + let error!: Error; + page.on('request', async request => { + try { + await request.continue({}, 0); + } catch (error_) { + error = error_ as 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 => { + return request.continue({}, 0); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(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 => { + return request.continue({}, 0); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(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 => { + return request.continue({}, 0); + }); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse(r => { + return r.url().endsWith('/one-style.woff'); + }); + }); + }); + + describe('Request.continue', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return 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(() => { + return 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); + }); + + const [consoleMessage] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + 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(() => { + return 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(() => { + return 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'); + }); + }); + + describe('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(() => { + return 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(() => { + return 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()).toHaveLength(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 => { + return (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(() => { + return 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/remote/test/puppeteer/test/src/requestinterception.spec.ts b/remote/test/puppeteer/test/src/requestinterception.spec.ts new file mode 100644 index 0000000000..51e63f4be0 --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts @@ -0,0 +1,934 @@ +/** + * 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 expect from 'expect'; +import {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + describe('Page.setRequestInterception', function () { + it('should intercept', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (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 => { + return request.continue(); + }); + await page.setContent(` + <form action='/rredirect' method='post'> + <input type="hidden" id="foo" name="foo" value="FOOBAR"> + </form> + `); + await Promise.all([ + page.$eval('form', form => { + return (form as HTMLFormElement).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: HTTPRequest[] = []; + page.on('request', request => { + if (!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 work with requests without networkId', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + + const cdp = await page.target().createCDPSession(); + await cdp.send('DOM.enable'); + const urls: string[] = []; + page.on('request', request => { + urls.push(request.url()); + return request.continue(); + }); + // This causes network requests without networkId. + await cdp.send('CSS.enable'); + expect(urls).toStrictEqual([server.EMPTY_PAGE]); + }); + 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 => { + return 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 => { + return 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 => { + return 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', () => { + return ++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'); + }); + const [failedRequest] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'requestfailed'), + 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 => { + return 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 => { + return request.abort(); + }); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (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: HTTPRequest[] = []; + 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).toHaveLength(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain).toHaveLength(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: HTTPRequest[] = []; + page.on('request', request => { + request.continue(); + if (!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) => { + return 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).toHaveLength(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain).toHaveLength(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 { + return await fetch('/non-existing.json'); + } catch (error) { + return (error as 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) => { + return res.end(responseCount++ * 11 + ''); + }); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', request => { + if (isFavicon(request)) { + request.continue(); + return; + } + spinner ? request.abort() : request.continue(); + spinner = !spinner; + }); + const results = await page.evaluate(() => { + return Promise.all([ + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return '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: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = (await page.goto(dataURL))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(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: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('<div>yo</div>'); + expect(requests).toHaveLength(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: HTTPRequest[] = []; + 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).toHaveLength(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 => { + return 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) => { + return res.end(); + }); + page.on('request', request => { + return 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: HTTPRequest[] = []; + page.on('request', request => { + request.continue(); + requests.push(request); + }); + const response = (await page.goto( + `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>` + ))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(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('<iframe></iframe>'); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + page.$eval( + 'iframe', + (frame, url) => { + return ((frame as HTMLIFrameElement).src = url as string); + }, + server.EMPTY_PAGE + ), + // Wait for request interception. + await waitEvent(page, 'request'); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', frame => { + return frame.remove(); + }); + let error!: Error; + await request.continue().catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should throw if interception is not enabled', async () => { + const {page, server} = getTestState(); + + let error!: Error; + page.on('request', async request => { + try { + await request.continue(); + } catch (error_) { + error = error_ as 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 => { + return request.continue(); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(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 => { + return request.continue(); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(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 => { + return request.continue(); + }); + + const responsePromise = page.waitForResponse(r => { + return r.url().endsWith('/one-style.woff'); + }); + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await responsePromise; + }); + }); + + describe('Request.continue', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return 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(() => { + return 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}); + }); + const [consoleMessage] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + 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(() => { + return 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(() => { + return 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!: Error; + await page.setRequestInterception(true); + page.on('request', async request => { + await request + .continue({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch(error_ => { + error = error_ as Error; + }); + await request.continue(); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); + + describe('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(() => { + return 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(() => { + return 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()).toHaveLength(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 => { + return cookie.name === 'first'; + }); + const secondCookie = cookies.find(cookie => { + return 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 => { + return (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(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should fail if the header value is invalid', async () => { + const {page, server} = getTestState(); + + let error!: Error; + await page.setRequestInterception(true); + page.on('request', async request => { + await request + .respond({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch(error_ => { + error = error_ as Error; + }); + await request.respond({ + status: 200, + body: 'Hello World', + }); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); +}); + +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/remote/test/puppeteer/test/src/screenshot.spec.ts b/remote/test/puppeteer/test/src/screenshot.spec.ts new file mode 100644 index 0000000000..f38d7ee897 --- /dev/null +++ b/remote/test/puppeteer/test/src/screenshot.spec.ts @@ -0,0 +1,388 @@ +/** + * 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.js'; + +describe('Screenshots', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.screenshot', function () { + it('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'); + }); + it('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'); + }); + it('should use scale for clip', async () => { + const {page, server} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 50, + y: 100, + width: 150, + height: 100, + scale: 2, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png'); + }); + it('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'); + }); + it('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 => { + return page.close(); + }) + ); + }); + it('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'); + }); + it('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'); + }); + it('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'); + }); + it('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', + }); + expect(Buffer.from(screenshot, 'base64')).toBeGolden( + 'screenshot-sanity.png' + ); + }); + it('should work in "fromSurface: false" mode', async () => { + const {page, server} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fromSurface: false, + }); + expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.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(() => { + return 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 work with a null viewport', async () => { + const {defaultBrowserOptions, puppeteer, server} = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + defaultViewport: null, + }); + + try { + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => { + return window.scrollBy(50, 100); + }); + const elementHandle = (await page.$('.box:nth-of-type(3)'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeTruthy(); + } finally { + await browser.close(); + } + }); + it('should take into account padding and border', async () => { + const {page} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + something above + <style>div { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div></div> + `); + const elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); + }); + it('should capture full element when larger than viewport', async () => { + const {page} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + + await page.setContent(` + something above + <style> + div.to-screenshot { + border: 1px solid blue; + width: 600px; + height: 600px; + margin-left: 50px; + } + ::-webkit-scrollbar{ + display: none; + } + </style> + <div class="to-screenshot"></div> + `); + 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(() => { + return { + 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 + <style>div.above { + border: 2px solid blue; + background: red; + height: 1500px; + } + div.to-screenshot { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div class="above"></div> + <div class="to-screenshot"></div> + `); + const elementHandle = (await page.$('div.to-screenshot'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-scrolled-into-view.png' + ); + }); + it('should work with a rotated element', async () => { + const {page} = getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(`<div style="position:absolute; + top: 100px; + left: 100px; + width: 100px; + height: 100px; + background: green; + transform: rotateZ(200deg);"> </div>`); + const elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-rotate.png'); + }); + it('should fail to screenshot a detached element', async () => { + const {page} = getTestState(); + + await page.setContent('<h1>remove this</h1>'); + const elementHandle = (await page.$('h1'))!; + await page.evaluate((element: HTMLElement) => { + return element.remove(); + }, elementHandle); + const screenshotError = await elementHandle.screenshot().catch(error => { + return 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('<div style="width: 50px; height: 0"></div>'); + const div = (await page.$('div'))!; + const error = await div.screenshot().catch(error_ => { + return 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( + '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>' + ); + const elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional.png'); + }); + it('should work for an element with an offset', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>' + ); + const elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/target.spec.ts b/remote/test/puppeteer/test/src/target.spec.ts new file mode 100644 index 0000000000..3b5e5c3401 --- /dev/null +++ b/remote/test/puppeteer/test/src/target.spec.ts @@ -0,0 +1,336 @@ +/** + * 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 {ServerResponse} from 'http'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; +import {Target} from 'puppeteer-core/internal/common/Target.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.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 => { + return target.type() === 'page' && target.url() === 'about:blank'; + }) + ).toBeTruthy(); + expect( + targets.some(target => { + return 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).toHaveLength(1); + expect(allPages).toContain(page); + }); + it('should contain browser target', async () => { + const {browser} = getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find(target => { + return 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 => { + return p !== page; + })!; + expect( + await originalPage.evaluate(() => { + return ['Hello', 'world'].join(' '); + }) + ).toBe('Hello world'); + expect(await originalPage.$('body')).toBeTruthy(); + }); + it('should be able to use async waitForTarget', async () => { + const {page, server, context} = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget(target => { + return target.page().then(page => { + return page!.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + }) + .then(target => { + return target.page(); + }), + page.evaluate((url: string) => { + return window.open(url); + }, server.CROSS_PROCESS_PREFIX + '/empty.html'), + ]); + expect(otherPage!.url()).toEqual( + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(page).not.toEqual(otherPage); + }); + it('should report when a new page is created and closed', async () => { + const {page, server, context} = getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget(target => { + return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }) + .then(target => { + return target.page(); + }), + page.evaluate((url: string) => { + return window.open(url); + }, server.CROSS_PROCESS_PREFIX + '/empty.html'), + ]); + expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX); + expect( + await otherPage!.evaluate(() => { + return ['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 [closedTarget] = await Promise.all([ + waitEvent<Target>(context, 'targetdestroyed'), + otherPage!.close(), + ]); + expect(await closedTarget.page()).toBe(otherPage); + + allPages = (await Promise.all( + context.targets().map(target => { + return target.page(); + }) + )) as Page[]; + expect(allPages).toContain(page); + expect(allPages).not.toContain(otherPage); + }); + it('should report when a service worker is created and destroyed', async () => { + const {page, server, context} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const createdTarget = waitEvent(context, 'targetcreated'); + + 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 = waitEvent(context, 'targetdestroyed'); + await page.evaluate(() => { + return ( + globalThis as unknown as { + registrationPromise: Promise<{unregister: () => void}>; + } + ).registrationPromise.then((registration: any) => { + return registration.unregister(); + }); + }); + expect(await destroyedTarget).toBe(await createdTarget); + }); + it('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 => { + return target.type() === 'service_worker'; + }); + const worker = (await target.worker())!; + expect( + await worker.evaluate(() => { + return self.toString(); + }) + ).toBe('[object ServiceWorkerGlobalScope]'); + }); + it('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 => { + return target.type() === 'shared_worker'; + }); + const worker = (await target.worker())!; + expect( + await worker.evaluate(() => { + return self.toString(); + }) + ).toBe('[object SharedWorkerGlobalScope]'); + }); + it('should report when a target url changes', async () => { + const {page, server, context} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + let changedTarget = waitEvent(context, 'targetchanged'); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = waitEvent(context, 'targetchanged'); + await page.goto(server.EMPTY_PAGE); + expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); + }); + it('should not report uninitialized pages', async () => { + const {context} = getTestState(); + + let targetChanged = false; + const listener = () => { + return (targetChanged = true); + }; + context.on('targetchanged', listener); + const targetPromise = waitEvent<Target>(context, 'targetcreated'); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = waitEvent<Target>(context, 'targetcreated'); + const evaluatePromise = newPage.evaluate(() => { + return window.open('about:blank'); + }); + const target2 = await targetPromise2; + expect(target2.url()).toBe('about:blank'); + await evaluatePromise; + await newPage.close(); + expect(targetChanged).toBe(false); + context.off('targetchanged', listener); + }); + it('should not crash while redirecting if original request was missed', async () => { + const {page, server, context} = getTestState(); + + let serverResponse!: ServerResponse; + server.setRoute('/one-style.css', (_req, res) => { + return (serverResponse = res); + }); + // Open a new page. Use window.open to connect to the page later. + await Promise.all([ + page.evaluate((url: string) => { + return window.open(url); + }, server.PREFIX + '/one-style.html'), + server.waitForRequest('/one-style.css'), + ]); + // Connect to the opened page. + const target = await context.waitForTarget(target => { + return 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(); + }); + it('should have an opener', async () => { + const {page, server, context} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + waitEvent<Target>(context, 'targetcreated'), + 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()).toBeUndefined(); + }); + + describe('Browser.waitForTarget', () => { + it('should wait for a target', async () => { + const {browser, server} = getTestState(); + + let resolved = false; + const targetPromise = browser.waitForTarget(target => { + return target.url() === server.EMPTY_PAGE; + }); + targetPromise + .then(() => { + return (resolved = true); + }) + .catch(error => { + resolved = true; + if (error instanceof 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 TimeoutError) { + console.error(error); + } else { + throw error; + } + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const {browser, server} = getTestState(); + + let error!: Error; + await browser + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/touchscreen.spec.ts b/remote/test/puppeteer/test/src/touchscreen.spec.ts new file mode 100644 index 0000000000..cd78f41b20 --- /dev/null +++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts @@ -0,0 +1,80 @@ +/** + * 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 {KnownDevices, BoundingBox} from 'puppeteer'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + + it('should report touches', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['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(() => { + return (globalThis as any).getResult(); + }) + ).toEqual(['Touchstart: 0', 'Touchend: 0']); + }); + + it('should report touchMove', async () => { + const {page, server} = getTestState(); + const iPhone = KnownDevices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches-move.html'); + const touch = (await page.$('#touch'))!; + const touchObj = (await touch.boundingBox()) as BoundingBox; + await page.touchscreen.touchStart(touchObj.x, touchObj.y); + const movePosx = 100; + const movePosy = 100; + await page.touchscreen.touchMove(movePosx, movePosy); + await page.touchscreen.touchEnd(); + expect( + await page.evaluate(() => { + return (globalThis as any).touchX; + }) + ).toBe(movePosx); + expect( + await page.evaluate(() => { + return (globalThis as any).touchY; + }) + ).toBe(movePosy); + }); +}); diff --git a/remote/test/puppeteer/test/src/tracing.spec.ts b/remote/test/puppeteer/test/src/tracing.spec.ts new file mode 100644 index 0000000000..b9553c4dac --- /dev/null +++ b/remote/test/puppeteer/test/src/tracing.spec.ts @@ -0,0 +1,157 @@ +/** + * 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 {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState} from './mocha-utils.js'; + +describe('Tracing', function () { + let outputFile!: string; + let browser!: Browser; + let page!: 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, 'trace.json'); + }); + + afterEach(async () => { + await browser.close(); + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + }); + 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!: Error; + await newPage.tracing.start({path: outputFile}).catch(error_ => { + return (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 undefined 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(undefined); + 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; + try { + await page.tracing.stop(); + } catch (error_) { + error = error_ as Error; + } + expect(error).toBeDefined(); + }); +}); diff --git a/remote/test/puppeteer/test/src/utils.ts b/remote/test/puppeteer/test/src/utils.ts new file mode 100644 index 0000000000..43c98e953f --- /dev/null +++ b/remote/test/puppeteer/test/src/utils.ts @@ -0,0 +1,150 @@ +/** + * 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 {Page} from 'puppeteer-core/internal/api/Page.js'; +import {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {Frame} from 'puppeteer-core/internal/common/Frame.js'; + +import {compare} from './golden-utils.js'; + +const PROJECT_ROOT = path.join(__dirname, '..', '..'); + +declare module 'expect' { + interface Matchers<R> { + toBeGolden(pathOrBuffer: string | Buffer): R; + } +} + +export const extendExpectWithToBeGolden = ( + goldenDir: string, + outputDir: string +): void => { + expect.extend({ + toBeGolden: (testScreenshot: string | Buffer, goldenFilePath: string) => { + const result = compare( + goldenDir, + outputDir, + testScreenshot, + goldenFilePath + ); + + if (result.pass) { + return { + pass: true, + message: () => { + return ''; + }, + }; + } else { + return { + pass: false, + message: () => { + return result.message; + }, + }; + } + }, + }); +}; + +export const projectRoot = (): string => { + return PROJECT_ROOT; +}; + +export const attachFrame = async ( + pageOrFrame: Page | Frame, + frameId: string, + url: string +): Promise<Frame | undefined> => { + const handle = await pageOrFrame.evaluateHandle(attachFrame, frameId, url); + return (await handle.asElement()?.contentFrame()) ?? undefined; + + async function attachFrame(frameId: string, url: string) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => { + return (frame.onload = x); + }); + return frame; + } +}; + +export const isFavicon = (request: {url: () => string | string[]}): boolean => { + return request.url().includes('favicon.ico'); +}; + +export async function detachFrame( + pageOrFrame: Page | Frame, + frameId: string +): Promise<void> { + await pageOrFrame.evaluate(detachFrame, frameId); + + function detachFrame(frameId: string) { + const frame = document.getElementById(frameId) as HTMLIFrameElement; + frame.remove(); + } +} + +export async function navigateFrame( + pageOrFrame: Page | Frame, + frameId: string, + url: string +): Promise<void> { + await pageOrFrame.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId: string, url: any) { + const frame = document.getElementById(frameId) as HTMLIFrameElement; + frame.src = url; + return new Promise(x => { + return (frame.onload = x); + }); + } +} + +export const dumpFrames = (frame: Frame, indentation?: string): string[] => { + indentation = indentation || ''; + let description = frame.url().replace(/:\d{4,5}\//, ':<PORT>/'); + if (frame.name()) { + description += ' (' + frame.name() + ')'; + } + const result = [indentation + description]; + for (const child of frame.childFrames()) { + result.push(...dumpFrames(child, ' ' + indentation)); + } + return result; +}; + +export const waitEvent = <T = any>( + emitter: EventEmitter, + eventName: string, + predicate: (event: T) => boolean = () => { + return true; + } +): Promise<T> => { + return new Promise(fulfill => { + emitter.on(eventName, (event: T) => { + if (!predicate(event)) { + return; + } + fulfill(event); + }); + }); +}; diff --git a/remote/test/puppeteer/test/src/waittask.spec.ts b/remote/test/puppeteer/test/src/waittask.spec.ts new file mode 100644 index 0000000000..27977fb7ac --- /dev/null +++ b/remote/test/puppeteer/test/src/waittask.spec.ts @@ -0,0 +1,867 @@ +/** + * 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 {TimeoutError, ElementHandle} from 'puppeteer'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; + +import { + createTimeout, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame} from './utils.js'; + +describe('waittask specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Frame.waitForFunction', function () { + it('should accept a string', async () => { + const {page} = getTestState(); + + const watchdog = page.waitForFunction('self.__FOO === 1'); + await page.evaluate(() => { + return ((self as unknown as {__FOO: number}).__FOO = 1); + }); + await watchdog; + }); + it('should work when resolved right before execution context disposal', async () => { + const {page} = getTestState(); + + await page.evaluateOnNewDocument(() => { + return ((globalThis as any).__RELOADED = true); + }); + await page.waitForFunction(() => { + if (!(globalThis as any).__RELOADED) { + window.location.reload(); + return false; + } + return true; + }); + }); + it('should poll on interval', async () => { + const {page} = getTestState(); + const startTime = Date.now(); + const polling = 100; + const watchdog = page.waitForFunction( + () => { + return (globalThis as any).__FOO === 'hit'; + }, + {polling} + ); + await page.evaluate(() => { + setTimeout(() => { + (globalThis as any).__FOO = 'hit'; + }, 50); + }); + 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( + () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling: 'mutation', + } + ) + .then(() => { + return (success = true); + }); + await page.evaluate(() => { + return ((globalThis as any).__FOO = 'hit'); + }); + expect(success).toBe(false); + await page.evaluate(() => { + return 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 () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling: 'mutation', + } + ) + .then(() => { + return (success = true); + }); + await page.evaluate(async () => { + return ((globalThis as any).__FOO = 'hit'); + }); + expect(success).toBe(false); + await page.evaluate(async () => { + return document.body.appendChild(document.createElement('div')); + }); + await watchdog; + }); + it('should poll on raf', async () => { + const {page} = getTestState(); + + const watchdog = page.waitForFunction( + () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling: 'raf', + } + ); + await page.evaluate(() => { + return ((globalThis as any).__FOO = 'hit'); + }); + await watchdog; + }); + it('should poll on raf async', async () => { + const {page} = getTestState(); + + const watchdog = page.waitForFunction( + async () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling: 'raf', + } + ); + await page.evaluate(async () => { + return ((globalThis as any).__FOO = 'hit'); + }); + await watchdog; + }); + it('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!: Error; + await Promise.all([ + page + .waitForFunction( + () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling: 'raf', + } + ) + .catch(error_ => { + return (error = error_); + }), + page.evaluate(() => { + return ((globalThis as any).__FOO = 'hit'); + }), + ]); + expect(error).toBeUndefined(); + }); + it('should throw negative polling interval', async () => { + const {page} = getTestState(); + + let error!: Error; + try { + await page.waitForFunction( + () => { + return !!document.body; + }, + {polling: -10} + ); + } catch (error_) { + if (isErrorLike(error_)) { + error = error_ as Error; + } + } + 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(() => { + return 5; + }) + ).jsonValue() + ).toBe(5); + }); + it('should return the window as a success value', async () => { + const {page} = getTestState(); + + expect( + await page.waitForFunction(() => { + return window; + }) + ).toBeTruthy(); + }); + it('should accept ElementHandle arguments', async () => { + const {page} = getTestState(); + + await page.setContent('<div></div>'); + const div = (await page.$('div'))!; + let resolved = false; + const waitForFunction = page + .waitForFunction( + (element: Element) => { + return element.localName === 'div' && !element.parentElement; + }, + {}, + div + ) + .then(() => { + return (resolved = true); + }); + expect(resolved).toBe(false); + await page.evaluate((element: HTMLElement) => { + return element.remove(); + }, div); + await waitForFunction; + }); + it('should respect timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page + .waitForFunction( + () => { + return false; + }, + {timeout: 10} + ) + .catch(error_ => { + return (error = error_); + }); + + expect(error).toBeInstanceOf(TimeoutError); + expect(error?.message).toContain('Waiting failed: 10ms exceeded'); + }); + it('should respect default timeout', async () => { + const {page} = getTestState(); + + page.setDefaultTimeout(1); + let error!: Error; + await page + .waitForFunction(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + expect(error?.message).toContain('Waiting failed: 1ms exceeded'); + }); + it('should disable timeout when its set to 0', async () => { + const {page} = getTestState(); + + const watchdog = page.waitForFunction( + () => { + (globalThis as any).__counter = + ((globalThis as any).__counter || 0) + 1; + return (globalThis as any).__injected; + }, + {timeout: 0, polling: 10} + ); + await page.waitForFunction(() => { + return (globalThis as any).__counter > 10; + }); + await page.evaluate(() => { + return ((globalThis as any).__injected = true); + }); + await watchdog; + }); + it('should survive cross-process navigation', async () => { + const {page, server} = getTestState(); + + let fooFound = false; + const waitForFunction = page + .waitForFunction(() => { + return (globalThis as unknown as {__FOO: number}).__FOO === 1; + }) + .then(() => { + return (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(() => { + return ((globalThis as any).__FOO = 1); + }); + await waitForFunction; + expect(fooFound).toBe(true); + }); + it('should survive navigations', async () => { + const {page, server} = getTestState(); + + const watchdog = page.waitForFunction(() => { + return (globalThis as any).__done; + }); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/consolelog.html'); + await page.evaluate(() => { + return ((globalThis as any).__done = true); + }); + await watchdog; + }); + it('should be cancellable', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const abortController = new AbortController(); + const task = page.waitForFunction( + () => { + return (globalThis as any).__done; + }, + { + signal: abortController.signal, + } + ); + abortController.abort(); + await expect(task).rejects.toThrow(/aborted/); + }); + }); + + 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: string) => { + return 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'); + }); + + it('should be cancellable', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const abortController = new AbortController(); + const task = page.waitForSelector('wrong', { + signal: abortController.signal, + }); + abortController.abort(); + await expect(task).rejects.toThrow(/aborted/); + }); + + it('should work with removed MutationObserver', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + // @ts-expect-error We want to remove it for the test. + return delete window.MutationObserver; + }); + const [handle] = await Promise.all([ + page.waitForSelector('.zombo'), + page.setContent(`<div class='zombo'>anything</div>`), + ]); + expect( + await page.evaluate(x => { + return 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 (await eHandle.getProperty('tagName')).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(() => { + return (document.querySelector('span')!.innerHTML = + '<h3><div></div></h3>'); + }); + await watchdog; + }); + + it('Page.waitForSelector is shortcut for main frame', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await 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?.frame).toBe(page.mainFrame()); + }); + + it('should run in specified frame', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await 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?.frame).toBe(frame2); + }); + + it('should throw when frame is detached', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]!; + let waitError: Error | undefined; + const waitPromise = frame.waitForSelector('.box').catch(error => { + return (waitError = error); + }); + await 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(() => { + return (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 element to be visible (display)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('<div style="display: none">text</div>'); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.removeProperty('display'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be visible (visibility)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('<div style="visibility: hidden">text</div>'); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.setProperty('visibility', 'collapse'); + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.removeProperty('visibility'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be visible (bounding box)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('<div style="width: 0">text</div>'); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.setProperty('height', '0'); + e.style.removeProperty('width'); + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.removeProperty('height'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be visible recursively', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div#inner', { + visible: true, + }); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>` + ); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + return e.style.removeProperty('display'); + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + return e.style.removeProperty('visibility'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be hidden (visibility)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`<div style='display: block;'>text</div>`); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + return e.style.setProperty('visibility', 'hidden'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be hidden (display)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`<div style='display: block;'>text</div>`); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + return e.style.setProperty('display', 'none'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be hidden (bounding box)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent('<div>text</div>'); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40)]) + ).resolves.toBeFalsy(); + await element.evaluate(e => { + e.style.setProperty('height', '0'); + }); + await expect(promise).resolves.toBeTruthy(); + }); + it('should wait for element to be hidden (removal)', async () => { + const {page} = getTestState(); + + const promise = page.waitForSelector('div', {hidden: true}); + await page.setContent(`<div>text</div>`); + const element = await page.evaluateHandle(() => { + return document.getElementsByTagName('div')[0]!; + }); + await expect( + Promise.race([promise, createTimeout(40, true)]) + ).resolves.toBeTruthy(); + await element.evaluate(e => { + e.remove(); + }); + await expect(promise).resolves.toBeFalsy(); + }); + 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} = getTestState(); + + let error!: Error; + await page.waitForSelector('div', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + expect(error?.message).toContain( + 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded' + ); + }); + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const {page} = getTestState(); + + await page.setContent(`<div>text</div>`); + let error!: Error; + await page + .waitForSelector('div', {hidden: true, timeout: 10}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error?.message).toContain( + 'Waiting for selector `div` failed: Waiting failed: 10ms exceeded' + ); + }); + + it('should respond to node attribute mutation', async () => { + const {page} = getTestState(); + + let divFound = false; + const waitForSelector = page.waitForSelector('.zombo').then(() => { + return (divFound = true); + }); + await page.setContent(`<div class='notZombo'></div>`); + expect(divFound).toBe(false); + await page.evaluate(() => { + return (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(`<div class='zombo'>anything</div>`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForSelector) + ).toBe('anything'); + }); + it('should have correct stack trace for timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error?.stack).toContain( + 'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded' + ); + // The extension is ts here as Mocha maps back via sourcemaps. + expect(error?.stack).toContain('WaitTask.ts'); + }); + }); + + describe('Frame.waitForXPath', function () { + const addElement = (tag: string) => { + return document.body.appendChild(document.createElement(tag)); + }; + + it('should support some fancy xpath', async () => { + const {page} = getTestState(); + + await page.setContent(`<p>red herring</p><p>hello world </p>`); + const waitForXPath = page.waitForXPath( + '//p[normalize-space(.)="hello world"]' + ); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForXPath) + ).toBe('hello world '); + }); + it('should respect timeout', async () => { + const {page} = getTestState(); + + let error!: Error; + await page.waitForXPath('//div', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + expect(error?.message).toContain('Waiting failed: 10ms exceeded'); + }); + it('should run in specified frame', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await 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?.frame).toBe(frame2); + }); + it('should throw when frame is detached', async () => { + const {page, server} = getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const frame = page.frames()[1]!; + let waitError: Error | undefined; + const waitPromise = frame + .waitForXPath('//*[@class="box"]') + .catch(error => { + return (waitError = error); + }); + await 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(`<div style='display: block;'>text</div>`); + const waitForXPath = page + .waitForXPath('//div', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + await page.waitForXPath('//div'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div') + ?.style.setProperty('display', 'none'); + }); + expect(await waitForXPath).toBe(true); + expect(divHidden).toBe(true); + }); + it('hidden should return null if the element is not found', async () => { + const {page} = getTestState(); + + const waitForXPath = await page.waitForXPath('//div', {hidden: true}); + + expect(waitForXPath).toBe(null); + }); + it('hidden should return an empty element handle if the element is found', async () => { + const {page} = getTestState(); + + await page.setContent(`<div style='display: none;'>text</div>`); + + const waitForXPath = await page.waitForXPath('//div', {hidden: true}); + + expect(waitForXPath).toBeInstanceOf(ElementHandle); + }); + it('should return the element handle', async () => { + const {page} = getTestState(); + + const waitForXPath = page.waitForXPath('//*[@class="zombo"]'); + await page.setContent(`<div class='zombo'>anything</div>`); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForXPath) + ).toBe('anything'); + }); + it('should allow you to select a text node', async () => { + const {page} = getTestState(); + + await page.setContent(`<div>some text</div>`); + 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(`<div>some text</div>`); + const waitForXPath = page.waitForXPath('/html/body/div'); + expect( + await page.evaluate(x => { + return x?.textContent; + }, await waitForXPath) + ).toBe('some text'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/worker.spec.ts b/remote/test/puppeteer/test/src/worker.spec.ts new file mode 100644 index 0000000000..733c5bdff1 --- /dev/null +++ b/remote/test/puppeteer/test/src/worker.spec.ts @@ -0,0 +1,123 @@ +/** + * 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 {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; +import {WebWorker} from 'puppeteer-core/internal/common/WebWorker.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const {page, server} = getTestState(); + + await Promise.all([ + waitEvent(page, 'workercreated'), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]!; + expect(worker?.url()).toContain('worker.js'); + + expect( + await worker?.evaluate(() => { + return (globalThis as any).workerFunction(); + }) + ).toBe('worker function result'); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers()).toHaveLength(0); + }); + it('should emit created and destroyed events', async () => { + const {page} = getTestState(); + + const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated'); + const workerObj = await page.evaluateHandle(() => { + return new Worker('data:text/javascript,1'); + }); + const worker = await workerCreatedPromise; + const workerThisObj = await worker.evaluateHandle(() => { + return this; + }); + const workerDestroyedPromise = waitEvent(page, 'workerdestroyed'); + await page.evaluate((workerObj: Worker) => { + return workerObj.terminate(); + }, workerObj); + expect(await workerDestroyedPromise).toBe(worker); + const error = await workerThisObj.getProperty('self').catch(error => { + return 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(() => { + return 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 = waitEvent<ConsoleMessage>(page, 'console'); + await page.evaluate(() => { + return 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()).toHaveLength(4); + expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const {page} = getTestState(); + + const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated'); + await page.evaluate(() => { + return 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 = waitEvent<Error>(page, 'pageerror'); + await page.evaluate(() => { + return 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/remote/test/puppeteer/test/tsconfig.json b/remote/test/puppeteer/test/tsconfig.json new file mode 100644 index 0000000000..10fdfa2924 --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "NodeNext", + "outDir": "build", + "rootDir": "src" + }, + "include": ["src"], + "references": [{"path": "../tools/mochaRunner/tsconfig.json"}] +} diff --git a/remote/test/puppeteer/tools/analyze_issue.mjs b/remote/test/puppeteer/tools/analyze_issue.mjs new file mode 100755 index 0000000000..74e35ca3f8 --- /dev/null +++ b/remote/test/puppeteer/tools/analyze_issue.mjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +// @ts-check + +'use strict'; + +import {writeFile, mkdir, copyFile} from 'fs/promises'; +import {dirname, join} from 'path'; +import semver from 'semver'; +import {fileURLToPath} from 'url'; +import core from '@actions/core'; +import packageJson from '../packages/puppeteer-core/package.json' assert {type: 'json'}; + +const codifyAndJoinValues = values => { + return values + .map(value => { + return `\`${value}\``; + }) + .join(' ,'); +}; +const formatMessage = value => { + return value.trim(); +}; +const removeVersionPrefix = value => { + return value.startsWith('v') ? value.slice(1) : value; +}; + +const LAST_PUPPETEER_VERSION = packageJson.version; +if (!LAST_PUPPETEER_VERSION) { + core.setFailed('No maintained version found.'); +} +const LAST_SUPPORTED_NODE_VERSION = removeVersionPrefix( + packageJson.engines.node.slice(2).trim() +); + +const SUPPORTED_OSES = ['windows', 'macos', 'linux']; +const SUPPORTED_PACKAGE_MANAGERS = ['yarn', 'npm', 'pnpm']; + +const ERROR_MESSAGES = { + unsupportedOs(value) { + return formatMessage(` +This issue has an unsupported OS: \`${value}\`. Only the following operating systems are supported: ${codifyAndJoinValues( + SUPPORTED_OSES + )}. Please verify the issue on a supported OS and update the form. +`); + }, + unsupportedPackageManager(value) { + return formatMessage(` +This issue has an unsupported package manager: \`${value}\`. Only the following package managers are supported: ${codifyAndJoinValues( + SUPPORTED_PACKAGE_MANAGERS + )}. Please verify the issue using a supported package manager and update the form. +`); + }, + invalidPackageManagerVersion(value) { + return formatMessage(` +This issue has an invalid package manager version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version. +`); + }, + unsupportedNodeVersion(value) { + return formatMessage(` +This issue has an unsupported Node.js version: \`${value}\`. Only versions above \`v${LAST_SUPPORTED_NODE_VERSION}\` are supported. Please verify the issue on a supported version of Node.js and update the form. +`); + }, + invalidNodeVersion(value) { + return formatMessage(` +This issue has an invalid Node.js version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version. +`); + }, + unsupportedPuppeteerVersion(value) { + return formatMessage(` +This issue has an outdated Puppeteer version: \`${value}\`. Please verify your issue on the latest \`${LAST_PUPPETEER_VERSION}\` version. Then update the form accordingly. +`); + }, + invalidPuppeteerVersion(value) { + return formatMessage(` +This issue has an invalid Puppeteer version: \`${value}\`. Versions must follow [SemVer](https://semver.org/) formatting. Please update the form with a valid version. +`); + }, +}; + +(async () => { + let input = ''; + // @ts-expect-error: `iterator` is new and experimental. + for await (const chunk of process.stdin.iterator({ + destroyOnReturn: false, + })) { + input += chunk; + } + input = JSON.parse(input).trim(); + + let mvce = ''; + let error = ''; + let configuration = ''; + let puppeteerVersion = ''; + let nodeVersion = ''; + let packageManagerVersion = ''; + let packageManager = ''; + let os = ''; + const behavior = {}; + const lines = input.split('\n'); + { + /** @type {(value: string) => void} */ + let set = () => { + return void 0; + }; + let j = 1; + let i = 1; + for (; i < lines.length; ++i) { + if (lines[i].startsWith('### Bug behavior')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + if (value.match(/\[x\] Flaky/i)) { + behavior.flaky = true; + } + if (value.match(/\[x\] pdf/i)) { + behavior.noError = true; + } + }; + } else if (lines[i].startsWith('### Minimal, reproducible example')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + mvce = value; + }; + } else if (lines[i].startsWith('### Error string')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + if (value.match(/no error/i)) { + behavior.noError = true; + } else { + error = value; + } + }; + } else if (lines[i].startsWith('### Puppeteer configuration')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + configuration = value; + }; + } else if (lines[i].startsWith('### Puppeteer version')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + puppeteerVersion = removeVersionPrefix(value); + }; + } else if (lines[i].startsWith('### Node version')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + nodeVersion = removeVersionPrefix(value); + }; + } else if (lines[i].startsWith('### Package manager version')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + packageManagerVersion = removeVersionPrefix(value); + }; + } else if (lines[i].startsWith('### Package manager')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + packageManager = value.toLowerCase(); + }; + } else if (lines[i].startsWith('### Operating system')) { + set(lines.slice(j, i).join('\n').trim()); + j = i + 1; + set = value => { + os = value.toLowerCase(); + }; + } + } + set(lines.slice(j, i).join('\n').trim()); + } + + let runsOn; + switch (os) { + case 'windows': + runsOn = 'windows-latest'; + break; + case 'macos': + runsOn = 'macos-latest'; + break; + case 'linux': + runsOn = 'ubuntu-latest'; + break; + default: + core.setOutput('errorMessage', ERROR_MESSAGES.unsupportedOs(os)); + core.setFailed(`Unsupported OS: ${os}`); + } + + if (!SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.unsupportedPackageManager(packageManager) + ); + core.setFailed(`Unsupported package manager: ${packageManager}`); + } + + if (!semver.valid(nodeVersion)) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.invalidNodeVersion(nodeVersion) + ); + core.setFailed('Invalid Node version'); + } + if (semver.lt(nodeVersion, LAST_SUPPORTED_NODE_VERSION)) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.unsupportedNodeVersion(nodeVersion) + ); + core.setFailed(`Unsupported node version: ${nodeVersion}`); + } + + if (!semver.valid(puppeteerVersion)) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.invalidPuppeteerVersion(puppeteerVersion) + ); + core.setFailed(`Invalid puppeteer version: ${puppeteerVersion}`); + } + if ( + !LAST_PUPPETEER_VERSION || + semver.lt(puppeteerVersion, LAST_PUPPETEER_VERSION) + ) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.unsupportedPuppeteerVersion(puppeteerVersion) + ); + core.setFailed(`Unsupported puppeteer version: ${puppeteerVersion}`); + } + + if (!semver.valid(packageManagerVersion)) { + core.setOutput( + 'errorMessage', + ERROR_MESSAGES.invalidPackageManagerVersion(packageManagerVersion) + ); + core.setFailed(`Invalid package manager version: ${packageManagerVersion}`); + } + + core.setOutput('errorMessage', ''); + core.setOutput('runsOn', runsOn); + core.setOutput('nodeVersion', nodeVersion); + core.setOutput('packageManager', packageManager); + + await mkdir('out'); + Promise.all([ + writeFile(join('out', 'main.ts'), mvce.split('\n').slice(1, -1).join('\n')), + writeFile(join('out', 'puppeteer-error.txt'), error), + writeFile( + join('out', 'puppeteer.config.js'), + configuration.split('\n').slice(1, -1).join('\n') + ), + writeFile(join('out', 'puppeteer-behavior.json'), JSON.stringify(behavior)), + writeFile( + join('out', 'package.json'), + JSON.stringify({ + packageManager: `${packageManager}@${packageManagerVersion}`, + scripts: { + start: 'tsx main.ts', + verify: 'tsx verify_issue.ts', + }, + dependencies: { + puppeteer: puppeteerVersion, + }, + devDependencies: { + tsx: 'latest', + }, + }) + ), + copyFile( + join( + dirname(fileURLToPath(import.meta.url)), + 'assets', + 'verify_issue.ts' + ), + join('out', 'verify_issue.ts') + ), + ]); +})(); diff --git a/remote/test/puppeteer/tools/assets/verify_issue.ts b/remote/test/puppeteer/tools/assets/verify_issue.ts new file mode 100755 index 0000000000..5814eff66c --- /dev/null +++ b/remote/test/puppeteer/tools/assets/verify_issue.ts @@ -0,0 +1,68 @@ +import {spawnSync} from 'child_process'; +import {readFile, writeFile} from 'fs/promises'; + +(async () => { + const error = await readFile('puppeteer-error.txt', 'utf-8'); + const behavior = JSON.parse( + await readFile('puppeteer-behavior.json', 'utf-8') + ) as {flaky?: boolean; noError?: boolean}; + + let maxRepetitions = 1; + if (behavior.flaky) { + maxRepetitions = 100; + } + + let status: number | null = null; + let stderr = ''; + let stdout = ''; + + const preHook = async () => { + console.log('Writing output and error logs...'); + await Promise.all([ + writeFile('output.log', stdout), + writeFile('error.log', stderr), + ]); + }; + + let checkStatusWithError: () => Promise<void>; + if (behavior.noError) { + checkStatusWithError = async () => { + if (status === 0) { + await preHook(); + console.log('Script ran successfully; no error found.'); + process.exit(0); + } + }; + } else { + checkStatusWithError = async () => { + if (status !== 0) { + await preHook(); + if (stderr.toLowerCase().includes(error.toLowerCase())) { + console.log('Script failed; error found.'); + process.exit(0); + } + console.error('Script failed; unknown error found.'); + process.exit(1); + } + }; + } + + for (let i = 0; i < maxRepetitions; ++i) { + const result = spawnSync('npm', ['start'], { + shell: true, + encoding: 'utf-8', + }); + status = result.status; + stdout = result.stdout ?? ''; + stderr = result.stderr ?? ''; + await checkStatusWithError(); + } + + await preHook(); + if (behavior.noError) { + console.error('Script failed; unknown error found.'); + } else { + console.error('Script ran successfully; no error found.'); + } + process.exit(1); +})(); diff --git a/remote/test/puppeteer/tools/chmod.ts b/remote/test/puppeteer/tools/chmod.ts new file mode 100644 index 0000000000..89afe0f9e7 --- /dev/null +++ b/remote/test/puppeteer/tools/chmod.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +/** + * Calls chmod with the mode in argv[2] on paths in argv[3...length-1]. + */ + +const mode = process.argv[2]; + +for (let i = 3; i < process.argv.length; i++) { + fs.chmodSync(process.argv[i], mode); +} diff --git a/remote/test/puppeteer/tools/cp.ts b/remote/test/puppeteer/tools/cp.ts new file mode 100644 index 0000000000..438c6ccd54 --- /dev/null +++ b/remote/test/puppeteer/tools/cp.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; + +/** + * Copies single file in argv[2] to argv[3] + */ +fs.cpSync(process.argv[2], process.argv[3]); diff --git a/remote/test/puppeteer/tools/ensure-pinned-deps.ts b/remote/test/puppeteer/tools/ensure-pinned-deps.ts new file mode 100644 index 0000000000..82f53df796 --- /dev/null +++ b/remote/test/puppeteer/tools/ensure-pinned-deps.ts @@ -0,0 +1,59 @@ +/** + * 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 {readdirSync, readFileSync} from 'fs'; +import {join} from 'path'; + +import {devDependencies} from '../package.json'; + +const LOCAL_PACKAGE_NAMES: string[] = []; + +const packagesDir = join(__dirname, '..', 'packages'); +for (const packageName of readdirSync(packagesDir)) { + const {name} = JSON.parse( + readFileSync(join(packagesDir, packageName, 'package.json'), 'utf8') + ); + LOCAL_PACKAGE_NAMES.push(name); +} + +const allDeps = {...devDependencies}; + +const invalidDeps = new Map<string, string>(); + +for (const [depKey, depValue] of Object.entries(allDeps)) { + if (LOCAL_PACKAGE_NAMES.includes(depKey)) { + continue; + } + if (/[0-9]/.test(depValue[0]!)) { + continue; + } + + invalidDeps.set(depKey, depValue); +} + +if (invalidDeps.size > 0) { + console.error('Found non-pinned dependencies in package.json:'); + console.log( + [...invalidDeps.keys()] + .map(k => { + return ` ${k}`; + }) + .join('\n') + ); + process.exit(1); +} + +process.exit(0); diff --git a/remote/test/puppeteer/tools/generate-matrix.js b/remote/test/puppeteer/tools/generate-matrix.js new file mode 100644 index 0000000000..1cb65d3491 --- /dev/null +++ b/remote/test/puppeteer/tools/generate-matrix.js @@ -0,0 +1,43 @@ +const fs = require('fs'); + +const data = JSON.parse(fs.readFileSync('./test/TestSuites.json', 'utf-8')); + +/** + * @param {string} platform + * @returns {string} + */ +function mapPlatform(platform) { + switch (platform) { + case 'linux': + return 'ubuntu-latest'; + case 'win32': + return 'windows-latest'; + case 'darwin': + return 'macos-latest'; + default: + throw new Error('Unsupported platform'); + } +} + +const result = []; +for (const suite of data.testSuites) { + for (const platform of suite.platforms) { + if (platform === 'linux' && suite.id !== 'firefox-bidi') { + for (const node of [14, 16, 18]) { + result.push(`- name: ${suite.id} + machine: ${mapPlatform(platform)} + xvfb: true + node: ${node} + suite: ${suite.id}`); + } + } else { + result.push(`- name: ${suite.id} + machine: ${mapPlatform(platform)} + xvfb: ${platform === 'linux'} + node: 18 + suite: ${suite.id}`); + } + } +} + +console.log(result.join('\n')); diff --git a/remote/test/puppeteer/tools/generate_docs.ts b/remote/test/puppeteer/tools/generate_docs.ts new file mode 100644 index 0000000000..86bc92f5f8 --- /dev/null +++ b/remote/test/puppeteer/tools/generate_docs.ts @@ -0,0 +1,152 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {copyFile, readFile, rm, writeFile} from 'fs/promises'; +import {join, resolve} from 'path'; +import {chdir} from 'process'; + +import semver from 'semver'; + +import {generateDocs} from './internal/custom_markdown_action.js'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +chdir(resolve(join(__dirname, '..'))); + +function getOffsetAndLimit( + sectionName: string, + lines: string[] +): [offset: number, limit: number] { + const offset = + lines.findIndex(line => { + return line.includes(`<!-- ${sectionName}-start -->`); + }) + 1; + const limit = lines.slice(offset).findIndex(line => { + return line.includes(`<!-- ${sectionName}-end -->`); + }); + return [offset, limit]; +} + +function spliceIntoSection( + sectionName: string, + content: string, + sectionContent: string +): string { + const lines = content.split('\n'); + const [offset, limit] = getOffsetAndLimit(sectionName, lines); + lines.splice(offset, limit, ...sectionContent.split('\n')); + return lines.join('\n'); +} + +(async () => { + const job1 = job('', async ({inputs, outputs}) => { + await copyFile(inputs[0]!, outputs[0]!); + }) + .inputs(['README.md']) + .outputs(['docs/index.md']) + .build(); + + // Chrome Versions + const job2 = job('', async ({inputs, outputs}) => { + let content = await readFile(inputs[2]!, {encoding: 'utf8'}); + const versionModulePath = join('..', inputs[0]!); + const {versionsPerRelease} = await import(versionModulePath); + const versionsArchived = JSON.parse(await readFile(inputs[1]!, 'utf8')); + + // Generate versions + const buffer: string[] = []; + for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) { + if (puppeteerVersion === 'NEXT') { + continue; + } + if (versionsArchived.includes(puppeteerVersion.substring(1))) { + if (semver.gte(puppeteerVersion, '20.0.0')) { + buffer.push( + ` * [Chrome for Testing](https://goo.gle/chrome-for-testing) ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://pptr.dev/${puppeteerVersion.slice( + 1 + )})` + ); + } else { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api/index.md)` + ); + } + } else if (semver.lt(puppeteerVersion, '15.0.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)` + ); + } else if (semver.gte(puppeteerVersion, '15.3.0')) { + buffer.push( + ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://pptr.dev/${puppeteerVersion.slice( + 1 + )})` + ); + } else { + buffer.push( + ` * Chromium ${chromiumVersion} - Puppeteer ${puppeteerVersion}` + ); + } + } + content = spliceIntoSection('version', content, buffer.join('\n')); + + await writeFile(outputs[0]!, content); + }) + .inputs([ + 'versions.js', + 'website/versionsArchived.json', + 'docs/chromium-support.md', + ]) + .outputs(['docs/chromium-support.md']) + .build(); + + await Promise.all([job1, job2]); + + // Generate documentation + const puppeteerDocs = job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs([ + 'docs/puppeteer.api.json', + 'tools/internal/custom_markdown_documenter.ts', + ]) + .outputs(['docs/api']) + .build(); + + const browsersDocs = job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs([ + 'docs/browsers.api.json', + 'tools/internal/custom_markdown_documenter.ts', + ]) + .outputs(['docs/browsers-api']) + .build(); + + await Promise.all([puppeteerDocs, browsersDocs]); + + await job('', async ({inputs, outputs}) => { + const readme = await readFile(inputs[1]!, 'utf-8'); + const index = await readFile(inputs[0]!, 'utf-8'); + await writeFile(outputs[0]!, index.replace('# API Reference\n', readme)); + }) + .inputs(['docs/browsers-api/index.md', 'packages/browsers/README.md']) + .outputs(['docs/browsers-api/index.md']) + .build(); +})(); diff --git a/remote/test/puppeteer/tools/generate_module_package_json.ts b/remote/test/puppeteer/tools/generate_module_package_json.ts new file mode 100644 index 0000000000..75737463d5 --- /dev/null +++ b/remote/test/puppeteer/tools/generate_module_package_json.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {mkdirSync, writeFileSync} from 'fs'; +import {dirname} from 'path'; + +/** + * Outputs the dummy package.json file to the path specified + * by the first argument. + */ + +mkdirSync(dirname(process.argv[2]), {recursive: true}); +writeFileSync(process.argv[2], `{"type": "module"}`); diff --git a/remote/test/puppeteer/tools/get_deprecated_version_range.js b/remote/test/puppeteer/tools/get_deprecated_version_range.js new file mode 100644 index 0000000000..c817df61bf --- /dev/null +++ b/remote/test/puppeteer/tools/get_deprecated_version_range.js @@ -0,0 +1,27 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { + versionsPerRelease, + lastMaintainedChromiumVersion, +} = require('../versions.js'); +const version = versionsPerRelease.get(lastMaintainedChromiumVersion); +if (version.toLowerCase() === 'next') { + console.error('Unexpected NEXT Puppeteer version in versions.js'); + process.exit(1); +} +console.log(`< ${version.substring(1)}`); +process.exit(0); diff --git a/remote/test/puppeteer/tools/internal/custom_markdown_action.ts b/remote/test/puppeteer/tools/internal/custom_markdown_action.ts new file mode 100644 index 0000000000..d8b3228736 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/custom_markdown_action.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ApiModel} from '@microsoft/api-extractor-model'; + +import {MarkdownDocumenter} from './custom_markdown_documenter.js'; + +export const generateDocs = (jsonPath: string, outputDir: string): void => { + const apiModel = new ApiModel(); + apiModel.loadPackage(jsonPath); + + const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter({ + apiModel: apiModel, + documenterConfig: undefined, + outputFolder: outputDir, + }); + markdownDocumenter.generateFiles(); +}; diff --git a/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts b/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts new file mode 100644 index 0000000000..366dde1d55 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/custom_markdown_documenter.ts @@ -0,0 +1,1481 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the +// MIT license. See LICENSE in the project root for license information. + +// Taken from +// https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/src/documenters/MarkdownDocumenter.ts +// This file has been edited to morph into Docusaurus's expected inputs. + +import * as path from 'path'; + +import {DocumenterConfig} from '@microsoft/api-documenter/lib/documenters/DocumenterConfig'; +import {CustomMarkdownEmitter} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; +import {CustomDocNodes} from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind'; +import {DocEmphasisSpan} from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan'; +import {DocHeading} from '@microsoft/api-documenter/lib/nodes/DocHeading'; +import {DocNoteBox} from '@microsoft/api-documenter/lib/nodes/DocNoteBox'; +import {DocTable} from '@microsoft/api-documenter/lib/nodes/DocTable'; +import {DocTableCell} from '@microsoft/api-documenter/lib/nodes/DocTableCell'; +import {DocTableRow} from '@microsoft/api-documenter/lib/nodes/DocTableRow'; +import {MarkdownDocumenterAccessor} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterAccessor'; +import { + IMarkdownDocumenterFeatureOnBeforeWritePageArgs, + MarkdownDocumenterFeatureContext, +} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterFeature'; +import {PluginLoader} from '@microsoft/api-documenter/lib/plugin/PluginLoader'; +import {Utilities} from '@microsoft/api-documenter/lib/utils/Utilities'; +import { + ApiClass, + ApiDeclaredItem, + ApiDocumentedItem, + ApiEnum, + ApiInitializerMixin, + ApiInterface, + ApiItem, + ApiItemKind, + ApiModel, + ApiNamespace, + ApiOptionalMixin, + ApiPackage, + ApiParameterListMixin, + ApiPropertyItem, + ApiProtectedMixin, + ApiReadonlyMixin, + ApiReleaseTagMixin, + ApiReturnTypeMixin, + ApiStaticMixin, + ApiTypeAlias, + Excerpt, + ExcerptToken, + ExcerptTokenKind, + IResolveDeclarationReferenceResult, + ReleaseTag, +} from '@microsoft/api-extractor-model'; +import { + DocBlock, + DocCodeSpan, + DocComment, + DocFencedCode, + DocLinkTag, + DocNodeContainer, + DocNodeKind, + DocParagraph, + DocPlainText, + DocSection, + StandardTags, + StringBuilder, + TSDocConfiguration, +} from '@microsoft/tsdoc'; +import { + FileSystem, + NewlineKind, + PackageName, +} from '@rushstack/node-core-library'; + +export interface IMarkdownDocumenterOptions { + apiModel: ApiModel; + documenterConfig: DocumenterConfig | undefined; + outputFolder: string; +} + +/** + * Renders API documentation in the Markdown file format. + * For more info: https://en.wikipedia.org/wiki/Markdown + */ +export class MarkdownDocumenter { + private readonly _apiModel: ApiModel; + private readonly _documenterConfig: DocumenterConfig | undefined; + private readonly _tsdocConfiguration: TSDocConfiguration; + private readonly _markdownEmitter: CustomMarkdownEmitter; + private readonly _outputFolder: string; + private readonly _pluginLoader: PluginLoader; + + public constructor(options: IMarkdownDocumenterOptions) { + this._apiModel = options.apiModel; + this._documenterConfig = options.documenterConfig; + this._outputFolder = options.outputFolder; + this._tsdocConfiguration = CustomDocNodes.configuration; + this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); + + this._pluginLoader = new PluginLoader(); + } + + public generateFiles(): void { + if (this._documenterConfig) { + this._pluginLoader.load(this._documenterConfig, () => { + return new MarkdownDocumenterFeatureContext({ + apiModel: this._apiModel, + outputFolder: this._outputFolder, + documenter: new MarkdownDocumenterAccessor({ + getLinkForApiItem: (apiItem: ApiItem) => { + console.log(apiItem); + return this._getLinkFilenameForApiItem(apiItem); + }, + }), + }); + }); + } + + console.log(); + this._deleteOldOutputFiles(); + + this._writeApiItemPage(this._apiModel.members[0]!); + + if (this._pluginLoader.markdownDocumenterFeature) { + this._pluginLoader.markdownDocumenterFeature.onFinished({}); + } + } + + private _writeApiItemPage(apiItem: ApiItem): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const output: DocSection = new DocSection({ + configuration: this._tsdocConfiguration, + }); + + const scopedName: string = apiItem.getScopedNameWithinPackage(); + + switch (apiItem.kind) { + case ApiItemKind.Class: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} class`}) + ); + break; + case ApiItemKind.Enum: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} enum`}) + ); + break; + case ApiItemKind.Interface: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} interface`}) + ); + break; + case ApiItemKind.Constructor: + case ApiItemKind.ConstructSignature: + output.appendNode(new DocHeading({configuration, title: scopedName})); + break; + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} method`}) + ); + break; + case ApiItemKind.Function: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} function`}) + ); + break; + case ApiItemKind.Model: + output.appendNode( + new DocHeading({configuration, title: `API Reference`}) + ); + break; + case ApiItemKind.Namespace: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} namespace`}) + ); + break; + case ApiItemKind.Package: + console.log(`Writing ${apiItem.displayName} package`); + output.appendNode( + new DocHeading({ + configuration, + title: `API Reference`, + }) + ); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} property`}) + ); + break; + case ApiItemKind.TypeAlias: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} type`}) + ); + break; + case ApiItemKind.Variable: + output.appendNode( + new DocHeading({configuration, title: `${scopedName} variable`}) + ); + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + this._writeBetaWarning(output); + } + } + + const decoratorBlocks: DocBlock[] = []; + + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + decoratorBlocks.push( + ...tsdocComment.customBlocks.filter(block => { + return ( + block.blockTag.tagNameWithUpperCase === + StandardTags.decorator.tagNameWithUpperCase + ); + }) + ); + + if (tsdocComment.deprecatedBlock) { + output.appendNode( + new DocNoteBox({configuration: this._tsdocConfiguration}, [ + new DocParagraph({configuration: this._tsdocConfiguration}, [ + new DocPlainText({ + configuration: this._tsdocConfiguration, + text: 'Warning: This API is now obsolete. ', + }), + ]), + ...tsdocComment.deprecatedBlock.content.nodes, + ]) + ); + } + + this._appendSection(output, tsdocComment.summarySection); + } + } + + if (apiItem instanceof ApiDeclaredItem) { + if (apiItem.excerpt.text.length > 0) { + output.appendNode( + new DocHeading({configuration, title: 'Signature:', level: 4}) + ); + + let code: string; + switch (apiItem.parent?.kind) { + case ApiItemKind.Class: + code = `class ${ + apiItem.parent.displayName + } {${apiItem.getExcerptWithModifiers()}}`; + break; + case ApiItemKind.Interface: + code = `interface ${ + apiItem.parent.displayName + } {${apiItem.getExcerptWithModifiers()}}`; + break; + default: + code = apiItem.getExcerptWithModifiers(); + } + output.appendNode( + new DocFencedCode({ + configuration, + code: code, + language: 'typescript', + }) + ); + } + + this._writeHeritageTypes(output, apiItem); + } + + if (decoratorBlocks.length > 0) { + output.appendNode( + new DocHeading({configuration, title: 'Decorators:', level: 4}) + ); + for (const decoratorBlock of decoratorBlocks) { + output.appendNodes(decoratorBlock.content.nodes); + } + } + + let appendRemarks = true; + switch (apiItem.kind) { + case ApiItemKind.Class: + case ApiItemKind.Interface: + case ApiItemKind.Namespace: + case ApiItemKind.Package: + this._writeRemarksSection(output, apiItem); + appendRemarks = false; + break; + } + + switch (apiItem.kind) { + case ApiItemKind.Class: + this._writeClassTables(output, apiItem as ApiClass); + break; + case ApiItemKind.Enum: + this._writeEnumTables(output, apiItem as ApiEnum); + break; + case ApiItemKind.Interface: + this._writeInterfaceTables(output, apiItem as ApiInterface); + break; + case ApiItemKind.Constructor: + case ApiItemKind.ConstructSignature: + case ApiItemKind.Method: + case ApiItemKind.MethodSignature: + case ApiItemKind.Function: + this._writeParameterTables(output, apiItem as ApiParameterListMixin); + this._writeThrowsSection(output, apiItem); + break; + case ApiItemKind.Namespace: + this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace); + break; + case ApiItemKind.Model: + this._writeModelTable(output, apiItem as ApiModel); + break; + case ApiItemKind.Package: + this._writePackageOrNamespaceTables(output, apiItem as ApiPackage); + break; + case ApiItemKind.Property: + case ApiItemKind.PropertySignature: + break; + case ApiItemKind.TypeAlias: + break; + case ApiItemKind.Variable: + break; + default: + throw new Error('Unsupported API item kind: ' + apiItem.kind); + } + + this._writeDefaultValueSection(output, apiItem); + + if (appendRemarks) { + this._writeRemarksSection(output, apiItem); + } + + const filename: string = path.join( + this._outputFolder, + this._getFilenameForApiItem(apiItem) + ); + const stringBuilder: StringBuilder = new StringBuilder(); + + this._markdownEmitter.emit(stringBuilder, output, { + contextApiItem: apiItem, + onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { + return this._getLinkFilenameForApiItem(apiItemForFilename); + }, + }); + + let pageContent: string = stringBuilder.toString(); + + if (this._pluginLoader.markdownDocumenterFeature) { + // Allow the plugin to customize the pageContent + const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = { + apiItem: apiItem, + outputFilename: filename, + pageContent: pageContent, + }; + this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs); + pageContent = eventArgs.pageContent; + } + + pageContent = + `---\nsidebar_label: ${this._getSidebarLabelForApiItem(apiItem)}\n---` + + pageContent; + pageContent = pageContent.replace('##', '#'); + pageContent = pageContent.replace(/<!-- -->/g, ''); + pageContent = pageContent.replace(/\\\*\\\*/g, '**'); + pageContent = pageContent.replace(/<b>|<\/b>/g, '**'); + FileSystem.writeFile(filename, pageContent, { + convertLineEndings: this._documenterConfig + ? this._documenterConfig.newlineKind + : NewlineKind.CrLf, + }); + } + + private _writeHeritageTypes( + output: DocSection, + apiItem: ApiDeclaredItem + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + if (apiItem instanceof ApiClass) { + if (apiItem.extendsType) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Extends: '}), + ]), + ] + ); + this._appendExcerptWithHyperlinks( + extendsParagraph, + apiItem.extendsType.excerpt + ); + output.appendNode(extendsParagraph); + } + if (apiItem.implementsTypes.length > 0) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Implements: '}), + ]), + ] + ); + let needsComma = false; + for (const implementsType of apiItem.implementsTypes) { + if (needsComma) { + extendsParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + this._appendExcerptWithHyperlinks( + extendsParagraph, + implementsType.excerpt + ); + needsComma = true; + } + output.appendNode(extendsParagraph); + } + } + + if (apiItem instanceof ApiInterface) { + if (apiItem.extendsTypes.length > 0) { + const extendsParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Extends: '}), + ]), + ] + ); + let needsComma = false; + for (const extendsType of apiItem.extendsTypes) { + if (needsComma) { + extendsParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + this._appendExcerptWithHyperlinks( + extendsParagraph, + extendsType.excerpt + ); + needsComma = true; + } + output.appendNode(extendsParagraph); + } + } + + if (apiItem instanceof ApiTypeAlias) { + const refs: ExcerptToken[] = apiItem.excerptTokens.filter(token => { + return ( + token.kind === ExcerptTokenKind.Reference && + token.canonicalReference && + this._apiModel.resolveDeclarationReference( + token.canonicalReference, + undefined + ).resolvedApiItem + ); + }); + if (refs.length > 0) { + const referencesParagraph: DocParagraph = new DocParagraph( + {configuration}, + [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'References: '}), + ]), + ] + ); + let needsComma = false; + const visited: Set<string> = new Set(); + for (const ref of refs) { + if (visited.has(ref.text)) { + continue; + } + visited.add(ref.text); + + if (needsComma) { + referencesParagraph.appendNode( + new DocPlainText({configuration, text: ', '}) + ); + } + + this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref); + needsComma = true; + } + output.appendNode(referencesParagraph); + } + } + } + + private _writeDefaultValueSection(output: DocSection, apiItem: ApiItem) { + if (apiItem instanceof ApiDocumentedItem) { + const block = apiItem.tsdocComment?.customBlocks.find(block => { + return ( + block.blockTag.tagNameWithUpperCase === + StandardTags.defaultValue.tagNameWithUpperCase + ); + }); + if (block) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Default value:', + level: 4, + }) + ); + this._appendSection(output, block.content); + } + } + } + + private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void { + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + // Write the @remarks block + if (tsdocComment.remarksBlock) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Remarks', + }) + ); + this._appendSection(output, tsdocComment.remarksBlock.content); + } + + // Write the @example blocks + const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter( + x => { + return ( + x.blockTag.tagNameWithUpperCase === + StandardTags.example.tagNameWithUpperCase + ); + } + ); + + let exampleNumber = 1; + for (const exampleBlock of exampleBlocks) { + const heading: string = + exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example'; + + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: heading, + }) + ); + + this._appendSection(output, exampleBlock.content); + + ++exampleNumber; + } + } + } + } + + private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void { + if (apiItem instanceof ApiDocumentedItem) { + const tsdocComment: DocComment | undefined = apiItem.tsdocComment; + + if (tsdocComment) { + // Write the @throws blocks + const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter(x => { + return ( + x.blockTag.tagNameWithUpperCase === + StandardTags.throws.tagNameWithUpperCase + ); + }); + + if (throwsBlocks.length > 0) { + const heading = 'Exceptions'; + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: heading, + }) + ); + + for (const throwsBlock of throwsBlocks) { + this._appendSection(output, throwsBlock.content); + } + } + } + } + } + + /** + * GENERATE PAGE: MODEL + */ + private _writeModelTable(output: DocSection, apiModel: ApiModel): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const packagesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Package', 'Description'], + }); + + for (const apiMember of apiModel.members) { + const row: DocTableRow = new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]); + + switch (apiMember.kind) { + case ApiItemKind.Package: + packagesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + } + } + + if (packagesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Packages', + }) + ); + output.appendNode(packagesTable); + } + } + + /** + * GENERATE PAGE: PACKAGE or NAMESPACE + */ + private _writePackageOrNamespaceTables( + output: DocSection, + apiContainer: ApiPackage | ApiNamespace + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const classesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Class', 'Description'], + }); + + const enumerationsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Enumeration', 'Description'], + }); + + const functionsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Function', 'Description'], + }); + + const interfacesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Interface', 'Description'], + }); + + const namespacesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Namespace', 'Description'], + }); + + const variablesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Variable', 'Description'], + }); + + const typeAliasesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Type Alias', 'Description'], + }); + + const apiMembers: readonly ApiItem[] = + apiContainer.kind === ApiItemKind.Package + ? (apiContainer as ApiPackage).entryPoints[0]!.members + : (apiContainer as ApiNamespace).members; + + for (const apiMember of apiMembers) { + const row: DocTableRow = new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]); + + switch (apiMember.kind) { + case ApiItemKind.Class: + classesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Enum: + enumerationsTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Interface: + interfacesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Namespace: + namespacesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Function: + functionsTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.TypeAlias: + typeAliasesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + + case ApiItemKind.Variable: + variablesTable.addRow(row); + this._writeApiItemPage(apiMember); + break; + } + } + + if (classesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Classes', + }) + ); + output.appendNode(classesTable); + } + + if (enumerationsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Enumerations', + }) + ); + output.appendNode(enumerationsTable); + } + if (functionsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Functions', + }) + ); + output.appendNode(functionsTable); + } + + if (interfacesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Interfaces', + }) + ); + output.appendNode(interfacesTable); + } + + if (namespacesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Namespaces', + }) + ); + output.appendNode(namespacesTable); + } + + if (variablesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Variables', + }) + ); + output.appendNode(variablesTable); + } + + if (typeAliasesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Type Aliases', + }) + ); + output.appendNode(typeAliasesTable); + } + } + + /** + * GENERATE PAGE: CLASS + */ + private _writeClassTables(output: DocSection, apiClass: ApiClass): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const constructorsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Constructor', 'Modifiers', 'Description'], + }); + + const propertiesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const methodsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Method', 'Modifiers', 'Description'], + }); + + for (const apiMember of apiClass.members) { + switch (apiMember.kind) { + case ApiItemKind.Constructor: { + constructorsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.Method: { + methodsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.Property: { + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember, true), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember, true), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } + break; + } + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Events', + }) + ); + output.appendNode(eventsTable); + } + + if (constructorsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Constructors', + }) + ); + output.appendNode(constructorsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Properties', + }) + ); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Methods', + }) + ); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: ENUM + */ + private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const enumMembersTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Member', 'Value', 'Description'], + }); + + for (const apiEnumMember of apiEnum.members) { + enumMembersTable.addRow( + new DocTableRow({configuration}, [ + new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({ + configuration, + text: Utilities.getConciseSignature(apiEnumMember), + }), + ]), + ]), + this._createInitializerCell(apiEnumMember), + this._createDescriptionCell(apiEnumMember), + ]) + ); + } + + if (enumMembersTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Enumeration Members', + }) + ); + output.appendNode(enumMembersTable); + } + } + + /** + * GENERATE PAGE: INTERFACE + */ + private _writeInterfaceTables( + output: DocSection, + apiClass: ApiInterface + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const eventsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], + }); + + const propertiesTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Property', 'Modifiers', 'Type', 'Description', 'Default'], + }); + + const methodsTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Method', 'Description'], + }); + + for (const apiMember of apiClass.members) { + switch (apiMember.kind) { + case ApiItemKind.ConstructSignature: + case ApiItemKind.MethodSignature: { + methodsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + + this._writeApiItemPage(apiMember); + break; + } + case ApiItemKind.PropertySignature: { + if ((apiMember as ApiPropertyItem).isEventProperty) { + eventsTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember, true), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember, true), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + this._createDefaultCell(apiMember), + ]) + ); + } + break; + } + } + } + + if (eventsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Events', + }) + ); + output.appendNode(eventsTable); + } + + if (propertiesTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Properties', + }) + ); + output.appendNode(propertiesTable); + } + + if (methodsTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Methods', + }) + ); + output.appendNode(methodsTable); + } + } + + /** + * GENERATE PAGE: FUNCTION-LIKE + */ + private _writeParameterTables( + output: DocSection, + apiParameterListMixin: ApiParameterListMixin + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const parametersTable: DocTable = new DocTable({ + configuration, + headerTitles: ['Parameter', 'Type', 'Description'], + }); + for (const apiParameter of apiParameterListMixin.parameters) { + const parameterDescription: DocSection = new DocSection({configuration}); + + if (apiParameter.isOptional) { + parameterDescription.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, italic: true}, [ + new DocPlainText({configuration, text: '(Optional)'}), + ]), + new DocPlainText({configuration, text: ' '}), + ]); + } + + if (apiParameter.tsdocParamBlock) { + this._appendAndMergeSection( + parameterDescription, + apiParameter.tsdocParamBlock.content + ); + } + + parametersTable.addRow( + new DocTableRow({configuration}, [ + new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({configuration, text: apiParameter.name}), + ]), + ]), + new DocTableCell({configuration}, [ + this._createParagraphForTypeExcerpt( + apiParameter.parameterTypeExcerpt + ), + ]), + new DocTableCell({configuration}, parameterDescription.nodes), + ]) + ); + } + + if (parametersTable.rows.length > 0) { + output.appendNode( + new DocHeading({ + configuration: this._tsdocConfiguration, + title: 'Parameters', + }) + ); + output.appendNode(parametersTable); + } + + if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) { + const returnTypeExcerpt: Excerpt = + apiParameterListMixin.returnTypeExcerpt; + output.appendNode( + new DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Returns:'}), + ]), + ]) + ); + + output.appendNode(this._createParagraphForTypeExcerpt(returnTypeExcerpt)); + + if (apiParameterListMixin instanceof ApiDocumentedItem) { + if ( + apiParameterListMixin.tsdocComment && + apiParameterListMixin.tsdocComment.returnsBlock + ) { + this._appendSection( + output, + apiParameterListMixin.tsdocComment.returnsBlock.content + ); + } + } + } + } + + private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const paragraph: DocParagraph = new DocParagraph({configuration}); + if (!excerpt.text.trim()) { + paragraph.appendNode( + new DocPlainText({configuration, text: '(not declared)'}) + ); + } else { + this._appendExcerptWithHyperlinks(paragraph, excerpt); + } + + return paragraph; + } + + private _appendExcerptWithHyperlinks( + docNodeContainer: DocNodeContainer, + excerpt: Excerpt + ): void { + for (const token of excerpt.spannedTokens) { + this._appendExcerptTokenWithHyperlinks(docNodeContainer, token); + } + } + + private _appendExcerptTokenWithHyperlinks( + docNodeContainer: DocNodeContainer, + token: ExcerptToken + ): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + // Markdown doesn't provide a standardized syntax for hyperlinks inside code + // spans, so we will render the type expression as DocPlainText. Instead of + // creating multiple DocParagraphs, we can simply discard any newlines and + // let the renderer do normal word-wrapping. + const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' '); + + // If it's hyperlinkable, then append a DocLinkTag + if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) { + const apiItemResult: IResolveDeclarationReferenceResult = + this._apiModel.resolveDeclarationReference( + token.canonicalReference, + undefined + ); + + if (apiItemResult.resolvedApiItem) { + docNodeContainer.appendNode( + new DocLinkTag({ + configuration, + tagName: StandardTags.link.tagName, + linkText: unwrappedTokenText, + urlDestination: this._getLinkFilenameForApiItem( + apiItemResult.resolvedApiItem + ), + }) + ); + return; + } + } + + // Otherwise append non-hyperlinked text + docNodeContainer.appendNode( + new DocPlainText({configuration, text: unwrappedTokenText}) + ); + } + + private _createTitleCell(apiItem: ApiItem, plain = false): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const text: string = Utilities.getConciseSignature(apiItem); + + return new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + plain + ? new DocPlainText({configuration, text}) + : new DocLinkTag({ + configuration, + tagName: '@link', + linkText: text, + urlDestination: this._getLinkFilenameForApiItem(apiItem), + }), + ]), + ]); + } + + /** + * This generates a DocTableCell for an ApiItem including the summary section + * and "(BETA)" annotation. + * + * @remarks + * We mostly assume that the input is an ApiDocumentedItem, but it's easier to + * perform this as a runtime check than to have each caller perform a type + * cast. + */ + private _createDescriptionCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { + if (apiItem.releaseTag === ReleaseTag.Beta) { + section.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, bold: true, italic: true}, [ + new DocPlainText({configuration, text: '(BETA)'}), + ]), + new DocPlainText({configuration, text: ' '}), + ]); + } + } + + if (apiItem instanceof ApiDocumentedItem) { + if (apiItem.tsdocComment !== undefined) { + this._appendAndMergeSection( + section, + apiItem.tsdocComment.summarySection + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createDefaultCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + if (apiItem instanceof ApiDocumentedItem) { + const block = apiItem.tsdocComment?.customBlocks.find(block => { + return ( + block.blockTag.tagNameWithUpperCase === + StandardTags.defaultValue.tagNameWithUpperCase + ); + }); + if (block !== undefined) { + return new DocTableCell({configuration}, block.content.getChildNodes()); + } + } + + return new DocTableCell({configuration}, []); + } + + private _createModifiersCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiProtectedMixin.isBaseClassOf(apiItem)) { + if (apiItem.isProtected) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'protected'}), + ]) + ); + } + } + + if (ApiReadonlyMixin.isBaseClassOf(apiItem)) { + if (apiItem.isReadonly) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'readonly'}), + ]) + ); + } + } + + if (ApiStaticMixin.isBaseClassOf(apiItem)) { + if (apiItem.isStatic) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'static'}), + ]) + ); + } + } + + if (ApiOptionalMixin.isBaseClassOf(apiItem)) { + if (apiItem.isOptional) { + section.appendNode( + new DocParagraph({configuration}, [ + new DocCodeSpan({configuration, code: 'optional'}), + ]) + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (apiItem instanceof ApiPropertyItem) { + section.appendNode( + this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt) + ); + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _createInitializerCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + const section: DocSection = new DocSection({configuration}); + + if (ApiInitializerMixin.isBaseClassOf(apiItem)) { + if (apiItem.initializerExcerpt) { + section.appendNodeInParagraph( + new DocCodeSpan({ + configuration, + code: apiItem.initializerExcerpt.text, + }) + ); + } + } + + return new DocTableCell({configuration}, section.nodes); + } + + private _writeBetaWarning(output: DocSection): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const betaWarning: string = + 'This API is provided as a preview for developers and may change' + + ' based on feedback that we receive. Do not use this API in a production environment.'; + output.appendNode( + new DocNoteBox({configuration}, [ + new DocParagraph({configuration}, [ + new DocPlainText({configuration, text: betaWarning}), + ]), + ]) + ); + } + + private _appendSection(output: DocSection, docSection: DocSection): void { + for (const node of docSection.nodes) { + output.appendNode(node); + } + } + + private _appendAndMergeSection( + output: DocSection, + docSection: DocSection + ): void { + let firstNode = true; + for (const node of docSection.nodes) { + if (firstNode) { + if (node.kind === DocNodeKind.Paragraph) { + output.appendNodesInParagraph(node.getChildNodes()); + firstNode = false; + continue; + } + } + firstNode = false; + + output.appendNode(node); + } + } + + private _getSidebarLabelForApiItem(apiItem: ApiItem): string { + if (apiItem.kind === ApiItemKind.Package) { + return 'API'; + } + + let baseName = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = hierarchyItem.displayName; + if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 1) { + // Subtract one for compatibility with earlier releases of API Documenter. + qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + case ApiItemKind.EnumMember: + case ApiItemKind.Package: + break; + default: + baseName += qualifiedName + '.'; + } + } + return baseName.slice(0, baseName.length - 1); + } + + private _getFilenameForApiItem(apiItem: ApiItem): string { + if (apiItem.kind === ApiItemKind.Package) { + return 'index.md'; + } + + let baseName = ''; + for (const hierarchyItem of apiItem.getHierarchy()) { + // For overloaded methods, add a suffix such as "MyClass.myMethod_2". + let qualifiedName: string = Utilities.getSafeFilenameForName( + hierarchyItem.displayName + ); + if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { + if (hierarchyItem.overloadIndex > 1) { + // Subtract one for compatibility with earlier releases of API Documenter. + // (This will get revamped when we fix GitHub issue #1308) + qualifiedName += `_${hierarchyItem.overloadIndex - 1}`; + } + } + + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + case ApiItemKind.EnumMember: + break; + case ApiItemKind.Package: + baseName = Utilities.getSafeFilenameForName( + PackageName.getUnscopedName(hierarchyItem.displayName) + ); + break; + default: + baseName += '.' + qualifiedName; + } + } + return baseName + '.md'; + } + + private _getLinkFilenameForApiItem(apiItem: ApiItem): string { + return './' + this._getFilenameForApiItem(apiItem); + } + + private _deleteOldOutputFiles(): void { + console.log('Deleting old output from ' + this._outputFolder); + FileSystem.ensureEmptyFolder(this._outputFolder); + } +} diff --git a/remote/test/puppeteer/tools/internal/job.ts b/remote/test/puppeteer/tools/internal/job.ts new file mode 100644 index 0000000000..ef6ea10237 --- /dev/null +++ b/remote/test/puppeteer/tools/internal/job.ts @@ -0,0 +1,153 @@ +import {createHash} from 'crypto'; +import {existsSync, Stats} from 'fs'; +import {mkdir, readFile, stat, writeFile} from 'fs/promises'; +import {tmpdir} from 'os'; +import {dirname, join} from 'path'; + +import glob from 'glob'; + +interface JobContext { + name: string; + inputs: string[]; + outputs: string[]; +} + +class JobBuilder { + #inputs: string[] = []; + #outputs: string[] = []; + #callback: (ctx: JobContext) => Promise<void>; + #name: string; + #value = ''; + #force = false; + + constructor(name: string, callback: (ctx: JobContext) => Promise<void>) { + this.#name = name; + this.#callback = callback; + } + + get jobHash(): string { + return createHash('sha256').update(this.#name).digest('hex'); + } + + force() { + this.#force = true; + return this; + } + + value(value: string) { + this.#value = value; + return this; + } + + inputs(inputs: string[]): JobBuilder { + this.#inputs = inputs.flatMap(value => { + if (glob.hasMagic(value)) { + return glob.sync(value); + } + return value; + }); + return this; + } + + outputs(outputs: string[]): JobBuilder { + if (!this.#name) { + this.#name = outputs.join(' and '); + } + + this.#outputs = outputs; + return this; + } + + async build(): Promise<void> { + console.log(`Running job ${this.#name}...`); + // For debugging. + if (this.#force) { + return this.#run(); + } + // In case we deleted an output file on purpose. + if (!this.getOutputStats()) { + return this.#run(); + } + // Run if the job has a value, but it changes. + if (this.#value) { + if (!(await this.isValueDifferent())) { + return; + } + return this.#run(); + } + // Always run when there is no output. + if (!this.#outputs.length) { + return this.#run(); + } + // Make-like comparator. + if (!(await this.areInputsNewer())) { + return; + } + return this.#run(); + } + + async isValueDifferent(): Promise<boolean> { + const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`); + await mkdir(dirname(file), {recursive: true}); + if (!existsSync(file)) { + await writeFile(file, this.#value); + return true; + } + return this.#value !== (await readFile(file, 'utf8')); + } + + #outputStats?: Stats[]; + async getOutputStats(): Promise<Stats[] | undefined> { + if (this.#outputStats) { + return this.#outputStats; + } + try { + this.#outputStats = await Promise.all( + this.#outputs.map(output => { + return stat(output); + }) + ); + } catch {} + return this.#outputStats; + } + + async areInputsNewer(): Promise<boolean> { + const inputStats = await Promise.all( + this.#inputs.map(input => { + return stat(input); + }) + ); + const outputStats = await this.getOutputStats(); + if ( + outputStats && + outputStats.reduce(reduceMinTime, Infinity) > + inputStats.reduce(reduceMaxTime, 0) + ) { + return false; + } + return true; + } + + #run(): Promise<void> { + return this.#callback({ + name: this.#name, + inputs: this.#inputs, + outputs: this.#outputs, + }); + } +} + +export const job = ( + name: string, + callback: (ctx: JobContext) => Promise<void> +): JobBuilder => { + return new JobBuilder(name, callback); +}; + +const reduceMaxTime = (time: number, stat: Stats) => { + return time < stat.mtimeMs ? stat.mtimeMs : time; +}; + +const reduceMinTime = (time: number, stat: Stats) => { + return time > stat.mtimeMs ? stat.mtimeMs : time; +}; diff --git a/remote/test/puppeteer/tools/internal/util.ts b/remote/test/puppeteer/tools/internal/util.ts new file mode 100644 index 0000000000..4ebbe8b86b --- /dev/null +++ b/remote/test/puppeteer/tools/internal/util.ts @@ -0,0 +1,14 @@ +import {spawnSync} from 'child_process'; + +export const spawnAndLog = (...args: string[]): void => { + const {stdout, stderr} = spawnSync(args[0]!, args.slice(1), { + encoding: 'utf-8', + shell: true, + }); + if (stdout) { + console.log(stdout); + } + if (stderr) { + console.error(stderr); + } +}; diff --git a/remote/test/puppeteer/tools/mochaRunner/README.md b/remote/test/puppeteer/tools/mochaRunner/README.md new file mode 100644 index 0000000000..1e4398a63c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/README.md @@ -0,0 +1,73 @@ +# Mocha Runner + +Mocha Runner is a test runner on top of mocha. +It uses `/test/TestSuites.json` and `/test/TestExpectations.json` files to run mocha tests in multiple configurations and interpret results. + +## Running tests for Mocha Runner itself. + +```bash +npm run build && npx c8 node tools/mochaRunner/lib/test.js +``` + +## Running tests using Mocha Runner + +```bash +npm run build && npm run test +``` + +By default, the runner runs all test suites applicable to the current platform. +To pick a test suite, provide the `--test-suite` arguments. For example, + +```bash +npm run build && npm run test -- --test-suite chrome-headless +``` + +## TestSuites.json + +Define test suites via the `testSuites` attribute. `parameters` can be used in the `TestExpectations.json` to disable tests +based on parameters. The meaning for parameters is defined in `parameterDefinitions` which tell what env object corresponds +to the given parameter. + +## TestExpectations.json + +An expectation looks like this: + +```json +{ + "testIdPattern": "[accessibility.spec]", + "platforms": ["darwin", "win32", "linux"], + "parameters": ["firefox"], + "expectations": ["SKIP"] +} +``` + +| Field | Description | Type | Match Logic | +| --------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------- | +| `testIdPattern` | Defines the full name (or pattern) to match against test name | string | - | +| `platforms` | Defines the platforms the expectation is for | Array<`linux` \| `win32` \|`darwin`> | `OR` | +| `parameters` | Defines the parameters that the test has to match | Array<[ParameterDefinitions](https://github.com/puppeteer/puppeteer/blob/main/test/TestSuites.json)> | `AND` | +| `expectations` | The list of test results that are considered to be acceptable | Array<`PASS` \| `FAIL` \| `TIMEOUT` \| `SKIP`> | `OR` | + +> Order of defining expectations matters. The latest expectation that is set will take president over earlier ones. + +> Adding `SKIP` to `expectations` will prevent the test from running, no matter if there are other expectations. + +### Using pattern in `testIdPattern` + +Sometimes we want a whole group of test to run. For that we can use a +pattern to achieve. +Pattern are defined with the use of `*` (using greedy method). + +Examples: +| Pattern | Description | Example Pattern | Example match | +|------------------------|---------------------------------------------------------------------------------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| `*` | Match all tests | - | - | +| `[test.spec] *` | Matches tests for the given file | `[jshandle.spec] *` | `[jshandle] JSHandle JSHandle.toString should work for primitives` | +| `[test.spec] <text> *` | Matches tests with for a given test with a specific prefixed test (usually a describe node) | `[page.spec] Page Page.goto *` | `[page.spec] Page Page.goto should work`,<br>`[page.spec] Page Page.goto should work with anchor navigation` | +| `[test.spec] * <text>` | Matches test with a surfix | `[navigation.spec] * should work` | `[navigation.spec] navigation Page.goto should work`,<br>`[navigation.spec] navigation Page.waitForNavigation should work` | + +## Updating Expectations + +Currently, expectations are updated manually. The test runner outputs the +suggested changes to the expectation file if the test run does not match +expectations. diff --git a/remote/test/puppeteer/tools/mochaRunner/src/interface.ts b/remote/test/puppeteer/tools/mochaRunner/src/interface.ts new file mode 100644 index 0000000000..79329fcb0d --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/interface.ts @@ -0,0 +1,130 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Mocha from 'mocha'; +import commonInterface from 'mocha/lib/interfaces/common'; + +import {testIdMatchesExpectationPattern} from './utils.js'; + +type SuiteFunction = ((this: Mocha.Suite) => void) | undefined; +type ExclusiveSuiteFunction = (this: Mocha.Suite) => void; + +const skippedTests: Array<{testIdPattern: string; skip: true}> = process.env[ + 'PUPPETEER_SKIPPED_TEST_CONFIG' +] + ? JSON.parse(process.env['PUPPETEER_SKIPPED_TEST_CONFIG']) + : []; + +function shouldSkipTest(test: Mocha.Test): boolean { + // TODO: more efficient lookup. + const definition = skippedTests.find(skippedTest => { + return testIdMatchesExpectationPattern(test, skippedTest.testIdPattern); + }); + if (definition && definition.skip) { + return true; + } + return false; +} + +function customBDDInterface(suite: Mocha.Suite) { + const suites = [suite]; + + suite.on( + Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, + function (context, file, mocha) { + const common = commonInterface(suites, context, mocha); + + context['before'] = common.before; + context['after'] = common.after; + context['beforeEach'] = common.beforeEach; + context['afterEach'] = common.afterEach; + if (mocha.options.delay) { + context['run'] = common.runWithSuite(suite); + } + function describe(title: string, fn: SuiteFunction) { + return common.suite.create({ + title: title, + file: file, + fn: fn, + }); + } + describe.only = function (title: string, fn: ExclusiveSuiteFunction) { + return common.suite.only({ + title: title, + file: file, + fn: fn, + isOnly: true, + }); + }; + + describe.skip = function (title: string, fn: SuiteFunction) { + return common.suite.skip({ + title: title, + file: file, + fn: fn, + }); + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + context['describe'] = describe; + + function it(title: string, fn: Mocha.TestFunction, itOnly = false) { + const suite = suites[0]!; + const test = new Mocha.Test(title, suite.isPending() ? undefined : fn); + test.file = file; + test.parent = suite; + + const describeOnly = Boolean( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + suite.parent?._onlySuites.find(child => { + return child === suite; + }) + ); + + if (shouldSkipTest(test) && !(itOnly || describeOnly)) { + const test = new Mocha.Test(title); + test.file = file; + suite.addTest(test); + return test; + } else { + suite.addTest(test); + return test; + } + } + + it.only = function (title: string, fn: Mocha.TestFunction) { + return common.test.only( + mocha, + (context['it'] as typeof it)(title, fn, true) + ); + }; + + it.skip = function (title: string) { + return context['it'](title); + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + context.it = it; + } + ); +} + +customBDDInterface.description = 'Custom BDD'; + +module.exports = customBDDInterface; diff --git a/remote/test/puppeteer/tools/mochaRunner/src/main.ts b/remote/test/puppeteer/tools/mochaRunner/src/main.ts new file mode 100644 index 0000000000..d2547e721c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/main.ts @@ -0,0 +1,259 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {randomUUID} from 'crypto'; +import fs from 'fs'; +import {spawn, SpawnOptions} from 'node:child_process'; +import os from 'os'; +import path from 'path'; + +import { + TestExpectation, + MochaResults, + zTestSuiteFile, + zPlatform, + TestSuite, + TestSuiteFile, + Platform, +} from './types.js'; +import { + extendProcessEnv, + filterByPlatform, + readJSON, + filterByParameters, + getExpectationUpdates, + printSuggestions, + RecommendedExpectation, + writeJSON, +} from './utils.js'; + +function getApplicableTestSuites( + parsedSuitesFile: TestSuiteFile, + platform: Platform +): TestSuite[] { + const testSuiteArgIdx = process.argv.indexOf('--test-suite'); + let applicableSuites: TestSuite[] = []; + + if (testSuiteArgIdx === -1) { + applicableSuites = filterByPlatform(parsedSuitesFile.testSuites, platform); + } else { + const testSuiteId = process.argv[testSuiteArgIdx + 1]; + const testSuite = parsedSuitesFile.testSuites.find(suite => { + return suite.id === testSuiteId; + }); + + if (!testSuite) { + console.error(`Test suite ${testSuiteId} is not defined`); + process.exit(1); + } + + if (!testSuite.platforms.includes(platform)) { + console.warn( + `Test suite ${testSuiteId} is not enabled for your platform. Running it anyway.` + ); + } + + applicableSuites = [testSuite]; + } + + return applicableSuites; +} + +async function main() { + const noCoverage = process.argv.indexOf('--no-coverage') !== -1; + const noSuggestions = process.argv.indexOf('--no-suggestions') !== -1; + + const statsFilenameIdx = process.argv.indexOf('--save-stats-to'); + let statsFilename = ''; + if (statsFilenameIdx !== -1) { + statsFilename = process.argv[statsFilenameIdx + 1] as string; + if (statsFilename.includes('INSERTID')) { + statsFilename = statsFilename.replace(/INSERTID/gi, randomUUID()); + } + } + + const platform = zPlatform.parse(os.platform()); + + const expectations = readJSON( + path.join(process.cwd(), 'test', 'TestExpectations.json') + ) as TestExpectation[]; + + const parsedSuitesFile = zTestSuiteFile.parse( + readJSON(path.join(process.cwd(), 'test', 'TestSuites.json')) + ); + + const applicableSuites = getApplicableTestSuites(parsedSuitesFile, platform); + + console.log('Planning to run the following test suites', applicableSuites); + if (statsFilename) { + console.log('Test stats will be saved to', statsFilename); + } + + let fail = false; + const recommendations: RecommendedExpectation[] = []; + try { + for (const suite of applicableSuites) { + const parameters = suite.parameters; + + const applicableExpectations = filterByParameters( + filterByPlatform(expectations, platform), + parameters + ).reverse(); + + // Add more logging when the GitHub Action Debugging option is set + // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + const githubActionDebugging = process.env['RUNNER_DEBUG'] + ? { + DEBUG: 'puppeteer:*', + EXTRA_LAUNCH_OPTIONS: JSON.stringify({ + extraPrefsFirefox: { + 'remote.log.level': 'Trace', + }, + }), + } + : {}; + + const env = extendProcessEnv([ + ...parameters.map(param => { + return parsedSuitesFile.parameterDefinitions[param]; + }), + { + PUPPETEER_SKIPPED_TEST_CONFIG: JSON.stringify( + applicableExpectations.map(ex => { + return { + testIdPattern: ex.testIdPattern, + skip: ex.expectations.includes('SKIP'), + }; + }) + ), + }, + githubActionDebugging, + ]); + + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'puppeteer-test-runner-') + ); + const tmpFilename = statsFilename + ? statsFilename + : path.join(tmpDir, 'output.json'); + console.log('Running', JSON.stringify(parameters), tmpFilename); + const reporterArgumentIndex = process.argv.indexOf('--reporter'); + const args = [ + '-u', + path.join(__dirname, 'interface.js'), + '-R', + reporterArgumentIndex === -1 + ? path.join(__dirname, 'reporter.js') + : process.argv[reporterArgumentIndex + 1] || '', + '-O', + 'output=' + tmpFilename, + ]; + const retriesArgumentIndex = process.argv.indexOf('--retries'); + const timeoutArgumentIndex = process.argv.indexOf('--timeout'); + if (retriesArgumentIndex > -1) { + args.push('--retries', process.argv[retriesArgumentIndex + 1] || ''); + } + if (timeoutArgumentIndex > -1) { + args.push('--timeout', process.argv[timeoutArgumentIndex + 1] || ''); + } + if (process.argv.indexOf('--no-parallel')) { + args.push('--no-parallel'); + } + if (process.argv.indexOf('--fullTrace')) { + args.push('--fullTrace'); + } + const spawnArgs: SpawnOptions = { + shell: true, + cwd: process.cwd(), + stdio: 'inherit', + env, + }; + const handle = noCoverage + ? spawn('npx', ['mocha', ...args], spawnArgs) + : spawn( + 'npx', + [ + 'c8', + '--check-coverage', + '--lines', + String(suite.expectedLineCoverage), + 'npx mocha', + ...args, + ], + spawnArgs + ); + await new Promise<void>((resolve, reject) => { + handle.on('error', err => { + reject(err); + }); + handle.on('close', () => { + resolve(); + }); + }); + console.log('Finished', JSON.stringify(parameters)); + try { + const results = readJSON(tmpFilename) as MochaResults; + const updates = getExpectationUpdates(results, applicableExpectations, { + platforms: [os.platform()], + parameters, + }); + results.parameters = parameters; + results.platform = platform; + results.date = new Date().toISOString(); + if (updates.length > 0) { + fail = true; + recommendations.push(...updates); + results.updates = updates; + writeJSON(tmpFilename, results); + } else { + console.log('Test run matches expectations'); + writeJSON(tmpFilename, results); + continue; + } + } catch (err) { + fail = true; + console.error(err); + } + } + } catch (err) { + fail = true; + console.error(err); + } finally { + if (!noSuggestions) { + printSuggestions( + recommendations, + 'add', + 'Add the following to TestExpectations.json to ignore the error:' + ); + printSuggestions( + recommendations, + 'remove', + 'Remove the following from the TestExpectations.json to ignore the error:' + ); + printSuggestions( + recommendations, + 'update', + 'Update the following expectations in the TestExpectations.json to ignore the error:' + ); + } + process.exit(fail ? 1 : 0); + } +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/remote/test/puppeteer/tools/mochaRunner/src/reporter.ts b/remote/test/puppeteer/tools/mochaRunner/src/reporter.ts new file mode 100644 index 0000000000..37ca586215 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/reporter.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Mocha from 'mocha'; + +class SpecJSONReporter extends Mocha.reporters.Spec { + constructor(runner: Mocha.Runner, options?: Mocha.MochaOptions) { + super(runner, options); + Mocha.reporters.JSON.call(this, runner, options); + } +} + +module.exports = SpecJSONReporter; diff --git a/remote/test/puppeteer/tools/mochaRunner/src/test.ts b/remote/test/puppeteer/tools/mochaRunner/src/test.ts new file mode 100644 index 0000000000..1e0328499c --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/test.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import assert from 'node:assert/strict'; +import {describe, test} from 'node:test'; + +import {TestExpectation} from './types.js'; +import { + filterByParameters, + getTestResultForFailure, + isWildCardPattern, + testIdMatchesExpectationPattern, +} from './utils.js'; +import {getFilename, extendProcessEnv} from './utils.js'; + +void test('extendProcessEnv', () => { + const env = extendProcessEnv([{TEST: 'TEST'}, {TEST2: 'TEST2'}]); + assert.equal(env['TEST'], 'TEST'); + assert.equal(env['TEST2'], 'TEST2'); +}); + +void test('getFilename', () => { + assert.equal(getFilename('/etc/test.ts'), 'test'); + assert.equal(getFilename('/etc/test.js'), 'test'); +}); + +void test('getTestResultForFailure', () => { + assert.equal( + getTestResultForFailure({err: {code: 'ERR_MOCHA_TIMEOUT'}}), + 'TIMEOUT' + ); + assert.equal(getTestResultForFailure({err: {code: 'ERROR'}}), 'FAIL'); +}); + +void test('filterByParameters', () => { + const expectations: TestExpectation[] = [ + { + testIdPattern: + '[oopif.spec] OOPIF "after all" hook for "should keep track of a frames OOP state"', + platforms: ['darwin'], + parameters: ['firefox', 'headless'], + expectations: ['FAIL'], + }, + ]; + assert.equal( + filterByParameters(expectations, ['firefox', 'headless']).length, + 1 + ); + assert.equal(filterByParameters(expectations, ['firefox']).length, 0); + assert.equal( + filterByParameters(expectations, ['firefox', 'headless', 'other']).length, + 1 + ); + assert.equal(filterByParameters(expectations, ['other']).length, 0); +}); + +void test('isWildCardPattern', () => { + assert.equal(isWildCardPattern(''), false); + assert.equal(isWildCardPattern('a'), false); + assert.equal(isWildCardPattern('*'), true); + + assert.equal(isWildCardPattern('[queryHandler.spec]'), false); + assert.equal(isWildCardPattern('[queryHandler.spec] *'), true); + assert.equal(isWildCardPattern(' [queryHandler.spec] '), false); + + assert.equal(isWildCardPattern('[queryHandler.spec] Query'), false); + assert.equal(isWildCardPattern('[queryHandler.spec] Page *'), true); + assert.equal(isWildCardPattern('[queryHandler.spec] Page Page.goto *'), true); +}); + +describe('testIdMatchesExpectationPattern', () => { + const expectations: Array<[string, boolean]> = [ + ['', false], + ['*', true], + ['* should work', true], + ['* Page.setContent *', true], + ['* should work as expected', false], + ['Page.setContent *', false], + ['[page.spec]', false], + ['[page.spec] *', true], + ['[page.spec] Page *', true], + ['[page.spec] Page Page.setContent *', true], + ['[page.spec] Page Page.setContent should work', true], + ['[page.spec] Page * should work', true], + ['[page.spec] * Page.setContent *', true], + ['[jshandle.spec] *', false], + ['[jshandle.spec] JSHandle should work', false], + ]; + + void test('with MochaTest', () => { + const test = { + title: 'should work', + file: 'page.spec.ts', + fullTitle() { + return 'Page Page.setContent should work'; + }, + } as any; + + for (const [pattern, expected] of expectations) { + assert.equal( + testIdMatchesExpectationPattern(test, pattern), + expected, + `Expected "${pattern}" to yield "${expected}"` + ); + } + }); + void test('with MochaTestResult', () => { + const test = { + title: 'should work', + file: 'page.spec.ts', + fullTitle: 'Page Page.setContent should work', + } as any; + + for (const [pattern, expected] of expectations) { + assert.equal( + testIdMatchesExpectationPattern(test, pattern), + expected, + `Expected "${pattern}" to yield "${expected}"` + ); + } + }); +}); diff --git a/remote/test/puppeteer/tools/mochaRunner/src/types.ts b/remote/test/puppeteer/tools/mochaRunner/src/types.ts new file mode 100644 index 0000000000..8d8a08ee98 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/types.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {z} from 'zod'; + +import {RecommendedExpectation} from './utils.js'; + +export const zPlatform = z.enum(['win32', 'linux', 'darwin']); + +export type Platform = z.infer<typeof zPlatform>; + +export const zTestSuite = z.object({ + id: z.string(), + platforms: z.array(zPlatform), + parameters: z.array(z.string()), + expectedLineCoverage: z.number(), +}); + +export type TestSuite = z.infer<typeof zTestSuite>; + +export const zTestSuiteFile = z.object({ + testSuites: z.array(zTestSuite), + parameterDefinitions: z.record(z.any()), +}); + +export type TestSuiteFile = z.infer<typeof zTestSuiteFile>; + +export type TestResult = 'PASS' | 'FAIL' | 'TIMEOUT' | 'SKIP'; + +export type TestExpectation = { + testIdPattern: string; + platforms: NodeJS.Platform[]; + parameters: string[]; + expectations: TestResult[]; +}; + +export type MochaTestResult = { + fullTitle: string; + title: string; + file: string; + err?: {code: string}; +}; + +export type MochaResults = { + stats: unknown; + pending: MochaTestResult[]; + passes: MochaTestResult[]; + failures: MochaTestResult[]; + // Added by mochaRunner. + updates?: RecommendedExpectation[]; + parameters?: string[]; + platform?: string; + date?: string; +}; diff --git a/remote/test/puppeteer/tools/mochaRunner/src/utils.ts b/remote/test/puppeteer/tools/mochaRunner/src/utils.ts new file mode 100644 index 0000000000..9fdbf65583 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/src/utils.ts @@ -0,0 +1,265 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import { + MochaTestResult, + TestExpectation, + MochaResults, + TestResult, +} from './types.js'; + +export function extendProcessEnv(envs: object[]): NodeJS.ProcessEnv { + return envs.reduce( + (acc: object, item: object) => { + Object.assign(acc, item); + return acc; + }, + { + ...process.env, + } + ) as NodeJS.ProcessEnv; +} + +export function getFilename(file: string): string { + return path.basename(file).replace(path.extname(file), ''); +} + +export function readJSON(path: string): unknown { + return JSON.parse(fs.readFileSync(path, 'utf-8')); +} + +export function writeJSON(path: string, json: unknown): unknown { + return fs.writeFileSync(path, JSON.stringify(json, null, 2)); +} + +export function filterByPlatform<T extends {platforms: NodeJS.Platform[]}>( + items: T[], + platform: NodeJS.Platform +): T[] { + return items.filter(item => { + return item.platforms.includes(platform); + }); +} + +export function prettyPrintJSON(json: unknown): void { + console.log(JSON.stringify(json, null, 2)); +} + +export function printSuggestions( + recommendations: RecommendedExpectation[], + action: RecommendedExpectation['action'], + message: string +): void { + const toPrint = recommendations.filter(item => { + return item.action === action; + }); + if (toPrint.length) { + console.log(message); + prettyPrintJSON( + toPrint.map(item => { + return item.expectation; + }) + ); + console.log( + 'The recommendations are based on the following applied expectaions:' + ); + prettyPrintJSON( + toPrint.map(item => { + return item.basedOn; + }) + ); + } +} + +export function filterByParameters( + expectations: TestExpectation[], + parameters: string[] +): TestExpectation[] { + const querySet = new Set(parameters); + return expectations.filter(ex => { + return ex.parameters.every(param => { + return querySet.has(param); + }); + }); +} + +/** + * The last expectation that matches an empty string as all tests pattern + * or the name of the file or the whole name of the test the filter wins. + */ +export function findEffectiveExpectationForTest( + expectations: TestExpectation[], + result: MochaTestResult +): TestExpectation | undefined { + return expectations.find(expectation => { + return testIdMatchesExpectationPattern(result, expectation.testIdPattern); + }); +} + +export type RecommendedExpectation = { + expectation: TestExpectation; + action: 'remove' | 'add' | 'update'; + basedOn?: TestExpectation; +}; + +export function isWildCardPattern(testIdPattern: string): boolean { + return testIdPattern.includes('*'); +} + +export function getExpectationUpdates( + results: MochaResults, + expectations: TestExpectation[], + context: { + platforms: NodeJS.Platform[]; + parameters: string[]; + } +): RecommendedExpectation[] { + const output: Map<string, RecommendedExpectation> = new Map(); + + for (const pass of results.passes) { + // If an error occurs during a hook + // the error not have a file associated with it + if (!pass.file) { + continue; + } + + const expectationEntry = findEffectiveExpectationForTest( + expectations, + pass + ); + if (expectationEntry && !expectationEntry.expectations.includes('PASS')) { + if (isWildCardPattern(expectationEntry.testIdPattern)) { + addEntry({ + expectation: { + testIdPattern: getTestId(pass.file, pass.fullTitle), + platforms: context.platforms, + parameters: context.parameters, + expectations: ['PASS'], + }, + action: 'add', + basedOn: expectationEntry, + }); + } else { + addEntry({ + expectation: expectationEntry, + action: 'remove', + basedOn: expectationEntry, + }); + } + } + } + + for (const failure of results.failures) { + // If an error occurs during a hook + // the error not have a file associated with it + if (!failure.file) { + continue; + } + + const expectationEntry = findEffectiveExpectationForTest( + expectations, + failure + ); + if (expectationEntry && !expectationEntry.expectations.includes('SKIP')) { + if ( + !expectationEntry.expectations.includes( + getTestResultForFailure(failure) + ) + ) { + // If the effective explanation is a wildcard, we recommend adding a new + // expectation instead of updating the wildcard that might affect multiple + // tests. + if (isWildCardPattern(expectationEntry.testIdPattern)) { + addEntry({ + expectation: { + testIdPattern: getTestId(failure.file, failure.fullTitle), + platforms: context.platforms, + parameters: context.parameters, + expectations: [getTestResultForFailure(failure)], + }, + action: 'add', + basedOn: expectationEntry, + }); + } else { + addEntry({ + expectation: { + ...expectationEntry, + expectations: [ + ...expectationEntry.expectations, + getTestResultForFailure(failure), + ], + }, + action: 'update', + basedOn: expectationEntry, + }); + } + } + } else if (!expectationEntry) { + addEntry({ + expectation: { + testIdPattern: getTestId(failure.file, failure.fullTitle), + platforms: context.platforms, + parameters: context.parameters, + expectations: [getTestResultForFailure(failure)], + }, + action: 'add', + }); + } + } + + function addEntry(value: RecommendedExpectation) { + const key = JSON.stringify(value); + if (!output.has(key)) { + output.set(key, value); + } + } + + return [...output.values()]; +} + +export function getTestResultForFailure( + test: Pick<MochaTestResult, 'err'> +): TestResult { + return test.err?.code === 'ERR_MOCHA_TIMEOUT' ? 'TIMEOUT' : 'FAIL'; +} + +export function getTestId(file: string, fullTitle?: string): string { + return fullTitle + ? `[${getFilename(file)}] ${fullTitle}` + : `[${getFilename(file)}]`; +} + +export function testIdMatchesExpectationPattern( + test: MochaTestResult | Mocha.Test, + pattern: string +): boolean { + const patternRegExString = pattern + // Replace `*` with non special character + .replace(/\*/g, '--STAR--') + // Escape special characters https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Replace placeholder with greedy match + .replace(/--STAR--/g, '(.*)?'); + // Match beginning and end explicitly + const patternRegEx = new RegExp(`^${patternRegExString}$`); + const fullTitle = + typeof test.fullTitle === 'string' ? test.fullTitle : test.fullTitle(); + + return patternRegEx.test(getTestId(test.file ?? '', fullTitle)); +} diff --git a/remote/test/puppeteer/tools/mochaRunner/tsconfig.json b/remote/test/puppeteer/tools/mochaRunner/tsconfig.json new file mode 100644 index 0000000000..c2576c2564 --- /dev/null +++ b/remote/test/puppeteer/tools/mochaRunner/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "composite": true, + "module": "CommonJS", + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/tools/remove_version_suffix.js b/remote/test/puppeteer/tools/remove_version_suffix.js new file mode 100644 index 0000000000..091a35ec9b --- /dev/null +++ b/remote/test/puppeteer/tools/remove_version_suffix.js @@ -0,0 +1,26 @@ +/** + * 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 + * + * 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. + */ + +const fs = require('fs'); +const json = fs.readFileSync('./package.json', 'utf8').toString(); +const pkg = JSON.parse(json); +const oldVersion = pkg.version; +const version = oldVersion.replace(/-post$/, ''); +const updated = json.replace( + `"version": "${oldVersion}"`, + `"version": "${version}"` +); +fs.writeFileSync('./package.json', updated); diff --git a/remote/test/puppeteer/tools/sort-test-expectations.js b/remote/test/puppeteer/tools/sort-test-expectations.js new file mode 100644 index 0000000000..96e32145e4 --- /dev/null +++ b/remote/test/puppeteer/tools/sort-test-expectations.js @@ -0,0 +1,59 @@ +/** + * Copyright 2023 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +// TODO: this could be an eslint rule probably. + +const fs = require('fs'); +const path = require('path'); + +const prettier = require('prettier'); + +const source = 'test/TestExpectations.json'; + +const testExpectations = JSON.parse(fs.readFileSync(source, 'utf-8')); + +function getSpecificity(item) { + return ( + item.parameters.length + + (item.testIdPattern.includes('*') + ? item.testIdPattern === '*' + ? 0 + : 1 + : 2) + ); +} + +testExpectations.sort((a, b) => { + const result = getSpecificity(a) - getSpecificity(b); + if (result === 0) { + return a.testIdPattern.localeCompare(b.testIdPattern); + } + return result; +}); + +testExpectations.forEach(item => { + item.parameters.sort(); + item.expectations.sort(); + item.platforms.sort(); +}); + +fs.writeFileSync( + source, + prettier.format(JSON.stringify(testExpectations), { + ...require(path.join(__dirname, '..', '.prettierrc.cjs')), + parser: 'json', + }) +); diff --git a/remote/test/puppeteer/tools/third_party/validate-licenses.ts b/remote/test/puppeteer/tools/third_party/validate-licenses.ts new file mode 100644 index 0000000000..4d1c05497b --- /dev/null +++ b/remote/test/puppeteer/tools/third_party/validate-licenses.ts @@ -0,0 +1,153 @@ +// The MIT License + +// Copyright (c) 2010-2022 Google LLC. http://angular.io/license + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Taken and adapted from https://github.com/angular/angular-cli/blob/173823d/scripts/validate-licenses.ts. + +import * as path from 'path'; + +import checker from 'license-checker'; +import spdxSatisfies from 'spdx-satisfies'; + +/** + * A general note on some black listed specific licenses: + * + * - CC0 This is not a valid license. It does not grant copyright of the + * code/asset, and does not resolve patents or other licensed work. The + * different claims also have no standing in court and do not provide + * protection to or from Google and/or third parties. We cannot use nor + * contribute to CC0 licenses. + * - Public Domain Same as CC0, it is not a valid license. + */ +const allowedLicenses = [ + // Regular valid open source licenses supported by Google. + 'MIT', + 'ISC', + 'Apache-2.0', + 'Python-2.0', + 'Artistic-2.0', + + 'BSD-2-Clause', + 'BSD-3-Clause', + 'BSD-4-Clause', + + // All CC-BY licenses have a full copyright grant and attribution section. + 'CC-BY-3.0', + 'CC-BY-4.0', + + // Have a full copyright grant. Validated by opensource team. + 'Unlicense', + 'CC0-1.0', + '0BSD', + + // Combinations. + '(AFL-2.1 OR BSD-2-Clause)', +]; + +// Name variations of SPDX licenses that some packages have. +// Licenses not included in SPDX but accepted will be converted to MIT. +const licenseReplacements: {[key: string]: string} = { + // Just a longer string that our script catches. SPDX official name is the shorter one. + 'Apache License, Version 2.0': 'Apache-2.0', + Apache2: 'Apache-2.0', + 'Apache 2.0': 'Apache-2.0', + 'Apache v2': 'Apache-2.0', + 'AFLv2.1': 'AFL-2.1', + // BSD is BSD-2-clause by default. + BSD: 'BSD-2-Clause', +}; + +// Specific packages to ignore, add a reason in a comment. Format: package-name@version. +const ignoredPackages = [ + // * Development only + 'spdx-license-ids@3.0.5', // CC0 but it's content only (index.json, no code) and not distributed. +]; + +// Check if a license is accepted by an array of accepted licenses +function _passesSpdx(licenses: string[], accepted: string[]) { + try { + return spdxSatisfies(licenses.join(' AND '), accepted.join(' OR ')); + } catch { + return false; + } +} + +function main(): Promise<number> { + return new Promise(resolve => { + const startFolder = path.join(__dirname, '..', '..'); + checker.init( + {start: startFolder, excludePrivatePackages: true}, + (err: Error, json: object) => { + if (err) { + console.error(`Something happened:\n${err.message}`); + resolve(1); + } else { + console.info(`Testing ${Object.keys(json).length} packages.\n`); + + // Packages with bad licenses are those that neither pass SPDX nor are ignored. + const badLicensePackages = Object.keys(json) + .map(key => { + return { + id: key, + licenses: ([] as string[]) + .concat((json[key] as {licenses: string[]}).licenses) + // `*` is used when the license is guessed. + .map(x => { + return x.replace(/\*$/, ''); + }) + .map(x => { + return x in licenseReplacements + ? licenseReplacements[x] + : x; + }), + }; + }) + .filter(pkg => { + return !_passesSpdx(pkg.licenses, allowedLicenses); + }) + .filter(pkg => { + return !ignoredPackages.find(ignored => { + return ignored === pkg.id; + }); + }); + + // Report packages with bad licenses + if (badLicensePackages.length > 0) { + console.error('Invalid package licences found:'); + badLicensePackages.forEach(pkg => { + console.error(`${pkg.id}: ${JSON.stringify(pkg.licenses)}`); + }); + console.error( + `\n${badLicensePackages.length} total packages with invalid licenses.` + ); + resolve(2); + } else { + console.info('All package licenses are valid.'); + resolve(0); + } + } + } + ); + }); +} + +main().then(code => { + return process.exit(code); +}); diff --git a/remote/test/puppeteer/tools/tsconfig.json b/remote/test/puppeteer/tools/tsconfig.json new file mode 100644 index 0000000000..393392c494 --- /dev/null +++ b/remote/test/puppeteer/tools/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.base.json", + "files": ["../package.json"] +} diff --git a/remote/test/puppeteer/tsconfig.base.json b/remote/test/puppeteer/tsconfig.base.json new file mode 100644 index 0000000000..9c29d30d9a --- /dev/null +++ b/remote/test/puppeteer/tsconfig.base.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "allowJs": true, + "alwaysStrict": true, + "checkJs": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": true, + "module": "ESNext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "target": "ES2019", + "useUnknownInCatchVariables": true + } +} diff --git a/remote/test/puppeteer/versions.js b/remote/test/puppeteer/versions.js new file mode 100644 index 0000000000..63be16fb3e --- /dev/null +++ b/remote/test/puppeteer/versions.js @@ -0,0 +1,73 @@ +/** + * 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 Chrome version => Puppeteer version. + // In Chrome roll patches, use `NEXT` for the Puppeteer version. + ['113.0.5672.63', 'v20.1.0'], + ['112.0.5615.121', 'v20.0.0'], + ['112.0.5614.0', 'v19.8.0'], + ['111.0.5556.0', 'v19.7.0'], + ['110.0.5479.0', 'v19.6.0'], + ['109.0.5412.0', 'v19.4.0'], + ['108.0.5351.0', 'v19.2.0'], + ['107.0.5296.0', 'v18.1.0'], + ['106.0.5249.0', 'v17.1.0'], + ['105.0.5173.0', 'v15.5.0'], + ['104.0.5109.0', 'v15.1.0'], + ['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'], +]); + +// Should not be more than 2 major versions behind Chrome Stable (https://chromestatus.com/roadmap). +const lastMaintainedChromeVersion = '109.0.5412.0'; + +if (!versionsPerRelease.has(lastMaintainedChromeVersion)) { + throw new Error( + 'lastMaintainedChromeVersion is missing from versionsPerRelease' + ); +} + +module.exports = { + versionsPerRelease, + lastMaintainedChromeVersion, +}; |