From 68b1690910ad36dfda7b16f229bf743ef2a2c295 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 25 Jan 2022 21:19:21 +0800 Subject: [PATCH 1/6] Add basic infrastructure of using @playwright/test --- .eslintrc.js | 31 +- .github/report-flaky-tests/index.js | 60 +- .github/workflows/end2end-test-playwright.yml | 68 ++ .github/workflows/flaky-tests.yml | 2 + bin/packages/build.js | 1 + bin/packages/watch.js | 2 +- docs/manifest.json | 6 + package-lock.json | 1086 ++++++++++++++++- package.json | 7 +- packages/e2e-test-utils-playwright/.npmrc | 1 + .../e2e-test-utils-playwright/CHANGELOG.md | 5 + packages/e2e-test-utils-playwright/README.md | 50 + .../e2e-test-utils-playwright/package.json | 46 + .../e2e-test-utils-playwright/src/config.ts | 12 + .../e2e-test-utils-playwright/src/index.ts | 13 + .../src/page/get-page-error.js | 26 + .../src/page/index.ts | 29 + .../src/page/is-current-url.js | 18 + .../src/page/visit-admin-page.js | 34 + .../src/request/blocks.js | 28 + .../src/request/index.ts | 121 ++ .../src/request/login.ts | 34 + .../src/request/plugins.ts | 77 ++ .../src/request/posts.js | 32 + .../src/request/rest.ts | 183 +++ .../src/request/themes.ts | 39 + .../e2e-test-utils-playwright/src/test.ts | 141 +++ .../e2e-test-utils-playwright/tsconfig.json | 21 + .../e2e-tests/config/flaky-tests-reporter.js | 1 + packages/eslint-plugin/CHANGELOG.md | 4 + .../configs/recommended-with-formatting.js | 20 - .../configs/test-e2e-playwright.js | 3 + packages/eslint-plugin/package.json | 1 + test/e2e/README.md | 40 + test/e2e/config/flaky-tests-reporter.ts | 84 ++ test/e2e/config/global-setup.ts | 31 + test/e2e/playwright.config.ts | 59 + test/e2e/specs/sanity.spec.js | 13 + test/e2e/tsconfig.json | 13 + test/unit/jest.config.js | 1 + tsconfig.json | 1 + 41 files changed, 2402 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/end2end-test-playwright.yml create mode 100644 packages/e2e-test-utils-playwright/.npmrc create mode 100644 packages/e2e-test-utils-playwright/CHANGELOG.md create mode 100644 packages/e2e-test-utils-playwright/README.md create mode 100644 packages/e2e-test-utils-playwright/package.json create mode 100644 packages/e2e-test-utils-playwright/src/config.ts create mode 100644 packages/e2e-test-utils-playwright/src/index.ts create mode 100644 packages/e2e-test-utils-playwright/src/page/get-page-error.js create mode 100644 packages/e2e-test-utils-playwright/src/page/index.ts create mode 100644 packages/e2e-test-utils-playwright/src/page/is-current-url.js create mode 100644 packages/e2e-test-utils-playwright/src/page/visit-admin-page.js create mode 100644 packages/e2e-test-utils-playwright/src/request/blocks.js create mode 100644 packages/e2e-test-utils-playwright/src/request/index.ts create mode 100644 packages/e2e-test-utils-playwright/src/request/login.ts create mode 100644 packages/e2e-test-utils-playwright/src/request/plugins.ts create mode 100644 packages/e2e-test-utils-playwright/src/request/posts.js create mode 100644 packages/e2e-test-utils-playwright/src/request/rest.ts create mode 100644 packages/e2e-test-utils-playwright/src/request/themes.ts create mode 100644 packages/e2e-test-utils-playwright/src/test.ts create mode 100644 packages/e2e-test-utils-playwright/tsconfig.json create mode 100644 packages/eslint-plugin/configs/test-e2e-playwright.js create mode 100644 test/e2e/README.md create mode 100644 test/e2e/config/flaky-tests-reporter.ts create mode 100644 test/e2e/config/global-setup.ts create mode 100644 test/e2e/playwright.config.ts create mode 100644 test/e2e/specs/sanity.spec.js create mode 100644 test/e2e/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 1e0ef899ce3a9..3ab0c5fe2777b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -225,14 +225,39 @@ module.exports = { }, }, { - files: [ 'packages/jest*/**/*.js' ], + files: [ 'packages/jest*/**/*.js', '**/test/**/*.js' ], + excludedFiles: [ 'test/e2e/**/*.js' ], extends: [ 'plugin:@wordpress/eslint-plugin/test-unit' ], }, { - files: [ 'packages/e2e-test*/**/*.js' ], + files: [ + 'packages/e2e-tests/**/*.js', + 'packages/e2e-test-utils/**/*.js', + ], extends: [ 'plugin:@wordpress/eslint-plugin/test-e2e' ], + }, + { + files: [ + 'test/e2e/**/*.[tj]s', + 'packages/e2e-test-utils-playwright/**/*.[tj]s', + ], + extends: [ 'plugin:@wordpress/eslint-plugin/test-e2e-playwright' ], rules: { - 'jest/expect-expect': 'off', + '@wordpress/no-global-active-element': 'off', + '@wordpress/no-global-get-selection': 'off', + 'no-restricted-syntax': [ + 'error', + { + selector: 'CallExpression[callee.name="$"]', + message: + '`$` is discouraged, please use `locator` instead', + }, + { + selector: 'CallExpression[callee.name="$$"]', + message: + '`$$` is discouraged, please use `locator` instead', + }, + ], }, }, { diff --git a/.github/report-flaky-tests/index.js b/.github/report-flaky-tests/index.js index bda7f6efecfd1..2a3755ac0584f 100644 --- a/.github/report-flaky-tests/index.js +++ b/.github/report-flaky-tests/index.js @@ -46,6 +46,7 @@ const metaData = { for ( const flakyTest of flakyTests ) { const { + runner: testRunner = 'jest-circus', title: testTitle, path: testPath, results: testResults, @@ -92,7 +93,11 @@ const metaData = { body.indexOf( TEST_RESULTS_LIST.close ) ) + [ - renderTestErrorMessage( { testPath, testResults } ), + renderTestErrorMessage( { + testRunner, + testPath, + testResults, + } ), TEST_RESULTS_LIST.close, ].join( '\n' ); @@ -121,6 +126,7 @@ const metaData = { title: issueTitle, body: renderIssueBody( { meta, + testRunner, testTitle, testPath, testResults, @@ -187,10 +193,16 @@ function getIssueTitle( testTitle ) { return `[Flaky Test] ${ testTitle }`; } -function renderIssueBody( { meta, testTitle, testPath, testResults } ) { +function renderIssueBody( { + meta, + testRunner, + testTitle, + testPath, + testResults, +} ) { return ( renderIssueDescription( { meta, testTitle, testPath } ) + - renderTestResults( { testPath, testResults } ) + renderTestResults( { testRunner, testPath, testResults } ) ); } @@ -211,14 +223,41 @@ ${ testTitle } `; } -function renderTestResults( { testPath, testResults } ) { +function renderTestResults( { testRunner, testPath, testResults } ) { return `${ TEST_RESULTS_LIST.open } -${ renderTestErrorMessage( { testPath, testResults } ) } +${ renderTestErrorMessage( { testRunner, testPath, testResults } ) } ${ TEST_RESULTS_LIST.close } `; } -function renderTestErrorMessage( { testPath, testResults } ) { +function renderTestResults( { testRunner, testResults, testPath } ) { + switch ( testRunner ) { + case '@playwright/test': { + // Could do a slightly better formatting than this. + return stripAnsi( + testResults.map( ( result ) => result.error.stack ).join( '\n' ) + ); + } + case 'jest-circus': + default: { + return stripAnsi( + formatResultsErrors( + testResults, + { + rootDir: path.join( + process.cwd(), + 'packages/e2e-tests' + ), + }, + {}, + testPath + ) + ); + } + } +} + +function renderTestErrorMessage( { testRunner, testPath, testResults } ) { const date = new Date().toISOString(); // It will look something like this without formatting: // ▶ [2021-08-31T16:15:19.875Z] Test passed after 2 failed attempts on trunk @@ -231,14 +270,7 @@ function renderTestErrorMessage( { testPath, testResults } ) { \`\`\` -${ stripAnsi( - formatResultsErrors( - testResults, - { rootDir: path.join( process.cwd(), 'packages/e2e-tests' ) }, - {}, - testPath - ) -) } +${ renderTestResults( { testRunner, testPath, testResults } ) } \`\`\` ${ TEST_RESULT.close }`; } diff --git a/.github/workflows/end2end-test-playwright.yml b/.github/workflows/end2end-test-playwright.yml new file mode 100644 index 0000000000000..83a64c991344d --- /dev/null +++ b/.github/workflows/end2end-test-playwright.yml @@ -0,0 +1,68 @@ +name: End-to-End Tests Playwright + +on: + pull_request: + push: + branches: + - trunk + - 'release/**' + - 'wp/**' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + e2e: + name: E2E Tests + runs-on: ubuntu-latest + if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + node: ['14'] + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + + - name: Use desired version of NodeJS + uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 + with: + node-version: ${{ matrix.node }} + cache: npm + + - name: Npm install and build + run: | + npm ci + npm run build + + - name: Install Playwright dependencies + run: | + npx playwright install chromium --with-deps + + - name: Install WordPress and start the server + run: | + npm run wp-env start + + - name: Run the tests + run: | + npm run test-e2e:playwright + + - name: Archive debug artifacts (screenshots, traces) + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + if: always() + with: + name: failures-artifacts + path: artifacts + if-no-files-found: ignore + + - name: Archive flaky tests report + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + if: always() + with: + name: flaky-tests-report + path: flaky-tests + if-no-files-found: ignore diff --git a/.github/workflows/flaky-tests.yml b/.github/workflows/flaky-tests.yml index 84e6344348b6b..c8604b83ae0c7 100644 --- a/.github/workflows/flaky-tests.yml +++ b/.github/workflows/flaky-tests.yml @@ -2,6 +2,8 @@ name: Report Flaky Tests on: workflow_run: + # We should also add 'End-to-End Tests Playwright' here but that + # wil run this workflow whenever either one of them completes. workflows: ['End-to-End Tests'] types: - completed diff --git a/bin/packages/build.js b/bin/packages/build.js index 479254ff885df..94db7f63eadee 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -224,6 +224,7 @@ if ( files.length ) { `**/benchmark/**`, `**/{__mocks__,__tests__,test}/**`, `**/{storybook,stories}/**`, + `**/e2e-test-utils-playwright/**`, ], onlyFiles: true, } diff --git a/bin/packages/watch.js b/bin/packages/watch.js index ac9034dcaeb16..b27fc10746f55 100644 --- a/bin/packages/watch.js +++ b/bin/packages/watch.js @@ -64,7 +64,7 @@ function isSourceFile( filename ) { return ( /\/src\/.+\.(js|json|scss|ts|tsx)$/.test( relativePath ) && ! [ - /\/(benchmark|__mocks__|__tests__|test|storybook|stories)\/.+/, + /\/(benchmark|__mocks__|__tests__|test|storybook|stories|e2e-test-utils-playwright)\/.+/, /.\.(spec|test)\.js$/, ].some( ( regex ) => regex.test( relativePath ) ) ); diff --git a/docs/manifest.json b/docs/manifest.json index 7251f7882dba4..4c9424d4f7f84 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1499,6 +1499,12 @@ "markdown_source": "../packages/dom/README.md", "parent": "packages" }, + { + "title": "@wordpress/e2e-test-utils-playwright", + "slug": "packages-e2e-test-utils-playwright", + "markdown_source": "../packages/e2e-test-utils-playwright/README.md", + "parent": "packages" + }, { "title": "@wordpress/e2e-test-utils", "slug": "packages-e2e-test-utils", diff --git a/package-lock.json b/package-lock.json index 9dee1c23ca90e..8a8ff52560747 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6969,6 +6969,936 @@ } } }, + "@playwright/test": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.20.0.tgz", + "integrity": "sha512-UpI5HTcgNLckR0kqXqwNvbcIXtRaDxk+hnO0OBwPSjfbBjRfRgAJ2ClA/b30C5E3UW5dJa17zhsy2qrk66l5cg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.16.7", + "@babel/core": "7.16.12", + "@babel/helper-plugin-utils": "7.16.7", + "@babel/plugin-proposal-class-properties": "7.16.7", + "@babel/plugin-proposal-dynamic-import": "7.16.7", + "@babel/plugin-proposal-export-namespace-from": "7.16.7", + "@babel/plugin-proposal-logical-assignment-operators": "7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7", + "@babel/plugin-proposal-numeric-separator": "7.16.7", + "@babel/plugin-proposal-optional-chaining": "7.16.7", + "@babel/plugin-proposal-private-methods": "7.16.11", + "@babel/plugin-proposal-private-property-in-object": "7.16.7", + "@babel/plugin-syntax-async-generators": "7.8.4", + "@babel/plugin-syntax-json-strings": "7.8.3", + "@babel/plugin-syntax-object-rest-spread": "7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "7.8.3", + "@babel/plugin-transform-modules-commonjs": "7.16.8", + "@babel/preset-typescript": "7.16.7", + "colors": "1.4.0", + "commander": "8.3.0", + "debug": "4.3.3", + "expect": "27.2.5", + "jest-matcher-utils": "27.2.5", + "json5": "2.2.0", + "mime": "3.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "open": "8.4.0", + "pirates": "4.0.4", + "playwright-core": "1.20.0", + "rimraf": "3.0.2", + "source-map-support": "0.4.18", + "stack-utils": "2.0.5", + "yazl": "2.5.1" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", + "dev": true + }, + "@babel/core": { + "version": "7.16.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", + "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.12", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.10", + "@babel/types": "^7.16.8", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", + "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", + "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", + "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", + "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.7.tgz", + "integrity": "sha512-TKsj9NkjJfTBxM7Phfy7kv6yYc4ZcOo+AaWGqQOKTPDOmcGkIFb5xNA746eKisQkm4yavUYh4InYM9S+VnO01w==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.7.tgz", + "integrity": "sha512-bm3AQf45vR4gKggRfvJdYJ0gFLoCbsPxiFLSH6hTVYABptNHY6l9NrhnucVjQ/X+SPtLANT9lc0fFhikj+VBRA==", + "dev": true + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", + "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", + "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", + "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", + "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", + "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", + "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", + "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", + "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.10", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", + "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", + "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz", + "integrity": "sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-simple-access": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", + "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-typescript": "^7.16.7" + } + }, + "@babel/preset-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", + "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-transform-typescript": "^7.16.7" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + } + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "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 + }, + "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": "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.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "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" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true + }, + "expect": { + "version": "27.2.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.2.5.tgz", + "integrity": "sha512-ZrO0w7bo8BgGoP/bLz+HDCI+0Hfei9jUSZs5yI/Wyn9VkG9w8oJ7rHRgYj+MA7yqqFa0IwHA3flJzZtYugShJA==", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "ansi-styles": "^5.0.0", + "jest-get-type": "^27.0.6", + "jest-matcher-utils": "^27.2.5", + "jest-message-util": "^27.2.5", + "jest-regex-util": "^27.0.6" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "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==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "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 + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + } + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "27.2.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.5.tgz", + "integrity": "sha512-qNR/kh6bz0Dyv3m68Ck2g1fLW5KlSOUNcFQh87VXHZwWc/gY6XwnKofx76Qytz3x5LDWT09/2+yXndTkaG4aWg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.2.5", + "jest-get-type": "^27.0.6", + "pretty-format": "^27.2.5" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + } + } + }, + "jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "dev": true + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true + }, + "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 + }, + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "pirates": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz", + "integrity": "sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==", + "dev": true + }, + "playwright-core": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.0.tgz", + "integrity": "sha512-d25IRcdooS278Cijlp8J8A5fLQZ+/aY3dKRJvgX5yjXA69N0huIUdnh3xXSgn+LsQ9DCNmB7Ngof3eY630jgdA==", + "dev": true, + "requires": { + "colors": "1.4.0", + "commander": "8.3.0", + "debug": "4.3.3", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "jpeg-js": "0.4.3", + "mime": "3.0.0", + "pixelmatch": "5.2.1", + "pngjs": "6.0.0", + "progress": "2.0.3", + "proper-lockfile": "4.1.2", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "socks-proxy-agent": "6.1.1", + "stack-utils": "2.0.5", + "ws": "8.4.2", + "yauzl": "2.10.0", + "yazl": "2.5.1" + } + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "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 + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", + "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "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 + } + } + }, + "ws": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz", + "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.2.tgz", @@ -15098,9 +16028,9 @@ "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" }, "@types/istanbul-lib-report": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", - "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "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==", "requires": { "@types/istanbul-lib-coverage": "*" } @@ -16563,6 +17493,18 @@ "node-fetch": "^2.6.0" } }, + "@wordpress/e2e-test-utils-playwright": { + "version": "file:packages/e2e-test-utils-playwright", + "dev": true, + "requires": { + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/url": "file:packages/url", + "form-data": "^4.0.0", + "lodash": "^4.17.21", + "role-selector": "0.5.0" + } + }, "@wordpress/e2e-tests": { "version": "file:packages/e2e-tests", "dev": true, @@ -16913,6 +17855,7 @@ "eslint-plugin-jest": "^25.2.3", "eslint-plugin-jsdoc": "^37.0.3", "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-playwright": "^0.8.0", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.27.0", "eslint-plugin-react-hooks": "^4.3.0", @@ -32798,6 +33741,12 @@ } } }, + "eslint-plugin-playwright": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.8.0.tgz", + "integrity": "sha512-9uJH25m6H3jwU5O7bHD5M8cLx46L72EnIUe3dZqTox6M+WzOFzeUWaDJHHCdLGXZ8XlAU4mbCZnP7uhjKepfRA==", + "dev": true + }, "eslint-plugin-prettier": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz", @@ -41334,6 +42283,12 @@ } } }, + "jpeg-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", + "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==", + "dev": true + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -47467,6 +48422,23 @@ "node-modules-regexp": "^1.0.0" } }, + "pixelmatch": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", + "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", + "dev": true, + "requires": { + "pngjs": "^4.0.1" + }, + "dependencies": { + "pngjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", + "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", + "dev": true + } + } + }, "pkg-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", @@ -47570,6 +48542,12 @@ "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 + }, "pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -48906,6 +49884,31 @@ "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + } + } + }, "property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", @@ -51822,6 +52825,74 @@ "inherits": "^2.0.1" } }, + "role-selector": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/role-selector/-/role-selector-0.5.0.tgz", + "integrity": "sha512-Gck33A6q/7Lqi8puscm6lcn5JwXZ1a5U/IYc3KinlEOols4aITMiqsytlwFvQ6b3Mla5jjYq20KnBRs4aUJuQw==", + "dev": true, + "requires": { + "@testing-library/dom": "^8.11.3" + }, + "dependencies": { + "@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + } + }, + "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": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true + }, + "dom-accessibility-api": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz", + "integrity": "sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, "rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -59093,6 +60164,15 @@ "fd-slicer": "~1.0.1" } }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 7d0a993810cae..7cced4f1064db 100755 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@emotion/jest": "11.7.1", "@emotion/native": "^11.0.0", "@octokit/rest": "16.26.0", + "@playwright/test": "1.20.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.2", "@storybook/addon-a11y": "6.4.9", "@storybook/addon-actions": "6.4.19", @@ -116,6 +117,7 @@ "@types/eslint": "7.28.0", "@types/estree": "0.0.50", "@types/highlight-words-core": "1.2.1", + "@types/istanbul-lib-report": "3.0.0", "@types/lodash": "4.14.172", "@types/npm-package-arg": "6.1.1", "@types/prettier": "2.3.2", @@ -134,6 +136,7 @@ "@wordpress/dependency-extraction-webpack-plugin": "file:packages/dependency-extraction-webpack-plugin", "@wordpress/docgen": "file:packages/docgen", "@wordpress/e2e-test-utils": "file:packages/e2e-test-utils", + "@wordpress/e2e-test-utils-playwright": "file:packages/e2e-test-utils-playwright", "@wordpress/e2e-tests": "file:packages/e2e-tests", "@wordpress/env": "file:packages/env", "@wordpress/eslint-plugin": "file:packages/eslint-plugin", @@ -176,6 +179,7 @@ "eslint-plugin-import": "2.25.2", "execa": "4.0.2", "fast-glob": "3.2.7", + "filenamify": "^4.2.0", "glob": "7.1.2", "husky": "7.0.0", "inquirer": "7.1.0", @@ -243,7 +247,7 @@ "precheck-local-changes": "npm run docs:build", "check-local-changes": "( git diff -U0 | xargs -0 node bin/process-git-diff ) || ( echo \"There are local uncommitted changes after one or both of 'npm install' or 'npm run docs:build'!\" && git diff --exit-code && exit 1 );", "dev": "npm run build:packages && concurrently \"wp-scripts start\" \"npm run dev:packages\"", - "dev:packages": "node ./bin/packages/watch.js", + "dev:packages": "concurrently \"node ./bin/packages/watch.js\" \"tsc --build --watch\"", "distclean": "rimraf node_modules packages/*/node_modules", "docs:gen": "node ./docs/tool/index.js", "docs:build": "npm-run-all docs:gen api-docs:*", @@ -273,6 +277,7 @@ "test": "npm run lint && npm run test-unit", "test:create-block": "./bin/test-create-block.sh", "test-e2e": "wp-scripts test-e2e --config packages/e2e-tests/jest.config.js", + "test-e2e:playwright": "playwright test --config test/e2e/playwright.config.ts", "test-e2e:debug": "wp-scripts --inspect-brk test-e2e --config packages/e2e-tests/jest.config.js --puppeteer-devtools", "test-e2e:watch": "npm run test-e2e -- --watch", "test-performance": "wp-scripts test-e2e --config packages/e2e-tests/jest.performance.config.js", diff --git a/packages/e2e-test-utils-playwright/.npmrc b/packages/e2e-test-utils-playwright/.npmrc new file mode 100644 index 0000000000000..43c97e719a5a8 --- /dev/null +++ b/packages/e2e-test-utils-playwright/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/e2e-test-utils-playwright/CHANGELOG.md b/packages/e2e-test-utils-playwright/CHANGELOG.md new file mode 100644 index 0000000000000..34c83ae9061ba --- /dev/null +++ b/packages/e2e-test-utils-playwright/CHANGELOG.md @@ -0,0 +1,5 @@ + + +## Unreleased + +-- Initial version of the package. diff --git a/packages/e2e-test-utils-playwright/README.md b/packages/e2e-test-utils-playwright/README.md new file mode 100644 index 0000000000000..d60fdfe07f52e --- /dev/null +++ b/packages/e2e-test-utils-playwright/README.md @@ -0,0 +1,50 @@ +# E2E Test Utils + +Experimental End-To-End (E2E) Playwright test utils for WordPress. + +**This package is still experimental and breaking changes could be introduced in future minor versions (`v0.x`). Use it at your own risks.** + +_It works properly with the minimum version of Gutenberg `9.2.0` or the minimum version of WordPress `5.6.0`._ + +## Installation + +Install the module + +```bash +npm install @wordpress/e2e-test-utils-playwright --save-dev +``` + +**Note**: This package requires Node.js 12.0.0 or later. It is not compatible with older versions. + +## API + +### test + +The extended Playwright's [test](https://playwright.dev/docs/api/class-test) module with the `pageUtils` and the `requestUtils` fixtures. + +### expect + +The Playwright/Jest's [expect](https://jestjs.io/docs/expect) function. + +### PageUtils + +Create a page utils instance of the current page. + +```js +const pageUtils = new PageUtils( page ); +``` + +### RequestUtils + +Create a request utils instance. + +```js +const requestUtils = await RequestUtils.setup( { + user: { + username: 'admin', + password: 'password', + }, +} ); +``` + +

Code is Poetry.

diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json new file mode 100644 index 0000000000000..d1f8c53d4ecd0 --- /dev/null +++ b/packages/e2e-test-utils-playwright/package.json @@ -0,0 +1,46 @@ +{ + "name": "@wordpress/e2e-test-utils-playwright", + "version": "0.0.0", + "description": "End-To-End (E2E) test utils for WordPress.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "e2e", + "utils", + "playwright" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/e2e-test-utils-playwright" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "files": [ + "build", + "build-types" + ], + "main": "./build/index.js", + "types": "./build-types", + "dependencies": { + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/keycodes": "file:../keycodes", + "@wordpress/url": "file:../url", + "form-data": "^4.0.0", + "lodash": "^4.17.21", + "role-selector": "0.5.0" + }, + "peerDependencies": { + "@playwright/test": ">=1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/e2e-test-utils-playwright/src/config.ts b/packages/e2e-test-utils-playwright/src/config.ts new file mode 100644 index 0000000000000..87c905d43f545 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/config.ts @@ -0,0 +1,12 @@ +const WP_ADMIN_USER = { + username: 'admin', + password: 'password', +} as const; + +const { + WP_USERNAME = WP_ADMIN_USER.username, + WP_PASSWORD = WP_ADMIN_USER.password, + WP_BASE_URL = 'http://localhost:8889', +} = process.env; + +export { WP_ADMIN_USER, WP_USERNAME, WP_PASSWORD, WP_BASE_URL }; diff --git a/packages/e2e-test-utils-playwright/src/index.ts b/packages/e2e-test-utils-playwright/src/index.ts new file mode 100644 index 0000000000000..6c2ad003ef989 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/index.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import { selectors } from '@playwright/test'; +import { selectorScript } from 'role-selector/playwright-test'; + +// Register role selector. +// Replace this with the native role engine when it's ready. +selectors.register( 'role', selectorScript, { contentScript: true } ); + +export { PageUtils } from './page'; +export { RequestUtils } from './request'; +export { test, expect } from './test'; diff --git a/packages/e2e-test-utils-playwright/src/page/get-page-error.js b/packages/e2e-test-utils-playwright/src/page/get-page-error.js new file mode 100644 index 0000000000000..0e41fb0a41b14 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/get-page-error.js @@ -0,0 +1,26 @@ +/** + * Regular expression matching a displayed PHP error within a markup string. + * + * @see https://github.com/php/php-src/blob/598175e/main/main.c#L1257-L1297 + * + * @type {RegExp} + */ +const REGEXP_PHP_ERROR = /()?(Fatal error|Recoverable fatal error|Warning|Parse error|Notice|Strict Standards|Deprecated|Unknown error)(<\/b>)?: (.*?) in (.*?) on line ()?\d+(<\/b>)?/; + +/** + * Returns a promise resolving to one of either a string or null. A string will + * be resolved if an error message is present in the contents of the page. If no + * error is present, a null value will be resolved instead. This requires the + * environment be configured to display errors. + * + * @see http://php.net/manual/en/function.error-reporting.php + * + * @this {import('./').PageUtils} + * @return {Promise} Promise resolving to a string or null, depending + * whether a page error is present. + */ +export async function getPageError() { + const content = await this.page.content(); + const match = content.match( REGEXP_PHP_ERROR ); + return match ? match[ 0 ] : null; +} diff --git a/packages/e2e-test-utils-playwright/src/page/index.ts b/packages/e2e-test-utils-playwright/src/page/index.ts new file mode 100644 index 0000000000000..2988bc444d5a5 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/index.ts @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import type { Browser, Page, BrowserContext } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { getPageError } from './get-page-error'; +import { isCurrentURL } from './is-current-url'; +import { visitAdminPage } from './visit-admin-page'; + +class PageUtils { + browser: Browser; + page: Page; + context: BrowserContext; + + constructor( page: Page ) { + this.page = page; + this.context = page.context(); + this.browser = this.context.browser()!; + } + + getPageError = getPageError; + isCurrentURL = isCurrentURL; + visitAdminPage = visitAdminPage; +} + +export { PageUtils }; diff --git a/packages/e2e-test-utils-playwright/src/page/is-current-url.js b/packages/e2e-test-utils-playwright/src/page/is-current-url.js new file mode 100644 index 0000000000000..0aa784da71a16 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/is-current-url.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { WP_BASE_URL } from '../config'; + +/** + * Checks if current URL is a WordPress path. + * + * @this {import('./').PageUtils} + * @param {string} WPPath String to be serialized as pathname. + * @return {boolean} Boolean represents whether current URL is or not a WordPress path. + */ +export function isCurrentURL( WPPath ) { + const currentURL = new URL( this.page.url() ); + const expectedURL = new URL( WPPath, WP_BASE_URL ); + + return expectedURL.pathname === currentURL.pathname; +} diff --git a/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js b/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js new file mode 100644 index 0000000000000..4314f12fa9831 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { join } from 'path'; + +/** + * Visits admin page; if user is not logged in then it logging in it first, then visits admin page. + * + * @this {import('./').PageUtils} + * @param {string} adminPath String to be serialized as pathname. + * @param {string} query String to be serialized as query portion of URL. + */ +export async function visitAdminPage( adminPath, query ) { + await this.page.goto( + join( 'wp-admin', adminPath ) + ( query ? `?${ query }` : '' ) + ); + + // Handle upgrade required screen + if ( this.isCurrentURL( 'wp-admin/upgrade.php' ) ) { + // Click update + await this.page.click( '.button.button-large.button-primary' ); + // Click continue + await this.page.click( '.button.button-large' ); + } + + if ( this.isCurrentURL( 'wp-login.php' ) ) { + throw new Error( 'Not logged in' ); + } + + const error = await this.getPageError(); + if ( error ) { + throw new Error( 'Unexpected error in page content: ' + error ); + } +} diff --git a/packages/e2e-test-utils-playwright/src/request/blocks.js b/packages/e2e-test-utils-playwright/src/request/blocks.js new file mode 100644 index 0000000000000..82f011c3f750e --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/blocks.js @@ -0,0 +1,28 @@ +/** + * Delete all blocks using REST API. + * + * @see https://developer.wordpress.org/rest-api/reference/blocks/#list-editor-blocks + * @this {import('./index').RequestUtils} + */ +export async function deleteAllBlocks() { + // List all blocks. + // https://developer.wordpress.org/rest-api/reference/blocks/#list-editor-blocks + const blocks = await this.rest( { + path: '/wp/v2/blocks', + params: { + per_page: 100, + // All possible statuses. + status: 'publish,future,draft,pending,private,trash', + }, + } ); + + // Delete blocks. + // https://developer.wordpress.org/rest-api/reference/blocks/#delete-a-editor-block + // "/wp/v2/posts" not yet supports batch requests. + await this.batchRest( + blocks.map( ( block ) => ( { + method: 'DELETE', + path: `/wp/v2/blocks/${ block.id }?force=true`, + } ) ) + ); +} diff --git a/packages/e2e-test-utils-playwright/src/request/index.ts b/packages/e2e-test-utils-playwright/src/request/index.ts new file mode 100644 index 0000000000000..dcd8a715ad746 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/index.ts @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { request } from '@playwright/test'; +import type { APIRequestContext, Cookie } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { WP_ADMIN_USER, WP_BASE_URL } from '../config'; +import type { User } from './login'; +import { login } from './login'; +import { setupRest, rest, getMaxBatchSize, batchRest } from './rest'; +import { getPluginsMap, activatePlugin, deactivatePlugin } from './plugins'; +import { activateTheme } from './themes'; +import { deleteAllBlocks } from './blocks'; +import { deleteAllPosts } from './posts'; + +interface StorageState { + cookies: Cookie[]; + nonce: string; + rootURL: string; +} + +class RequestUtils { + request: APIRequestContext; + user: User; + maxBatchSize?: number; + storageState?: StorageState; + storageStatePath?: string; + baseURL?: string; + + pluginsMap: Record< string, string > | null = null; + + static async setup( { + user, + storageStatePath, + baseURL = WP_BASE_URL, + }: { + user?: User; + storageStatePath?: string; + baseURL?: string; + } ) { + let storageState: StorageState | undefined; + if ( storageStatePath ) { + await fs.mkdir( path.dirname( storageStatePath ), { + recursive: true, + } ); + + try { + storageState = JSON.parse( + await fs.readFile( storageStatePath, 'utf-8' ) + ); + } catch ( error ) { + if ( + error instanceof Error && + ( error as NodeJS.ErrnoException ).code === 'ENOENT' + ) { + // Ignore errors if the state is not found. + } else { + throw error; + } + } + } + + const requestContext = await request.newContext( { + baseURL, + storageState: storageState && { + cookies: storageState.cookies, + origins: [], + }, + } ); + + const requestUtils = new RequestUtils( requestContext, { + user, + storageState, + storageStatePath, + baseURL, + } ); + + return requestUtils; + } + + constructor( + requestContext: APIRequestContext, + { + user = WP_ADMIN_USER, + storageState, + storageStatePath, + baseURL = WP_BASE_URL, + }: { + user?: User; + storageState?: StorageState; + storageStatePath?: string; + baseURL?: string; + } = {} + ) { + this.user = user; + this.request = requestContext; + this.storageStatePath = storageStatePath; + this.storageState = storageState; + this.baseURL = baseURL; + } + + login = login; + setupRest = setupRest; + rest = rest; + getMaxBatchSize = getMaxBatchSize; + batchRest = batchRest; + getPluginsMap = getPluginsMap; + activatePlugin = activatePlugin; + deactivatePlugin = deactivatePlugin; + activateTheme = activateTheme; + deleteAllBlocks = deleteAllBlocks; + deleteAllPosts = deleteAllPosts; +} + +export type { StorageState }; +export { RequestUtils }; diff --git a/packages/e2e-test-utils-playwright/src/request/login.ts b/packages/e2e-test-utils-playwright/src/request/login.ts new file mode 100644 index 0000000000000..de8727385161c --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/login.ts @@ -0,0 +1,34 @@ +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; + +export interface User { + username: string; + password: string; +} + +async function login( this: RequestUtils, user: User = this.user ) { + // Login to admin using request context. + let response = await this.request.post( '/wp-login.php', { + failOnStatusCode: true, + form: { + log: user.username, + pwd: user.password, + }, + } ); + await response.dispose(); + + // Get the nonce. + response = await this.request.get( + '/wp-admin/admin-ajax.php?action=rest-nonce', + { + failOnStatusCode: true, + } + ); + const nonce = await response.text(); + + return nonce; +} + +export { login }; diff --git a/packages/e2e-test-utils-playwright/src/request/plugins.ts b/packages/e2e-test-utils-playwright/src/request/plugins.ts new file mode 100644 index 0000000000000..6f986858dde42 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/plugins.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { kebabCase } from 'lodash'; + +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; + +/** + * Fetch the plugins from API and cache them in memory, + * since they are unlikely to change during testing. + * + * @param {} this RequestUtils. + * @param {} [forceRefetch] Force refetch the installed plugins to update the cache. + */ +async function getPluginsMap( this: RequestUtils, forceRefetch = false ) { + if ( ! forceRefetch && this.pluginsMap ) { + return this.pluginsMap; + } + + const plugins = await this.rest( { + path: '/wp/v2/plugins', + } ); + this.pluginsMap = {}; + for ( const plugin of plugins ) { + // Ideally, we should be using sanitize_title() in PHP rather than kebabCase(), + // but we don't have the exact port of it in JS. + this.pluginsMap[ kebabCase( plugin.name ) ] = plugin.plugin; + } + return this.pluginsMap; +} + +/** + * Activates an installed plugin. + * + * @param {this} this RequestUtils. + * @param {string} slug Plugin slug. + */ +async function activatePlugin( this: RequestUtils, slug: string ) { + const pluginsMap = await this.getPluginsMap(); + const plugin = pluginsMap[ slug ]; + + if ( ! plugin ) { + throw new Error( `The plugin "${ slug }" isn't installed` ); + } + + await this.rest( { + method: 'PUT', + path: `/wp/v2/plugins/${ plugin }`, + data: { status: 'active' }, + } ); +} + +/** + * Deactivates an active plugin. + * + * @param {this} this RequestUtils. + * @param {string} slug Plugin slug. + */ +async function deactivatePlugin( this: RequestUtils, slug: string ) { + const pluginsMap = await this.getPluginsMap(); + const plugin = pluginsMap[ slug ]; + + if ( ! plugin ) { + throw new Error( `The plugin "${ slug }" isn't installed` ); + } + + await this.rest( { + method: 'PUT', + path: `/wp/v2/plugins/${ plugin }`, + data: { status: 'inactive' }, + } ); +} + +export { getPluginsMap, activatePlugin, deactivatePlugin }; diff --git a/packages/e2e-test-utils-playwright/src/request/posts.js b/packages/e2e-test-utils-playwright/src/request/posts.js new file mode 100644 index 0000000000000..4b3d7bbc44400 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/posts.js @@ -0,0 +1,32 @@ +/** + * Delete all posts using REST API. + * + * @this {import('./index').RequestUtils} + */ +export async function deleteAllPosts() { + // List all posts. + // https://developer.wordpress.org/rest-api/reference/posts/#list-posts + const posts = await this.rest( { + path: '/wp/v2/posts', + params: { + per_page: 100, + // All possible statuses. + status: 'publish,future,draft,pending,private,trash', + }, + } ); + + // Delete all posts one by one. + // https://developer.wordpress.org/rest-api/reference/posts/#delete-a-post + // "/wp/v2/posts" not yet supports batch requests. + await Promise.all( + posts.map( ( post ) => + this.rest( { + method: 'DELETE', + path: `/wp/v2/posts/${ post.id }`, + params: { + force: true, + }, + } ) + ) + ); +} diff --git a/packages/e2e-test-utils-playwright/src/request/rest.ts b/packages/e2e-test-utils-playwright/src/request/rest.ts new file mode 100644 index 0000000000000..af1bed16820e0 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/rest.ts @@ -0,0 +1,183 @@ +/** + * External dependencies + */ +import * as fs from 'fs/promises'; +import { dirname } from 'path'; +import type { APIRequestContext } from '@playwright/test'; +import { chunk } from 'lodash'; + +/** + * Internal dependencies + */ +import { WP_BASE_URL } from '../config'; +import type { RequestUtils, StorageState } from './index'; + +async function getAPIRootURL( request: APIRequestContext ) { + // Discover the API root url using link header. + // See https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/#link-header + const response = await request.head( WP_BASE_URL ); + const links = response.headers().link; + const restLink = links?.match( /<([^>]+)>; rel="https:\/\/api\.w\.org\/"/ ); + + if ( ! restLink ) { + throw new Error( `Failed to discover REST API endpoint. + Link header: ${ links }` ); + } + + const [ , rootURL ] = restLink; + + return rootURL; +} + +async function setupRest( this: RequestUtils ): Promise< StorageState > { + const [ nonce, rootURL ] = await Promise.all( [ + this.login(), + getAPIRootURL( this.request ), + ] ); + + const { cookies } = await this.request.storageState(); + + const storageState: StorageState = { + cookies, + nonce, + rootURL, + }; + + if ( this.storageStatePath ) { + await fs.mkdir( dirname( this.storageStatePath ), { recursive: true } ); + await fs.writeFile( + this.storageStatePath, + JSON.stringify( storageState ), + 'utf-8' + ); + } + + this.storageState = storageState; + + return storageState; +} + +type RequestFetchOptions = Exclude< + Parameters< APIRequestContext[ 'fetch' ] >[ 1 ], + undefined +>; +interface RestOptions extends RequestFetchOptions { + path: string; +} + +async function rest< RestResponse = any >( + this: RequestUtils, + options: RestOptions +): Promise< RestResponse > { + const { path, ...fetchOptions } = options; + + if ( ! path ) { + throw new Error( '"path" is required to make a REST call' ); + } + + if ( ! this.storageState?.nonce || ! this.storageState?.rootURL ) { + await this.setupRest(); + } + + const relativePath = path.startsWith( '/' ) ? path.slice( 1 ) : path; + + const url = this.storageState!.rootURL + relativePath; + + try { + const response = await this.request.fetch( url, { + ...fetchOptions, + failOnStatusCode: false, + headers: { + 'X-WP-Nonce': this.storageState!.nonce, + ...( fetchOptions.headers || {} ), + }, + } ); + const json: RestResponse = await response.json(); + + if ( ! response.ok() ) { + throw json; + } + + return json; + } catch ( error ) { + // Nonce in invalid, retry again with a renewed nonce. + if ( + typeof error === 'object' && + error !== null && + Object.prototype.hasOwnProperty.call( error, 'code' ) && + ( error as { code: string } ).code === 'rest_cookie_invalid_nonce' + ) { + await this.setupRest(); + + return this.rest( options ); + } + + throw error; + } +} + +/** + * Get the maximum batch size for the REST API. + * + * @param {} this RequestUtils. + * @param {} forceRefetch Force revalidate the cached max batch size. + */ +async function getMaxBatchSize( this: RequestUtils, forceRefetch = false ) { + if ( ! forceRefetch && this.maxBatchSize ) { + return this.maxBatchSize; + } + + const response = await this.rest< { + endpoints: { + args: { + requests: { + maxItems: number; + }; + }; + }[]; + } >( { + method: 'OPTIONS', + path: '/batch/v1', + } ); + this.maxBatchSize = response.endpoints[ 0 ].args.requests.maxItems; + return this.maxBatchSize; +} + +interface BatchRequest { + method?: string; + path: string; + headers: Record< string, string | string[] >; + body: any; +} + +async function batchRest< BatchResponse >( + this: RequestUtils, + requests: BatchRequest[] +): Promise< BatchResponse[] > { + const maxBatchSize = await this.getMaxBatchSize(); + + if ( requests.length > maxBatchSize ) { + const chunks = chunk( requests, maxBatchSize ); + + const chunkResponses = await Promise.all( + chunks.map( ( chunkRequests ) => + this.batchRest< BatchResponse >( chunkRequests ) + ) + ); + + return chunkResponses.flat(); + } + + const { responses } = await this.rest< { responses: BatchResponse[] } >( { + method: 'POST', + path: '/batch/v1', + data: { + requests, + validation: 'require-all-validate', + }, + } ); + + return responses; +} + +export { setupRest, rest, getMaxBatchSize, batchRest }; diff --git a/packages/e2e-test-utils-playwright/src/request/themes.ts b/packages/e2e-test-utils-playwright/src/request/themes.ts new file mode 100644 index 0000000000000..1bbb7fb410b49 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request/themes.ts @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; +import { WP_BASE_URL } from '../config'; + +const THEMES_URL = new URL( '/wp-admin/themes.php', WP_BASE_URL ).href; + +async function activateTheme( + this: RequestUtils, + themeSlug: string +): Promise< void > { + let response = await this.request.get( THEMES_URL ); + const html = await response.text(); + const matchGroup = html.match( + new RegExp( + `action=activate&stylesheet=${ themeSlug }&_wpnonce=[a-z0-9]+` + ) + ); + + if ( ! matchGroup ) { + if ( html.includes( `data-slug="${ themeSlug }"` ) ) { + // The theme is already activated. + return; + } + + throw new Error( `The theme "${ themeSlug }" is not installed` ); + } + + const [ activateQuery ] = matchGroup; + const activateLink = + THEMES_URL + `?${ activateQuery }`.replace( /&/g, '&' ); + + response = await this.request.get( activateLink ); + + await response.dispose(); +} + +export { activateTheme }; diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts new file mode 100644 index 0000000000000..0766c4e6d7620 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -0,0 +1,141 @@ +/** + * External dependencies + */ +import * as path from 'path'; +import { test as base, expect } from '@playwright/test'; +import type { ConsoleMessage } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { PageUtils, RequestUtils } from './index'; + +const STORAGE_STATE_PATH = + process.env.STORAGE_STATE_PATH || + path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); + +/** + * Set of console logging types observed to protect against unexpected yet + * handled (i.e. not catastrophic) errors or warnings. Each key corresponds + * to the Playwright ConsoleMessage type, its value the corresponding function + * on the console global object. + */ +const OBSERVED_CONSOLE_MESSAGE_TYPES = [ 'warn', 'error' ] as const; + +/** + * Adds a page event handler to emit uncaught exception to process if one of + * the observed console logging types is encountered. + * + * @param message The console message. + */ +function observeConsoleLogging( message: ConsoleMessage ) { + const type = message.type(); + if ( + ! OBSERVED_CONSOLE_MESSAGE_TYPES.includes( + type as typeof OBSERVED_CONSOLE_MESSAGE_TYPES[ number ] + ) + ) { + return; + } + + const text = message.text(); + + // An exception is made for _blanket_ deprecation warnings: Those + // which log regardless of whether a deprecated feature is in use. + if ( text.includes( 'This is a global warning' ) ) { + return; + } + + // A chrome advisory warning about SameSite cookies is informational + // about future changes, tracked separately for improvement in core. + // + // See: https://core.trac.wordpress.org/ticket/37000 + // See: https://www.chromestatus.com/feature/5088147346030592 + // See: https://www.chromestatus.com/feature/5633521622188032 + if ( text.includes( 'A cookie associated with a cross-site resource' ) ) { + return; + } + + // Viewing posts on the front end can result in this error, which + // has nothing to do with Gutenberg. + if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) { + return; + } + + // TODO: Not implemented yet. + // Network errors are ignored only if we are intentionally testing + // offline mode. + // if ( + // text.includes( 'net::ERR_INTERNET_DISCONNECTED' ) && + // isOfflineMode() + // ) { + // return; + // } + + // As of WordPress 5.3.2 in Chrome 79, navigating to the block editor + // (Posts > Add New) will display a console warning about + // non - unique IDs. + // See: https://core.trac.wordpress.org/ticket/23165 + if ( text.includes( 'elements with non-unique id #_wpnonce' ) ) { + return; + } + + // Ignore all JQMIGRATE (jQuery migrate) deprecation warnings. + if ( text.includes( 'JQMIGRATE' ) ) { + return; + } + + const logFunction = type as typeof OBSERVED_CONSOLE_MESSAGE_TYPES[ number ]; + + // Disable reason: We intentionally bubble up the console message + // which, unless the test explicitly anticipates the logging via + // @wordpress/jest-console matchers, will cause the intended test + // failure. + + // eslint-disable-next-line no-console + console[ logFunction ]( text ); +} + +const test = base.extend< + { + pageUtils: PageUtils; + }, + { + requestUtils: RequestUtils; + } +>( { + page: async ( { page }, use ) => { + page.on( 'console', observeConsoleLogging ); + + await use( page ); + + // Clear local storage after each test. + await page.evaluate( () => { + window.localStorage.clear(); + } ); + + await page.close(); + }, + pageUtils: async ( { page }, use ) => { + await use( new PageUtils( page ) ); + }, + requestUtils: [ + async ( {}, use, workerInfo ) => { + const requestUtils = await RequestUtils.setup( { + baseURL: workerInfo.project.use.baseURL, + storageStatePath: STORAGE_STATE_PATH, + } ); + + await Promise.all( [ + requestUtils.activateTheme( 'twentytwentyone' ), + requestUtils.deleteAllPosts(), + requestUtils.deleteAllBlocks(), + ] ); + + await use( requestUtils ); + }, + { scope: 'worker' }, + ], +} ); + +export { test, expect }; diff --git a/packages/e2e-test-utils-playwright/tsconfig.json b/packages/e2e-test-utils-playwright/tsconfig.json new file mode 100644 index 0000000000000..52eeefa6913dd --- /dev/null +++ b/packages/e2e-test-utils-playwright/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "composite": false, + "module": "CommonJS", + "rootDir": "src", + "noEmit": false, + "outDir": "build", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "build-types", + "emitDeclarationOnly": false, + "allowJs": true, + "checkJs": false, + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/e2e-tests/config/flaky-tests-reporter.js b/packages/e2e-tests/config/flaky-tests-reporter.js index 238acad6c361a..23b5aed5d9a8b 100644 --- a/packages/e2e-tests/config/flaky-tests-reporter.js +++ b/packages/e2e-tests/config/flaky-tests-reporter.js @@ -56,6 +56,7 @@ class FlakyTestsReporter { await fs.writeFile( `flaky-tests/${ filenamify( testTitle ) }.json`, JSON.stringify( { + runner: 'jest-circus', title: testTitle, path: testPath, results: failingResults, diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index cfc259b06d9e1..ebf952a2d6730 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -12,6 +12,10 @@ - Replaced no-shadow eslint rule with @typescript-eslint/no-shadow ([#38665](https://github.com/WordPress/gutenberg/pull/38665)). +### Breaking Changes + +- Remove automatic environment detection of `test-unit` and `test-e2e` for the `recommended` preset. It's now recommended to opt-in to specific preset explicitly. + ## 10.0.0 (2022-01-27) ### Breaking Changes diff --git a/packages/eslint-plugin/configs/recommended-with-formatting.js b/packages/eslint-plugin/configs/recommended-with-formatting.js index ce82405c94b7f..c59293bdc01ce 100644 --- a/packages/eslint-plugin/configs/recommended-with-formatting.js +++ b/packages/eslint-plugin/configs/recommended-with-formatting.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -const { isPackageInstalled } = require( '../utils' ); - // Exclude bundled WordPress packages from the list. const wpPackagesRegExp = '^@wordpress/(?!(icons|interface))'; @@ -45,19 +40,4 @@ const config = { }, }; -if ( isPackageInstalled( 'jest' ) ) { - config.overrides = [ - { - // Unit test files and their helpers only. - files: [ '**/@(test|__tests__)/**/*.js', '**/?(*.)test.js' ], - extends: [ require.resolve( './test-unit.js' ) ], - }, - { - // End-to-end test files and their helpers only. - files: [ '**/specs/**/*.js', '**/?(*.)spec.js' ], - extends: [ require.resolve( './test-e2e.js' ) ], - }, - ]; -} - module.exports = config; diff --git a/packages/eslint-plugin/configs/test-e2e-playwright.js b/packages/eslint-plugin/configs/test-e2e-playwright.js new file mode 100644 index 0000000000000..4c7cd1d9ee213 --- /dev/null +++ b/packages/eslint-plugin/configs/test-e2e-playwright.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [ 'plugin:playwright/playwright-test' ], +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ad46950115177..82d69b7693aae 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -42,6 +42,7 @@ "eslint-plugin-jest": "^25.2.3", "eslint-plugin-jsdoc": "^37.0.3", "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-playwright": "^0.8.0", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.27.0", "eslint-plugin-react-hooks": "^4.3.0", diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000000000..96a6f39baf9ef --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,40 @@ +# E2E Tests + +End-To-End (E2E) tests for WordPress. + +## Running tests + +The following command is available on the Gutenberg repo: + +```json +{ + "test-e2e:playwright": "playwright test --config test/e2e/playwright.config.ts" +} +``` + +### Run all available tests +```bash +npm run test-e2e:playwright +``` + +### Headed mode + +```bash +npm run test-e2e:playwright -- --headed +``` + +### Run a specific test file +```bash +npm run test-e2e:playwright -- +``` +### Debugging + +Makes e2e tests available to debug in Playwright Inspector. +```bash +npm run test-e2e:playwright -- --debug +``` + + +**Note**: This package requires Node.js 12.0.0 or later. It is not compatible with older versions. + +

Code is Poetry.

diff --git a/test/e2e/config/flaky-tests-reporter.ts b/test/e2e/config/flaky-tests-reporter.ts new file mode 100644 index 0000000000000..1b2fdb6328aea --- /dev/null +++ b/test/e2e/config/flaky-tests-reporter.ts @@ -0,0 +1,84 @@ +/** + * A **flaky** test is defined as a test which passed after auto-retrying. + * - By default, all tests run once if they pass. + * - If a test fails, it will automatically re-run at most 2 times. + * - If it pass after retrying (below 2 times), then it's marked as **flaky** + * but displayed as **passed** in the original test suite. + * - If it fail all 3 times, then it's a **failed** test. + */ +/** + * External dependencies + */ +import fs from 'fs'; +import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter'; +import filenamify from 'filenamify'; + +type FormattedTestResult = Omit< TestResult, 'steps' >; + +// Remove "steps" to prevent stringify circular structure. +function formatTestResult( testResult: TestResult ): FormattedTestResult { + const result = { ...testResult, steps: undefined }; + delete result.steps; + return result; +} + +class FlakyTestsReporter implements Reporter { + failingTestCaseResults = new Map< string, FormattedTestResult[] >(); + + onBegin() { + try { + fs.mkdirSync( 'flaky-tests' ); + } catch ( err ) { + if ( + err instanceof Error && + ( err as NodeJS.ErrnoException ).code === 'EEXIST' + ) { + // Ignore the error if the directory already exists. + } else { + throw err; + } + } + } + + onTestEnd( test: TestCase, testCaseResult: TestResult ) { + const testPath = test.location.file; + const testTitle = test.title; + + switch ( test.outcome() ) { + case 'unexpected': { + if ( ! this.failingTestCaseResults.has( testTitle ) ) { + this.failingTestCaseResults.set( testTitle, [] ); + } + this.failingTestCaseResults + .get( testTitle )! + .push( formatTestResult( testCaseResult ) ); + break; + } + case 'flaky': { + fs.writeFileSync( + `flaky-tests/${ filenamify( testTitle ) }.json`, + JSON.stringify( { + runner: '@playwright/test', + title: testTitle, + path: testPath, + results: this.failingTestCaseResults.get( testTitle ), + } ), + 'utf-8' + ); + break; + } + default: + break; + } + } + + onEnd() { + this.failingTestCaseResults.clear(); + } + + printsToStdio() { + return false; + } +} + +module.exports = FlakyTestsReporter; diff --git a/test/e2e/config/global-setup.ts b/test/e2e/config/global-setup.ts new file mode 100644 index 0000000000000..10f2822fdfe1a --- /dev/null +++ b/test/e2e/config/global-setup.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { request } from '@playwright/test'; +import type { FullConfig } from '@playwright/test'; + +/** + * WordPress dependencies + */ +import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; + +async function globalSetup( config: FullConfig ) { + const { storageState, baseURL } = config.projects[ 0 ].use; + const storageStatePath = + typeof storageState === 'string' ? storageState : undefined; + + const requestContext = await request.newContext( { + baseURL, + } ); + + const requestUtils = new RequestUtils( requestContext, { + storageStatePath, + } ); + + // Authenticate and save the storageState to disk. + await requestUtils.setupRest(); + + await requestContext.dispose(); +} + +export default globalSetup; diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts new file mode 100644 index 0000000000000..a78a60c7d7449 --- /dev/null +++ b/test/e2e/playwright.config.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import path from 'path'; +import { devices } from '@playwright/test'; +import type { PlaywrightTestConfig } from '@playwright/test'; + +const STORAGE_STATE_PATH = + process.env.STORAGE_STATE_PATH || + path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); + +const config: PlaywrightTestConfig = { + reporter: process.env.CI + ? [ [ 'github' ], [ './config/flaky-tests-reporter.ts' ] ] + : 'list', + forbidOnly: !! process.env.CI, + workers: 1, + retries: process.env.CI ? 2 : 0, + timeout: parseInt( process.env.TIMEOUT || '', 10 ) || 100_000, // Defaults to 100 seconds. + // Don't report slow test "files", as we will be running our tests in serial. + reportSlowTests: null, + testDir: new URL( './specs', 'file:' + __filename ).pathname, + outputDir: path.join( process.cwd(), 'artifacts/test-results' ), + globalSetup: new URL( './config/global-setup.ts', 'file:' + __filename ) + .pathname, + use: { + baseURL: process.env.WP_BASE_URL || 'http://localhost:8889', + headless: true, + viewport: { + width: 960, + height: 700, + }, + ignoreHTTPSErrors: true, + locale: 'en-US', + contextOptions: { + reducedMotion: 'reduce', + strictSelectors: true, + }, + storageState: STORAGE_STATE_PATH, + actionTimeout: 10_000, // 10 seconds. + trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'on-first-retry', + }, + webServer: { + command: 'npm run wp-env start', + port: 8889, + timeout: 120_000, // 120 seconds. + reuseExistingServer: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices[ 'Desktop Chrome' ] }, + }, + ], +}; + +export default config; diff --git a/test/e2e/specs/sanity.spec.js b/test/e2e/specs/sanity.spec.js new file mode 100644 index 0000000000000..4f9464da854b2 --- /dev/null +++ b/test/e2e/specs/sanity.spec.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +// Example sanity check test, should be removed once there are other tests. +test.describe( 'Sanity check', () => { + test( 'Expect site loaded', async ( { page, pageUtils } ) => { + await pageUtils.visitAdminPage( '/' ); + + await expect( page ).toHaveTitle( /Dashboard/ ); + } ); +} ); diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json new file mode 100644 index 0000000000000..afd38ab25fa5e --- /dev/null +++ b/test/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false, + "allowJs": true, + "checkJs": false + }, + "include": [ + "**/*" + ], + "exclude": [] +} diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 8bad52d969c07..985edca495e6e 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -26,6 +26,7 @@ module.exports = { '/.git/', '/node_modules/', '/packages/e2e-tests', + '/packages/e2e-test-utils-playwright/src/test.ts', '/.*/build/', '/.*/build-module/', '/.+.native.js$', diff --git a/tsconfig.json b/tsconfig.json index 53ab6c77aa41b..8a78ab8e69a83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ { "path": "packages/dom" }, { "path": "packages/element" }, { "path": "packages/dom-ready" }, + { "path": "packages/e2e-test-utils-playwright" }, { "path": "packages/escape-html" }, { "path": "packages/eslint-plugin" }, { "path": "packages/html-entities" }, From 7ef158b449c28d97b590ef544341a63f6462b851 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 18 Mar 2022 10:07:50 +0800 Subject: [PATCH 2/6] Fix comment of visitAdminPage --- packages/e2e-test-utils-playwright/src/page/visit-admin-page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js b/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js index 4314f12fa9831..79fb53da26dea 100644 --- a/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js +++ b/packages/e2e-test-utils-playwright/src/page/visit-admin-page.js @@ -4,7 +4,7 @@ import { join } from 'path'; /** - * Visits admin page; if user is not logged in then it logging in it first, then visits admin page. + * Visits admin page and handle errors. * * @this {import('./').PageUtils} * @param {string} adminPath String to be serialized as pathname. From 85ef71f899bdab0ebbb7f4e9476068c9ffeea9b4 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 18 Mar 2022 14:36:27 +0800 Subject: [PATCH 3/6] Add initial guideline for the migration --- docs/contributors/code/testing-overview.md | 4 +- test/e2e/MIGRATION.md | 21 ++++++ test/e2e/README.md | 75 ++++++++++++++++------ 3 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 test/e2e/MIGRATION.md diff --git a/docs/contributors/code/testing-overview.md b/docs/contributors/code/testing-overview.md index 5b25051c2d0e0..21dbb4ddd9b1d 100644 --- a/docs/contributors/code/testing-overview.md +++ b/docs/contributors/code/testing-overview.md @@ -485,7 +485,9 @@ There is an ongoing effort to add integration tests to the native mobile project ## End-to-end Testing -End-to-end tests use [Puppeteer](https://github.com/puppeteer/puppeteer) as a headless Chromium driver, and are otherwise still run by a [Jest](https://jestjs.io/) test runner. +End-to-end tests currently use [Puppeteer](https://github.com/puppeteer/puppeteer) as a headless Chromium driver to run the tests in `packages/e2e-tests`, and are otherwise still run by a [Jest](https://jestjs.io/) test runner. + +> There's a ongoing [project](https://github.com/WordPress/gutenberg/issues/38851) to migrate them from Puppeteer to Playwright. See the [README](https://github.com/WordPress/gutenberg/tree/HEAD/test/e2e/README.md) of the new E2E tests for the updated guideline and best practices. ### Using wp-env diff --git a/test/e2e/MIGRATION.md b/test/e2e/MIGRATION.md new file mode 100644 index 0000000000000..e914e45edd4e6 --- /dev/null +++ b/test/e2e/MIGRATION.md @@ -0,0 +1,21 @@ +# Migration guide + +## Migration steps for tests + +1. Choose a test suite to migrate in `packages/e2e-tests/specs`, rename `.test.js` into `.spec.js` and put it in the same folder structure inside `test/e2e/specs`. +2. Require the test helpers from `@wordpress/e2e-test-utils-playwright`: + ```js + const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + ``` +3. Change all occurrences of `describe`, `beforeAll`, `beforeEach`, `afterEach` and `afterAll` with the `test.` prefix. For instance, `describe` turns into `test.describe`. +4. Use the [fixtures API](https://playwright.dev/docs/test-fixtures) to require previously global variables like `page` and `browser`. +5. Delete all the imports of `e2e-test-utils`. Instead, use the fixtures API to directly get the `pageUtils` and `requestUtils`. (However, `pageUtils` is not allowed in `beforeAll` and `afterAll`, rewrite them using `requestUtils` instead.) +6. If there's a missing util, go ahead and [migrate it](#migration-steps-for-test-utils). +7. Manually migrate other details in the tests following the proposed [best practices](https://github.com/WordPress/gutenberg/tree/HEAD/test/e2e/README.md#best-practices). Note that even though the differences in the API of Playwright and Puppeteer are similar, some manual changes are still required. + +## Migration steps for test utils + +1. Copy the existing file in `e2e-test-utils` and paste it in the `page` or `request` folder in `e2e-test-utils-playwright` depending on whether it's a page util or a request util. (If it sets or clears states, then it probably is a request util. If it can only be used when a page instance is available, it probably is a page util.) +2. Global `page` and `browser` are available in `pageUtils`'s `this.page` and `this.browser`. You can get autocomplete and type inference by adding `@this {import('./').PageUtils}` to the JSDoc. +3. All the other utils in the same class are available in `this` and bound to the same instance. You can remove the internal imports and use `this` to access them. +4. Import the newly migrated util in `index.ts` and put it inside the `PageUtils`/`RequestUtils` class as an instance field. diff --git a/test/e2e/README.md b/test/e2e/README.md index 96a6f39baf9ef..d14766e6e6ce6 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -2,39 +2,78 @@ End-To-End (E2E) tests for WordPress. -## Running tests +This directory is the new place for E2E tests in Gutenberg. We expect new tests to be placed here and follow the [best practices](#best-practices) listed below. We use [Playwright](https://playwright.dev/) and its test runner to run the tests in Chromium by default. [`@wordpress/e2e-test-utils`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) is used as a helper package to simplify the usages. See the documentation of both for more information. -The following command is available on the Gutenberg repo: +See the [migration guide](https://github.com/WordPress/gutenberg/tree/HEAD/test/e2e/MIGRATION.md) if you're coming from the previous Jest + Puppeteer framework. -```json -{ - "test-e2e:playwright": "playwright test --config test/e2e/playwright.config.ts" -} +## Best practices + +### Forbid `$`, use `locator` instead + +In fact, any API that returns `ElementHandle` is [discouraged](https://playwright.dev/docs/api/class-page#page-query-selector). This includes `$`, `$$`, `$eval`, `$$eval`, etc. [`Locator`](https://playwright.dev/docs/api/class-locator) is a much better API and can be used with playwright's [assertions](https://playwright.dev/docs/api/class-locatorassertions). This also works great with Page Object Model since that locator is lazy and doesn't return a promise. + +### Use accessible selectors + +Use the selector engine [role-selector](https://github.com/kevin940726/role-selector) to construct the query wherever possible. It enables us to write accessible queries without having to rely on internal implementations. It's an experimental library and could be swapped out if playwright supports it [natively](https://github.com/microsoft/playwright/issues/11182) instead. The syntax should be straightforward and looks like this: + +```js +// Select a button with the accessible name "Hello World". +page.locator( 'role=button[name="Hello World"]' ); ``` -### Run all available tests -```bash -npm run test-e2e:playwright +It can also be chained with built-in selector engines to perform complex queries: + +```js +// Select a button with a name ends with `Back` and is visible on the screen. +page.locator( 'role=button[name=/Back$/] >> visible=true' ); +// Select a button with name "View options" under `#some-section`. +page.locator( 'css=#some-section >> role=button[name="View options"]' ); ``` -### Headed mode +`role-selector` under the hood uses [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) to compute the query the elements and compute the accessible attributes. + +### Selectors are strict by default + +To encourage better practices for querying elements, selectors are [strict](https://playwright.dev/docs/api/class-browser#browser-new-page-option-strict-selectors) by default, meaning that it will throw an error if the query returns more than one element. + +### Don't overload test-utils, inline simple utils + +`e2e-test-utils` are too bloated with too many utils. Most of them are simple enough to be inlined directly in tests. With the help of accessible selectors, simple utils are easier to write now. For utils that only take place on a certain page, use Page Object Model instead (with an exception of clearing states with `requestUtils` which are better placed in `e2e-test-utils`). Otherwise, only create an util if the action is complex and repetitive enough. + +### Favor Page Object Model over utils + +As mentioned above, [Page Object Model](https://playwright.dev/docs/test-pom) is the preferred way to create reusable utility functions on a certain page. + +The rationale behind using a POM is to group utils under namespaces to be easier to discover and use. In fact, `PageUtils` in the `e2e-test-utils-playwright` package is also a POM, which avoids the need for global variables, and utils can reference each other with `this`. + +### Restify actions to clear or set states. + +It's slow to set states manually before or after tests, especially when they're repeated multiple times between tests. It's recommended to set them via API calls. Use `requestUtils.rest` and `requestUtils.batchRest` instead to call the [REST API](https://developer.wordpress.org/rest-api/reference/) (and add them to `requestUtils` if needed). We should still add a test for manually setting them, but that should only be tested once. + +### Avoid global variables + +Previously in our Jest + Puppeteer E2E tests, `page` and `browser` are exposed as global variables. This makes it harder to work with when we have multiple pages/tabs in the same test, or if we want to run multiple tests in parallel. `@playwright/test` has the concept of [fixtures](https://playwright.dev/docs/test-fixtures) which allows us to inject `page`, `browser`, and other parameters into the tests. + +### Make explicit assertions + +We can insert as many assertions in one test as needed. It's better to make explicit assertions whenever possible. For instance, if we want to assert that a button exists before clicking on it, we can do `expect( locator ).toBeVisible()` before performing `locator.click()`. This makes the tests flow better and easier to read. + +## Commands ```bash +# Run all available tests. +npm run test-e2e:playwright + +# Run in headed mode. npm run test-e2e:playwright -- --headed -``` -### Run a specific test file -```bash +# Run a single test file. npm run test-e2e:playwright -- -``` -### Debugging -Makes e2e tests available to debug in Playwright Inspector. -```bash +# Debugging npm run test-e2e:playwright -- --debug ``` - **Note**: This package requires Node.js 12.0.0 or later. It is not compatible with older versions.

Code is Poetry.

From 701c92abeb7acf09185cfcc7c4946eccf732dbb6 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 18 Mar 2022 14:37:34 +0800 Subject: [PATCH 4/6] Mark e2e-test-utils-playwright as private for now --- docs/manifest.json | 6 ------ packages/e2e-test-utils-playwright/package.json | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 4c9424d4f7f84..7251f7882dba4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1499,12 +1499,6 @@ "markdown_source": "../packages/dom/README.md", "parent": "packages" }, - { - "title": "@wordpress/e2e-test-utils-playwright", - "slug": "packages-e2e-test-utils-playwright", - "markdown_source": "../packages/e2e-test-utils-playwright/README.md", - "parent": "packages" - }, { "title": "@wordpress/e2e-test-utils", "slug": "packages-e2e-test-utils", diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index d1f8c53d4ecd0..8979cdbbb0267 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -1,6 +1,7 @@ { "name": "@wordpress/e2e-test-utils-playwright", "version": "0.0.0", + "private": true, "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 243db4f856ef1b01eee3b9ba277448db5c6fa0eb Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 22 Mar 2022 10:58:44 +1100 Subject: [PATCH 5/6] Fix link name --- test/e2e/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/README.md b/test/e2e/README.md index d14766e6e6ce6..0a6ecdc312191 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -2,7 +2,7 @@ End-To-End (E2E) tests for WordPress. -This directory is the new place for E2E tests in Gutenberg. We expect new tests to be placed here and follow the [best practices](#best-practices) listed below. We use [Playwright](https://playwright.dev/) and its test runner to run the tests in Chromium by default. [`@wordpress/e2e-test-utils`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) is used as a helper package to simplify the usages. See the documentation of both for more information. +This directory is the new place for E2E tests in Gutenberg. We expect new tests to be placed here and follow the [best practices](#best-practices) listed below. We use [Playwright](https://playwright.dev/) and its test runner to run the tests in Chromium by default. [`@wordpress/e2e-test-utils-playwright`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) is used as a helper package to simplify the usages. See the documentation of both for more information. See the [migration guide](https://github.com/WordPress/gutenberg/tree/HEAD/test/e2e/MIGRATION.md) if you're coming from the previous Jest + Puppeteer framework. From 998f48091ee2666e4fb54d5c569512b9e40497eb Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 22 Mar 2022 16:24:51 +0800 Subject: [PATCH 6/6] Mark e2e-tests as private and deprecate puppeteer packages --- docs/manifest.json | 6 ------ packages/e2e-test-utils/README.md | 2 ++ packages/e2e-tests/README.md | 2 ++ packages/e2e-tests/package.json | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index 7251f7882dba4..070a2ef8709c4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1505,12 +1505,6 @@ "markdown_source": "../packages/e2e-test-utils/README.md", "parent": "packages" }, - { - "title": "@wordpress/e2e-tests", - "slug": "packages-e2e-tests", - "markdown_source": "../packages/e2e-tests/README.md", - "parent": "packages" - }, { "title": "@wordpress/edit-post", "slug": "packages-edit-post", diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 91232e0aa6940..2582a6fcc5267 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -4,6 +4,8 @@ End-To-End (E2E) test utils for WordPress. _It works properly with the minimum version of Gutenberg `9.2.0` or the minimum version of WordPress `5.6.0`._ +**Note that there's currently an ongoing [project](https://github.com/WordPress/gutenberg/issues/38851) to migrate E2E tests to Playwright instead. This package is deprecated and will only accept bug fixes until fully migrated.** + ## Installation Install the module diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index 3024112381e8c..de87194690db8 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -2,6 +2,8 @@ End-To-End (E2E) tests for WordPress. +**Note that there's currently an ongoing [project](https://github.com/WordPress/gutenberg/issues/38851) to migrate E2E tests to Playwright instead. This package is deprecated and will only accept bug fixes until fully migrated.** + ## Installation Install the module diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 6847583330d36..63258d8498e18 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,5 +1,6 @@ { "name": "@wordpress/e2e-tests", + "private": true, "version": "3.1.1", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors",