# WooCommerce Blocks End-to-End Tests This living document serves as a guide for writing end-to-end (E2E) tests with Playwright in the WooCommerce Blocks project. ## Preparing the environment Please refer to [the Getting Started section of the main `README.md`](https://github.com/woocommerce/woocommerce/blob/trunk/README.md) for a general-purpose guide on getting started. The rest of this document will assume that you've installed all of the prequisites and setup described there. Run the following command from the repository root to build the WooCommerce plugin: ```sh pnpm --filter='@woocommerce/plugin-woocommerce' watch:build ``` Next, run the following command from the [`woocommerce-blocks` plugin folder](../../../woocommerce-blocks/) to start a `wp-env` instance and install all the testing products, languages, etc.: ```sh cd plugins/woocommerce-blocks/ pnpm env:start ``` > [!TIP] > If you want to start/stop the environment without running the whole setup, use the native `wp-env` commands directly, e.g. `npx wp-env start` and `npx wp-env stop`. The testing environment should now be ready under [localhost:8889](http://localhost:8889). ### Resetting the environment Occasionally, you'll need to reset the environment, e.g., when testing products have been updated. To do that, run the following command and go make yourself some coffee: ```sh pnpm env:restart ``` ## Running and debugging tests > [!NOTE] > If you're using VSCode, we recommend using the [Playwright Test](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) extension to run and debug the tests. Here is a basic set of commands to quickly start running and debugging the tests. For full documentation, see the official Playwright guide on [Running and Debugging Tests](https://playwright.dev/docs/running-tests). ```sh # Run all available tests. pnpm test:e2e # Run in headed mode. pnpm test:e2e --headed # Run/Debug in UI mode. pnpm test:e2e --ui # Run a single test file. pnpm run test:e2e cart-block.spec.ts # Run a set of files from a different directories. pnpm run test:e2e tests/cart/ tests/products/ # Run files that have `cart` or `checkout` in the file name. pnpm run test:e2e cart checkout # Run a single test via its line number. pnpm run test:e2e cart-block.spec.ts:38 # Run/Debug a test with a specific title. pnpm run test:e2e -g "should display a discount label" --ui ``` > [!TIP] > When a test fails, it leaves a trace info at the bottom. You can quickly jump into debugging mode by running the generated trace view command, for example: > > ![Playwright failed test report](../.media/images/e2e-open-trace-cmd.png) ### Debugging tests in CI When a test fails in CI, a failure artifact is zipped and uploaded to the Summary page of the current job: ![Summary page of the Blocks end-to-end tests job](../.media/images/e2e-download-failure-artifacts.png) Once you download and extract that zip, you'll see dedicated folders for the failed test artifacts. In CI, we retry running a failed test twice before considering it a failure, so there can be up to three folders per failed test. Each of those folders should contain a Playwright trace zip file and a screenshot from the failure moment. On the first retry, we also record the entire test, so the first retry folder should contain a video recording as well. To view a trace, head to the [Playwright Trace Viewer](https://trace.playwright.dev) page and drag and drop the trace zip file there, or run it from the command line: ```sh npx playwright show-trace ``` ## Writing Tests We're using the [`@wordpress/e2e-test-utils-playwright`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright) package as our framework base, so we generally follow Gutenberg E2E's [best practices](https://github.com/WordPress/gutenberg/blob/trunk/docs/contributors/code/e2e/README.md#best-practices) to reduce the framework entry threshold and necessary maintenance. However, our framework has specific aspects, which you can learn about in this section. > [!TIP] > Using the right selectors can be a daunting task, so let Playwright pick the right selector for you. Open the page you're testing against via `npx playwright open localhost:8889/path-to-the-page`. From there, you can use Playwright Inspector to generate the recommended locators: > > ![Playwright Inspector example usage](../.media/images/e2e-playwright-inspector.png) > > Read more about generating tests with Playwright in the [Generating Tests](https://playwright.dev/docs/codegen-intro) guide. ### Setup and teardown We isolate our tests from each other by resetting the database to its initial state **for every test**. Since every test starts with a clean slate, and there's no need to manually reset the environment, we only allow the `beforeEach` hook as there's no point in using `beforeAll`, `afterAll`, or `afterEach`. This approach might seem like a limitation at first, but ultimately it makes tests more stable and easier to write. This convention is enforced by an ESLint rule, so you don't need to worry about it. ### Plugins To use a custom plugin with your tests, first create the plugin PHP file and save it to the [test plugins folder](../../tests/e2e/plugins/). Here's a handy snippet to help you get started: ```php // plugins/my-fancy-plugin.php { await requestUtils.activatePlugin( 'woocommerce-blocks-test-my-fancy-plugin' ); await page.goto( '/shop' ); await expect( page.getByText( 'Howdy!' ) ).toBeVisible(); } ); ``` > [!IMPORTANT] > A plugin's slug is created automatically **from the plugin's name**, not from the `@package` statement as you might think. So, if your plugin is named `WooCommerce Blocks Test Bazzinga`, you'll need to activate it by `woocommerce-blocks-test-bazzinga`. ### Themes Currently, the default theme is Twenty Twenty Four. Activating other themes is done by the `RequestUtils.activateTheme()` API, for example: ```ts test.beforeEach( async ( { page, requestUtils } ) => { await requestUtils.activateTheme( 'storefront' ); } ); ``` > [!NOTE] > Unless it's a one-off thing, remember to use the `beforeEach` hook to activate your theme. Each test starts with a clean database, which means the theme will be reset to the default one as well. #### Adding a new theme If you've created a custom theme and want to use it in your tests, save it in the [test themes folder](../../tests/e2e/themes/). Check out the themes that are already there for inspiration. The activation part was explained above, so you're good to go! ### Utilities We have a handful of [custom utilities](../../tests/e2e/utils/) built on top of core's [`@wordpress/e2e-test-utils-playwright`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/e2e-test-utils-playwright), so make sure to familiarize yourself with them. #### Creating new utilities Most of the time, it's better to do a little repetition instead of creating a utility, as over-abstracting can make the code too vague, complicated, or confusing. However, if the piece is complex and repeated enough, we recommend abstracting it into a [POM (Page Object Model)](https://playwright.dev/docs/pom), for example: ```ts import { test as base, expect, Editor } from '@woocommerce/e2e-utils'; class CartUtils { editor: Editor; constructor( { editor }: { editor: Editor } ) { this.editor = editor; } async addClothes( list ) { // Add clothes from the list. } async addBooks( list ) { // Add books from the list. } } const test = base.extend< { cartUtils: CartUtils } >( { cartUtils: async ( { editor }, use ) => { await use( new CartUtils( { editor } ) ); }, } ); test( 'Add products', async ( { admin, cartUtils } ) => { await admin.createNewPost(); await cartUtils.addClotes( [ 'Shirt', 'Cap', 'Pants' ] ); await cartUtils.addBooks( [ 'Cooking with Woo' ] ); await page.goto( '/cart' ); await expect( this.page.getByLabel( 'Shirt' ) ).toBeVisible(); // etc. } ); ``` #### Extending Core utilities If you've come up with a utility that you think should be a part of the Core utilities (`Admin`, `Editor`, `RequestUtils`, etc.), go ahead and make a PR in Gutenberg - it's best to move as much as possible upstream. If you think that the utility still fits under, e.g., `Editor` but is Woo-specific, you'll need to write an extension, for example: ```ts // utils/editor/index.ts import { Editor as CoreEditor } from '@wordpress/e2e-test-utils-playwright'; export class Editor extends CoreEditor { async insertAllWooBlocks() { for ( const wooBlock of [ 'all', 'woo', 'blocks' ] ) { await this.insertBlock( wooBlock ); } } } ``` ### Content Templates We have created `RequestUtils.createPostFromFile()` and `RequestUtils.createTemplateFromFile()` utilities that enable creating complex content testing scenarios with Handlebars templates. The template files are kept in the [content-templates](../../tests/e2e/content-templates/) folder, so you can head there for some inspiration. > [!IMPORTANT] > The Handlebars template filenames must be prefixed with the entity type. For posts, an example filename would be `post_with-filters.handlebars`, and for templates `template_archive-product_with-filters.handlebars`. Notice that the latter contains the slug of the template (`archive-product`) before the name (`with-filters`), separated with an underscore - it's necessary for the template to be properly loaded and created. When you have the template ready, we recommend creating a [test fixture](https://playwright.dev/docs/test-fixtures) that will be used to compile and create your template with given data, for example: ```ts // tests/product-collection.spec.ts import { test as base, expect, TemplateCompiler } from '@woocommerce/e2e-utils'; const test = base.extend< { filteredProductsTemplate: TemplateCompiler; } >( { filteredProductsTemplate: async ( { requestUtils }, use ) => { const compiler = await requestUtils.createTemplateFromFile( 'archive-product_with-filters' ); await use( compiler ); }, } ); test( 'Renders correct products for $10-$99 price range', async ( { page, filteredProductsTemplate, } ) => { await filteredProductsTemplate.compile( { price: { from: '$10', to: '$99', }, } ); await page.goto( '/shop' ); await expect( page.getByLabel( 'Products' ) ).toHaveText( [ 'Socks', 'T-Shirt', ] ); } ); test( 'Renders correct products for $100-$999 price range', async ( { page, filteredProductsTemplate, } ) => { await filteredProductsTemplate.compile( { price: { from: '$100', to: '$990', }, } ); await page.goto( '/shop' ); await expect( page.getByLabel( 'Products' ) ).toHaveText( [ 'Rolex', 'Lambo', ] ); } ); ```