Introducing e2e testing to WordPress block development

Introduction

Playwright is now supported in @wordpress/scripts version 26.13.0. In this article, I will introduce a simple way to implement E2E testing using Playwright in block development.

Notes

This article is based on version 26.16.0 of @wordpress/scripts. If the version is between 26.13.0 and 26.15.0, please see the “(Supplementary) Preparing the config file” section.

Create a block template

Use @wordpress/create-block to create a plugin template with one block. At the same time, add thewp-env option so that you can also launch the local environment.

npx @wordpress/create-block playwright-test --wp-env

Starting the local environment.

npm run env start

First of all, let’s confirm that we can insert the block.

Block Template

By default only fixed text is displayed, so make the text inside editable.

{
  ...
  "attributes": {
    "content": {
      "type": "string",
      "source": "html",
      "selector": "p"
    }
  },
  ...
}
import {
  RichText,
  useBlockProps,
} from '@wordpress/block-editor';

export default function Edit( { attributes, setAttributes } ) {
  const { content } = attributes;
  return (
    <RichText
      tagName="p"
      { ...useBlockProps() }
      value={ content }
      onChange={ ( newContent ) =>
         setAttributes( { content: newContent } )
      }
    />
  );
}
import { RichText, useBlockProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  const { content } = attributes;
  return (
    <p { ...useBlockProps.save() }>
      <RichText.Content value={ content } />
    </p>
  );
}

Let’s try building the block again. You should now be able to edit and save the text of the inserted block.

npm run build

Preparing for Playwright

When you install the latest @wordpress/scripts, the necessary libraries are included in the dependencies, so there is no need to install any new npm packages.

$ npm ls @playwright/test
playwright-test@0.1.0 /home/username/projects/playwright-test
└─┬ @wordpress/scripts@26.16.0
  ├── @playwright/test@1.39.0
  └─┬ @wordpress/e2e-test-utils-playwright@0.13.0
    └── @playwright/test@1.39.0 deduped
$ npm ls @wordpress/e2e-test-utils-playwright
playwright-test@0.1.0 /home/wildworks/projects/playwright-test
└─┬ @wordpress/scripts@26.16.0
  └── @wordpress/e2e-test-utils-playwright@0.13.0

Add scripts to run e2e tests

{
  ...
  "scripts": {
    "test:e2e": "wp-scripts test-playwright",
    "test:e2e:debug": "wp-scripts test-playwright --debug"
  }
  ...
}

@wordpress/scripts has the default config file for Playwright, so it’s ready to run e2e tests.

Note: If the version of @wordpress/scripts is between 26.13.0 and 26.15.0, please see the “(Supplementary) Preparing the config file” section.

There is no test file yet, let’s try running the e2e test using Playwright.

npm run test:e2e

> playwright-test@0.1.0 test:e2e
> wp-scripts test-playwright
Error: No tests found

Although you got a message that the test could not be found, you were able to confirm that it worked without any errors.

Here, in order to try overwriting the default configuration file, we will change the directory referenced by the tests from the default specs to the tests/e2e directory.

import { defineConfig } from '@playwright/test';

const baseConfig = require( '@wordpress/scripts/config/playwright.config.js' );
const config = defineConfig( {
  ...baseConfig,
  testDir: './tests/e2e',
} );
export default config;

Writing a test

Let’s actually write a simple test. First, we are confirming that the custom block is inserted properly without errors.

import { test, expect } from '@wordpress/e2e-test-utils-playwright';

test.describe( 'Block', () => {
  test.beforeEach( async ( { admin } ) => {
    // Create a new post before each test
    await admin.createNewPost();
  } );

  test( 'should be created', async ( { editor } ) => {
    // Insert a block
    await editor.insertBlock( { name: 'create-block/playwright-test' } );
    // Test that post content matches snapshot
    expect( await editor.getEditedPostContent() ).toMatchSnapshot();
  } );
} );

Run e2e tests.

npm run test:e2e

The first time you run it, there is no snapshot, so it should fail with an error like the one below:

Error: A snapshot doesn't exist at /home/username/projects/playwright-test/tests/e2e/__snapshots__/Block-should-be-created-1-chromium.txt, writing actual.

Instead, a snapshot is created with the following contents: This snapshot shows that the custom block has been properly inserted, so it is the snapshot we expected.

tests/e2e/__snapshots__/Block-should-be-created-1-chromium.txt

<!-- wp:create-block/playwright-test -->
<p class="wp-block-create-block-playwright-test"></p>
<!-- /wp:create-block/playwright-test -->

Run the e2e test again.

npm run test:e2e

The test should now pass.

> playwright-test@0.1.0 test:e2e
> wp-scripts test-playwright

Running 1 test using 1 worker
  ✓  1 [chromium] › test.spec.js:9:6 › Block › should be created (2.2s)
  1 passed (3.1s)

Debugging the test

Try running the test in debug mode. This is useful when writing complex tests and tracing whether operations are performed as expected.

npm run test:e2e:debug

Two windows should appear as shown below. Click once on the step-over button in the smaller window.

Debugging the test

Every time you press the Step Over button, you will see the test procedure execute as expected.

Writing additional test

Since the text in this custom block is editable, so we will write a test to check whether the changed text is reflected correctly.

test.describe( 'Block', () => {
  // ...

  test( 'should be updated the content', async ( { editor, page } ) => {
    // Insert a block
    await editor.insertBlock( { name: 'create-block/playwright-test' } );
    // Update the text inside the block
    await page.keyboard.type( 'Hello World!' );
    // Test that post content matches snapshot
    expect( await editor.getEditedPostContent() ).toMatchSnapshot();
  } );
} );

The second new test fails and creates a snapshot, but it should be the snapshot you expected.

<!-- wp:create-block/playwright-test -->
<p class="wp-block-create-block-playwright-test">Hello World!</p>
<!-- /wp:create-block/playwright-test -->

At the end

With @wordpres/scripts supporting Playwright, I think it has become easier to perform e2e testing in block development. Also, @wordpress/e2e-test-utils-playwright has many useful utilities. For a concrete usage example, you can take a look at the e2e test for the Gutenberg project.

(Supplementary) Preparing the config file

The handbook mentions that “Playwright will automatically detect the configuration file”, but as reported in this issue, depending on the version of @wordpress/scripts, you may have to explicitly prepare a configuration file. A pull request that resolves this issue has already been merged, but this pull request is part of @wordpress/scripts version 26.16.0, so if your version is between 26.13.0 and 26.15.0, you need to define a config file like the one below.

Follow the default configuration of @wordpress/scripts and override the globalSetup property to make it work. The directory referenced in the test has also been changed.

playwright.config.js

import { defineConfig } from '@playwright/test';
const baseConfig = require( '@wordpress/scripts/config/playwright.config.js' );
const config = defineConfig( {
  ...baseConfig,
  globalSetup: require.resolve( './tests/e2e/global-setup.js' ),
  testDir: './tests/e2e',
} );
export default config;

Prepare the file specified by the globalSetup property and write it as follows. This code is based on global-setup.js, which is also defined in WordPress core.

tests/e2e/global-setup.js

const { request } = require( '@playwright/test' );
const { RequestUtils } = require( '@wordpress/e2e-test-utils-playwright' );

async function globalSetup( config ) {
  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();
  // Reset the test environment before running the tests.
  await Promise.all( [
    requestUtils.activateTheme( 'twentytwentyone' ),
    requestUtils.deleteAllPosts(),
    requestUtils.deleteAllBlocks(),
    requestUtils.resetPreferences(),
  ] );
  await requestContext.dispose();
}
export default globalSetup;