diff --git a/plugins/woocommerce-blocks/.github/workflows/php-js-e2e-tests.yml b/plugins/woocommerce-blocks/.github/workflows/php-js-e2e-tests.yml index eb23d10fa1d..841746849f7 100644 --- a/plugins/woocommerce-blocks/.github/workflows/php-js-e2e-tests.yml +++ b/plugins/woocommerce-blocks/.github/workflows/php-js-e2e-tests.yml @@ -37,7 +37,7 @@ jobs: run: npm ci --no-optional - name: Build Assets - run: FORCE_REDUCED_MOTION=true npm run build:e2e-test + run: FORCE_REDUCED_MOTION=true npm run build - name: blocks.ini setup run: | @@ -124,7 +124,7 @@ jobs: npm install --no-optional --no-audit - name: Build Assets - run: FORCE_REDUCED_MOTION=true npm run build:e2e-test + run: FORCE_REDUCED_MOTION=true npm run build - name: blocks.ini setup run: | diff --git a/plugins/woocommerce-blocks/.github/workflows/playwright.yml b/plugins/woocommerce-blocks/.github/workflows/playwright.yml index 3ee5237eb63..a11fac4e599 100644 --- a/plugins/woocommerce-blocks/.github/workflows/playwright.yml +++ b/plugins/woocommerce-blocks/.github/workflows/playwright.yml @@ -1,5 +1,4 @@ name: Playwright Tests - on: push: branches: [ trunk ] @@ -7,27 +6,12 @@ on: jobs: PlaywrightE2ETests: - if: false + name: Playwright E2E tests timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - ref: trunk - - - name: Cache node_modules - id: cache-node-modules - uses: actions/cache@v3 - env: - cache-name: cache-node-modules - with: - path: node_modules - key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - name: Setup node version and npm cache uses: actions/setup-node@v3 @@ -36,27 +20,45 @@ jobs: cache: 'npm' - name: Install Node dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: npm install --no-optional --no-audit + run: npm ci - - name: Load wp-env - run: npm run env:start --update + - name: Build Assets + run: FORCE_REDUCED_MOTION=true npm run build - - name: Fix permissions # We need to figure this out https://github.com/WordPress/gutenberg/issues/22515#issuecomment-1308346256 + - name: blocks.ini setup run: | - WP_ENV_DIR=$(npm run wp-env install-path --silent 2>&1 | head -1) - cd $WP_ENV_DIR - mkdir -p tests-WordPress/wp-content/languages tests-WordPress/wp-content/upgrade - chmod -R 767 tests-WordPress/wp-content/languages tests-WordPress/wp-content/upgrade - cd - + echo -e 'woocommerce_blocks_phase = 3\nwoocommerce_blocks_env = tests' > blocks.ini + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- - - name: Install Playwright Browsers + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + coverage: none + tools: composer + + - name: Composer install + run: composer install + + - name: Install Playwright run: npx playwright install --with-deps + + - name: Load wp-env + run: npm run env:start + - name: Run Playwright tests - run: npx playwright test --config=tests/e2e-pw/playwright.config.ts + run: npm run test:e2e-pw + - uses: actions/upload-artifact@v3 with: name: playwright-report - path: playwright-report/ - retention-days: 30 + path: ./tests/e2e-pw/artifacts/test-results diff --git a/plugins/woocommerce-blocks/.github/workflows/unit-tests.yml b/plugins/woocommerce-blocks/.github/workflows/unit-tests.yml index 8ee113d6d03..0c15515760e 100644 --- a/plugins/woocommerce-blocks/.github/workflows/unit-tests.yml +++ b/plugins/woocommerce-blocks/.github/workflows/unit-tests.yml @@ -37,7 +37,7 @@ jobs: - name: Npm install and build run: | npm ci --no-optional - FORCE_REDUCED_MOTION=true npm run build:e2e-test + FORCE_REDUCED_MOTION=true npm run build - name: blocks.ini setup run: echo -e 'woocommerce_blocks_phase = 3\nwoocommerce_blocks_env = test' > blocks.ini diff --git a/plugins/woocommerce-blocks/.gitignore b/plugins/woocommerce-blocks/.gitignore index f865be23e4a..f3ec8cbd182 100644 --- a/plugins/woocommerce-blocks/.gitignore +++ b/plugins/woocommerce-blocks/.gitignore @@ -44,6 +44,8 @@ tests/cli/vendor # E2E tests /tests/e2e-tests/config/local-*.json /tests/e2e-pw/test-results/ +/tests/e2e-pw/artifacts/ +/artifacts/ # Logs /logs diff --git a/plugins/woocommerce-blocks/.wp-env.json b/plugins/woocommerce-blocks/.wp-env.json index 8a150435888..eb415e8c496 100644 --- a/plugins/woocommerce-blocks/.wp-env.json +++ b/plugins/woocommerce-blocks/.wp-env.json @@ -3,11 +3,23 @@ "plugins": [ "https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip", "https://github.com/WP-API/Basic-Auth/archive/master.zip", - "./tests/mocks/woo-test-helper", - "." + "https://downloads.wordpress.org/plugin/wordpress-importer.0.8.zip", + "./tests/mocks/woo-test-helper" ], + "env": { + "tests": { + "mappings": { + "wp-content/mu-plugins": "./node_modules/@wordpress/e2e-tests/mu-plugins", + "wp-content/plugins/gutenberg-test-plugins": "./node_modules/@wordpress/e2e-tests/plugins", + "wp-content/plugins/woocommerce-blocks": ".", + "wp-cli.yml": "./wp-cli.yml" + } + } + }, "themes": [ "https://downloads.wordpress.org/theme/storefront.latest-stable.zip", + "https://downloads.wordpress.org/theme/twentytwentyone.latest-stable.zip", + "https://downloads.wordpress.org/theme/twentytwentythree.latest-stable.zip", "./tests/mocks/emptytheme", "./tests/mocks/theme-with-woo-templates" ], diff --git a/plugins/woocommerce-blocks/bin/wp-env-config.sh b/plugins/woocommerce-blocks/bin/wp-env-config.sh index 2d0631cf725..95d4754d05d 100755 --- a/plugins/woocommerce-blocks/bin/wp-env-config.sh +++ b/plugins/woocommerce-blocks/bin/wp-env-config.sh @@ -13,8 +13,7 @@ else fi ## set permalinks for easier wp-json -wp rewrite structure '/%postname%/' -wp rewrite flush +wp rewrite structure '/%postname%/' --hard wp core version --extra wp plugin list wp theme activate storefront @@ -22,6 +21,7 @@ wp wc customer update 1 --user=1 --billing='{"first_name":"John","last_name":"Do ## Prepare translation for the test suite wp language core install nl_NL wp language plugin install woocommerce nl_NL +wp plugin activate woocommerce-blocks ## We download a full version of .po (that has translation for js files as well). curl https://translate.wordpress.org/projects/wp-plugins/woo-gutenberg-products-block/stable/nl/default/export-translations/ --output ./wp-content/languages/plugins/woo-gutenberg-products-block-nl_NL.po sleep 5 diff --git a/plugins/woocommerce-blocks/docs/contributors/README.md b/plugins/woocommerce-blocks/docs/contributors/README.md index a2836e09bc3..4d81636218b 100644 --- a/plugins/woocommerce-blocks/docs/contributors/README.md +++ b/plugins/woocommerce-blocks/docs/contributors/README.md @@ -7,6 +7,7 @@ This folder contains documentation for developers and contributors looking to ge | [Getting Started](contributing/getting-started.md) | This doc covers tooling and creating builds during development. | | [Coding Guidelines](contributing/coding-guidelines.md) | This doc covers development best practices. | | [JavaScript Testing](contributing/javascript-testing.md) | This doc explains how to run automated JavaScript tests. | +| [E2E Testing Guidelines](contributing/e2e-guidelines.md) | This doc covers development best practices about E2E tests. | | [Developing Components (& Storybook)](components.md) | This doc outlines where our reusable components live, and how to test them in Storybook. | | [Block Script Assets](contributing/block-assets.md) | This doc explains how Block Script Assets are loaded and used. | | [JS build system](contributing/javascript-build-system.md) | This doc explains how JavaScript files are built. | diff --git a/plugins/woocommerce-blocks/docs/contributors/contributing/README.md b/plugins/woocommerce-blocks/docs/contributors/contributing/README.md index 62fb54c25aa..416eb02547f 100644 --- a/plugins/woocommerce-blocks/docs/contributors/contributing/README.md +++ b/plugins/woocommerce-blocks/docs/contributors/contributing/README.md @@ -2,15 +2,17 @@ This folder contains documentation for developers and contributors looking to get started with WooCommerce Block Development. -| Document | Description | -| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| [Getting Started](getting-started.md) | This doc covers tooling and creating builds during development. | -| [Coding Guidelines](coding-guidelines.md) | This doc covers development best practices. | -| [Block Script Assets](block-assets.md) | This doc explains how Block Script Assets are loaded and used. | -| [CSS Build System](css-build-system.md) | This doc explains how CSS files are built. | -| [JavaScript Build System](javascript-build-system.md) | This doc explains how JavaScript files are built. | -| [JavaScript Testing](javascript-testing.md) | This doc explains how to run automated JavaScript tests. | -| [Storybook & Components](storybook-and-components.md) | This doc outlines where our reusable components live, and how to test them in Storybook. | +| Document | Description | +|----------------------------------------------------------|------------------------------------------------------------------------------------------| +| [Getting Started](getting-started.md) | This doc covers tooling and creating builds during development. | +| [Coding Guidelines](coding-guidelines.md) | This doc covers development best practices. | +| [Block Script Assets](block-assets.md) | This doc explains how Block Script Assets are loaded and used. | +| [CSS Build System](css-build-system.md) | This doc explains how CSS files are built. | +| [JavaScript Build System](javascript-build-system.md) | This doc explains how JavaScript files are built. | +| [JavaScript Testing](javascript-testing.md) | This doc explains how to run automated JavaScript tests. | +| [E2E Testing Guidelines](e2e-guidelines.md) | This doc covers development best practices about E2E tests. | +| [Storybook & Components](storybook-and-components.md) | This doc outlines where our reusable components live, and how to test them in Storybook. | + diff --git a/plugins/woocommerce-blocks/docs/contributors/contributing/e2e-guidelines.md b/plugins/woocommerce-blocks/docs/contributors/contributing/e2e-guidelines.md new file mode 100644 index 00000000000..5db4ea854a4 --- /dev/null +++ b/plugins/woocommerce-blocks/docs/contributors/contributing/e2e-guidelines.md @@ -0,0 +1,55 @@ +# E2E Guidelines + +## Table of contents + +- [Structure](#structure) +- [Playwright](#playwright) + - [Structure](#structure-1) + +This living document serves to prescribe coding guidelines specific to the WooCommerce Blocks project E2E tests. For more information on how to run Playwright end-to-end (E2E) tests, please refer to the [dedicated resource](../../../tests/e2e-pw/README.md). + +## Structure + +There are two folders dedicated to E2E tests. + +The first folder is named "e2e" and it contains all the E2E tests that were created with the deprecated infrastructure Jest + Puppetter. The "e2e-pw" folder contains all the E2E tests that were created with the current infrastructure: Playwright. These tests are actively maintained and should be used for all new E2E testing. + +### Playwright + +#### Structure + +There are two Playwright projects configuration: + +- blockTheme +- classicTheme + +The blockTheme project runs the tests with the suffix *block_theme*. In this case, the theme is a block theme. The block theme is the default WordPress theme. Currently, it is Twenty-Twenty Three. You should use this configuration if you want test the block with the Site Editor. + +The classicTheme project runs the tests with the suffix *classic_theme*. In this case, the theme is a Twenty Twenty-One. You should use this configuration if you want test the block with a classic theme. + +Each block should have a dedicated folder with a scoped util file if you want share some logic related to the block. + +#### Code Guidelines + +##### Make tests as isolated as possible - Avoid side effects + +Each test should be completely isolated from another test and should run independently with its own local storage, session storage, data, cookies etc. Test isolation improves reproducibility, makes debugging easier and prevents cascading test failures. + +In order to avoid repetition for a particular part of your test you can use before and after hooks. Within your test file add a before hook to run a part of your test before each test such as going to a particular URL or logging in to a part of your app. This keeps your tests isolated as no test relies on another. However it is also ok to have a little duplication when tests are simple enough especially if it keeps your tests clearer and easier to read and maintain. Avoid using functions that impact other tests, such as the `deleteAllTemplates` function, which restores all templates and can break other tests since E2E tests run in parallel. After running a suite of tests for a specific block, it is important to clean up any changes made during the tests to ensure a clean slate for subsequent test runs. + +For more detail see [Make Tests as Isolated as Possible](https://playwright.dev/docs/best-practices#make-tests-as-isolated-as-possible). + +##### Use Locators + +In order to write end to end tests we need to first find elements on the webpage. We can do this by using Playwright's built in locators. Locators come with auto waiting and retry-ability. Auto waiting means that Playwright performs a range of actionability checks on the elements, such as ensuring the element is visible and enabled before it performs the click. To make tests resilient, we recommend prioritizing user-facing attributes and explicit contracts. For more detail see [Use Locators](https://playwright.dev/docs/best-practices#use-locators). + +##### Avoid Using Relative Imports + +In order to make the codebase cleaner, you should import the function from the packages: + +- "@woocommerce/e2e-utils": Contains generic utils for interactive with the page. +- "@woocommerce/e2e-types": Contains generic types. +- "@woocommerce/e2e-playwright-utils": Contains utils for playwright for example custom hooks. + +By using these packages, you can make your code more modular and easier to maintain. + diff --git a/plugins/woocommerce-blocks/docs/contributors/contributing/javascript-testing.md b/plugins/woocommerce-blocks/docs/contributors/contributing/javascript-testing.md index e11adf0bf43..368fc663f1f 100644 --- a/plugins/woocommerce-blocks/docs/contributors/contributing/javascript-testing.md +++ b/plugins/woocommerce-blocks/docs/contributors/contributing/javascript-testing.md @@ -36,7 +36,7 @@ Additionally, - `test:update` updates the snapshot tests for components, used if you change a component that has tests attached. - `test:watch` keeps watch of files and automatically re-runs tests when things change. -## How to run end-to-end tests +## How to run end-to-end tests with deprecated infrastructure End-to-end tests are implemented in `tests/e2e-tests/specs/`. @@ -44,7 +44,7 @@ Since these drive the user interface, they need to run against a test environmen To set up to run e2e tests: -- `npm run build:e2e-test` builds the assets (js/css), you can exclude this step if you've already got built files to test with. +- `npm run build` builds the assets (js/css), you can exclude this step if you've already got built files to test with. - `npm run wp-env start` to start the test environment Then, to run the tests: @@ -60,6 +60,10 @@ When you're done, you may want to shut down the test environment: **Note:** There are a number of other useful `wp-env` commands. You can find out more in the [wp-env docs](https://github.com/WordPress/gutenberg/blob/master/packages/env/README.md). +## How to run end-to-end tests + +Visit the [dedicated documentation](../../../tests/e2e-pw/README.md). + ### Debugging e2e tests using generated reports When e2e test suites are run in a GitHub automation, a report is generated automatically for every suite that failed. This can be a useful tool to debug failing tests, as it provides a visual way to inspect the tests that failed and, additionally, it includes some screenshots. diff --git a/plugins/woocommerce-blocks/package-lock.json b/plugins/woocommerce-blocks/package-lock.json index fcec97b4b20..435be080c4b 100644 --- a/plugins/woocommerce-blocks/package-lock.json +++ b/plugins/woocommerce-blocks/package-lock.json @@ -111,7 +111,8 @@ "@wordpress/dependency-extraction-webpack-plugin": "3.2.1", "@wordpress/dom": "3.27.0", "@wordpress/e2e-test-utils": "10.1.0", - "@wordpress/e2e-tests": "4.6.0", + "@wordpress/e2e-test-utils-playwright": "https://github.com/woocommerce/woocommerce-blocks/files/11286971/wordpress-e2e-test-utils-playwright-0.0.0.tgz", + "@wordpress/e2e-tests": "^4.6.0", "@wordpress/element": "4.20.0", "@wordpress/env": "5.16.0", "@wordpress/html-entities": "3.24.0", @@ -14131,6 +14132,65 @@ "puppeteer-core": ">=11" } }, + "node_modules/@wordpress/e2e-test-utils-playwright": { + "version": "0.0.0", + "resolved": "https://github.com/woocommerce/woocommerce-blocks/files/11286971/wordpress-e2e-test-utils-playwright-0.0.0.tgz", + "integrity": "sha512-BLtq8Vcsyxbac1d8I+klE2Zrhbso3DJ2YTRl3ZmZ6KMiIiYWCvPLLIbtYAWiIDy6NU2jiT1wLlJ4G4jyV3DhIQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/keycodes": "file:../keycodes", + "@wordpress/url": "file:../url", + "change-case": "^4.1.2", + "form-data": "^4.0.0", + "mime": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@playwright/test": ">=1" + } + }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/api-fetch": { + "resolved": "node_modules/@wordpress/api-fetch", + "link": true + }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/keycodes": { + "resolved": "node_modules/@wordpress/keycodes", + "link": true + }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/@wordpress/url": { + "resolved": "node_modules/@wordpress/url", + "link": true + }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@wordpress/e2e-test-utils/node_modules/@wordpress/api-fetch": { "version": "6.27.0", "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.27.0.tgz", @@ -61979,6 +62039,87 @@ } } }, + "@wordpress/e2e-test-utils-playwright": { + "version": "https://github.com/woocommerce/woocommerce-blocks/files/11286971/wordpress-e2e-test-utils-playwright-0.0.0.tgz", + "integrity": "sha512-BLtq8Vcsyxbac1d8I+klE2Zrhbso3DJ2YTRl3ZmZ6KMiIiYWCvPLLIbtYAWiIDy6NU2jiT1wLlJ4G4jyV3DhIQ==", + "dev": true, + "requires": { + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/keycodes": "file:../keycodes", + "@wordpress/url": "file:../url", + "change-case": "^4.1.2", + "form-data": "^4.0.0", + "mime": "^3.0.0" + }, + "dependencies": { + "@wordpress/api-fetch": { + "version": "file:node_modules/@wordpress/api-fetch", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.24.0", + "@wordpress/url": "^3.25.0" + }, + "dependencies": { + "@wordpress/url": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.25.0.tgz", + "integrity": "sha512-2W4CP3Tyj7IRrPTb2XzUUvbckkimcW31v1g9Ly8ud1K0qSO4PvBrVxHfkEemkD9jI/KSvm3iPku++bhKY502wg==", + "requires": { + "@babel/runtime": "^7.16.0", + "remove-accents": "^0.4.2" + } + } + } + }, + "@wordpress/keycodes": { + "version": "file:node_modules/@wordpress/keycodes", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/i18n": "^4.30.0", + "change-case": "^4.1.2" + }, + "dependencies": { + "@wordpress/i18n": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.30.0.tgz", + "integrity": "sha512-vIntwrTBSU2MXOBlpyFntPgimHP+RW+k7/Y00BMPL+xoxPr7J7sXX/GNoYlH1BNsAo7XOi5AY5FrUnQ7ZIYdtQ==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/hooks": "^3.30.0", + "gettext-parser": "^1.3.1", + "memize": "^1.1.0", + "sprintf-js": "^1.1.1", + "tannin": "^1.2.0" + } + } + } + }, + "@wordpress/url": { + "version": "file:node_modules/@wordpress/url", + "requires": { + "@babel/runtime": "^7.16.0", + "remove-accents": "^0.4.2" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true + } + } + }, "@wordpress/e2e-tests": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@wordpress/e2e-tests/-/e2e-tests-4.6.0.tgz", diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index 4c58536978f..6e57db14ad2 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -39,7 +39,6 @@ "build": "rimraf build/* && cross-env BABEL_ENV=default NODE_ENV=production webpack", "build:check-assets": "rimraf build/* && cross-env ASSET_CHECK=true BABEL_ENV=default NODE_ENV=production webpack", "build:deploy": "rimraf vendor/* && cross-env WOOCOMMERCE_BLOCKS_PHASE=2 composer install --no-dev && cross-env WOOCOMMERCE_BLOCKS_PHASE=2 npm run build --loglevel error", - "build:e2e-test": "npm run build", "prebuild:docs": "rimraf docs/extensibility/actions.md & rimraf docs/extensibility/filters.md", "build:docs": "./vendor/bin/wp-hooks-generator --input=src --output=bin/hook-docs/data && node ./bin/hook-docs", "postbuild:docs": "./bin/add-doc-footer.sh", @@ -159,7 +158,8 @@ "@wordpress/dependency-extraction-webpack-plugin": "3.2.1", "@wordpress/dom": "3.27.0", "@wordpress/e2e-test-utils": "10.1.0", - "@wordpress/e2e-tests": "4.6.0", + "@wordpress/e2e-test-utils-playwright": "https://github.com/woocommerce/woocommerce-blocks/files/11286971/wordpress-e2e-test-utils-playwright-0.0.0.tgz", + "@wordpress/e2e-tests": "^4.6.0", "@wordpress/element": "4.20.0", "@wordpress/env": "5.16.0", "@wordpress/html-entities": "3.24.0", diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/bin/test-env-setup.sh b/plugins/woocommerce-blocks/tests/e2e-pw/bin/test-env-setup.sh index 4250d89e67c..e51afa16846 100755 --- a/plugins/woocommerce-blocks/tests/e2e-pw/bin/test-env-setup.sh +++ b/plugins/woocommerce-blocks/tests/e2e-pw/bin/test-env-setup.sh @@ -6,12 +6,6 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -################################################################################################### -# Install the WordPress Importer plugin and activate it -################################################################################################### - -wp-env run tests-cli "wp plugin install wordpress-importer --activate" - ################################################################################################### # Empty site to prevent conflicts with existing data ################################################################################################### @@ -342,9 +336,10 @@ wp-env run tests-cli "wp wc tax create \ ################################################################################################### # Adjust and flush rewrite rules ################################################################################################### - -wp-env run tests-cli "wp rewrite structure /%postname%/" -wp-env run tests-cli "wp rewrite flush" +# Currently, the rewrite rules don't work properly in the test environment: https://github.com/WordPress/gutenberg/issues/28201 +wp-env run tests-wordpress "chmod -c ugo+w /var/www/html" +wp-env run tests-cli "wp rewrite structure /%postname%/ --hard" +wp-env run tests-cli "wp rewrite flush --hard" ################################################################################################### # Create a customer diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/block-theme.setup.ts b/plugins/woocommerce-blocks/tests/e2e-pw/block-theme.setup.ts new file mode 100644 index 00000000000..c0980580fa5 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/block-theme.setup.ts @@ -0,0 +1,6 @@ +/** + * External dependencies + */ +import { BLOCK_THEME_SLUG, cli } from '@woocommerce/e2e-utils'; + +cli( `npm run wp-env run tests-cli "wp theme activate ${ BLOCK_THEME_SLUG }` ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/classic-theme.setup.ts b/plugins/woocommerce-blocks/tests/e2e-pw/classic-theme.setup.ts new file mode 100644 index 00000000000..1c76538a6de --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/classic-theme.setup.ts @@ -0,0 +1,8 @@ +/** + * External dependencies + */ +import { CLASSIC_THEME_SLUG, cli } from '@woocommerce/e2e-utils'; + +cli( + `npm run wp-env run tests-cli "wp theme activate ${ CLASSIC_THEME_SLUG }` +); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/global-setup.ts b/plugins/woocommerce-blocks/tests/e2e-pw/global-setup.ts index a838d1d6992..df5981615dd 100644 --- a/plugins/woocommerce-blocks/tests/e2e-pw/global-setup.ts +++ b/plugins/woocommerce-blocks/tests/e2e-pw/global-setup.ts @@ -1,37 +1,23 @@ +/* eslint-disable no-console */ /** * External dependencies */ -import { chromium, expect } from '@playwright/test'; +import { FullConfig, chromium, request } from '@playwright/test'; +import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; import fs from 'fs'; /** * Internal dependencies */ -import { admin, customer } from './test-data/data/data'; +import { customer } from './test-data/data/data'; -/* eslint-disable no-console */ - -module.exports = async ( config ) => { +const loginAsCustomer = async ( config: FullConfig ) => { const { stateDir, baseURL, userAgent } = config.projects[ 0 ].use; - console.log( `State Dir: ${ stateDir }` ); - console.log( `Base URL: ${ baseURL }` ); - // used throughout tests for authentication process.env.ADMINSTATE = `${ stateDir }adminState.json`; process.env.CUSTOMERSTATE = `${ stateDir }customerState.json`; - // Clear out the previous save states - try { - fs.unlinkSync( process.env.ADMINSTATE ); - console.log( 'Admin state file deleted successfully.' ); - } catch ( err ) { - if ( err.code === 'ENOENT' ) { - console.log( 'Admin state file does not exist.' ); - } else { - console.log( 'Admin state file could not be deleted: ' + err ); - } - } try { fs.unlinkSync( process.env.CUSTOMERSTATE ); console.log( 'Customer state file deleted successfully.' ); @@ -43,8 +29,6 @@ module.exports = async ( config ) => { } } - // Pre-requisites - let adminLoggedIn = false; let customerLoggedIn = false; // Specify user agent when running against an external test site to avoid getting HTTP 406 NOT ACCEPTABLE errors. @@ -52,70 +36,19 @@ module.exports = async ( config ) => { // Create browser, browserContext, and page for customer and admin users const browser = await chromium.launch(); - const adminContext = await browser.newContext( contextOptions ); const customerContext = await browser.newContext( contextOptions ); - const adminPage = await adminContext.newPage(); const customerPage = await customerContext.newPage(); - // Sign in as admin user and save state - const adminRetries = 5; - for ( let i = 0; i < adminRetries; i++ ) { - try { - console.log( 'Trying to log-in as admin...' ); - await adminPage.goto( `/wp-admin` ); - await adminPage.fill( 'input[name="log"]', admin.username ); - await adminPage.fill( 'input[name="pwd"]', admin.password ); - await adminPage.click( 'text=Log In' ); - await adminPage.waitForLoadState( 'networkidle' ); - await adminPage.goto( `/wp-admin` ); - await adminPage.waitForLoadState( 'domcontentloaded' ); - - await expect( adminPage.locator( 'div.wrap > h1' ) ).toHaveText( - 'Dashboard' - ); - await adminPage - .context() - .storageState( { path: process.env.ADMINSTATE } ); - console.log( 'Logged-in as admin successfully.' ); - adminLoggedIn = true; - break; - } catch ( e ) { - console.log( - `Admin log-in failed, Retrying... ${ i }/${ adminRetries }` - ); - console.log( e ); - } - } - - if ( ! adminLoggedIn ) { - console.error( - 'Cannot proceed e2e test, as admin login failed. Please check if the test site has been setup correctly.' - ); - - process.exit( 1 ); - } - // Sign in as customer user and save state const customerRetries = 5; for ( let i = 0; i < customerRetries; i++ ) { try { - console.log( 'Trying to log-in as customer...' ); await customerPage.goto( `/wp-admin` ); await customerPage.fill( 'input[name="log"]', customer.username ); await customerPage.fill( 'input[name="pwd"]', customer.password ); await customerPage.click( 'text=Log In' ); await customerPage.goto( `/my-account` ); - await expect( - customerPage.locator( - '.woocommerce-MyAccount-navigation-link--customer-logout' - ) - ).toBeVisible(); - await expect( - customerPage.locator( - 'div.woocommerce-MyAccount-content > p >> nth=0' - ) - ).toContainText( 'Hello' ); await customerPage .context() @@ -138,7 +71,27 @@ module.exports = async ( config ) => { process.exit( 1 ); } - await adminContext.close(); await customerContext.close(); await browser.close(); }; + +async function globalSetup( config: FullConfig ) { + const { storageState, baseURL } = config.projects[ 0 ].use; + const storageStatePath = + typeof storageState === 'string' ? storageState : ''; + + const requestContext = await request.newContext( { + baseURL: baseURL ?? '', + } ); + + const requestUtils = new RequestUtils( requestContext, { + storageStatePath, + } ); + + await requestUtils.setupRest(); + await requestContext.dispose(); + + await loginAsCustomer( config ); +} + +export default globalSetup; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/index.ts b/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/index.ts new file mode 100644 index 00000000000..607718c2a5c --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/index.ts @@ -0,0 +1 @@ +export * from './test'; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/test.ts b/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/test.ts new file mode 100644 index 00000000000..f3dc7ef94b7 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/playwright-utils/test.ts @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import { test as base, expect, request } from '@playwright/test'; +import type { ConsoleMessage } from '@playwright/test'; +import { + Admin, + Editor, + PageUtils, + RequestUtils, +} from '@wordpress/e2e-test-utils-playwright'; + +import { TemplateApiUtils, STORAGE_STATE_PATH } from '@woocommerce/e2e-utils'; + +/** + * 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; + } + + // 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< + { + admin: Admin; + editor: Editor; + pageUtils: PageUtils; + templateApiUtils: TemplateApiUtils; + snapshotConfig: void; + }, + { + requestUtils: RequestUtils; + } +>( { + admin: async ( { page, pageUtils }, use ) => { + await use( new Admin( { page, pageUtils } ) ); + }, + editor: async ( { page }, use ) => { + await use( new Editor( { page } ) ); + }, + 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 } ) ); + }, + templateApiUtils: async ( {}, use ) => + await use( new TemplateApiUtils( request ) ), + requestUtils: [ + async ( {}, use, workerInfo ) => { + const requestUtils = await RequestUtils.setup( { + baseURL: workerInfo.project.use.baseURL, + storageStatePath: STORAGE_STATE_PATH, + } ); + + await use( requestUtils ); + }, + { scope: 'worker', auto: true }, + ], +} ); + +export { test, expect }; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/playwright.config.ts b/plugins/woocommerce-blocks/tests/e2e-pw/playwright.config.ts index a722251670e..5124db0ea68 100644 --- a/plugins/woocommerce-blocks/tests/e2e-pw/playwright.config.ts +++ b/plugins/woocommerce-blocks/tests/e2e-pw/playwright.config.ts @@ -1,7 +1,11 @@ /** * External dependencies */ -import { defineConfig, devices, PlaywrightTestConfig } from '@playwright/test'; +import { defineConfig, PlaywrightTestConfig } from '@playwright/test'; +import { BASE_URL, STORAGE_STATE_PATH } from '@woocommerce/e2e-utils'; +import path from 'path'; + +import { fileURLToPath } from 'url'; interface ExtendedPlaywrightTestConfig extends PlaywrightTestConfig { use: { @@ -9,52 +13,52 @@ interface ExtendedPlaywrightTestConfig extends PlaywrightTestConfig { } & PlaywrightTestConfig[ 'use' ]; } -const { - BASE_URL, - CI, - DEFAULT_TIMEOUT_OVERRIDE, - E2E_MAX_FAILURES, - PLAYWRIGHT_HTML_REPORT, -} = process.env; +const { CI, DEFAULT_TIMEOUT_OVERRIDE, E2E_MAX_FAILURES } = process.env; const config: ExtendedPlaywrightTestConfig = { timeout: DEFAULT_TIMEOUT_OVERRIDE ? Number( DEFAULT_TIMEOUT_OVERRIDE ) : 90 * 1000, expect: { timeout: 20 * 1000 }, - outputDir: './test-results/report', - globalSetup: require.resolve( './global-setup' ), + outputDir: path.join( process.cwd(), 'artifacts/test-results' ), + globalSetup: fileURLToPath( + new URL( 'global-setup.ts', 'file:' + __filename ).href + ), globalTeardown: require.resolve( './global-teardown' ), testDir: 'tests', - retries: CI ? 4 : 0, + retries: CI ? 2 : 0, workers: 4, fullyParallel: true, - reporter: [ - [ 'list' ], - [ - 'html', - { - outputFolder: - PLAYWRIGHT_HTML_REPORT ?? - './test-results/playwright-report', - open: CI ? 'never' : 'always', - }, - ], - [ 'json', { outputFile: './test-results/test-results.json' } ], - ], + reporter: process.env.CI ? [ [ 'github' ], [ 'list' ] ] : 'list', maxFailures: E2E_MAX_FAILURES ? Number( E2E_MAX_FAILURES ) : 0, use: { - baseURL: BASE_URL ?? 'http://localhost:8889', + baseURL: BASE_URL, screenshot: 'only-on-failure', stateDir: './tests/e2e-pw/test-results/storage/', trace: 'retain-on-failure', video: 'on-first-retry', viewport: { width: 1280, height: 720 }, + storageState: STORAGE_STATE_PATH, }, projects: [ { - name: 'Chrome', - use: { ...devices[ 'Desktop Chrome' ] }, + name: 'blockThemeConfiguration', + testMatch: /block-theme.setup.ts/, + }, + { + name: 'blockTheme', + testMatch: /.*.block_theme.spec.ts/, + dependencies: [ 'blockThemeConfiguration' ], + }, + { + name: 'classicThemeConfiguration', + testMatch: /block-theme.setup.ts/, + dependencies: [ 'blockTheme' ], + }, + { + name: 'classicTheme', + testMatch: /.*.classic_theme.spec.ts/, + dependencies: [ 'classicThemeConfiguration' ], }, ], }; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.spec.ts b/plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.block_theme.spec.ts similarity index 81% rename from plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.spec.ts rename to plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.block_theme.spec.ts index 5bf22b622af..799dd1d830f 100644 --- a/plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tests/basic.block_theme.spec.ts @@ -1,23 +1,26 @@ /** * External dependencies */ -import { test, expect } from '@playwright/test'; +import { test, expect } from '@woocommerce/e2e-playwright-utils'; + +/** + * Internal dependencies + */ test.describe( 'A basic set of tests to ensure WP, wp-admin and my-account load', - () => { + async () => { test( 'Load the home page', async ( { page } ) => { await page.goto( '/' ); - const title = page.locator( 'header .wp-block-site-title a' ); + const title = page + .locator( 'header' ) + .locator( '.wp-block-site-title' ); await expect( title ).toHaveText( 'WooCommerce Blocks E2E Test Suite' ); } ); test.describe( 'Sign in as admin', () => { - test.use( { - storageState: process.env.ADMINSTATE, - } ); test( 'Load wp-admin', async ( { page } ) => { await page.goto( '/wp-admin' ); const title = page.locator( 'div.wrap > h1' ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/price-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/price-filter.block_theme.spec.ts new file mode 100644 index 00000000000..65eee1d1b26 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/price-filter.block_theme.spec.ts @@ -0,0 +1,181 @@ +/** + * External dependencies + */ +import { BlockData } from '@woocommerce/e2e-types'; +import { test, expect } from '@woocommerce/e2e-playwright-utils'; +import { BASE_URL, getBlockByName } from '@woocommerce/e2e-utils'; + +/** + * Internal dependencies + */ +import { getMinMaxPriceInputs } from './utils'; + +const blockData: BlockData< { + urlSearchParamWhenFilterIsApplied: string; + endpointAPI: string; + placeholderUrl: string; +} > = { + name: 'woocommerce/price-filter', + mainClass: '.wc-block-price-filter', + selectors: { + frontend: {}, + editor: {}, + }, + urlSearchParamWhenFilterIsApplied: '?max_price=10', + endpointAPI: 'max_price=1000', + placeholderUrl: `${ BASE_URL }/wp-content/plugins/woocommerce/assets/images/placeholder.png`, +}; + +test.describe( `${ blockData.name } Block - with All products Block`, () => { + test.beforeEach( async ( { admin, page, editor } ) => { + await admin.createNewPost(); + await editor.insertBlock( { name: 'woocommerce/all-products' } ); + await editor.insertBlock( { + name: 'woocommerce/filter-wrapper', + attributes: { + filterType: 'price-filter', + heading: 'Filter By Price', + }, + } ); + await editor.publishPost(); + await page.waitForLoadState( 'networkidle' ); + const url = new URL( page.url() ); + const postId = url.searchParams.get( 'post' ); + await page.goto( `/?p=${ postId }`, { waitUntil: 'networkidle' } ); + } ); + + test( 'should show all products', async ( { page } ) => { + const allProductsBlock = await getBlockByName( { + page, + name: 'woocommerce/all-products', + } ); + + await page.waitForLoadState( 'networkidle' ); + + const img = await allProductsBlock.locator( 'img' ).first(); + + await expect( img ).not.toHaveAttribute( + 'src', + blockData.placeholderUrl + ); + + const products = await allProductsBlock.getByRole( 'listitem' ).all(); + + expect( products ).toHaveLength( 9 ); + } ); + + test( 'should show only products that match the filter', async ( { + page, + pageUtils, + } ) => { + const { maxPriceInput } = await getMinMaxPriceInputs( { + page, + blockName: 'woocommerce/filter-wrapper', + } ); + + await maxPriceInput.selectText(); + await maxPriceInput.type( '10' ); + await pageUtils.pressKeys( 'Tab' ); + await page.waitForResponse( ( response ) => + response.url().includes( blockData.endpointAPI ) + ); + + await page.waitForLoadState( 'networkidle' ); + + const allProductsBlock = await getBlockByName( { + page, + name: 'woocommerce/all-products', + } ); + + await page.waitForLoadState( 'networkidle' ); + const img = await allProductsBlock.locator( 'img' ).first(); + + await expect( img ).not.toHaveAttribute( + 'src', + blockData.placeholderUrl + ); + + const products = await allProductsBlock.getByRole( 'listitem' ).all(); + + expect( products ).toHaveLength( 1 ); + expect( page.url() ).toContain( + blockData.urlSearchParamWhenFilterIsApplied + ); + } ); +} ); + +test.describe( `${ blockData.name } Block - with PHP classic template`, () => { + test.beforeEach( async ( { admin, page, editor } ) => { + await admin.visitSiteEditor( { + postId: 'woocommerce/woocommerce//archive-product', + postType: 'wp_template', + } ); + + await editor.canvas.click( 'body' ); + + await editor.insertBlock( { + name: 'woocommerce/filter-wrapper', + attributes: { + filterType: 'price-filter', + heading: 'Filter By Price', + }, + } ); + await editor.saveSiteEditorEntities(); + await page.goto( `/shop`, { waitUntil: 'networkidle' } ); + } ); + + test.afterEach( async ( { templateApiUtils } ) => { + await templateApiUtils.revertTemplate( + 'woocommerce/woocommerce//archive-product' + ); + } ); + + test( 'should show all products', async ( { page } ) => { + const legacyTemplate = await getBlockByName( { + page, + name: 'woocommerce/legacy-template', + } ); + + await page.waitForLoadState( 'networkidle' ); + + const products = await legacyTemplate + .getByRole( 'list' ) + .locator( '.product' ) + .all(); + + expect( products ).toHaveLength( 16 ); + } ); + + test( 'should show only products that match the filter', async ( { + page, + pageUtils, + } ) => { + const { maxPriceInput } = await getMinMaxPriceInputs( { + page, + blockName: 'woocommerce/filter-wrapper', + } ); + + await maxPriceInput.selectText(); + await maxPriceInput.type( '10' ); + await pageUtils.pressKeys( 'Tab', { + delay: 100, + } ); + await page.waitForURL( ( url ) => + url + .toString() + .includes( blockData.urlSearchParamWhenFilterIsApplied ) + ); + + const legacyTemplate = await getBlockByName( { + page, + name: 'woocommerce/legacy-template', + } ); + + const products = await legacyTemplate + .getByRole( 'list' ) + .locator( '.product' ) + .all(); + + expect( products ).toHaveLength( 1 ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/utils.ts b/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/utils.ts new file mode 100644 index 00000000000..3b1af63ba8f --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tests/price-filter/utils.ts @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { Page } from '@playwright/test'; +import { getBlockByName } from '@woocommerce/e2e-utils'; + +export const getMinMaxPriceInputs = async ( { + page, + blockName, +}: { + page: Page; + blockName: string; +} ) => { + const priceFilterBlock = await getBlockByName( { + page, + name: blockName, + } ); + + const maxPriceInput = await priceFilterBlock.locator( + '.wc-block-price-filter__amount--max' + ); + + const minPriceInput = await priceFilterBlock.locator( + '.wc-block-price-filter__amount--min' + ); + + return { maxPriceInput, minPriceInput }; +}; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tsconfig.json b/plugins/woocommerce-blocks/tests/e2e-pw/tsconfig.json new file mode 100644 index 00000000000..f52a4d12b49 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json", +} diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/types/block-data.ts b/plugins/woocommerce-blocks/tests/e2e-pw/types/block-data.ts new file mode 100644 index 00000000000..6437a0f68b8 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/types/block-data.ts @@ -0,0 +1,5 @@ +export type BlockData< T = undefined > = { + name: string; + mainClass: string; + selectors: Record< 'editor' | 'frontend', Record< string, unknown > >; +} & ( T extends undefined ? Record< string, never > : T ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/types/index.ts b/plugins/woocommerce-blocks/tests/e2e-pw/types/index.ts new file mode 100644 index 00000000000..49a29e1ff75 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/types/index.ts @@ -0,0 +1 @@ +export * from './block-data'; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/TemplateApiUtils.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/TemplateApiUtils.ts new file mode 100644 index 00000000000..6b8170e40db --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/TemplateApiUtils.ts @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { request as req } from '@playwright/test'; +import fs from 'fs/promises'; +/** + * Internal dependencies + */ +import { BASE_URL, STORAGE_STATE_PATH } from '../constants'; + +export class TemplateApiUtils { + request: typeof req; + constructor( request: typeof req ) { + this.request = request; + } + async revertTemplate( slug: string ) { + const storageState = JSON.parse( + await fs.readFile( STORAGE_STATE_PATH, 'utf-8' ) + ); + const requestUtils = await this.request.newContext( { + baseURL: BASE_URL, + storageState: storageState && { + cookies: storageState.cookies, + origins: [], + }, + } ); + + const response = await requestUtils.get( + `/wp-json/wp/v2/templates/${ slug }?context=edit&source=theme&_locale=user`, + { + headers: { + 'X-WP-Nonce': storageState.nonce, + }, + } + ); + + const { content } = await response.json(); + + await requestUtils.post( + `wp-json/wp/v2/templates/${ slug }?_locale=user`, + { + data: { + id: slug, + content: content.raw, + source: 'theme', + }, + headers: { + 'X-WP-Nonce': storageState.nonce, + }, + } + ); + } +} diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/index.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/index.ts new file mode 100644 index 00000000000..b1a3cbc357b --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/api/index.ts @@ -0,0 +1 @@ +export * from './TemplateApiUtils'; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/cli.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/cli.ts new file mode 100644 index 00000000000..ec77d4a4c0a --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/cli.ts @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { exec } from 'child_process'; + +export function cli( cmd, args = [] ) { + return new Promise( ( resolve ) => { + exec( `${ cmd } ${ args.join( ' ' ) }`, ( error, stdout, stderr ) => { + resolve( { + code: error && error.code ? error.code : 0, + error, + stdout, + stderr, + } ); + } ); + } ); +} diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/constants.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/constants.ts new file mode 100644 index 00000000000..64bf893b51e --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/constants.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import path from 'path'; + +export const BLOCK_THEME_SLUG = 'twentytwentythree'; +export const CLASSIC_THEME_SLUG = 'twentytwentyone'; +export const MIN_TIMEOUT = 10000; +export const BASE_URL = 'http://localhost:8889'; +export const STORAGE_STATE_PATH = path.join( + process.cwd(), + 'artifacts/storage-states/admin.json' +); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/get-block-by-name.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/get-block-by-name.ts new file mode 100644 index 00000000000..c0ed515d9ca --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/get-block-by-name.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ + +import { Page } from '@playwright/test'; + +export const getBlockByName = ( { + page, + name, +}: { + page: Page; + name: string; +} ) => page.locator( `[data-block-name="${ name }"]` ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/index.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/index.ts new file mode 100644 index 00000000000..0ee9b84b491 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/index.ts @@ -0,0 +1 @@ +export * from './get-block-by-name'; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/go-to-shop.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/go-to-shop.ts new file mode 100644 index 00000000000..9722768c0d5 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/go-to-shop.ts @@ -0,0 +1,5 @@ +export const goToShop = () => { + page.goto( BASE_URL + '/shop', { + waitUntil: 'networkidle0', + } ); +}; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/index.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/index.ts new file mode 100644 index 00000000000..becfa58d01c --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/index.ts @@ -0,0 +1,5 @@ +export * from './frontend'; +export * from './constants'; +export * from './use-block-theme'; +export * from './cli'; +export * from './api'; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/use-block-theme.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/use-block-theme.ts new file mode 100644 index 00000000000..09ec81fd532 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/use-block-theme.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import { test as Test } from '@wordpress/e2e-test-utils-playwright'; + +/** + * Internal dependencies + */ +import { BLOCK_THEME_SLUG } from './constants'; + +export const useBlockTheme = ( test: typeof Test ) => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( BLOCK_THEME_SLUG ); + } ); +}; diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/shopper/filter-products-by-price.test.ts b/plugins/woocommerce-blocks/tests/e2e/specs/shopper/filter-products-by-price.test.ts index c9a68fccf2f..7d755a39cd9 100644 --- a/plugins/woocommerce-blocks/tests/e2e/specs/shopper/filter-products-by-price.test.ts +++ b/plugins/woocommerce-blocks/tests/e2e/specs/shopper/filter-products-by-price.test.ts @@ -3,7 +3,6 @@ */ import { createNewPost, - deleteAllTemplates, insertBlock, switchUserToAdmin, publishPost, @@ -13,16 +12,7 @@ import { selectBlockByName } from '@woocommerce/blocks-test-utils'; /** * Internal dependencies */ -import { - BASE_URL, - enableApplyFiltersButton, - goToTemplateEditor, - insertAllProductsBlock, - saveTemplate, - useTheme, - waitForCanvas, - waitForAllProductsBlockLoaded, -} from '../../utils'; +import { enableApplyFiltersButton, waitForCanvas } from '../../utils'; import { clickLink, saveOrPublish } from '../../../utils'; const block = { @@ -44,11 +34,6 @@ const block = { const { selectors } = block; -const goToShopPage = () => - page.goto( BASE_URL + '/shop', { - waitUntil: 'networkidle0', - } ); - const setMaxPrice = async () => { await page.waitForSelector( selectors.frontend.priceMaxAmount ); await page.focus( selectors.frontend.priceMaxAmount ); @@ -60,166 +45,6 @@ const setMaxPrice = async () => { }; describe( `${ block.name } Block`, () => { - describe( 'with All Products Block', () => { - beforeAll( async () => { - await switchUserToAdmin(); - await createNewPost( { - postType: 'post', - title: block.name, - } ); - - await insertBlock( block.name ); - await insertAllProductsBlock(); - await insertBlock( 'Active Filters' ); - await publishPost(); - - const link = await page.evaluate( () => - wp.data.select( 'core/editor' ).getPermalink() - ); - await page.goto( link ); - } ); - - it( 'should render products', async () => { - await waitForAllProductsBlockLoaded(); - const products = await page.$$( selectors.frontend.productsList ); - - expect( products ).toHaveLength( 5 ); - } ); - - it( 'should show only products that match the filter', async () => { - const isRefreshed = jest.fn( () => void 0 ); - - page.on( 'load', isRefreshed ); - - await setMaxPrice(); - - await waitForAllProductsBlockLoaded(); - - await expect( page ).toMatchElement( '.wc-blocks-filter-wrapper', { - text: 'Active filters', - } ); - - const products = await page.$$( selectors.frontend.productsList ); - - expect( isRefreshed ).not.toBeCalled(); - - expect( products ).toHaveLength( 1 ); - - await expect( page ).toMatch( block.foundProduct ); - } ); - } ); - - describe( 'with PHP classic template', () => { - const productCatalogTemplateId = - 'woocommerce/woocommerce//archive-product'; - - useTheme( 'emptytheme' ); - beforeAll( async () => { - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - - await goToTemplateEditor( { - postId: productCatalogTemplateId, - } ); - await insertBlock( block.name ); - await saveTemplate(); - await goToShopPage(); - } ); - - beforeEach( async () => { - await goToShopPage(); - } ); - - afterAll( async () => { - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - } ); - - it( 'should render products', async () => { - const products = await page.$$( - selectors.frontend.classicProductsList - ); - - expect( products ).toHaveLength( 5 ); - } ); - - it( 'should show only products that match the filter', async () => { - const isRefreshed = jest.fn( () => void 0 ); - page.on( 'load', isRefreshed ); - - await page.waitForSelector( block.class + '.is-loading', { - hidden: true, - } ); - - await expect( page ).toMatch( block.foundProduct ); - expect( isRefreshed ).not.toBeCalled(); - - await Promise.all( [ page.waitForNavigation(), setMaxPrice() ] ); - - await page.waitForSelector( - selectors.frontend.classicProductsList - ); - const products = await page.$$( - selectors.frontend.classicProductsList - ); - - const pageURL = page.url(); - const parsedURL = new URL( pageURL ); - - expect( isRefreshed ).toBeCalledTimes( 1 ); - expect( products ).toHaveLength( 1 ); - - expect( parsedURL.search ).toEqual( - block.urlSearchParamWhenFilterIsApplied - ); - await expect( page ).toMatch( block.foundProduct ); - } ); - - it( 'should refresh the page only if the user clicks on button', async () => { - await goToTemplateEditor( { - postId: productCatalogTemplateId, - } ); - - await waitForCanvas(); - await selectBlockByName( block.slug ); - await enableApplyFiltersButton(); - await saveTemplate(); - await goToShopPage(); - - const isRefreshed = jest.fn( () => void 0 ); - page.on( 'load', isRefreshed ); - await page.waitForSelector( block.class + '.is-loading', { - hidden: true, - } ); - - expect( isRefreshed ).not.toBeCalled(); - - await setMaxPrice(); - - expect( isRefreshed ).not.toBeCalled(); - - await clickLink( selectors.frontend.submitButton ); - - await page.waitForSelector( - selectors.frontend.classicProductsList - ); - - const products = await page.$$( - selectors.frontend.classicProductsList - ); - - const pageURL = page.url(); - const parsedURL = new URL( pageURL ); - - expect( isRefreshed ).toBeCalledTimes( 1 ); - expect( products ).toHaveLength( 1 ); - await expect( page ).toMatch( block.foundProduct ); - expect( parsedURL.search ).toEqual( - block.urlSearchParamWhenFilterIsApplied - ); - } ); - } ); - describe( 'with Product Query Block', () => { let editorPageUrl = ''; let frontedPageUrl = ''; diff --git a/plugins/woocommerce-blocks/tsconfig.base.json b/plugins/woocommerce-blocks/tsconfig.base.json index 51f44814c19..d9bd8e1d42f 100644 --- a/plugins/woocommerce-blocks/tsconfig.base.json +++ b/plugins/woocommerce-blocks/tsconfig.base.json @@ -61,7 +61,11 @@ "@woocommerce/type-defs/*": [ "assets/js/types/type-defs/*" ], "@woocommerce/types": [ "assets/js/types" ], "@woocommerce/storybook-controls": [ "storybook/custom-controls" ], - "@woocommerce/utils": [ "assets/js/utils" ] + "@woocommerce/utils": [ "assets/js/utils" ], + "@woocommerce/e2e-utils": [ "tests/e2e-pw/utils" ], + "@woocommerce/e2e-types": [ "tests/e2e-pw/types" ], + "@woocommerce/e2e-playwright-utils": [ "tests/e2e-pw/playwright-utils" ], + } } } diff --git a/plugins/woocommerce-blocks/wp-cli.yml b/plugins/woocommerce-blocks/wp-cli.yml new file mode 100644 index 00000000000..e377d2f1803 --- /dev/null +++ b/plugins/woocommerce-blocks/wp-cli.yml @@ -0,0 +1,2 @@ +apache_modules: + - mod_rewrite