diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /remote/test/puppeteer | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/test/puppeteer')
379 files changed, 66275 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..2cd9862cd7 --- /dev/null +++ b/remote/test/puppeteer/.eslintignore @@ -0,0 +1,42 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# Generated files +**/*.tsbuildinfo +puppeteer.api.json +puppeteer*.tgz +yarn.lock +.docusaurus/ +.cache-loader +.local-chromium/ +.local-firefox/ +test/output-*/ +.dev_profile* +coverage/ +src/generated + +# IDE Artifacts +.vscode +.devcontainer + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +## [END] Keep in sync with .gitignore + +# ESLint ignores. +assets/ +vendor/ 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..466d7ebef3 --- /dev/null +++ b/remote/test/puppeteer/.eslintrc.js @@ -0,0 +1,189 @@ +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: [2, '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': 2, + // syntax preferences + 'spaced-comment': [ + 2, + 'always', + { + markers: ['*'], + }, + ], + eqeqeq: [2], + 'accessor-pairs': [ + 2, + { + getWithoutSet: false, + setWithoutGet: false, + }, + ], + 'new-parens': 2, + 'func-call-spacing': 2, + 'prefer-const': 2, + + 'max-len': [ + 2, + { + /* this setting doesn't impact things as we use Prettier to format + * our code and hence dictate the line length. + * Prettier aims for 80 but sometimes makes the decision to go just + * over 80 chars as it decides that's better than wrapping. ESLint's + * rule defaults to 80 but therefore conflicts with Prettier. So we + * set it to something far higher than Prettier would allow to avoid + * it causing issues and conflicting with Prettier. + */ + code: 200, + comments: 90, + ignoreTemplateLiterals: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreRegExpLiterals: true, + }, + ], + // anti-patterns + 'no-var': 2, + 'no-with': 2, + 'no-multi-str': 2, + 'no-caller': 2, + 'no-implied-eval': 2, + 'no-labels': 2, + 'no-new-object': 2, + 'no-octal-escape': 2, + 'no-self-compare': 2, + 'no-shadow-restricted-names': 2, + 'no-cond-assign': 2, + 'no-debugger': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-unreachable': 2, + 'no-unsafe-negation': 2, + radix: 2, + 'valid-typeof': 2, + 'no-unused-vars': [ + 2, + { + args: 'none', + vars: 'local', + varsIgnorePattern: + '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', + }, + ], + 'no-implicit-globals': [2], + + // es2015 features + 'require-yield': 2, + 'template-curly-spacing': [2, 'never'], + + // ensure we don't have any it.only or describe.only in prod + 'mocha/no-exclusive-tests': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: ['*Events'], + paths: [ + { + name: 'mitt', + message: + 'Import Mitt from the vendored location: vendor/mitt/src/index.js', + }, + ], + }, + ], + 'import/extensions': ['error', 'ignorePackages'], + + 'no-restricted-syntax': [ + 'error', + // Don't allow underscored declarations on camelCased variables/properties. + // ...RESTRICTED_UNDERSCORED_IDENTIFIERS, + ], + }, + overrides: [ + { + files: ['*.ts'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + plugins: ['eslint-plugin-tsdoc', 'local'], + rules: { + // Keeps comments formatted. + 'local/prettier-comments': 2, + // Brackets keep code readable. + curly: [2, 'all'], + // Brackets keep code readable and `return` intentions clear. + 'arrow-body-style': ['error', 'always'], + // Error if comments do not adhere to `tsdoc`. + 'tsdoc/syntax': 2, + // Keeps array types simple only when they are simple for readability. + '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': [ + 'error', + {argsIgnorePattern: '^_'}, + ], + 'func-call-spacing': 0, + '@typescript-eslint/func-call-spacing': 2, + semi: 0, + '@typescript-eslint/semi': 2, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-use-before-define': 0, + // We have to use any on some types so the warning isn't valuable. + '@typescript-eslint/no-explicit-any': 0, + // We don't require explicit return types on basic functions or + // dummy functions in tests, for example + '@typescript-eslint/explicit-function-return-type': 0, + // We allow non-null assertions if the value was asserted using `assert` API. + '@typescript-eslint/no-non-null-assertion': 0, + /** + * This is the default options (as per + * https://github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/docs/rules/ban-types.md), + * + * Unfortunately there's no way to + */ + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + /* + * Puppeteer's API accepts generic functions in many places so it's + * not a useful linting rule to ban the `Function` type. This turns off + * the banning of the `Function` type which is a default rule. + */ + Function: false, + }, + }, + ], + // By default this is a warning but we want it to error. + '@typescript-eslint/explicit-module-boundary-types': 2, + '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`.', + }, + ], + }, + }, + ], +}; 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..e7168715d8 --- /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 ? 2 : 0, + parallel: !!process.env.PARALLEL, + timeout: 25 * 1000, + 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..738ed9e55b --- /dev/null +++ b/remote/test/puppeteer/.prettierignore @@ -0,0 +1,47 @@ +## [START] Keep in sync with .gitignore +# Dependencies +node_modules + +# Production +build/ +lib/ + +# Generated files +**/*.tsbuildinfo +puppeteer.api.json +puppeteer*.tgz +yarn.lock +.docusaurus/ +.cache-loader +.local-chromium/ +.local-firefox/ +test/output-*/ +.dev_profile* +coverage/ +src/generated + +# IDE Artifacts +.vscode +.devcontainer + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +## [END] Keep in sync with .gitignore + +# Prettier-only ignores. +assets/ +CHANGELOG.md +package-lock.json +package.json +test/assets/ +vendor/ +docs/ +versioned_*/ diff --git a/remote/test/puppeteer/.prettierrc.cjs b/remote/test/puppeteer/.prettierrc.cjs new file mode 100644 index 0000000000..f994e8a179 --- /dev/null +++ b/remote/test/puppeteer/.prettierrc.cjs @@ -0,0 +1 @@ +module.exports = require('gts/.prettierrc.json'); diff --git a/remote/test/puppeteer/.release-please-manifest.json b/remote/test/puppeteer/.release-please-manifest.json new file mode 100644 index 0000000000..eaffe70bed --- /dev/null +++ b/remote/test/puppeteer/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "17.1.2" +} diff --git a/remote/test/puppeteer/CHANGELOG.md b/remote/test/puppeteer/CHANGELOG.md new file mode 100644 index 0000000000..47fc4e0ecf --- /dev/null +++ b/remote/test/puppeteer/CHANGELOG.md @@ -0,0 +1,844 @@ +# 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. + +## [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/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..77ef5c9701 --- /dev/null +++ b/remote/test/puppeteer/README.md @@ -0,0 +1,415 @@ +# Puppeteer + +<!-- [START badges] --> + +[![Build status](https://github.com/puppeteer/puppeteer/workflows/CI/badge.svg)](https://github.com/puppeteer/puppeteer/actions?query=workflow%3ACI) [![npm puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) + +<!-- [END badges] --> + +<img src="https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png" height="200" align="right"/> + +###### [API](https://pptr.dev/api) | [FAQ](https://pptr.dev/faq) | [Contributing](https://pptr.dev/contributing) | [Troubleshooting](https://pptr.dev/troubleshooting) + +> Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). Puppeteer runs [headless](https://developers.google.com/web/updates/2017/04/headless-chrome) by default, but can be configured to run full (non-headless) Chrome or Chromium. + +<!-- [START usecases] --> + +##### What can I do? + +Most things that you can do manually in the browser can be done using Puppeteer! Here are a few examples to get you started: + +- Generate screenshots and PDFs of pages. +- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). +- Automate form submission, UI testing, keyboard input, etc. +- Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. +- Capture a [timeline trace](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) of your site to help diagnose performance issues. +- Test Chrome Extensions. +<!-- [END usecases] --> + +<!-- [START getstarted] --> + +## Getting Started + +### Installation + +To use Puppeteer in your project, run: + +```bash +npm i puppeteer +# or "yarn add puppeteer" +``` + +When you install Puppeteer, it downloads a recent version of Chromium (~170MB Mac, ~282MB Linux, ~280MB Win) that is guaranteed to work with the API (customizable through [Environment Variables](#environment-variables)). For a version of Puppeteer purely for connection, see [`puppeteer-core`](#puppeteer-core). + +#### Environment Variables + +Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. +If Puppeteer doesn't find them in the environment during the installation step, a lowercased variant of these variables will be used from the [npm config](https://docs.npmjs.com/cli/config). + +- `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` - defines HTTP proxy settings that are used to download and run the browser. +- `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` - do not download bundled Chromium during installation step. +- `PUPPETEER_TMP_DIR` - defines the directory to be used by Puppeteer for creating temporary files. Defaults to [`os.tmpdir()`](https://nodejs.org/api/os.html#os_os_tmpdir). +- `PUPPETEER_DOWNLOAD_HOST` - overwrite URL prefix that is used to download Chromium. Note: this includes protocol and might even include path prefix. Defaults to `https://storage.googleapis.com`. +- `PUPPETEER_DOWNLOAD_PATH` - overwrite the path for the downloads folder. Defaults to `<root>/.local-chromium`, where `<root>` is Puppeteer's package root. +- `PUPPETEER_CHROMIUM_REVISION` - specify a certain version of Chromium you'd like Puppeteer to use. See [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) on how executable path is inferred. +- `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch). +- `PUPPETEER_PRODUCT` - specify which browser you'd like Puppeteer to use. Must be one of `chrome` or `firefox`. This can also be used during installation to fetch the recommended browser binary. Setting `product` programmatically in [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) supersedes this environment variable. The product is exposed in [`puppeteer.product`](https://pptr.dev/api/puppeteer.product) +- `PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM` — specify Puppeteer download Chromium for Apple M1. On Apple M1 devices Puppeteer by default downloads the version for Intel's processor which runs via Rosetta. It works without any problems, however, with this option, you should get more efficient resource usage (CPU and RAM) that could lead to a faster execution time. + +:::danger + +Puppeteer is only [guaranteed to work](https://pptr.dev/faq#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. + +::: + +:::caution + +`PUPPETEER_*` env variables are not accounted for in [`puppeteer-core`](#puppeteer-core). + +::: + +#### puppeteer-core + +Every release since v1.7.0 we publish two packages: + +- [`puppeteer`](https://www.npmjs.com/package/puppeteer) +- [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) + +`puppeteer` is a _product_ for browser automation. When installed, it downloads a version of +Chromium, which it then drives using `puppeteer-core`. Being an end-user product, `puppeteer` supports a bunch of convenient `PUPPETEER_*` env variables to tweak its behavior. + +`puppeteer-core` is a _library_ to help drive anything that supports DevTools protocol. `puppeteer-core` doesn't download Chromium when installed. Being a library, `puppeteer-core` is fully driven +through its programmatic interface and disregards all the `PUPPETEER_*` env variables. + +To sum up, the only differences between `puppeteer-core` and `puppeteer` are: + +- `puppeteer-core` doesn't automatically download Chromium when installed. +- `puppeteer-core` ignores all `PUPPETEER_*` env variables. + +In most cases, you'll be fine using the `puppeteer` package. + +However, you should use `puppeteer-core` if: + +- you're building another end-user product or library atop of DevTools protocol. For example, one might build a PDF generator using `puppeteer-core` and write a custom `install.js` script that downloads [`headless_shell`](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) instead of Chromium to save disk space. +- you're bundling Puppeteer to use in Chrome Extension / browser with the DevTools protocol where downloading an additional Chromium binary is unnecessary. +- you're building a set of tools where `puppeteer-core` is one of the ingredients and you want to postpone `install.js` script execution until Chromium is about to be used. + +When using `puppeteer-core`, remember to change the _include_ line: + +```ts +const puppeteer = require('puppeteer-core'); +``` + +You will then need to call [`puppeteer.connect`](https://pptr.dev/api/puppeteer.puppeteer.connect) or [`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) with an explicit `executablePath` or `channel` option. + +### Usage + +Puppeteer follows the latest [maintenance LTS](https://github.com/nodejs/Release#release-schedule) version of Node. + +Puppeteer will be familiar to people using other browser testing frameworks. You create an instance +of `Browser`, open pages, and then manipulate them with [Puppeteer's API](https://pptr.dev/api). + +**Example** - navigating to https://example.com and saving a screenshot as _example.png_: + +Save file as **example.js** + +```ts +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + await page.screenshot({path: 'example.png'}); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node example.js +``` + +Puppeteer sets an initial page size to 800×600px, which defines the screenshot size. The page size can be customized with [`Page.setViewport()`](https://pptr.dev/api/puppeteer.page.setviewport). + +**Example** - create a PDF. + +Save file as **hn.js** + +```ts +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://news.ycombinator.com', { + waitUntil: 'networkidle2', + }); + await page.pdf({path: 'hn.pdf', format: 'a4'}); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node hn.js +``` + +See [`Page.pdf`](https://pptr.dev/api/puppeteer.page.pdf) for more information about creating pdfs. + +**Example** - evaluate script in the context of the page + +Save file as **get-dimensions.js** + +```ts +const puppeteer = require('puppeteer'); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('https://example.com'); + + // Get the "viewport" of the page, as reported by the page. + const dimensions = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio, + }; + }); + + console.log('Dimensions:', dimensions); + + await browser.close(); +})(); +``` + +Execute script on the command line + +```bash +node get-dimensions.js +``` + +See [`Page.evaluate`](https://pptr.dev/api/puppeteer.page.evaluate) and related methods like [`Page.evaluateOnNewDocument`](https://pptr.dev/api/puppeteer.page.evaluateOnNewDocument) and [`Page.exposeFunction`](https://pptr.dev/api/puppeteer.page.exposeFunction). + +<!-- [END getstarted] --> + +### Running in Docker + +Puppeteer offers a Docker image that includes Chromium along with the required dependencies and a pre-installed Puppeteer version. The image is available via the [GitHub Container Registry](https://github.com/puppeteer/puppeteer/pkgs/container/puppeteer). The latest image is tagged as `latest` and other tags match Puppeteer versions. For example, + +```sh +docker pull ghcr.io/puppeteer/puppeteer:latest # pulls the latest +docker pull ghcr.io/puppeteer/puppeteer:16.1.0 # pulls the image that contains Puppeteer v16.1.0 +``` + +The image is meant for running the browser in the sandbox mode and therefore, running the image requires the `SYS_ADMIN` capability. For example, + +```sh +docker run -i --init --cap-add=SYS_ADMIN --rm ghcr.io/puppeteer/puppeteer:latest node -e "`cat docker/test/smoke-test.js`" +``` + +Replace the path to [`smoke-test.js`](https://raw.githubusercontent.com/puppeteer/puppeteer/main/docker/test/smoke-test.js) with a path to your script. +The script can import or require the `puppeteer` module because it's pre-installed inside the image. + +Currently, the image includes the LTS version of Node.js. If you need to build an image based on a different base image, you can use our [`Dockerfile`](https://github.com/puppeteer/puppeteer/blob/main/docker/Dockerfile) as the starting point. + +### Working with Chrome Extensions + +Puppeteer can be used for testing Chrome Extensions. + +:::caution + +Extensions in Chrome / Chromium currently only work in non-headless mode and experimental Chrome headless mode. + +::: + +The following is code for getting a handle to the [background page](https://developer.chrome.com/extensions/background_pages) of an extension whose source is located in `./my-extension`: + +```ts +const puppeteer = require('puppeteer'); + +(async () => { + const pathToExtension = require('path').join(__dirname, 'my-extension'); + const browser = await puppeteer.launch({ + headless: 'chrome', + args: [ + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + ], + }); + const backgroundPageTarget = await browser.waitForTarget( + target => target.type() === 'background_page' + ); + const backgroundPage = await backgroundPageTarget.page(); + // Test the background page as you would any other page. + await browser.close(); +})(); +``` + +:::note + +Chrome Manifest V3 extensions have a background ServiceWorker of type 'service_worker', instead of a page of type 'background_page'. + +::: + +:::note + +It is not yet possible to test extension popups or content scripts. + +::: + +<!-- [START runtimesettings] --> + +## Default runtime settings + +**1. Uses Headless mode** + +Puppeteer launches Chromium in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). To launch a full version of Chromium, set the [`headless` option](https://pptr.dev/api/puppeteer.browserlaunchargumentoptions.headless) when launching a browser: + +```ts +const browser = await puppeteer.launch({headless: false}); // default is true +``` + +**2. Runs a bundled version of Chromium** + +By default, Puppeteer downloads and uses a specific version of Chromium so its API +is guaranteed to work out of the box. To use Puppeteer with a different version of Chrome or Chromium, +pass in the executable's path when creating a `Browser` instance: + +```ts +const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); +``` + +You can also use Puppeteer with Firefox Nightly (experimental support). See [`Puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch) for more information. + +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**. + +<!-- [END runtimesettings] --> + +## Resources + +- [API Documentation](https://pptr.dev/api) +- [Examples](https://github.com/puppeteer/puppeteer/tree/main/examples) +- [Community list of Puppeteer resources](https://github.com/transitive-bullshit/awesome-puppeteer) + +<!-- [START debugging] --> + +## Debugging tips + +1. Turn off headless mode - sometimes it's useful to see what the browser is + displaying. Instead of launching in headless mode, launch a full version of + the browser using `headless: false`: + + ```ts + const browser = await puppeteer.launch({headless: false}); + ``` + +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the + specified amount of milliseconds. It's another way to help see what's going on. + + ```ts + const browser = await puppeteer.launch({ + headless: false, + slowMo: 250, // slow down by 250ms + }); + ``` + +3. Capture console output - You can listen for the `console` event. + This is also handy when debugging code in `page.evaluate()`: + + ```ts + page.on('console', msg => console.log('PAGE LOG:', msg.text())); + + await page.evaluate(() => console.log(`url is ${location.href}`)); + ``` + +4. Use debugger in application code browser + + There are two execution context: node.js that is running test code, and the browser + running application code being tested. This lets you debug code in the + application code browser; ie code inside `evaluate()`. + + - Use `{devtools: true}` when launching Puppeteer: + + ```ts + const browser = await puppeteer.launch({devtools: true}); + ``` + + - Change default test timeout: + + jest: `jest.setTimeout(100000);` + + jasmine: `jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;` + + mocha: `this.timeout(100000);` (don't forget to change test to use [function and not '=>'](https://stackoverflow.com/a/23492442)) + + - Add an evaluate statement with `debugger` inside / add `debugger` to an existing evaluate statement: + + ```ts + await page.evaluate(() => { + debugger; + }); + ``` + + The test will now stop executing in the above evaluate statement, and chromium will stop in debug mode. + +5. Use debugger in node.js + + This will let you debug test code. For example, you can step over `await page.click()` in the node.js script and see the click happen in the application code browser. + + Note that you won't be able to run `await page.click()` in + DevTools console due to this [Chromium bug](https://bugs.chromium.org/p/chromium/issues/detail?id=833928). So if + you want to try something out, you have to add it to your test file. + + - Add `debugger;` to your test, eg: + + ```ts + debugger; + await page.click('a[target=_blank]'); + ``` + + - Set `headless` to `false` + - Run `node --inspect-brk`, eg `node --inspect-brk node_modules/.bin/jest tests` + - In Chrome open `chrome://inspect/#devices` and click `inspect` + - In the newly opened test browser, type `F8` to resume test execution + - Now your `debugger` will be hit and you can debug in the test browser + +6. Enable verbose logging - internal DevTools protocol traffic + will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. + + # Basic verbose logging + env DEBUG="puppeteer:*" node script.js + + # Protocol traffic can be rather noisy. This example filters out all Network domain messages + env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network' + +7. Debug your Puppeteer (node) code easily, using [ndb](https://github.com/GoogleChromeLabs/ndb) + +- `npm install -g ndb` (or even better, use [npx](https://github.com/zkat/npx)!) + +- add a `debugger` to your Puppeteer (node) code + +- add `ndb` (or `npx ndb`) before your test command. For example: + + `ndb jest` or `ndb mocha` (or `npx ndb jest` / `npx ndb mocha`) + +- debug your test inside chromium like a boss! + +<!-- [END debugging] --> + +## Contributing + +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/api-extractor.json b/remote/test/puppeteer/api-extractor.json new file mode 100644 index 0000000000..8a80ee1e3e --- /dev/null +++ b/remote/test/puppeteer/api-extractor.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "<projectFolder>/lib/esm/puppeteer/types.d.ts", + "bundledPackages": [], + + "apiReport": { + "enabled": false + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "<projectFolder>/docs/<unscopedPackageName>.api.json" + }, + + "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/commitlint.config.js b/remote/test/puppeteer/commitlint.config.js new file mode 100644 index 0000000000..6f6801b097 --- /dev/null +++ b/remote/test/puppeteer/commitlint.config.js @@ -0,0 +1,23 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-max-line-length': [0, 'always', 100], + 'footer-max-line-length': [0, 'always', 100], + 'subject-case': [0, 'never'], + }, +}; diff --git a/remote/test/puppeteer/compat/README.md b/remote/test/puppeteer/compat/README.md new file mode 100644 index 0000000000..a72ecab4e8 --- /dev/null +++ b/remote/test/puppeteer/compat/README.md @@ -0,0 +1,16 @@ +# Compatibility layer + +This directory provides an additional compatibility layer between ES modules and CommonJS. + +## Why? + +Both `./cjs/compat.ts` and `./esm/compat.ts` are written as ES modules, but `./cjs/compat.ts` can additionally use NodeJS CommonJS globals such as `__dirname` and `require` while these are disabled in ES module mode. For more information, see [Differences between ES modules and CommonJS](https://nodejs.org/api/esm.html#differences-between-es-modules-and-commonjs). + +## Adding exports + +In order to add exports, two things need to be done: + +- The exports must be declared in `src/compat.ts`. +- The exports must be realized in `./cjs/compat.ts` and `./esm/compat.ts`. + +In the event `compat.ts` becomes too large, you can place declarations in another file. Just make sure `./cjs`, `./esm`, and `src` have the same structure. diff --git a/remote/test/puppeteer/compat/cjs/compat.ts b/remote/test/puppeteer/compat/cjs/compat.ts new file mode 100644 index 0000000000..782727e00e --- /dev/null +++ b/remote/test/puppeteer/compat/cjs/compat.ts @@ -0,0 +1,19 @@ +import {dirname} from 'path'; + +/** + * @internal + */ +let puppeteerDirname: string; + +try { + // In some environments, like esbuild, this will throw an error. + // We suppress the error since the bundled binary is not expected + // to be used or installed in this case and, therefore, the + // root directory does not have to be known. + puppeteerDirname = dirname(require.resolve('./compat')); +} catch (error) { + // Fallback to __dirname. + puppeteerDirname = __dirname; +} + +export {puppeteerDirname}; diff --git a/remote/test/puppeteer/compat/cjs/tsconfig.json b/remote/test/puppeteer/compat/cjs/tsconfig.json new file mode 100644 index 0000000000..5429268843 --- /dev/null +++ b/remote/test/puppeteer/compat/cjs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../../lib/cjs/puppeteer", + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/compat/esm/compat.ts b/remote/test/puppeteer/compat/esm/compat.ts new file mode 100644 index 0000000000..8d74765870 --- /dev/null +++ b/remote/test/puppeteer/compat/esm/compat.ts @@ -0,0 +1,22 @@ +import {createRequire} from 'module'; +import {dirname} from 'path'; +import {fileURLToPath} from 'url'; + +const require = createRequire(import.meta.url); + +/** + * @internal + */ +let puppeteerDirname: string; + +try { + // In some environments, like esbuild, this will throw an error. + // We suppress the error since the bundled binary is not expected + // to be used or installed in this case and, therefore, the + // root directory does not have to be known. + puppeteerDirname = dirname(require.resolve('./compat')); +} catch (error) { + puppeteerDirname = dirname(fileURLToPath(import.meta.url)); +} + +export {puppeteerDirname}; diff --git a/remote/test/puppeteer/compat/esm/tsconfig.json b/remote/test/puppeteer/compat/esm/tsconfig.json new file mode 100644 index 0000000000..5afe459a05 --- /dev/null +++ b/remote/test/puppeteer/compat/esm/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../../lib/esm/puppeteer", + "module": "esnext" + } +} diff --git a/remote/test/puppeteer/examples/README.md b/remote/test/puppeteer/examples/README.md new file mode 100644 index 0000000000..3faccc9cc1 --- /dev/null +++ b/remote/test/puppeteer/examples/README.md @@ -0,0 +1,41 @@ +# 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: + +```sh +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. 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..fbe9166ae8 --- /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 = '.titlelink'; + 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..d1ad58a493 --- /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-results .gs-title'; + 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/install.js b/remote/test/puppeteer/install.js new file mode 100644 index 0000000000..87fa7a32db --- /dev/null +++ b/remote/test/puppeteer/install.js @@ -0,0 +1,89 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file is part of public API. + * + * By default, the `puppeteer` package runs this script during the installation + * process unless one of the env flags is provided. + * `puppeteer-core` package doesn't include this step at all. However, it's + * still possible to install a supported browser using this script when + * necessary. + */ + +const compileTypeScriptIfRequired = require('./typescript-if-required.js'); + +async function download() { + await compileTypeScriptIfRequired(); + // need to ensure TS is compiled before loading the installer + const { + downloadBrowser, + logPolitely, + } = require('./lib/cjs/puppeteer/node/install.js'); + + if (process.env.PUPPETEER_SKIP_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" environment variable was found.' + ); + return; + } + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in npm config.' + ); + return; + } + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_DOWNLOAD" was set in project config.' + ); + return; + } + if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.' + ); + return; + } + if ( + process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.' + ); + return; + } + if ( + process.env.NPM_PACKAGE_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || + process.env.npm_package_config_puppeteer_skip_chromium_download + ) { + logPolitely( + '**INFO** Skipping browser download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in project config.' + ); + return; + } + + downloadBrowser(); +} + +download(); diff --git a/remote/test/puppeteer/json-mocha-reporter.js b/remote/test/puppeteer/json-mocha-reporter.js new file mode 100644 index 0000000000..47fc74a9c5 --- /dev/null +++ b/remote/test/puppeteer/json-mocha-reporter.js @@ -0,0 +1,68 @@ +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); + 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..b52604b806 --- /dev/null +++ b/remote/test/puppeteer/moz.yaml @@ -0,0 +1,10 @@ +schema: 1 +bugzilla: + component: Agent + product: Remote Protocol +origin: + description: Headless Chrome Node API + license: Apache-2.0 + name: puppeteer + release: 0d2d99efeca73fba255fb10b28b5d3f50c2e20e4 + url: https://github.com/puppeteer/puppeteer diff --git a/remote/test/puppeteer/package-lock.json b/remote/test/puppeteer/package-lock.json new file mode 100644 index 0000000000..70e3a1267f --- /dev/null +++ b/remote/test/puppeteer/package-lock.json @@ -0,0 +1,13506 @@ +{ + "name": "puppeteer", + "version": "17.1.2", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "puppeteer", + "version": "17.1.2", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1036444", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.8.1" + }, + "devDependencies": { + "@commitlint/cli": "17.0.3", + "@commitlint/config-conventional": "17.0.3", + "@microsoft/api-documenter": "7.19.4", + "@microsoft/api-extractor": "7.29.2", + "@microsoft/api-extractor-model": "7.23.0", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/glob": "7.2.0", + "@types/mime": "3.0.1", + "@types/mocha": "9.1.1", + "@types/node": "18.7.1", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/rimraf": "3.0.2", + "@types/semver": "7.3.11", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.33.0", + "@typescript-eslint/parser": "5.33.0", + "c8": "7.12.0", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "0.15.5", + "eslint": "8.21.0", + "eslint-config-prettier": "8.5.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.16", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "25.2.7", + "glob": "8.0.3", + "gts": "4.0.0", + "husky": "8.0.1", + "jpeg-js": "0.4.4", + "mime": "3.0.0", + "minimist": "1.2.6", + "mocha": "10.0.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "6.0.0", + "prettier": "2.7.1", + "semver": "7.3.7", + "sinon": "14.0.0", + "source-map-support": "0.5.21", + "text-diff": "1.0.1", + "tsd": "0.22.0", + "tsx": "3.8.2", + "typescript": "4.7.4" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "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", + "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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@commitlint/cli": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.0.3.tgz", + "integrity": "sha512-oAo2vi5d8QZnAbtU5+0cR2j+A7PO8zuccux65R/EycwvsZrDVyW518FFrnJK2UQxbRtHFFIG+NjQ6vOiJV0Q8A==", + "dev": true, + "dependencies": { + "@commitlint/format": "^17.0.0", + "@commitlint/lint": "^17.0.3", + "@commitlint/load": "^17.0.3", + "@commitlint/read": "^17.0.0", + "@commitlint/types": "^17.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.19", + "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.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.0.3.tgz", + "integrity": "sha512-HCnzTm5ATwwwzNVq5Y57poS0a1oOOcd5pc1MmBpLbGmSysc4i7F/++JuwtdFPu16sgM3H9J/j2zznRLOSGVO2A==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.0.3.tgz", + "integrity": "sha512-3tLRPQJKapksGE7Kee9axv+9z5I2GDHitDH4q63q7NmNA0wkB+DAorJ0RHz2/K00Zb1/MVdHzhCga34FJvDihQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.0.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "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/@commitlint/config-validator/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==", + "dev": true + }, + "node_modules/@commitlint/ensure": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.0.0.tgz", + "integrity": "sha512-M2hkJnNXvEni59S0QPOnqCKIK52G1XyXBGw51mvh7OXDudCmZ9tZiIPpU882p475Mhx48Ien1MbWjCP1zlyC0A==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.0.0", + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.0.0.tgz", + "integrity": "sha512-nVjL/w/zuqjCqSJm8UfpNaw66V9WzuJtQvEnCrK4jDw6qKTmZB+1JQ8m6BQVZbNBcwfYdDNKnhIhqI0Rk7lgpQ==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/format": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-17.0.0.tgz", + "integrity": "sha512-MZzJv7rBp/r6ZQJDEodoZvdRM0vXu1PfQvMTNWFb8jFraxnISMTnPBWMMjr2G/puoMashwaNM//fl7j8gGV5lA==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.0.0", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.0.3.tgz", + "integrity": "sha512-/wgCXAvPtFTQZxsVxj7owLeRf5wwzcXLaYmrZPR4a87iD4sCvUIRl1/ogYrtOyUmHwWfQsvjqIB4mWE/SqWSnA==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.0.0", + "semver": "7.3.7" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/lint": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.0.3.tgz", + "integrity": "sha512-2o1fk7JUdxBUgszyt41sHC/8Nd5PXNpkmuOo9jvGIjDHzOwXyV0PSdbEVTH3xGz9NEmjohFHr5l+N+T9fcxong==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^17.0.3", + "@commitlint/parse": "^17.0.0", + "@commitlint/rules": "^17.0.0", + "@commitlint/types": "^17.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/load": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.0.3.tgz", + "integrity": "sha512-3Dhvr7GcKbKa/ey4QJ5MZH3+J7QFlARohUow6hftQyNjzoXXROm+RwpBes4dDFrXG1xDw9QPXA7uzrOShCd4bw==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.0.3", + "@commitlint/execute-rule": "^17.0.0", + "@commitlint/resolve-extends": "^17.0.3", + "@commitlint/types": "^17.0.0", + "@types/node": ">=12", + "chalk": "^4.1.0", + "cosmiconfig": "^7.0.0", + "cosmiconfig-typescript-loader": "^2.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "typescript": "^4.6.4" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/message": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.0.0.tgz", + "integrity": "sha512-LpcwYtN+lBlfZijHUdVr8aNFTVpHjuHI52BnfoV01TF7iSLnia0jttzpLkrLmI8HNQz6Vhr9UrxDWtKZiMGsBw==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/parse": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.0.0.tgz", + "integrity": "sha512-cKcpfTIQYDG1ywTIr5AG0RAiLBr1gudqEsmAGCTtj8ffDChbBRxm6xXs2nv7GvmJN7msOt7vOKleLvcMmRa1+A==", + "dev": true, + "dependencies": { + "@commitlint/types": "^17.0.0", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/read": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.0.0.tgz", + "integrity": "sha512-zkuOdZayKX3J6F6mPnVMzohK3OBrsEdOByIqp4zQjA9VLw1hMsDEFQ18rKgUc2adkZar+4S01QrFreDCfZgbxA==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^17.0.0", + "@commitlint/types": "^17.0.0", + "fs-extra": "^10.0.0", + "git-raw-commits": "^2.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.0.3.tgz", + "integrity": "sha512-H/RFMvrcBeJCMdnVC4i8I94108UDccIHrTke2tyQEg9nXQnR5/Hd6MhyNWkREvcrxh9Y+33JLb+PiPiaBxCtBA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^17.0.3", + "@commitlint/types": "^17.0.0", + "import-fresh": "^3.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/rules": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.0.0.tgz", + "integrity": "sha512-45nIy3dERKXWpnwX9HeBzK5SepHwlDxdGBfmedXhL30fmFCkJOdxHyOJsh0+B0RaVsLGT01NELpfzJUmtpDwdQ==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^17.0.0", + "@commitlint/message": "^17.0.0", + "@commitlint/to-lines": "^17.0.0", + "@commitlint/types": "^17.0.0", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.0.0.tgz", + "integrity": "sha512-nEi4YEz04Rf2upFbpnEorG8iymyH7o9jYIVFBG1QdzebbIFET3ir+8kQvCZuBE5pKCtViE4XBUsRZz139uFrRQ==", + "dev": true, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/top-level": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.0.0.tgz", + "integrity": "sha512-dZrEP1PBJvodNWYPOYiLWf6XZergdksKQaT6i1KSROLdjf5Ai0brLOv5/P+CPxBeoj3vBxK4Ax8H1Pg9t7sHIQ==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/types": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.0.0.tgz", + "integrity": "sha512-hBAw6U+SkAT5h47zDMeOu3HSiD0SODw4Aq7rRNh1ceUmL7GyLKYhPbUvlRWqZ65XjBLPHZhFyQlRaPNz8qvUyQ==", + "dev": true, + "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.3.3", + "resolved": "https://registry.npmjs.org/@esbuild-kit/cjs-loader/-/cjs-loader-2.3.3.tgz", + "integrity": "sha512-Rt4O1mXlPEDVxvjsHLgbtHVdUXYK9C1/6ThpQnt7FaXIjUOsI6qhHYMgALhNnlIMZffag44lXd6Dqgx3xALbpQ==", + "dev": true, + "dependencies": { + "@esbuild-kit/core-utils": "^2.1.0", + "get-tsconfig": "^4.1.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-2.2.0.tgz", + "integrity": "sha512-RsVE6OT7yINJ27+daxemksMJFR+jdTEATivyomEXUxMsRmQMD1e99ogf1hifxVmdrnyLmvmfR4iAhXw/HMbRvQ==", + "dev": true, + "dependencies": { + "esbuild": "~0.15.4", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.4.2.tgz", + "integrity": "sha512-N9dPKAj8WOx6djVnStgILWXip4fjDcBk9L7azO0/uQDpu8Ee0eaL78mkN4Acid9BzvNAKWwdYXFJZnsVahNEew==", + "dev": true, + "dependencies": { + "@esbuild-kit/core-utils": "^2.1.0", + "get-tsconfig": "^4.1.0" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.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" + } + }, + "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/@humanwhocodes/config-array": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/@jest/types/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "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.19.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.19.4.tgz", + "integrity": "sha512-HWStmTGjfVTAGrJc8dWnVHWFXOpYpGzXGSc9zGNz94Bn/+vKAPVk0hKBtT1R2Oz4AanhqrsRvQJ2ELlgspuUDg==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.23.0", + "@microsoft/tsdoc": "0.14.1", + "@rushstack/node-core-library": "3.50.1", + "@rushstack/ts-command-line": "4.12.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.17.0" + }, + "bin": { + "api-documenter": "bin/api-documenter" + } + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.29.2.tgz", + "integrity": "sha512-MwT/Xi1DperfrBO+SU3f/xKdyR6bMvk59/WN6w7g1rHmDBMegan3Ya6npMo+abJAgQOtp6uExY/elHXcYE/Ofw==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.23.0", + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.50.1", + "@rushstack/rig-package": "0.3.14", + "@rushstack/ts-command-line": "4.12.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.7.4" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.23.0.tgz", + "integrity": "sha512-h+2aVyf8IYidPZp+N+yIc/LY/aBwRZ1Vxlsx7rU31807bba5ScJ94bj7OjsPMje0vRYALf+yjZToYT0HdP6omA==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.50.1" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", + "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz", + "integrity": "sha512-2RqkwiD4uN6MLnHFljqBlZIXlt/SaUT6cuogU1w2ARw4nKuuppSmR0+s+NC+7kXBQykd9zzu0P4HtBpZT5zBpQ==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.1", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.50.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.50.1.tgz", + "integrity": "sha512-9d2xE7E9yQEBS6brTptdP8cslt6iL5+UnkY2lRxQQ4Q/jlXtsrWCCJCxwr56W/eJEe9YT/yHR4mMn5QY64Ps2w==", + "dev": true, + "dependencies": { + "@types/node": "12.20.24", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "timsort": "~0.3.0", + "z-schema": "~5.0.2" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/@types/node": { + "version": "12.20.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz", + "integrity": "sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==", + "dev": true + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.14.tgz", + "integrity": "sha512-Ic9EN3kWJCK6iOxEDtwED9nrM146zCDrQaUxbeGOF+q/VLZ/HNHPw+aLqrqmTl0ZT66Sf75Qk6OG+rySjTorvQ==", + "dev": true, + "dependencies": { + "resolve": "~1.17.0", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.12.2.tgz", + "integrity": "sha512-poBtnumLuWmwmhCEkVAgynWgtnF9Kygekxyp4qtQUSbBrkuyPQTL85c8Cva1YfoUpOdOXxezMAkUt0n5SNKGqw==", + "dev": true, + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", + "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@swc/core": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.237.tgz", + "integrity": "sha512-Di1WUEA913jzOJLJ59ouzOWudQym3ODCxRnSKcOT7dgOxb+X76Xpj1kBkN5u5o2SsTjYj32Cm34rQM4TjzXZIg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "bin": { + "swcx": "run_swcx.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-android-arm-eabi": "1.2.237", + "@swc/core-android-arm64": "1.2.237", + "@swc/core-darwin-arm64": "1.2.237", + "@swc/core-darwin-x64": "1.2.237", + "@swc/core-freebsd-x64": "1.2.237", + "@swc/core-linux-arm-gnueabihf": "1.2.237", + "@swc/core-linux-arm64-gnu": "1.2.237", + "@swc/core-linux-arm64-musl": "1.2.237", + "@swc/core-linux-x64-gnu": "1.2.237", + "@swc/core-linux-x64-musl": "1.2.237", + "@swc/core-win32-arm64-msvc": "1.2.237", + "@swc/core-win32-ia32-msvc": "1.2.237", + "@swc/core-win32-x64-msvc": "1.2.237" + } + }, + "node_modules/@swc/core-android-arm-eabi": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.237.tgz", + "integrity": "sha512-Jc9EGzp9zJzV3piPy2w92glNbp3MW7jDGk8c62pJbRBBymbJ2DSujmFiHaU9vTwFdGHRpJAMAuHGotBYSgRigw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.122" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-android-arm64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.237.tgz", + "integrity": "sha512-nJVCxuz1p6/H7RSv2vz5hsxp0oCrg/Gke0OitWgLvEOtuw9hcyAeYeK4/hYsT0DSt1qqagFj8W+tkHcJcyl55g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-android-arm64/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.237.tgz", + "integrity": "sha512-Epq+MDE9M92h2DyEaY3XP1uCUI9KFms7rtYeCQxqB1Md84WSpXitg/2rloeDJYBU73y4i/Gv1Zew+7Noimzptg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.237.tgz", + "integrity": "sha512-CXqSyZV62aG5ybMjJg7SiJAPvQxshfhCwV/Mp5InlABrQoiyyNyri1yQFpWWMx0ZVmiMiLMkFfJjcuUTpSSWkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-freebsd-x64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.237.tgz", + "integrity": "sha512-uW7FLWzEH8JOWO8ydXt47Fg1qD76zUGrCGNA3YXvNUal5I9k/KWfw4SbL8XZBRlES3lyBBs2Gl6tJiv+RdgIkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-freebsd-x64/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.237.tgz", + "integrity": "sha512-i76H/tvYDGjqAaPn58tAMxDNlj91Mp4nVscCn0iJ4sv8/zHXInYlYCnDaPIUsjudh9C3V6rXzKqdEVthGo/Vhw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.237.tgz", + "integrity": "sha512-kOQtBtzq+U1edoi7aVazuGN6zWV/61ecO4jHBxYsR2YKePLBh9fpoZ8QKByprlN1l3rGFfI4nuRFEwdMQh2f/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.237.tgz", + "integrity": "sha512-8NcG6oY323JmsUvxwTdt55lCvDZmQoiJn0tkp/4GXNo/KVn1tJ6GCAgQ43vohCHPitOWokPyaIOdn0HbZrbbZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.237.tgz", + "integrity": "sha512-bEsroO1ysD8mF2H6aQHz8OjjdVEODXoK3tzGB2jyFqNxxfai3St0c9vF+iOnOmiwXEKZiEU268gZO6uki34TlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.237.tgz", + "integrity": "sha512-xU9CwzyWWlW0kJ6/AVrXH8hTUCsWWLtL32gcpgAdEyiwUlRBD5MvqPGkEsa0sMvGOuCkUPYRflPAFFc5aLRFiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.237.tgz", + "integrity": "sha512-ySZU45r2KAfLNOsbm4XdOat2d/d9Tp4M16bY1+f+tdve57Pre6KPBbLjVH8WLZBlYPW/dqcO/Kvey9hYuBEIqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.237.tgz", + "integrity": "sha512-q6oVDD54KphaM1xQDuDjrGvcEskW0ZXett3VsjhWbehcxQnOmfCe1CBKitpJDC53S/S+Xv9rAce9wsIxRClaTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "dependencies": { + "@swc/wasm": "1.2.130" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc/node_modules/@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.237.tgz", + "integrity": "sha512-R9P4DkLi46eW+KRRh3le/OWjuDwb76SdBSMLC9kQ5iSxaXkQxGFcrruKiw/SfXyLylSVY+WQ5dy7b5S8QFQQdA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/wasm": { + "version": "1.2.122", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.122.tgz", + "integrity": "sha512-sM1VCWQxmNhFtdxME+8UXNyPNhxNu7zdb6ikWpz0YKAQQFRGT5ThZgJrubEpah335SUToNg8pkdDF7ibVCjxbQ==", + "dev": true, + "optional": true, + "peer": true + }, + "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.7.4", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-jbtC+RgKZ9Kk65zuRZbKLTACf+tvFW4Rfq0JEMXrlmV3P3yme+Hm+pnb5fJRyt61SjIitcrC810wj7+1tgsEmg==", + "dev": true, + "bin": { + "tsc": "typescript/bin/tsc", + "tsserver": "typescript/bin/tsserver" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.2.tgz", + "integrity": "sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.52.tgz", + "integrity": "sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz", + "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==", + "devOptional": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/pixelmatch": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.4.tgz", + "integrity": "sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pngjs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz", + "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/progress": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.5.tgz", + "integrity": "sha512-ZYYVc/kSMkhH9W/4dNK/sLNra3cnkfT2nJyOAIDY+C2u6w72wa0s1aXAezVtbTsnN8HID1uhXCrLwDE2ZXpplg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/proxy-from-env": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.1.tgz", + "integrity": "sha512-luG++TFHyS61eKcfkR1CVV6a1GMNXDjtqEQIIfaSHax75xp0HU3SlezjOi1yqubJwrG8e9DeW59n6wTblIDwFg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dev": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.11.tgz", + "integrity": "sha512-R9HhjC4aKx3jL0FLwU7x6qMTysTvLh7jesRslXmxgCOXZwyh5dsnmrPQQToMyess8D4U+8G9x9mBFZoC/1o/Tw==", + "dev": true + }, + "node_modules/@types/sinon": { + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", + "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "node_modules/@types/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-qlsQyIY9sN7p221xHuXKNoMfUenOcvEBN4zI8dGsYbYCqHtTarXOEXSIgUnK+GcR0fZDse6pAIc5pIrCh9NefQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "node_modules/@types/tar-stream": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.2.2.tgz", + "integrity": "sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", + "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unbzip2-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/unbzip2-stream/-/unbzip2-stream-1.4.0.tgz", + "integrity": "sha512-4ZM9zX+6BbEVgo3xCmsM9PXPz94jl1XM0e8dCg3gNn3xPsxYoqxi/A1/WtpIA9FW2BL65eZhlJrHOtKLZP/RPg==", + "dev": true, + "dependencies": { + "@types/through": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz", + "integrity": "sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/type-utils": "5.33.0", + "@typescript-eslint/utils": "5.33.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.33.0.tgz", + "integrity": "sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz", + "integrity": "sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0" + }, + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz", + "integrity": "sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.33.0", + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.33.0.tgz", + "integrity": "sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==", + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz", + "integrity": "sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0", + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.0.tgz", + "integrity": "sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz", + "integrity": "sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.33.0", + "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/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==" + }, + "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/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "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/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/c8": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", + "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "dev": true, + "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/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "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/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "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/camelcase-keys/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "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==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, + "node_modules/commonmark": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.30.0.tgz", + "integrity": "sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-5.0.0.tgz", + "integrity": "sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==", + "dev": true, + "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.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-2.0.2.tgz", + "integrity": "sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7", + "ts-node": "^10.8.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=7", + "typescript": ">=3" + } + }, + "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", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "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/devtools-protocol": { + "version": "0.0.1036444", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz", + "integrity": "sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw==" + }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "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.1", + "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.4", + "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.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "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", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "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.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "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.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.3", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-formatter-codeframe": { + "version": "7.32.1", + "resolved": "https://registry.npmjs.org/eslint-formatter-codeframe/-/eslint-formatter-codeframe-7.32.1.tgz", + "integrity": "sha512-DK/3Q3+zVKq/7PdSYiCxPrsDF8H/TRMK5n8Hziwr4IMkMy+XiKSwbpj25AdajS63I/B61Snetq4uVvX9fOLyAg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/eslint-formatter-codeframe/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "dev": true, + "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.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/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/eslint-import-resolver-node/node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "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/eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/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/eslint-module-utils/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.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": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-plugin-import/node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "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/eslint-plugin-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-local/-/eslint-plugin-local-1.0.0.tgz", + "integrity": "sha512-bcwcQnKL/Iw5Vi/F2lG1he5oKD2OGjhsLmrcctkWrWq5TujgiaYb0cj3pZgr3XI54inNVnneOFdAx1daLoYLJQ==", + "dev": true + }, + "node_modules/eslint-plugin-mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.1.0.tgz", + "integrity": "sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "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.16", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.16.tgz", + "integrity": "sha512-F/RWMnyDQuGlg82vQEFHQtGyWi7++XJKdYNn0ulIbyMOFqYIjoJOUdE6olORxgwgLkpJxsCJpJbTHgxJ/ggfXw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "0.16.1" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/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/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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/espree": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.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", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/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", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "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": "25.2.7", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.7.tgz", + "integrity": "sha512-yA+U2Ph0MkMsJ9N8q5hs9WgWI6oJYfecdXta6LkP/alY/jZZL1MHlJ2wbLh60Ucqf3G+51ytbqV3mlGfmxkpNw==", + "dev": true, + "dependencies": { + "@jest/types": "^25.2.6", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.2.7", + "jest-message-util": "^25.2.6", + "jest-regex-util": "^25.2.6" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "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", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "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==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "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-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "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", + "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/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "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/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "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.2.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz", + "integrity": "sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==", + "dev": true, + "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.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "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": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "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.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "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", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/gts": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/gts/-/gts-4.0.0.tgz", + "integrity": "sha512-DTyK7X2PLkv9D5/ttQJvxHqdzDXfbB3Am+O2NVzMcxNq5WJDgCZfjxDjI1NxxQLALBYuraxMtJzdyjxZ5sfzwA==", + "dev": true, + "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/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gts/node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "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/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/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "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/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", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/irregular-plurals": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", + "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "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-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "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.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "dev": true, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "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", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "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", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "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==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "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/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "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", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/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/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "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/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/mocha/node_modules/glob/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/mocha/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/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "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-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "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", + "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/npm-run-all/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/npm-run-all/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/npm-run-all/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/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "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", + "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/npm-run-all/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/npm-run-all/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/npm-run-all/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/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "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==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "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/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "dev": true, + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "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", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "dependencies": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rambda": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.1.4.tgz", + "integrity": "sha512-bPK8sSiVHIC7CqdWga8R+hRi5hfc4hK6S01lZW4KrLwSNryQoKaCOJA9GNiF20J7Nbe1vejRfR37/ASQXFL5EA==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "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": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/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-pkg/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-pkg/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-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" + }, + "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", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "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==", + "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==", + "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/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sinon": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", + "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^6.1.1", + "diff": "^5.0.0", + "nise": "^5.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/stack-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", + "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/string_decoder/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/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz", + "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha512-1BH+X+1hSthZFW+X+JaUkjkkUPwIlLEMJBLANN3hOob3RhEk5snLWNECDnYbgn/m5c5JV7Ersu1Yubaf+05cIA==", + "dev": true + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "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", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "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/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "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", + "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/text-diff": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz", + "integrity": "sha1-bBBZBUNeM3hXN1ydL2ymPkU/9WU=", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "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/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsd": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.22.0.tgz", + "integrity": "sha512-NH+tfEDQ0Ze8gH7TorB6IxYybD+M68EYawe45YNVrbQcydNBfdQHP9IiD0QbnqmwNXrv+l9GAiULT68mo4q/xA==", + "dev": true, + "dependencies": { + "@tsd/typescript": "~4.7.4", + "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", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "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", + "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/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "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.8.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.8.2.tgz", + "integrity": "sha512-Jf9izq3Youry5aEarspf6Gm+v/IE2A2xP7YVhtNH1VSCpM0jjACg7C3oD5rIoLBfXWGJSZj4KKC2bwE0TgLb2Q==", + "dev": true, + "dependencies": { + "@esbuild-kit/cjs-loader": "^2.3.3", + "@esbuild-kit/core-utils": "^2.1.0", + "@esbuild-kit/esm-loader": "^2.4.2" + }, + "bin": { + "tsx": "dist/cli.js" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "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.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "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", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "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": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "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.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", + "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.7", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "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/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", + "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/ws": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "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", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "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.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "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/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "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", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", + "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "dev": true, + "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": "^2.20.3" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.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 + }, + "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 + }, + "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" + } + } + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@commitlint/cli": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.0.3.tgz", + "integrity": "sha512-oAo2vi5d8QZnAbtU5+0cR2j+A7PO8zuccux65R/EycwvsZrDVyW518FFrnJK2UQxbRtHFFIG+NjQ6vOiJV0Q8A==", + "dev": true, + "requires": { + "@commitlint/format": "^17.0.0", + "@commitlint/lint": "^17.0.3", + "@commitlint/load": "^17.0.3", + "@commitlint/read": "^17.0.0", + "@commitlint/types": "^17.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.19", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + } + }, + "@commitlint/config-conventional": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.0.3.tgz", + "integrity": "sha512-HCnzTm5ATwwwzNVq5Y57poS0a1oOOcd5pc1MmBpLbGmSysc4i7F/++JuwtdFPu16sgM3H9J/j2zznRLOSGVO2A==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^5.0.0" + } + }, + "@commitlint/config-validator": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.0.3.tgz", + "integrity": "sha512-3tLRPQJKapksGE7Kee9axv+9z5I2GDHitDH4q63q7NmNA0wkB+DAorJ0RHz2/K00Zb1/MVdHzhCga34FJvDihQ==", + "dev": true, + "requires": { + "@commitlint/types": "^17.0.0", + "ajv": "^8.11.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "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" + } + }, + "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==", + "dev": true + } + } + }, + "@commitlint/ensure": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-17.0.0.tgz", + "integrity": "sha512-M2hkJnNXvEni59S0QPOnqCKIK52G1XyXBGw51mvh7OXDudCmZ9tZiIPpU882p475Mhx48Ien1MbWjCP1zlyC0A==", + "dev": true, + "requires": { + "@commitlint/types": "^17.0.0", + "lodash": "^4.17.19" + } + }, + "@commitlint/execute-rule": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-17.0.0.tgz", + "integrity": "sha512-nVjL/w/zuqjCqSJm8UfpNaw66V9WzuJtQvEnCrK4jDw6qKTmZB+1JQ8m6BQVZbNBcwfYdDNKnhIhqI0Rk7lgpQ==", + "dev": true + }, + "@commitlint/format": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-17.0.0.tgz", + "integrity": "sha512-MZzJv7rBp/r6ZQJDEodoZvdRM0vXu1PfQvMTNWFb8jFraxnISMTnPBWMMjr2G/puoMashwaNM//fl7j8gGV5lA==", + "dev": true, + "requires": { + "@commitlint/types": "^17.0.0", + "chalk": "^4.1.0" + } + }, + "@commitlint/is-ignored": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.0.3.tgz", + "integrity": "sha512-/wgCXAvPtFTQZxsVxj7owLeRf5wwzcXLaYmrZPR4a87iD4sCvUIRl1/ogYrtOyUmHwWfQsvjqIB4mWE/SqWSnA==", + "dev": true, + "requires": { + "@commitlint/types": "^17.0.0", + "semver": "7.3.7" + } + }, + "@commitlint/lint": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.0.3.tgz", + "integrity": "sha512-2o1fk7JUdxBUgszyt41sHC/8Nd5PXNpkmuOo9jvGIjDHzOwXyV0PSdbEVTH3xGz9NEmjohFHr5l+N+T9fcxong==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^17.0.3", + "@commitlint/parse": "^17.0.0", + "@commitlint/rules": "^17.0.0", + "@commitlint/types": "^17.0.0" + } + }, + "@commitlint/load": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.0.3.tgz", + "integrity": "sha512-3Dhvr7GcKbKa/ey4QJ5MZH3+J7QFlARohUow6hftQyNjzoXXROm+RwpBes4dDFrXG1xDw9QPXA7uzrOShCd4bw==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.0.3", + "@commitlint/execute-rule": "^17.0.0", + "@commitlint/resolve-extends": "^17.0.3", + "@commitlint/types": "^17.0.0", + "@types/node": ">=12", + "chalk": "^4.1.0", + "cosmiconfig": "^7.0.0", + "cosmiconfig-typescript-loader": "^2.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "typescript": "^4.6.4" + } + }, + "@commitlint/message": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-17.0.0.tgz", + "integrity": "sha512-LpcwYtN+lBlfZijHUdVr8aNFTVpHjuHI52BnfoV01TF7iSLnia0jttzpLkrLmI8HNQz6Vhr9UrxDWtKZiMGsBw==", + "dev": true + }, + "@commitlint/parse": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-17.0.0.tgz", + "integrity": "sha512-cKcpfTIQYDG1ywTIr5AG0RAiLBr1gudqEsmAGCTtj8ffDChbBRxm6xXs2nv7GvmJN7msOt7vOKleLvcMmRa1+A==", + "dev": true, + "requires": { + "@commitlint/types": "^17.0.0", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.2.2" + } + }, + "@commitlint/read": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-17.0.0.tgz", + "integrity": "sha512-zkuOdZayKX3J6F6mPnVMzohK3OBrsEdOByIqp4zQjA9VLw1hMsDEFQ18rKgUc2adkZar+4S01QrFreDCfZgbxA==", + "dev": true, + "requires": { + "@commitlint/top-level": "^17.0.0", + "@commitlint/types": "^17.0.0", + "fs-extra": "^10.0.0", + "git-raw-commits": "^2.0.0" + } + }, + "@commitlint/resolve-extends": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.0.3.tgz", + "integrity": "sha512-H/RFMvrcBeJCMdnVC4i8I94108UDccIHrTke2tyQEg9nXQnR5/Hd6MhyNWkREvcrxh9Y+33JLb+PiPiaBxCtBA==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^17.0.3", + "@commitlint/types": "^17.0.0", + "import-fresh": "^3.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/rules": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-17.0.0.tgz", + "integrity": "sha512-45nIy3dERKXWpnwX9HeBzK5SepHwlDxdGBfmedXhL30fmFCkJOdxHyOJsh0+B0RaVsLGT01NELpfzJUmtpDwdQ==", + "dev": true, + "requires": { + "@commitlint/ensure": "^17.0.0", + "@commitlint/message": "^17.0.0", + "@commitlint/to-lines": "^17.0.0", + "@commitlint/types": "^17.0.0", + "execa": "^5.0.0" + } + }, + "@commitlint/to-lines": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-17.0.0.tgz", + "integrity": "sha512-nEi4YEz04Rf2upFbpnEorG8iymyH7o9jYIVFBG1QdzebbIFET3ir+8kQvCZuBE5pKCtViE4XBUsRZz139uFrRQ==", + "dev": true + }, + "@commitlint/top-level": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-17.0.0.tgz", + "integrity": "sha512-dZrEP1PBJvodNWYPOYiLWf6XZergdksKQaT6i1KSROLdjf5Ai0brLOv5/P+CPxBeoj3vBxK4Ax8H1Pg9t7sHIQ==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + } + }, + "@commitlint/types": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-17.0.0.tgz", + "integrity": "sha512-hBAw6U+SkAT5h47zDMeOu3HSiD0SODw4Aq7rRNh1ceUmL7GyLKYhPbUvlRWqZ65XjBLPHZhFyQlRaPNz8qvUyQ==", + "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.3.3", + "resolved": "https://registry.npmjs.org/@esbuild-kit/cjs-loader/-/cjs-loader-2.3.3.tgz", + "integrity": "sha512-Rt4O1mXlPEDVxvjsHLgbtHVdUXYK9C1/6ThpQnt7FaXIjUOsI6qhHYMgALhNnlIMZffag44lXd6Dqgx3xALbpQ==", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^2.1.0", + "get-tsconfig": "^4.1.0" + } + }, + "@esbuild-kit/core-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-2.2.0.tgz", + "integrity": "sha512-RsVE6OT7yINJ27+daxemksMJFR+jdTEATivyomEXUxMsRmQMD1e99ogf1hifxVmdrnyLmvmfR4iAhXw/HMbRvQ==", + "dev": true, + "requires": { + "esbuild": "~0.15.4", + "source-map-support": "^0.5.21" + } + }, + "@esbuild-kit/esm-loader": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.4.2.tgz", + "integrity": "sha512-N9dPKAj8WOx6djVnStgILWXip4fjDcBk9L7azO0/uQDpu8Ee0eaL78mkN4Acid9BzvNAKWwdYXFJZnsVahNEew==", + "dev": true, + "requires": { + "@esbuild-kit/core-utils": "^2.1.0", + "get-tsconfig": "^4.1.0" + } + }, + "@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "dev": true, + "optional": true + }, + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.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": { + "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" + } + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + }, + "dependencies": { + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "@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.19.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.19.4.tgz", + "integrity": "sha512-HWStmTGjfVTAGrJc8dWnVHWFXOpYpGzXGSc9zGNz94Bn/+vKAPVk0hKBtT1R2Oz4AanhqrsRvQJ2ELlgspuUDg==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.23.0", + "@microsoft/tsdoc": "0.14.1", + "@rushstack/node-core-library": "3.50.1", + "@rushstack/ts-command-line": "4.12.2", + "colors": "~1.2.1", + "js-yaml": "~3.13.1", + "resolve": "~1.17.0" + } + }, + "@microsoft/api-extractor": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.29.2.tgz", + "integrity": "sha512-MwT/Xi1DperfrBO+SU3f/xKdyR6bMvk59/WN6w7g1rHmDBMegan3Ya6npMo+abJAgQOtp6uExY/elHXcYE/Ofw==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.23.0", + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.50.1", + "@rushstack/rig-package": "0.3.14", + "@rushstack/ts-command-line": "4.12.2", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.7.4" + } + }, + "@microsoft/api-extractor-model": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.23.0.tgz", + "integrity": "sha512-h+2aVyf8IYidPZp+N+yIc/LY/aBwRZ1Vxlsx7rU31807bba5ScJ94bj7OjsPMje0vRYALf+yjZToYT0HdP6omA==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.50.1" + } + }, + "@microsoft/tsdoc": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", + "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz", + "integrity": "sha512-2RqkwiD4uN6MLnHFljqBlZIXlt/SaUT6cuogU1w2ARw4nKuuppSmR0+s+NC+7kXBQykd9zzu0P4HtBpZT5zBpQ==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.1", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rushstack/node-core-library": { + "version": "3.50.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.50.1.tgz", + "integrity": "sha512-9d2xE7E9yQEBS6brTptdP8cslt6iL5+UnkY2lRxQQ4Q/jlXtsrWCCJCxwr56W/eJEe9YT/yHR4mMn5QY64Ps2w==", + "dev": true, + "requires": { + "@types/node": "12.20.24", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "timsort": "~0.3.0", + "z-schema": "~5.0.2" + }, + "dependencies": { + "@types/node": { + "version": "12.20.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz", + "integrity": "sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.14.tgz", + "integrity": "sha512-Ic9EN3kWJCK6iOxEDtwED9nrM146zCDrQaUxbeGOF+q/VLZ/HNHPw+aLqrqmTl0ZT66Sf75Qk6OG+rySjTorvQ==", + "dev": true, + "requires": { + "resolve": "~1.17.0", + "strip-json-comments": "~3.1.1" + } + }, + "@rushstack/ts-command-line": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.12.2.tgz", + "integrity": "sha512-poBtnumLuWmwmhCEkVAgynWgtnF9Kygekxyp4qtQUSbBrkuyPQTL85c8Cva1YfoUpOdOXxezMAkUt0n5SNKGqw==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", + "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@swc/core": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.237.tgz", + "integrity": "sha512-Di1WUEA913jzOJLJ59ouzOWudQym3ODCxRnSKcOT7dgOxb+X76Xpj1kBkN5u5o2SsTjYj32Cm34rQM4TjzXZIg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/core-android-arm-eabi": "1.2.237", + "@swc/core-android-arm64": "1.2.237", + "@swc/core-darwin-arm64": "1.2.237", + "@swc/core-darwin-x64": "1.2.237", + "@swc/core-freebsd-x64": "1.2.237", + "@swc/core-linux-arm-gnueabihf": "1.2.237", + "@swc/core-linux-arm64-gnu": "1.2.237", + "@swc/core-linux-arm64-musl": "1.2.237", + "@swc/core-linux-x64-gnu": "1.2.237", + "@swc/core-linux-x64-musl": "1.2.237", + "@swc/core-win32-arm64-msvc": "1.2.237", + "@swc/core-win32-ia32-msvc": "1.2.237", + "@swc/core-win32-x64-msvc": "1.2.237" + } + }, + "@swc/core-android-arm-eabi": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.237.tgz", + "integrity": "sha512-Jc9EGzp9zJzV3piPy2w92glNbp3MW7jDGk8c62pJbRBBymbJ2DSujmFiHaU9vTwFdGHRpJAMAuHGotBYSgRigw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.122" + } + }, + "@swc/core-android-arm64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.237.tgz", + "integrity": "sha512-nJVCxuz1p6/H7RSv2vz5hsxp0oCrg/Gke0OitWgLvEOtuw9hcyAeYeK4/hYsT0DSt1qqagFj8W+tkHcJcyl55g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.130" + }, + "dependencies": { + "@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "@swc/core-darwin-arm64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.237.tgz", + "integrity": "sha512-Epq+MDE9M92h2DyEaY3XP1uCUI9KFms7rtYeCQxqB1Md84WSpXitg/2rloeDJYBU73y4i/Gv1Zew+7Noimzptg==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-darwin-x64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.237.tgz", + "integrity": "sha512-CXqSyZV62aG5ybMjJg7SiJAPvQxshfhCwV/Mp5InlABrQoiyyNyri1yQFpWWMx0ZVmiMiLMkFfJjcuUTpSSWkQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-freebsd-x64": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.237.tgz", + "integrity": "sha512-uW7FLWzEH8JOWO8ydXt47Fg1qD76zUGrCGNA3YXvNUal5I9k/KWfw4SbL8XZBRlES3lyBBs2Gl6tJiv+RdgIkQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.130" + }, + "dependencies": { + "@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.237.tgz", + "integrity": "sha512-i76H/tvYDGjqAaPn58tAMxDNlj91Mp4nVscCn0iJ4sv8/zHXInYlYCnDaPIUsjudh9C3V6rXzKqdEVthGo/Vhw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.130" + }, + "dependencies": { + "@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.237.tgz", + "integrity": "sha512-kOQtBtzq+U1edoi7aVazuGN6zWV/61ecO4jHBxYsR2YKePLBh9fpoZ8QKByprlN1l3rGFfI4nuRFEwdMQh2f/Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.237.tgz", + "integrity": "sha512-8NcG6oY323JmsUvxwTdt55lCvDZmQoiJn0tkp/4GXNo/KVn1tJ6GCAgQ43vohCHPitOWokPyaIOdn0HbZrbbZw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.237.tgz", + "integrity": "sha512-bEsroO1ysD8mF2H6aQHz8OjjdVEODXoK3tzGB2jyFqNxxfai3St0c9vF+iOnOmiwXEKZiEU268gZO6uki34TlA==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.237.tgz", + "integrity": "sha512-xU9CwzyWWlW0kJ6/AVrXH8hTUCsWWLtL32gcpgAdEyiwUlRBD5MvqPGkEsa0sMvGOuCkUPYRflPAFFc5aLRFiw==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.237.tgz", + "integrity": "sha512-ySZU45r2KAfLNOsbm4XdOat2d/d9Tp4M16bY1+f+tdve57Pre6KPBbLjVH8WLZBlYPW/dqcO/Kvey9hYuBEIqw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.130" + }, + "dependencies": { + "@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.237.tgz", + "integrity": "sha512-q6oVDD54KphaM1xQDuDjrGvcEskW0ZXett3VsjhWbehcxQnOmfCe1CBKitpJDC53S/S+Xv9rAce9wsIxRClaTw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@swc/wasm": "1.2.130" + }, + "dependencies": { + "@swc/wasm": { + "version": "1.2.130", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.130.tgz", + "integrity": "sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "@swc/core-win32-x64-msvc": { + "version": "1.2.237", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.237.tgz", + "integrity": "sha512-R9P4DkLi46eW+KRRh3le/OWjuDwb76SdBSMLC9kQ5iSxaXkQxGFcrruKiw/SfXyLylSVY+WQ5dy7b5S8QFQQdA==", + "dev": true, + "optional": true, + "peer": true + }, + "@swc/wasm": { + "version": "1.2.122", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.122.tgz", + "integrity": "sha512-sM1VCWQxmNhFtdxME+8UXNyPNhxNu7zdb6ikWpz0YKAQQFRGT5ThZgJrubEpah335SUToNg8pkdDF7ibVCjxbQ==", + "dev": true, + "optional": true, + "peer": 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.7.4", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-jbtC+RgKZ9Kk65zuRZbKLTACf+tvFW4Rfq0JEMXrlmV3P3yme+Hm+pnb5fJRyt61SjIitcrC810wj7+1tgsEmg==", + "dev": true + }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/diff": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.2.tgz", + "integrity": "sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==", + "dev": true + }, + "@types/eslint": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", + "integrity": "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.52.tgz", + "integrity": "sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==", + "dev": true + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "@types/mocha": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "@types/node": { + "version": "18.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz", + "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==", + "devOptional": true + }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/pixelmatch": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.4.tgz", + "integrity": "sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/pngjs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.1.tgz", + "integrity": "sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/progress": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.5.tgz", + "integrity": "sha512-ZYYVc/kSMkhH9W/4dNK/sLNra3cnkfT2nJyOAIDY+C2u6w72wa0s1aXAezVtbTsnN8HID1uhXCrLwDE2ZXpplg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/proxy-from-env": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/proxy-from-env/-/proxy-from-env-1.0.1.tgz", + "integrity": "sha512-luG++TFHyS61eKcfkR1CVV6a1GMNXDjtqEQIIfaSHax75xp0HU3SlezjOi1yqubJwrG8e9DeW59n6wTblIDwFg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "@types/semver": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.11.tgz", + "integrity": "sha512-R9HhjC4aKx3jL0FLwU7x6qMTysTvLh7jesRslXmxgCOXZwyh5dsnmrPQQToMyess8D4U+8G9x9mBFZoC/1o/Tw==", + "dev": true + }, + "@types/sinon": { + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", + "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-qlsQyIY9sN7p221xHuXKNoMfUenOcvEBN4zI8dGsYbYCqHtTarXOEXSIgUnK+GcR0fZDse6pAIc5pIrCh9NefQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "@types/tar-stream": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-2.2.2.tgz", + "integrity": "sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/through": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", + "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/unbzip2-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/unbzip2-stream/-/unbzip2-stream-1.4.0.tgz", + "integrity": "sha512-4ZM9zX+6BbEVgo3xCmsM9PXPz94jl1XM0e8dCg3gNn3xPsxYoqxi/A1/WtpIA9FW2BL65eZhlJrHOtKLZP/RPg==", + "dev": true, + "requires": { + "@types/through": "*" + } + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz", + "integrity": "sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/type-utils": "5.33.0", + "@typescript-eslint/utils": "5.33.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.33.0.tgz", + "integrity": "sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz", + "integrity": "sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz", + "integrity": "sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.33.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.33.0.tgz", + "integrity": "sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz", + "integrity": "sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/visitor-keys": "5.33.0", + "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.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.0.tgz", + "integrity": "sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.33.0", + "@typescript-eslint/types": "5.33.0", + "@typescript-eslint/typescript-estree": "5.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz", + "integrity": "sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.33.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "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", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "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" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "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==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "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" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "c8": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.12.0.tgz", + "integrity": "sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==", + "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": { + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "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" + } + } + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "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" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, + "commonmark": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.30.0.tgz", + "integrity": "sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA==", + "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", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "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", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-5.0.0.tgz", + "integrity": "sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==", + "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.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cosmiconfig-typescript-loader": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-2.0.2.tgz", + "integrity": "sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==", + "dev": true, + "requires": { + "cosmiconfig": "^7", + "ts-node": "^10.8.1" + } + }, + "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", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "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", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "devtools-protocol": { + "version": "0.0.1036444", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1036444.tgz", + "integrity": "sha512-0y4f/T8H9lsESV9kKP1HDUXgHxCdniFeJh6Erq+FbdOEvp/Ydp9t8kcAAM5gOd17pMrTDlFWntoHtzzeTUWKNw==" + }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "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.1", + "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.4", + "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.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" + } + }, + "esbuild-android-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "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.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.3", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "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", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "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 + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "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 + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "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" + } + } + } + }, + "eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "requires": {} + }, + "eslint-formatter-codeframe": { + "version": "7.32.1", + "resolved": "https://registry.npmjs.org/eslint-formatter-codeframe/-/eslint-formatter-codeframe-7.32.1.tgz", + "integrity": "sha512-DK/3Q3+zVKq/7PdSYiCxPrsDF8H/TRMK5n8Hziwr4IMkMy+XiKSwbpj25AdajS63I/B61Snetq4uVvX9fOLyAg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "chalk": "^4.0.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + } + } + }, + "eslint-formatter-pretty": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-pretty/-/eslint-formatter-pretty-4.1.0.tgz", + "integrity": "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==", + "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.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "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" + } + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "dependencies": { + "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" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-plugin-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-local/-/eslint-plugin-local-1.0.0.tgz", + "integrity": "sha512-bcwcQnKL/Iw5Vi/F2lG1he5oKD2OGjhsLmrcctkWrWq5TujgiaYb0cj3pZgr3XI54inNVnneOFdAx1daLoYLJQ==", + "dev": true + }, + "eslint-plugin-mocha": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.1.0.tgz", + "integrity": "sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "rambda": "^7.1.0" + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "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", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-tsdoc": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.16.tgz", + "integrity": "sha512-F/RWMnyDQuGlg82vQEFHQtGyWi7++XJKdYNn0ulIbyMOFqYIjoJOUdE6olORxgwgLkpJxsCJpJbTHgxJ/ggfXw==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc-config": "0.16.1" + } + }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, + "eslint-rule-docs": { + "version": "1.1.235", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", + "integrity": "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==", + "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", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.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 + } + } + }, + "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", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "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": "25.2.7", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.7.tgz", + "integrity": "sha512-yA+U2Ph0MkMsJ9N8q5hs9WgWI6oJYfecdXta6LkP/alY/jZZL1MHlJ2wbLh60Ucqf3G+51ytbqV3mlGfmxkpNw==", + "dev": true, + "requires": { + "@jest/types": "^25.2.6", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.2.7", + "jest-message-util": "^25.2.6", + "jest-regex-util": "^25.2.6" + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "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", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "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==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "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" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + }, + "dependencies": { + "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 + } + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-tsconfig": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz", + "integrity": "sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==", + "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.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "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.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "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.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "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", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "gts": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/gts/-/gts-4.0.0.tgz", + "integrity": "sha512-DTyK7X2PLkv9D5/ttQJvxHqdzDXfbB3Am+O2NVzMcxNq5WJDgCZfjxDjI1NxxQLALBYuraxMtJzdyjxZ5sfzwA==", + "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": { + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "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", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "husky": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz", + "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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==" + }, + "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", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "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", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "irregular-plurals": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", + "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "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", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "dev": true + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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 + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.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", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "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", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "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", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "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 + }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "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", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "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" + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "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": { + "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" + } + }, + "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" + } + } + } + }, + "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" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "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" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", + "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": ">=5", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "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", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "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", + "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 + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "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", + "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 + }, + "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" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "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" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "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" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "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" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "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==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "dev": true, + "requires": { + "pngjs": "^6.0.0" + } + }, + "plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "requires": { + "irregular-plurals": "^3.2.0" + } + }, + "pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "rambda": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.1.4.tgz", + "integrity": "sha512-bPK8sSiVHIC7CqdWga8R+hRi5hfc4hK6S01lZW4KrLwSNryQoKaCOJA9GNiF20J7Nbe1vejRfR37/ASQXFL5EA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "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": { + "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" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "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", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "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" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "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==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "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", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "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==", + "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" + } + } + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "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==", + "dev": true + }, + "sinon": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", + "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^9.1.2", + "@sinonjs/samsam": "^6.1.1", + "diff": "^5.0.0", + "nise": "^5.1.1", + "supports-color": "^7.2.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", + "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "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" + }, + "dependencies": { + "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==" + } + } + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz", + "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==", + "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", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha512-1BH+X+1hSthZFW+X+JaUkjkkUPwIlLEMJBLANN3hOob3RhEk5snLWNECDnYbgn/m5c5JV7Ersu1Yubaf+05cIA==", + "dev": true + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "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", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "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", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "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", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "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" + } + } + } + }, + "text-diff": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz", + "integrity": "sha1-bBBZBUNeM3hXN1ydL2ymPkU/9WU=", + "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", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "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" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "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", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tsd": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.22.0.tgz", + "integrity": "sha512-NH+tfEDQ0Ze8gH7TorB6IxYybD+M68EYawe45YNVrbQcydNBfdQHP9IiD0QbnqmwNXrv+l9GAiULT68mo4q/xA==", + "dev": true, + "requires": { + "@tsd/typescript": "~4.7.4", + "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", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "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", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tsx": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.8.2.tgz", + "integrity": "sha512-Jf9izq3Youry5aEarspf6Gm+v/IE2A2xP7YVhtNH1VSCpM0jjACg7C3oD5rIoLBfXWGJSZj4KKC2bwE0TgLb2Q==", + "dev": true, + "requires": { + "@esbuild-kit/cjs-loader": "^2.3.3", + "@esbuild-kit/core-utils": "^2.1.0", + "@esbuild-kit/esm-loader": "^2.4.2", + "fsevents": "~2.3.2" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "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.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "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", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "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", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "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": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "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.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", + "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.7", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "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" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", + "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "ws": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "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.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "dependencies": { + "yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "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", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "z-schema": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", + "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "dev": true, + "requires": { + "commander": "^2.20.3", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } + } + } +} diff --git a/remote/test/puppeteer/package.json b/remote/test/puppeteer/package.json new file mode 100644 index 0000000000..ef1bf03e1b --- /dev/null +++ b/remote/test/puppeteer/package.json @@ -0,0 +1,144 @@ +{ + "name": "puppeteer", + "version": "17.1.2", + "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", + "exports": { + ".": { + "types": "./lib/types.d.ts", + "import": "./lib/esm/puppeteer/puppeteer.js", + "require": "./lib/cjs/puppeteer/puppeteer.js" + }, + "./*": { + "import": "./*", + "require": "./*" + } + }, + "types": "lib/types.d.ts", + "repository": "github:puppeteer/puppeteer", + "engines": { + "node": ">=14.1.0" + }, + "scripts": { + "test": "c8 --check-coverage --lines 93 run-s test:chrome:* test:firefox", + "test:types": "tsd", + "test:install": "scripts/test-install.sh", + "test:firefox": "cross-env PUPPETEER_PRODUCT=firefox MOZ_WEBRENDER=0 PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", + "test:chrome": "run-s test:chrome:*", + "test:chrome:headless": "cross-env HEADLESS=true PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", + "test:chrome:headless-chrome": "cross-env HEADLESS=chrome PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", + "test:chrome:headful": "cross-env HEADLESS=false PUPPETEER_DEFERRED_PROMISE_DEBUG_TIMEOUT=20000 mocha", + "prepublishOnly": "npm run build", + "prepare": "node typescript-if-required.js && husky install", + "lint": "run-s lint:prettier lint:eslint", + "lint:prettier": "prettier --check .", + "lint:eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", + "install": "node install.js", + "generate:sources": "tsx utils/generate_sources.ts", + "generate:artifacts": "tsx utils/generate_artifacts.ts", + "generate:markdown": "tsx utils/generate_docs.ts", + "format": "run-s format:*", + "format:prettier": "prettier --write .", + "format:eslint": "eslint --ext js --ext ts --fix .", + "docs": "run-s build generate:markdown", + "debug": "npm run build:dev && mocha --inspect-brk", + "commitlint": "commitlint --from=HEAD~1", + "clean": "rimraf lib && rimraf test/build", + "check": "run-p check:*", + "check:protocol-revision": "tsx scripts/ensure-correct-devtools-protocol-package", + "check:pinned-deps": "tsx scripts/ensure-pinned-deps", + "build": "npm run build:prod", + "build:dev": "run-s generate:sources build:tsc:dev generate:artifacts", + "build:prod": "run-s generate:sources build:tsc:prod generate:artifacts", + "build:tsc:dev": "tsc -b test", + "build:tsc:prod": "tsc -b tsconfig.lib.json" + }, + "files": [ + "lib", + "install.js", + "typescript-if-required.js", + "!**/*.tsbuildinfo" + ], + "author": "The Chromium Authors", + "license": "Apache-2.0", + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1036444", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.8.1" + }, + "devDependencies": { + "@commitlint/cli": "17.0.3", + "@commitlint/config-conventional": "17.0.3", + "@microsoft/api-documenter": "7.19.4", + "@microsoft/api-extractor": "7.29.2", + "@microsoft/api-extractor-model": "7.23.0", + "@types/debug": "4.1.7", + "@types/diff": "5.0.2", + "@types/glob": "7.2.0", + "@types/mime": "3.0.1", + "@types/mocha": "9.1.1", + "@types/node": "18.7.1", + "@types/pixelmatch": "5.2.4", + "@types/pngjs": "6.0.1", + "@types/progress": "2.0.5", + "@types/proxy-from-env": "1.0.1", + "@types/rimraf": "3.0.2", + "@types/semver": "7.3.11", + "@types/sinon": "10.0.13", + "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.33.0", + "@typescript-eslint/parser": "5.33.0", + "c8": "7.12.0", + "commonmark": "0.30.0", + "cross-env": "7.0.3", + "diff": "5.1.0", + "esbuild": "0.15.5", + "eslint": "8.21.0", + "eslint-config-prettier": "8.5.0", + "eslint-formatter-codeframe": "7.32.1", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-local": "1.0.0", + "eslint-plugin-mocha": "10.1.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-tsdoc": "0.2.16", + "eslint-plugin-unused-imports": "2.0.0", + "esprima": "4.0.1", + "expect": "25.2.7", + "glob": "8.0.3", + "gts": "4.0.0", + "husky": "8.0.1", + "jpeg-js": "0.4.4", + "mime": "3.0.0", + "minimist": "1.2.6", + "mocha": "10.0.0", + "ncp": "2.0.0", + "npm-run-all": "4.1.5", + "pixelmatch": "5.3.0", + "pngjs": "6.0.0", + "prettier": "2.7.1", + "semver": "7.3.7", + "sinon": "14.0.0", + "source-map-support": "0.5.21", + "text-diff": "1.0.1", + "tsd": "0.22.0", + "tsx": "3.8.2", + "typescript": "4.7.4" + } +} diff --git a/remote/test/puppeteer/release-please-config.json b/remote/test/puppeteer/release-please-config.json new file mode 100644 index 0000000000..c018138698 --- /dev/null +++ b/remote/test/puppeteer/release-please-config.json @@ -0,0 +1,8 @@ +{ + "packages": { + ".": { + "releaseType": "node", + "include-component-in-tag": false + } + } +} diff --git a/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts b/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts new file mode 100644 index 0000000000..40b3c3b79f --- /dev/null +++ b/remote/test/puppeteer/scripts/ensure-correct-devtools-protocol-package.ts @@ -0,0 +1,84 @@ +/** + * 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 Chromium 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. + * + * Chromium branches/releases are figured out at a later point in time, so it's + * not true that each Chromium 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 Chromium revision. + */ + +// eslint-disable-next-line import/extensions +import {PUPPETEER_REVISIONS} from '../src/revisions'; +import {execSync} from 'child_process'; + +import packageJson from '../package.json'; + +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); +} + +// find the right revision for our Chromium revision + +const command = `npm view "devtools-protocol@<=0.0.${PUPPETEER_REVISIONS.chromium}" 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 Chromium revision: ${PUPPETEER_REVISIONS.chromium} + 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); diff --git a/remote/test/puppeteer/scripts/ensure-pinned-deps.ts b/remote/test/puppeteer/scripts/ensure-pinned-deps.ts new file mode 100644 index 0000000000..365b231027 --- /dev/null +++ b/remote/test/puppeteer/scripts/ensure-pinned-deps.ts @@ -0,0 +1,43 @@ +/** + * 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 packageJson from '../package.json'; + +const allDeps = {...packageJson.dependencies, ...packageJson.devDependencies}; + +const invalidDeps = new Map<string, string>(); + +for (const [depKey, depValue] of Object.entries(allDeps)) { + 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/scripts/test-install.sh b/remote/test/puppeteer/scripts/test-install.sh new file mode 100755 index 0000000000..15d1a552de --- /dev/null +++ b/remote/test/puppeteer/scripts/test-install.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env sh +set -e + +# All tests are headed by a echo 'Test'. +# The general schema is: +# 1. Check we can install from the tarball. +# 2. The install script works and correctly exits without errors +# 3. Requiring/importing Puppeteer from Node works. + +## Puppeter tests + +echo "Setting up Puppeteer tests..." +ROOTDIR="$(pwd)" +npm pack +tarball="$(realpath puppeteer-*.tgz)" + +echo "Testing... Chrome CommonJS" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer')" +node --eval="require('puppeteer/lib/cjs/puppeteer/revisions.js');" +ls $TMPDIR/node_modules/puppeteer/.local-chromium/ + +echo "Testing... Chrome ES Modules" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +echo '{"type":"module"}' >>$TMPDIR/package.json +npm install --loglevel silent "${tarball}" +node --input-type="module" --eval="import puppeteer from 'puppeteer'" +node --input-type="module" --eval="import 'puppeteer/lib/esm/puppeteer/revisions.js';" +node --input-type="module" --eval=" +import puppeteer from '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(); +})(); +" + +echo "Testing... Chrome ES Modules Destructuring" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +echo '{"type":"module"}' >>$TMPDIR/package.json +npm install --loglevel silent "${tarball}" +node --input-type="module" --eval="import puppeteer from 'puppeteer'" +node --input-type="module" --eval="import 'puppeteer/lib/esm/puppeteer/revisions.js';" +node --input-type="module" --eval=" +import { launch } from 'puppeteer'; +(async () => { + const browser = await launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.screenshot({ path: 'example.png' }); + await browser.close(); +})(); +" + +echo "Testing... Chrome Webpack ES Modules" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +echo '{"type": "module"}' >>$TMPDIR/package.json +npm install --loglevel silent "${tarball}" +npm install -D --loglevel silent webpack webpack-cli@4.9.2 +echo 'export default { + mode: "production", + entry: "./index.js", + target: "node", + output: { + filename: "bundle.cjs", + }, +};' >>$TMPDIR/webpack.config.js +echo " +import puppeteer from '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(); +})(); +" >>$TMPDIR/index.js +npx webpack +cp -r node_modules/puppeteer/.local-chromium . +rm -rf node_modules +node dist/bundle.cjs + +echo "Testing... Firefox CommonJS" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +PUPPETEER_PRODUCT=firefox npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer')" +node --eval="require('puppeteer/lib/cjs/puppeteer/revisions.js');" +ls $TMPDIR/node_modules/puppeteer/.local-firefox/linux-*/firefox/firefox + +echo "Testing... Firefox ES Modules" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +echo '{"type":"module"}' >>$TMPDIR/package.json +PUPPETEER_PRODUCT=firefox npm install --loglevel silent "${tarball}" +node --input-type="module" --eval="import puppeteer from 'puppeteer'" +node --input-type="module" --eval="import 'puppeteer/lib/esm/puppeteer/revisions.js';" +ls $TMPDIR/node_modules/puppeteer/.local-firefox/linux-*/firefox/firefox + +## Puppeteer Core tests + +echo "Setting up Puppeteer Core tests..." +cd $ROOTDIR +rm "${tarball}" +node ./utils/prepare_puppeteer_core.js +npm pack +tarball="$(realpath puppeteer-core-*.tgz)" + +echo "Testing... Puppeteer Core CommonJS" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +npm install --loglevel silent "${tarball}" +node --eval="require('puppeteer-core')" +node --eval="require('puppeteer-core/lib/cjs/puppeteer/revisions.js');" + +echo "Testing... Puppeteer Core ES Modules" +TMPDIR="$(mktemp -d)" +cd $TMPDIR +echo '{"type":"module"}' >>$TMPDIR/package.json +npm install --loglevel silent "${tarball}" +node --input-type="module" --eval="import puppeteer from 'puppeteer-core'" +node --input-type="module" --eval="import 'puppeteer-core/lib/esm/puppeteer/revisions.js';" diff --git a/remote/test/puppeteer/scripts/tsconfig.json b/remote/test/puppeteer/scripts/tsconfig.json new file mode 100644 index 0000000000..c8d842173c --- /dev/null +++ b/remote/test/puppeteer/scripts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/src/common/Accessibility.ts b/remote/test/puppeteer/src/common/Accessibility.ts new file mode 100644 index 0000000000..ea540c01d7 --- /dev/null +++ b/remote/test/puppeteer/src/common/Accessibility.ts @@ -0,0 +1,572 @@ +/** + * 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 {CDPSession} from './Connection.js'; +import {ElementHandle} from './ElementHandle.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 Chromium'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 Chromium accessibility tree contains nodes that go unused on + * most platforms and by most screen readers. Puppeteer will discard them as + * well for an easier to process tree, unless `interestingOnly` is set to + * `false`. + * + * @example + * 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.remoteObject().objectId, + }); + 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 || []) { + node.children.push(nodeById.get(childId)!); + } + } + return nodeById.values().next().value; + } +} diff --git a/remote/test/puppeteer/src/common/AriaQueryHandler.ts b/remote/test/puppeteer/src/common/AriaQueryHandler.ts new file mode 100644 index 0000000000..2ec86f5bf2 --- /dev/null +++ b/remote/test/puppeteer/src/common/AriaQueryHandler.ts @@ -0,0 +1,189 @@ +/** + * 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 {assert} from '../util/assert.js'; +import {CDPSession} from './Connection.js'; +import {ElementHandle} from './ElementHandle.js'; +import {Frame} from './Frame.js'; +import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js'; +import {InternalQueryHandler} from './QueryHandler.js'; + +async function queryAXTree( + client: CDPSession, + element: ElementHandle<Node>, + accessibleName?: string, + role?: string +): Promise<Protocol.Accessibility.AXNode[]> { + const {nodes} = await client.send('Accessibility.queryAXTree', { + objectId: element.remoteObject().objectId, + accessibleName, + role, + }); + const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter( + (node: Protocol.Accessibility.AXNode) => { + return !node.role || node.role.value !== 'StaticText'; + } + ); + return filteredNodes; +} + +const normalizeValue = (value: string): string => { + return value.replace(/ +/g, ' ').trim(); +}; +const knownAttributes = new Set(['name', 'role']); +const attributeRegexp = + /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g; + +type ARIAQueryOption = {name?: string; role?: string}; +function isKnownAttribute( + attribute: string +): attribute is keyof ARIAQueryOption { + return knownAttributes.has(attribute); +} + +/** + * The selectors consist of an accessible name to query for and optionally + * 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'. + */ +function parseAriaSelector(selector: string): ARIAQueryOption { + const queryOptions: ARIAQueryOption = {}; + const defaultName = selector.replace( + attributeRegexp, + (_, attribute: string, _quote: string, value: string) => { + 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; +} + +const queryOneId = async (element: ElementHandle<Node>, selector: string) => { + const {name, role} = parseAriaSelector(selector); + const res = await queryAXTree(element.client, element, name, role); + if (!res[0] || !res[0].backendDOMNodeId) { + return null; + } + return res[0].backendDOMNodeId; +}; + +const queryOne: InternalQueryHandler['queryOne'] = async ( + element, + selector +) => { + const id = await queryOneId(element, selector); + if (!id) { + return null; + } + return (await element.frame.worlds[MAIN_WORLD].adoptBackendNode( + id + )) as ElementHandle<Node>; +}; + +const waitFor: InternalQueryHandler['waitFor'] = async ( + elementOrFrame, + selector, + options +) => { + let frame: Frame; + let element: ElementHandle<Node> | undefined; + if (elementOrFrame instanceof Frame) { + frame = elementOrFrame; + } else { + frame = elementOrFrame.frame; + element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); + } + const binding: PageBinding = { + name: 'ariaQuerySelector', + pptrFunction: async (selector: string) => { + const id = await queryOneId( + element || (await frame.worlds[PUPPETEER_WORLD].document()), + selector + ); + if (!id) { + return null; + } + return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode( + id + )) as ElementHandle<Node>; + }, + }; + const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage( + (_: Element, selector: string) => { + return ( + globalThis as unknown as { + ariaQuerySelector(selector: string): Node | null; + } + ).ariaQuerySelector(selector); + }, + element, + selector, + options, + binding + ); + if (element) { + await element.dispose(); + } + if (!result) { + return null; + } + if (!(result instanceof ElementHandle)) { + await result.dispose(); + return null; + } + return result.frame.worlds[MAIN_WORLD].transferHandle(result); +}; + +const queryAll: InternalQueryHandler['queryAll'] = async ( + element, + selector +) => { + const exeCtx = element.executionContext(); + const {name, role} = parseAriaSelector(selector); + const res = await queryAXTree(exeCtx._client, element, name, role); + const world = exeCtx._world!; + return Promise.all( + res.map(axNode => { + return world.adoptBackendNode(axNode.backendDOMNodeId) as Promise< + ElementHandle<Node> + >; + }) + ); +}; + +/** + * @internal + */ +export const ariaHandler: InternalQueryHandler = { + queryOne, + waitFor, + queryAll, +}; diff --git a/remote/test/puppeteer/src/common/Browser.ts b/remote/test/puppeteer/src/common/Browser.ts new file mode 100644 index 0000000000..253cfff291 --- /dev/null +++ b/remote/test/puppeteer/src/common/Browser.ts @@ -0,0 +1,980 @@ +/** + * 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 {assert} from '../util/assert.js'; +import {CDPSession, Connection, ConnectionEmittedEvents} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {waitWithTimeout} from './util.js'; +import {Page} from './Page.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {Target} from './Target.js'; +import {TaskQueue} from './TaskQueue.js'; +import {TargetManager, TargetManagerEmittedEvents} from './TargetManager.js'; +import {ChromeTargetManager} from './ChromeTargetManager.js'; +import {FirefoxTargetManager} from './FirefoxTargetManager.js'; + +/** + * 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 semi-colon-separated 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; + +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 seconds. + */ + timeout?: number; +} + +/** + * All the events a {@link Browser | browser instance} may emit. + * + * @public + */ +export const enum BrowserEmittedEvents { + /** + * Emitted when Puppeteer gets disconnected from the Chromium instance. This + * might happen because of one of the following: + * + * - Chromium 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 Chromium 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 + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await browser.close(); + * })(); + * ``` + * + * @example + * An example of disconnecting from and reconnecting to a {@link Browser}: + * + * ```ts + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * // Store the endpoint to be able to reconnect to Chromium + * const browserWSEndpoint = browser.wsEndpoint(); + * // Disconnect puppeteer from Chromium + * browser.disconnect(); + * + * // Use the endpoint to reestablish a connection + * const browser2 = await puppeteer.connect({browserWSEndpoint}); + * // Close Chromium + * await browser2.close(); + * })(); + * ``` + * + * @public + */ +export class Browser extends EventEmitter { + /** + * @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<Browser> { + const browser = new Browser( + 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: BrowserContext; + #contexts: Map<string, BrowserContext>; + #screenshotTaskQueue: TaskQueue; + #targetManager: TargetManager; + + /** + * @internal + */ + 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 BrowserContext(this.#connection, this); + this.#contexts = new Map(); + for (const contextId of contextIds) { + this.#contexts.set( + contextId, + new BrowserContext(this.#connection, this, contextId) + ); + } + } + + #emitDisconnected = () => { + this.emit(BrowserEmittedEvents.Disconnected); + }; + + /** + * @internal + */ + 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 + */ + _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}. + */ + 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 + */ + _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'); + * })(); + * ``` + */ + async createIncognitoBrowserContext( + options: BrowserContextOptions = {} + ): Promise<BrowserContext> { + const {proxyServer, proxyBypassList} = options; + + const {browserContextId} = await this.#connection.send( + 'Target.createBrowserContext', + { + proxyServer, + proxyBypassList: proxyBypassList && proxyBypassList.join(','), + } + ); + const context = new BrowserContext( + 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}. + */ + browserContexts(): BrowserContext[] { + return [this.#defaultContext, ...Array.from(this.#contexts.values())]; + } + + /** + * Returns the default browser context. The default browser context cannot be closed. + */ + defaultBrowserContext(): BrowserContext { + return this.#defaultContext; + } + + /** + * @internal + */ + 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}. + */ + wsEndpoint(): string { + return this.#connection.url(); + } + + /** + * Promise which resolves to a new {@link Page} object. The Page is created in + * a default browser context. + */ + async newPage(): Promise<Page> { + return this.#defaultContext.newPage(); + } + + /** + * @internal + */ + 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. + */ + targets(): Target[] { + return Array.from( + this.#targetManager.getAvailableTargets().values() + ).filter(target => { + return target._isInitialized; + }); + } + + /** + * The target associated with the browser. + */ + 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/' + * ); + * ``` + */ + async waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options: WaitForTargetOptions = {} + ): Promise<Target> { + const {timeout = 30000} = options; + let resolve: (value: Target | PromiseLike<Target>) => void; + let isResolved = false; + const targetPromise = new Promise<Target>(x => { + return (resolve = x); + }); + 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)) && !isResolved) { + isResolved = true; + 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}. + */ + 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); + }, []); + } + + /** + * A string representing the browser name and version. + * + * @remarks + * + * For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For + * non-headless, this is similar to `Chrome/61.0.3153.0`. + * + * The format of browser.version() might change with future releases of Chromium. + */ + 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}. + */ + async userAgent(): Promise<string> { + const version = await this.#getVersion(); + return version.userAgent; + } + + /** + * Closes Chromium and all of its pages (if any were opened). The {@link Browser} object + * itself is considered to be disposed and cannot be used anymore. + */ + async close(): Promise<void> { + await this.#closeCallback.call(null); + this.disconnect(); + } + + /** + * Disconnects Puppeteer from the browser, but leaves the Chromium process running. + * After calling `disconnect`, the {@link Browser} object is considered disposed and + * cannot be used anymore. + */ + disconnect(): void { + this.#targetManager.dispose(); + this.#connection.dispose(); + } + + /** + * Indicates that the browser is connected. + */ + isConnected(): boolean { + return !this.#connection._closed; + } + + #getVersion(): Promise<Protocol.Browser.GetVersionResponse> { + return this.#connection.send('Browser.getVersion'); + } +} +/** + * @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', +} + +/** + * 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 { + #connection: Connection; + #browser: Browser; + #id?: string; + + /** + * @internal + */ + constructor(connection: Connection, browser: Browser, contextId?: string) { + super(); + this.#connection = connection; + this.#browser = browser; + this.#id = contextId; + } + + /** + * An array of all active targets inside the browser context. + */ + 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 timout, + * which is the maximum wait time in milliseconds. + * Pass `0` to disable the timeout. Defaults to 30 seconds. + * @returns Promise which resolves to the first target found + * that matches the `predicate` function. + */ + waitForTarget( + predicate: (x: Target) => boolean | Promise<boolean>, + options: {timeout?: number} = {} + ): Promise<Target> { + 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}. + */ + 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. + */ + 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. + */ + 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(); + * ``` + */ + async clearPermissionOverrides(): Promise<void> { + await this.#connection.send('Browser.resetPermissions', { + browserContextId: this.#id || undefined, + }); + } + + /** + * Creates a new page in the browser context. + */ + newPage(): Promise<Page> { + return this.#browser._createPageInContext(this.#id); + } + + /** + * The browser this browser context belongs to. + */ + browser(): Browser { + 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. + */ + 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/src/common/BrowserConnector.ts b/remote/test/puppeteer/src/common/BrowserConnector.ts new file mode 100644 index 0000000000..8d65873c31 --- /dev/null +++ b/remote/test/puppeteer/src/common/BrowserConnector.ts @@ -0,0 +1,159 @@ +/** + * 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 {debugError} from './util.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {isNode} from '../environment.js'; +import {assert} from '../util/assert.js'; +import { + Browser, + IsPageTargetCallback, + TargetFilterCallback, +} from './Browser.js'; +import {Connection} from './Connection.js'; +import {ConnectionTransport} from './ConnectionTransport.js'; +import {getFetch} from './fetch.js'; +import {Viewport} from './PuppeteerViewport.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; +} + +const getWebSocketTransportClass = async () => { + return isNode + ? (await import('../node/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 _connectToBrowser( + options: BrowserConnectOptions & { + browserWSEndpoint?: string; + browserURL?: string; + transport?: ConnectionTransport; + } +): Promise<Browser> { + const { + browserWSEndpoint, + browserURL, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + transport, + slowMo = 0, + targetFilter, + _isPageTarget: isPageTarget, + } = 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); + } else if (browserWSEndpoint) { + const WebSocketClass = await getWebSocketTransportClass(); + const connectionTransport: ConnectionTransport = + await WebSocketClass.create(browserWSEndpoint); + connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); + } 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); + } + 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 Browser._create( + product || 'chrome', + connection, + browserContextIds, + ignoreHTTPSErrors, + defaultViewport, + undefined, + () => { + return connection.send('Browser.close').catch(debugError); + }, + targetFilter, + isPageTarget + ); + await browser.pages(); + 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/src/common/BrowserWebSocketTransport.ts b/remote/test/puppeteer/src/common/BrowserWebSocketTransport.ts new file mode 100644 index 0000000000..5fe32e6526 --- /dev/null +++ b/remote/test/puppeteer/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/src/common/ChromeTargetManager.ts b/remote/test/puppeteer/src/common/ChromeTargetManager.ts new file mode 100644 index 0000000000..445225edf3 --- /dev/null +++ b/remote/test/puppeteer/src/common/ChromeTargetManager.ts @@ -0,0 +1,411 @@ +/** + * 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 {assert} from '../util/assert.js'; +import {CDPSession, Connection} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {Target} from './Target.js'; +import {debugError} from './util.js'; +import {TargetFilterCallback} from './Browser.js'; +import { + TargetInterceptor, + TargetFactory, + TargetManager, + TargetManagerEmittedEvents, +} from './TargetManager.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(); + + #initializeCallback = () => {}; + #initializePromise: Promise<void> = new Promise(resolve => { + this.#initializeCallback = resolve; + }); + #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); + + // TODO: remove `as any` once the protocol definitions are updated with the + // next Chromium roll. + this.#connection + .send('Target.setDiscoverTargets', { + discover: true, + filter: [{type: 'tab', exclude: true}, {}], + } as any) + .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); + } + + if (event.targetInfo.type === 'shared_worker') { + // Special case (https://crbug.com/1338156): currently, shared_workers + // don't get auto-attached. This should be removed once the auto-attach + // works. + await this.#connection._createSession(event.targetInfo, true); + } + }; + + #onTargetDestroyed = (event: Protocol.Target.TargetDestroyedEvent) => { + 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())); + } + await 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.#initializeCallback(); + } + } + + #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/src/common/Connection.ts b/remote/test/puppeteer/src/common/Connection.ts new file mode 100644 index 0000000000..541e225e4e --- /dev/null +++ b/remote/test/puppeteer/src/common/Connection.ts @@ -0,0 +1,447 @@ +/** + * 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 {debug} from './Debug.js'; +const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); +const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀'); + +import {Protocol} from 'devtools-protocol'; +import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; +import {ConnectionTransport} from './ConnectionTransport.js'; +import {EventEmitter} from './EventEmitter.js'; +import {ProtocolError} from './Errors.js'; + +/** + * @public + */ +export {ConnectionTransport, ProtocolMapping}; + +/** + * @public + */ +export interface ConnectionCallback { + resolve(args: unknown): void; + reject(args: unknown): void; + error: ProtocolError; + method: string; +} + +/** + * Internal events that the Connection class emits. + * + * @internal + */ +export const ConnectionEmittedEvents = { + Disconnected: Symbol('Connection.Disconnected'), +} as const; + +/** + * @public + */ +export class Connection extends EventEmitter { + #url: string; + #transport: ConnectionTransport; + #delay: number; + #lastId = 0; + #sessions: Map<string, CDPSession> = new Map(); + #closed = false; + #callbacks: Map<number, ConnectionCallback> = new Map(); + #manuallyAttached = new Set<string>(); + + constructor(url: string, transport: ConnectionTransport, delay = 0) { + super(); + this.#url = url; + this.#delay = delay; + + 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(); + } + + /** + * @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; + const id = this._rawSend({method, params}); + return new Promise((resolve, reject) => { + this.#callbacks.set(id, { + resolve, + reject, + error: new ProtocolError(), + method, + }); + }); + } + + /** + * @internal + */ + _rawSend(message: Record<string, unknown>): number { + const id = ++this.#lastId; + const stringifiedMessage = JSON.stringify(Object.assign({}, message, {id})); + debugProtocolSend(stringifiedMessage); + this.#transport.send(stringifiedMessage); + return id; + } + + /** + * @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 CDPSession( + 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) { + const callback = this.#callbacks.get(object.id); + // Callbacks could be all rejected if someone has called `.dispose()`. + if (callback) { + this.#callbacks.delete(object.id); + if (object.error) { + callback.reject( + createProtocolError(callback.error, callback.method, object) + ); + } else { + callback.resolve(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; + for (const callback of this.#callbacks.values()) { + callback.reject( + rewriteError( + callback.error, + `Protocol error (${callback.method}): Target closed.` + ) + ); + } + 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 { + #sessionId: string; + #targetType: string; + #callbacks: Map<number, ConnectionCallback> = new Map(); + #connection?: Connection; + + /** + * @internal + */ + constructor(connection: Connection, targetType: string, sessionId: string) { + super(); + this.#connection = connection; + this.#targetType = targetType; + this.#sessionId = sessionId; + } + + connection(): Connection | undefined { + return this.#connection; + } + + 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; + + const id = this.#connection._rawSend({ + sessionId: this.#sessionId, + method, + params, + }); + + return new Promise((resolve, reject) => { + this.#callbacks.set(id, { + resolve, + reject, + error: new ProtocolError(), + method, + }); + }); + } + + /** + * @internal + */ + _onMessage(object: CDPSessionOnMessageObject): void { + const callback = object.id ? this.#callbacks.get(object.id) : undefined; + if (object.id && callback) { + this.#callbacks.delete(object.id); + if (object.error) { + callback.reject( + createProtocolError(callback.error, callback.method, object) + ); + } else { + callback.resolve(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. + */ + 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 { + for (const callback of this.#callbacks.values()) { + callback.reject( + rewriteError( + callback.error, + `Protocol error (${callback.method}): Target closed.` + ) + ); + } + this.#callbacks.clear(); + this.#connection = undefined; + this.emit(CDPSessionEmittedEvents.Disconnected); + } + + /** + * Returns the session's id. + */ + id(): string { + return this.#sessionId; + } +} + +function createProtocolError( + error: ProtocolError, + method: string, + object: {error: {message: string; data: any; code: number}} +): Error { + let message = `Protocol error (${method}): ${object.error.message}`; + if ('data' in object.error) { + message += ` ${object.error.data}`; + } + return rewriteError(error, message, object.error.message); +} + +function rewriteError( + error: ProtocolError, + message: string, + originalMessage?: string +): Error { + error.message = message; + error.originalMessage = originalMessage ?? error.originalMessage; + return error; +} diff --git a/remote/test/puppeteer/src/common/ConnectionTransport.ts b/remote/test/puppeteer/src/common/ConnectionTransport.ts new file mode 100644 index 0000000000..753379fd56 --- /dev/null +++ b/remote/test/puppeteer/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/src/common/ConsoleMessage.ts b/remote/test/puppeteer/src/common/ConsoleMessage.ts new file mode 100644 index 0000000000..4616eea6dd --- /dev/null +++ b/remote/test/puppeteer/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 './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; + } + + /** + * @returns The type of the console message. + */ + type(): ConsoleMessageType { + return this.#type; + } + + /** + * @returns The text of the console message. + */ + text(): string { + return this.#text; + } + + /** + * @returns An array of arguments passed to the console. + */ + args(): JSHandle[] { + return this.#args; + } + + /** + * @returns The location of the console message. + */ + location(): ConsoleMessageLocation { + return this.#stackTraceLocations[0] ?? {}; + } + + /** + * @returns The array of locations on the stack of the console message. + */ + stackTrace(): ConsoleMessageLocation[] { + return this.#stackTraceLocations; + } +} diff --git a/remote/test/puppeteer/src/common/Coverage.ts b/remote/test/puppeteer/src/common/Coverage.ts new file mode 100644 index 0000000000..a75a772a69 --- /dev/null +++ b/remote/test/puppeteer/src/common/Coverage.ts @@ -0,0 +1,489 @@ +/** + * 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 {addEventListener, debugError, PuppeteerEventListener} from './util.js'; +import {Protocol} from 'devtools-protocol'; +import {CDPSession} from './Connection.js'; + +import {EVALUATION_SCRIPT_URL} from './ExecutionContext.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; +} + +/** + * 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 gathers 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` + * @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 will have `pptr://__puppeteer_evaluation_script__` as their URL. + */ + async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { + return await this.#jsCoverage.start(options); + } + + /** + * @returns 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); + } + + /** + * @returns 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; + } = {} + ): Promise<void> { + assert(!this.#enabled, 'JSCoverage is already enabled'); + const { + resetOnNavigation = true, + reportAnonymousScripts = false, + includeRawScriptCoverage = false, + } = 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: true, + }), + 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/src/common/Debug.ts b/remote/test/puppeteer/src/common/Debug.ts new file mode 100644 index 0000000000..425b7c305a --- /dev/null +++ b/remote/test/puppeteer/src/common/Debug.ts @@ -0,0 +1,109 @@ +/** + * 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[]) => { + (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); + }; +}; diff --git a/remote/test/puppeteer/src/common/DeviceDescriptors.ts b/remote/test/puppeteer/src/common/DeviceDescriptors.ts new file mode 100644 index 0000000000..58cd9dde4c --- /dev/null +++ b/remote/test/puppeteer/src/common/DeviceDescriptors.ts @@ -0,0 +1,1565 @@ +/** + * 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. + */ + +/** + * @public + */ +export interface Device { + name: string; + userAgent: string; + viewport: { + width: number; + height: number; + deviceScaleFactor: number; + isMobile: boolean; + hasTouch: boolean; + isLandscape: boolean; + }; +} + +const deviceArray: Device[] = [ + { + 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, + }, + }, +]; + +/** + * @public + */ +export type DevicesMap = { + [name: string]: Device; +}; + +/** + * A list of devices to be used with `page.emulate(options)`. Actual list of devices can be found in {@link https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts | src/common/DeviceDescriptors.ts}. + * + * @example + * + * ```ts + * const puppeteer = require('puppeteer'); + * const iPhone = puppeteer.devices['iPhone 6']; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulate(iPhone); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * @public + */ +const devices: DevicesMap = {}; + +for (const device of deviceArray) { + devices[device.name] = device; +} + +export {devices}; diff --git a/remote/test/puppeteer/src/common/Dialog.ts b/remote/test/puppeteer/src/common/Dialog.ts new file mode 100644 index 0000000000..6d84fd0d0b --- /dev/null +++ b/remote/test/puppeteer/src/common/Dialog.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {assert} from '../util/assert.js'; +import {CDPSession} from './Connection.js'; +import {Protocol} from 'devtools-protocol'; + +/** + * Dialog instances are dispatched by the {@link Page} via the `dialog` event. + * + * @remarks + * + * @example + * + * ```ts + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * page.on('dialog', async dialog => { + * console.log(dialog.message()); + * await dialog.dismiss(); + * await browser.close(); + * }); + * page.evaluate(() => alert('1')); + * })(); + * ``` + * + * @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; + } + + /** + * @returns The type of the dialog. + */ + type(): Protocol.Page.DialogType { + return this.#type; + } + + /** + * @returns The message displayed in the dialog. + */ + message(): string { + return this.#message; + } + + /** + * @returns The default value of the prompt, or an empty string if the dialog + * is not a `prompt`. + */ + defaultValue(): string { + return this.#defaultValue; + } + + /** + * @param promptText - optional text that will be entered in the dialog + * prompt. Has no effect if the dialog's type is not `prompt`. + * + * @returns A promise that resolves when the dialog has been accepted. + */ + 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, + }); + } + + /** + * @returns 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/src/common/ElementHandle.ts b/remote/test/puppeteer/src/common/ElementHandle.ts new file mode 100644 index 0000000000..8cb19fa042 --- /dev/null +++ b/remote/test/puppeteer/src/common/ElementHandle.ts @@ -0,0 +1,1039 @@ +import {Protocol} from 'devtools-protocol'; +import {assert} from '../util/assert.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {FrameManager} from './FrameManager.js'; +import {WaitForSelectorOptions} from './IsolatedWorld.js'; +import { + BoundingBox, + BoxModel, + ClickOptions, + JSHandle, + Offset, + Point, + PressOptions, +} from './JSHandle.js'; +import {Page, ScreenshotOptions} from './Page.js'; +import {getQueryHandlerAndSelector} from './QueryHandler.js'; +import {EvaluateFunc, 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}; + }); +}; + +/** + * ElementHandle represents an in-page DOM element. + * + * @remarks + * ElementHandles can be created with the {@link Page.$} method. + * + * ```ts + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * const hrefElement = await page.$('a'); + * await hrefElement.click(); + * // ... + * })(); + * ``` + * + * ElementHandle prevents the DOM element from being garbage-collected unless the + * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed + * when their origin frame gets navigated. + * + * ElementHandle instances can be used as arguments in {@link Page.$eval} and + * {@link Page.evaluate} methods. + * + * If you're using TypeScript, ElementHandle takes a generic argument that + * denotes the type of element the handle is holding within. For example, if you + * have a handle to a `<select>` element, you can type it as + * `ElementHandle<HTMLSelectElement>` and you get some nicer type checks. + * + * @public + */ + +export class ElementHandle< + ElementType extends Node = Element +> extends JSHandle<ElementType> { + #frame: Frame; + + /** + * @internal + */ + constructor( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject, + frame: Frame + ) { + super(context, remoteObject); + this.#frame = frame; + } + + get #frameManager(): FrameManager { + return this.#frame._frameManager; + } + + get #page(): Page { + return this.#frame.page(); + } + + get frame(): Frame { + return this.#frame; + } + + /** + * Queries the current element for an element matching the given selector. + * + * @param selector - The selector to query for. + * @returns A {@link ElementHandle | element handle} to the first element + * matching the given selector. Otherwise, `null`. + */ + async $<Selector extends string>( + selector: Selector + ): Promise<ElementHandle<NodeFor<Selector>> | null> { + const {updatedSelector, queryHandler} = + getQueryHandlerAndSelector(selector); + assert( + queryHandler.queryOne, + 'Cannot handle queries for a single element with the given selector' + ); + return (await queryHandler.queryOne( + this, + updatedSelector + )) as ElementHandle<NodeFor<Selector>> | null; + } + + /** + * Queries the current element for all elements matching the given selector. + * + * @param selector - The selector to query for. + * @returns An array of {@link ElementHandle | element handles} that point to + * elements matching the given selector. + */ + async $$<Selector extends string>( + selector: Selector + ): Promise<Array<ElementHandle<NodeFor<Selector>>>> { + const {updatedSelector, queryHandler} = + getQueryHandlerAndSelector(selector); + assert( + queryHandler.queryAll, + 'Cannot handle queries for a multiple element with the given selector' + ); + return (await queryHandler.queryAll(this, updatedSelector)) as Array< + ElementHandle<NodeFor<Selector>> + >; + } + + /** + * 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 EvaluateFunc< + [ElementHandle<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[ElementHandle<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; + } + + /** + * 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 EvaluateFunc< + [Array<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + const {updatedSelector, queryHandler} = + getQueryHandlerAndSelector(selector); + assert( + queryHandler.queryAll, + 'Cannot handle queries for a multiple element with the given selector' + ); + const handles = (await queryHandler.queryAll( + this, + updatedSelector + )) as Array<HandleFor<NodeFor<Selector>>>; + const elements = await this.evaluateHandle((_, ...elements) => { + return elements; + }, ...handles); + const [result] = await Promise.all([ + elements.evaluate(pageFunction, ...args), + ...handles.map(handle => { + return handle.dispose(); + }), + ]); + await elements.dispose(); + return result; + } + + /** + * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix. + * + * The method evaluates the XPath expression relative to the elementHandle. + * 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>>> { + if (expression.startsWith('//')) { + expression = `.${expression}`; + } + return this.$$(`xpath/${expression}`); + } + + /** + * Wait for an element matching the given selector to appear in the current + * element. + * + * Unlike {@link Frame.waitForSelector}, this method does not work across + * navigations or if the element is detached from DOM. + * + * @example + * + * ```ts + * const puppeteer = require('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); + assert(queryHandler.waitFor, 'Query handler does not support waiting'); + return (await queryHandler.waitFor( + this, + updatedSelector, + options + )) as ElementHandle<NodeFor<Selector>> | null; + } + + /** + * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath` + * prefix. + * + * Wait for the `xpath` within the element. If at the moment of calling the + * method the `xpath` already exists, the method will return immediately. If + * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the + * function will throw. + * + * If `xpath` starts with `//` instead of `.//`, the dot will be appended + * automatically. + * + * This method works across navigation + * + * ```ts + * const puppeteer = require('puppeteer'); + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForXPath('//img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param xpath - A + * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an + * element to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves when element specified by xpath string is + * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is + * not found in DOM. + * @remarks + * The optional Argument `options` have properties: + * + * - `visible`: A boolean to wait for element to be present in DOM and to be + * visible, i.e. to not have `display: none` or `visibility: hidden` CSS + * properties. Defaults to `false`. + * + * - `hidden`: A boolean wait for element to not be found in the DOM or to be + * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. + * Defaults to `false`. + * + * - `timeout`: A number which is maximum time to wait for in milliseconds. + * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + * default value can be changed by using the {@link Page.setDefaultTimeout} + * method. + */ + async waitForXPath( + xpath: string, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } = {} + ): Promise<ElementHandle<Node> | null> { + if (xpath.startsWith('//')) { + xpath = `.${xpath}`; + } + return this.waitForSelector(`xpath/${xpath}`, options); + } + + override asElement(): ElementHandle<ElementType> | null { + return this; + } + + /** + * Resolves to the content frame for element handles referencing + * iframe nodes, or null otherwise + */ + async contentFrame(): Promise<Frame | null> { + 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); + } + + async #scrollIntoViewIfNeeded(this: ElementHandle<Element>): 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); + } + + try { + await this.client.send('DOM.scrollIntoViewIfNeeded', { + objectId: this.remoteObject().objectId, + }); + } catch (_err) { + // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported + await this.evaluate( + async (element, pageJavascriptEnabled): Promise<void> => { + const visibleRatio = async () => { + return await new Promise(resolve => { + const observer = new IntersectionObserver(entries => { + resolve(entries[0]!.intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + }; + if (!pageJavascriptEnabled || (await visibleRatio()) !== 1.0) { + 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', + }); + } + }, + this.#page.isJavaScriptEnabled() + ); + } + } + + 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}; + } + + /** + * Returns the middle point within an element unless a specific offset is provided. + */ + 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._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.remoteObject().objectId, + }; + 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. + */ + async hover(this: ElementHandle<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. + */ + async click( + this: ElementHandle<Element>, + options: 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. + */ + async drag( + this: ElementHandle<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); + } + + /** + * This method creates a `dragenter` event on the element. + */ + async dragEnter( + this: ElementHandle<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); + } + + /** + * This method creates a `dragover` event on the element. + */ + async dragOver( + this: ElementHandle<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); + } + + /** + * This method triggers a drop on the element. + */ + async drop( + this: ElementHandle<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); + } + + /** + * This method triggers a dragenter, dragover, and drop on the element. + */ + async dragAndDrop( + this: ElementHandle<Element>, + target: ElementHandle<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); + } + + /** + * 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[]> { + 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); + } + + /** + * This method expects `elementHandle` to point to an + * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}. + * + * @param filePaths - Sets the value of the file input to these paths. + * If a path is relative, then it is resolved against the + * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. + * Note for locals script connecting to remote chrome environments, + * paths must be absolute. + */ + async uploadFile( + this: ElementHandle<HTMLInputElement>, + ...filePaths: string[] + ): Promise<void> { + 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, + }); + } + } + + /** + * 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> { + await this.#scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.#page.touchscreen.tap(x, y); + } + + /** + * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. + */ + async focus(): Promise<void> { + await this.evaluate(element => { + if (!(element instanceof HTMLElement)) { + throw new Error('Cannot focus non-HTMLElement'); + } + return element.focus(); + }); + } + + /** + * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and + * `keyup` event for each character in the text. + * + * To press a special key, like `Control` or `ArrowDown`, + * use {@link ElementHandle.press}. + * + * @example + * + * ```ts + * await elementHandle.type('Hello'); // Types instantly + * await elementHandle.type('World', {delay: 100}); // Types slower, like a user + * ``` + * + * @example + * An example of typing into a text field and then submitting the form: + * + * ```ts + * const elementHandle = await page.$('input'); + * await elementHandle.type('some text'); + * await elementHandle.press('Enter'); + * ``` + */ + async type(text: string, options?: {delay: number}): Promise<void> { + await this.focus(); + await this.#page.keyboard.type(text, options); + } + + /** + * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}. + * + * @remarks + * If `key` is a single character and no modifier keys besides `Shift` + * are being held down, a `keypress`/`input` event will also be generated. + * The `text` option can be specified to force an input event to be generated. + * + * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift` + * will type the text in upper case. + * + * @param key - Name of key to press, such as `ArrowLeft`. + * See {@link KeyInput} for a list of all key names. + */ + async press(key: KeyInput, options?: PressOptions): Promise<void> { + await this.focus(); + await this.#page.keyboard.press(key, options); + } + + /** + * This method returns the bounding box of the element (relative to the main frame), + * or `null` if the element is not visible. + */ + async boundingBox(): Promise<BoundingBox | null> { + 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}; + } + + /** + * 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> { + 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, + }; + } + + /** + * This method scrolls element into view if needed, and then uses + * {@link Page.screenshot} to take a screenshot of the element. + * If the element is detached from DOM, the method throws an error. + */ + async screenshot( + this: ElementHandle<Element>, + options: ScreenshotOptions = {} + ): Promise<string | Buffer> { + 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; + } + + /** + * Resolves to true if the element is visible in the current viewport. + */ + async isIntersectingViewport( + this: ElementHandle<Element>, + options?: { + threshold?: number; + } + ): Promise<boolean> { + const {threshold = 0} = options ?? {}; + return await this.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); + } +} + +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/src/common/EmulationManager.ts b/remote/test/puppeteer/src/common/EmulationManager.ts new file mode 100644 index 0000000000..ba108d3124 --- /dev/null +++ b/remote/test/puppeteer/src/common/EmulationManager.ts @@ -0,0 +1,62 @@ +/** + * 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 {CDPSession} from './Connection.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {Protocol} from 'devtools-protocol'; + +/** + * @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/src/common/Errors.ts b/remote/test/puppeteer/src/common/Errors.ts new file mode 100644 index 0000000000..56a6117b8f --- /dev/null +++ b/remote/test/puppeteer/src/common/Errors.ts @@ -0,0 +1,84 @@ +/** + * 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. + */ + +/** + * @public + */ +export class CustomError extends Error { + 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 { + public code?: number; + public originalMessage = ''; +} + +/** + * @public + */ +export interface PuppeteerErrors { + TimeoutError: typeof TimeoutError; + ProtocolError: typeof ProtocolError; +} + +/** + * 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 puppeteer.errors.TimeoutError) { + * // Do something if this is a timeout. + * } + * } + * ``` + * + * @public + */ +export const errors: PuppeteerErrors = Object.freeze({ + TimeoutError, + ProtocolError, +}); diff --git a/remote/test/puppeteer/src/common/EventEmitter.ts b/remote/test/puppeteer/src/common/EventEmitter.ts new file mode 100644 index 0000000000..ce78874b43 --- /dev/null +++ b/remote/test/puppeteer/src/common/EventEmitter.ts @@ -0,0 +1,149 @@ +import mitt, { + Emitter, + EventType, + Handler, +} from '../../vendor/mitt/src/index.js'; + +/** + * @public + */ +export {EventType, Handler}; + +/** + * @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; + private eventsMap = new Map<EventType, Handler[]>(); + + /** + * @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): 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): EventEmitter { + this.emitter.off(event, handler); + return this; + } + + /** + * Remove an event listener. + * @deprecated please use {@link EventEmitter.off} instead. + */ + removeListener(event: EventType, handler: Handler): EventEmitter { + this.off(event, handler); + return this; + } + + /** + * Add an event listener. + * @deprecated please use {@link EventEmitter.on} instead. + */ + addListener(event: EventType, handler: Handler): 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): EventEmitter { + const onceHandler: Handler = 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/src/common/ExecutionContext.ts b/remote/test/puppeteer/src/common/ExecutionContext.ts new file mode 100644 index 0000000000..13dfbc5122 --- /dev/null +++ b/remote/test/puppeteer/src/common/ExecutionContext.ts @@ -0,0 +1,364 @@ +/** + * 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 {IsolatedWorld} from './IsolatedWorld.js'; +import {JSHandle} from './JSHandle.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 { + /** + * @internal + */ + _client: CDPSession; + /** + * @internal + */ + _world?: IsolatedWorld; + /** + * @internal + */ + _contextId: number; + /** + * @internal + */ + _contextName: string; + + /** + * @internal + */ + constructor( + client: CDPSession, + contextPayload: Protocol.Runtime.ExecutionContextDescription, + world?: IsolatedWorld + ) { + this._client = client; + this._world = world; + this._contextId = contextPayload.id; + this._contextName = contextPayload.name; + } + + /** + * 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 functionText = pageFunction.toString(); + try { + new Function('(' + functionText + ')'); + } catch (error) { + // This means we might have a function shorthand. Try another + // time prefixing 'function '. + if (functionText.startsWith('async ')) { + functionText = + 'async function ' + functionText.substring('async '.length); + } else { + functionText = 'function ' + functionText; + } + try { + new Function('(' + functionText + ')'); + } catch (error) { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function is not well-serializable!'); + } + } + let callFunctionOnPromise; + try { + callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { + functionDeclaration: functionText + '\n' + suffix + '\n', + executionContextId: this._contextId, + arguments: 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); + + function convertArgument( + this: ExecutionContext, + arg: unknown + ): Protocol.Runtime.CallArgument { + 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 JSHandle ? 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/src/common/FileChooser.ts b/remote/test/puppeteer/src/common/FileChooser.ts new file mode 100644 index 0000000000..c0a68ff967 --- /dev/null +++ b/remote/test/puppeteer/src/common/FileChooser.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Protocol} from 'devtools-protocol'; +import {assert} from '../util/assert.js'; +import {ElementHandle} from './ElementHandle.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 given paths. + * + * @param filePaths - If some of the `filePaths` are relative paths, then + * they are resolved relative to the + * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. + */ + async accept(filePaths: string[]): Promise<void> { + assert( + !this.#handled, + 'Cannot accept FileChooser which is already handled!' + ); + this.#handled = true; + await this.#element.uploadFile(...filePaths); + } + + /** + * 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/src/common/FirefoxTargetManager.ts b/remote/test/puppeteer/src/common/FirefoxTargetManager.ts new file mode 100644 index 0000000000..e31a205aa1 --- /dev/null +++ b/remote/test/puppeteer/src/common/FirefoxTargetManager.ts @@ -0,0 +1,256 @@ +/** + * 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 {assert} from '../util/assert.js'; +import {CDPSession, Connection} from './Connection.js'; +import {Target} from './Target.js'; +import {TargetFilterCallback} from './Browser.js'; +import { + TargetFactory, + TargetInterceptor, + TargetManagerEmittedEvents, + TargetManager, +} from './TargetManager.js'; +import {EventEmitter} from './EventEmitter.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(); + + #initializeCallback = () => {}; + #initializePromise: Promise<void> = new Promise(resolve => { + this.#initializeCallback = resolve; + }); + #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}); + 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.#initializeCallback(); + } + } +} diff --git a/remote/test/puppeteer/src/common/Frame.ts b/remote/test/puppeteer/src/common/Frame.ts new file mode 100644 index 0000000000..9d2d7759f0 --- /dev/null +++ b/remote/test/puppeteer/src/common/Frame.ts @@ -0,0 +1,1097 @@ +import {Protocol} from 'devtools-protocol'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {CDPSession} from './Connection.js'; +import {ElementHandle} from './ElementHandle.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {FrameManager} from './FrameManager.js'; +import {HTTPResponse} from './HTTPResponse.js'; +import {MouseButton} from './Input.js'; +import { + IsolatedWorld, + IsolatedWorldChart, + MAIN_WORLD, + PUPPETEER_WORLD, + WaitForSelectorOptions, +} from './IsolatedWorld.js'; +import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; +import {Page} from './Page.js'; +import {getQueryHandlerAndSelector} from './QueryHandler.js'; +import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; +import {importFS} 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?: string | 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; +} + +/** + * @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 + * const puppeteer = require('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 { + #parentFrame: Frame | null; + #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 + */ + _childFrames: Set<Frame>; + + /** + * @internal + */ + constructor( + frameManager: FrameManager, + parentFrame: Frame | null, + frameId: string, + client: CDPSession + ) { + this._frameManager = frameManager; + this.#parentFrame = parentFrame ?? null; + this.#url = ''; + this._id = frameId; + this.#detached = false; + + this._loaderId = ''; + + this._childFrames = new Set(); + if (this.#parentFrame) { + this.#parentFrame._childFrames.add(this); + } + + this.updateClient(client); + } + + /** + * @internal + */ + updateClient(client: CDPSession): void { + this.#client = client; + this.worlds = { + [MAIN_WORLD]: new IsolatedWorld(this), + [PUPPETEER_WORLD]: new IsolatedWorld(this, true), + }; + } + + /** + * @returns The page associated with the frame. + */ + page(): Page { + return this._frameManager.page(); + } + + /** + * @returns `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; + timeout?: number; + waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[]; + } = {} + ): Promise<HTTPResponse | null> { + const { + referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'], + 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, 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, + frameId: string + ): Promise<Error | null> { + try { + const response = await client.send('Page.navigate', { + url, + referrer, + frameId, + }); + ensureNewDocumentNavigation = !!response.loaderId; + 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 EvaluateFunc< + [ElementHandle<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[ElementHandle<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 EvaluateFunc< + [Array<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[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. + * + * This method evaluates the given XPath expression and returns the results. + * @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 + * const puppeteer = require('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); + assert(queryHandler.waitFor, 'Query handler does not support waiting'); + return (await queryHandler.waitFor( + this, + updatedSelector, + options + )) as ElementHandle<NodeFor<Selector>> | null; + } + + /** + * @deprecated Use {@link Frame.waitForSelector} with the `xpath` prefix. + * + * 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 visiblity 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 + * const puppeteer = require('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>>>> { + // TODO: Fix when NodeHandle has been added. + return this.worlds[MAIN_WORLD].waitForFunction( + pageFunction, + options, + ...args + ) as Promise<HandleFor<Awaited<ReturnType<Func>>>>; + } + + /** + * @returns 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); + } + + /** + * @returns 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 || ''; + } + + /** + * @returns The frame's URL. + */ + url(): string { + return this.#url; + } + + /** + * @returns The parent frame, if any. Detached and main frames return `null`. + */ + parentFrame(): Frame | null { + return this.#parentFrame; + } + + /** + * @returns An array of child frames. + */ + childFrames(): Frame[] { + return Array.from(this._childFrames); + } + + /** + * @returns `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) { + let fs; + try { + fs = (await import('fs')).promises; + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Can only pass a file path in a Node-like environment.' + ); + } + throw error; + } + 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 ({url, id, type, content}) => { + const promise = InjectedUtil.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; + }, + {...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) { + let fs: typeof import('fs').promises; + try { + fs = (await importFS()).promises; + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Can only pass a file path in a Node-like environment.' + ); + } + throw error; + } + + 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 ({url, content}) => { + const promise = InjectedUtil.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; + }, + 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: { + delay?: number; + button?: MouseButton; + clickCount?: number; + } = {} + ): 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 Use `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); + }); + } + + /** + * @returns the frame's title. + */ + async title(): Promise<string> { + return this.worlds[PUPPETEER_WORLD].title(); + } + + /** + * @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(); + if (this.#parentFrame) { + this.#parentFrame._childFrames.delete(this); + } + this.#parentFrame = null; + } +} diff --git a/remote/test/puppeteer/src/common/FrameManager.ts b/remote/test/puppeteer/src/common/FrameManager.ts new file mode 100644 index 0000000000..8ab336bc26 --- /dev/null +++ b/remote/test/puppeteer/src/common/FrameManager.ts @@ -0,0 +1,519 @@ +/** + * 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 {isErrorLike} from '../util/ErrorLike.js'; +import {CDPSession} from './Connection.js'; +import {EventEmitter} from './EventEmitter.js'; +import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {IsolatedWorld, MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js'; +import {NetworkManager} from './NetworkManager.js'; +import {Page} from './Page.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; + #frames = new Map<string, Frame>(); + #contextIdToContext = new Map<string, ExecutionContext>(); + #isolatedWorlds = new Set<string>(); + #mainFrame?: Frame; + #client: CDPSession; + /** + * Keeps track of OOPIF targets/frames (target ID == frame ID for OOPIFs) + * that are being initialized. + */ + #framesPendingTargetInit = new Map<string, DeferredPromise<void>>(); + /** + * Keeps track of frames that are in the process of being attached in #onFrameAttached. + */ + #framesPendingAttachment = new Map<string, DeferredPromise<void>>(); + + 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.#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( + targetId: string, + client: CDPSession = this.#client + ): Promise<void> { + try { + if (!this.#framesPendingTargetInit.has(targetId)) { + this.#framesPendingTargetInit.set( + targetId, + createDebuggableDeferredPromise( + `Waiting for target frame ${targetId} failed` + ) + ); + } + 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) && + (error.message.includes('Target closed') || + error.message.includes('Session closed')) + ) { + return; + } + + throw error; + } finally { + this.#framesPendingTargetInit.get(targetId)?.resolve(); + this.#framesPendingTargetInit.delete(targetId); + } + } + + executionContextById( + contextId: number, + session: CDPSession = this.#client + ): ExecutionContext { + const key = `${session.id()}:${contextId}`; + const context = this.#contextIdToContext.get(key); + assert(context, 'INTERNAL ERROR: missing context with id = ' + contextId); + return context; + } + + page(): Page { + return this.#page; + } + + mainFrame(): Frame { + assert(this.#mainFrame, 'Requesting main frame too early!'); + return this.#mainFrame; + } + + frames(): Frame[] { + return Array.from(this.#frames.values()); + } + + frame(frameId: string): Frame | null { + return this.#frames.get(frameId) || null; + } + + onAttachedToTarget(target: Target): void { + if (target._getTargetInfo().type !== 'iframe') { + return; + } + + const frame = this.#frames.get(target._getTargetInfo().targetId); + if (frame) { + frame.updateClient(target._session()!); + } + this.setupEventListeners(target._session()!); + this.initialize(target._getTargetInfo().targetId, target._session()); + } + + onDetachedFromTarget(target: Target): void { + const frame = this.#frames.get(target._targetId); + if (frame && frame.isOOPFrame()) { + // When an OOP iframe is removed from the page, it + // will only get a Target.detachedFromTarget event. + this.#removeFramesRecursively(frame); + } + } + + #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { + const frame = this.#frames.get(event.frameId); + if (!frame) { + return; + } + frame._onLifecycleEvent(event.loaderId, event.name); + this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); + } + + #onFrameStartedLoading(frameId: string): void { + const frame = this.#frames.get(frameId); + if (!frame) { + return; + } + frame._onLoadingStarted(); + } + + #onFrameStoppedLoading(frameId: string): void { + const frame = this.#frames.get(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 + ); + } + this.#onFrameNavigated(frameTree.frame); + if (!frameTree.childFrames) { + return; + } + + for (const child of frameTree.childFrames) { + this.#handleFrameTree(session, child); + } + } + + #onFrameAttached( + session: CDPSession, + frameId: string, + parentFrameId: string + ): void { + if (this.#frames.has(frameId)) { + const frame = this.#frames.get(frameId)!; + 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; + } + const parentFrame = this.#frames.get(parentFrameId); + + const complete = (parentFrame: Frame) => { + assert(parentFrame, `Parent frame ${parentFrameId} not found`); + const frame = new Frame(this, parentFrame, frameId, session); + this.#frames.set(frame._id, frame); + this.emit(FrameManagerEmittedEvents.FrameAttached, frame); + }; + + if (parentFrame) { + return complete(parentFrame); + } + + const frame = this.#framesPendingTargetInit.get(parentFrameId); + if (frame) { + if (!this.#framesPendingAttachment.has(frameId)) { + this.#framesPendingAttachment.set( + frameId, + createDebuggableDeferredPromise( + `Waiting for frame ${frameId} to attach failed` + ) + ); + } + frame.then(() => { + complete(this.#frames.get(parentFrameId)!); + this.#framesPendingAttachment.get(frameId)?.resolve(); + this.#framesPendingAttachment.delete(frameId); + }); + return; + } + + throw new Error(`Parent frame ${parentFrameId} not found`); + } + + #onFrameNavigated(framePayload: Protocol.Page.Frame): void { + const frameId = framePayload.id; + const isMainFrame = !framePayload.parentId; + const frame = isMainFrame ? this.#mainFrame : this.#frames.get(frameId); + + const complete = (frame?: Frame) => { + assert( + isMainFrame || frame, + `Missing frame isMainFrame=${isMainFrame}, frameId=${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.#frames.delete(frame._id); + frame._id = frameId; + } else { + // Initial main frame navigation. + frame = new Frame(this, null, frameId, this.#client); + } + this.#frames.set(frameId, frame); + this.#mainFrame = frame; + } + + // Update frame payload. + assert(frame); + frame._navigated(framePayload); + + this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); + }; + const pendingFrame = this.#framesPendingAttachment.get(frameId); + if (pendingFrame) { + pendingFrame.then(() => { + complete(isMainFrame ? this.#mainFrame : this.#frames.get(frameId)); + }); + } else { + complete(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.#frames.get(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.#frames.get(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.#frames.get(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.#frames.delete(frame._id); + this.emit(FrameManagerEmittedEvents.FrameDetached, frame); + } +} diff --git a/remote/test/puppeteer/src/common/HTTPRequest.ts b/remote/test/puppeteer/src/common/HTTPRequest.ts new file mode 100644 index 0000000000..b35c80890b --- /dev/null +++ b/remote/test/puppeteer/src/common/HTTPRequest.ts @@ -0,0 +1,839 @@ +/** + * 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 {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; +import {assert} from '../util/assert.js'; +import {ProtocolError} from './Errors.js'; +import {EventEmitter} from './EventEmitter.js'; +import {Frame} from './Frame.js'; +import {debugError, isString} from './util.js'; +import {HTTPResponse} from './HTTPResponse.js'; + +/** + * @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; + +interface CDPSession extends EventEmitter { + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']>; +} + +/** + * 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: string; + /** + * @internal + */ + _interceptionId: string | undefined; + /** + * @internal + */ + _failureText: string | null = null; + /** + * @internal + */ + _response: HTTPResponse | null = null; + /** + * @internal + */ + _fromMemoryCache = false; + /** + * @internal + */ + _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; + + /** + * Warning! Using this client can break Puppeteer. Use with caution. + * + * @experimental + */ + get client(): CDPSession { + return this.#client; + } + + /** + * @internal + */ + constructor( + client: CDPSession, + frame: Frame | null, + interceptionId: string | undefined, + allowInterception: boolean, + event: Protocol.Network.RequestWillBeSentEvent, + redirectChain: HTTPRequest[] + ) { + this.#client = client; + this._requestId = event.requestId; + this.#isNavigationRequest = + event.requestId === event.loaderId && event.type === 'Document'; + this._interceptionId = interceptionId; + this.#allowInterception = allowInterception; + this.#url = event.request.url; + this.#resourceType = (event.type || 'other').toLowerCase() as ResourceType; + this.#method = event.request.method; + this.#postData = event.request.postData; + this.#frame = frame; + this._redirectChain = redirectChain; + this.#continueRequestOverrides = {}; + this.#interceptHandlers = []; + this.#initiator = event.initiator; + + for (const [key, value] of Object.entries(event.request.headers)) { + this.#headers[key.toLowerCase()] = value; + } + } + + /** + * @returns the URL of the request + */ + url(): string { + return this.#url; + } + + /** + * @returns the `ContinueRequestOverrides` that will be used + * if the interception is allowed to continue (ie, `abort()` and + * `respond()` aren't called). + */ + continueRequestOverrides(): ContinueRequestOverrides { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#continueRequestOverrides; + } + + /** + * @returns The `ResponseForRequest` that gets used if the + * interception is allowed to respond (ie, `abort()` is not called). + */ + responseForRequest(): Partial<ResponseForRequest> | null { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#responseForRequest; + } + + /** + * @returns the most recent reason for aborting the request + */ + abortErrorReason(): Protocol.Network.ErrorReason | null { + assert(this.#allowInterception, 'Request Interception is not enabled!'); + return this.#abortErrorReason; + } + + /** + * @returns 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 { + if (!this.#allowInterception) { + return {action: InterceptResolutionAction.Disabled}; + } + if (this.#interceptionHandled) { + return {action: InterceptResolutionAction.AlreadyHandled}; + } + return {...this.#interceptResolutionState}; + } + + /** + * @returns `true` if the intercept resolution has already been handled, + * `false` otherwise. + */ + isInterceptResolutionHandled(): boolean { + return this.#interceptionHandled; + } + + /** + * 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 { + this.#interceptHandlers.push(pendingHandler); + } + + /** + * Awaits pending interception handlers and then decides how to fulfill + * the request interception. + */ + 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); + } + } + + /** + * Contains the request's resource type as it was perceived by the rendering + * engine. + */ + resourceType(): ResourceType { + return this.#resourceType; + } + + /** + * @returns the method used (`GET`, `POST`, etc.) + */ + method(): string { + return this.#method; + } + + /** + * @returns the request's post body, if any. + */ + postData(): string | undefined { + return this.#postData; + } + + /** + * @returns an object with HTTP headers associated with the request. All + * header names are lower-case. + */ + headers(): Record<string, string> { + return this.#headers; + } + + /** + * @returns A matching `HTTPResponse` object, or null if the response has not + * been received yet. + */ + response(): HTTPResponse | null { + return this._response; + } + + /** + * @returns the frame that initiated the request, or null if navigating to + * error pages. + */ + frame(): Frame | null { + return this.#frame; + } + + /** + * @returns true if the request is the driver of the current frame's navigation. + */ + isNavigationRequest(): boolean { + return this.#isNavigationRequest; + } + + /** + * @returns the initiator of the request. + */ + initiator(): Protocol.Network.Initiator { + return this.#initiator; + } + + /** + * 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[] { + return this._redirectChain.slice(); + } + + /** + * 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 { + if (!this._failureText) { + return null; + } + return { + errorText: this._failureText, + }; + } + + /** + * 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> { + // 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); + }); + } + + /** + * 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> { + // 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); + }); + } + + /** + * 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 = '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); + } +} + +/** + * @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'; + +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; + +/** + * @public + */ +export type ActionResult = 'continue' | 'abort' | 'respond'; + +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; +} + +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); +} + +// List taken from +// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +// with extra 306 and 418 codes. +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/src/common/HTTPResponse.ts b/remote/test/puppeteer/src/common/HTTPResponse.ts new file mode 100644 index 0000000000..ae3c372908 --- /dev/null +++ b/remote/test/puppeteer/src/common/HTTPResponse.ts @@ -0,0 +1,278 @@ +/** + * 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 {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; + +import {EventEmitter} from './EventEmitter.js'; +import {Frame} from './Frame.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {SecurityDetails} from './SecurityDetails.js'; +import {Protocol} from 'devtools-protocol'; +import {ProtocolError} from './Errors.js'; + +/** + * @public + */ +export interface RemoteAddress { + ip?: string; + port?: number; +} + +interface CDPSession extends EventEmitter { + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']>; +} + +/** + * The HTTPResponse class represents responses which are received by the + * {@link Page} class. + * + * @public + */ +export class HTTPResponse { + #client: CDPSession; + #request: HTTPRequest; + #contentPromise: Promise<Buffer> | null = null; + #bodyLoadedPromise: Promise<Error | void>; + #bodyLoadedPromiseFulfill: (err: Error | void) => 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; + + /** + * @internal + */ + constructor( + client: CDPSession, + request: HTTPRequest, + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null + ) { + this.#client = client; + this.#request = request; + + this.#bodyLoadedPromise = new Promise(fulfill => { + this.#bodyLoadedPromiseFulfill = fulfill; + }); + + 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; + } + + /** + * @internal + */ + _resolveBody(err: Error | null): void { + if (err) { + return this.#bodyLoadedPromiseFulfill(err); + } + return this.#bodyLoadedPromiseFulfill(); + } + + /** + * @returns The IP address and port number used to connect to the remote + * server. + */ + remoteAddress(): RemoteAddress { + return this.#remoteAddress; + } + + /** + * @returns The URL of the response. + */ + url(): string { + return this.#url; + } + + /** + * @returns True if the response was successful (status in the range 200-299). + */ + ok(): boolean { + // TODO: document === 0 case? + return this.#status === 0 || (this.#status >= 200 && this.#status <= 299); + } + + /** + * @returns The status code of the response (e.g., 200 for a success). + */ + status(): number { + return this.#status; + } + + /** + * @returns The status text of the response (e.g. usually an "OK" for a + * success). + */ + statusText(): string { + return this.#statusText; + } + + /** + * @returns An object with HTTP headers associated with the response. All + * header names are lower-case. + */ + headers(): Record<string, string> { + return this.#headers; + } + + /** + * @returns {@link SecurityDetails} if the response was received over the + * secure connection, or `null` otherwise. + */ + securityDetails(): SecurityDetails | null { + return this.#securityDetails; + } + + /** + * @returns Timing information related to the response. + */ + timing(): Protocol.Network.ResourceTiming | null { + return this.#timing; + } + + /** + * @returns Promise which resolves to a buffer with response body. + */ + 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; + } + + /** + * @returns Promise which resolves to a text representation of response body. + */ + async text(): Promise<string> { + const content = await this.buffer(); + return content.toString('utf8'); + } + + /** + * + * @returns 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); + } + + /** + * @returns A matching {@link HTTPRequest} object. + */ + request(): HTTPRequest { + return this.#request; + } + + /** + * @returns True if the response was served from either the browser's disk + * cache or memory cache. + */ + fromCache(): boolean { + return this.#fromDiskCache || this.#request._fromMemoryCache; + } + + /** + * @returns True if the response was served by a service worker. + */ + fromServiceWorker(): boolean { + return this.#fromServiceWorker; + } + + /** + * @returns A {@link Frame} that initiated this response, or `null` if + * navigating to error pages. + */ + frame(): Frame | null { + return this.#request.frame(); + } +} diff --git a/remote/test/puppeteer/src/common/Input.ts b/remote/test/puppeteer/src/common/Input.ts new file mode 100644 index 0000000000..8234f49355 --- /dev/null +++ b/remote/test/puppeteer/src/common/Input.ts @@ -0,0 +1,677 @@ +/** + * 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 {CDPSession} from './Connection.js'; +import {_keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout.js'; +import {Protocol} from 'devtools-protocol'; +import {Point} from './JSHandle.js'; + +type KeyDescription = Required< + Pick<KeyDefinition, 'keyCode' | 'key' | 'text' | 'code' | 'location'> +>; + +/** + * 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. + */ + async down( + key: KeyInput, + options: {text?: string} = {text: undefined} + ): 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, + }); + } + + #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. + */ + async press( + key: KeyInput, + options: {delay?: number; text?: 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 type MouseButton = 'left' | 'right' | 'middle' | 'back' | 'forward'; + +/** + * @public + */ +export interface MouseOptions { + button?: MouseButton; + clickCount?: number; +} + +/** + * @public + */ +export interface MouseWheelOptions { + deltaX?: number; + deltaY?: 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; + #x = 0; + #y = 0; + #button: MouseButton | 'none' = 'none'; + + /** + * @internal + */ + constructor(client: CDPSession, keyboard: Keyboard) { + this.#client = client; + this.#keyboard = keyboard; + } + + /** + * Dispatches a `mousemove` event. + * @param x - Horizontal position of the mouse. + * @param y - Vertical position of the mouse. + * @param options - Optional object. If specified, the `steps` property + * sends intermediate `mousemove` events when set to `1` (default). + */ + async move( + x: number, + y: number, + options: {steps?: number} = {} + ): Promise<void> { + const {steps = 1} = options; + const fromX = this.#x, + fromY = this.#y; + this.#x = x; + this.#y = y; + for (let i = 1; i <= steps; i++) { + await this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseMoved', + button: this.#button, + x: fromX + (this.#x - fromX) * (i / steps), + y: fromY + (this.#y - fromY) * (i / steps), + modifiers: this.#keyboard._modifiers, + }); + } + } + + /** + * 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 - Optional `MouseOptions`. + */ + async click( + x: number, + y: number, + options: MouseOptions & {delay?: number} = {} + ): Promise<void> { + const {delay = null} = options; + if (delay !== null) { + await this.move(x, y); + await this.down(options); + await new Promise(f => { + return setTimeout(f, delay); + }); + await this.up(options); + } else { + await this.move(x, y); + await this.down(options); + await this.up(options); + } + } + + /** + * Dispatches a `mousedown` event. + * @param options - Optional `MouseOptions`. + */ + async down(options: MouseOptions = {}): Promise<void> { + const {button = 'left', clickCount = 1} = options; + this.#button = button; + await this.#client.send('Input.dispatchMouseEvent', { + type: 'mousePressed', + button, + x: this.#x, + y: this.#y, + modifiers: this.#keyboard._modifiers, + clickCount, + }); + } + + /** + * Dispatches a `mouseup` event. + * @param options - Optional `MouseOptions`. + */ + async up(options: MouseOptions = {}): Promise<void> { + const {button = 'left', clickCount = 1} = options; + this.#button = 'none'; + await this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseReleased', + button, + x: this.#x, + y: this.#y, + modifiers: this.#keyboard._modifiers, + clickCount, + }); + } + + /** + * 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; + await this.#client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: this.#x, + y: this.#y, + deltaX, + deltaY, + modifiers: this.#keyboard._modifiers, + pointerType: 'mouse', + }); + } + + /** + * 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 target - 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> { + const touchPoints = [{x: Math.round(x), y: Math.round(y)}]; + await this.#client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints, + modifiers: this.#keyboard._modifiers, + }); + await this.#client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [], + modifiers: this.#keyboard._modifiers, + }); + } +} diff --git a/remote/test/puppeteer/src/common/IsolatedWorld.ts b/remote/test/puppeteer/src/common/IsolatedWorld.ts new file mode 100644 index 0000000000..9fe587e7c4 --- /dev/null +++ b/remote/test/puppeteer/src/common/IsolatedWorld.ts @@ -0,0 +1,907 @@ +/** + * 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 {source as injectedSource} from '../generated/injected.js'; +import {assert} from '../util/assert.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; +import {CDPSession} from './Connection.js'; +import {ElementHandle} from './ElementHandle.js'; +import {TimeoutError} from './Errors.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {Frame} from './Frame.js'; +import {FrameManager} from './FrameManager.js'; +import {MouseButton} from './Input.js'; +import {JSHandle} from './JSHandle.js'; +import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; +import {TimeoutSettings} from './TimeoutSettings.js'; +import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; +import { + createJSHandle, + debugError, + isNumber, + isString, + makePredicateString, + pageBindingInitString, +} from './util.js'; + +// predicateQueryHandler and checkWaitForOptions are declared here so that +// TypeScript knows about them when used in the predicate function below. +declare const predicateQueryHandler: ( + element: Element | Document, + selector: string +) => Promise<Element | Element[] | NodeListOf<Element>>; +declare const checkWaitForOptions: ( + node: Node | null, + waitForVisible: boolean, + waitForHidden: boolean +) => Element | null | boolean; + +/** + * @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 `30000` (30 seconds) + */ + timeout?: number; +} + +/** + * @internal + */ +export interface PageBinding { + name: string; + pptrFunction: Function; +} + +/** + * A unique key for {@link IsolatedWorldChart} to denote the default world. + * Execution contexts are automatically created in the default world. + * + * @internal + */ +export const MAIN_WORLD = Symbol('mainWorld'); +/** + * A unique key for {@link IsolatedWorldChart} to denote the puppeteer world. + * This world contains all puppeteer-internal bindings/code. + * + * @internal + */ +export const PUPPETEER_WORLD = Symbol('puppeteerWorld'); +/** + * @internal + */ +export interface IsolatedWorldChart { + [key: string]: IsolatedWorld; + [MAIN_WORLD]: IsolatedWorld; + [PUPPETEER_WORLD]: IsolatedWorld; +} + +/** + * @internal + */ +export class IsolatedWorld { + #frame: Frame; + #injected: boolean; + #document?: ElementHandle<Document>; + #context = createDeferredPromise<ExecutionContext>(); + #detached = false; + + // Set of bindings that have been registered in the current context. + #ctxBindings = new Set<string>(); + + // Contains mapping from functions that should be bound to Puppeteer functions. + #boundFunctions = new Map<string, Function>(); + #waitTasks = new Set<WaitTask>(); + + get _waitTasks(): Set<WaitTask> { + return this.#waitTasks; + } + + get _boundFunctions(): Map<string, Function> { + return this.#boundFunctions; + } + + static #bindingIdentifier = (name: string, contextId: number) => { + return `${name}_${contextId}`; + }; + + constructor(frame: Frame, injected = false) { + // Keep own reference to client because it might differ from the FrameManager's + // client for OOP iframes. + this.#frame = frame; + this.#injected = injected; + 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 { + if (this.#injected) { + context.evaluate(injectedSource).catch(debugError); + } + this.#ctxBindings.clear(); + this.#context.resolve(context); + for (const waitTask of this._waitTasks) { + waitTask.rerun(); + } + } + + hasContext(): boolean { + return this.#context.resolved(); + } + + _detach(): void { + this.#detached = true; + this.#client.off('Runtime.bindingCalled', this.#onBindingCalled); + for (const waitTask of this._waitTasks) { + waitTask.terminate( + 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 EvaluateFunc< + [ElementHandle<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[ElementHandle<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 EvaluateFunc< + [Array<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[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; + // We rely upon the fact that document.open() will reset frame lifecycle with "init" + // lifecycle event. @see https://crrev.com/608658 + await this.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, 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: {delay?: number; button?: MouseButton; clickCount?: number} + ): 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. + #settingUpBinding: Promise<void> | null = null; + + async _addBindingToContext( + context: ExecutionContext, + name: string + ): Promise<void> { + // Previous operation added the binding so we are done. + if ( + this.#ctxBindings.has( + IsolatedWorld.#bindingIdentifier(name, context._contextId) + ) + ) { + return; + } + // Wait for other operation to finish + if (this.#settingUpBinding) { + await this.#settingUpBinding; + return this._addBindingToContext(context, name); + } + + const bind = async (name: string) => { + const expression = pageBindingInitString('internal', name); + try { + // TODO: In theory, it would be enough to call this just once + await context._client.send('Runtime.addBinding', { + name, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore The protocol definition is not up to date. + executionContextName: context._contextName, + }); + await context.evaluate(expression); + } 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 + const ctxDestroyed = (error as Error).message.includes( + 'Execution context was destroyed' + ); + const ctxNotFound = (error as Error).message.includes( + 'Cannot find context with specified id' + ); + if (ctxDestroyed || ctxNotFound) { + return; + } else { + debugError(error); + return; + } + } + this.#ctxBindings.add( + IsolatedWorld.#bindingIdentifier(name, context._contextId) + ); + }; + + this.#settingUpBinding = bind(name); + await this.#settingUpBinding; + this.#settingUpBinding = null; + } + + #onBindingCalled = async ( + event: Protocol.Runtime.BindingCalledEvent + ): Promise<void> => { + let payload: {type: string; name: string; seq: number; args: unknown[]}; + if (!this.hasContext()) { + return; + } + const context = await this.executionContext(); + 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} = payload; + if ( + type !== 'internal' || + !this.#ctxBindings.has( + IsolatedWorld.#bindingIdentifier(name, context._contextId) + ) + ) { + return; + } + if (context._contextId !== event.executionContextId) { + return; + } + try { + const fn = this._boundFunctions.get(name); + if (!fn) { + throw new Error(`Bound function $name is not found`); + } + const result = await fn(...args); + await context.evaluate(deliverResult, name, seq, result); + } catch (error) { + // The WaitTask may already have been resolved by timing out, or the + // exection context may have been destroyed. + // In both caes, the promises above are rejected with a protocol error. + // We can safely ignores these, as the WaitTask is re-installed in + // the next execution context if needed. + if ((error as Error).message.includes('Protocol error')) { + return; + } + debugError(error); + } + function deliverResult(name: string, seq: number, result: unknown): void { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Code is evaluated in a different context. + (globalThis as any)[name].callbacks.get(seq).resolve(result); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Code is evaluated in a different context. + (globalThis as any)[name].callbacks.delete(seq); + } + }; + + async _waitForSelectorInPage( + queryOne: Function, + root: ElementHandle<Node> | undefined, + selector: string, + options: WaitForSelectorOptions, + binding?: PageBinding + ): Promise<JSHandle<unknown> | null> { + const { + visible: waitForVisible = false, + hidden: waitForHidden = false, + timeout = this.#timeoutSettings.timeout(), + } = options; + const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; + const title = `selector \`${selector}\`${ + waitForHidden ? ' to be hidden' : '' + }`; + async function predicate( + root: Element | Document, + selector: string, + waitForVisible: boolean, + waitForHidden: boolean + ): Promise<Node | null | boolean> { + const node = (await predicateQueryHandler(root, selector)) as Element; + return checkWaitForOptions(node, waitForVisible, waitForHidden); + } + const waitTaskOptions: WaitTaskOptions = { + isolatedWorld: this, + predicateBody: makePredicateString(predicate, queryOne), + predicateAcceptsContextElement: true, + title, + polling, + timeout, + args: [selector, waitForVisible, waitForHidden], + binding, + root, + }; + const waitTask = new WaitTask(waitTaskOptions); + return waitTask.promise; + } + + waitForFunction( + pageFunction: Function | string, + options: {polling?: string | number; timeout?: number} = {}, + ...args: unknown[] + ): Promise<JSHandle> { + const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} = + options; + const waitTaskOptions: WaitTaskOptions = { + isolatedWorld: this, + predicateBody: pageFunction, + predicateAcceptsContextElement: false, + title: 'function', + polling, + timeout, + args, + }; + const waitTask = new WaitTask(waitTaskOptions); + return waitTask.promise; + } + + 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 executionContext = await this.executionContext(); + assert( + handle.executionContext() !== executionContext, + 'Cannot adopt handle that already belongs to this execution context' + ); + const nodeInfo = await this.#client.send('DOM.describeNode', { + objectId: handle.remoteObject().objectId, + }); + return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; + } + + async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { + const result = await this.adoptHandle(handle); + await handle.dispose(); + return result; + } +} + +/** + * @internal + */ +export interface WaitTaskOptions { + isolatedWorld: IsolatedWorld; + predicateBody: Function | string; + predicateAcceptsContextElement: boolean; + title: string; + polling: string | number; + timeout: number; + binding?: PageBinding; + args: unknown[]; + root?: ElementHandle<Node>; +} + +const noop = (): void => {}; + +/** + * @internal + */ +export class WaitTask { + #isolatedWorld: IsolatedWorld; + #polling: 'raf' | 'mutation' | number; + #timeout: number; + #predicateBody: string; + #predicateAcceptsContextElement: boolean; + #args: unknown[]; + #binding?: PageBinding; + #runCount = 0; + #resolve: (x: JSHandle) => void = noop; + #reject: (x: Error) => void = noop; + #timeoutTimer?: NodeJS.Timeout; + #terminated = false; + #root: ElementHandle<Node> | null = null; + + promise: Promise<JSHandle>; + + constructor(options: WaitTaskOptions) { + if (isString(options.polling)) { + assert( + options.polling === 'raf' || options.polling === 'mutation', + 'Unknown polling option: ' + options.polling + ); + } else if (isNumber(options.polling)) { + assert( + options.polling > 0, + 'Cannot poll with non-positive interval: ' + options.polling + ); + } else { + throw new Error('Unknown polling options: ' + options.polling); + } + + function getPredicateBody(predicateBody: Function | string) { + if (isString(predicateBody)) { + return `return (${predicateBody});`; + } + return `return (${predicateBody})(...args);`; + } + + this.#isolatedWorld = options.isolatedWorld; + this.#polling = options.polling; + this.#timeout = options.timeout; + this.#root = options.root || null; + this.#predicateBody = getPredicateBody(options.predicateBody); + this.#predicateAcceptsContextElement = + options.predicateAcceptsContextElement; + this.#args = options.args; + this.#binding = options.binding; + this.#runCount = 0; + this.#isolatedWorld._waitTasks.add(this); + if (this.#binding) { + this.#isolatedWorld._boundFunctions.set( + this.#binding.name, + this.#binding.pptrFunction + ); + } + this.promise = new Promise<JSHandle>((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + }); + // Since page navigation requires us to re-install the pageScript, we should track + // timeout on our end. + if (options.timeout) { + const timeoutError = new TimeoutError( + `waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded` + ); + this.#timeoutTimer = setTimeout(() => { + return this.terminate(timeoutError); + }, options.timeout); + } + this.rerun(); + } + + terminate(error: Error): void { + this.#terminated = true; + this.#reject(error); + this.#cleanup(); + } + + async rerun(): Promise<void> { + const runCount = ++this.#runCount; + let success: JSHandle | null = null; + let error: Error | null = null; + const context = await this.#isolatedWorld.executionContext(); + if (this.#terminated || runCount !== this.#runCount) { + return; + } + if (this.#binding) { + await this.#isolatedWorld._addBindingToContext( + context, + this.#binding.name + ); + } + if (this.#terminated || runCount !== this.#runCount) { + return; + } + try { + success = await context.evaluateHandle( + waitForPredicatePageFunction, + this.#root || null, + this.#predicateBody, + this.#predicateAcceptsContextElement, + this.#polling, + this.#timeout, + ...this.#args + ); + } catch (error_) { + error = error_ as Error; + } + + if (this.#terminated || runCount !== this.#runCount) { + if (success) { + await success.dispose(); + } + return; + } + + // Ignore timeouts in pageScript - we track timeouts ourselves. + // If the frame's execution context has already changed, `frame.evaluate` will + // throw an error - ignore this predicate run altogether. + if ( + !error && + (await this.#isolatedWorld + .evaluate(s => { + return !s; + }, success) + .catch(() => { + return true; + })) + ) { + if (!success) { + throw new Error('Assertion: result handle is not available'); + } + await success.dispose(); + return; + } + if (error) { + if (error.message.includes('TypeError: binding is not a function')) { + return this.rerun(); + } + // 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' + ) + ) { + this.terminate( + new Error('waitForFunction failed: frame got detached.') + ); + return; + } + + // 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; + } + + this.#reject(error); + } else { + if (!success) { + throw new Error('Assertion: result handle is not available'); + } + this.#resolve(success); + } + this.#cleanup(); + } + + #cleanup(): void { + this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer); + this.#isolatedWorld._waitTasks.delete(this); + } +} + +async function waitForPredicatePageFunction( + root: Node | null, + predicateBody: string, + predicateAcceptsContextElement: boolean, + polling: 'raf' | 'mutation' | number, + timeout: number, + ...args: unknown[] +): Promise<unknown> { + root = root || document; + const predicate = new Function('...args', predicateBody); + let timedOut = false; + if (timeout) { + setTimeout(() => { + return (timedOut = true); + }, timeout); + } + switch (polling) { + case 'raf': + return await pollRaf(); + case 'mutation': + return await pollMutation(); + default: + return await pollInterval(polling); + } + + async function pollMutation(): Promise<unknown> { + const success = predicateAcceptsContextElement + ? await predicate(root, ...args) + : await predicate(...args); + if (success) { + return Promise.resolve(success); + } + + let fulfill = (_?: unknown) => {}; + const result = new Promise(x => { + return (fulfill = x); + }); + const observer = new MutationObserver(async () => { + if (timedOut) { + observer.disconnect(); + fulfill(); + } + const success = predicateAcceptsContextElement + ? await predicate(root, ...args) + : await predicate(...args); + if (success) { + observer.disconnect(); + fulfill(success); + } + }); + if (!root) { + throw new Error('Root element is not found.'); + } + observer.observe(root, { + childList: true, + subtree: true, + attributes: true, + }); + return result; + } + + async function pollRaf(): Promise<unknown> { + let fulfill = (_?: unknown): void => {}; + const result = new Promise(x => { + return (fulfill = x); + }); + await onRaf(); + return result; + + async function onRaf(): Promise<void> { + if (timedOut) { + fulfill(); + return; + } + const success = predicateAcceptsContextElement + ? await predicate(root, ...args) + : await predicate(...args); + if (success) { + fulfill(success); + } else { + requestAnimationFrame(onRaf); + } + } + } + + async function pollInterval(pollInterval: number): Promise<unknown> { + let fulfill = (_?: unknown): void => {}; + const result = new Promise(x => { + return (fulfill = x); + }); + await onTimeout(); + return result; + + async function onTimeout(): Promise<void> { + if (timedOut) { + fulfill(); + return; + } + const success = predicateAcceptsContextElement + ? await predicate(root, ...args) + : await predicate(...args); + if (success) { + fulfill(success); + } else { + setTimeout(onTimeout, pollInterval); + } + } + } +} diff --git a/remote/test/puppeteer/src/common/JSHandle.ts b/remote/test/puppeteer/src/common/JSHandle.ts new file mode 100644 index 0000000000..55613eb610 --- /dev/null +++ b/remote/test/puppeteer/src/common/JSHandle.ts @@ -0,0 +1,332 @@ +/** + * 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 {assert} from '../util/assert.js'; +import {CDPSession} from './Connection.js'; +import type {ElementHandle} from './ElementHandle.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {MouseButton} from './Input.js'; +import {EvaluateFunc, HandleFor, HandleOr} from './types.js'; +import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js'; + +declare const __JSHandleSymbol: unique symbol; + +/** + * @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; +} + +/** + * 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; + + #disposed = false; + #context: ExecutionContext; + #remoteObject: Protocol.Runtime.RemoteObject; + + /** + * @internal + */ + get client(): CDPSession { + return this.#context._client; + } + + /** + * @internal + */ + get disposed(): boolean { + return this.#disposed; + } + + /** + * @internal + */ + constructor( + context: ExecutionContext, + remoteObject: Protocol.Runtime.RemoteObject + ) { + this.#context = context; + this.#remoteObject = remoteObject; + } + + /** + * @internal + */ + executionContext(): ExecutionContext { + return this.#context; + } + + /** + * Evaluates the given function with the current handle as its first argument. + * + * @see {@link ExecutionContext.evaluate} for more details. + */ + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< + [this, ...Params] + > + >( + pageFunction: Func | string, + ...args: Params + ): // @ts-expect-error Circularity here is okay because we only need the return + // type which doesn't use `this`. + Promise<Awaited<ReturnType<Func>>> { + return await this.executionContext().evaluate(pageFunction, this, ...args); + } + + /** + * Evaluates the given function with the current handle as its first argument. + * + * @see {@link ExecutionContext.evaluateHandle} for more details. + */ + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< + [this, ...Params] + > + >( + pageFunction: Func | string, + ...args: Params + ): // @ts-expect-error Circularity here is okay because we only need the return + // type which doesn't use `this`. + Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return await this.executionContext().evaluateHandle( + pageFunction, + this, + ...args + ); + } + + /** + * 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]>> { + return this.evaluateHandle((object, propertyName) => { + return object[propertyName]; + }, propertyName); + } + + /** + * Gets a map of handles representing the properties of the current handle. + * + * @example + * + * ```ts + * const listHandle = await page.evaluateHandle(() => document.body.children); + * const properties = await listHandle.getProperties(); + * const children = []; + * for (const property of properties.values()) { + * const element = property.asElement(); + * if (element) { + * children.push(element); + * } + * } + * children; // holds elementHandles to all children of document.body + * ``` + */ + async getProperties(): Promise<Map<string, JSHandle>> { + 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; + } + + /** + * @returns A vanilla object representing the serializable portions of the + * referenced object. + * @throws Throws if the object cannot be serialized due to circularity. + * + * @remarks + * If the object has a `toJSON` function, it **will not** be called. + */ + async jsonValue(): Promise<T> { + 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; + } + + /** + * @returns Either `null` or the handle itself if the handle is an + * instance of {@link ElementHandle}. + */ + asElement(): ElementHandle<Node> | null { + return null; + } + + /** + * Releases the object referenced by the handle for garbage collection. + */ + async dispose(): Promise<void> { + if (this.#disposed) { + return; + } + this.#disposed = true; + await releaseObject(this.client, this.#remoteObject); + } + + /** + * Returns a string representation of the JSHandle. + * + * @remarks + * Useful during debugging. + */ + toString(): string { + if (!this.#remoteObject.objectId) { + return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); + } + const type = this.#remoteObject.subtype || this.#remoteObject.type; + return 'JSHandle@' + type; + } + + /** + * Provides access to the + * [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) + * backing this handle. + */ + remoteObject(): Protocol.Runtime.RemoteObject { + return this.#remoteObject; + } +} + +/** + * @public + */ +export interface Offset { + /** + * x-offset for the clickable point relative to the top-left corner of the border box. + */ + x: number; + /** + * y-offset for the clickable point relative to the top-left corner of the border box. + */ + y: number; +} + +/** + * @public + */ +export interface ClickOptions { + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. + * + * @defaultValue 0 + */ + delay?: number; + /** + * @defaultValue 'left' + */ + button?: MouseButton; + /** + * @defaultValue 1 + */ + clickCount?: number; + /** + * Offset for the clickable point relative to the top-left corner of the border box. + */ + offset?: Offset; +} + +/** + * @public + */ +export interface PressOptions { + /** + * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. + */ + delay?: number; + /** + * If specified, generates an input event with this text. + */ + text?: string; +} + +/** + * @public + */ +export interface Point { + x: number; + y: number; +} diff --git a/remote/test/puppeteer/src/common/LifecycleWatcher.ts b/remote/test/puppeteer/src/common/LifecycleWatcher.ts new file mode 100644 index 0000000000..b68e22dcd4 --- /dev/null +++ b/remote/test/puppeteer/src/common/LifecycleWatcher.ts @@ -0,0 +1,309 @@ +/** + * 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 {assert} from '../util/assert.js'; +import { + addEventListener, + PuppeteerEventListener, + removeEventListeners, +} from './util.js'; +import { + DeferredPromise, + createDeferredPromise, +} from '../util/DeferredPromise.js'; +import {TimeoutError} from './Errors.js'; +import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; +import {Frame} from './Frame.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {HTTPResponse} from './HTTPResponse.js'; +import {NetworkManagerEmittedEvents} from './NetworkManager.js'; +import {CDPSessionEmittedEvents} from './Connection.js'; +/** + * @public + */ +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; + + #sameDocumentNavigationCompleteCallback: (x?: Error) => void = noop; + #sameDocumentNavigationPromise = new Promise<Error | undefined>(fulfill => { + this.#sameDocumentNavigationCompleteCallback = fulfill; + }); + + #lifecycleCallback: () => void = noop; + #lifecyclePromise: Promise<void> = new Promise(fulfill => { + this.#lifecycleCallback = fulfill; + }); + + #newDocumentNavigationCompleteCallback: (x?: Error) => void = noop; + #newDocumentNavigationPromise: Promise<Error | undefined> = new Promise( + fulfill => { + this.#newDocumentNavigationCompleteCallback = fulfill; + } + ); + + #terminationCallback: (x?: Error) => void = noop; + #terminationPromise: Promise<Error | undefined> = new Promise(fulfill => { + this.#terminationCallback = fulfill; + }); + + #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) + ), + ]; + + 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(); + } + } + + #onResponse(response: HTTPResponse): void { + if (this.#navigationRequest?._requestId !== response.request()._requestId) { + return; + } + this.#navigationResponseReceived?.resolve(); + } + + #onFrameDetached(frame: Frame): void { + if (this.#frame === frame) { + this.#terminationCallback.call( + null, + 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.#terminationCallback.call(null, 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.#lifecycleCallback(); + if (this.#hasSameDocumentNavigation) { + this.#sameDocumentNavigationCompleteCallback(); + } + if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) { + this.#newDocumentNavigationCompleteCallback(); + } + + 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/src/common/NetworkConditions.ts b/remote/test/puppeteer/src/common/NetworkConditions.ts new file mode 100644 index 0000000000..786a732305 --- /dev/null +++ b/remote/test/puppeteer/src/common/NetworkConditions.ts @@ -0,0 +1,57 @@ +/** + * 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 + * `page.emulateNetworkConditions(networkConditions)`. Actual list of predefined + * conditions can be found in + * {@link https://github.com/puppeteer/puppeteer/blob/main/src/common/NetworkConditions.ts | src/common/NetworkConditions.ts}. + * + * @example + * + * ```ts + * const puppeteer = require('puppeteer'); + * const slow3G = puppeteer.networkConditions['Slow 3G']; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulateNetworkConditions(slow3G); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * @public + */ +export const networkConditions: Readonly<{ + 'Slow 3G': NetworkConditions; + 'Fast 3G': NetworkConditions; +}> = Object.freeze({ + 'Slow 3G': { + download: ((500 * 1000) / 8) * 0.8, + upload: ((500 * 1000) / 8) * 0.8, + latency: 400 * 5, + }, + 'Fast 3G': { + download: ((1.6 * 1000 * 1000) / 8) * 0.9, + upload: ((750 * 1000) / 8) * 0.9, + latency: 150 * 3.75, + }, +}); diff --git a/remote/test/puppeteer/src/common/NetworkEventManager.ts b/remote/test/puppeteer/src/common/NetworkEventManager.ts new file mode 100644 index 0000000000..0b0e373fc1 --- /dev/null +++ b/remote/test/puppeteer/src/common/NetworkEventManager.ts @@ -0,0 +1,203 @@ +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/src/common/NetworkManager.ts b/remote/test/puppeteer/src/common/NetworkManager.ts new file mode 100644 index 0000000000..da1a0d19f1 --- /dev/null +++ b/remote/test/puppeteer/src/common/NetworkManager.ts @@ -0,0 +1,636 @@ +/** + * 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 {EventEmitter} from './EventEmitter.js'; +import {Frame} from './Frame.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {HTTPResponse} from './HTTPResponse.js'; +import {FetchRequestId, NetworkEventManager} from './NetworkEventManager.js'; +import {debugError, isString} from './util.js'; +import {DeferredPromise} from '../util/DeferredPromise.js'; +import {createDebuggableDeferredPromise} from '../util/DebuggableDeferredPromise.js'; + +/** + * @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; + +interface CDPSession extends EventEmitter { + send<T extends keyof ProtocolMapping.Commands>( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']>; +} + +interface FrameManager { + frame(frameId: string): Frame | null; +} + +/** + * @internal + */ +export class NetworkManager extends EventEmitter { + #client: CDPSession; + #ignoreHTTPSErrors: boolean; + #frameManager: FrameManager; + #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: FrameManager + ) { + 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 { + /* TODO(jacktfranklin): This is defined in protocol.d.ts but not + * in an easily referrable way - we should look at exposing it. + */ + type AuthResponse = 'Default' | 'CancelAuth' | 'ProvideCredentials'; + let response: AuthResponse = '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) { + 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, + }; + } + + #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); + 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 + ) + ); + } + + 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/src/common/PDFOptions.ts b/remote/test/puppeteer/src/common/PDFOptions.ts new file mode 100644 index 0000000000..2613b4273f --- /dev/null +++ b/remote/test/puppeteer/src/common/PDFOptions.ts @@ -0,0 +1,202 @@ +/** + * 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.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 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 the empty string, 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 + * @defaultValue 30000 + */ + timeout?: number; +} + +/** + * @internal + */ +export interface PaperFormatDimensions { + width: number; + height: number; +} + +/** + * @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/src/common/Page.ts b/remote/test/puppeteer/src/common/Page.ts new file mode 100644 index 0000000000..ab3a2a2c82 --- /dev/null +++ b/remote/test/puppeteer/src/common/Page.ts @@ -0,0 +1,3633 @@ +/** + * 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 {Readable} from 'stream'; +import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {Accessibility} from './Accessibility.js'; +import {Browser, BrowserContext} from './Browser.js'; +import {CDPSession, CDPSessionEmittedEvents} from './Connection.js'; +import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; +import {Coverage} from './Coverage.js'; +import {Dialog} from './Dialog.js'; +import {ElementHandle} from './ElementHandle.js'; +import {EmulationManager} from './EmulationManager.js'; +import {EventEmitter, Handler} from './EventEmitter.js'; +import {FileChooser} from './FileChooser.js'; +import { + Frame, + FrameAddScriptTagOptions, + FrameAddStyleTagOptions, +} from './Frame.js'; +import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js'; +import {HTTPRequest} from './HTTPRequest.js'; +import {HTTPResponse} from './HTTPResponse.js'; +import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js'; +import {MAIN_WORLD, WaitForSelectorOptions} from './IsolatedWorld.js'; +import {JSHandle} from './JSHandle.js'; +import {PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; +import { + Credentials, + NetworkConditions, + NetworkManagerEmittedEvents, +} from './NetworkManager.js'; +import {LowerCasePaperFormat, PDFOptions, _paperFormats} 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 {EvaluateFunc, HandleFor, NodeFor} from './types.js'; +import { + createJSHandle, + debugError, + evaluationString, + getExceptionMessage, + getReadableAsBuffer, + getReadableFromProtocolStream, + importFS, + isNumber, + isString, + pageBindingDeliverErrorString, + pageBindingDeliverErrorValueString, + pageBindingDeliverResultString, + pageBindingInitString, + releaseObject, + valueFromRemoteObject, + waitForEvent, + waitWithTimeout, +} from './util.js'; +import {WebWorker} from './WebWorker.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. + * @eventProperty + */ + 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`: objec 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 Chromium. + * + * :::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 + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * await page.screenshot({path: 'screenshot.png'}); + * await browser.close(); + * })(); + * ``` + * + * The Page class 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 { + /** + * @internal + */ + static async _create( + client: CDPSession, + target: Target, + ignoreHTTPSErrors: boolean, + defaultViewport: Viewport | null, + screenshotTaskQueue: TaskQueue + ): Promise<Page> { + const page = new Page( + client, + target, + ignoreHTTPSErrors, + screenshotTaskQueue + ); + await page.#initialize(); + if (defaultViewport) { + await page.setViewport(defaultViewport); + } + 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; + #pageBindings = new Map<string, Function>(); + #coverage: Coverage; + #javascriptEnabled = true; + #viewport: Viewport | null; + #screenshotTaskQueue: TaskQueue; + #workers = new Map<string, WebWorker>(); + #fileChooserPromises = new Set<DeferredPromise<FileChooser>>(); + + #disconnectPromise?: Promise<Error>; + #userDragInterceptionEnabled = false; + #handlerMap = new WeakMap<Handler, Handler>(); + + /** + * @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); + }); + 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(); + + this.#frameManager.onDetachedFromTarget(target); + + const worker = this.#workers.get(sessionId!); + if (!worker) { + return; + } + this.#workers.delete(sessionId!); + this.emit(PageEmittedEvents.WorkerDestroyed, worker); + }; + + #onAttachedToTarget = async (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> { + await Promise.all([ + this.#frameManager.initialize(this.#target._targetId), + this.#client.send('Performance.enable'), + this.#client.send('Log.enable'), + ]); + } + + 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(); + } + + /** + * @returns `true` if drag events are being intercepted, `false` otherwise. + */ + isDragInterceptionEnabled(): boolean { + return this.#userDragInterceptionEnabled; + } + + /** + * @returns `true` if the page has JavaScript enabled, `false` otherwise. + */ + isJavaScriptEnabled(): boolean { + return this.#javascriptEnabled; + } + + /** + * 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 non-headless Chromium, 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> { + 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; + }); + } + + /** + * 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> { + 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, + }); + } + + /** + * @returns A target this page was created from. + */ + target(): Target { + return this.#target; + } + + /** + * @internal + */ + _client(): CDPSession { + return this.#client; + } + + /** + * Get the browser the page belongs to. + */ + browser(): Browser { + return this.#target.browser(); + } + + /** + * Get the browser context that the page belongs to. + */ + 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}]) + ); + } + } + + /** + * @returns The page's main frame. + * + * @remarks + * Page is guaranteed to have a main frame which persists during navigations. + */ + mainFrame(): Frame { + return this.#frameManager.mainFrame(); + } + + get keyboard(): Keyboard { + return this.#keyboard; + } + + get touchscreen(): Touchscreen { + return this.#touchscreen; + } + + get coverage(): Coverage { + return this.#coverage; + } + + get tracing(): Tracing { + return this.#tracing; + } + + get accessibility(): Accessibility { + return this.#accessibility; + } + + /** + * @returns An array of all frames attached to the page. + */ + frames(): Frame[] { + return this.#frameManager.frames(); + } + + /** + * @returns 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[] { + return Array.from(this.#workers.values()); + } + + /** + * 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. + * + * Enabling request interception disables page caching. + * + * 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 + * const puppeteer = require('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> { + return this.#frameManager.networkManager.setRequestInterception(value); + } + + /** + * @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> { + this.#userDragInterceptionEnabled = enabled; + return this.#client.send('Input.setInterceptDrags', {enabled}); + } + + /** + * @param enabled - When `true`, enables offline mode for the page. + * @remarks + * NOTE: while this method sets the network connection to offline, it does + * not change the parameters used in [page.emulateNetworkConditions(networkConditions)] + * (#pageemulatenetworkconditionsnetworkconditions) + */ + setOfflineMode(enabled: boolean): Promise<void> { + return this.#frameManager.networkManager.setOfflineMode(enabled); + } + + /** + * @param networkConditions - Passing `null` disables network condition emulation. + * @example + * + * ```ts + * const puppeteer = require('puppeteer'); + * const slow3G = puppeteer.networkConditions['Slow 3G']; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulateNetworkConditions(slow3G); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * @remarks + * NOTE: This does not affect WebSockets and WebRTC PeerConnections (see + * https://crbug.com/563644). To set the page offline, you can use + * [page.setOfflineMode(enabled)](#pagesetofflinemodeenabled). + */ + emulateNetworkConditions( + networkConditions: NetworkConditions | null + ): Promise<void> { + return this.#frameManager.networkManager.emulateNetworkConditions( + networkConditions + ); + } + + /** + * 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 { + this.#timeoutSettings.setDefaultNavigationTimeout(timeout); + } + + /** + * @param timeout - Maximum time in milliseconds. + */ + setDefaultTimeout(timeout: number): void { + this.#timeoutSettings.setDefaultTimeout(timeout); + } + + /** + * @returns Maximum time in milliseconds. + */ + getDefaultTimeout(): number { + return this.#timeoutSettings.timeout(); + } + + /** + * Runs `document.querySelector` within the page. If no element matches the + * 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> { + return this.mainFrame().$(selector); + } + + /** + * 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>>>> { + return this.mainFrame().$$(selector); + } + + /** + * @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.evaluteHandle` 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>>>> { + const context = await this.mainFrame().executionContext(); + return context.evaluateHandle(pageFunction, ...args); + } + + /** + * 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[]>> { + const context = await this.mainFrame().executionContext(); + assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); + const remoteObject = prototypeHandle.remoteObject(); + assert( + remoteObject.objectId, + 'Prototype JSHandle must not be referencing primitive value' + ); + const response = await context._client.send('Runtime.queryObjects', { + prototypeObjectId: remoteObject.objectId, + }); + return createJSHandle(context, response.objects) as HandleFor<Prototype[]>; + } + + /** + * 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 EvaluateFunc< + [ElementHandle<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[ElementHandle<NodeFor<Selector>>, ...Params]> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.mainFrame().$eval(selector, pageFunction, ...args); + } + + /** + * 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 EvaluateFunc< + [Array<NodeFor<Selector>>, ...Params] + > = EvaluateFunc<[Array<NodeFor<Selector>>, ...Params]> + >( + selector: Selector, + pageFunction: Func | string, + ...args: Params + ): Promise<Awaited<ReturnType<Func>>> { + return this.mainFrame().$$eval(selector, pageFunction, ...args); + } + + /** + * 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>>> { + return this.mainFrame().$x(expression); + } + + /** + * 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[]> { + 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); + } + + 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); + } + } + + /** + * @example + * + * ```ts + * await page.setCookie(cookieObject1, cookieObject2); + * ``` + */ + 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}); + } + } + + /** + * 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>> { + return this.mainFrame().addScriptTag(options); + } + + /** + * 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 | 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>> { + return this.mainFrame().addStyleTag(options); + } + + /** + * 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 + * const puppeteer = require('puppeteer'); + * const crypto = require('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 + * const puppeteer = require('puppeteer'); + * const fs = require('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> { + if (this.#pageBindings.has(name)) { + throw new Error( + `Failed to add page binding with name ${name}: window['${name}'] already exists!` + ); + } + + let exposedFunction: Function; + switch (typeof pptrFunction) { + case 'function': + exposedFunction = pptrFunction; + break; + default: + exposedFunction = pptrFunction.default; + break; + } + + this.#pageBindings.set(name, exposedFunction); + + 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); + }) + ); + } + + /** + * Provide credentials for `HTTP authentication`. + * + * @remarks + * To disable authentication, pass `null`. + */ + async authenticate(credentials: Credentials): Promise<void> { + return this.#frameManager.networkManager.authenticate(credentials); + } + + /** + * 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> { + return this.#frameManager.networkManager.setExtraHTTPHeaders(headers); + } + + /** + * @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> { + return this.#frameManager.networkManager.setUserAgent( + userAgent, + userAgentMetadata + ); + } + + /** + * @returns Object containing metrics as key/value pairs. + * + * - `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> { + 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.executionContextById( + event.executionContextId, + this.#client + ); + 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: {type: string; name: string; seq: number; args: unknown[]}; + 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} = payload; + if (type !== 'exposedFun' || !this.#pageBindings.has(name)) { + return; + } + let expression = null; + try { + const pageBinding = this.#pageBindings.get(name); + assert(pageBinding); + const result = await pageBinding(...args); + expression = pageBindingDeliverResultString(name, seq, result); + } catch (error) { + if (isErrorLike(error)) { + expression = pageBindingDeliverErrorString( + name, + seq, + error.message, + error.stack + ); + } else { + expression = pageBindingDeliverErrorValueString(name, seq, error); + } + } + this.#client + .send('Runtime.evaluate', { + expression, + contextId: event.executionContextId, + }) + .catch(debugError); + } + + #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}, + }); + } + + /** + * + * @returns + * @remarks Shortcut for + * {@link Frame.url | page.mainFrame().url()}. + */ + url(): string { + return this.mainFrame().url(); + } + + async content(): Promise<string> { + return await this.#frameManager.mainFrame().content(); + } + + /** + * @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> { + await this.#frameManager.mainFrame().setContent(html, options); + } + + /** + * @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()}. + * + * `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} = {} + ): Promise<HTTPResponse | null> { + return await this.#frameManager.mainFrame().goto(url, options); + } + + /** + * @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> { + const result = await Promise.all([ + this.waitForNavigation(options), + this.#client.send('Page.reload'), + ]); + + return result[0]; + } + + /** + * 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> { + 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; + } + + /** + * @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 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> { + const {timeout = this.#timeoutSettings.timeout()} = options; + return waitForEvent( + this.#frameManager.networkManager, + NetworkManagerEmittedEvents.Request, + request => { + if (isString(urlOrPredicate)) { + return urlOrPredicate === request.url(); + } + if (typeof urlOrPredicate === 'function') { + return !!urlOrPredicate(request); + } + return false; + }, + timeout, + this.#sessionClosePromise() + ); + } + + /** + * @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> { + 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() + ); + } + + /** + * @param options - Optional waiting parameters + * @returns Promise which resolves when network is idle + */ + async waitForNetworkIdle( + options: {idleTime?: number; timeout?: number} = {} + ): Promise<void> { + const {idleTime = 500, timeout = this.#timeoutSettings.timeout()} = options; + + const networkManager = this.#frameManager.networkManager; + + let idleResolveCallback: () => void; + const idlePromise = new Promise<void>(resolve => { + idleResolveCallback = resolve; + }); + + let abortRejectCallback: (error: Error) => void; + const abortPromise = new Promise<Error>((_, reject) => { + abortRejectCallback = reject; + }); + + let idleTimer: NodeJS.Timeout; + const onIdle = () => { + return idleResolveCallback(); + }; + + const cleanup = () => { + idleTimer && clearTimeout(idleTimer); + abortRejectCallback(new Error('abort')); + }; + + const evaluate = () => { + idleTimer && clearTimeout(idleTimer); + if (networkManager.numRequestsInProgress() === 0) { + idleTimer = setTimeout(onIdle, 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), + ]; + + await Promise.race([ + idlePromise, + ...eventPromises, + this.#sessionClosePromise(), + ]).then( + r => { + cleanup(); + return r; + }, + error => { + cleanup(); + throw error; + } + ); + } + + /** + * @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> { + 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; + } + + /** + * 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> { + return this.#go(-1, options); + } + + /** + * 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> { + 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]; + } + + /** + * Brings page to front (activates tab). + */ + async bringToFront(): Promise<void> { + await this.#client.send('Page.bringToFront'); + } + + /** + * Emulates given device metrics and user agent. + * + * @remarks + * This method is a shortcut for calling two methods: + * {@link Page.setUserAgent} and {@link Page.setViewport} To aid emulation, + * Puppeteer provides a list of device descriptors that can be obtained via + * {@link devices}. `page.emulate` 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 + * const puppeteer = require('puppeteer'); + * const iPhone = puppeteer.devices['iPhone 6']; + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.emulate(iPhone); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * @remarks List of all available devices is available in the source code: + * {@link https://github.com/puppeteer/puppeteer/blob/main/src/common/DeviceDescriptors.ts | src/common/DeviceDescriptors.ts}. + */ + async emulate(options: { + viewport: Viewport; + userAgent: string; + }): Promise<void> { + await Promise.all([ + this.setViewport(options.viewport), + this.setUserAgent(options.userAgent), + ]); + } + + /** + * @param enabled - Whether or not to enable JavaScript on the page. + * @returns + * @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> { + if (this.#javascriptEnabled === enabled) { + return; + } + this.#javascriptEnabled = enabled; + await this.#client.send('Emulation.setScriptExecutionDisabled', { + value: !enabled, + }); + } + + /** + * 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> { + await this.#client.send('Page.setBypassCSP', {enabled}); + } + + /** + * @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> { + assert( + type === 'screen' || + type === 'print' || + (type ?? undefined) === undefined, + 'Unsupported media type: ' + type + ); + await this.#client.send('Emulation.setEmulatedMedia', { + media: type || '', + }); + } + + /** + * 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> { + 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, + }); + } + + /** + * @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> { + 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, + }); + } + } + + /** + * @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> { + 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; + } + } + + /** + * 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> { + if (overrides) { + await this.#client.send('Emulation.setIdleOverride', { + isUserActive: overrides.isUserActive, + isScreenUnlocked: overrides.isScreenUnlocked, + }); + } else { + await this.#client.send('Emulation.clearIdleOverride'); + } + } + + /** + * Simulates the given vision deficiency on the page. + * + * @example + * + * ```ts + * const puppeteer = require('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> { + 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; + } + } + + /** + * `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> { + const needsReload = await this.#emulationManager.emulateViewport(viewport); + this.#viewport = viewport; + if (needsReload) { + await this.reload(); + } + } + + /** + * @returns + * + * - `width`: page's width in pixels + * + * - `height`: page's height in pixels + * + * - `deviceScalarFactor`: 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 { + return this.#viewport; + } + + /** + * Evaluates a function in the page's context and returns the result. + * + * If the function passed to `page.evaluteHandle` 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>>> { + return this.#frameManager.mainFrame().evaluate(pageFunction, ...args); + } + + /** + * 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> { + const source = evaluationString(pageFunction, ...args); + await this.#client.send('Page.addScriptToEvaluateOnNewDocument', { + source, + }); + } + + /** + * Toggles ignoring cache for each request based on the enabled state. By + * default, caching is enabled. + * @param enabled - sets the `enabled` state of cache + */ + async setCacheEnabled(enabled = true): Promise<void> { + await this.#frameManager.networkManager.setCacheEnabled(enabled); + } + + /** + * @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`. + * + * NOTE: Screenshots take at least 1/6 second on OS X. See + * {@link https://crbug.com/741689} for discussion. + * @returns Promise which resolves to buffer or a base64 string (depending on + * the value of `encoding`) with captured screenshot. + */ + 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; + const captureBeyondViewport = + typeof options.captureBeyondViewport === 'boolean' + ? options.captureBeyondViewport + : true; + const fromSurface = + typeof options.fromSurface === 'boolean' + ? options.fromSurface + : undefined; + + if (options.fullPage) { + const metrics = await this.#client.send('Page.getLayoutMetrics'); + // Fallback to `contentSize` in case of using Firefox. + const {width, height} = metrics.cssContentSize || metrics.contentSize; + + // Overwrite clip for full page. + clip = {x: 0, y: 0, width, height, scale: 1}; + + if (!captureBeyondViewport) { + 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, + }); + } + } + 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 === undefined ? 1 : clip.scale, + } + : undefined, + captureBeyondViewport, + fromSurface, + }); + if (shouldSetDefaultBackground) { + await this.#resetDefaultBackgroundColor(); + } + + if (options.fullPage && this.#viewport) { + await this.setViewport(this.#viewport); + } + + const buffer = + options.encoding === 'base64' + ? result.data + : Buffer.from(result.data, 'base64'); + + if (options.path) { + try { + const fs = (await importFS()).promises; + await fs.writeFile(options.path, buffer); + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Screenshots can only be written to a file path in a Node-like environment.' + ); + } + throw error; + } + } + 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}; + } + } + + /** + * Generates a PDF of the page with the `print` CSS media type. + * @remarks + * + * NOTE: PDF generation is only supported in Chrome headless mode. + * + * 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> { + const { + scale = 1, + displayHeaderFooter = false, + headerTemplate = '', + footerTemplate = '', + printBackground = false, + landscape = false, + pageRanges = '', + preferCSSPageSize = false, + margin = {}, + omitBackground = false, + timeout = 30000, + } = options; + + let paperWidth = 8.5; + let paperHeight = 11; + if (options.format) { + const format = + _paperFormats[options.format.toLowerCase() as LowerCasePaperFormat]; + assert(format, 'Unknown paper format: ' + options.format); + paperWidth = format.width; + paperHeight = format.height; + } else { + paperWidth = convertPrintParameterToInches(options.width) || paperWidth; + paperHeight = + convertPrintParameterToInches(options.height) || paperHeight; + } + + const marginTop = convertPrintParameterToInches(margin.top) || 0; + const marginLeft = convertPrintParameterToInches(margin.left) || 0; + const marginBottom = convertPrintParameterToInches(margin.bottom) || 0; + const marginRight = convertPrintParameterToInches(margin.right) || 0; + + if (omitBackground) { + await this.#setTransparentBackgroundColor(); + } + + const printCommandPromise = this.#client.send('Page.printToPDF', { + transferMode: 'ReturnAsStream', + landscape, + displayHeaderFooter, + headerTemplate, + footerTemplate, + printBackground, + scale, + paperWidth, + paperHeight, + marginTop, + marginBottom, + marginLeft, + marginRight, + 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); + } + + /** + * @param options - + * @returns + */ + 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; + } + + /** + * @returns The page's title + * @remarks + * Shortcut for {@link Frame.title | page.mainFrame().title()}. + */ + async title(): Promise<string> { + return this.mainFrame().title(); + } + + 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; + } + } + + /** + * Indicates that the page has been closed. + * @returns + */ + isClosed(): boolean { + return this.#closed; + } + + get mouse(): Mouse { + return this.#mouse; + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link 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: { + delay?: number; + button?: MouseButton; + clickCount?: number; + } = {} + ): Promise<void> { + return this.mainFrame().click(selector, options); + } + + /** + * 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> { + return this.mainFrame().focus(selector); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link 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> { + return this.mainFrame().hover(selector); + } + + /** + * 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[]> { + return this.mainFrame().select(selector, ...values); + } + + /** + * This method fetches an element with `selector`, scrolls it into view if + * needed, and then uses {@link 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> { + return this.mainFrame().tap(selector); + } + + /** + * 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> { + return this.mainFrame().type(selector, text, options); + } + + /** + * @deprecated Use `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> { + return this.mainFrame().waitForTimeout(milliseconds); + } + + /** + * 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. + * + * This method works across navigations: + * + * ```ts + * const puppeteer = require('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> { + return await this.mainFrame().waitForSelector(selector, options); + } + + /** + * 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. + * + * This method works across navigation + * + * ```ts + * const puppeteer = require('puppeteer'); + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForXPath('//img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of [ + * 'https://example.com', + * 'https://google.com', + * 'https://bbc.com', + * ]) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * + * @param xpath - A + * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an + * element to wait for + * @param options - Optional waiting parameters + * @returns Promise which resolves when element specified by xpath string is + * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is + * not found in DOM. + * @remarks + * The optional Argument `options` have properties: + * + * - `visible`: A boolean to wait for element to be present in DOM and to be + * visible, i.e. to not have `display: none` or `visibility: hidden` CSS + * properties. Defaults to `false`. + * + * - `hidden`: A boolean wait for element to not be found in the DOM or to be + * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. + * Defaults to `false`. + * + * - `timeout`: A number which is maximum time to wait for in milliseconds. + * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default + * value can be changed by using the {@link Page.setDefaultTimeout} method. + */ + waitForXPath( + xpath: string, + options: { + visible?: boolean; + hidden?: boolean; + timeout?: number; + } = {} + ): Promise<ElementHandle<Node> | null> { + return this.mainFrame().waitForXPath(xpath, options); + } + + /** + * 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 + * const puppeteer = require('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 - Optional waiting parameters + * + * - `polling` - 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. + * - `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. + * @param args - Arguments to pass to `pageFunction` + * @returns A `Promise` which resolves to a JSHandle/ElementHandle of the the + * `pageFunction`'s return value. + */ + waitForFunction< + Params extends unknown[], + Func extends EvaluateFunc<Params> = EvaluateFunc<Params> + >( + pageFunction: Func | string, + options: { + timeout?: number; + polling?: string | number; + } = {}, + ...args: Params + ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { + return this.mainFrame().waitForFunction(pageFunction, options, ...args); + } +} + +const supportedMetrics = new Set<string>([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', +]); + +const unitToPixels = { + px: 1, + in: 96, + cm: 37.8, + mm: 3.78, +}; + +function convertPrintParameterToInches( + parameter?: string | number +): 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 / 96; +} diff --git a/remote/test/puppeteer/src/common/Product.ts b/remote/test/puppeteer/src/common/Product.ts new file mode 100644 index 0000000000..58a62fad3e --- /dev/null +++ b/remote/test/puppeteer/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/src/common/Puppeteer.ts b/remote/test/puppeteer/src/common/Puppeteer.ts new file mode 100644 index 0000000000..579f68a937 --- /dev/null +++ b/remote/test/puppeteer/src/common/Puppeteer.ts @@ -0,0 +1,167 @@ +/** + * 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 './Browser.js'; +import {BrowserConnectOptions, _connectToBrowser} from './BrowserConnector.js'; +import {ConnectionTransport} from './ConnectionTransport.js'; +import {devices} from './DeviceDescriptors.js'; +import {errors} from './Errors.js'; +import {networkConditions} from './NetworkConditions.js'; +import { + clearCustomQueryHandlers, + CustomQueryHandler, + customQueryHandlerNames, + registerCustomQueryHandler, + unregisterCustomQueryHandler, +} from './QueryHandler.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; +} + +/** + * 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 { + protected _isPuppeteerCore: boolean; + 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 _connectToBrowser(options); + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {devices} from 'puppeteer'; + * ``` + */ + get devices(): typeof devices { + return devices; + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {errors} from 'puppeteer'; + * ``` + */ + get errors(): typeof errors { + return errors; + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {networkConditions} from 'puppeteer'; + * ``` + */ + get networkConditions(): typeof networkConditions { + return networkConditions; + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {registerCustomQueryHandler} from 'puppeteer'; + * ``` + */ + registerCustomQueryHandler( + name: string, + queryHandler: CustomQueryHandler + ): void { + return registerCustomQueryHandler(name, queryHandler); + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {unregisterCustomQueryHandler} from 'puppeteer'; + * ``` + */ + unregisterCustomQueryHandler(name: string): void { + return unregisterCustomQueryHandler(name); + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {customQueryHandlerNames} from 'puppeteer'; + * ``` + */ + customQueryHandlerNames(): string[] { + return customQueryHandlerNames(); + } + + /** + * @deprecated Import directly puppeteer. + * @example + * + * ```ts + * import {clearCustomQueryHandlers} from 'puppeteer'; + * ``` + */ + clearCustomQueryHandlers(): void { + return clearCustomQueryHandlers(); + } +} diff --git a/remote/test/puppeteer/src/common/PuppeteerViewport.ts b/remote/test/puppeteer/src/common/PuppeteerViewport.ts new file mode 100644 index 0000000000..48a0823b52 --- /dev/null +++ b/remote/test/puppeteer/src/common/PuppeteerViewport.ts @@ -0,0 +1,51 @@ +/** + * 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. + * @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/src/common/QueryHandler.ts b/remote/test/puppeteer/src/common/QueryHandler.ts new file mode 100644 index 0000000000..5fd360e06d --- /dev/null +++ b/remote/test/puppeteer/src/common/QueryHandler.ts @@ -0,0 +1,354 @@ +/** + * 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 {ariaHandler} from './AriaQueryHandler.js'; +import {ElementHandle} from './ElementHandle.js'; +import {Frame} from './Frame.js'; +import { + MAIN_WORLD, + PUPPETEER_WORLD, + WaitForSelectorOptions, +} from './IsolatedWorld.js'; + +/** + * @public + */ +export interface CustomQueryHandler { + /** + * @returns A {@link Node} matching the given `selector` from {@link node}. + */ + queryOne?: (node: Node, selector: string) => Node | null; + /** + * @returns Some {@link Node}s matching the given `selector` from {@link node}. + */ + queryAll?: (node: Node, selector: string) => Node[]; +} + +/** + * @internal + */ +export interface InternalQueryHandler { + /** + * Queries for a single node given a selector and {@link ElementHandle}. + * + * Akin to {@link Window.prototype.querySelector}. + */ + queryOne?: ( + element: ElementHandle<Node>, + selector: string + ) => Promise<ElementHandle<Node> | null>; + /** + * Queries for multiple nodes given a selector and {@link ElementHandle}. + * + * Akin to {@link Window.prototype.querySelectorAll}. + */ + queryAll?: ( + element: ElementHandle<Node>, + selector: string + ) => Promise<Array<ElementHandle<Node>>>; + + /** + * Waits until a single node appears for a given selector and + * {@link ElementHandle}. + */ + waitFor?: ( + elementOrFrame: ElementHandle<Node> | Frame, + selector: string, + options: WaitForSelectorOptions + ) => Promise<ElementHandle<Node> | null>; +} + +function internalizeCustomQueryHandler( + handler: CustomQueryHandler +): InternalQueryHandler { + const internalHandler: InternalQueryHandler = {}; + + if (handler.queryOne) { + const queryOne = handler.queryOne; + internalHandler.queryOne = async (element, selector) => { + const jsHandle = await element.evaluateHandle(queryOne, selector); + const elementHandle = jsHandle.asElement(); + if (elementHandle) { + return elementHandle; + } + await jsHandle.dispose(); + return null; + }; + internalHandler.waitFor = async (elementOrFrame, selector, options) => { + let frame: Frame; + let element: ElementHandle<Node> | undefined; + if (elementOrFrame instanceof Frame) { + frame = elementOrFrame; + } else { + frame = elementOrFrame.frame; + element = await frame.worlds[PUPPETEER_WORLD].adoptHandle( + elementOrFrame + ); + } + const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage( + queryOne, + element, + selector, + options + ); + if (element) { + await element.dispose(); + } + if (!result) { + return null; + } + if (!(result instanceof ElementHandle)) { + await result.dispose(); + return null; + } + return frame.worlds[MAIN_WORLD].transferHandle(result); + }; + } + + if (handler.queryAll) { + const queryAll = handler.queryAll; + internalHandler.queryAll = async (element, selector) => { + const jsHandle = await element.evaluateHandle(queryAll, selector); + const properties = await jsHandle.getProperties(); + await jsHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) { + result.push(elementHandle); + } + } + return result; + }; + } + + return internalHandler; +} + +const defaultHandler = internalizeCustomQueryHandler({ + queryOne: (element, selector) => { + if (!('querySelector' in element)) { + throw new Error( + `Could not invoke \`querySelector\` on node of type ${element.nodeName}.` + ); + } + return ( + element as unknown as {querySelector(selector: string): Element} + ).querySelector(selector); + }, + queryAll: (element, selector) => { + if (!('querySelectorAll' in element)) { + throw new Error( + `Could not invoke \`querySelectorAll\` on node of type ${element.nodeName}.` + ); + } + return [ + ...( + element as unknown as { + querySelectorAll(selector: string): NodeList; + } + ).querySelectorAll(selector), + ]; + }, +}); + +const pierceHandler = internalizeCustomQueryHandler({ + queryOne: (element, selector) => { + let found: Node | null = null; + const search = (root: Node) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as HTMLElement; + 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 (element instanceof Document) { + element = element.documentElement; + } + search(element); + return found; + }, + + queryAll: (element, selector) => { + const result: Node[] = []; + const collect = (root: Node) => { + const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + do { + const currentNode = iter.currentNode as HTMLElement; + 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; + }, +}); + +const xpathHandler = internalizeCustomQueryHandler({ + queryOne: (element, selector) => { + const doc = element.ownerDocument || document; + const result = doc.evaluate( + selector, + element, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE + ); + return result.singleNodeValue; + }, + + queryAll: (element, selector) => { + const doc = element.ownerDocument || document; + const iterator = doc.evaluate( + selector, + element, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + const array: Node[] = []; + let item; + while ((item = iterator.iterateNext())) { + array.push(item); + } + return array; + }, +}); + +interface RegisteredQueryHandler { + handler: InternalQueryHandler; + transformSelector?: (selector: string) => string; +} + +const INTERNAL_QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>([ + ['aria', {handler: ariaHandler}], + ['pierce', {handler: pierceHandler}], + ['xpath', {handler: xpathHandler}], +]); +const QUERY_HANDLERS = new Map<string, RegisteredQueryHandler>(); + +/** + * 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 + */ +export function registerCustomQueryHandler( + name: string, + handler: CustomQueryHandler +): void { + if (INTERNAL_QUERY_HANDLERS.has(name)) { + throw new Error(`A query handler named "${name}" already exists`); + } + if (QUERY_HANDLERS.has(name)) { + throw new Error(`A custom query handler named "${name}" already exists`); + } + + const isValidName = /^[a-zA-Z]+$/.test(name); + if (!isValidName) { + throw new Error(`Custom query handler names may only contain [a-zA-Z]`); + } + + QUERY_HANDLERS.set(name, {handler: internalizeCustomQueryHandler(handler)}); +} + +/** + * @param name - The name of the query handler to unregistered. + * + * @public + */ +export function unregisterCustomQueryHandler(name: string): void { + QUERY_HANDLERS.delete(name); +} + +/** + * @returns a list with the names of all registered custom query handlers. + * + * @public + */ +export function customQueryHandlerNames(): string[] { + return [...QUERY_HANDLERS.keys()]; +} + +/** + * Clears all registered handlers. + * + * @public + */ +export function clearCustomQueryHandlers(): void { + QUERY_HANDLERS.clear(); +} + +const CUSTOM_QUERY_SEPARATORS = ['=', '/']; + +/** + * @internal + */ +export function getQueryHandlerAndSelector(selector: string): { + updatedSelector: string; + queryHandler: InternalQueryHandler; +} { + for (const handlerMap of [QUERY_HANDLERS, INTERNAL_QUERY_HANDLERS]) { + for (const [ + name, + {handler: queryHandler, transformSelector}, + ] of handlerMap) { + for (const separator of CUSTOM_QUERY_SEPARATORS) { + const prefix = `${name}${separator}`; + if (selector.startsWith(prefix)) { + selector = selector.slice(prefix.length); + if (transformSelector) { + selector = transformSelector(selector); + } + return {updatedSelector: selector, queryHandler}; + } + } + } + } + return {updatedSelector: selector, queryHandler: defaultHandler}; +} diff --git a/remote/test/puppeteer/src/common/SecurityDetails.ts b/remote/test/puppeteer/src/common/SecurityDetails.ts new file mode 100644 index 0000000000..7df6aef88a --- /dev/null +++ b/remote/test/puppeteer/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; + } + + /** + * @returns The name of the issuer of the certificate. + */ + issuer(): string { + return this.#issuer; + } + + /** + * @returns {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the start of the certificate's validity. + */ + validFrom(): number { + return this.#validFrom; + } + + /** + * @returns {@link https://en.wikipedia.org/wiki/Unix_time | Unix timestamp} + * marking the end of the certificate's validity. + */ + validTo(): number { + return this.#validTo; + } + + /** + * @returns The security protocol being used, e.g. "TLS 1.2". + */ + protocol(): string { + return this.#protocol; + } + + /** + * @returns The name of the subject to which the certificate was issued. + */ + subjectName(): string { + return this.#subjectName; + } + + /** + * @returns 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/src/common/Target.ts b/remote/test/puppeteer/src/common/Target.ts new file mode 100644 index 0000000000..5744834f1b --- /dev/null +++ b/remote/test/puppeteer/src/common/Target.ts @@ -0,0 +1,276 @@ +/** + * 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 {Page, PageEmittedEvents} from './Page.js'; +import {WebWorker} from './WebWorker.js'; +import {CDPSession} from './Connection.js'; +import {Browser, BrowserContext, IsPageTargetCallback} from './Browser.js'; +import {Viewport} from './PuppeteerViewport.js'; +import {Protocol} from 'devtools-protocol'; +import {TaskQueue} from './TaskQueue.js'; +import {TargetManager} from './TargetManager.js'; + +/** + * @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 Page._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/src/common/TargetManager.ts b/remote/test/puppeteer/src/common/TargetManager.ts new file mode 100644 index 0000000000..1b82599ada --- /dev/null +++ b/remote/test/puppeteer/src/common/TargetManager.ts @@ -0,0 +1,71 @@ +/** + * 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 +) => Promise<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/src/common/TaskQueue.ts b/remote/test/puppeteer/src/common/TaskQueue.ts new file mode 100644 index 0000000000..97cfe7c769 --- /dev/null +++ b/remote/test/puppeteer/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/src/common/TimeoutSettings.ts b/remote/test/puppeteer/src/common/TimeoutSettings.ts new file mode 100644 index 0000000000..97acc70147 --- /dev/null +++ b/remote/test/puppeteer/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/src/common/Tracing.ts b/remote/test/puppeteer/src/common/Tracing.ts new file mode 100644 index 0000000000..500e17269b --- /dev/null +++ b/remote/test/puppeteer/src/common/Tracing.ts @@ -0,0 +1,143 @@ +/** + * 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 {getReadableAsBuffer, getReadableFromProtocolStream} from './util.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {CDPSession} from './Connection.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/src/common/USKeyboardLayout.ts b/remote/test/puppeteer/src/common/USKeyboardLayout.ts new file mode 100644 index 0000000000..f6a042e5ce --- /dev/null +++ b/remote/test/puppeteer/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/src/common/WebWorker.ts b/remote/test/puppeteer/src/common/WebWorker.ts new file mode 100644 index 0000000000..cfc029c75c --- /dev/null +++ b/remote/test/puppeteer/src/common/WebWorker.ts @@ -0,0 +1,170 @@ +/** + * 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 {CDPSession} from './Connection.js'; +import {ConsoleMessageType} from './ConsoleMessage.js'; +import {EvaluateFunc, HandleFor} from './types.js'; +import {EventEmitter} from './EventEmitter.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {JSHandle} from './JSHandle.js'; +import {debugError} from './util.js'; +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +/** + * @internal + */ +export type ConsoleAPICalledCallback = ( + eventType: ConsoleMessageType, + handles: JSHandle[], + 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 JSHandle(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; + } + + /** + * @returns The URL of this web worker. + */ + url(): string { + return this.#url; + } + + /** + * 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/src/common/fetch.ts b/remote/test/puppeteer/src/common/fetch.ts new file mode 100644 index 0000000000..5f13a35288 --- /dev/null +++ b/remote/test/puppeteer/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/src/common/types.ts b/remote/test/puppeteer/src/common/types.ts new file mode 100644 index 0000000000..8af6f23f29 --- /dev/null +++ b/remote/test/puppeteer/src/common/types.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {JSHandle} from './JSHandle.js'; +import {ElementHandle} from './ElementHandle.js'; + +/** + * @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; +/** + * @public + */ +export type InnerParams<T extends unknown[]> = { + [K in keyof T]: FlattenHandle<T[K]>; +}; + +/** + * @public + */ +export type EvaluateFunc<T extends unknown[]> = ( + ...params: InnerParams<T> +) => Awaitable<unknown>; + +/** + * @public + */ +export type NodeFor<Selector extends string> = + Selector extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[Selector] + : Selector extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[Selector] + : Element; diff --git a/remote/test/puppeteer/src/common/util.ts b/remote/test/puppeteer/src/common/util.ts new file mode 100644 index 0000000000..0b280292ee --- /dev/null +++ b/remote/test/puppeteer/src/common/util.ts @@ -0,0 +1,488 @@ +/** + * 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 {Readable} from 'stream'; +import {isNode} from '../environment.js'; +import {assert} from '../util/assert.js'; +import {isErrorLike} from '../util/ErrorLike.js'; +import {CDPSession} from './Connection.js'; +import {debug} from './Debug.js'; +import {ElementHandle} from './ElementHandle.js'; +import {TimeoutError} from './Errors.js'; +import {CommonEventEmitter} from './EventEmitter.js'; +import {ExecutionContext} from './ExecutionContext.js'; +import {JSHandle} 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' && typeof BigInt !== 'undefined') { + 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 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 ElementHandle(context, remoteObject, context._world.frame()); + } + return new JSHandle(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 pageBindingInitString(type: string, name: string): string { + function addPageBinding(type: string, bindingName: string): void { + /* Cast window to any here as we're about to add properties to it + * via win[bindingName] which TypeScript doesn't like. + */ + const win = window as any; + const binding = win[bindingName]; + + win[bindingName] = (...args: unknown[]): Promise<unknown> => { + const me = (window as any)[bindingName]; + let callbacks = me.callbacks; + if (!callbacks) { + callbacks = new Map(); + me.callbacks = callbacks; + } + const seq = (me.lastSeq || 0) + 1; + me.lastSeq = seq; + const promise = new Promise((resolve, reject) => { + return callbacks.set(seq, {resolve, reject}); + }); + binding(JSON.stringify({type, name: bindingName, seq, args})); + return promise; + }; + } + return evaluationString(addPageBinding, type, name); +} + +/** + * @internal + */ +export function pageBindingDeliverResultString( + name: string, + seq: number, + result: unknown +): string { + function deliverResult(name: string, seq: number, result: unknown): void { + (window as any)[name].callbacks.get(seq).resolve(result); + (window as any)[name].callbacks.delete(seq); + } + return evaluationString(deliverResult, name, seq, result); +} + +/** + * @internal + */ +export function pageBindingDeliverErrorString( + name: string, + seq: number, + message: string, + stack?: string +): string { + function deliverError( + name: string, + seq: number, + message: string, + stack?: string + ): void { + const error = new Error(message); + error.stack = stack; + (window as any)[name].callbacks.get(seq).reject(error); + (window as any)[name].callbacks.delete(seq); + } + return evaluationString(deliverError, name, seq, message, stack); +} + +/** + * @internal + */ +export function pageBindingDeliverErrorValueString( + name: string, + seq: number, + value: unknown +): string { + function deliverErrorValue(name: string, seq: number, value: unknown): void { + (window as any)[name].callbacks.get(seq).reject(value); + (window as any)[name].callbacks.delete(seq); + } + return evaluationString(deliverErrorValue, name, seq, value); +} + +/** + * @internal + */ +export function makePredicateString( + predicate: Function, + predicateQueryHandler: Function +): string { + function checkWaitForOptions( + node: Node | null, + waitForVisible: boolean, + waitForHidden: boolean + ): Node | null | boolean { + if (!node) { + return waitForHidden; + } + if (!waitForVisible && !waitForHidden) { + return node; + } + const element = + node.nodeType === Node.TEXT_NODE + ? (node.parentElement as Element) + : (node as Element); + + const style = window.getComputedStyle(element); + const isVisible = + style && style.visibility !== 'hidden' && hasVisibleBoundingBox(); + const success = + waitForVisible === isVisible || waitForHidden === !isVisible; + return success ? node : null; + + function hasVisibleBoundingBox(): boolean { + const rect = element.getBoundingClientRect(); + return !!(rect.top || rect.bottom || rect.width || rect.height); + } + } + + return ` + (() => { + const predicateQueryHandler = ${predicateQueryHandler}; + const checkWaitForOptions = ${checkWaitForOptions}; + return (${predicate})(...args) + })() `; +} + +/** + * @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<T>((_res, 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') | null = null; +/** + * @internal + */ +export async function importFS(): Promise<typeof import('fs')> { + if (!fs) { + fs = await import('fs'); + } + return fs; +} + +/** + * @internal + */ +export async function getReadableAsBuffer( + readable: Readable, + path?: string +): Promise<Buffer | null> { + const buffers = []; + if (path) { + let fs: typeof import('fs').promises; + try { + fs = (await importFS()).promises; + } catch (error) { + if (error instanceof TypeError) { + throw new Error( + 'Cannot write to a path outside of a Node-like environment.' + ); + } + throw error; + } + const fileHandle = await fs.open(path, 'w+'); + for await (const chunk of readable) { + buffers.push(chunk); + await fileHandle.writeFile(chunk); + } + 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; + } + + 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); + } + }, + }); +} diff --git a/remote/test/puppeteer/src/compat.d.ts b/remote/test/puppeteer/src/compat.d.ts new file mode 100644 index 0000000000..6c330dc078 --- /dev/null +++ b/remote/test/puppeteer/src/compat.d.ts @@ -0,0 +1,3 @@ +declare const puppeteerDirname: string; + +export {puppeteerDirname}; diff --git a/remote/test/puppeteer/src/constants.ts b/remote/test/puppeteer/src/constants.ts new file mode 100644 index 0000000000..d8e562ccec --- /dev/null +++ b/remote/test/puppeteer/src/constants.ts @@ -0,0 +1,7 @@ +import {dirname} from 'path'; +import {puppeteerDirname} from './compat.js'; + +/** + * @internal + */ +export const rootDirname = dirname(dirname(dirname(puppeteerDirname))); diff --git a/remote/test/puppeteer/src/environment.ts b/remote/test/puppeteer/src/environment.ts new file mode 100644 index 0000000000..80258c67fe --- /dev/null +++ b/remote/test/puppeteer/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/src/generated/version.ts b/remote/test/puppeteer/src/generated/version.ts new file mode 100644 index 0000000000..6b5116dfd5 --- /dev/null +++ b/remote/test/puppeteer/src/generated/version.ts @@ -0,0 +1,4 @@ +/** + * @internal + */ +export const packageVersion = '17.1.2'; diff --git a/remote/test/puppeteer/src/initializePuppeteer.ts b/remote/test/puppeteer/src/initializePuppeteer.ts new file mode 100644 index 0000000000..8d1e9a5157 --- /dev/null +++ b/remote/test/puppeteer/src/initializePuppeteer.ts @@ -0,0 +1,46 @@ +/** + * 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 {Product} from './common/Product.js'; +import {rootDirname} from './constants.js'; +import {PuppeteerNode} from './node/Puppeteer.js'; +import {PUPPETEER_REVISIONS} from './revisions.js'; +import {getPackageDirectory} from './util/getPackageDirectory.js'; + +/** + * @internal + */ +export const initializePuppeteer = (packageName: string): PuppeteerNode => { + const isPuppeteerCore = packageName === 'puppeteer-core'; + let preferredRevision = PUPPETEER_REVISIONS.chromium; + // puppeteer-core ignores environment variables + const productName = !isPuppeteerCore + ? ((process.env['PUPPETEER_PRODUCT'] || + process.env['npm_config_puppeteer_product'] || + process.env['npm_package_config_puppeteer_product']) as Product) + : undefined; + + if (!isPuppeteerCore && productName === 'firefox') { + preferredRevision = PUPPETEER_REVISIONS.firefox; + } + + return new PuppeteerNode({ + projectRoot: isPuppeteerCore ? undefined : getPackageDirectory(rootDirname), + preferredRevision, + isPuppeteerCore, + productName, + }); +}; diff --git a/remote/test/puppeteer/src/injected/Poller.ts b/remote/test/puppeteer/src/injected/Poller.ts new file mode 100644 index 0000000000..c6748ebbc4 --- /dev/null +++ b/remote/test/puppeteer/src/injected/Poller.ts @@ -0,0 +1,156 @@ +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; +import {assert} from '../util/assert.js'; + +interface Poller<T> { + start(): Promise<T>; + stop(): Promise<void>; + result(): Promise<T>; +} + +export class MutationPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + + #root: Node; + + #observer?: MutationObserver; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>, root: Node) { + this.#fn = fn; + this.#root = root; + } + + async start(): Promise<T> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return result; + } + + this.#observer = new MutationObserver(async () => { + const result = await this.#fn(); + if (!result) { + return; + } + promise.resolve(result); + await this.stop(); + }); + this.#observer.observe(this.#root, { + childList: true, + subtree: true, + attributes: true, + }); + + return this.#promise; + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + if (this.#observer) { + this.#observer.disconnect(); + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} + +export class RAFPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>) { + this.#fn = fn; + } + + async start(): Promise<T> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return result; + } + + const poll = async () => { + if (promise.finished()) { + return; + } + const result = await this.#fn(); + if (!result) { + window.requestAnimationFrame(poll); + return; + } + promise.resolve(result); + await this.stop(); + }; + window.requestAnimationFrame(poll); + + return this.#promise; + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} + +export class IntervalPoller<T> implements Poller<T> { + #fn: () => Promise<T>; + #ms: number; + + #interval?: NodeJS.Timer; + #promise?: DeferredPromise<T>; + constructor(fn: () => Promise<T>, ms: number) { + this.#fn = fn; + this.#ms = ms; + } + + async start(): Promise<T> { + const promise = (this.#promise = createDeferredPromise<T>()); + const result = await this.#fn(); + if (result) { + promise.resolve(result); + return result; + } + + this.#interval = setInterval(async () => { + const result = await this.#fn(); + if (!result) { + return; + } + promise.resolve(result); + await this.stop(); + }, this.#ms); + + return this.#promise; + } + + async stop(): Promise<void> { + assert(this.#promise, 'Polling never started.'); + if (!this.#promise.finished()) { + this.#promise.reject(new Error('Polling stopped')); + } + if (this.#interval) { + clearInterval(this.#interval); + } + } + + result(): Promise<T> { + assert(this.#promise, 'Polling never started.'); + return this.#promise; + } +} diff --git a/remote/test/puppeteer/src/injected/README.md b/remote/test/puppeteer/src/injected/README.md new file mode 100644 index 0000000000..0a4af11cce --- /dev/null +++ b/remote/test/puppeteer/src/injected/README.md @@ -0,0 +1,5 @@ +# Injected + +This folder contains code that is injected into every Puppeteer execution context. Each file is transpiled using esbuild into a script in `src/generated` which is then imported into server code. + +See `utils/generate_injected.ts` for more information. diff --git a/remote/test/puppeteer/src/injected/injected.ts b/remote/test/puppeteer/src/injected/injected.ts new file mode 100644 index 0000000000..7650a28fd5 --- /dev/null +++ b/remote/test/puppeteer/src/injected/injected.ts @@ -0,0 +1,14 @@ +import {createDeferredPromise} from '../util/DeferredPromise.js'; +import * as Poller from './Poller.js'; +import * as util from './util.js'; + +Object.assign( + self, + Object.freeze({ + InjectedUtil: { + ...Poller, + ...util, + createDeferredPromise, + }, + }) +); diff --git a/remote/test/puppeteer/src/injected/util.ts b/remote/test/puppeteer/src/injected/util.ts new file mode 100644 index 0000000000..79e68e5e0f --- /dev/null +++ b/remote/test/puppeteer/src/injected/util.ts @@ -0,0 +1,18 @@ +const createdFunctions = new Map<string, (...args: unknown[]) => unknown>(); + +/** + * Creates a function from a string. + */ +export const createFunction = ( + functionValue: string +): ((...args: unknown[]) => unknown) => { + let fn = createdFunctions.get(functionValue); + if (fn) { + return fn; + } + fn = new Function(`return ${functionValue}`)() as ( + ...args: unknown[] + ) => unknown; + createdFunctions.set(functionValue, fn); + return fn; +}; diff --git a/remote/test/puppeteer/src/node/BrowserFetcher.ts b/remote/test/puppeteer/src/node/BrowserFetcher.ts new file mode 100644 index 0000000000..7c425c2dbb --- /dev/null +++ b/remote/test/puppeteer/src/node/BrowserFetcher.ts @@ -0,0 +1,701 @@ +/** + * 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 os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import * as childProcess from 'child_process'; +import * as https from 'https'; +import * as http from 'http'; + +import {Product} from '../common/Product.js'; +import extractZip from 'extract-zip'; +import {debug} from '../common/Debug.js'; +import {promisify} from 'util'; +import removeRecursive from 'rimraf'; +import * as URL from 'url'; +import createHttpsProxyAgent, { + HttpsProxyAgent, + HttpsProxyAgentOptions, +} from 'https-proxy-agent'; +import {getProxyForUrl} from 'proxy-from-env'; +import {assert} from '../util/assert.js'; + +import tar from 'tar-fs'; +import bzip from 'unbzip2-stream'; + +const {PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM} = process.env; + +const debugFetcher = debug('puppeteer:fetcher'); + +const downloadURLs: Record<Product, Partial<Record<Platform, string>>> = { + chrome: { + linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', + mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', + mac_arm: '%s/chromium-browser-snapshots/Mac_Arm/%d/%s.zip', + win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip', + win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip', + }, + firefox: { + linux: '%s/firefox-%s.en-US.%s-x86_64.tar.bz2', + mac: '%s/firefox-%s.en-US.%s.dmg', + win32: '%s/firefox-%s.en-US.%s.zip', + win64: '%s/firefox-%s.en-US.%s.zip', + }, +}; + +const browserConfig = { + chrome: { + host: 'https://storage.googleapis.com', + destination: '.local-chromium', + }, + firefox: { + host: 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central', + destination: '.local-firefox', + }, +} as const; + +/** + * Supported platforms. + * + * @public + */ +export type Platform = 'linux' | 'mac' | 'mac_arm' | 'win32' | 'win64'; + +function archiveName( + product: Product, + platform: Platform, + revision: string +): string { + switch (product) { + case 'chrome': + switch (platform) { + case 'linux': + return 'chrome-linux'; + case 'mac_arm': + case 'mac': + return 'chrome-mac'; + case 'win32': + case 'win64': + // Windows archive name changed at r591479. + return parseInt(revision, 10) > 591479 + ? 'chrome-win' + : 'chrome-win32'; + } + case 'firefox': + return platform; + } +} + +function downloadURL( + product: Product, + platform: Platform, + host: string, + revision: string +): string { + const url = util.format( + downloadURLs[product][platform], + host, + revision, + archiveName(product, platform, revision) + ); + return url; +} + +function handleArm64(): void { + let exists = fs.existsSync('/usr/bin/chromium-browser'); + if (exists) { + return; + } + exists = fs.existsSync('/usr/bin/chromium'); + if (exists) { + return; + } + console.error( + 'The chromium binary is not available for arm64.' + + '\nIf you are on Ubuntu, you can install with: ' + + '\n\n sudo apt install chromium\n' + + '\n\n sudo apt install chromium-browser\n' + ); + throw new Error(); +} + +const readdirAsync = promisify(fs.readdir.bind(fs)); +const mkdirAsync = promisify(fs.mkdir.bind(fs)); +const unlinkAsync = promisify(fs.unlink.bind(fs)); +const chmodAsync = promisify(fs.chmod.bind(fs)); + +function existsAsync(filePath: string): Promise<boolean> { + return new Promise(resolve => { + fs.access(filePath, err => { + return resolve(!err); + }); + }); +} + +/** + * @public + */ +export interface BrowserFetcherOptions { + platform?: Platform; + product?: string; + path?: string; + host?: string; +} + +/** + * @public + */ +export interface BrowserFetcherRevisionInfo { + folderPath: string; + executablePath: string; + url: string; + local: boolean; + revision: string; + product: string; +} +/** + * BrowserFetcher can download and manage different versions of Chromium and Firefox. + * + * @remarks + * BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. `"533271"`. Revision strings can be obtained from {@link http://omahaproxy.appspot.com/ | omahaproxy.appspot.com}. + * In the Firefox case, BrowserFetcher downloads Firefox Nightly and + * operates on version numbers such as `"75"`. + * + * @example + * An example of using BrowserFetcher to download a specific version of Chromium + * and running Puppeteer against it: + * + * ```ts + * const browserFetcher = puppeteer.createBrowserFetcher(); + * const revisionInfo = await browserFetcher.download('533271'); + * const browser = await puppeteer.launch({ + * executablePath: revisionInfo.executablePath, + * }); + * ``` + * + * **NOTE** BrowserFetcher is not designed to work concurrently with other + * instances of BrowserFetcher that share the same downloads directory. + * + * @public + */ + +export class BrowserFetcher { + #product: Product; + #downloadsFolder: string; + #downloadHost: string; + #platform: Platform; + + /** + * @internal + */ + constructor(projectRoot: string, options: BrowserFetcherOptions = {}) { + this.#product = (options.product || 'chrome').toLowerCase() as Product; + assert( + this.#product === 'chrome' || this.#product === 'firefox', + `Unknown product: "${options.product}"` + ); + + this.#downloadsFolder = + options.path || + path.join(projectRoot, browserConfig[this.#product].destination); + this.#downloadHost = options.host || browserConfig[this.#product].host; + + if (options.platform) { + this.#platform = options.platform; + } else { + const platform = os.platform(); + switch (platform) { + case 'darwin': + switch (this.#product) { + case 'chrome': + this.#platform = + os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM + ? 'mac_arm' + : 'mac'; + break; + case 'firefox': + this.#platform = 'mac'; + break; + } + break; + case 'linux': + this.#platform = 'linux'; + break; + case 'win32': + this.#platform = os.arch() === 'x64' ? 'win64' : 'win32'; + return; + default: + assert(false, 'Unsupported platform: ' + platform); + } + } + + assert( + downloadURLs[this.#product][this.#platform], + 'Unsupported platform: ' + this.#platform + ); + } + + /** + * @returns Returns the current `Platform`, which is one of `mac`, `linux`, + * `win32` or `win64`. + */ + platform(): Platform { + return this.#platform; + } + + /** + * @returns Returns the current `Product`, which is one of `chrome` or + * `firefox`. + */ + product(): Product { + return this.#product; + } + + /** + * @returns The download host being used. + */ + host(): string { + return this.#downloadHost; + } + + /** + * Initiates a HEAD request to check if the revision is available. + * @remarks + * This method is affected by the current `product`. + * @param revision - The revision to check availability for. + * @returns A promise that resolves to `true` if the revision could be downloaded + * from the host. + */ + canDownload(revision: string): Promise<boolean> { + const url = downloadURL( + this.#product, + this.#platform, + this.#downloadHost, + revision + ); + return new Promise(resolve => { + const request = httpRequest( + url, + 'HEAD', + response => { + resolve(response.statusCode === 200); + }, + false + ); + request.on('error', error => { + console.error(error); + resolve(false); + }); + }); + } + + /** + * Initiates a GET request to download the revision from the host. + * @remarks + * This method is affected by the current `product`. + * @param revision - The revision to download. + * @param progressCallback - A function that will be called with two arguments: + * How many bytes have been downloaded and the total number of bytes of the download. + * @returns A promise with revision information when the revision is downloaded + * and extracted. + */ + async download( + revision: string, + progressCallback: (x: number, y: number) => void = (): void => {} + ): Promise<BrowserFetcherRevisionInfo | undefined> { + const url = downloadURL( + this.#product, + this.#platform, + this.#downloadHost, + revision + ); + const fileName = url.split('/').pop(); + assert(fileName, `A malformed download URL was found: ${url}.`); + const archivePath = path.join(this.#downloadsFolder, fileName); + const outputPath = this.#getFolderPath(revision); + if (await existsAsync(outputPath)) { + return this.revisionInfo(revision); + } + if (!(await existsAsync(this.#downloadsFolder))) { + await mkdirAsync(this.#downloadsFolder); + } + + // Use system Chromium builds on Linux ARM devices + if (os.platform() !== 'darwin' && os.arch() === 'arm64') { + handleArm64(); + return; + } + try { + await _downloadFile(url, archivePath, progressCallback); + await install(archivePath, outputPath); + } finally { + if (await existsAsync(archivePath)) { + await unlinkAsync(archivePath); + } + } + const revisionInfo = this.revisionInfo(revision); + if (revisionInfo) { + await chmodAsync(revisionInfo.executablePath, 0o755); + } + return revisionInfo; + } + + /** + * @remarks + * This method is affected by the current `product`. + * @returns A promise with a list of all revision strings (for the current `product`) + * available locally on disk. + */ + async localRevisions(): Promise<string[]> { + if (!(await existsAsync(this.#downloadsFolder))) { + return []; + } + const fileNames = await readdirAsync(this.#downloadsFolder); + return fileNames + .map(fileName => { + return parseFolderPath(this.#product, fileName); + }) + .filter( + ( + entry + ): entry is {product: string; platform: string; revision: string} => { + return (entry && entry.platform === this.#platform) ?? false; + } + ) + .map(entry => { + return entry.revision; + }); + } + + /** + * @remarks + * This method is affected by the current `product`. + * @param revision - A revision to remove for the current `product`. + * @returns A promise that resolves when the revision has been removes or + * throws if the revision has not been downloaded. + */ + async remove(revision: string): Promise<void> { + const folderPath = this.#getFolderPath(revision); + assert( + await existsAsync(folderPath), + `Failed to remove: revision ${revision} is not downloaded` + ); + await new Promise(fulfill => { + return removeRecursive(folderPath, fulfill); + }); + } + + /** + * @param revision - The revision to get info for. + * @returns The revision info for the given revision. + */ + revisionInfo(revision: string): BrowserFetcherRevisionInfo { + const folderPath = this.#getFolderPath(revision); + let executablePath = ''; + if (this.#product === 'chrome') { + if (this.#platform === 'mac' || this.#platform === 'mac_arm') { + executablePath = path.join( + folderPath, + archiveName(this.#product, this.#platform, revision), + 'Chromium.app', + 'Contents', + 'MacOS', + 'Chromium' + ); + } else if (this.#platform === 'linux') { + executablePath = path.join( + folderPath, + archiveName(this.#product, this.#platform, revision), + 'chrome' + ); + } else if (this.#platform === 'win32' || this.#platform === 'win64') { + executablePath = path.join( + folderPath, + archiveName(this.#product, this.#platform, revision), + 'chrome.exe' + ); + } else { + throw new Error('Unsupported platform: ' + this.#platform); + } + } else if (this.#product === 'firefox') { + if (this.#platform === 'mac' || this.#platform === 'mac_arm') { + executablePath = path.join( + folderPath, + 'Firefox Nightly.app', + 'Contents', + 'MacOS', + 'firefox' + ); + } else if (this.#platform === 'linux') { + executablePath = path.join(folderPath, 'firefox', 'firefox'); + } else if (this.#platform === 'win32' || this.#platform === 'win64') { + executablePath = path.join(folderPath, 'firefox', 'firefox.exe'); + } else { + throw new Error('Unsupported platform: ' + this.#platform); + } + } else { + throw new Error('Unsupported product: ' + this.#product); + } + const url = downloadURL( + this.#product, + this.#platform, + this.#downloadHost, + revision + ); + const local = fs.existsSync(folderPath); + debugFetcher({ + revision, + executablePath, + folderPath, + local, + url, + product: this.#product, + }); + return { + revision, + executablePath, + folderPath, + local, + url, + product: this.#product, + }; + } + + #getFolderPath(revision: string): string { + return path.resolve(this.#downloadsFolder, `${this.#platform}-${revision}`); + } +} + +function parseFolderPath( + product: Product, + folderPath: string +): {product: string; platform: string; revision: string} | undefined { + const name = path.basename(folderPath); + const splits = name.split('-'); + if (splits.length !== 2) { + return; + } + const [platform, revision] = splits; + if (!revision || !platform || !(platform in downloadURLs[product])) { + return; + } + return {product, platform, revision}; +} + +/** + * @internal + */ +function _downloadFile( + url: string, + destinationPath: string, + progressCallback?: (x: number, y: number) => void +): Promise<void> { + debugFetcher(`Downloading binary from ${url}`); + let fulfill: (value: void | PromiseLike<void>) => void; + let reject: (err: Error) => void; + const promise = new Promise<void>((x, y) => { + fulfill = x; + reject = y; + }); + + let downloadedBytes = 0; + let totalBytes = 0; + + 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 = fs.createWriteStream(destinationPath); + file.on('finish', () => { + return fulfill(); + }); + 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); + }); + return promise; + + function onData(chunk: string): void { + downloadedBytes += chunk.length; + progressCallback!(downloadedBytes, totalBytes); + } +} + +function install(archivePath: string, folderPath: string): Promise<unknown> { + debugFetcher(`Installing ${archivePath} to ${folderPath}`); + if (archivePath.endsWith('.zip')) { + return extractZip(archivePath, {dir: folderPath}); + } else if (archivePath.endsWith('.tar.bz2')) { + return _extractTar(archivePath, folderPath); + } else if (archivePath.endsWith('.dmg')) { + return mkdirAsync(folderPath).then(() => { + return _installDMG(archivePath, folderPath); + }); + } else { + throw new Error(`Unsupported archive format: ${archivePath}`); + } +} + +/** + * @internal + */ +function _extractTar(tarPath: string, folderPath: string): Promise<unknown> { + return new Promise((fulfill, reject) => { + const tarStream = tar.extract(folderPath); + tarStream.on('error', reject); + tarStream.on('finish', fulfill); + const readStream = fs.createReadStream(tarPath); + readStream.pipe(bzip()).pipe(tarStream); + }); +} + +/** + * @internal + */ +function _installDMG(dmgPath: string, folderPath: string): Promise<void> { + let mountPath: string | undefined; + + return new Promise<void>((fulfill, reject): void => { + const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`; + childProcess.exec(mountCommand, (err, stdout) => { + if (err) { + return reject(err); + } + const volumes = stdout.match(/\/Volumes\/(.*)/m); + if (!volumes) { + return reject(new Error(`Could not find volume path in ${stdout}`)); + } + mountPath = volumes[0]!; + readdirAsync(mountPath) + .then(fileNames => { + const appName = fileNames.find(item => { + return typeof item === 'string' && item.endsWith('.app'); + }); + if (!appName) { + return reject(new Error(`Cannot find app in ${mountPath}`)); + } + const copyPath = path.join(mountPath!, appName); + debugFetcher(`Copying ${copyPath} to ${folderPath}`); + childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, err => { + if (err) { + reject(err); + } else { + fulfill(); + } + }); + }) + .catch(reject); + }); + }) + .catch(error => { + console.error(error); + }) + .finally((): void => { + if (!mountPath) { + return; + } + const unmountCommand = `hdiutil detach "${mountPath}" -quiet`; + debugFetcher(`Unmounting ${mountPath}`); + childProcess.exec(unmountCommand, err => { + if (err) { + console.error(`Error unmounting dmg: ${err}`); + } + }); + }); +} + +function httpRequest( + url: string, + method: string, + response: (x: http.IncomingMessage) => void, + keepAlive = true +): http.ClientRequest { + const urlParsed = URL.parse(url); + + type Options = Partial<URL.UrlWithStringQuery> & { + method?: string; + agent?: HttpsProxyAgent; + rejectUnauthorized?: boolean; + headers?: http.OutgoingHttpHeaders | undefined; + }; + + let options: Options = { + ...urlParsed, + method, + headers: keepAlive + ? { + Connection: 'keep-alive', + } + : undefined, + }; + + const proxyURL = getProxyForUrl(url); + if (proxyURL) { + if (url.startsWith('http:')) { + const proxy = URL.parse(proxyURL); + options = { + path: options.href, + host: proxy.hostname, + port: proxy.port, + }; + } else { + const parsedProxyURL = URL.parse(proxyURL); + + const proxyOptions = { + ...parsedProxyURL, + secureProxy: parsedProxyURL.protocol === 'https:', + } as HttpsProxyAgentOptions; + + options.agent = createHttpsProxyAgent(proxyOptions); + options.rejectUnauthorized = false; + } + } + + const requestCallback = (res: http.IncomingMessage): void => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + httpRequest(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; +} diff --git a/remote/test/puppeteer/src/node/BrowserRunner.ts b/remote/test/puppeteer/src/node/BrowserRunner.ts new file mode 100644 index 0000000000..c18350a6da --- /dev/null +++ b/remote/test/puppeteer/src/node/BrowserRunner.ts @@ -0,0 +1,359 @@ +/** + * 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 * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as readline from 'readline'; +import removeFolder from 'rimraf'; +import {promisify} from 'util'; +import {assert} from '../util/assert.js'; +import {Connection} from '../common/Connection.js'; +import {debug} from '../common/Debug.js'; +import {TimeoutError} from '../common/Errors.js'; +import { + debugError, + addEventListener, + PuppeteerEventListener, + removeEventListeners, +} from '../common/util.js'; +import {isErrnoException, isErrorLike} from '../util/ErrorLike.js'; +import {Product} from '../common/Product.js'; +import {NodeWebSocketTransport as WebSocketTransport} from '../node/NodeWebSocketTransport.js'; +import {LaunchOptions} from './LaunchOptions.js'; +import {PipeTransport} from './PipeTransport.js'; + +const removeFolderAsync = promisify(removeFolder); +const renameAsync = promisify(fs.rename); +const unlinkAsync = promisify(fs.unlink); + +const debugLauncher = debug('puppeteer:launcher'); + +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 + */ +export class BrowserRunner { + #product: Product; + #executablePath: string; + #processArguments: string[]; + #userDataDir: string; + #isTempUserDataDir?: boolean; + #closed = true; + #listeners: PuppeteerEventListener[] = []; + #processClosing!: Promise<void>; + + proc?: childProcess.ChildProcess; + connection?: Connection; + + constructor( + product: Product, + executablePath: string, + processArguments: string[], + userDataDir: string, + isTempUserDataDir?: boolean + ) { + this.#product = product; + this.#executablePath = executablePath; + this.#processArguments = processArguments; + this.#userDataDir = userDataDir; + this.#isTempUserDataDir = isTempUserDataDir; + } + + start(options: LaunchOptions): void { + const {handleSIGINT, handleSIGTERM, handleSIGHUP, dumpio, env, pipe} = + options; + let stdio: Array<'ignore' | 'pipe'>; + if (pipe) { + if (dumpio) { + stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']; + } else { + stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe']; + } + } else { + if (dumpio) { + stdio = ['pipe', 'pipe', 'pipe']; + } else { + stdio = ['pipe', 'ignore', 'pipe']; + } + } + assert(!this.proc, 'This process has previously been started.'); + debugLauncher( + `Calling ${this.#executablePath} ${this.#processArguments.join(' ')}` + ); + this.proc = childProcess.spawn( + this.#executablePath, + this.#processArguments, + { + // On non-windows platforms, `detached: true` makes child process a + // leader of a new process group, making it possible to kill child + // process tree with `.kill(-pid)` command. @see + // https://nodejs.org/api/child_process.html#child_process_options_detached + detached: process.platform !== 'win32', + env, + stdio, + } + ); + if (dumpio) { + this.proc.stderr?.pipe(process.stderr); + this.proc.stdout?.pipe(process.stdout); + } + this.#closed = false; + this.#processClosing = new Promise((fulfill, reject) => { + this.proc!.once('exit', async () => { + this.#closed = true; + // Cleanup as processes exit. + if (this.#isTempUserDataDir) { + try { + await removeFolderAsync(this.#userDataDir); + fulfill(); + } catch (error) { + debugError(error); + reject(error); + } + } else { + if (this.#product === 'firefox') { + try { + // When an existing user profile has been used remove the user + // preferences file and restore possibly backuped preferences. + await unlinkAsync(path.join(this.#userDataDir, 'user.js')); + + const prefsBackupPath = path.join( + this.#userDataDir, + 'prefs.js.puppeteer' + ); + if (fs.existsSync(prefsBackupPath)) { + const prefsPath = path.join(this.#userDataDir, 'prefs.js'); + await unlinkAsync(prefsPath); + await renameAsync(prefsBackupPath, prefsPath); + } + } catch (error) { + debugError(error); + reject(error); + } + } + + fulfill(); + } + }); + }); + this.#listeners = [addEventListener(process, 'exit', this.kill.bind(this))]; + if (handleSIGINT) { + this.#listeners.push( + addEventListener(process, 'SIGINT', () => { + this.kill(); + process.exit(130); + }) + ); + } + if (handleSIGTERM) { + this.#listeners.push( + addEventListener(process, 'SIGTERM', this.close.bind(this)) + ); + } + if (handleSIGHUP) { + this.#listeners.push( + addEventListener(process, 'SIGHUP', this.close.bind(this)) + ); + } + } + + close(): Promise<void> { + if (this.#closed) { + return Promise.resolve(); + } + if (this.#isTempUserDataDir) { + this.kill(); + } else if (this.connection) { + // Attempt to close the browser gracefully + this.connection.send('Browser.close').catch(error => { + debugError(error); + this.kill(); + }); + } + // Cleanup this listener last, as that makes sure the full callback runs. If we + // perform this earlier, then the previous function calls would not happen. + removeEventListeners(this.#listeners); + return this.#processClosing; + } + + kill(): void { + // If the process failed to launch (for example if the browser executable path + // is invalid), then the process does not get a pid assigned. A call to + // `proc.kill` would error, as the `pid` to-be-killed can not be found. + if (this.proc && this.proc.pid && pidExists(this.proc.pid)) { + const proc = this.proc; + try { + if (process.platform === 'win32') { + childProcess.exec(`taskkill /pid ${this.proc.pid} /T /F`, error => { + if (error) { + // taskkill can fail to kill the process e.g. due to missing permissions. + // Let's kill the process via Node API. This delays killing of all child + // processes of `this.proc` until the main Node.js process dies. + proc.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.proc.pid; + + try { + process.kill(processGroupId, 'SIGKILL'); + } catch (error) { + // Killing the process group can fail due e.g. to missing permissions. + // Let's kill the process via Node API. This delays killing of all child + // processes of `this.proc` until the main Node.js process dies. + proc.kill('SIGKILL'); + } + } + } catch (error) { + throw new Error( + `${PROCESS_ERROR_EXPLANATION}\nError cause: ${ + isErrorLike(error) ? error.stack : error + }` + ); + } + } + + // Attempt to remove temporary profile directory to avoid littering. + try { + if (this.#isTempUserDataDir) { + removeFolder.sync(this.#userDataDir); + } + } catch (error) {} + + // Cleanup this listener last, as that makes sure the full callback runs. If we + // perform this earlier, then the previous function calls would not happen. + removeEventListeners(this.#listeners); + } + + async setupConnection(options: { + usePipe?: boolean; + timeout: number; + slowMo: number; + preferredRevision: string; + }): Promise<Connection> { + assert(this.proc, 'BrowserRunner not started.'); + + const {usePipe, timeout, slowMo, preferredRevision} = options; + if (!usePipe) { + const browserWSEndpoint = await waitForWSEndpoint( + this.proc, + timeout, + preferredRevision + ); + const transport = await WebSocketTransport.create(browserWSEndpoint); + this.connection = new Connection(browserWSEndpoint, transport, slowMo); + } else { + // stdio was assigned during start(), and the 'pipe' option there adds the + // 4th and 5th items to stdio array + const {3: pipeWrite, 4: pipeRead} = this.proc.stdio; + const transport = new PipeTransport( + pipeWrite as NodeJS.WritableStream, + pipeRead as NodeJS.ReadableStream + ); + this.connection = new Connection('', transport, slowMo); + } + return this.connection; + } +} + +function waitForWSEndpoint( + browserProcess: childProcess.ChildProcess, + timeout: number, + preferredRevision: string +): Promise<string> { + assert(browserProcess.stderr, '`browserProcess` does not have stderr.'); + const rl = readline.createInterface(browserProcess.stderr); + let stderr = ''; + + return new Promise((resolve, reject) => { + const listeners = [ + addEventListener(rl, 'line', onLine), + addEventListener(rl, 'close', () => { + return onClose(); + }), + addEventListener(browserProcess, 'exit', () => { + return onClose(); + }), + addEventListener(browserProcess, 'error', error => { + return onClose(error); + }), + ]; + const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0; + + function onClose(error?: Error): void { + cleanup(); + reject( + new Error( + [ + 'Failed to launch the browser process!' + + (error ? ' ' + error.message : ''), + stderr, + '', + 'TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md', + '', + ].join('\n') + ) + ); + } + + function onTimeout(): void { + cleanup(); + reject( + new TimeoutError( + `Timed out after ${timeout} ms while trying to connect to the browser! Only Chrome at revision r${preferredRevision} is guaranteed to work.` + ) + ); + } + + function onLine(line: string): void { + stderr += line + '\n'; + const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); + if (!match) { + return; + } + cleanup(); + // The RegExp matches, so this will obviously exist. + resolve(match[1]!); + } + + function cleanup(): void { + if (timeoutId) { + clearTimeout(timeoutId); + } + removeEventListeners(listeners); + } + }); +} + +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; + } +} diff --git a/remote/test/puppeteer/src/node/ChromeLauncher.ts b/remote/test/puppeteer/src/node/ChromeLauncher.ts new file mode 100644 index 0000000000..ab07b56f6e --- /dev/null +++ b/remote/test/puppeteer/src/node/ChromeLauncher.ts @@ -0,0 +1,265 @@ +import fs from 'fs'; +import path from 'path'; +import {assert} from '../util/assert.js'; +import {Browser} from '../common/Browser.js'; +import {Product} from '../common/Product.js'; +import {BrowserRunner} from './BrowserRunner.js'; +import { + BrowserLaunchArgumentOptions, + ChromeReleaseChannel, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; +import { + executablePathForChannel, + ProductLauncher, + resolveExecutablePath, +} from './ProductLauncher.js'; +import {tmpdir} from './util.js'; + +/** + * @internal + */ +export class ChromeLauncher implements ProductLauncher { + /** + * @internal + */ + _projectRoot: string | undefined; + /** + * @internal + */ + _preferredRevision: string; + /** + * @internal + */ + _isPuppeteerCore: boolean; + + constructor( + projectRoot: string | undefined, + preferredRevision: string, + isPuppeteerCore: boolean + ) { + this._projectRoot = projectRoot; + this._preferredRevision = preferredRevision; + this._isPuppeteerCore = isPuppeteerCore; + } + + async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + channel, + executablePath, + pipe = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + slowMo = 0, + timeout = 30000, + waitForInitialPage = true, + debuggingPort, + } = 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 fs.promises.mkdtemp( + path.join(tmpdir(), 'puppeteer_dev_chrome_profile-') + )}` + ); + userDataDirIndex = chromeArguments.length - 1; + } + + const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1]; + assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed'); + + let chromeExecutable = executablePath; + if (channel) { + // executablePath is detected by channel, so it should not be specified by user. + assert( + !chromeExecutable, + '`executablePath` must not be specified when `channel` is given.' + ); + + chromeExecutable = executablePathForChannel(channel); + } else if (!chromeExecutable) { + const {missingText, executablePath} = resolveExecutablePath(this); + if (missingText) { + throw new Error(missingText); + } + chromeExecutable = executablePath; + } + + const usePipe = chromeArguments.includes('--remote-debugging-pipe'); + const runner = new BrowserRunner( + this.product, + chromeExecutable, + chromeArguments, + userDataDir, + isTempUserDataDir + ); + runner.start({ + handleSIGHUP, + handleSIGTERM, + handleSIGINT, + dumpio, + env, + pipe: usePipe, + }); + + let browser; + try { + const connection = await runner.setupConnection({ + usePipe, + timeout, + slowMo, + preferredRevision: this._preferredRevision, + }); + browser = await Browser._create( + this.product, + connection, + [], + ignoreHTTPSErrors, + defaultViewport, + runner.proc, + runner.close.bind(runner), + options.targetFilter + ); + } catch (error) { + runner.kill(); + throw error; + } + + if (waitForInitialPage) { + try { + await browser.waitForTarget( + t => { + return t.type() === 'page'; + }, + {timeout} + ); + } catch (error) { + await browser.close(); + throw error; + } + } + + return browser; + } + + defaultArgs(options: BrowserLaunchArgumentOptions = {}): string[] { + const chromeArguments = [ + '--allow-pre-commit-input', + '--disable-background-networking', + '--enable-features=NetworkServiceInProcess2', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + // TODO: remove AvoidUnnecessaryBeforeUnloadCheckSync below + // once crbug.com/1324138 is fixed and released. + // AcceptCHFrame disabled because of crbug.com/1348106. + '--disable-features=Translate,BackForwardCache,AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--force-color-profile=srgb', + '--metrics-recording-only', + '--no-first-run', + '--enable-automation', + '--password-store=basic', + '--use-mock-keychain', + // TODO(sadym): remove '--enable-blink-features=IdleDetection' + // once IdleDetection is turned on by default. + '--enable-blink-features=IdleDetection', + '--export-tagged-pdf', + ]; + 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 === 'chrome' ? '--headless=chrome' : '--headless', + '--hide-scrollbars', + '--mute-audio' + ); + } + if ( + args.every(arg => { + return arg.startsWith('-'); + }) + ) { + chromeArguments.push('about:blank'); + } + chromeArguments.push(...args); + return chromeArguments; + } + + executablePath(channel?: ChromeReleaseChannel): string { + if (channel) { + return executablePathForChannel(channel); + } else { + const results = resolveExecutablePath(this); + return results.executablePath; + } + } + + get product(): Product { + return 'chrome'; + } +} diff --git a/remote/test/puppeteer/src/node/FirefoxLauncher.ts b/remote/test/puppeteer/src/node/FirefoxLauncher.ts new file mode 100644 index 0000000000..c27f1b8d8b --- /dev/null +++ b/remote/test/puppeteer/src/node/FirefoxLauncher.ts @@ -0,0 +1,502 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {assert} from '../util/assert.js'; +import {Browser} from '../common/Browser.js'; +import {Product} from '../common/Product.js'; +import {BrowserFetcher} from './BrowserFetcher.js'; +import {BrowserRunner} from './BrowserRunner.js'; +import { + BrowserLaunchArgumentOptions, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; +import {ProductLauncher, resolveExecutablePath} from './ProductLauncher.js'; +import {tmpdir} from './util.js'; + +/** + * @internal + */ +export class FirefoxLauncher implements ProductLauncher { + /** + * @internal + */ + _projectRoot: string | undefined; + /** + * @internal + */ + _preferredRevision: string; + /** + * @internal + */ + _isPuppeteerCore: boolean; + + constructor( + projectRoot: string | undefined, + preferredRevision: string, + isPuppeteerCore: boolean + ) { + this._projectRoot = projectRoot; + this._preferredRevision = preferredRevision; + this._isPuppeteerCore = isPuppeteerCore; + } + + async launch(options: PuppeteerNodeLaunchOptions = {}): Promise<Browser> { + const { + ignoreDefaultArgs = false, + args = [], + dumpio = false, + executablePath = null, + pipe = false, + env = process.env, + handleSIGINT = true, + handleSIGTERM = true, + handleSIGHUP = true, + ignoreHTTPSErrors = false, + defaultViewport = {width: 800, height: 600}, + slowMo = 0, + timeout = 30000, + extraPrefsFirefox = {}, + waitForInitialPage = true, + 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; + const prefs = this.defaultPreferences(extraPrefsFirefox); + this.writePreferences(prefs, userDataDir); + } else { + userDataDir = await this._createProfile(extraPrefsFirefox); + firefoxArguments.push('--profile'); + firefoxArguments.push(userDataDir); + } + + await this._updateRevision(); + let firefoxExecutable = executablePath; + if (!executablePath) { + const {missingText, executablePath} = resolveExecutablePath(this); + if (missingText) { + throw new Error(missingText); + } + firefoxExecutable = executablePath; + } + + if (!firefoxExecutable) { + throw new Error('firefoxExecutable is not found.'); + } + + const runner = new BrowserRunner( + this.product, + firefoxExecutable, + firefoxArguments, + userDataDir, + isTempUserDataDir + ); + runner.start({ + handleSIGHUP, + handleSIGTERM, + handleSIGINT, + dumpio, + env, + pipe, + }); + + let browser; + try { + const connection = await runner.setupConnection({ + usePipe: pipe, + timeout, + slowMo, + preferredRevision: this._preferredRevision, + }); + browser = await Browser._create( + this.product, + connection, + [], + ignoreHTTPSErrors, + defaultViewport, + runner.proc, + runner.close.bind(runner), + options.targetFilter + ); + } catch (error) { + runner.kill(); + throw error; + } + + if (waitForInitialPage) { + try { + await browser.waitForTarget( + t => { + return t.type() === 'page'; + }, + {timeout} + ); + } catch (error) { + await browser.close(); + throw error; + } + } + + return browser; + } + + executablePath(): string { + return resolveExecutablePath(this).executablePath; + } + + async _updateRevision(): Promise<void> { + // replace 'latest' placeholder with actual downloaded revision + if (this._preferredRevision === 'latest') { + if (!this._projectRoot) { + throw new Error( + '_projectRoot is undefined. Unable to create a BrowserFetcher.' + ); + } + const browserFetcher = new BrowserFetcher(this._projectRoot, { + product: this.product, + }); + const localRevisions = await browserFetcher.localRevisions(); + if (localRevisions[0]) { + this._preferredRevision = localRevisions[0]; + } + } + } + + get product(): Product { + return 'firefox'; + } + + 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; + } + + defaultPreferences(extraPrefs: {[x: string]: unknown}): { + [x: 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 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 writePreferences( + prefs: {[x: string]: unknown}, + profilePath: string + ): Promise<void> { + const lines = Object.entries(prefs).map(([key, value]) => { + return `user_pref(${JSON.stringify(key)}, ${JSON.stringify(value)});`; + }); + + await fs.promises.writeFile( + path.join(profilePath, 'user.js'), + lines.join('\n') + ); + + // Create a backup of the preferences file if it already exitsts. + const prefsPath = path.join(profilePath, 'prefs.js'); + if (fs.existsSync(prefsPath)) { + const prefsBackupPath = path.join(profilePath, 'prefs.js.puppeteer'); + await fs.promises.copyFile(prefsPath, prefsBackupPath); + } + } + + async _createProfile(extraPrefs: {[x: string]: unknown}): Promise<string> { + const temporaryProfilePath = await fs.promises.mkdtemp( + path.join(tmpdir(), 'puppeteer_dev_firefox_profile-') + ); + + const prefs = this.defaultPreferences(extraPrefs); + await this.writePreferences(prefs, temporaryProfilePath); + + return temporaryProfilePath; + } +} diff --git a/remote/test/puppeteer/src/node/LaunchOptions.ts b/remote/test/puppeteer/src/node/LaunchOptions.ts new file mode 100644 index 0000000000..ab10642d1b --- /dev/null +++ b/remote/test/puppeteer/src/node/LaunchOptions.ts @@ -0,0 +1,144 @@ +/** + * 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. + * @defaultValue true + */ + headless?: boolean | 'chrome'; + /** + * 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; + /** + * + */ + 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 30000 (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/src/node/NodeWebSocketTransport.ts b/remote/test/puppeteer/src/node/NodeWebSocketTransport.ts new file mode 100644 index 0000000000..fbe8e7c92c --- /dev/null +++ b/remote/test/puppeteer/src/node/NodeWebSocketTransport.ts @@ -0,0 +1,69 @@ +/** + * 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): 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}`, + }, + }); + + 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/src/node/PipeTransport.ts b/remote/test/puppeteer/src/node/PipeTransport.ts new file mode 100644 index 0000000000..3b7b042b0e --- /dev/null +++ b/remote/test/puppeteer/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 {assert} from '../util/assert.js'; +import {ConnectionTransport} from '../common/ConnectionTransport.js'; +import { + addEventListener, + debugError, + PuppeteerEventListener, + removeEventListeners, +} from '../common/util.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/src/node/ProductLauncher.ts b/remote/test/puppeteer/src/node/ProductLauncher.ts new file mode 100644 index 0000000000..a1d7c84f46 --- /dev/null +++ b/remote/test/puppeteer/src/node/ProductLauncher.ts @@ -0,0 +1,217 @@ +/** + * 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 os from 'os'; + +import {Browser} from '../common/Browser.js'; +import {BrowserFetcher} from './BrowserFetcher.js'; + +import { + BrowserLaunchArgumentOptions, + ChromeReleaseChannel, + PuppeteerNodeLaunchOptions, +} from './LaunchOptions.js'; + +import {Product} from '../common/Product.js'; +import {ChromeLauncher} from './ChromeLauncher.js'; +import {FirefoxLauncher} from './FirefoxLauncher.js'; +import {accessSync, existsSync} from 'fs'; + +/** + * Describes a launcher - a class that is able to create and launch a browser instance. + * @public + */ +export interface ProductLauncher { + launch(object: PuppeteerNodeLaunchOptions): Promise<Browser>; + executablePath: (path?: any) => string; + defaultArgs(object: BrowserLaunchArgumentOptions): string[]; + product: Product; +} + +/** + * @internal + */ +export function executablePathForChannel( + channel: ChromeReleaseChannel +): string { + const platform = os.platform(); + + let chromePath: string | undefined; + switch (platform) { + case 'win32': + switch (channel) { + case 'chrome': + chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`; + break; + case 'chrome-beta': + chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`; + break; + case 'chrome-canary': + chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`; + break; + case 'chrome-dev': + chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`; + break; + } + break; + case 'darwin': + switch (channel) { + case 'chrome': + chromePath = + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + break; + case 'chrome-beta': + chromePath = + '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta'; + break; + case 'chrome-canary': + chromePath = + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'; + break; + case 'chrome-dev': + chromePath = + '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev'; + break; + } + break; + case 'linux': + switch (channel) { + case 'chrome': + chromePath = '/opt/google/chrome/chrome'; + break; + case 'chrome-beta': + chromePath = '/opt/google/chrome-beta/chrome'; + break; + case 'chrome-dev': + chromePath = '/opt/google/chrome-unstable/chrome'; + break; + } + break; + } + + if (!chromePath) { + throw new Error( + `Unable to detect browser executable path for '${channel}' on ${platform}.` + ); + } + + // Check if Chrome exists and is accessible. + try { + accessSync(chromePath); + } catch (error) { + throw new Error( + `Could not find Google Chrome executable for channel '${channel}' at '${chromePath}'.` + ); + } + + return chromePath; +} + +/** + * @internal + */ +export function resolveExecutablePath( + launcher: ChromeLauncher | FirefoxLauncher +): { + executablePath: string; + missingText?: string; +} { + const {product, _isPuppeteerCore, _projectRoot, _preferredRevision} = + launcher; + let downloadPath: string | undefined; + // puppeteer-core doesn't take into account PUPPETEER_* env variables. + if (!_isPuppeteerCore) { + const executablePath = + process.env['PUPPETEER_EXECUTABLE_PATH'] || + process.env['npm_config_puppeteer_executable_path'] || + process.env['npm_package_config_puppeteer_executable_path']; + if (executablePath) { + const missingText = !existsSync(executablePath) + ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + + executablePath + : undefined; + return {executablePath, missingText}; + } + const ubuntuChromiumPath = '/usr/bin/chromium-browser'; + if ( + product === 'chrome' && + os.platform() !== 'darwin' && + os.arch() === 'arm64' && + existsSync(ubuntuChromiumPath) + ) { + return {executablePath: ubuntuChromiumPath, missingText: undefined}; + } + downloadPath = + process.env['PUPPETEER_DOWNLOAD_PATH'] || + process.env['npm_config_puppeteer_download_path'] || + process.env['npm_package_config_puppeteer_download_path']; + } + if (!_projectRoot) { + throw new Error( + '_projectRoot is undefined. Unable to create a BrowserFetcher.' + ); + } + const browserFetcher = new BrowserFetcher(_projectRoot, { + product: product, + path: downloadPath, + }); + + if (!_isPuppeteerCore && product === 'chrome') { + const revision = process.env['PUPPETEER_CHROMIUM_REVISION']; + if (revision) { + const revisionInfo = browserFetcher.revisionInfo(revision); + const missingText = !revisionInfo.local + ? 'Tried to use PUPPETEER_CHROMIUM_REVISION env variable to launch browser but did not find executable at: ' + + revisionInfo.executablePath + : undefined; + return {executablePath: revisionInfo.executablePath, missingText}; + } + } + const revisionInfo = browserFetcher.revisionInfo(_preferredRevision); + + const firefoxHelp = `Run \`PUPPETEER_PRODUCT=firefox npm install\` to download a supported Firefox browser binary.`; + const chromeHelp = `Run \`npm install\` to download the correct Chromium revision (${launcher._preferredRevision}).`; + const missingText = !revisionInfo.local + ? `Could not find expected browser (${product}) locally. ${ + product === 'chrome' ? chromeHelp : firefoxHelp + }` + : undefined; + return {executablePath: revisionInfo.executablePath, missingText}; +} + +/** + * @internal + */ +export function createLauncher( + projectRoot: string | undefined, + preferredRevision: string, + isPuppeteerCore: boolean, + product: Product = 'chrome' +): ProductLauncher { + switch (product) { + case 'firefox': + return new FirefoxLauncher( + projectRoot, + preferredRevision, + isPuppeteerCore + ); + case 'chrome': + return new ChromeLauncher( + projectRoot, + preferredRevision, + isPuppeteerCore + ); + } +} diff --git a/remote/test/puppeteer/src/node/Puppeteer.ts b/remote/test/puppeteer/src/node/Puppeteer.ts new file mode 100644 index 0000000000..374b861648 --- /dev/null +++ b/remote/test/puppeteer/src/node/Puppeteer.ts @@ -0,0 +1,244 @@ +/** + * 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 { + Puppeteer, + CommonPuppeteerSettings, + ConnectOptions, +} from '../common/Puppeteer.js'; +import {BrowserFetcher, BrowserFetcherOptions} from './BrowserFetcher.js'; +import {LaunchOptions, BrowserLaunchArgumentOptions} from './LaunchOptions.js'; +import {BrowserConnectOptions} from '../common/BrowserConnector.js'; +import {Browser} from '../common/Browser.js'; +import {createLauncher, ProductLauncher} from './ProductLauncher.js'; +import {PUPPETEER_REVISIONS} from '../revisions.js'; +import {Product} from '../common/Product.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 + * const puppeteer = require('puppeteer'); + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://www.google.com'); + * // other actions... + * await browser.close(); + * })(); + * ``` + * + * 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; + #projectRoot?: string; + #productName?: Product; + + _preferredRevision: string; + + /** + * @internal + */ + constructor( + settings: { + projectRoot?: string; + preferredRevision: string; + productName?: Product; + } & CommonPuppeteerSettings + ) { + const {projectRoot, preferredRevision, productName, ...commonSettings} = + settings; + super(commonSettings); + this.#projectRoot = projectRoot; + this.#productName = productName; + this._preferredRevision = preferredRevision; + + 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.createBrowserFetcher = this.createBrowserFetcher.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); + } + + /** + * @internal + */ + get _productName(): Product | undefined { + return this.#productName; + } + set _productName(name: Product | undefined) { + if (this.#productName !== name) { + this._changedProduct = true; + } + this.#productName = name; + } + + /** + * Launches puppeteer and launches a browser instance with given arguments and + * options when specified. + * + * @example + * You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: + * + * ```ts + * const browser = await puppeteer.launch({ + * ignoreDefaultArgs: ['--mute-audio'], + * }); + * ``` + * + * @remarks + * **NOTE** Puppeteer can also be used to control the Chrome browser, but it + * works best with the version of Chromium it is bundled with. There is no + * guarantee it will work with any other version. Use `executablePath` option + * with extreme caution. If Google Chrome (rather than Chromium) 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. In `puppeteer.launch([options])`, any mention of + * Chromium also applies to Chrome. 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. + * + * @param options - Set of configurable options to set on the browser. + * @returns Promise which resolves to browser instance. + */ + launch(options: PuppeteerLaunchOptions = {}): Promise<Browser> { + if (options.product) { + this._productName = options.product; + } + return this._launcher.launch(options); + } + + /** + * @remarks + * **NOTE** `puppeteer.executablePath()` is affected by the + * `PUPPETEER_EXECUTABLE_PATH` and `PUPPETEER_CHROMIUM_REVISION` environment + * variables. + * + * @returns A path where Puppeteer expects to find the bundled browser. The + * browser binary might not be there if the download was skipped with the + * `PUPPETEER_SKIP_DOWNLOAD` environment variable. + */ + executablePath(channel?: string): string { + return this._launcher.executablePath(channel); + } + + /** + * @internal + */ + get _launcher(): ProductLauncher { + if ( + !this.#launcher || + this.#launcher.product !== this._productName || + this._changedProduct + ) { + switch (this._productName) { + case 'firefox': + this._preferredRevision = PUPPETEER_REVISIONS.firefox; + break; + case 'chrome': + default: + this._preferredRevision = PUPPETEER_REVISIONS.chromium; + } + this._changedProduct = false; + this.#launcher = createLauncher( + this.#projectRoot, + this._preferredRevision, + this._isPuppeteerCore, + this._productName + ); + } + return this.#launcher; + } + + /** + * The name of the browser that is under automation (`"chrome"` or + * `"firefox"`) + * + * @remarks + * The product is set by the `PUPPETEER_PRODUCT` environment variable or the + * `product` option in `puppeteer.launch([options])` and defaults to `chrome`. + * Firefox support is experimental. + */ + 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); + } + + /** + * @param options - Set of configurable options to specify the settings of the + * BrowserFetcher. + * @returns A new BrowserFetcher instance. + */ + createBrowserFetcher(options: BrowserFetcherOptions): BrowserFetcher { + if (!this.#projectRoot) { + throw new Error( + '_projectRoot is undefined. Unable to create a BrowserFetcher.' + ); + } + return new BrowserFetcher(this.#projectRoot, options); + } +} diff --git a/remote/test/puppeteer/src/node/install.ts b/remote/test/puppeteer/src/node/install.ts new file mode 100644 index 0000000000..9480a33470 --- /dev/null +++ b/remote/test/puppeteer/src/node/install.ts @@ -0,0 +1,232 @@ +/** + * 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 https, {RequestOptions} from 'https'; +import ProgressBar from 'progress'; +import URL from 'url'; +import puppeteer from '../puppeteer.js'; +import {PUPPETEER_REVISIONS} from '../revisions.js'; +import {PuppeteerNode} from './Puppeteer.js'; +import createHttpsProxyAgent, {HttpsProxyAgentOptions} from 'https-proxy-agent'; +import {getProxyForUrl} from 'proxy-from-env'; + +/** + * @internal + */ +const supportedProducts = { + chrome: 'Chromium', + firefox: 'Firefox Nightly', +} as const; + +/** + * @internal + */ +function getProduct(input: string): 'chrome' | 'firefox' { + if (input !== 'chrome' && input !== 'firefox') { + throw new Error(`Unsupported product ${input}`); + } + return input; +} + +/** + * @internal + */ +export async function downloadBrowser(): Promise<void> { + const downloadHost = + process.env['PUPPETEER_DOWNLOAD_HOST'] || + process.env['npm_config_puppeteer_download_host'] || + process.env['npm_package_config_puppeteer_download_host']; + const product = getProduct( + process.env['PUPPETEER_PRODUCT'] || + process.env['npm_config_puppeteer_product'] || + process.env['npm_package_config_puppeteer_product'] || + 'chrome' + ); + const downloadPath = + process.env['PUPPETEER_DOWNLOAD_PATH'] || + process.env['npm_config_puppeteer_download_path'] || + process.env['npm_package_config_puppeteer_download_path']; + const browserFetcher = (puppeteer as PuppeteerNode).createBrowserFetcher({ + product, + host: downloadHost, + path: downloadPath, + }); + const revision = await getRevision(); + await fetchBinary(revision); + + async function getRevision(): Promise<string> { + if (product === 'chrome') { + return ( + process.env['PUPPETEER_CHROMIUM_REVISION'] || + process.env['npm_config_puppeteer_chromium_revision'] || + PUPPETEER_REVISIONS.chromium + ); + } else if (product === 'firefox') { + (puppeteer as PuppeteerNode)._preferredRevision = + PUPPETEER_REVISIONS.firefox; + return getFirefoxNightlyVersion().catch(error => { + console.error(error); + process.exit(1); + }); + } else { + throw new Error(`Unsupported product ${product}`); + } + } + + function fetchBinary(revision: string) { + const revisionInfo = browserFetcher.revisionInfo(revision); + + // Do nothing if the revision is already downloaded. + if (revisionInfo.local) { + logPolitely( + `${supportedProducts[product]} is already in ${revisionInfo.folderPath}; skipping download.` + ); + return; + } + + // 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; + } + + function onSuccess(localRevisions: string[]): void { + logPolitely( + `${supportedProducts[product]} (${revisionInfo.revision}) downloaded to ${revisionInfo.folderPath}` + ); + localRevisions = localRevisions.filter(revision => { + return revision !== revisionInfo.revision; + }); + const cleanupOldVersions = localRevisions.map(revision => { + return browserFetcher.remove(revision); + }); + Promise.all([...cleanupOldVersions]); + } + + function onError(error: Error) { + console.error( + `ERROR: Failed to set up ${supportedProducts[product]} r${revision}! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.` + ); + console.error(error); + process.exit(1); + } + + let progressBar: ProgressBar | null = null; + let lastDownloadedBytes = 0; + function onProgress(downloadedBytes: number, totalBytes: number) { + if (!progressBar) { + progressBar = new ProgressBar( + `Downloading ${ + supportedProducts[product] + } r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + + return browserFetcher + .download(revisionInfo.revision, onProgress) + .then(() => { + return browserFetcher.localRevisions(); + }) + .then(onSuccess) + .catch(onError); + } + + function toMegabytes(bytes: number) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } + + async function getFirefoxNightlyVersion(): Promise<string> { + const firefoxVersionsUrl = + 'https://product-details.mozilla.org/1.0/firefox_versions.json'; + + const proxyURL = getProxyForUrl(firefoxVersionsUrl); + + const requestOptions: RequestOptions = {}; + + if (proxyURL) { + const parsedProxyURL = URL.parse(proxyURL); + + const proxyOptions = { + ...parsedProxyURL, + secureProxy: parsedProxyURL.protocol === 'https:', + } as HttpsProxyAgentOptions; + + requestOptions.agent = createHttpsProxyAgent(proxyOptions); + requestOptions.rejectUnauthorized = false; + } + + const promise = new Promise<string>((resolve, reject) => { + let data = ''; + logPolitely( + `Requesting latest Firefox Nightly version from ${firefoxVersionsUrl}` + ); + https + .get(firefoxVersionsUrl, requestOptions, r => { + if (r.statusCode && r.statusCode >= 400) { + return reject(new Error(`Got status code ${r.statusCode}`)); + } + r.on('data', chunk => { + data += chunk; + }); + r.on('end', () => { + try { + const versions = JSON.parse(data); + return resolve(versions.FIREFOX_NIGHTLY); + } catch { + return reject(new Error('Firefox version not found')); + } + }); + }) + .on('error', reject); + }); + return promise; + } +} + +/** + * @internal + */ +export 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); + } +} diff --git a/remote/test/puppeteer/src/node/util.ts b/remote/test/puppeteer/src/node/util.ts new file mode 100644 index 0000000000..c362a39e65 --- /dev/null +++ b/remote/test/puppeteer/src/node/util.ts @@ -0,0 +1,13 @@ +import * as os from 'os'; + +/** + * Gets the temporary directory, either from the environmental variable + * `PUPPETEER_TMP_DIR` or the `os.tmpdir`. + * + * @returns The temporary directory path. + * + * @internal + */ +export const tmpdir = (): string => { + return process.env['PUPPETEER_TMP_DIR'] || os.tmpdir(); +}; diff --git a/remote/test/puppeteer/src/puppeteer-core.ts b/remote/test/puppeteer/src/puppeteer-core.ts new file mode 100644 index 0000000000..50da718345 --- /dev/null +++ b/remote/test/puppeteer/src/puppeteer-core.ts @@ -0,0 +1,34 @@ +/** + * 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 {initializePuppeteer} from './initializePuppeteer.js'; + +export * from './common/NetworkConditions.js'; +export * from './common/QueryHandler.js'; +export * from './common/DeviceDescriptors.js'; +export * from './common/Errors.js'; + +const puppeteer = initializePuppeteer('puppeteer-core'); + +export const { + connect, + createBrowserFetcher, + defaultArgs, + executablePath, + launch, +} = puppeteer; + +export default puppeteer; diff --git a/remote/test/puppeteer/src/puppeteer.ts b/remote/test/puppeteer/src/puppeteer.ts new file mode 100644 index 0000000000..63317cbf7d --- /dev/null +++ b/remote/test/puppeteer/src/puppeteer.ts @@ -0,0 +1,39 @@ +/** + * 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 {initializePuppeteer} from './initializePuppeteer.js'; + +export * from './common/NetworkConditions.js'; +export * from './common/QueryHandler.js'; +export * from './common/DeviceDescriptors.js'; +export * from './common/Errors.js'; + +export {Protocol} from 'devtools-protocol'; + +/** + * @public + */ +const puppeteer = initializePuppeteer('puppeteer'); + +export const { + connect, + createBrowserFetcher, + defaultArgs, + executablePath, + launch, +} = puppeteer; + +export default puppeteer; diff --git a/remote/test/puppeteer/src/revisions.ts b/remote/test/puppeteer/src/revisions.ts new file mode 100644 index 0000000000..5c7a747a8f --- /dev/null +++ b/remote/test/puppeteer/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({ + chromium: '1036745', + firefox: 'latest', +}); diff --git a/remote/test/puppeteer/src/templates/README.md b/remote/test/puppeteer/src/templates/README.md new file mode 100644 index 0000000000..548987eafc --- /dev/null +++ b/remote/test/puppeteer/src/templates/README.md @@ -0,0 +1,3 @@ +# Templated Artifacts + +These files are generated as TypeScript files in the `src/generated` folder. diff --git a/remote/test/puppeteer/src/templates/injected.ts.tmpl b/remote/test/puppeteer/src/templates/injected.ts.tmpl new file mode 100644 index 0000000000..74146a4115 --- /dev/null +++ b/remote/test/puppeteer/src/templates/injected.ts.tmpl @@ -0,0 +1,10 @@ +import {createDeferredPromise} from '../util/DeferredPromise.js'; + +declare global { + const InjectedUtil: { + createDeferredPromise: typeof createDeferredPromise; + }; +} + +/** @internal */ +export const source = SOURCE_CODE; diff --git a/remote/test/puppeteer/src/templates/version.ts.tmpl b/remote/test/puppeteer/src/templates/version.ts.tmpl new file mode 100644 index 0000000000..73b984d2ff --- /dev/null +++ b/remote/test/puppeteer/src/templates/version.ts.tmpl @@ -0,0 +1,4 @@ +/** + * @internal + */ +export const packageVersion = 'PACKAGE_VERSION'; diff --git a/remote/test/puppeteer/src/tsconfig.cjs.json b/remote/test/puppeteer/src/tsconfig.cjs.json new file mode 100644 index 0000000000..c92ca3f87c --- /dev/null +++ b/remote/test/puppeteer/src/tsconfig.cjs.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/puppeteer", + "module": "CommonJS" + }, + "references": [ + {"path": "../vendor/tsconfig.cjs.json"}, + {"path": "../compat/cjs/tsconfig.json"} + ], + "exclude": ["injected/injected.ts"] +} diff --git a/remote/test/puppeteer/src/tsconfig.esm.json b/remote/test/puppeteer/src/tsconfig.esm.json new file mode 100644 index 0000000000..aba2f24035 --- /dev/null +++ b/remote/test/puppeteer/src/tsconfig.esm.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/puppeteer", + "module": "esnext" + }, + "references": [ + {"path": "../vendor/tsconfig.esm.json"}, + {"path": "../compat/esm/tsconfig.json"} + ], + "exclude": ["injected/injected.ts"] +} diff --git a/remote/test/puppeteer/src/types.ts b/remote/test/puppeteer/src/types.ts new file mode 100644 index 0000000000..00bed28c5a --- /dev/null +++ b/remote/test/puppeteer/src/types.ts @@ -0,0 +1,74 @@ +// AUTOGENERATED - Use `npm run generate:sources` to regenerate. + +export * from './common/Accessibility.js'; +export * from './common/AriaQueryHandler.js'; +export * from './common/Browser.js'; +export * from './common/BrowserConnector.js'; +export * from './common/BrowserWebSocketTransport.js'; +export * from './common/ChromeTargetManager.js'; +export * from './common/Connection.js'; +export * from './common/ConnectionTransport.js'; +export * from './common/ConsoleMessage.js'; +export * from './common/Coverage.js'; +export * from './common/Debug.js'; +export * from './common/DeviceDescriptors.js'; +export * from './common/Dialog.js'; +export * from './common/ElementHandle.js'; +export * from './common/EmulationManager.js'; +export * from './common/Errors.js'; +export * from './common/EventEmitter.js'; +export * from './common/ExecutionContext.js'; +export * from './common/fetch.js'; +export * from './common/FileChooser.js'; +export * from './common/FirefoxTargetManager.js'; +export * from './common/Frame.js'; +export * from './common/FrameManager.js'; +export * from './common/HTTPRequest.js'; +export * from './common/HTTPResponse.js'; +export * from './common/Input.js'; +export * from './common/IsolatedWorld.js'; +export * from './common/JSHandle.js'; +export * from './common/LifecycleWatcher.js'; +export * from './common/NetworkConditions.js'; +export * from './common/NetworkEventManager.js'; +export * from './common/NetworkManager.js'; +export * from './common/Page.js'; +export * from './common/PDFOptions.js'; +export * from './common/Product.js'; +export * from './common/Puppeteer.js'; +export * from './common/PuppeteerViewport.js'; +export * from './common/QueryHandler.js'; +export * from './common/SecurityDetails.js'; +export * from './common/Target.js'; +export * from './common/TargetManager.js'; +export * from './common/TaskQueue.js'; +export * from './common/TimeoutSettings.js'; +export * from './common/Tracing.js'; +export * from './common/types.js'; +export * from './common/USKeyboardLayout.js'; +export * from './common/util.js'; +export * from './common/WebWorker.js'; +export * from './compat.d.js'; +export * from './constants.js'; +export * from './environment.js'; +export * from './generated/injected.js'; +export * from './generated/version.js'; +export * from './initializePuppeteer.js'; +export * from './node/BrowserFetcher.js'; +export * from './node/BrowserRunner.js'; +export * from './node/ChromeLauncher.js'; +export * from './node/FirefoxLauncher.js'; +export * from './node/install.js'; +export * from './node/LaunchOptions.js'; +export * from './node/NodeWebSocketTransport.js'; +export * from './node/PipeTransport.js'; +export * from './node/ProductLauncher.js'; +export * from './node/Puppeteer.js'; +export * from './node/util.js'; +export * from './puppeteer.js'; +export * from './revisions.js'; +export * from './util/assert.js'; +export * from './util/DebuggableDeferredPromise.js'; +export * from './util/DeferredPromise.js'; +export * from './util/ErrorLike.js'; +export * from './util/getPackageDirectory.js'; diff --git a/remote/test/puppeteer/src/util/DebuggableDeferredPromise.ts b/remote/test/puppeteer/src/util/DebuggableDeferredPromise.ts new file mode 100644 index 0000000000..474743273c --- /dev/null +++ b/remote/test/puppeteer/src/util/DebuggableDeferredPromise.ts @@ -0,0 +1,20 @@ +import {DEFERRED_PROMISE_DEBUG_TIMEOUT} from '../environment.js'; +import {DeferredPromise, createDeferredPromise} from './DeferredPromise.js'; + +/** + * Creates and returns a deferred promise using DEFERRED_PROMISE_DEBUG_TIMEOUT + * if it's specified or a normal deferred promise otherwise. + * + * @internal + */ +export function createDebuggableDeferredPromise<T>( + message: string +): DeferredPromise<T> { + if (DEFERRED_PROMISE_DEBUG_TIMEOUT > 0) { + return createDeferredPromise({ + message, + timeout: DEFERRED_PROMISE_DEBUG_TIMEOUT, + }); + } + return createDeferredPromise(); +} diff --git a/remote/test/puppeteer/src/util/DeferredPromise.ts b/remote/test/puppeteer/src/util/DeferredPromise.ts new file mode 100644 index 0000000000..2f21087ba8 --- /dev/null +++ b/remote/test/puppeteer/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: (_: T) => void; + reject: (_: Error) => void; +} + +/** + * @internal + */ +export interface DeferredPromiseOptions { + message: string; + timeout: number; +} + +/** + * Creates and returns a promise along with the resolve/reject functions. + * + * If the promise has not been resolved/rejected within the `timeout` period, + * the promise gets rejected with a timeout error. `timeout` has to be greater than 0 or + * it is ignored. + * + * @internal + */ +export function createDeferredPromise<T>( + opts?: DeferredPromiseOptions +): DeferredPromise<T> { + let isResolved = false; + let isRejected = false; + let resolver = (_: T): void => {}; + let rejector = (_: Error) => {}; + const taskPromise = new Promise<T>((resolve, reject) => { + resolver = resolve; + rejector = reject; + }); + const timeoutId = + opts && opts.timeout > 0 + ? setTimeout(() => { + isRejected = true; + rejector(new TimeoutError(opts.message)); + }, opts.timeout) + : undefined; + return Object.assign(taskPromise, { + resolved: () => { + return isResolved; + }, + finished: () => { + return isResolved || isRejected; + }, + resolve: (value: T) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + isResolved = true; + resolver(value); + }, + reject: (err: Error) => { + clearTimeout(timeoutId); + isRejected = true; + rejector(err); + }, + }); +} diff --git a/remote/test/puppeteer/src/util/ErrorLike.ts b/remote/test/puppeteer/src/util/ErrorLike.ts new file mode 100644 index 0000000000..e5659ce3e3 --- /dev/null +++ b/remote/test/puppeteer/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/src/util/assert.ts b/remote/test/puppeteer/src/util/assert.ts new file mode 100644 index 0000000000..bd8b10e731 --- /dev/null +++ b/remote/test/puppeteer/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/src/util/getPackageDirectory.ts b/remote/test/puppeteer/src/util/getPackageDirectory.ts new file mode 100644 index 0000000000..d0c5fc4cad --- /dev/null +++ b/remote/test/puppeteer/src/util/getPackageDirectory.ts @@ -0,0 +1,18 @@ +import {existsSync} from 'fs'; +import {dirname, join, parse} from 'path'; + +/** + * @internal + */ +export const getPackageDirectory = (from: string): string => { + let found = existsSync(join(from, 'package.json')); + const root = parse(from).root; + while (!found) { + if (from === root) { + throw new Error('Cannot find package directory'); + } + from = dirname(from); + found = existsSync(join(from, 'package.json')); + } + return from; +}; 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..8f3090c45c --- /dev/null +++ b/remote/test/puppeteer/test-d/ElementHandle.test-d.ts @@ -0,0 +1,158 @@ +import {expectNotType, expectType} from 'tsd'; +import {ElementHandle} from '../lib/esm/puppeteer/common/ElementHandle.js'; + +declare const handle: ElementHandle; + +{ + { + expectType<ElementHandle<HTMLAnchorElement> | null>(await handle.$('a')); + expectNotType<ElementHandle<Element> | null>(await handle.$('a')); + } + { + expectType<ElementHandle<HTMLDivElement> | null>(await handle.$('div')); + expectNotType<ElementHandle<Element> | null>(await handle.$('div')); + } + { + expectType<ElementHandle<Element> | null>(await handle.$('some-custom')); + } +} + +{ + { + expectType<Array<ElementHandle<HTMLAnchorElement>>>(await handle.$$('a')); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('a')); + } + { + expectType<Array<ElementHandle<HTMLDivElement>>>(await handle.$$('div')); + expectNotType<Array<ElementHandle<Element>>>(await handle.$$('div')); + } + { + expectType<Array<ElementHandle<Element>>>(await handle.$$('some-custom')); + } +} + +{ + 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..c978c81187 --- /dev/null +++ b/remote/test/puppeteer/test-d/JSHandle.test-d.ts @@ -0,0 +1,84 @@ +import {expectNotAssignable, expectNotType, expectType} from 'tsd'; +import {ElementHandle} from '../lib/esm/puppeteer/common/ElementHandle.js'; +import {JSHandle} from '../lib/esm/puppeteer/common/JSHandle.js'; + +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') + ); + } +} + +{ + handle.evaluate((value, other) => { + expectType<unknown>(value); + expectType<{test: number}>(other); + }, handle2); +} 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..0b137d1cdd --- /dev/null +++ b/remote/test/puppeteer/test-d/puppeteer.test-d.ts @@ -0,0 +1,15 @@ +import {expectType} from 'tsd'; +import { + connect, + createBrowserFetcher, + defaultArgs, + executablePath, + launch, + default as puppeteer, +} from '../lib/esm/puppeteer/puppeteer.js'; + +expectType<typeof launch>(puppeteer.launch); +expectType<typeof connect>(puppeteer.connect); +expectType<typeof createBrowserFetcher>(puppeteer.createBrowserFetcher); +expectType<typeof defaultArgs>(puppeteer.defaultArgs); +expectType<typeof executablePath>(puppeteer.executablePath); diff --git a/remote/test/puppeteer/test-d/tsconfig.json b/remote/test/puppeteer/test-d/tsconfig.json new file mode 100644 index 0000000000..259d60c123 --- /dev/null +++ b/remote/test/puppeteer/test-d/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "ESNext" + }, + "references": [{"path": "../src/tsconfig.esm.json"}] +} 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..6197547356 --- /dev/null +++ b/remote/test/puppeteer/test/README.md @@ -0,0 +1,85 @@ +# Puppeteer unit 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 `utils/testserver` for more). +- `httpsServer`: a dummy test server HTTPS instance (see `utils/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 + +Tests that are not expected to pass in Firefox can be skipped. You can skip an individual test by using `itFailsFirefox` rather than `it`. Similarly you can skip a describe block with `describeFailsFirefox`. + +There is also `describeChromeOnly` and `itChromeOnly` which will only execute the test if running in Chromium. Note that this is different from `describeFailsFirefox`: the goal is to get any `FailsFirefox` calls passing in Firefox, whereas `describeChromeOnly` should be used to test behaviour that will only ever apply in Chromium. + +There are also tests that assume a normal install flow, with browser binaries ending up in `.local-<browser>`, for example. Such tests are skipped with +`itOnlyRegularInstall` which checks `BINARY` and `PUPPETEER_ALT_INSTALL` environment variables. + +[mocha]: https://mochajs.org/ +[expect]: https://www.npmjs.com/package/expect + +## Running tests + +- To run all tests: + +```bash +npm test +``` + +- **Important**: don't forget to first build the code if you're testing local changes: + +```bash +npm run build && npm test +``` + +- 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 `xit` (mnemonic rule: '_cross it_'): + +```ts + ... + // Using "xit" to skip specific test + xit('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 +``` 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/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.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..a537a7da6a --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ +<script> +function unused(){}console.log('used!');</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/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-chromium/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt new file mode 100644 index 0000000000..9b851d0bd3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/csscoverage-involved.txt @@ -0,0 +1,16 @@ +[ + { + "url": "http://localhost:<PORT>/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..ff282e989b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..91a1cb8510 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png Binary files differnew file mode 100644 index 0000000000..7b01753b6a --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-2.png diff --git a/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png Binary files differnew file mode 100644 index 0000000000..b9b8b2922b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/grid-cell-3.png diff --git a/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chromium/jscoverage-involved.txt new file mode 100644 index 0000000000..016b30bde8 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/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-chromium/mock-binary-response.png b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png Binary files differnew file mode 100644 index 0000000000..8595e0598e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/mock-binary-response.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..b010d1f87f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect-scale2.png Binary files differnew file mode 100644 index 0000000000..d713d27943 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect-scale2.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..ac23b7de50 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..32e05bf05b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..cc8669d598 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..35c53377f9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..5fcdb92355 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..52e2a0f6d3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..d6d38217f7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..e503f801ec --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..ecab61fe17 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-chromium/transparent.png b/remote/test/puppeteer/test/golden-chromium/transparent.png Binary files differnew file mode 100644 index 0000000000..1cf45d8688 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/transparent.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png Binary files differnew file mode 100644 index 0000000000..4d74aac44c --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-achromatopsia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png Binary files differnew file mode 100644 index 0000000000..0efb9ca066 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-blurredVision.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png Binary files differnew file mode 100644 index 0000000000..79b4b0fa1b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-deuteranopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png Binary files differnew file mode 100644 index 0000000000..bede7c1ed0 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-protanopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png Binary files differnew file mode 100644 index 0000000000..d5f6bbec2e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/vision-deficiency-tritanopia.png diff --git a/remote/test/puppeteer/test/golden-chromium/white.jpg b/remote/test/puppeteer/test/golden-chromium/white.jpg Binary files differnew file mode 100644 index 0000000000..fb9070def3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chromium/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/src/CDPSession.spec.ts b/remote/test/puppeteer/test/src/CDPSession.spec.ts new file mode 100644 index 0000000000..920c31051c --- /dev/null +++ b/remote/test/puppeteer/test/src/CDPSession.spec.ts @@ -0,0 +1,136 @@ +/** + * 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 {waitEvent} from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils.js'; +import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; + +describeChromeOnly('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 = []; + client.on('Network.requestWillBeSent', event => { + return events.push(event); + }); + await page.goto(server.EMPTY_PAGE); + expect(events.length).toBe(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/EventEmitter.spec.ts b/remote/test/puppeteer/test/src/EventEmitter.spec.ts new file mode 100644 index 0000000000..67e1ca610d --- /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 {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; +import sinon from 'sinon'; +import expect from 'expect'; + +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..cd8d647746 --- /dev/null +++ b/remote/test/puppeteer/test/src/NetworkManager.spec.ts @@ -0,0 +1,1126 @@ +/** + * 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 {describeChromeOnly} from './mocha-utils.js'; + +import expect from 'expect'; +import { + NetworkManager, + NetworkManagerEmittedEvents, +} from '../../lib/cjs/puppeteer/common/NetworkManager.js'; +import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js'; +import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; +import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js'; +import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; + +class MockCDPSession extends EventEmitter { + async send(): Promise<any> {} +} + +describeChromeOnly('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.length).toBe(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.length).toBe(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.length).toBe(1); + expect(finishedRequests.length).toBe(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.length).toBe(1); + expect(finishedRequests.length).toBe(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.length).toBe(1); + expect(responses.length).toBe(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.length).toBe(1); + expect(responses.length).toBe(1); + expect(requests[0]!.response()).not.toEqual(null); + }); +}); 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..175cde72f3 --- /dev/null +++ b/remote/test/puppeteer/test/src/TargetManager.spec.ts @@ -0,0 +1,111 @@ +/** + * 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 {describeChromeOnly, getTestState} from './mocha-utils'; // eslint-disable-line import/extensions +import utils from './utils.js'; + +import expect from 'expect'; + +import { + Browser, + BrowserContext, +} from '../../lib/cjs/puppeteer/common/Browser.js'; + +describeChromeOnly('TargetManager', () => { + /* We use a special browser for this test as we need the --site-per-process flag */ + let browser: Browser; + let context: BrowserContext; + + 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(); + }); + + 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 utils.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 utils.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 utils.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..fe066e9d01 --- /dev/null +++ b/remote/test/puppeteer/test/src/accessibility.spec.ts @@ -0,0 +1,536 @@ +/** + * 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 '../../lib/cjs/puppeteer/common/Accessibility.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils.js'; + +describeFailsFirefox('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('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". + describeFailsFirefox('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..3c05349084 --- /dev/null +++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts @@ -0,0 +1,693 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils.js'; + +import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; +import utils from './utils.js'; +import assert from 'assert'; + +describeChromeOnly('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 () => { + 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 ElementHandler.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 utils.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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + const frame1 = page.frames()[1]; + const frame2 = page.frames()[2]; + const waitForSelectorPromise = frame2!.waitForSelector( + '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 utils.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 utils.detachFrame(page, 'frame1'); + await waitPromise; + expect(waitError).toBeTruthy(); + expect(waitError.message).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + + it('should survive cross-process navigation', async () => { + const {page, server} = getTestState(); + + let 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;'></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;'></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'></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, puppeteer} = getTestState(); + + let error!: Error; + await page + .waitForSelector('aria/[role="button"]', {timeout: 10}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="button"]` failed: timeout' + ); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const {page} = getTestState(); + + await page.setContent(`<div role='main'></div>`); + let error!: Error; + await page + .waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'waiting for selector `[role="main"]` to be hidden failed: timeout' + ); + }); + + 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'); + }); + }); + + 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/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts new file mode 100644 index 0000000000..375b3dba93 --- /dev/null +++ b/remote/test/puppeteer/test/src/browser.spec.ts @@ -0,0 +1,90 @@ +/** + * 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 !== 'chrome' + ); + }); + }); + + describe('Browser.userAgent', function () { + it('should include WebKit', async () => { + const {browser, isChrome} = getTestState(); + + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (isChrome) { + expect(userAgent).toContain('WebKit'); + } else { + expect(userAgent).toContain('Gecko'); + } + }); + }); + + describe('Browser.target', function () { + it('should return browser target', async () => { + const {browser} = getTestState(); + + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function () { + it('should return child_process instance', async () => { + const {browser} = getTestState(); + + const process = await browser.process(); + expect(process!.pid).toBeGreaterThan(0); + }); + it('should not return child_process for remote browser', async () => { + const {browser, puppeteer} = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + }); + expect(remoteBrowser.process()).toBe(null); + remoteBrowser.disconnect(); + }); + }); + + describe('Browser.isConnected', () => { + it('should set the browser connected state', async () => { + const {browser, puppeteer} = getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await puppeteer.connect({ + browserWSEndpoint, + }); + expect(newBrowser.isConnected()).toBe(true); + newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/browsercontext.spec.ts b/remote/test/puppeteer/test/src/browsercontext.spec.ts new file mode 100644 index 0000000000..5c1321529e --- /dev/null +++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts @@ -0,0 +1,232 @@ +/** + * 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'; +import {waitEvent} from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + it('should have default context', async () => { + const {browser} = getTestState(); + expect(browser.browserContexts().length).toEqual(1); + const defaultContext = browser.browserContexts()[0]!; + expect(defaultContext!.isIncognito()).toBe(false); + let error!: 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().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts().length).toBe(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts().length).toBe(1); + }); + it('should close all belonging targets once closing context', async () => { + const {browser} = getTestState(); + + expect((await browser.pages()).length).toBe(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect((await browser.pages()).length).toBe(2); + expect((await context.pages()).length).toBe(1); + + await context.close(); + expect((await browser.pages()).length).toBe(1); + }); + 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, puppeteer, 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 puppeteer.errors.TimeoutError) { + console.error(error); + } else { + throw error; + } + }); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else { + throw error; + } + } + await context.close(); + }); + + it('should timeout waiting for a non-existent target', async () => { + const {browser, puppeteer, server} = getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return error_; + }); + expect(error).toBeInstanceOf(puppeteer.errors.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().length).toBe(0); + expect(context2.targets().length).toBe(0); + + // Create a page in first incognito context. + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + await page1.evaluate(() => { + localStorage.setItem('name', 'page1'); + document.cookie = 'name=page1'; + }); + + expect(context1.targets().length).toBe(1); + expect(context2.targets().length).toBe(0); + + // Create a page in second incognito context. + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + await page2.evaluate(() => { + localStorage.setItem('name', 'page2'); + document.cookie = 'name=page2'; + }); + + expect(context1.targets().length).toBe(1); + expect(context1.targets()[0]!).toBe(page1.target()); + expect(context2.targets().length).toBe(1); + expect(context2.targets()[0]!).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect( + await page1.evaluate(() => { + 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().length).toBe(1); + }); + + it('should work across sessions', async () => { + const {browser, puppeteer} = getTestState(); + + expect(browser.browserContexts().length).toBe(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + remoteBrowser.disconnect(); + await context.close(); + }); +}); diff --git a/remote/test/puppeteer/test/src/chromiumonly.spec.ts b/remote/test/puppeteer/test/src/chromiumonly.spec.ts new file mode 100644 index 0000000000..5cb9cd3f2e --- /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 expect from 'expect'; +import {IncomingMessage} from 'http'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeChromeOnly, +} from './mocha-utils.js'; + +describeChromeOnly('Chromium-Specific Launcher tests', function () { + describe('Puppeteer.launch |browserURL| option', function () { + it('should be able to connect using browserUrl, with and without trailing slash', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const originalBrowser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + args: ['--remote-debugging-port=21222'], + }) + ); + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await puppeteer.connect({browserURL}); + const page1 = await browser1.newPage(); + expect( + await page1.evaluate(() => { + 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(); + 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' + ); + + 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' + ); + originalBrowser.close(); + }); + }); + + describe('Puppeteer.launch |pipe| option', function () { + it('should support the pipe option', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + expect((await browser.pages()).length).toBe(1); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should support the pipe argument', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const browser = await puppeteer.launch(options); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + it('should fire "disconnected" when closing with pipe', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({pipe: true}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const disconnectedEventPromise = new Promise(resolve => { + return browser.once('disconnected', resolve); + }); + // Emulate user exiting browser. + browser.process()!.kill(); + await disconnectedEventPromise; + }); + }); +}); + +describeChromeOnly('Chromium-Specific Page Tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.setRequestInterception should work with intervention headers', async () => { + const {server, page} = getTestState(); + + server.setRoute('/intervention', (_req, res) => { + 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..9574643bce --- /dev/null +++ b/remote/test/puppeteer/test/src/click.spec.ts @@ -0,0 +1,472 @@ +/** + * 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'; +import utils from './utils.js'; + +describe('Page.click', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the button', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect( + await page.evaluate(() => { + 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 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.click('textarea'); + await page.click('textarea', {clickCount: 2}); + await page.click('textarea', {clickCount: 3}); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea!.value.substring( + textarea!.selectionStart, + textarea!.selectionEnd + ); + }) + ).toBe(text); + }); + it('should click offscreen buttons', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages: 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, puppeteer} = getTestState(); + + await page.setViewport(puppeteer.devices['iPhone 6']!.viewport); + await page.mouse.down(); + await page.mouse.move(100, 10); + await page.mouse.up(); + }); + it('should scroll and click the button', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-5'); + expect( + await page.evaluate(() => { + 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({clickCount: 2}); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + const button = document.querySelector('button'); + button!.textContent = 'Some really long text that will go offscreen'; + button!.style.position = 'absolute'; + button!.style.left = '368px'; + }); + await page.click('button'); + expect( + await page.evaluate(() => { + 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 utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame!.$('button'); + await button!.click(); + expect( + await frame!.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + xit('should click the button with fixed position inside an iframe', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 500, height: 500}); + await page.setContent( + '<div style="width:100px;height:2000px">spacer</div>' + ); + await utils.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 utils.attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + const button = await frame!.$('button'); + await button!.click(); + expect( + await frame!.evaluate(() => { + 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..186535317c --- /dev/null +++ b/remote/test/puppeteer/test/src/cookies.spec.ts @@ -0,0 +1,575 @@ +/** + * 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.length).toBe(1); + expect(cookies[0]!.httpOnly).toBe(true); + }); + it('should properly report "Strict" sameSite cookie', async () => { + const {page, server} = getTestState(); + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Strict'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0]!.sameSite).toBe('Strict'); + }); + it('should properly report "Lax" sameSite cookie', async () => { + const {page, server} = getTestState(); + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Lax'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies.length).toBe(1); + expect(cookies[0]!.sameSite).toBe('Lax'); + }); + it('should get multiple cookies', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + document.cookie = 'password=1234'; + }); + const cookies = await page.cookies(); + cookies.sort((a, b) => { + 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.length).toBe(1); + expect(cookies2.length).toBe(1); + expect(cookies1[0]!.name).toBe('page1cookie'); + expect(cookies1[0]!.value).toBe('page1value'); + expect(cookies2[0]!.name).toBe('page2cookie'); + expect(cookies2[0]!.value).toBe('page2value'); + await anotherContext.close(); + }); + 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 unsecure cookie for HTTP website', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie!.secure).toBe(false); + }); + 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..6e0fc5fe55 --- /dev/null +++ b/remote/test/puppeteer/test/src/coverage.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 expect from 'expect'; +import { + getTestState, + setupTestPageAndContextHooks, + setupTestBrowserHooks, + describeChromeOnly, +} from './mocha-utils.js'; + +describe('Coverage specs', function () { + describeChromeOnly('JSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, server} = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.url).toContain('/jscoverage/simple.html'); + expect(coverage[0]!.ranges).toEqual([ + {start: 0, end: 17}, + {start: 35, end: 61}, + ]); + }); + it('should report sourceURLs', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/sourceurl.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.url).toBe('nicename.js'); + }); + it('should ignore eval() scripts by default', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + }); + it("shouldn't ignore eval() scripts if reportAnonymousScripts is true", async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + coverage.find(entry => { + return entry.url.startsWith('debugger://'); + }) + ).not.toBe(null); + expect(coverage.length).toBe(2); + }); + it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => { + return console.log('bar'); + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + it('should report multiple scripts', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => { + 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.length).toBe(1); + const entry = coverage[0]!; + expect(entry.ranges.length).toBe(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!');` + ); + }); + it('should report scripts that have no coverage', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/unused.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + const entry = coverage[0]!; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges.length).toBe(0); + }); + it('should work with conditionals', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/involved.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/') + ).toBeGolden('jscoverage-involved.txt'); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + describe('resetOnNavigation', function () { + it('should report scripts across navigations when disabled', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + }); + + it('should NOT report scripts across navigations when enabled', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + describe('includeRawScriptCoverage', function () { + it('should not include rawScriptCoverage field when disabled', async () => { + const {page, server} = getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.rawScriptCoverage).toBeUndefined(); + }); + it('should include rawScriptCoverage field when enabled', async () => { + const {page, server} = getTestState(); + await page.coverage.startJSCoverage({ + includeRawScriptCoverage: true, + }); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'networkidle0', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.rawScriptCoverage).toBeTruthy(); + }); + }); + // @see https://crbug.com/990945 + xit('should not hang when there is a debugger statement', async () => { + const {page, server} = getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describeChromeOnly('CSSCoverage', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/simple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.url).toContain('/csscoverage/simple.html'); + expect(coverage[0]!.ranges).toEqual([{start: 1, end: 22}]); + const range = coverage[0]!.ranges[0]!; + expect(coverage[0]!.text.substring(range.start, range.end)).toBe( + 'div { color: green; }' + ); + }); + it('should report sourceURLs', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/sourceurl.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.url).toBe('nicename.css'); + }); + it('should report multiple stylesheets', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + coverage.sort((a, b) => { + 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.length).toBe(1); + expect(coverage[0]!.url).toBe('unused.css'); + expect(coverage[0]!.ranges.length).toBe(0); + }); + it('should work with media queries', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/media.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(1); + expect(coverage[0]!.url).toContain('/csscoverage/media.html'); + expect(coverage[0]!.ranges).toEqual([{start: 17, end: 38}]); + }); + it('should work with complicated usecases', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/involved.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4,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.length).toEqual(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.length).toBe(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.length).toBe(1); + }); + describe('resetOnNavigation', function () { + it('should report stylesheets across navigations', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should NOT report scripts across navigations', async () => { + const {page, server} = getTestState(); + + await page.coverage.startCSSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage.length).toBe(0); + }); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000..cbdf34f634 --- /dev/null +++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts @@ -0,0 +1,114 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import expect from 'expect'; +import { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} 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..0b0f93b5c9 --- /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, + describeChromeOnly, +} from './mocha-utils.js'; + +describeChromeOnly('Input.drag', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should throw an exception if not enabled before usage', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + const draggable = (await page.$('#drag'))!; + + try { + await draggable!.drag({x: 1, y: 1}); + } catch (error) { + expect((error 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.length).toBe(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..928145f767 --- /dev/null +++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts @@ -0,0 +1,609 @@ +/** + * 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 {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +import utils 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 utils.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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + const elementHandle = (await page.$('#frame1'))!; + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]!); + }); + }); + + describe('ElementHandle.click', function () { + // See https://github.com/puppeteer/puppeteer/issues/7175 + 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 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('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); + }); + }); + + describe('Custom queries', function () { + this.afterEach(() => { + const {puppeteer} = getTestState(); + puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const {page, puppeteer} = 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 { + const {puppeteer} = getTestState(); + 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, puppeteer} = 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, puppeteer} = 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, puppeteer} = 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, puppeteer} = 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, puppeteer} = 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, puppeteer} = 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.length).toBe(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const {page, puppeteer} = 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'); + }); + }); +}); 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..45548a373a --- /dev/null +++ b/remote/test/puppeteer/test/src/emulation.spec.ts @@ -0,0 +1,520 @@ +/** + * 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 {Device} from '../../lib/cjs/puppeteer/common/DeviceDescriptors.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Emulation', () => { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + let iPhone!: Device; + let iPhoneLandscape!: Device; + + before(() => { + const {puppeteer} = getTestState(); + iPhone = puppeteer.devices['iPhone 6']!; + iPhoneLandscape = puppeteer.devices['iPhone 6 landscape']!; + }); + + describe('Page.viewport', function () { + it('should get the proper viewport size', async () => { + const {page} = getTestState(); + + expect(page.viewport()).toEqual({width: 800, height: 600}); + await page.setViewport({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 123, height: 456}); + }); + it('should support mobile emulation', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + 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 passign 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, puppeteer} = getTestState(); + + const slow3G = puppeteer.networkConditions['Slow 3G']!; + const fast3G = puppeteer.networkConditions['Fast 3G']!; + + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('4g'); + await page.emulateNetworkConditions(fast3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('3g'); + await page.emulateNetworkConditions(slow3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('2g'); + await page.emulateNetworkConditions(null); + }); + }); + + 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..45fe4abbcd --- /dev/null +++ b/remote/test/puppeteer/test/src/evaluation.spec.ts @@ -0,0 +1,598 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +const bigint = typeof BigInt !== 'undefined'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const {page} = getTestState(); + + const result = await page.evaluate(() => { + return 7 * 3; + }); + expect(result).toBe(21); + }); + (bigint ? it : xit)('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); + }); + (bigint ? it : xit)('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 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 utils.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 () { + 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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(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().length).toBe(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..e20054dc6d --- /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 expect from 'expect'; +import {getTestState, itHeadlessOnly} from './mocha-utils.js'; + +import path from 'path'; + +describe('Fixtures', function () { + itHeadlessOnly('dumpio option should work with pipe option', async () => { + const {defaultBrowserOptions, puppeteerPath, headless} = getTestState(); + if (headless === 'chrome') { + // 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 = [ + new Promise(resolve => { + return browser.once('disconnected', resolve); + }), + 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..5f10b84f71 --- /dev/null +++ b/remote/test/puppeteer/test/src/frame.spec.ts @@ -0,0 +1,357 @@ +/** + * 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 '../../lib/cjs/puppeteer/common/Connection.js'; +import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import utils, {dumpFrames} 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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames().length).toBe(2); + const [frame1, frame2] = page.frames(); + const context1 = await frame1!.executionContext(); + const context2 = await frame2!.executionContext(); + expect(context1).toBeTruthy(); + expect(context2).toBeTruthy(); + expect(context1 !== context2).toBeTruthy(); + expect(context1._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 utils.attachFrame( + page, + 'frame1', + server.EMPTY_PAGE + ))!; + await utils.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 utils.attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames.length).toBe(1); + expect(attachedFrames[0]!.url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames: Frame[] = []; + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await utils.navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames.length).toBe(1); + expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames: Frame[] = []; + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + await utils.detachFrame(page, 'frame1'); + expect(detachedFrames.length).toBe(1); + expect(detachedFrames[0]!.isDetached()).toBe(true); + } + ); + it('should send "framenavigated" when navigating on anchor URLs', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.goto(server.EMPTY_PAGE + '#foo'), + utils.waitEvent(page, 'framenavigated'), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + }); + it('should persist mainFrame on cross-process navigation', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(page.mainFrame() === mainFrame).toBeTruthy(); + }); + it('should not send attach/detach events for main frame', async () => { + const {page, server} = getTestState(); + + let hasEvents = false; + page.on('frameattached', () => { + 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 = []; + let detachedFrames = []; + let navigatedFrames = []; + 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.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should support framesets', async () => { + const {page, server} = getTestState(); + + let attachedFrames = []; + let detachedFrames = []; + let navigatedFrames = []; + page.on('frameattached', frame => { + 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.length).toBe(4); + expect(detachedFrames.length).toBe(0); + expect(navigatedFrames.length).toBe(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames.length).toBe(0); + expect(detachedFrames.length).toBe(4); + expect(navigatedFrames.length).toBe(1); + }); + it('should 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().length).toBe(2); + expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should report frame.name()', async () => { + const {page, server} = getTestState(); + + await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate((url: string) => { + const frame = document.createElement('iframe'); + frame.name = 'theFrameName'; + frame.src = url; + document.body.appendChild(frame); + return new Promise(x => { + 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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); + expect(page.frames()[0]!.parentFrame()).toBe(null); + expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame()); + expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame()); + }); + it( + 'should report different frame instance when frame re-attaches', + async () => { + const {page, server} = getTestState(); + + const frame1 = await utils.attachFrame( + page, + 'frame1', + server.EMPTY_PAGE + ); + await page.evaluate(() => { + (globalThis as any).frame = document.querySelector('#frame1'); + (globalThis as any).frame.remove(); + }); + expect(frame1!.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + utils.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().length).toBe(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..bb393c69fb --- /dev/null +++ b/remote/test/puppeteer/test/src/golden-utils.ts @@ -0,0 +1,172 @@ +/** + * 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 {diffLines} from 'diff'; +import fs from 'fs'; +import jpeg from 'jpeg-js'; +import mime from 'mime'; +import path from 'path'; +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..95abfaa2c6 --- /dev/null +++ b/remote/test/puppeteer/test/src/headful.spec.ts @@ -0,0 +1,445 @@ +/** + * 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 fs from 'fs'; +import os from 'os'; +import path from 'path'; +import rimraf from 'rimraf'; +import {promisify} from 'util'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from '../../lib/cjs/puppeteer/node/Puppeteer.js'; +import { + describeChromeOnly, + getTestState, + itFailsWindows, +} from './mocha-utils.js'; + +const rmAsync = promisify(rimraf); +const mkdtempAsync = promisify(fs.mkdtemp); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +const extensionPath = path.join(__dirname, '../assets', 'simple-extension'); +const serviceWorkerExtensionPath = path.join( + __dirname, + '..', + 'assets', + 'serviceworkers', + 'extension' +); + +describeChromeOnly('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20 * 1000); + + let headfulOptions: 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(); + }); + itFailsWindows( + 'headless should be able to read cookies written by headful', + async () => { + /* Needs investigation into why but this fails consistently on Windows CI. */ + const {server, puppeteer} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await launchBrowser( + puppeteer, + Object.assign({userDataDir}, headfulOptions) + ); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate(() => { + 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 + await rmAsync(userDataDir).catch(() => {}); + expect(cookie).toBe('foo=true'); + } + ); + // TODO: Support OOOPIF. @see https://github.com/puppeteer/puppeteer/issues/2548 + xit('OOPIF: should report google.com frame', async () => { + const {server, puppeteer} = getTestState(); + + // https://google.com is isolated by default in Chromium embedder. + const browser = await launchBrowser(puppeteer, headfulOptions); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', (r: {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..4402f934c5 --- /dev/null +++ b/remote/test/puppeteer/test/src/idle_override.spec.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import {ElementHandle} from '../../lib/cjs/puppeteer/common/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..e9dd56e6cd --- /dev/null +++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts @@ -0,0 +1,145 @@ +/** + * 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 {TLSSocket} from 'tls'; +import { + Browser, + BrowserContext, +} from '../../lib/cjs/puppeteer/common/Browser.js'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.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.length).toBe(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().length).toBe(2); + // Make sure blocked iframe has functional execution context + // @see https://github.com/puppeteer/puppeteer/issues/2709 + expect(await page.frames()[0]!.evaluate('1 + 2')).toBe(3); + expect(await page.frames()[1]!.evaluate('2 + 3')).toBe(5); + }); +}); diff --git a/remote/test/puppeteer/test/src/injected.spec.ts b/remote/test/puppeteer/test/src/injected.spec.ts new file mode 100644 index 0000000000..b2fa5ed8b5 --- /dev/null +++ b/remote/test/puppeteer/test/src/injected.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import '../../lib/cjs/puppeteer/generated/injected.js'; +import {PUPPETEER_WORLD} from '../../lib/cjs/puppeteer/common/IsolatedWorld.js'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('InjectedUtil tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page} = getTestState(); + + const handle = await page + .mainFrame() + .worlds[PUPPETEER_WORLD].evaluate(() => { + return typeof InjectedUtil === 'object'; + }); + expect(handle).toBeTruthy(); + }); +}); 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..831c362e40 --- /dev/null +++ b/remote/test/puppeteer/test/src/input.spec.ts @@ -0,0 +1,408 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'path'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + describeFailsFirefox, +} from './mocha-utils.js'; + +const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describeFailsFirefox('input', function () { + it('should upload the file', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + const input = (await page.$('input'))!; + await page.evaluate((e: HTMLElement) => { + (globalThis 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'); + }); + }); + + describeFailsFirefox('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, puppeteer} = getTestState(); + + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const {page, puppeteer} = getTestState(); + + page.setDefaultTimeout(1); + let error!: Error; + await page.waitForFileChooser().catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const {page, puppeteer} = getTestState(); + + page.setDefaultTimeout(0); + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const {page} = getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({timeout: 0}), + page.evaluate(() => { + 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); + }); + }); + + describeFailsFirefox('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]), + new Promise(x => { + return page.once('metrics', x); + }), + ]); + 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!' + ); + }); + }); + + describeFailsFirefox('FileChooser.cancel', function () { + it('should cancel dialog', async () => { + const {page} = getTestState(); + + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(`<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!' + ); + }); + }); + + describeFailsFirefox('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..a54b59a6fb --- /dev/null +++ b/remote/test/puppeteer/test/src/jshandle.spec.ts @@ -0,0 +1,446 @@ +/** + * 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, + shortWaitForArrayToHaveAtLeastNElements, +} 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, isChrome} = 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_); + }); + if (isChrome) { + expect(error.message).toContain( + 'Could not serialize referenced object' + ); + } else { + expect(error.message).toContain('Object is not serializable'); + } + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const {page} = getTestState(); + + const aHandle = await page.evaluateHandle(() => { + 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' + ); + }); + }); + + describe('JSHandle.clickablePoint', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` + <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('JSHandle.click', function () { + it('should work', 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 + ]); + }); + }); +}); 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..0bcb2a57f4 --- /dev/null +++ b/remote/test/puppeteer/test/src/keyboard.spec.ts @@ -0,0 +1,528 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {KeyInput} from '../../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +describe('Keyboard', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should type into a textarea', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + const textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.focus(); + }); + const text = 'Hello world. I am the text that was typed!'; + await page.keyboard.type(text); + expect( + await page.evaluate(() => { + 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!'); + }); + 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 utils.attachFrame( + page, + 'emoji-test', + server.PREFIX + '/input/textarea.html' + ); + const frame = page.frames()[1]!; + const textarea = (await frame.$('textarea'))!; + await textarea.type('👹 Tokyo street Japan 🇯🇵'); + expect( + await frame.$eval('textarea', textarea => { + 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..8ecaa5f619 --- /dev/null +++ b/remote/test/puppeteer/test/src/launcher.spec.ts @@ -0,0 +1,1070 @@ +/** + * 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 expect from 'expect'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import rimraf from 'rimraf'; +import sinon from 'sinon'; +import {TLSSocket} from 'tls'; +import {promisify} from 'util'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import {Product} from '../../lib/cjs/puppeteer/common/Product.js'; +import { + getTestState, + itChromeOnly, + itFirefoxOnly, + itOnlyRegularInstall, +} from './mocha-utils.js'; +import utils from './utils.js'; + +const mkdtempAsync = promisify(fs.mkdtemp); +const readFileAsync = promisify(fs.readFile); +const rmAsync = promisify(rimraf); +const statAsync = promisify(fs.stat); +const writeFileAsync = promisify(fs.writeFile); + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const FIREFOX_TIMEOUT = 30 * 1000; + +describe('Launcher specs', function () { + if (getTestState().isFirefox) { + this.timeout(FIREFOX_TIMEOUT); + } + + describe('Puppeteer', function () { + describe('BrowserFetcher', function () { + it('should download and extract chrome linux binary', async () => { + const {server, puppeteer} = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + }); + const expectedRevision = '123456'; + let revisionInfo = browserFetcher.revisionInfo(expectedRevision); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile(req, res, '/chromium-linux.zip'); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('chrome'); + expect(!!browserFetcher.host()).toBe(true); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedRevision)).toBe(true); + + revisionInfo = (await browserFetcher.download(expectedRevision))!; + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedRevision, + ]); + await browserFetcher.remove(expectedRevision); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + it('should download and extract firefox linux binary', async () => { + const {server, puppeteer} = getTestState(); + + const downloadsFolder = await mkdtempAsync(TMP_FOLDER); + const browserFetcher = puppeteer.createBrowserFetcher({ + platform: 'linux', + path: downloadsFolder, + host: server.PREFIX, + product: 'firefox', + }); + const expectedVersion = '75.0a1'; + let revisionInfo = browserFetcher.revisionInfo(expectedVersion); + server.setRoute( + revisionInfo.url.substring(server.PREFIX.length), + (req, res) => { + server.serveFile( + req, + res, + `/firefox-${expectedVersion}.en-US.linux-x86_64.tar.bz2` + ); + } + ); + + expect(revisionInfo.local).toBe(false); + expect(browserFetcher.platform()).toBe('linux'); + expect(browserFetcher.product()).toBe('firefox'); + expect(await browserFetcher.canDownload('100000')).toBe(false); + expect(await browserFetcher.canDownload(expectedVersion)).toBe(true); + + revisionInfo = (await browserFetcher.download(expectedVersion))!; + expect(revisionInfo.local).toBe(true); + expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe( + 'FIREFOX LINUX BINARY\n' + ); + const expectedPermissions = os.platform() === 'win32' ? 0o666 : 0o755; + expect( + (await statAsync(revisionInfo.executablePath)).mode & 0o777 + ).toBe(expectedPermissions); + expect(await browserFetcher.localRevisions()).toEqual([ + expectedVersion, + ]); + await browserFetcher.remove(expectedVersion); + expect(await browserFetcher.localRevisions()).toEqual([]); + await rmAsync(downloadsFolder); + }); + }); + + describe('Browser.disconnect', function () { + it('should reject navigation when browser closes', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + server.setRoute('/one-style.css', () => {}); + const browser = await puppeteer.launch(defaultBrowserOptions); + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + }); + const page = await remote.newPage(); + const navigationPromise = page + .goto(server.PREFIX + '/one-style.html', {timeout: 60000}) + .catch(error_ => { + 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('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 mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + itChromeOnly('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-') + ); + process.env['PUPPETEER_TMP_DIR'] = testTmpDir; + + // Path should be empty before starting the browser. + expect(fs.readdirSync(testTmpDir).length).toEqual(0); + const browser = await puppeteer.launch(defaultBrowserOptions); + + // One profile folder should have been created at this moment. + const profiles = fs.readdirSync(testTmpDir); + expect(profiles.length).toEqual(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).length).toEqual(0); + // Restore env var + process.env['PUPPETEER_TMP_DIR'] = ''; + }); + itFirefoxOnly('userDataDir option restores preferences', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + + const prefsJSPath = path.join(userDataDir, 'prefs.js'); + const prefsJSContent = 'user_pref("browser.warnOnQuit", true)'; + await writeFileAsync(prefsJSPath, prefsJSContent); + + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + // Open a page to make sure its functional. + await browser.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + + expect(await readFileAsync(prefsJSPath, 'utf8')).toBe(prefsJSContent); + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir argument', async () => { + const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + itChromeOnly('userDataDir argument with non-existent dir', async () => { + const {isChrome, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + await rmAsync(userDataDir); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const browser = await puppeteer.launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await browser.close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir option should restore state', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + 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 + await rmAsync(userDataDir).catch(() => {}); + }); + it('userDataDir option should restore cookies', async () => { + const {server, puppeteer, defaultBrowserOptions} = getTestState(); + + const userDataDir = await mkdtempAsync(TMP_FOLDER); + const options = Object.assign({userDataDir}, defaultBrowserOptions); + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + 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 + await rmAsync(userDataDir).catch(() => {}); + }); + it('should return the default arguments', async () => { + const {isChrome, isFirefox, puppeteer} = getTestState(); + + if (isChrome) { + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + `--user-data-dir=${path.resolve('foo')}` + ); + } else if (isFirefox) { + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs()).toContain('--no-remote'); + if (os.platform() === 'darwin') { + expect(puppeteer.defaultArgs()).toContain('--foreground'); + } else { + expect(puppeteer.defaultArgs()).not.toContain('--foreground'); + } + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + '--profile' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo'); + } else { + expect(puppeteer.defaultArgs()).toContain('-headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '-headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + '-profile' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + path.resolve('foo') + ); + } + }); + it('should report the correct product', async () => { + const {isChrome, isFirefox, puppeteer} = getTestState(); + if (isChrome) { + expect(puppeteer.product).toBe('chrome'); + } else if (isFirefox) { + expect(puppeteer.product).toBe('firefox'); + } + }); + it('should work with no default arguments', async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions); + options.ignoreDefaultArgs = true; + const browser = await puppeteer.launch(options); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browser.close(); + }); + itChromeOnly( + 'should filter out ignored default arguments in Chrome', + async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]], + }) + ); + const spawnargs = browser.process()!.spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1); + await browser.close(); + } + ); + itFirefoxOnly( + 'should filter out ignored default argument in Firefox', + async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + + const defaultArgs = puppeteer.defaultArgs(); + const browser = await puppeteer.launch( + Object.assign({}, defaultBrowserOptions, { + // Only the first argument is fixed, others are optional. + ignoreDefaultArgs: [defaultArgs[0]!], + }) + ); + const spawnargs = browser.process()!.spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1); + await browser.close(); + } + ); + it('should have default URL when launching browser', async function () { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const browser = await puppeteer.launch(defaultBrowserOptions); + const pages = (await browser.pages()).map(page => { + 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.length).toBe(1); + const page = pages[0]!; + if (page.url() !== server.EMPTY_PAGE) { + await page.waitForNavigation(); + } + expect(page.url()).toBe(server.EMPTY_PAGE); + await browser.close(); + } + ); + it('should pass the timeout parameter to browser.waitForTarget', async () => { + const {puppeteer, defaultBrowserOptions} = getTestState(); + const options = Object.assign({}, defaultBrowserOptions, { + timeout: 1, + }); + let error!: Error; + await puppeteer.launch(options).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.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'); + }); + itChromeOnly( + 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', + async () => { + const {defaultBrowserOptions, puppeteer} = getTestState(); + const options = { + waitForInitialPage: false, + // This is needed to prevent Puppeteer from adding an initial blank page. + // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200 + ignoreDefaultArgs: true, + ...defaultBrowserOptions, + args: ['--no-startup-window'], + }; + const browser = await puppeteer.launch(options); + const pages = await browser.pages(); + expect(pages.length).toBe(0); + await browser.close(); + } + ); + }); + + describe('Puppeteer.launch', function () { + let productName!: Product; + + before(async () => { + const {puppeteer} = getTestState(); + productName = puppeteer._productName!; + }); + + after(async () => { + const {puppeteer} = getTestState(); + // @ts-expect-error launcher is a private property that users can't + // touch, but for testing purposes we need to reset it. + puppeteer._lazyLauncher = undefined; + puppeteer._productName = productName; + }); + + itOnlyRegularInstall('should be able to launch Chrome', async () => { + const {puppeteer} = getTestState(); + const browser = await puppeteer.launch({product: 'chrome'}); + const userAgent = await browser.userAgent(); + await browser.close(); + expect(userAgent).toContain('Chrome'); + }); + + 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([ + utils.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([ + utils.waitEvent(originalBrowser, 'disconnected'), + remoteBrowser.close(), + ]); + }); + it('should support ignoreHTTPSErrors option', async () => { + const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState(); + + const originalBrowser = await puppeteer.launch(defaultBrowserOptions); + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + ignoreHTTPSErrors: true, + }); + const page = await browser.newPage(); + let error!: 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.length).toEqual(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(utils.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 PUPPETEER_EXECUTABLE_PATH is set', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + process.env['PUPPETEER_EXECUTABLE_PATH'] = ''; + sandbox + .stub(process.env, 'PUPPETEER_EXECUTABLE_PATH') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => { + return sandbox.restore(); + }); + + it('its value is returned', async () => { + const {puppeteer} = getTestState(); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('SOME_CUSTOM_EXECUTABLE'); + }); + }); + + describe('when the product is chrome, platform is not darwin, and arch is arm64', () => { + describe('and the executable exists', () => { + itChromeOnly('returns /usr/bin/chromium-browser', async () => { + const {puppeteer} = getTestState(); + const osPlatformStub = sinon.stub(os, 'platform').returns('linux'); + const osArchStub = sinon.stub(os, 'arch').returns('arm64'); + const fsExistsStub = sinon.stub(fs, 'existsSync'); + fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(true); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('/usr/bin/chromium-browser'); + + osPlatformStub.restore(); + osArchStub.restore(); + fsExistsStub.restore(); + }); + describe('and PUPPETEER_EXECUTABLE_PATH is set', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + process.env['PUPPETEER_EXECUTABLE_PATH'] = ''; + sandbox + .stub(process.env, 'PUPPETEER_EXECUTABLE_PATH') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => { + return sandbox.restore(); + }); + + it('its value is returned', async () => { + const {puppeteer} = getTestState(); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).toEqual('SOME_CUSTOM_EXECUTABLE'); + }); + }); + }); + describe('and the executable does not exist', () => { + itChromeOnly( + 'does not return /usr/bin/chromium-browser', + async () => { + const {puppeteer} = getTestState(); + const osPlatformStub = sinon + .stub(os, 'platform') + .returns('linux'); + const osArchStub = sinon.stub(os, 'arch').returns('arm64'); + const fsExistsStub = sinon.stub(fs, 'existsSync'); + fsExistsStub.withArgs('/usr/bin/chromium-browser').returns(false); + + const executablePath = puppeteer.executablePath(); + + expect(executablePath).not.toEqual('/usr/bin/chromium-browser'); + + osPlatformStub.restore(); + osArchStub.restore(); + fsExistsStub.restore(); + } + ); + }); + }); + }); + }); + + describe('Browser target events', function () { + 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([ + utils.waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + utils.waitEvent(remoteBrowser1, 'disconnected'), + utils.waitEvent(originalBrowser, 'disconnected'), + originalBrowser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + } + ); + }); +}); diff --git a/remote/test/puppeteer/test/src/mocha-utils.ts b/remote/test/puppeteer/test/src/mocha-utils.ts new file mode 100644 index 0000000000..709a67158f --- /dev/null +++ b/remote/test/puppeteer/test/src/mocha-utils.ts @@ -0,0 +1,389 @@ +/** + * 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 expect from 'expect'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import rimraf from 'rimraf'; +import sinon from 'sinon'; +import { + Browser, + BrowserContext, +} from '../../lib/cjs/puppeteer/common/Browser.js'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; +import { + PuppeteerLaunchOptions, + PuppeteerNode, +} from '../../lib/cjs/puppeteer/node/Puppeteer.js'; +import puppeteer from '../../lib/cjs/puppeteer/puppeteer.js'; +import {TestServer} from '../../utils/testserver/lib/index.js'; +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'] || 'Chromium'; + +const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false; + +const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase(); +const isHeadless = headless === 'true' || headless === 'chrome'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'Chromium'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}'); +} catch (error) { + 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 === 'chrome' ? ('chrome' as const) : isHeadless, + dumpio: !!process.env['DUMPIO'], + }, + extraLaunchOptions +); + +(async (): Promise<void> => { + if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); + } else { + // TODO(jackfranklin): declare updateRevision in some form for the Firefox + // launcher. + if (product === 'firefox') { + // @ts-expect-error _updateRevision is defined on the FF launcher + // but not the Chrome one. The types need tidying so that TS can infer that + // properly and not error here. + await puppeteer._launcher._updateRevision(); + } + const executablePath = puppeteer.executablePath(); + if (!fs.existsSync(executablePath)) { + throw new Error( + `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` + ); + } + } +})(); + +declare module 'expect/build/types' { + interface Matchers<R> { + toBeGolden(x: string): R; + } +} + +const setupGoldenAssertions = (): void => { + const suffix = product.toLowerCase(); + const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`); + const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`); + if (fs.existsSync(OUTPUT_DIR)) { + rimraf.sync(OUTPUT_DIR); + } + 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: string; + puppeteerPath: string; +} +const state: Partial<PuppeteerTestState> = {}; + +export const itFailsFirefox = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) { + return xit(description, body); + } else { + return it(description, body); + } +}; + +export const itChromeOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome) { + return it(description, body); + } else { + return xit(description, body); + } +}; + +export const itHeadlessOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome && isHeadless === true) { + return it(description, body); + } else { + return xit(description, body); + } +}; + +export const itHeadfulOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isChrome && isHeadless === false) { + return it(description, body); + } else { + return xit(description, body); + } +}; + +export const itFirefoxOnly = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (isFirefox) { + return it(description, body); + } else { + return xit(description, body); + } +}; + +export const itOnlyRegularInstall = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (alternativeInstall || process.env['BINARY']) { + return xit(description, body); + } else { + return it(description, body); + } +}; + +export const itFailsWindowsUntilDate = ( + date: Date, + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32' && Date.now() < date.getTime()) { + // we are within the deferred time so skip the test + return xit(description, body); + } + + return it(description, body); +}; + +export const itFailsWindows = ( + description: string, + body: Mocha.Func +): Mocha.Test => { + if (os.platform() === 'win32') { + return xit(description, body); + } + return it(description, body); +}; + +export const describeFailsFirefox = ( + description: string, + body: (this: Mocha.Suite) => void +): void | Mocha.Suite => { + if (isFirefox) { + return xdescribe(description, body); + } else { + return describe(description, body); + } +}; + +export const describeChromeOnly = ( + description: string, + body: (this: Mocha.Suite) => void +): Mocha.Suite | void => { + if (isChrome) { + return describe(description, body); + } +}; + +if (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 === 'chrome' + ? '--headless=chrome' + : '--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, '../..')); + }, + ], + + 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]!); + } +}; + +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); + }); + } +}; 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..86f1a72f0d --- /dev/null +++ b/remote/test/puppeteer/test/src/mouse.spec.ts @@ -0,0 +1,263 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import os from 'os'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {KeyInput} from '../../lib/cjs/puppeteer/common/USKeyboardLayout.js'; + +interface Dimensions { + x: number; + y: number; + width: number; + height: number; +} + +function dimensions(): Dimensions { + const rect = document.querySelector('textarea')!.getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; +} + +describe('Mouse', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('should click the document', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + (globalThis 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}); + }); +}); 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..127cef3a9e --- /dev/null +++ b/remote/test/puppeteer/test/src/navigation.spec.ts @@ -0,0 +1,835 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import os from 'os'; +import {ServerResponse} from 'http'; +import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.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'); + } + }); + + function getExpectedSSLCertMessage(): string { + const {headless} = getTestState(); + /** + * If you are running this on pre-Catalina versions of macOS this will fail + * locally. Mac OSX Catalina outputs a different message than other + * platforms. See https://support.google.com/chrome/thread/18125056?hl=en + * for details. If you're running pre-Catalina Mac OSX this test will fail + * locally. + * In chrome-headless, the message is also different. + */ + return os.platform() === 'darwin' && headless !== 'chrome' + ? '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).toContain(getExpectedSSLCertMessage()); + } else { + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + } + + expect(requests.length).toBe(2); + expect(requests[0]!).toBe('request'); + expect(requests[1]!).toBe('requestfailed'); + }); + it('should fail when navigating to bad SSL after redirects', async () => { + const {page, server, httpsServer, isChrome} = getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + let error!: Error; + await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toContain(getExpectedSSLCertMessage()); + } 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, puppeteer} = 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(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const {page, server, puppeteer} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: 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(puppeteer.errors.TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const {page, server, puppeteer} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: 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(puppeteer.errors.TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const {page, server, puppeteer} = getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: 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(puppeteer.errors.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 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'), + ]); + const secondFetchResourceRequested = server.waitForRequest( + '/fetch-request-d.js' + ); + + // Navigate to a page which loads immediately and then does a bunch of + // requests via javascript's fetch method. + const navigationPromise = page.goto( + server.PREFIX + '/networkidle.html', + { + waitUntil: 'networkidle0', + } + ); + // Track when the navigation gets completed. + let navigationFinished = false; + navigationPromise.then(() => { + return (navigationFinished = true); + }); + + // Wait for the page's 'load' event. + await new Promise(fulfill => { + return page.once('load', fulfill); + }); + expect(navigationFinished).toBe(false); + + // Wait for the initial three resources to be requested. + await initialFetchResourcesRequested; + + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to initial requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + // Reset responses array + responses = []; + + // Wait for the second round to be requested. + await secondFetchResourceRequested; + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + const response = (await navigationPromise)!; + // Expect navigation to succeed. + expect(response.ok()).toBe(true); + } + ); + it('should not leak listeners during navigation', async () => { + const {page, server} = getTestState(); + + let warning = null; + const warningHandler: 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 () => { + 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 () => { + 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 !utils.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.length).toBe(1); + expect(requests[0]!.url()).toBe(dataURL); + } + ); + it( + 'should navigate to URL with hash and fire requests without hash', + async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !utils.isFavicon(request) && requests.push(request); + }); + const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!; + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + } + ); + it('should work with self requesting page', async () => { + const {page, server} = getTestState(); + + const response = (await page.goto(server.PREFIX + '/self-request.html'))!; + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it('should fail when navigating and show the url at the error message', async () => { + const {page, httpsServer} = getTestState(); + + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error!: 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 [request1, request2] = await Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]); + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate((url: string) => { + 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'); + 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 utils.waitEvent(page, 'frameattached'); + await new Promise<void>(fulfill => { + page.on('framenavigated', f => { + if (f === frame) { + fulfill(); + } + }); + }); + 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'); + + 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, chromium will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame2', server.EMPTY_PAGE), + utils.attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses: 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..b46c485555 --- /dev/null +++ b/remote/test/puppeteer/test/src/network.spec.ts @@ -0,0 +1,876 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, + itFailsFirefox, + itChromeOnly, + itFirefoxOnly, +} from './mocha-utils.js'; +import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js'; +import {HTTPResponse} from '../../lib/cjs/puppeteer/common/HTTPResponse.js'; +import {ServerResponse} from 'http'; + +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 !utils.isFavicon(request) && requests.push(request); + }); + (await page.goto(server.EMPTY_PAGE))!; + expect(requests.length).toBe(1); + }); + it('should fire for iframes', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !utils.isFavicon(request) && requests.push(request); + }); + (await page.goto(server.EMPTY_PAGE))!; + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(2); + }); + it('should fire for fetches', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !utils.isFavicon(request) && requests.push(request); + }); + (await page.goto(server.EMPTY_PAGE))!; + await page.evaluate(() => { + return fetch('/empty.html'); + }); + expect(requests.length).toBe(2); + }); + }); + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !utils.isFavicon(request) && requests.push(request); + }); + (await page.goto(server.EMPTY_PAGE))!; + expect(requests.length).toBe(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 !utils.isFavicon(request) && requests.push(request); + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests.length).toBe(1); + expect(requests[0]!.frame()).toBe(page.frames()[1]!); + }); + it('should work for fetch requests', async () => { + const {page, server} = getTestState(); + + (await page.goto(server.EMPTY_PAGE))!; + let requests: HTTPRequest[] = []; + page.on('request', request => { + return !utils.isFavicon(request) && requests.push(request); + }); + await page.evaluate(() => { + return fetch('/digits/1.png'); + }); + requests = requests.filter(request => { + return !request.url().includes('favicon'); + }); + expect(requests.length).toBe(1); + expect(requests[0]!.frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + itChromeOnly('should define Chrome as user agent header', async () => { + const {page, server} = getTestState(); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().headers()['user-agent']).toContain('Chrome'); + }); + + itFirefoxOnly('should define Firefox as user agent header', async () => { + const {page, server} = getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().headers()['user-agent']).toContain('Firefox'); + }); + }); + + describe('Response.headers', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + 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 ( + !utils.isFavicon(r.request()) && + responses.set(r.url().split('/').pop(), r) + ); + }); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + 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 ( + !utils.isFavicon(r) && responses.set(r.url().split('/').pop(), r) + ); + }); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', { + waitUntil: 'networkidle2', + }); + await page.evaluate(async () => { + 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(); + }); + let request!: HTTPRequest; + page.on('request', r => { + if (!utils.isFavicon(r)) { + request = r; + } + }); + await 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.length).toBe(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 !utils.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 = new Promise<HTTPResponse>(resolve => { + page.on('response', response => { + // Get the preflight response. + if ( + response.request().method() === 'OPTIONS' && + response.url() === url + ) { + resolve(response); + } + }); + }); + + // Trigger a request with a preflight. + await page.evaluate(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.length).toBe(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.length).toBe(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[0]!.method()).toBe('GET'); + expect(requests[0]!.response()).toBeTruthy(); + expect(requests[0]!.frame() === page.mainFrame()).toBe(true); + expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE); + }); + it('Page.Events.RequestServedFromCache', async () => { + const {page, server} = getTestState(); + + const cached: 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.length).toBe(1); + expect(responses[0]!.url()).toBe(server.EMPTY_PAGE); + expect(responses[0]!.status()).toBe(200); + expect(responses[0]!.ok()).toBe(true); + expect(responses[0]!.request()).toBeTruthy(); + const remoteAddress = responses[0]!.remoteAddress(); + // Either IPv6 or IPv4, depending on environment. + expect( + remoteAddress.ip!.includes('::1') || remoteAddress.ip === '127.0.0.1' + ).toBe(true); + expect(remoteAddress.port).toBe(server.PORT); + }); + + it('Page.Events.RequestFailed', async () => { + const {page, server, isChrome} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('css')) { + request.abort(); + } else { + request.continue(); + } + }); + const failedRequests: HTTPRequest[] = []; + page.on('requestfailed', request => { + return failedRequests.push(request); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests.length).toBe(1); + expect(failedRequests[0]!.url()).toContain('one-style.css'); + expect(failedRequests[0]!.response()).toBe(null); + expect(failedRequests[0]!.resourceType()).toBe('stylesheet'); + if (isChrome) { + expect(failedRequests[0]!.failure()!.errorText).toBe('net::ERR_FAILED'); + } else { + expect(failedRequests[0]!.failure()!.errorText).toBe( + 'NS_ERROR_FAILURE' + ); + } + expect(failedRequests[0]!.frame()).toBeTruthy(); + }); + it('Page.Events.RequestFinished', async () => { + const {page, server} = getTestState(); + + const requests: HTTPRequest[] = []; + page.on('requestfinished', request => { + return requests.push(request); + }); + (await page.goto(server.EMPTY_PAGE))!; + expect(requests.length).toBe(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + expect(requests[0]!.response()).toBeTruthy(); + expect(requests[0]!.frame() === page.mainFrame()).toBe(true); + expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE); + }); + it('should fire events in proper order', async () => { + const {page, server} = getTestState(); + + const events: 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.length).toBe(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); + }); + // This `itFailsFirefox` should be preserved in mozilla-central (Firefox). + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1748254 + // or https://github.com/puppeteer/puppeteer/pull/7846 + itFailsFirefox('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 = new Promise<HTTPResponse>(resolve => { + return page.on('response', response => { + return resolve(response); + }); + }); + page.evaluate(() => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/foo'); + xhr.send(); + }); + const subresourceResponse = await responsePromise; + expect(subresourceResponse.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Cross-origin set-cookie', async () => { + const {httpsServer, puppeteer, defaultBrowserOptions} = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/empty.html'); + + const setCookieString = 'hello=world'; + httpsServer.setRoute('/setcookie.html', (_req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('set-cookie', setCookieString); + res.end(); + }); + await page.goto(httpsServer.PREFIX + '/setcookie.html'); + const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; + const response = await new Promise<HTTPResponse>(resolve => { + page.on('response', response => { + if (response.url() === url) { + resolve(response); + } + }); + page.evaluate(src => { + 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..98bbe1fe96 --- /dev/null +++ b/remote/test/puppeteer/test/src/oopif.spec.ts @@ -0,0 +1,457 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + describeChromeOnly, +} from './mocha-utils.js'; +import { + Browser, + BrowserContext, +} from '../../lib/cjs/puppeteer/common/Browser.js'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; + +describeChromeOnly('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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await utils.attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(page.mainFrame().childFrames()).toHaveLength(2); + }); + it('should track navigations within OOP iframes', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/assets/frame.html' + ); + expect(frame.url()).toContain('/assets/frame.html'); + }); + it('should support OOP iframes becoming normal iframes again', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.isOOPFrame()).toBe(false); + expect(page.frames()).toHaveLength(2); + }); + it('should support frames within OOP frames', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const frame1Promise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + const frame2Promise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 2; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html' + ); + + const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]); + + expect( + await frame1.evaluate(() => { + 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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await utils.detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should support wait for navigation for transitions from local to OOPIF', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + const nav = frame.waitForNavigation(); + await utils.navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await nav; + expect(frame.isOOPFrame()).toBe(true); + await utils.detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should keep track of a frames OOP state', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await utils.navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.url()).toBe(server.EMPTY_PAGE); + }); + it('should support evaluating in oop iframes', async () => { + const {server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + _test = 'Test 123!'; + }); + const result = await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return window._test; + }); + expect(result).toBe('Test 123!'); + }); + it('should provide access to elements', async () => { + const {server, isHeadless, headless} = getTestState(); + + if (!isHeadless || headless === 'chrome') { + // TODO: this test is partially blocked on crbug.com/1334119. Enable test once + // the upstream is fixed. + // TLDR: when we dispatch events ot 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 utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame = await framePromise; + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + button.onclick = () => { + button.id = 'clicked'; + }; + document.body.appendChild(button); + }); + await page.evaluate(() => { + document.body.style.border = '150px solid black'; + document.body.style.margin = '250px'; + document.body.style.padding = '50px'; + }); + await frame.waitForSelector('#test-button', {visible: true}); + await frame.click('#test-button'); + await frame.waitForSelector('#clicked'); + }); + it('should report oopif frames', async () => { + const {server} = getTestState(); + + const frame = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + }); + + it('should wait for inner OOPIFs', async () => { + const {server} = getTestState(); + await page.goto(`http://mainframe:${server.PORT}/main-frame.html`); + const frame2 = await page.waitForFrame(frame => { + return frame.url().endsWith('inner-frame2.html'); + }); + expect(oopifs(context).length).toBe(2); + expect( + page.frames().filter(frame => { + return frame.isOOPFrame(); + }).length + ).toBe(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).length).toBe(1); + }); + it('should support frames within OOP iframes', async () => { + const {server} = getTestState(); + + const oopIframePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + const oopIframe = await oopIframePromise; + await utils.attachFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame1 = oopIframe.childFrames()[0]!; + expect(frame1.url()).toMatch(/empty.html$/); + await utils.navigateFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/oopif.html' + ); + expect(frame1.url()).toMatch(/oopif.html$/); + await frame1.goto( + server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document', + {waitUntil: 'load'} + ); + expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/); + await utils.detachFrame(oopIframe, 'frame1'); + expect(oopIframe.childFrames()).toHaveLength(0); + }); + + it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => { + const {server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await utils.attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await page.evaluate(() => { + document.body.style.border = '50px solid black'; + document.body.style.margin = '50px'; + document.body.style.padding = '50px'; + }); + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + document.body.appendChild(button); + }); + const button = (await frame.waitForSelector('#test-button', { + visible: true, + }))!; + const result = await button.clickablePoint(); + expect(result.x).toBeGreaterThan(150); // padding + margin + border left + expect(result.y).toBeGreaterThan(150); // padding + margin + border top + const resultBoxModel = (await button.boxModel())!; + for (const quad of [ + resultBoxModel.content, + resultBoxModel.border, + resultBoxModel.margin, + resultBoxModel.padding, + ]) { + for (const part of quad) { + expect(part.x).toBeGreaterThan(150); // padding + margin + border left + expect(part.y).toBeGreaterThan(150); // padding + margin + border top + } + } + const resultBoundingBox = (await button.boundingBox())!; + expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left + expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top + }); + + it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => { + const {server, puppeteer} = getTestState(); + + const frame = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context).length).toBe(1); + expect(page.frames().length).toBe(2); + + const browserURL = 'http://127.0.0.1:21222'; + const browser1 = await puppeteer.connect({browserURL}); + const target = await browser1.waitForTarget(target => { + 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 utils.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..82dbd5c24f --- /dev/null +++ b/remote/test/puppeteer/test/src/page.spec.ts @@ -0,0 +1,2293 @@ +/** + * 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 fs from 'fs'; +import {ServerResponse} from 'http'; +import path from 'path'; +import sinon from 'sinon'; +import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.js'; +import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js'; +import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import { + getTestState, + itFailsFirefox, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import utils, {attachFrame, 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'), + utils.waitEvent(page, 'load'), + ]); + }); + }); + + describe('removing and adding event handlers', () => { + it('should correctly fire event handlers as they are added and then removed', async () => { + const {page, server} = getTestState(); + + const handler = sinon.spy(); + const onResponse = (response: {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} = getTestState(); + + let error!: Error; + page.on('error', err => { + return (error = err); + }); + page.goto('chrome://crash').catch(() => {}); + await waitEvent(page, 'error'); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe('Page.Events.Popup', function () { + it('should work', async () => { + const {page} = getTestState(); + + const [popup] = await Promise.all([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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([ + new Promise<Page>(x => { + return page.once('popup', x); + }), + 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, isHeadless} = getTestState(); + + // TODO: re-enable this test in headful once crbug.com/1324480 rolls out. + if (!isHeadless) { + return; + } + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + (globalThis 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(); + + // Instantiate an object + await page.evaluate(() => { + return ((globalThis as any).set = new Set(['hello', 'world'])); + }); + const prototypeHandle = await page.evaluateHandle(() => { + return Set.prototype; + }); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => { + return objects.length; + }, objectsHandle); + expect(count).toBe(1); + const values = await page.evaluate(objects => { + return Array.from(objects[0]!.values()); + }, objectsHandle); + expect(values).toEqual(['hello', 'world']); + }); + it('should work for non-blank page', async () => { + const {page, server} = getTestState(); + + // Instantiate an object + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return ((globalThis as any).set = new Set(['hello', 'world'])); + }); + const prototypeHandle = await page.evaluateHandle(() => { + return Set.prototype; + }); + const objectsHandle = await page.queryObjects(prototypeHandle); + const count = await page.evaluate(objects => { + return objects.length; + }, objectsHandle); + expect(count).toBe(1); + }); + 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(); + + let message!: ConsoleMessage; + page.once('console', m => { + return (message = m); + }); + await Promise.all([ + page.evaluate(() => { + return console.log('hello', 5, {foo: 'bar'}); + }), + waitEvent(page, 'console'), + ]); + expect(message.text()).toEqual('hello 5 JSHandle@object'); + expect(message.type()).toEqual('log'); + expect(message.args()).toHaveLength(3); + expect(message.location()).toEqual({ + url: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), + }); + + expect(await message.args()[0]!.jsonValue()).toEqual('hello'); + expect(await message.args()[1]!.jsonValue()).toEqual(5); + expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'}); + }); + it('should work for different console API calls', async () => { + const {page} = getTestState(); + + const messages: 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'); + 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(['timeEnd', 'trace', 'dir', 'warning', 'error', 'log']); + expect(messages[0]!.text()).toContain('calling console.time'); + expect( + messages.slice(1).map(msg => { + return msg.text(); + }) + ).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should not fail for window object', async () => { + const {page} = getTestState(); + + let message!: ConsoleMessage; + page.once('console', msg => { + return (message = msg); + }); + await Promise.all([ + page.evaluate(() => { + return console.error(window); + }), + waitEvent(page, 'console'), + ]); + expect(message.text()).toBe('JSHandle@object'); + }); + it('should trigger correct Log', async () => { + const {page, server, isChrome} = getTestState(); + + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(async (url: string) => { + 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 = new Promise<{metrics: Metrics; title: string}>( + fulfill => { + return page.once('metrics', fulfill); + } + ); + 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(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with predicate', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(request => { + return request.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page, puppeteer} = getTestState(); + + let error!: Error; + await page + .waitForRequest( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const {page, puppeteer} = getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with no timeout', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForResponse', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page, puppeteer} = getTestState(); + + let error!: Error; + await page + .waitForResponse( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect default timeout', async () => { + const {page, puppeteer} = getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should work with predicate', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(response => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with async predicate', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(async response => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with no timeout', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForNetworkIdle', function () { + it('should work', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + let res; + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle().then(r => { + res = r; + return Date.now(); + }), + page + .evaluate(() => { + 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, puppeteer} = getTestState(); + let error!: Error; + await page.waitForNetworkIdle({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should respect idleTime', async () => { + const {page, server} = getTestState(); + await page.goto(server.EMPTY_PAGE); + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle({idleTime: 10}).then(() => { + 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(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(result).toBe(undefined); + }); + }); + + 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 utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + await utils.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(); + + let error!: Error; + page.once('pageerror', e => { + return (error = e); + }); + await Promise.all([ + page.goto(server.PREFIX + '/error.html'), + waitEvent(page, 'pageerror'), + ]); + expect(error.message).toContain('Fancy'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const {page, server} = getTestState(); + + expect( + await page.evaluate(() => { + 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'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const {page, server, puppeteer} = getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).not.toContain('iPhone'); + await page.setUserAgent(puppeteer.devices['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, puppeteer} = 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(puppeteer.errors.TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const {page, server, puppeteer} = getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + it('should await resources to load', async () => { + const {page, server} = getTestState(); + + const imgPath = '/img.png'; + let imgResponse!: 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 (globalThis as any).__es6injected; + }) + ).toBe(42); + }); + + it('should work with a path and type=module', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, '../assets/es6/es6pathimport.js'), + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect( + await page.evaluate(() => { + return (globalThis as any).__es6injected; + }) + ).toBe(42); + }); + + it('should work with a content and type=module', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + content: `import num from '/es6/es6module.js';window.__es6injected = num;`, + type: 'module', + }); + await page.waitForFunction('window.__es6injected'); + expect( + await page.evaluate(() => { + return (globalThis as any).__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 + xit('should throw when added with content to the CSP page', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: 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 => { + 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('printing to PDF', function () { + it('can print to PDF and save to file', async () => { + // Printing to pdf is currently only supported in headless + const {isHeadless, page} = getTestState(); + + if (!isHeadless) { + return; + } + + const outputFile = __dirname + '/../assets/output.pdf'; + await page.pdf({path: outputFile}); + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + }); + + it('can print to PDF and stream the result', async () => { + // Printing to pdf is currently only supported in headless + const {isHeadless, page} = getTestState(); + + if (!isHeadless) { + return; + } + + const stream = await page.createPDFStream(); + let size = 0; + for await (const chunk of stream) { + size += chunk.length; + } + expect(size).toBeGreaterThan(0); + }); + + // This test should be skipped in mozilla-central (Firefox). + // It intermittently makes the whole test suite fail. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255 + itFailsFirefox('should respect timeout', async () => { + const {isHeadless, page, server, puppeteer} = getTestState(); + if (!isHeadless) { + return; + } + + await page.goto(server.PREFIX + '/pdf.html'); + + let error!: Error; + await page.pdf({timeout: 1}).catch(_error => { + return (error = _error); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + }); + }); + + describe('Page.title', function () { + it('should return the page title', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function () { + it('should select single option', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => { + 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.length).toEqual(1); + }); + it('should return [] on no values', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select'); + expect(result).toEqual([]); + }); + it('should deselect all options when passed no values for a multiple select', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => { + 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 () { + itFailsFirefox('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 = new Promise(x => { + return newPage.on('close', x); + }); + 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 = new Promise(x => { + return newPage.on('close', x); + }); + await newPage.close(); + await closedPromise; + }); + }); + + describe('Page.browser', function () { + it('should return the correct browser instance', async () => { + const {page, browser} = getTestState(); + + expect(page.browser()).toBe(browser); + }); + }); + + describe('Page.browserContext', function () { + it('should return the correct browser context instance', async () => { + const {page, context} = getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); + + describe('Page.client', function () { + it('should return the client instance', async () => { + const {page} = getTestState(); + expect(page._client()).toBeInstanceOf(CDPSession); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/proxy.spec.ts b/remote/test/puppeteer/test/src/proxy.spec.ts new file mode 100644 index 0000000000..eb71905355 --- /dev/null +++ b/remote/test/puppeteer/test/src/proxy.spec.ts @@ -0,0 +1,241 @@ +/** + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import expect from 'expect'; +import http from 'http'; +import os from 'os'; +import { + getTestState, + describeFailsFirefox, + itFailsWindows, +} from './mocha-utils.js'; +import type {Server, IncomingMessage, ServerResponse} from 'http'; +import type {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; +import type {AddressInfo} from 'net'; +import {TestServer} from '../../utils/testserver/lib/index.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}`; +} + +describeFailsFirefox('request proxy', () => { + let browser: Browser; + let proxiedRequestUrls: string[]; + let proxyServer: Server; + let proxyServerUrl: string; + const defaultArgs = [ + '--disable-features=NetworkTimeServiceQuerying', // We disable this in tests so that proxy-related tests don't intercept queries from this service in headful. + ]; + + beforeEach(() => { + proxiedRequestUrls = []; + + proxyServer = http + .createServer( + ( + originalRequest: IncomingMessage, + originalResponse: ServerResponse + ) => { + proxiedRequestUrls.push(originalRequest.url as string); + + const proxyRequest = http.request( + originalRequest.url as string, + { + method: originalRequest.method, + headers: originalRequest.headers, + }, + proxyResponse => { + originalResponse.writeHead( + proxyResponse.statusCode as number, + proxyResponse.headers + ); + proxyResponse.pipe(originalResponse, {end: true}); + } + ); + + originalRequest.pipe(proxyRequest, {end: true}); + } + ) + .listen(); + + proxyServerUrl = `http://${HOSTNAME}:${ + (proxyServer.address() as AddressInfo).port + }`; + }); + + afterEach(async () => { + await browser.close(); + + await new Promise((resolve, reject) => { + proxyServer.close(error => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); + }); + + it('should proxy requests when configured', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + }); + + it('should respect proxy bypass list', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + describe('in incognito browser context', () => { + it('should proxy requests when configured at browser level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + }); + + it('should respect proxy bypass list when configured at browser level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + + /** + * See issues #7873, #7719, and #7698. + */ + itFailsWindows( + 'should proxy requests when configured at context level', + async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + }); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + } + ); + + it('should respect proxy bypass list when configured at context level', async () => { + const {puppeteer, defaultBrowserOptions, server} = getTestState(); + const emptyPageUrl = getEmptyPageUrl(server); + + browser = await puppeteer.launch({ + ...defaultBrowserOptions, + args: defaultArgs, + }); + + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + proxyBypassList: [new URL(emptyPageUrl).host], + }); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + + expect(proxiedRequestUrls).toEqual([]); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts new file mode 100644 index 0000000000..30e5d69254 --- /dev/null +++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts @@ -0,0 +1,160 @@ +/** + * 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 {ElementHandle} from '../../lib/cjs/puppeteer/common/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('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')).length).toBe(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')).length + ).toBe(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.length).toBe(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`)).length).toBe(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`)).length).toBe(0); + }); + }); + }); +}); 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..7eeab8e44e --- /dev/null +++ b/remote/test/puppeteer/test/src/queryselector.spec.ts @@ -0,0 +1,501 @@ +/** + * 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 {CustomQueryHandler} from '../../lib/cjs/puppeteer/common/QueryHandler.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 () => { + 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.length).toBe(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.length).toBe(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.length).toBe(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = getTestState(); + + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div></div><div></div>'); + const elements = await page.$x('/html/body/div'); + expect(elements.length).toBe(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<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.length).toBe(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.length).toBe(0); + }); + }); + + describe('ElementHandle.$x', function () { + it('should query existing element', async () => { + const {page, server} = getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<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(() => { + const {puppeteer} = getTestState(); + puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + const {puppeteer} = getTestState(); + expect( + puppeteer.customQueryHandlerNames().includes('allArray') + ).toBeTruthy(); + }); + it('$$ should query existing elements', async () => { + const {page} = getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + const html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements.length).toBe(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.length).toBe(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 () => { + 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..a0c6da6fd6 --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts @@ -0,0 +1,981 @@ +/** + * Copyright 2021 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js'; +import { + ActionResult, + HTTPRequest, + InterceptResolutionAction, +} from '../../lib/cjs/puppeteer/common/HTTPRequest.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.length).toBe(1); + expect(actionResults[0]!).toBe(expectedAction); + expect(response!.ok()).toBe(true); + }); + } + + it('should intercept', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (utils.isFavicon(request)) { + request.continue({}, 0); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame()!.url()).toBe('about:blank'); + request.continue({}, 0); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response!.ok()).toBe(true); + expect(response!.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const {page, server} = getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', request => { + 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 (!utils.isFavicon(request)) { + requests.push(request); + } + request.continue({}, 0); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1]!.url()).toContain('/one-style.css'); + expect(requests[1]!.headers()['referer']).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const {page, server} = getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({name: 'foo', value: 'bar'}); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', request => { + 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); + }); + let failedRequest!: HTTPRequest; + page.on('requestfailed', request => { + return (failedRequest = request); + }); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure()!.errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const {page, server} = getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', request => { + 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.length).toBe(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response!.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0]!.url()).toContain('/non-existing-page.html'); + expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]!; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + request.continue({}, 0); + if (!utils.isFavicon(request)) { + requests.push(request); + } + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (_req, res) => { + 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.length).toBe(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain.length).toBe(3); + expect(redirectChain[0]!.url()).toContain('/one-style.css'); + expect(redirectChain[2]!.url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const {page, server, isChrome} = getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', request => { + if (request.url().includes('non-existing-2')) { + request.abort('failed', 0); + } else { + request.continue({}, 0); + } + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + 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 (utils.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.length).toBe(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !utils.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.length).toBe(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + const requests: 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.length).toBe(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const {page, server} = getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', request => { + 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.length).toBe(2); + expect(requests[1]!.response()!.status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const {page, server} = getTestState(); + + await page.setContent('<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 utils.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 = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache enabled', async () => { + const {page, server} = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue({}, 0); + }); + + const cached = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached.length).toBe(1); + }); + it('should load fonts if cache enabled', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + 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); + }); + let consoleMessage!: ConsoleMessage; + page.on('console', msg => { + return (consoleMessage = msg); + }); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + request.continue({method: 'POST'}, 0); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + 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().length).toBe(1); + expect(response!.request().redirectChain()[0]!.url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response!.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking binary responses', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + request.respond( + { + contentType: 'image/png', + body: imageBuffer, + }, + 0 + ); + }); + await page.evaluate(PREFIX => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise(fulfill => { + 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..cfbe67e958 --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts @@ -0,0 +1,917 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import utils from './utils.js'; +import expect from 'expect'; +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {HTTPRequest} from '../../lib/cjs/puppeteer/common/HTTPRequest.js'; +import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.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 (utils.isFavicon(request)) { + request.continue(); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.headers()['accept']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame()!.url()).toBe('about:blank'); + request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const {page, server} = getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', request => { + 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 (!utils.isFavicon(request)) { + requests.push(request); + } + request.continue(); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1]!.url()).toContain('/one-style.css'); + expect(requests[1]!.headers()['referer']).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const {page, server} = getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({name: 'foo', value: 'bar'}); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', request => { + 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'); + }); + let failedRequest!: HTTPRequest; + page.on('requestfailed', request => { + return (failedRequest = request); + }); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure()!.errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const {page, server} = getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', request => { + 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.length).toBe(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0]!.url()).toContain('/non-existing-page.html'); + expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]!; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + request.continue(); + if (!utils.isFavicon(request)) { + requests.push(request); + } + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (_req, res) => { + 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.length).toBe(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain.length).toBe(3); + expect(redirectChain[0]!.url()).toContain('/one-style.css'); + expect(redirectChain[2]!.url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const {page, server, isChrome} = getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', request => { + if (request.url().includes('non-existing-2')) { + request.abort(); + } else { + request.continue(); + } + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + 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 (utils.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.length).toBe(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !utils.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.length).toBe(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + const requests: 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.length).toBe(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const {page, server} = getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', request => { + 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.length).toBe(2); + expect(requests[1]!.response()!.status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const {page, server} = getTestState(); + + await page.setContent('<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 utils.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 = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached.length).toBe(0); + }); + it('should cache if cache enabled', async () => { + const {page, server} = getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue(); + }); + + const cached = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached.length).toBe(1); + }); + it('should load fonts if cache enabled', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + 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}); + }); + let consoleMessage!: ConsoleMessage; + page.on('console', msg => { + return (consoleMessage = msg); + }); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + request.continue({method: 'POST'}); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + 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().length).toBe(1); + expect(response.request().redirectChain()[0]!.url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking multiple headers with same key', async () => { + const {page, server} = getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + request.respond({ + status: 200, + headers: { + foo: 'bar', + arr: ['1', '2'], + 'set-cookie': ['first=1', 'second=2'], + }, + body: 'Hello world', + }); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + const cookies = await page.cookies(); + const firstCookie = cookies.find(cookie => { + 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..1d0b795a3b --- /dev/null +++ b/remote/test/puppeteer/test/src/screenshot.spec.ts @@ -0,0 +1,399 @@ +/** + * 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, + itHeadfulOnly, + itChromeOnly, +} 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', + }); + // TODO (@jackfranklin): improve the screenshot types. + // - if we pass encoding: 'base64', it returns a string + // - else it returns a buffer. + // If we can fix that we can avoid this "as string" here. + expect(Buffer.from(screenshot as string, 'base64')).toBeGolden( + 'screenshot-sanity.png' + ); + }); + itHeadfulOnly('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'); + }); + itChromeOnly('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..71501ffeeb --- /dev/null +++ b/remote/test/puppeteer/test/src/target.spec.ts @@ -0,0 +1,377 @@ +/** + * 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 {ServerResponse} from 'http'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import {Target} from '../../lib/cjs/puppeteer/common/Target.js'; +import { + getTestState, + itFailsFirefox, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import utils from './utils.js'; +const {waitEvent} = utils; + +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.length).toBe(1); + expect(allPages).toContain(page); + }); + it('should contain browser target', async () => { + const {browser} = getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find(target => { + 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(); + }); + // This test should be skipped in mozilla-central (Firefox). + // It intermittently makes some tests fail and triggers errors in the test hooks. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1748255 + itFailsFirefox('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 closePagePromise = new Promise(fulfill => { + return context.once('targetdestroyed', target => { + return fulfill(target.page()); + }); + }); + await otherPage!.close(); + expect(await closePagePromise).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 = new Promise<Target>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target); + }); + }); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + expect((await createdTarget).type()).toBe('service_worker'); + expect((await createdTarget).url()).toBe( + server.PREFIX + '/serviceworkers/empty/sw.js' + ); + + const destroyedTarget = new Promise(fulfill => { + return context.once('targetdestroyed', target => { + return fulfill(target); + }); + }); + 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 = new Promise<Target>(fulfill => { + return context.once('targetchanged', target => { + return fulfill(target); + }); + }); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = new Promise(fulfill => { + return context.once('targetchanged', target => { + return fulfill(target); + }); + }); + 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 = new Promise<Target>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target); + }); + }); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = new Promise<Target>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target); + }); + }); + 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.removeListener('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([ + new Promise<Target>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target); + }); + }), + page.goto(server.PREFIX + '/popup/window-open.html'), + ]); + expect((await createdTarget.page())!.url()).toBe( + server.PREFIX + '/popup/popup.html' + ); + expect(createdTarget.opener()).toBe(page.target()); + expect(page.target().opener()).toBeUndefined(); + }); + + describe('Browser.waitForTarget', () => { + it('should wait for a target', async () => { + const {browser, puppeteer, 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 puppeteer.errors.TimeoutError) { + console.error(error); + } else { + throw error; + } + }); + const page = await browser.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof puppeteer.errors.TimeoutError) { + console.error(error); + } else { + throw error; + } + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const {browser, server, puppeteer} = getTestState(); + + let error!: Error; + await browser + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.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..bfdfb3bbc5 --- /dev/null +++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts @@ -0,0 +1,53 @@ +/** + * 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('Touchscreen', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should tap the button', async () => { + const {puppeteer, page, server} = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + await page.tap('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should report touches', async () => { + const {puppeteer, page, server} = getTestState(); + const iPhone = puppeteer.devices['iPhone 6']!; + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/touches.html'); + const button = (await page.$('button'))!; + await button.tap(); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toEqual(['Touchstart: 0', 'Touchend: 0']); + }); +}); 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..2a7a4675cb --- /dev/null +++ b/remote/test/puppeteer/test/src/tracing.spec.ts @@ -0,0 +1,155 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import expect from 'expect'; +import {getTestState, describeChromeOnly} from './mocha-utils.js'; +import {Browser} from '../../lib/cjs/puppeteer/common/Browser.js'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; + +describeChromeOnly('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..8f11d78b07 --- /dev/null +++ b/remote/test/puppeteer/test/src/utils.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 expect from 'expect'; +import path from 'path'; +import {Frame} from '../../lib/cjs/puppeteer/common/Frame.js'; +import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; +import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; +import {compare} from './golden-utils.js'; + +const PROJECT_ROOT = path.join(__dirname, '..', '..'); + +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 void 0; + }, + }; + } 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 = ( + emitter: EventEmitter, + eventName: string, + predicate: (event: any) => boolean = () => { + return true; + } +): Promise<any> => { + return new Promise(fulfill => { + emitter.on(eventName, function listener(event: any) { + if (!predicate(event)) { + return; + } + emitter.off(eventName, listener); + fulfill(event); + }); + }); +}; + +/** + * @deprecated Use exports directly. + */ +export default { + extendExpectWithToBeGolden, + waitEvent, + dumpFrames, + navigateFrame, + isFavicon, + attachFrame, + projectRoot, + detachFrame, +}; 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..0535a49e4e --- /dev/null +++ b/remote/test/puppeteer/test/src/waittask.spec.ts @@ -0,0 +1,823 @@ +/** + * 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 '../../lib/cjs/puppeteer/util/ErrorLike.js'; +import { + 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('window.__FOO === 1'); + await page.evaluate(() => { + return ((globalThis as any).__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 true; + }); + }); + it('should poll on interval', async () => { + const {page} = getTestState(); + + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction( + () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling, + } + ) + .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; + expect(Date.now() - startTime).not.toBeLessThan(polling / 2); + }); + it('should poll on interval async', async () => { + const {page} = getTestState(); + let success = false; + const startTime = Date.now(); + const polling = 100; + const watchdog = page + .waitForFunction( + async () => { + return (globalThis as any).__FOO === 'hit'; + }, + { + polling, + } + ) + .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; + 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 on bad polling value', async () => { + const {page} = getTestState(); + + let error!: Error; + try { + await page.waitForFunction( + () => { + return !!document.body; + }, + { + polling: 'unknown', + } + ); + } catch (error_) { + if (isErrorLike(error_)) { + error = error_ as Error; + } + } + expect(error?.message).toContain('polling'); + }); + 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, puppeteer} = getTestState(); + + let error!: Error; + await page.waitForFunction('false', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error?.message).toContain('waiting for function failed: timeout'); + }); + it('should respect default timeout', async () => { + const {page, puppeteer} = getTestState(); + + page.setDefaultTimeout(1); + let error!: Error; + await page.waitForFunction('false').catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error?.message).toContain('waiting for function failed: timeout'); + }); + it('should disable timeout when its set to 0', async () => { + const {page} = getTestState(); + + const watchdog = page.waitForFunction( + () => { + (globalThis 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('globalThis.__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; + }); + }); + + 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 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 visible', async () => { + const {page} = getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('div', {visible: true}) + .then(() => { + return (divFound = true); + }); + await page.setContent( + `<div 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('div#inner', {visible: true}) + .then(() => { + return (divVisible = true); + }); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div id="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 style='display: block;'></div>`); + const waitForSelector = page + .waitForSelector('div', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + await page.waitForSelector('div'); // 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 style='display: block;'></div>`); + const waitForSelector = page + .waitForSelector('div', {hidden: true}) + .then(() => { + return (divHidden = true); + }); + await page.waitForSelector('div'); // 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></div>`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('div', {hidden: true}) + .then(() => { + return (divRemoved = true); + }); + await page.waitForSelector('div'); // 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('non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + it('should respect timeout', async () => { + const {page, puppeteer} = getTestState(); + + let error!: Error; + await page.waitForSelector('div', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error?.message).toContain( + 'waiting for selector `div` failed: timeout' + ); + }); + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const {page} = getTestState(); + + await page.setContent(`<div></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` to be hidden failed: timeout' + ); + }); + + it('should respond to node attribute mutation', async () => { + const {page} = getTestState(); + + let divFound = false; + const waitForSelector = page.waitForSelector('.zombo').then(() => { + 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'); + // The extension is ts here as Mocha maps back via sourcemaps. + expect(error?.stack).toContain('waittask.spec.ts'); + }); + }); + + describe('Frame.waitForXPath', function () { + const addElement = (tag: 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, puppeteer} = getTestState(); + + let error!: Error; + await page.waitForXPath('//div', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); + expect(error?.message).toContain( + 'waiting for selector `.//div` failed: timeout 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;'></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('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..c45febc3d4 --- /dev/null +++ b/remote/test/puppeteer/test/src/worker.spec.ts @@ -0,0 +1,135 @@ +/** + * 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 '../../lib/cjs/puppeteer/common/ConsoleMessage.js'; +import {WebWorker} from '../../lib/cjs/puppeteer/common/WebWorker.js'; +import { + describeFailsFirefox, + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describeFailsFirefox('Workers', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + it('Page.workers', async () => { + const {page, server} = getTestState(); + + await Promise.all([ + new Promise(x => { + return page.once('workercreated', x); + }), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]!; + expect(worker?.url()).toContain('worker.js'); + + expect( + await worker?.evaluate(() => { + return (globalThis as any).workerFunction(); + }) + ).toBe('worker function result'); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers().length).toBe(0); + }); + it('should emit created and destroyed events', async () => { + const {page} = getTestState(); + + const workerCreatedPromise = new Promise<WebWorker>(x => { + return page.once('workercreated', x); + }); + 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 = new Promise(x => { + return page.once('workerdestroyed', x); + }); + 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 = new Promise<ConsoleMessage>(x => { + return page.on('console', x); + }); + 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().length).toBe(4); + expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const {page} = getTestState(); + + const workerCreatedPromise = new Promise<WebWorker>(x => { + return page.once('workercreated', x); + }); + 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 = new Promise<Error>(x => { + return page.on('pageerror', x); + }); + 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..962644f958 --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false, + "module": "CommonJS", + "outDir": "build", + "rootDir": "src" + }, + "include": ["src"], + "references": [ + {"path": "../tsconfig.lib.json"}, + {"path": "../utils/testserver/tsconfig.json"} + ] +} diff --git a/remote/test/puppeteer/tsconfig.base.json b/remote/test/puppeteer/tsconfig.base.json new file mode 100644 index 0000000000..3473213af7 --- /dev/null +++ b/remote/test/puppeteer/tsconfig.base.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "allowJs": true, + "alwaysStrict": true, + "checkJs": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "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/tsconfig.json b/remote/test/puppeteer/tsconfig.json new file mode 100644 index 0000000000..2e154c1f46 --- /dev/null +++ b/remote/test/puppeteer/tsconfig.json @@ -0,0 +1,13 @@ +/** + * This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation. + * For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`. + * See the details in CONTRIBUTING.md that describes our TypeScript setup. + */ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "esnext" + }, + "include": ["src"] +} diff --git a/remote/test/puppeteer/tsconfig.lib.json b/remote/test/puppeteer/tsconfig.lib.json new file mode 100644 index 0000000000..6f668b4e54 --- /dev/null +++ b/remote/test/puppeteer/tsconfig.lib.json @@ -0,0 +1,16 @@ +/** + * This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation. + * For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`. + * See the details in CONTRIBUTING.md that describes our TypeScript setup. + */ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "composite": true + }, + "references": [ + {"path": "src/tsconfig.esm.json"}, + {"path": "src/tsconfig.cjs.json"} + ], + "exclude": ["**/*"] +} diff --git a/remote/test/puppeteer/typescript-if-required.js b/remote/test/puppeteer/typescript-if-required.js new file mode 100644 index 0000000000..e1a1e8bbdd --- /dev/null +++ b/remote/test/puppeteer/typescript-if-required.js @@ -0,0 +1,70 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const child_process = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const {promisify} = require('util'); + +const exec = promisify(child_process.exec); +const fsAccess = promisify(fs.access); + +const fileExists = async filePath => { + return fsAccess(filePath) + .then(() => { + return true; + }) + .catch(() => { + return false; + }); +}; +/* + + * Now Puppeteer is built with TypeScript, we need to ensure that + * locally we have the generated output before trying to install. + * + * For users installing puppeteer this is fine, they will have the + * generated lib/ directory as we ship it when we publish to npm. + * + * However, if you're cloning the repo to contribute, you won't have the + * generated lib/ directory so this script checks if we need to run + * TypeScript first to ensure the output exists and is in the right + * place. + */ +async function compileTypeScript() { + return exec('npm run build').catch(error => { + console.error('Error running TypeScript', error); + process.exit(1); + }); +} + +async function compileTypeScriptIfRequired() { + const libPath = path.join(__dirname, 'lib'); + const libExists = await fileExists(libPath); + if (libExists) { + return; + } + + console.log('Puppeteer:', 'Compiling TypeScript...'); + await compileTypeScript(); +} + +// It's being run as node typescript-if-required.js, not require('..') +if (require.main === module) { + compileTypeScriptIfRequired(); +} + +module.exports = compileTypeScriptIfRequired; diff --git a/remote/test/puppeteer/utils/bisect.js b/remote/test/puppeteer/utils/bisect.js new file mode 100755 index 0000000000..bbec3d2431 --- /dev/null +++ b/remote/test/puppeteer/utils/bisect.js @@ -0,0 +1,314 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const URL = require('url'); +const debug = require('debug'); +const pptr = require('..'); +const browserFetcher = pptr.createBrowserFetcher(); +const path = require('path'); +const fs = require('fs'); +const {fork, spawn, execSync} = require('child_process'); + +const COLOR_RESET = '\x1b[0m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; +const COLOR_YELLOW = '\x1b[33m'; + +const argv = require('minimist')(process.argv.slice(2), {}); + +const help = ` +Usage: + node bisect.js --good <revision> --bad <revision> <script> + +Parameters: + --good revision that is known to be GOOD, defaults to the chromium revision in src/revision.ts in the main branch + --bad revision that is known to be BAD, defaults to the chromium revision in src/revision.ts + --no-cache do not keep downloaded Chromium revisions + --unit-test pattern that identifies a unit tests that should be checked + --script path to a script that returns non-zero code for BAD revisions and 0 for good + +Example: + node utils/bisect.js --unit-test test + node utils/bisect.js --good 577361 --bad 599821 --script simple.js + node utils/bisect.js --good 577361 --bad 599821 --unit-test test +`; + +if (argv.h || argv.help) { + console.log(help); + process.exit(0); +} + +if (typeof argv.good !== 'number') { + argv.good = getChromiumRevision('main'); + if (typeof argv.good !== 'number') { + console.log( + COLOR_RED + + 'ERROR: Could not parse current Chromium revision' + + COLOR_RESET + ); + console.log(help); + process.exit(1); + } +} + +if (typeof argv.bad !== 'number') { + argv.bad = getChromiumRevision(); + if (typeof argv.bad !== 'number') { + console.log( + COLOR_RED + + 'ERROR: Could not parse Chromium revision in the main branch' + + COLOR_RESET + ); + console.log(help); + process.exit(1); + } +} + +if (!argv.script && !argv['unit-test']) { + console.log( + COLOR_RED + + 'ERROR: Expected to be given a script or a unit test to run' + + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +const scriptPath = argv.script ? path.resolve(argv.script) : null; + +if (argv.script && !fs.existsSync(scriptPath)) { + console.log( + COLOR_RED + + 'ERROR: Expected to be given a path to a script to run' + + COLOR_RESET + ); + console.log(help); + process.exit(1); +} + +(async (scriptPath, good, bad, pattern, noCache) => { + const span = Math.abs(good - bad); + console.log( + `Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + + while (true) { + const middle = Math.round((good + bad) / 2); + const revision = await findDownloadableRevision(middle, good, bad); + if (!revision || revision === good || revision === bad) { + break; + } + let info = browserFetcher.revisionInfo(revision); + const shouldRemove = noCache && !info.local; + info = await downloadRevision(revision); + const exitCode = await (pattern + ? runUnitTest(pattern, info) + : runScript(scriptPath, info)); + if (shouldRemove) { + await browserFetcher.remove(revision); + } + let outcome; + if (exitCode) { + bad = revision; + outcome = COLOR_RED + 'BAD' + COLOR_RESET; + } else { + good = revision; + outcome = COLOR_GREEN + 'GOOD' + COLOR_RESET; + } + const span = Math.abs(good - bad); + let fromText = ''; + let toText = ''; + if (good < bad) { + fromText = COLOR_GREEN + good + COLOR_RESET; + toText = COLOR_RED + bad + COLOR_RESET; + } else { + fromText = COLOR_RED + bad + COLOR_RESET; + toText = COLOR_GREEN + good + COLOR_RESET; + } + console.log( + `- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${ + span.toString(2).length + }${COLOR_RESET} iterations` + ); + } + + const [fromSha, toSha] = await Promise.all([ + revisionToSha(Math.min(good, bad)), + revisionToSha(Math.max(good, bad)), + ]); + console.log( + `RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}` + ); +})(scriptPath, argv.good, argv.bad, argv['unit-test'], argv['no-cache']); + +function runScript(scriptPath, revisionInfo) { + const log = debug('bisect:runscript'); + log('Running script'); + const child = fork(scriptPath, [], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath, + }, + }); + return new Promise((resolve, reject) => { + child.on('error', err => { + return reject(err); + }); + child.on('exit', code => { + return resolve(code); + }); + }); +} + +function runUnitTest(pattern, revisionInfo) { + const log = debug('bisect:rununittest'); + log('Running unit test'); + const child = spawn('npm run test:chrome:headless', ['--', '-g', pattern], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + shell: true, + env: { + ...process.env, + PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath, + }, + }); + return new Promise((resolve, reject) => { + child.on('error', err => { + return reject(err); + }); + child.on('exit', code => { + return resolve(code); + }); + }); +} + +async function downloadRevision(revision) { + const log = debug('bisect:download'); + log(`Downloading ${revision}`); + let progressBar = null; + let lastDownloadedBytes = 0; + return await browserFetcher.download( + revision, + (downloadedBytes, totalBytes) => { + if (!progressBar) { + const ProgressBar = require('progress'); + progressBar = new ProgressBar( + `- downloading Chromium r${revision} - ${toMegabytes( + totalBytes + )} [:bar] :percent :etas `, + { + complete: '=', + incomplete: ' ', + width: 20, + total: totalBytes, + } + ); + } + const delta = downloadedBytes - lastDownloadedBytes; + lastDownloadedBytes = downloadedBytes; + progressBar.tick(delta); + } + ); + function toMegabytes(bytes) { + const mb = bytes / 1024 / 1024; + return `${Math.round(mb * 10) / 10} Mb`; + } +} + +async function findDownloadableRevision(rev, from, to) { + const log = debug('bisect:findrev'); + const min = Math.min(from, to); + const max = Math.max(from, to); + log(`Looking around ${rev} from [${min}, ${max}]`); + if (await browserFetcher.canDownload(rev)) { + return rev; + } + let down = rev; + let up = rev; + while (min <= down || up <= max) { + const [downOk, upOk] = await Promise.all([ + down > min ? probe(--down) : Promise.resolve(false), + up < max ? probe(++up) : Promise.resolve(false), + ]); + if (downOk) { + return down; + } + if (upOk) { + return up; + } + } + return null; + + async function probe(rev) { + const result = await browserFetcher.canDownload(rev); + log(` ${rev} - ${result ? 'OK' : 'missing'}`); + return result; + } +} + +async function revisionToSha(revision) { + const json = await fetchJSON( + 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision + ); + return json.git_sha; +} + +function fetchJSON(url) { + return new Promise((resolve, reject) => { + const agent = url.startsWith('https://') + ? require('https') + : require('http'); + const options = URL.parse(url); + options.method = 'GET'; + options.headers = { + 'Content-Type': 'application/json', + }; + const req = agent.request(options, function (res) { + let result = ''; + res.setEncoding('utf8'); + res.on('data', chunk => { + return (result += chunk); + }); + res.on('end', () => { + return resolve(JSON.parse(result)); + }); + }); + req.on('error', err => { + return reject(err); + }); + req.end(); + }); +} + +function getChromiumRevision(gitRevision = null) { + const fileName = 'src/revisions.ts'; + const command = gitRevision + ? `git show ${gitRevision}:${fileName}` + : `cat ${fileName}`; + const result = execSync(command, { + encoding: 'utf8', + shell: true, + }); + + const m = result.match(/chromium: '(\d+)'/); + if (!m) { + return null; + } + return parseInt(m[1], 10); +} diff --git a/remote/test/puppeteer/utils/check_availability.js b/remote/test/puppeteer/utils/check_availability.js new file mode 100755 index 0000000000..531d0a2a85 --- /dev/null +++ b/remote/test/puppeteer/utils/check_availability.js @@ -0,0 +1,359 @@ +#!/usr/bin/env node +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const assert = require('assert'); +const https = require('https'); +const BrowserFetcher = + require('../lib/cjs/puppeteer/node/BrowserFetcher.js').BrowserFetcher; + +const SUPPORTED_PLATFORMS = ['linux', 'mac', 'mac_arm', 'win32', 'win64']; + +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', +}; + +class Table { + /** + * @param {!Array<number>} columnWidths + */ + constructor(columnWidths) { + this.widths = columnWidths; + } + + /** + * @param {!Array<string>} values + */ + drawRow(values) { + assert(values.length === this.widths.length); + let row = ''; + for (let i = 0; i < values.length; ++i) { + row += padCenter(values[i], this.widths[i]); + } + console.log(row); + } +} + +const helpMessage = ` +This script checks availability of prebuilt Chromium snapshots. + +Usage: node check_availability.js [<options>] [<browser version(s)>] + +options + -f full mode checks availability of all the platforms, default mode + -r roll mode checks for the most recent stable Chromium roll candidate + -rb roll mode checks for the most recent beta Chromium roll candidate + -rd roll mode checks for the most recent dev Chromium roll candidate + -p $platform print the latest revision for the given platform (${SUPPORTED_PLATFORMS.join( + ',' + )}). + -h show this help + +browser version(s) + <revision> single revision number means checking for this specific revision + <from> <to> checks all the revisions within a given range, inclusively + +Examples + To check Chromium availability of a certain revision + node check_availability.js [revision] + + To find a Chromium roll candidate for current stable Linux version + node check_availability.js -r + use -rb for beta and -rd for dev versions. + + To check Chromium availability from the latest revision in a descending order + node check_availability.js +`; + +function main() { + const args = process.argv.slice(2); + + if (args.length > 3) { + console.log(helpMessage); + return; + } + + if (args.length === 0) { + checkOmahaProxyAvailability(); + return; + } + + if (args[0].startsWith('-')) { + const option = args[0].substring(1); + switch (option) { + case 'f': + break; + case 'r': + checkRollCandidate('stable'); + return; + case 'rb': + checkRollCandidate('beta'); + return; + case 'rd': + checkRollCandidate('dev'); + return; + case 'p': + printLatestRevisionForPlatform(args[1]); + return; + default: + console.log(helpMessage); + return; + } + args.splice(0, 1); // remove options arg since we are done with options + } + + if (args.length === 1) { + const revision = parseInt(args[0], 10); + checkRangeAvailability({ + fromRevision: revision, + toRevision: revision, + stopWhenAllAvailable: false, + }); + } else { + const fromRevision = parseInt(args[0], 10); + const toRevision = parseInt(args[1], 10); + checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable: false, + }); + } +} + +async function checkOmahaProxyAvailability() { + const latestRevisions = ( + await Promise.all([ + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE' + ), + ]) + ).map(s => { + return parseInt(s, 10); + }); + const from = Math.max(...latestRevisions); + await checkRangeAvailability({ + fromRevision: from, + toRevision: 0, + stopWhenAllAvailable: false, + }); +} + +async function printLatestRevisionForPlatform(platform) { + const latestRevisions = ( + await Promise.all([ + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Mac_Arm/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win/LAST_CHANGE' + ), + fetch( + 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/LAST_CHANGE' + ), + ]) + ).map(s => { + return parseInt(s, 10); + }); + const from = Math.max(...latestRevisions); + await checkRangeAvailability({ + fromRevision: from, + toRevision: 0, + stopWhenAllAvailable: true, + printAsTable: false, + platforms: [platform], + }); +} + +async function checkRollCandidate(channel) { + const omahaResponse = await fetch( + `https://omahaproxy.appspot.com/all.json?channel=${channel}&os=linux` + ); + const linuxInfo = JSON.parse(omahaResponse)[0]; + if (!linuxInfo) { + console.error(`no ${channel} linux information available from omahaproxy`); + return; + } + + const linuxRevision = parseInt( + linuxInfo.versions[0].branch_base_position, + 10 + ); + const currentRevision = parseInt( + require('../lib/cjs/puppeteer/revisions.js').PUPPETEER_REVISIONS.chromium, + 10 + ); + + checkRangeAvailability({ + fromRevision: linuxRevision, + toRevision: currentRevision, + stopWhenAllAvailable: true, + }); +} + +/** + * @param {*} options + */ +async function checkRangeAvailability({ + fromRevision, + toRevision, + stopWhenAllAvailable, + platforms, + printAsTable = true, +}) { + platforms = platforms || SUPPORTED_PLATFORMS; + let table; + if (printAsTable) { + table = new Table([ + 10, + ...platforms.map(() => { + return 7; + }), + ]); + table.drawRow([''].concat(platforms)); + } + + const fetchers = platforms.map(platform => { + return new BrowserFetcher('', {platform}); + }); + + const inc = fromRevision < toRevision ? 1 : -1; + const revisionToStop = toRevision + inc; // +inc so the range is fully inclusive + for ( + let revision = fromRevision; + revision !== revisionToStop; + revision += inc + ) { + const promises = fetchers.map(fetcher => { + return fetcher.canDownload(revision); + }); + const availability = await Promise.all(promises); + const allAvailable = availability.every(e => { + return !!e; + }); + if (table) { + const values = [ + ' ' + + (allAvailable ? colors.green + revision + colors.reset : revision), + ]; + for (let i = 0; i < availability.length; ++i) { + const decoration = availability[i] ? '+' : '-'; + const color = availability[i] ? colors.green : colors.red; + values.push(color + decoration + colors.reset); + } + table.drawRow(values); + } else { + if (allAvailable) { + console.log(revision); + } + } + if (allAvailable && stopWhenAllAvailable) { + break; + } + } +} + +/** + * @param {string} url + * @returns {!Promise<?string>} + */ +function fetch(url) { + let resolve; + const promise = new Promise(x => { + return (resolve = x); + }); + https + .get(url, response => { + if (response.statusCode !== 200) { + resolve(null); + return; + } + let body = ''; + response.on('data', function (chunk) { + body += chunk; + }); + response.on('end', function () { + resolve(body); + }); + }) + .on('error', function (e) { + // This is okay; the server may just be faster at closing than us after + // the body is fully sent. + if (e.message.includes('ECONNRESET')) { + return; + } + console.error(`Error fetching json from ${url}: ${e}`); + resolve(null); + }); + return promise; +} + +/** + * @param {number} size + * @returns {string} + */ +function spaceString(size) { + return new Array(size).fill(' ').join(''); +} + +/** + * @param {string} text + * @returns {string} + */ +function filterOutColors(text) { + for (const colorName in colors) { + const color = colors[colorName]; + text = text.replace(color, ''); + } + return text; +} + +/** + * @param {string} text + * @param {number} length + * @returns {string} + */ +function padCenter(text, length) { + const printableCharacters = filterOutColors(text); + if (printableCharacters.length >= length) { + return text; + } + const left = Math.floor((length - printableCharacters.length) / 2); + const right = Math.ceil((length - printableCharacters.length) / 2); + return spaceString(left) + text + spaceString(right); +} + +main(); diff --git a/remote/test/puppeteer/utils/generate_artifacts.ts b/remote/test/puppeteer/utils/generate_artifacts.ts new file mode 100644 index 0000000000..e150c45e2e --- /dev/null +++ b/remote/test/puppeteer/utils/generate_artifacts.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import {writeFile} from 'fs/promises'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +(async () => { + job('', async ({outputs}) => { + await writeFile(outputs[0]!, '{"type": "module"}'); + }) + .outputs(['lib/esm/package.json']) + .build(); + + job('', async ({outputs}) => { + spawnAndLog('api-extractor', 'run', '--local'); + spawnAndLog( + 'eslint', + '--ext=ts', + '--no-ignore', + '--no-eslintrc', + '-c=.eslintrc.types.cjs', + '--fix', + outputs[0]! + ); + }) + .inputs(['lib/esm/puppeteer/types.d.ts']) + .outputs(['lib/types.d.ts', 'docs/puppeteer.api.json']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/generate_docs.ts b/remote/test/puppeteer/utils/generate_docs.ts new file mode 100644 index 0000000000..35d2c8a57a --- /dev/null +++ b/remote/test/puppeteer/utils/generate_docs.ts @@ -0,0 +1,117 @@ +/** + * 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 {readFile, rm, writeFile} from 'fs/promises'; +import semver from 'semver'; +import {generateDocs} from './internal/custom_markdown_action.js'; +import {job} from './internal/job.js'; +import {spawnAndLog} from './internal/util.js'; + +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}) => { + const content = await readFile(inputs[0]!, 'utf-8'); + const sectionContent = ` +--- +sidebar_position: 1 +--- +`; + await writeFile(outputs[0]!, sectionContent + content); + }) + .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 {versionsPerRelease} = await import(inputs[0]!); + 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))) { + 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 + job('', async ({inputs, outputs}) => { + await rm(outputs[0]!, {recursive: true, force: true}); + generateDocs(inputs[0]!, outputs[0]!); + spawnAndLog('prettier', '--ignore-path', 'none', '--write', 'docs'); + }) + .inputs(['docs/puppeteer.api.json']) + .outputs(['docs/api']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/generate_sources.ts b/remote/test/puppeteer/utils/generate_sources.ts new file mode 100644 index 0000000000..1507d11e5e --- /dev/null +++ b/remote/test/puppeteer/utils/generate_sources.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env node +import {createHash} from 'crypto'; +import esbuild from 'esbuild'; +import {mkdir, mkdtemp, readFile, rm, writeFile} from 'fs/promises'; +import {sync as glob} from 'glob'; +import path from 'path'; +import {job} from './internal/job.js'; + +const INCLUDED_FOLDERS = ['common', 'node', 'generated', 'util']; + +(async () => { + await job('', async ({outputs}) => { + await Promise.all( + outputs.map(outputs => { + return mkdir(outputs, {recursive: true}); + }) + ); + }) + .outputs(['src/generated']) + .build(); + + await job('', async ({name, inputs, outputs}) => { + const input = inputs.find(input => { + return input.endsWith('injected.ts'); + })!; + const template = await readFile( + inputs.find(input => { + return input.includes('injected.ts.tmpl'); + })!, + 'utf8' + ); + const tmp = await mkdtemp(name); + await esbuild.build({ + entryPoints: [input], + bundle: true, + outdir: tmp, + format: 'cjs', + platform: 'browser', + target: 'ES2019', + }); + const baseName = path.basename(input); + const content = await readFile( + path.join(tmp, baseName.replace('.ts', '.js')), + 'utf-8' + ); + const scriptContent = template.replace( + 'SOURCE_CODE', + JSON.stringify(content) + ); + await writeFile(outputs[0]!, scriptContent); + await rm(tmp, {recursive: true, force: true}); + }) + .inputs(['src/templates/injected.ts.tmpl', 'src/injected/**/*.ts']) + .outputs(['src/generated/injected.ts']) + .build(); + + const sources = glob( + `src/{@(${INCLUDED_FOLDERS.join('|')})/*.ts,!(types|puppeteer-core).ts}` + ); + await job('', async ({outputs}) => { + let types = + '// AUTOGENERATED - Use `npm run generate:sources` to regenerate.\n\n'; + for (const input of sources.map(source => { + return `.${source.slice(3)}`; + })) { + types += `export * from '${input.replace('.ts', '.js')}';\n`; + } + await writeFile(outputs[0]!, types); + }) + .value( + sources + .reduce((hmac, value) => { + return hmac.update(value); + }, createHash('sha256')) + .digest('hex') + ) + .outputs(['src/types.ts']) + .build(); + + if (process.env['PUBLISH']) { + job('', async ({inputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + inputs[1]!, + ( + await readFile(inputs[1]!, { + encoding: 'utf-8', + }) + ).replace("'NEXT'", `'v${version}'`) + ); + }) + .inputs(['package.json', 'versions.js']) + .build(); + } + + job('', async ({inputs, outputs}) => { + const version = JSON.parse(await readFile(inputs[0]!, 'utf8')).version; + await writeFile( + outputs[0]!, + (await readFile(inputs[1]!, 'utf8')).replace('PACKAGE_VERSION', version) + ); + }) + .inputs(['package.json', 'src/templates/version.ts.tmpl']) + .outputs(['src/generated/version.ts']) + .build(); +})(); diff --git a/remote/test/puppeteer/utils/get_deprecated_version_range.js b/remote/test/puppeteer/utils/get_deprecated_version_range.js new file mode 100644 index 0000000000..280bbf8ab0 --- /dev/null +++ b/remote/test/puppeteer/utils/get_deprecated_version_range.js @@ -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 + * + * 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'); +let version = versionsPerRelease.get(lastMaintainedChromiumVersion); +if (version.toLowerCase() === 'next') { + console.error('Unexpected NEXT Puppeteer version in versions.js'); + process.exit(1); +} +if (version.startsWith('v')) { + version = version.substring(1); +} +console.log('<' + version); +process.exit(0); diff --git a/remote/test/puppeteer/utils/internal/custom_markdown_action.ts b/remote/test/puppeteer/utils/internal/custom_markdown_action.ts new file mode 100644 index 0000000000..acd304df76 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/custom_markdown_action.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 + * + * 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/utils/internal/custom_markdown_documenter.ts b/remote/test/puppeteer/utils/internal/custom_markdown_documenter.ts new file mode 100644 index 0000000000..b9caf88b27 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/custom_markdown_documenter.ts @@ -0,0 +1,1452 @@ +/** + * 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 { + 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'; +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'; + +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 DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Signature:'}), + ]), + ]) + ); + + 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 DocParagraph({configuration}, [ + new DocEmphasisSpan({configuration, bold: true}, [ + new DocPlainText({configuration, text: 'Decorators:'}), + ]), + ]) + ); + 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); + } + + 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 _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), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } + + this._writeApiItemPage(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'], + }); + + 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), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } else { + propertiesTable.addRow( + new DocTableRow({configuration}, [ + this._createTitleCell(apiMember), + this._createModifiersCell(apiMember), + this._createPropertyTypeCell(apiMember), + this._createDescriptionCell(apiMember), + ]) + ); + } + + this._writeApiItemPage(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: '@link', + linkText: unwrappedTokenText, + urlDestination: this._getLinkFilenameForApiItem( + apiItemResult.resolvedApiItem + ), + }) + ); + return; + } + } + + // Otherwise append non-hyperlinked text + docNodeContainer.appendNode( + new DocPlainText({configuration, text: unwrappedTokenText}) + ); + } + + private _createTitleCell(apiItem: ApiItem): DocTableCell { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + let linkText: string = Utilities.getConciseSignature(apiItem); + if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) { + linkText += '?'; + } + + return new DocTableCell({configuration}, [ + new DocParagraph({configuration}, [ + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: linkText, + 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 (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) { + section.appendNodesInParagraph([ + new DocEmphasisSpan({configuration, italic: true}, [ + new DocPlainText({configuration, text: '(Optional)'}), + ]), + 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 _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'}), + ]) + ); + } + } + + 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. + // (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: + 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/utils/internal/job.ts b/remote/test/puppeteer/utils/internal/job.ts new file mode 100644 index 0000000000..2e331a7552 --- /dev/null +++ b/remote/test/puppeteer/utils/internal/job.ts @@ -0,0 +1,161 @@ +import {createHash} from 'crypto'; +import {existsSync, Stats} from 'fs'; +import {mkdir, readFile, stat, writeFile} from 'fs/promises'; +import {glob} from 'glob'; +import {tmpdir} from 'os'; +import {dirname, join, resolve} from 'path'; +import {chdir} from 'process'; + +const packageRoot = resolve(join(__dirname, '..', '..')); +chdir(packageRoot); + +interface JobContext { + name: string; + inputs: string[]; + outputs: string[]; +} + +class JobBuilder { + #inputs: string[] = []; + #outputs: string[] = []; + #callback: (ctx: JobContext) => Promise<void>; + #name: string; + #value = ''; + #force = false; + + constructor(name: string, callback: (ctx: JobContext) => Promise<void>) { + this.#name = name; + this.#callback = callback; + } + + get jobHash(): string { + return createHash('sha256').update(this.#name).digest('hex'); + } + + force() { + this.#force = true; + return this; + } + + value(value: string) { + this.#value = value; + return this; + } + + inputs(inputs: string[]): JobBuilder { + this.#inputs = inputs.flatMap(value => { + if (glob.hasMagic(value)) { + return glob.sync(value).map(value => { + // Glob doesn't support `\` on Windows, so we join here. + return join(packageRoot, value); + }); + } + return join(packageRoot, value); + }); + return this; + } + + outputs(outputs: string[]): JobBuilder { + if (!this.#name) { + this.#name = outputs.join(' and '); + } + + this.#outputs = outputs.map(value => { + return join(packageRoot, value); + }); + return this; + } + + async build(): Promise<void> { + console.log(`Running job ${this.#name}...`); + // For debugging. + if (this.#force) { + return this.#run(); + } + // In case we deleted an output file on purpose. + if (!this.getOutputStats()) { + return this.#run(); + } + // Run if the job has a value, but it changes. + if (this.#value) { + if (!(await this.isValueDifferent())) { + return; + } + return this.#run(); + } + // Always run when there is no output. + if (!this.#outputs.length) { + return this.#run(); + } + // Make-like comparator. + if (!(await this.areInputsNewer())) { + return; + } + return this.#run(); + } + + async isValueDifferent(): Promise<boolean> { + const file = join(tmpdir(), `puppeteer/${this.jobHash}.txt`); + await mkdir(dirname(file), {recursive: true}); + if (!existsSync(file)) { + await writeFile(file, this.#value); + return true; + } + return this.#value !== (await readFile(file, 'utf8')); + } + + #outputStats?: Stats[]; + async getOutputStats(): Promise<Stats[] | undefined> { + if (this.#outputStats) { + return this.#outputStats; + } + try { + this.#outputStats = await Promise.all( + this.#outputs.map(output => { + return stat(output); + }) + ); + } catch {} + return this.#outputStats; + } + + async areInputsNewer(): Promise<boolean> { + const inputStats = await Promise.all( + this.#inputs.map(input => { + return stat(input); + }) + ); + const outputStats = await this.getOutputStats(); + if ( + outputStats && + outputStats.reduce(reduceMinTime, Infinity) > + inputStats.reduce(reduceMaxTime, 0) + ) { + return false; + } + return true; + } + + #run(): Promise<void> { + return this.#callback({ + name: this.#name, + inputs: this.#inputs, + outputs: this.#outputs, + }); + } +} + +export const job = ( + name: string, + callback: (ctx: JobContext) => Promise<void> +): JobBuilder => { + return new JobBuilder(name, callback); +}; + +const reduceMaxTime = (time: number, stat: Stats) => { + return time < stat.mtimeMs ? stat.mtimeMs : time; +}; + +const reduceMinTime = (time: number, stat: Stats) => { + return time > stat.mtimeMs ? stat.mtimeMs : time; +}; diff --git a/remote/test/puppeteer/utils/internal/util.ts b/remote/test/puppeteer/utils/internal/util.ts new file mode 100644 index 0000000000..4ebbe8b86b --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/prepare_puppeteer_core.js b/remote/test/puppeteer/utils/prepare_puppeteer_core.js new file mode 100755 index 0000000000..fe7a0dfc73 --- /dev/null +++ b/remote/test/puppeteer/utils/prepare_puppeteer_core.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); + +const packagePath = path.join(__dirname, '..', 'package.json'); +const json = require(packagePath); + +delete json.scripts.install; + +json.name = 'puppeteer-core'; +json.main = './lib/cjs/puppeteer/puppeteer-core.js'; +json.exports['.'].import = './lib/esm/puppeteer/puppeteer-core.js'; +json.exports['.'].require = './lib/cjs/puppeteer/puppeteer-core.js'; + +fs.writeFileSync(packagePath, JSON.stringify(json, null, ' ')); diff --git a/remote/test/puppeteer/utils/remove_version_suffix.js b/remote/test/puppeteer/utils/remove_version_suffix.js new file mode 100644 index 0000000000..091a35ec9b --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/testserver/LICENSE b/remote/test/puppeteer/utils/testserver/LICENSE new file mode 100644 index 0000000000..afdfe50e72 --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/testserver/README.md b/remote/test/puppeteer/utils/testserver/README.md new file mode 100644 index 0000000000..d22b2da449 --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/testserver/cert.pem b/remote/test/puppeteer/utils/testserver/cert.pem new file mode 100644 index 0000000000..fd3838535a --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/testserver/key.pem b/remote/test/puppeteer/utils/testserver/key.pem new file mode 100644 index 0000000000..cbc3acb229 --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/testserver/lib/index.d.ts b/remote/test/puppeteer/utils/testserver/lib/index.d.ts new file mode 100644 index 0000000000..055c9863d7 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.d.ts @@ -0,0 +1,45 @@ +/** + * 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. + */ +/// <reference types="node" /> +/// <reference types="node" /> +import { IncomingMessage, ServerResponse } from 'http'; +import { ServerOptions as HttpsServerOptions } from 'https'; +declare type TestIncomingMessage = IncomingMessage & { + postBody?: Promise<string>; +}; +export declare class TestServer { + #private; + PORT: number; + PREFIX: string; + CROSS_PROCESS_PREFIX: string; + EMPTY_PAGE: string; + static create(dirPath: string): Promise<TestServer>; + static createHTTPS(dirPath: string): Promise<TestServer>; + constructor(dirPath: string, sslOptions?: HttpsServerOptions); + get port(): number; + enableHTTPCache(pathPrefix: string): void; + setAuth(path: string, username: string, password: string): void; + enableGzip(path: string): void; + setCSP(path: string, csp: string): void; + stop(): Promise<void>; + setRoute(path: string, handler: (req: IncomingMessage, res: ServerResponse) => void): void; + setRedirect(from: string, to: string): void; + waitForRequest(path: string): Promise<TestIncomingMessage>; + reset(): void; + serveFile(request: IncomingMessage, response: ServerResponse, pathName: string): void; +} +export {}; +//# sourceMappingURL=index.d.ts.map
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map b/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map new file mode 100644 index 0000000000..a2c07af2df --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;;;AAIH,OAAO,EAEL,eAAe,EAGf,cAAc,EACf,MAAM,MAAM,CAAC;AACd,OAAO,EAGL,aAAa,IAAI,kBAAkB,EACpC,MAAM,OAAO,CAAC;AAcf,aAAK,mBAAmB,GAAG,eAAe,GAAG;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAAC,CAAC;AAE1E,qBAAa,UAAU;;IACrB,IAAI,EAAG,MAAM,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;IAChB,oBAAoB,EAAG,MAAM,CAAC;IAC9B,UAAU,EAAG,MAAM,CAAC;WAmBP,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;WAY5C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;gBAgBlD,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,kBAAkB;IA2B5D,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIzC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI/D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAIjC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,GAC3D,IAAI;IAIP,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAe1D,KAAK,IAAI,IAAI;IA8Db,SAAS,CACP,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,MAAM,GACf,IAAI;CAoDR"}
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.js b/remote/test/puppeteer/utils/testserver/lib/index.js new file mode 100644 index 0000000000..8cfcc71139 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.js @@ -0,0 +1,261 @@ +"use strict"; +/** + * 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. + */ +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +var _TestServer_dirPath, _TestServer_server, _TestServer_wsServer, _TestServer_startTime, _TestServer_cachedPathPrefix, _TestServer_connections, _TestServer_routes, _TestServer_auths, _TestServer_csp, _TestServer_gzipRoutes, _TestServer_requestSubscribers, _TestServer_onServerConnection, _TestServer_onRequest, _TestServer_onWebSocketConnection; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TestServer = void 0; +const assert_1 = __importDefault(require("assert")); +const fs_1 = require("fs"); +const http_1 = require("http"); +const https_1 = require("https"); +const mime_1 = require("mime"); +const path_1 = require("path"); +const ws_1 = require("ws"); +const zlib_1 = require("zlib"); +class TestServer { + constructor(dirPath, sslOptions) { + _TestServer_dirPath.set(this, void 0); + _TestServer_server.set(this, void 0); + _TestServer_wsServer.set(this, void 0); + _TestServer_startTime.set(this, new Date()); + _TestServer_cachedPathPrefix.set(this, void 0); + _TestServer_connections.set(this, new Set()); + _TestServer_routes.set(this, new Map()); + _TestServer_auths.set(this, new Map()); + _TestServer_csp.set(this, new Map()); + _TestServer_gzipRoutes.set(this, new Set()); + _TestServer_requestSubscribers.set(this, new Map()); + _TestServer_onServerConnection.set(this, (connection) => { + __classPrivateFieldGet(this, _TestServer_connections, "f").add(connection); + // ECONNRESET is a legit error given + // that tab closing simply kills process. + connection.on('error', error => { + if (error.code !== 'ECONNRESET') { + throw error; + } + }); + connection.once('close', () => { + return __classPrivateFieldGet(this, _TestServer_connections, "f").delete(connection); + }); + }); + _TestServer_onRequest.set(this, (request, response) => { + request.on('error', (error) => { + if (error.code === 'ECONNRESET') { + response.end(); + } + else { + throw error; + } + }); + request.postBody = new Promise(resolve => { + let body = ''; + request.on('data', (chunk) => { + return (body += chunk); + }); + request.on('end', () => { + return resolve(body); + }); + }); + (0, assert_1.default)(request.url); + const url = new URL(request.url, `https://${request.headers.host}`); + const path = url.pathname + url.search; + const auth = __classPrivateFieldGet(this, _TestServer_auths, "f").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 = __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").get(path); + if (subscriber) { + subscriber.resolve.call(undefined, request); + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").delete(path); + } + const handler = __classPrivateFieldGet(this, _TestServer_routes, "f").get(path); + if (handler) { + handler.call(undefined, request, response); + } + else { + this.serveFile(request, response, path); + } + }); + _TestServer_onWebSocketConnection.set(this, (socket) => { + socket.send('opened'); + }); + __classPrivateFieldSet(this, _TestServer_dirPath, dirPath, "f"); + if (sslOptions) { + __classPrivateFieldSet(this, _TestServer_server, (0, https_1.createServer)(sslOptions, __classPrivateFieldGet(this, _TestServer_onRequest, "f")), "f"); + } + else { + __classPrivateFieldSet(this, _TestServer_server, (0, http_1.createServer)(__classPrivateFieldGet(this, _TestServer_onRequest, "f")), "f"); + } + __classPrivateFieldGet(this, _TestServer_server, "f").on('connection', __classPrivateFieldGet(this, _TestServer_onServerConnection, "f")); + __classPrivateFieldSet(this, _TestServer_wsServer, new ws_1.Server({ server: __classPrivateFieldGet(this, _TestServer_server, "f") }), "f"); + __classPrivateFieldGet(this, _TestServer_wsServer, "f").on('connection', __classPrivateFieldGet(this, _TestServer_onWebSocketConnection, "f")); + } + static async create(dirPath) { + let res; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath); + __classPrivateFieldGet(server, _TestServer_server, "f").once('listening', res); + __classPrivateFieldGet(server, _TestServer_server, "f").listen(0); + await promise; + return server; + } + static async createHTTPS(dirPath) { + let res; + const promise = new Promise(resolve => { + res = resolve; + }); + const server = new TestServer(dirPath, { + key: (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'key.pem')), + cert: (0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'cert.pem')), + passphrase: 'aaaa', + }); + __classPrivateFieldGet(server, _TestServer_server, "f").once('listening', res); + __classPrivateFieldGet(server, _TestServer_server, "f").listen(0); + await promise; + return server; + } + get port() { + return __classPrivateFieldGet(this, _TestServer_server, "f").address().port; + } + enableHTTPCache(pathPrefix) { + __classPrivateFieldSet(this, _TestServer_cachedPathPrefix, pathPrefix, "f"); + } + setAuth(path, username, password) { + __classPrivateFieldGet(this, _TestServer_auths, "f").set(path, { username, password }); + } + enableGzip(path) { + __classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").add(path); + } + setCSP(path, csp) { + __classPrivateFieldGet(this, _TestServer_csp, "f").set(path, csp); + } + async stop() { + this.reset(); + for (const socket of __classPrivateFieldGet(this, _TestServer_connections, "f")) { + socket.destroy(); + } + __classPrivateFieldGet(this, _TestServer_connections, "f").clear(); + await new Promise(x => { + return __classPrivateFieldGet(this, _TestServer_server, "f").close(x); + }); + } + setRoute(path, handler) { + __classPrivateFieldGet(this, _TestServer_routes, "f").set(path, handler); + } + setRedirect(from, to) { + this.setRoute(from, (_, res) => { + res.writeHead(302, { location: to }); + res.end(); + }); + } + waitForRequest(path) { + const subscriber = __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").get(path); + if (subscriber) { + return subscriber.promise; + } + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").set(path, { resolve, reject, promise }); + return promise; + } + reset() { + __classPrivateFieldGet(this, _TestServer_routes, "f").clear(); + __classPrivateFieldGet(this, _TestServer_auths, "f").clear(); + __classPrivateFieldGet(this, _TestServer_csp, "f").clear(); + __classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").clear(); + const error = new Error('Static Server has been reset'); + for (const subscriber of __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").values()) { + subscriber.reject.call(undefined, error); + } + __classPrivateFieldGet(this, _TestServer_requestSubscribers, "f").clear(); + } + serveFile(request, response, pathName) { + if (pathName === '/') { + pathName = '/index.html'; + } + const filePath = (0, path_1.join)(__classPrivateFieldGet(this, _TestServer_dirPath, "f"), pathName.substring(1)); + if (__classPrivateFieldGet(this, _TestServer_cachedPathPrefix, "f") && filePath.startsWith(__classPrivateFieldGet(this, _TestServer_cachedPathPrefix, "f"))) { + 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', __classPrivateFieldGet(this, _TestServer_startTime, "f").toISOString()); + } + else { + response.setHeader('Cache-Control', 'no-cache, no-store'); + } + const csp = __classPrivateFieldGet(this, _TestServer_csp, "f").get(pathName); + if (csp) { + response.setHeader('Content-Security-Policy', csp); + } + (0, fs_1.readFile)(filePath, (err, data) => { + if (err) { + response.statusCode = 404; + response.end(`File not found: ${filePath}`); + return; + } + const mimeType = (0, mime_1.getType)(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 (__classPrivateFieldGet(this, _TestServer_gzipRoutes, "f").has(pathName)) { + response.setHeader('Content-Encoding', 'gzip'); + (0, zlib_1.gzip)(data, (_, result) => { + response.end(result); + }); + } + else { + response.end(data); + } + }); + } +} +exports.TestServer = TestServer; +_TestServer_dirPath = new WeakMap(), _TestServer_server = new WeakMap(), _TestServer_wsServer = new WeakMap(), _TestServer_startTime = new WeakMap(), _TestServer_cachedPathPrefix = new WeakMap(), _TestServer_connections = new WeakMap(), _TestServer_routes = new WeakMap(), _TestServer_auths = new WeakMap(), _TestServer_csp = new WeakMap(), _TestServer_gzipRoutes = new WeakMap(), _TestServer_requestSubscribers = new WeakMap(), _TestServer_onServerConnection = new WeakMap(), _TestServer_onRequest = new WeakMap(), _TestServer_onWebSocketConnection = new WeakMap(); +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/lib/index.js.map b/remote/test/puppeteer/utils/testserver/lib/index.js.map new file mode 100644 index 0000000000..2ab772d086 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;AAEH,oDAA4B;AAC5B,2BAA0C;AAC1C,+BAMc;AACd,iCAIe;AACf,+BAA4C;AAE5C,+BAA0B;AAE1B,2BAAwD;AACxD,+BAA0B;AAU1B,MAAa,UAAU;IAmDrB,YAAY,OAAe,EAAE,UAA+B;QA7C5D,sCAAiB;QACjB,qCAAkC;QAClC,uCAA2B;QAE3B,gCAAa,IAAI,IAAI,EAAE,EAAC;QACxB,+CAA2B;QAE3B,kCAAe,IAAI,GAAG,EAAU,EAAC;QACjC,6BAAU,IAAI,GAAG,EAGd,EAAC;QACJ,4BAAS,IAAI,GAAG,EAAgD,EAAC;QACjE,0BAAO,IAAI,GAAG,EAAkB,EAAC;QACjC,iCAAc,IAAI,GAAG,EAAU,EAAC;QAChC,yCAAsB,IAAI,GAAG,EAAsB,EAAC;QA2CpD,yCAAsB,CAAC,UAAkB,EAAQ,EAAE;YACjD,uBAAA,IAAI,+BAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAClC,oCAAoC;YACpC,yCAAyC;YACzC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;gBAC7B,IAAK,KAA+B,CAAC,IAAI,KAAK,YAAY,EAAE;oBAC1D,MAAM,KAAK,CAAC;iBACb;YACH,CAAC,CAAC,CAAC;YACH,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC5B,OAAO,uBAAA,IAAI,+BAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC9C,CAAC,CAAC,CAAC;QACL,CAAC,EAAC;QA0EF,gCAA8B,CAC5B,OAA4B,EAC5B,QAAQ,EACF,EAAE;YACR,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAqB,EAAE,EAAE;gBAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE;oBAC/B,QAAQ,CAAC,GAAG,EAAE,CAAC;iBAChB;qBAAM;oBACL,MAAM,KAAK,CAAC;iBACb;YACH,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;gBACvC,IAAI,IAAI,GAAG,EAAE,CAAC;gBACd,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;oBACnC,OAAO,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC;gBACzB,CAAC,CAAC,CAAC;gBACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;oBACrB,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;gBACvB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YACH,IAAA,gBAAM,EAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACpB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YACpE,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC;YACvC,MAAM,IAAI,GAAG,uBAAA,IAAI,yBAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,IAAI,EAAE;gBACR,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAC7B,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EACzD,QAAQ,CACT,CAAC,QAAQ,EAAE,CAAC;gBACb,IAAI,WAAW,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE;oBACvD,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE;wBACtB,kBAAkB,EAAE,2BAA2B;qBAChD,CAAC,CAAC;oBACH,QAAQ,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;oBAC9D,OAAO;iBACR;aACF;YACD,MAAM,UAAU,GAAG,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,UAAU,EAAE;gBACd,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;gBAC5C,uBAAA,IAAI,sCAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;aACvC;YACD,MAAM,OAAO,GAAG,uBAAA,IAAI,0BAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,OAAO,EAAE;gBACX,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;aAC5C;iBAAM;gBACL,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;aACzC;QACH,CAAC,EAAC;QAuDF,4CAAyB,CAAC,MAAiB,EAAQ,EAAE;YACnD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxB,CAAC,EAAC;QA3MA,uBAAA,IAAI,uBAAY,OAAO,MAAA,CAAC;QAExB,IAAI,UAAU,EAAE;YACd,uBAAA,IAAI,sBAAW,IAAA,oBAAiB,EAAC,UAAU,EAAE,uBAAA,IAAI,6BAAW,CAAC,MAAA,CAAC;SAC/D;aAAM;YACL,uBAAA,IAAI,sBAAW,IAAA,mBAAgB,EAAC,uBAAA,IAAI,6BAAW,CAAC,MAAA,CAAC;SAClD;QACD,uBAAA,IAAI,0BAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,uBAAA,IAAI,sCAAoB,CAAC,CAAC;QACxD,uBAAA,IAAI,wBAAa,IAAI,WAAe,CAAC,EAAC,MAAM,EAAE,uBAAA,IAAI,0BAAQ,EAAC,CAAC,MAAA,CAAC;QAC7D,uBAAA,IAAI,4BAAU,CAAC,EAAE,CAAC,YAAY,EAAE,uBAAA,IAAI,yCAAuB,CAAC,CAAC;IAC/D,CAAC;IAvCD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAe;QACjC,IAAI,GAA8B,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;YACpC,GAAG,GAAG,OAAO,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;QACvC,uBAAA,MAAM,0BAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACtC,uBAAA,MAAM,0BAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,OAAO,CAAC;QACd,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,OAAe;QACtC,IAAI,GAA8B,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;YACpC,GAAG,GAAG,OAAO,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,EAAE;YACrC,GAAG,EAAE,IAAA,iBAAY,EAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YACnD,IAAI,EAAE,IAAA,iBAAY,EAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;YACrD,UAAU,EAAE,MAAM;SACnB,CAAC,CAAC;QACH,uBAAA,MAAM,0BAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACtC,uBAAA,MAAM,0BAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,OAAO,CAAC;QACd,OAAO,MAAM,CAAC;IAChB,CAAC;IA6BD,IAAI,IAAI;QACN,OAAQ,uBAAA,IAAI,0BAAQ,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;IACtD,CAAC;IAED,eAAe,CAAC,UAAkB;QAChC,uBAAA,IAAI,gCAAqB,UAAU,MAAA,CAAC;IACtC,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAE,QAAgB;QACtD,uBAAA,IAAI,yBAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAC,QAAQ,EAAE,QAAQ,EAAC,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,uBAAA,IAAI,8BAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,CAAC,IAAY,EAAE,GAAW;QAC9B,uBAAA,IAAI,uBAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,KAAK,MAAM,MAAM,IAAI,uBAAA,IAAI,+BAAa,EAAE;YACtC,MAAM,CAAC,OAAO,EAAE,CAAC;SAClB;QACD,uBAAA,IAAI,+BAAa,CAAC,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE;YACpB,OAAO,uBAAA,IAAI,0BAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,QAAQ,CACN,IAAY,EACZ,OAA4D;QAE5D,uBAAA,IAAI,0BAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,EAAU;QAClC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;YAC7B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAC,QAAQ,EAAE,EAAE,EAAC,CAAC,CAAC;YACnC,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,cAAc,CAAC,IAAY;QACzB,MAAM,UAAU,GAAG,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,UAAU,EAAE;YACd,OAAO,UAAU,CAAC,OAAO,CAAC;SAC3B;QACD,IAAI,OAA0C,CAAC;QAC/C,IAAI,MAAiC,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACxD,OAAO,GAAG,GAAG,CAAC;YACd,MAAM,GAAG,GAAG,CAAC;QACf,CAAC,CAAC,CAAC;QACH,uBAAA,IAAI,sCAAoB,CAAC,GAAG,CAAC,IAAI,EAAE,EAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAC,CAAC,CAAC;QAC/D,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK;QACH,uBAAA,IAAI,0BAAQ,CAAC,KAAK,EAAE,CAAC;QACrB,uBAAA,IAAI,yBAAO,CAAC,KAAK,EAAE,CAAC;QACpB,uBAAA,IAAI,uBAAK,CAAC,KAAK,EAAE,CAAC;QAClB,uBAAA,IAAI,8BAAY,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QACxD,KAAK,MAAM,UAAU,IAAI,uBAAA,IAAI,sCAAoB,CAAC,MAAM,EAAE,EAAE;YAC1D,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;SAC1C;QACD,uBAAA,IAAI,sCAAoB,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAoDD,SAAS,CACP,OAAwB,EACxB,QAAwB,EACxB,QAAgB;QAEhB,IAAI,QAAQ,KAAK,GAAG,EAAE;YACpB,QAAQ,GAAG,aAAa,CAAC;SAC1B;QACD,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,uBAAA,IAAI,2BAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5D,IAAI,uBAAA,IAAI,oCAAkB,IAAI,QAAQ,CAAC,UAAU,CAAC,uBAAA,IAAI,oCAAkB,CAAC,EAAE;YACzE,IAAI,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE;gBACxC,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,eAAe;gBAC1C,QAAQ,CAAC,GAAG,EAAE,CAAC;gBACf,OAAO;aACR;YACD,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;YAChE,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,uBAAA,IAAI,6BAAW,CAAC,WAAW,EAAE,CAAC,CAAC;SACpE;aAAM;YACL,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;SAC3D;QACD,MAAM,GAAG,GAAG,uBAAA,IAAI,uBAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,GAAG,EAAE;YACP,QAAQ,CAAC,SAAS,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;SACpD;QAED,IAAA,aAAQ,EAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YAC/B,IAAI,GAAG,EAAE;gBACP,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;gBAC1B,QAAQ,CAAC,GAAG,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;gBAC5C,OAAO;aACR;YACD,MAAM,QAAQ,GAAG,IAAA,cAAW,EAAC,QAAQ,CAAC,CAAC;YACvC,IAAI,QAAQ,EAAE;gBACZ,MAAM,cAAc,GAAG,yCAAyC,CAAC,IAAI,CACnE,QAAQ,CACT,CAAC;gBACF,MAAM,WAAW,GAAG,cAAc;oBAChC,CAAC,CAAC,GAAG,QAAQ,iBAAiB;oBAC9B,CAAC,CAAC,QAAQ,CAAC;gBACb,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;aACjD;YACD,IAAI,uBAAA,IAAI,8BAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;gBAClC,QAAQ,CAAC,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;gBAC/C,IAAA,WAAI,EAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;oBACvB,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACvB,CAAC,CAAC,CAAC;aACJ;iBAAM;gBACL,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;aACpB;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CAKF;AAhQD,gCAgQC"}
\ No newline at end of file diff --git a/remote/test/puppeteer/utils/testserver/package.json b/remote/test/puppeteer/utils/testserver/package.json new file mode 100644 index 0000000000..1029f16d0a --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/package.json @@ -0,0 +1,15 @@ +{ + "name": "@pptr/testserver", + "version": "0.5.0", + "description": "testing server", + "main": "lib/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/puppeteer/puppeteer/tree/main/utils/testserver" + }, + "author": "The Chromium Authors", + "license": "Apache-2.0" +} diff --git a/remote/test/puppeteer/utils/testserver/src/index.ts b/remote/test/puppeteer/utils/testserver/src/index.ts new file mode 100644 index 0000000000..c98ed9b4d0 --- /dev/null +++ b/remote/test/puppeteer/utils/testserver/src/index.ts @@ -0,0 +1,302 @@ +/** + * 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 {getType as getMimeType} from 'mime'; +import {AddressInfo} from 'net'; +import {join} from 'path'; +import {Duplex} from 'stream'; +import {Server as WebSocketServer, WebSocket} from 'ws'; +import {gzip} from 'zlib'; + +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/utils/testserver/tsconfig.json b/remote/test/puppeteer/utils/testserver/tsconfig.json new file mode 100644 index 0000000000..c2576c2564 --- /dev/null +++ b/remote/test/puppeteer/utils/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/utils/tsconfig.json b/remote/test/puppeteer/utils/tsconfig.json new file mode 100644 index 0000000000..2552d66004 --- /dev/null +++ b/remote/test/puppeteer/utils/tsconfig.json @@ -0,0 +1,12 @@ +/** + * This configuration only exists for the API Extractor tool and for VSCode to use. It is NOT the tsconfig used for compilation. + * For CJS builds, `tsconfig.cjs.json` is used, and for ESM, it's `tsconfig.esm.json`. + * See the details in CONTRIBUTING.md that describes our TypeScript setup. + */ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "module": "CommonJS" + } +} diff --git a/remote/test/puppeteer/vendor/README.md b/remote/test/puppeteer/vendor/README.md new file mode 100644 index 0000000000..182a80addf --- /dev/null +++ b/remote/test/puppeteer/vendor/README.md @@ -0,0 +1,13 @@ +# Vendoring third party dependencies + +Because we are working towards an agnostic Puppeteer that can run in any environment (see [#6125](https://github.com/puppeteer/puppeteer/issues/6125)) we cannot import common dependencies in a way that relies on Node's resolution to find them. For example, `import mitt from 'mitt'` works fine in Node, but in an ESM build running in the browser, the browser has no idea where to find `'mitt'`. + +Therefore we put all common dependencies into this directory, `vendor`. This means there are extra criteria for these dependencies; ideally they will not depend on any other modules. If they do, we should consider an alternative way of managing our dependencies. + +The process for updating a vendored dependency is: + +1. `npm install {DEP NAME HERE}` +2. `cp -r node_modules/DEP vendor` +3. Update `eslintrc.js` to forbid importing DEP directly (see the `Mitt` rule already defined in there). +4. Use the new DEP, and run `npm run build` to check everything compiles successfully. +5. If the dep ships as compiled JS, you may need to disable TypeScript checking the file. Add an entry to the `excludes` property of the TSConfig files in `vendor`. (again, see the entry that's already there for Mitt as an example). Don't forget to update both the ESM and CJS config files. diff --git a/remote/test/puppeteer/vendor/mitt/README.md b/remote/test/puppeteer/vendor/mitt/README.md new file mode 100644 index 0000000000..aba94b4517 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/README.md @@ -0,0 +1,179 @@ +<p align="center"> + <img src="https://i.imgur.com/BqsX9NT.png" width="300" height="300" alt="mitt"> + <br> + <a href="https://www.npmjs.org/package/mitt"><img src="https://img.shields.io/npm/v/mitt.svg" alt="npm"></a> + <img src="https://github.com/developit/mitt/workflows/CI/badge.svg" alt="build status"> + <a href="https://unpkg.com/mitt/dist/mitt.js"><img src="https://img.badgesize.io/https://unpkg.com/mitt/dist/mitt.js?compression=gzip" alt="gzip size"></a> +</p> + +# Mitt + +> Tiny 200b functional event emitter / pubsub. + +- **Microscopic:** weighs less than 200 bytes gzipped +- **Useful:** a wildcard `"*"` event type listens to all events +- **Familiar:** same names & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +- **Functional:** methods don't rely on `this` +- **Great Name:** somehow [mitt](https://npm.im/mitt) wasn't taken + +Mitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+. + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) +- [Examples & Demos](#examples--demos) +- [API](#api) +- [Contribute](#contribute) +- [License](#license) + +## Install + +This project uses [node](http://nodejs.org) and [npm](https://npmjs.com). Go check them out if you don't have them locally installed. + +```sh +$ npm install --save mitt +``` + +Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else: + +```javascript +// using ES6 modules +import mitt from 'mitt' + +// using CommonJS modules +var mitt = require('mitt') +``` + +The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com): + +```html +<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script> +``` + +You can find the library on `window.mitt`. + +## Usage + +```ts +import mitt from 'mitt' + +const emitter = mitt() + +// listen to an event +emitter.on('foo', e => console.log('foo', e) ) + +// listen to all events +emitter.on('*', (type, e) => console.log(type, e) ) + +// fire an event +emitter.emit('foo', { a: 'b' }) + +// clearing all events +emitter.all.clear() + +// working with handler references: +function onFoo() {} +emitter.on('foo', onFoo) // listen +emitter.off('foo', onFoo) // unlisten +``` + +### Typescript + +```ts +import mitt from 'mitt'; +const emitter: mitt.Emitter = mitt(); +``` + +## Examples & Demos + +<a href="http://codepen.io/developit/pen/rjMEwW?editors=0110"> + <b>Preact + Mitt Codepen Demo</b> + <br> + <img src="https://i.imgur.com/CjBgOfJ.png" width="278" alt="preact + mitt preview"> +</a> + +* * * + +## API + +<!-- Generated by documentation.js. Update this documentation by updating the source code. --> + +#### Table of Contents + +- [mitt](#mitt) +- [all](#all) +- [on](#on) + - [Parameters](#parameters) +- [off](#off) + - [Parameters](#parameters-1) +- [emit](#emit) + - [Parameters](#parameters-2) + +### mitt + +Mitt: Tiny (~200b) functional event emitter / pubsub. + +Returns **Mitt** + +### all + +A Map of event names to registered handler functions. + +### on + +Register an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `"*"` for all events +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event + +### off + +Remove an event handler for the given type. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from, or `"*"` +- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove + +### emit + +Invoke all handlers for the given type. +If present, `"*"` handlers are invoked after type-matched handlers. + +Note: Manually firing "\*" handlers is not supported. + +#### Parameters + +- `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) \| [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke +- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler + +## Contribute + +First off, thanks for taking the time to contribute! +Now, take a moment to be sure your contributions make sense to everyone else. + +### Reporting Issues + +Found a problem? Want a new feature? First of all see if your issue or idea has [already been reported](../../issues). +If don't, just open a [new clear and descriptive issue](../../issues/new). + +### Submitting pull requests + +Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits. + +- Fork it! +- Clone your fork: `git clone https://github.com/<your-username>/mitt` +- Navigate to the newly cloned directory: `cd mitt` +- Create a new branch for the new feature: `git checkout -b my-new-feature` +- Install the tools necessary for development: `npm install` +- Make your changes. +- Commit your changes: `git commit -am 'Add some feature'` +- Push to the branch: `git push origin my-new-feature` +- Submit a pull request with full remarks documenting your changes. + +## License + +[MIT License](https://opensource.org/licenses/MIT) © [Jason Miller](https://jasonformat.com/) diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js new file mode 100644 index 0000000000..889e27282f --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js @@ -0,0 +1,2 @@ +export default function(n){return{all:n=n||new Map,on:function(t,e){var i=n.get(t);i&&i.push(e)||n.set(t,[e])},off:function(t,e){var i=n.get(t);i&&i.splice(i.indexOf(e)>>>0,1)},emit:function(t,e){(n.get(t)||[]).slice().map(function(n){n(e)}),(n.get("*")||[]).slice().map(function(n){n(t,e)})}}} +//# sourceMappingURL=mitt.es.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map new file mode 100644 index 0000000000..6576278e2d --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.es.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.es.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.js new file mode 100644 index 0000000000..2bd0cf9e44 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.js @@ -0,0 +1,2 @@ +module.exports=function(n){return{all:n=n||new Map,on:function(e,t){var i=n.get(e);i&&i.push(t)||n.set(e,[t])},off:function(e,t){var i=n.get(e);i&&i.splice(i.indexOf(t)>>>0,1)},emit:function(e,t){(n.get(e)||[]).slice().map(function(n){n(t)}),(n.get("*")||[]).slice().map(function(n){n(e,t)})}}}; +//# sourceMappingURL=mitt.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map new file mode 100644 index 0000000000..37f6f59ebd --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js new file mode 100644 index 0000000000..0777f6de72 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js @@ -0,0 +1,2 @@ +export default function(e){return{all:e=e||new Map,on(t,n){const s=e.get(t);s&&s.push(n)||e.set(t,[n])},off(t,n){const s=e.get(t);s&&s.splice(s.indexOf(n)>>>0,1)},emit(t,n){(e.get(t)||[]).slice().map(e=>{e(n)}),(e.get("*")||[]).slice().map(e=>{e(t,n)})}}} +//# sourceMappingURL=mitt.modern.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map new file mode 100644 index 0000000000..5f669b2d61 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.modern.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.modern.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"wBAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,GAAYC,EAAiBC,GAC5B,MAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,IAAaN,EAAiBC,GAC7B,MAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,KAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAKX,IAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAKX,IAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js new file mode 100644 index 0000000000..ce0e0059ae --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js @@ -0,0 +1,2 @@ +!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).mitt=n()}(this,function(){return function(e){return{all:e=e||new Map,on:function(n,t){var f=e.get(n);f&&f.push(t)||e.set(n,[t])},off:function(n,t){var f=e.get(n);f&&f.splice(f.indexOf(t)>>>0,1)},emit:function(n,t){(e.get(n)||[]).slice().map(function(e){e(t)}),(e.get("*")||[]).slice().map(function(e){e(n,t)})}}}}); +//# sourceMappingURL=mitt.umd.js.map diff --git a/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map new file mode 100644 index 0000000000..642c894006 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/dist/mitt.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mitt.umd.js","sources":["../src/index.ts"],"sourcesContent":["export type EventType = string | symbol;\n\n// An event handler can take an optional event argument\n// and should not return a value\nexport type Handler<T = any> = (event?: T) => void;\nexport type WildcardHandler = (type: EventType, event?: any) => void;\n\n// An array of all currently registered event handlers for a type\nexport type EventHandlerList = Array<Handler>;\nexport type WildCardEventHandlerList = Array<WildcardHandler>;\n\n// A map of event types and their corresponding event handlers.\nexport type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;\n\nexport interface Emitter {\n\tall: EventHandlerMap;\n\n\ton<T = any>(type: EventType, handler: Handler<T>): void;\n\ton(type: '*', handler: WildcardHandler): void;\n\n\toff<T = any>(type: EventType, handler: Handler<T>): void;\n\toff(type: '*', handler: WildcardHandler): void;\n\n\temit<T = any>(type: EventType, event?: T): void;\n\temit(type: '*', event?: any): void;\n}\n\n/**\n * Mitt: Tiny (~200b) functional event emitter / pubsub.\n * @name mitt\n * @returns {Mitt}\n */\nexport default function mitt(all?: EventHandlerMap): Emitter {\n\tall = all || new Map();\n\n\treturn {\n\n\t\t/**\n\t\t * A Map of event names to registered handler functions.\n\t\t */\n\t\tall,\n\n\t\t/**\n\t\t * Register an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to listen for, or `\"*\"` for all events\n\t\t * @param {Function} handler Function to call in response to given event\n\t\t * @memberOf mitt\n\t\t */\n\t\ton<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tconst added = handlers && handlers.push(handler);\n\t\t\tif (!added) {\n\t\t\t\tall.set(type, [handler]);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an event handler for the given type.\n\t\t * @param {string|symbol} type Type of event to unregister `handler` from, or `\"*\"`\n\t\t * @param {Function} handler Handler function to remove\n\t\t * @memberOf mitt\n\t\t */\n\t\toff<T = any>(type: EventType, handler: Handler<T>) {\n\t\t\tconst handlers = all.get(type);\n\t\t\tif (handlers) {\n\t\t\t\thandlers.splice(handlers.indexOf(handler) >>> 0, 1);\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Invoke all handlers for the given type.\n\t\t * If present, `\"*\"` handlers are invoked after type-matched handlers.\n\t\t *\n\t\t * Note: Manually firing \"*\" handlers is not supported.\n\t\t *\n\t\t * @param {string|symbol} type The event type to invoke\n\t\t * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler\n\t\t * @memberOf mitt\n\t\t */\n\t\temit<T = any>(type: EventType, evt: T) {\n\t\t\t((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });\n\t\t\t((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });\n\t\t}\n\t};\n}\n"],"names":["all","Map","on","type","handler","handlers","get","push","set","off","splice","indexOf","emit","evt","slice","map"],"mappings":"6LAgC6BA,GAG5B,MAAO,CAKNA,IAPDA,EAAMA,GAAO,IAAIC,IAehBC,YAAYC,EAAiBC,GAC5B,IAAMC,EAAWL,EAAIM,IAAIH,GACXE,GAAYA,EAASE,KAAKH,IAEvCJ,EAAIQ,IAAIL,EAAM,CAACC,KAUjBK,aAAaN,EAAiBC,GAC7B,IAAMC,EAAWL,EAAIM,IAAIH,GACrBE,GACHA,EAASK,OAAOL,EAASM,QAAQP,KAAa,EAAG,IAcnDQ,cAAcT,EAAiBU,IAC5Bb,EAAIM,IAAIH,IAAS,IAAyBW,QAAQC,IAAI,SAACX,GAAcA,EAAQS,MAC7Eb,EAAIM,IAAI,MAAQ,IAAiCQ,QAAQC,IAAI,SAACX,GAAcA,EAAQD,EAAMU"}
\ No newline at end of file diff --git a/remote/test/puppeteer/vendor/mitt/index.d.ts b/remote/test/puppeteer/vendor/mitt/index.d.ts new file mode 100644 index 0000000000..55346dd976 --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/index.d.ts @@ -0,0 +1,21 @@ +export declare type EventType = string | symbol;
+export declare type Handler<T = any> = (event?: T) => void;
+export declare type WildcardHandler = (type: EventType, event?: any) => void;
+export declare type EventHandlerList = Array<Handler>;
+export declare type WildCardEventHandlerList = Array<WildcardHandler>;
+export declare type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;
+export interface Emitter {
+ all: EventHandlerMap;
+ on<T = any>(type: EventType, handler: Handler<T>): void;
+ on(type: '*', handler: WildcardHandler): void;
+ off<T = any>(type: EventType, handler: Handler<T>): void;
+ off(type: '*', handler: WildcardHandler): void;
+ emit<T = any>(type: EventType, event?: T): void;
+ emit(type: '*', event?: any): void;
+}
+/**
+ * Mitt: Tiny (~200b) functional event emitter / pubsub.
+ * @name mitt
+ * @returns {Mitt}
+ */
+export default function mitt(all?: EventHandlerMap): Emitter;
diff --git a/remote/test/puppeteer/vendor/mitt/package.json b/remote/test/puppeteer/vendor/mitt/package.json new file mode 100644 index 0000000000..0105524a0d --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/package.json @@ -0,0 +1,141 @@ +{ + "_from": "mitt@latest", + "_id": "mitt@2.1.0", + "_inBundle": false, + "_integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "_location": "/mitt", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "mitt@latest", + "name": "mitt", + "escapedName": "mitt", + "rawSpec": "latest", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "_shasum": "f740577c23176c6205b121b2973514eade1b2230", + "_spec": "mitt@latest", + "_where": "/Users/jacktfranklin/src/puppeteer", + "authors": [ + "Jason Miller <jason@developit.ca>" + ], + "bugs": { + "url": "https://github.com/developit/mitt/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Tiny 200b functional Event Emitter / pubsub.", + "devDependencies": { + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.4", + "@types/sinon-chai": "^3.2.4", + "@typescript-eslint/eslint-plugin": "^3.0.1", + "@typescript-eslint/parser": "^3.0.1", + "chai": "^4.2.0", + "documentation": "^13.0.0", + "eslint": "^7.1.0", + "eslint-config-developit": "^1.2.0", + "esm": "^3.2.25", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", + "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", + "sinon": "^9.0.2", + "sinon-chai": "^3.5.0", + "ts-node": "^8.10.2", + "typescript": "^3.9.3" + }, + "eslintConfig": { + "extends": [ + "developit", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "env": { + "browser": true, + "mocha": true, + "jest": false, + "es6": true + }, + "globals": { + "expect": true + }, + "rules": { + "semi": [ + 2, + "always" + ], + "jest/valid-expect": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0 + } + }, + "eslintIgnore": [ + "dist", + "index.d.ts" + ], + "esmodules": "dist/mitt.modern.js", + "files": [ + "src", + "dist", + "index.d.ts" + ], + "homepage": "https://github.com/developit/mitt", + "jsnext:main": "dist/mitt.es.js", + "keywords": [ + "events", + "eventemitter", + "emitter", + "pubsub" + ], + "license": "MIT", + "main": "dist/mitt.js", + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, + "module": "dist/mitt.es.js", + "name": "mitt", + "repository": { + "type": "git", + "url": "git+https://github.com/developit/mitt.git" + }, + "scripts": { + "build": "npm-run-all --silent clean -p bundle -s docs", + "bundle": "microbundle", + "clean": "rimraf dist", + "docs": "documentation readme src/index.ts --section API -q --parse-extension ts", + "lint": "eslint src test --ext ts --ext js", + "mocha": "mocha test", + "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "test": "npm-run-all --silent typecheck lint mocha test-types", + "test-types": "tsc test/test-types-compilation.ts --noEmit", + "typecheck": "tsc --noEmit" + }, + "source": "src/index.ts", + "typings": "index.d.ts", + "umd:main": "dist/mitt.umd.js", + "version": "2.1.0" +} diff --git a/remote/test/puppeteer/vendor/mitt/src/index.ts b/remote/test/puppeteer/vendor/mitt/src/index.ts new file mode 100644 index 0000000000..7b5342f09e --- /dev/null +++ b/remote/test/puppeteer/vendor/mitt/src/index.ts @@ -0,0 +1,92 @@ + +/** + * @public + */ +export type EventType = string | symbol; + +// An event handler can take an optional event argument +// and should not return a value +/** + * @public + */ +export type Handler<T = any> = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; + +// An array of all currently registered event handlers for a type +export type EventHandlerList = Array<Handler>; +export type WildCardEventHandlerList = Array<WildcardHandler>; + +// A map of event types and their corresponding event handlers. +export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>; + +export interface Emitter { + all: EventHandlerMap; + + on<T = any>(type: EventType, handler: Handler<T>): void; + on(type: '*', handler: WildcardHandler): void; + + off<T = any>(type: EventType, handler: Handler<T>): void; + off(type: '*', handler: WildcardHandler): void; + + emit<T = any>(type: EventType, event?: T): void; + emit(type: '*', event?: any): void; +} + +/** + * Mitt: Tiny (~200b) functional event emitter / pubsub. + * @name mitt + * @returns {Mitt} + */ +export default function mitt(all?: EventHandlerMap): Emitter { + all = all || new Map(); + + return { + + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `"*"` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf mitt + */ + on<T = any>(type: EventType, handler: Handler<T>) { + const handlers = all.get(type); + const added = handlers && handlers.push(handler); + if (!added) { + all.set(type, [handler]); + } + }, + + /** + * Remove an event handler for the given type. + * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` + * @param {Function} handler Handler function to remove + * @memberOf mitt + */ + off<T = any>(type: EventType, handler: Handler<T>) { + const handlers = all.get(type); + if (handlers) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } + }, + + /** + * Invoke all handlers for the given type. + * If present, `"*"` handlers are invoked after type-matched handlers. + * + * Note: Manually firing "*" handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf mitt + */ + emit<T = any>(type: EventType, evt: T) { + ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); + ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); + } + }; +} diff --git a/remote/test/puppeteer/vendor/tsconfig.cjs.json b/remote/test/puppeteer/vendor/tsconfig.cjs.json new file mode 100644 index 0000000000..5fadf882b8 --- /dev/null +++ b/remote/test/puppeteer/vendor/tsconfig.cjs.json @@ -0,0 +1,12 @@ +{ + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/cjs/vendor", + "module": "CommonJS", + "moduleResolution": "node", + "strict": false + } +} diff --git a/remote/test/puppeteer/vendor/tsconfig.esm.json b/remote/test/puppeteer/vendor/tsconfig.esm.json new file mode 100644 index 0000000000..42a098c18c --- /dev/null +++ b/remote/test/puppeteer/vendor/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "exclude": [ + "mitt/dist" + ], + "compilerOptions": { + "composite": true, + "outDir": "../lib/esm/vendor", + "module": "esnext", + "moduleResolution": "node", + "strict": false + } +} diff --git a/remote/test/puppeteer/versions.js b/remote/test/puppeteer/versions.js new file mode 100644 index 0000000000..3a2d3a73b8 --- /dev/null +++ b/remote/test/puppeteer/versions.js @@ -0,0 +1,65 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const versionsPerRelease = new Map([ + // This is a mapping from Chromium version => Puppeteer version. + // In Chromium roll patches, use `NEXT` for the Puppeteer version. + ['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'], +]); + +// The same major version as the current Chrome Stable per https://chromestatus.com/roadmap. +const lastMaintainedChromiumVersion = '103.0.5059.0'; + +if (!versionsPerRelease.has(lastMaintainedChromiumVersion)) { + throw new Error( + 'lastMaintainedChromiumVersion is missing from versionsPerRelease' + ); +} + +module.exports = { + versionsPerRelease, + lastMaintainedChromiumVersion, +}; |