Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add docs for snapshot testing in Playwright e2e tests #45709

Merged
merged 5 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Migration guide

This document outlines a typical flow of migrating a Jest + Puppeteer test to Playwright. Note that the migration process is also a good opportunity to refactor or rewrite parts of the tests. Please read the [best practices](https://github.com/WordPress/gutenberg/tree/HEAD/docs/contributors/code/e2e/README.md#best-practices) guide before starting the migration.

## 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`.
Expand All @@ -24,12 +26,17 @@
},
} );
```
7. If there's a missing util, go ahead and [migrate it](#migration-steps-for-test-utils).
8. 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.
7. If there's a missing util, try to inline the operations directly in the test if there are only a few steps. If you think it deserves to be implemented as a test util, then follow the [guide](#migration-steps-for-test-utils) below.
8. Manually migrate other details in the tests following the proposed [best practices](https://github.com/WordPress/gutenberg/tree/HEAD/docs/contributors/code/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

Playwright utilities are organized a little differently to those in the `e2e-test-utils` package. The `e2e-test-utils-playwright` package has the following folders that utils are divided up into:
Before migrating a test utility function, think twice about whether it's necessary. Playwright offers a lot of readable and powerful APIs which make a lot of the utils obsolete. Try implementing the same thing inline directly in the test first. Only follow the below guide if that doesn't work for you. Some examples of utils that deserve to be implemented in the `e2e-test-utils-playwright` package include complex browser APIs (like `pageUtils.dragFiles` and `pageUtils.pressKeyWithModifier`) and APIs that set states (`requestUtils.*`).

> **Note**
> The `e2e-test-utils-playwright` package is not meant to be a drop-in replacement of the Jest + Puppeteer's `e2e-test-utils` package. Some utils are only created to ease the migration process, but they are not necessarily required.

Playwright utilities are organized a little differently from those in the `e2e-test-utils` package. The `e2e-test-utils-playwright` package has the following folders that utils are divided up into:
- `admin` - Utilities related to WordPress admin or WordPress admin's user interface (e.g. `visitAdminPage`).
- `editor` - Utilities for the block editor (e.g. `clickBlockToolbarButton`).
- `pageUtils` - General utilities for interacting with the browser (e.g. `pressKeyWithModifier`).
Expand Down
131 changes: 131 additions & 0 deletions docs/contributors/code/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# End-to-End Testing

This living document serves to prescribe instructions and best practices for writing end-to-end (E2E) tests with Playwright in the Gutenberg project.

> **Note**
> See the dedicated guide if you're working with the previous Jest + Puppeteer framework. See the [migration guide](https://github.com/WordPress/gutenberg/tree/HEAD/docs/contributors/code/e2e/MIGRATION.md) if you're migrating tests from Jest + Puppeteer.

## Running tests

```bash
# Run all available tests.
npm run test:e2e:playwright

# Run in headed mode.
npm run test:e2e:playwright -- --headed

# Run tests with specific browsers (`chromium`, `firefox`, or `webkit`).
npm run test:e2e:playwright -- --project=webkit --project=firefox

# Run a single test file.
npm run test:e2e:playwright -- <path_to_test_file> # E.g., npm run test:e2e:playwright -- site-editor/title.spec.js

# Debugging.
npm run test:e2e:playwright -- --debug
```

If you're developing in Linux, it currently requires testing Webkit browsers in headed mode. If you don't want to or can't run it with the GUI (e.g. if you don't have a graphic interface), prepend the command with [`xvfb-run`](https://manpages.ubuntu.com/manpages/xenial/man1/xvfb-run.1.html) to run it in a virtual environment.

```bash
# Run all available tests.
xvfb-run npm run test:e2e:playwright

# Only run webkit tests.
xvfb-run -- npm run test:e2e:playwright -- --project=webkit
```

## Best practices

<details>
<summary><h3>Forbid `$`, use `locator` instead</h3></summary>

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.
</details>
Comment on lines +39 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation: The "Best practices" individual sections aren't massive, and details/summary usage might make it less readable. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it seems like GitHub will render large margins between these details tags. I'm fine either way, my main concern is that it might make the "Common pitfalls" section less discoverable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add ToC for level 2 headings? It should make it easier to scan what's in the document.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! I considered that too. We'd have to make sure future revisions will reflect the TOC though. I wonder if there's already a linter or something similar in place so that we don't have to introduce it in this PR 🤔.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


<details>
<summary><h3>Use accessible selectors</h3></summary>

Use the selector engine [role-selector](https://playwright.dev/docs/selectors#role-selector) to construct the query wherever possible. It enables us to write accessible queries without having to rely on internal implementations. The syntax should be straightforward and looks like this:

```js
// Select a button with the accessible name "Hello World" (case-insensitive).
page.locator( 'role=button[name="Hello World"i]' );

// Using short-form API, the `name` is case-insensitive by default.
page.getByRole( 'button', { name: 'Hello World' } );
```

It's recommended to append `i` to the name attribute to match it case-insensitively wherever it makes sense. 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.
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved
page.locator( 'role=button[name=/Back$/] >> visible=true' );
// Select a button with the (exact) name "View options" under `#some-section`.
page.locator( 'css=#some-section >> role=button[name="View options"]' );
```

See the [official documentation](https://playwright.dev/docs/selectors#role-selector) for more info on how to use them.
</details>

<details>
<summary><h3>Selectors are strict by default</h3></summary>

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.
</details>

<details>
<summary><h3>Don't overload test-utils, inline simple utils</h3></summary>

`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.
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved
</details>

<details>
<summary><h3>Favor Page Object Model over utils</h3></summary>

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`.
</details>

<details>
<summary><h3>Restify actions to clear or set states</h3></summary>

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.
</details>

<details>
<summary><h3>Avoid global variables</h3></summary>

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.
</details>

<details>
<summary><h3>Make explicit assertions</h3></summary>

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.
</details>

## Common pitfalls

### [Overusing snapshots](https://github.com/WordPress/gutenberg/tree/HEAD/docs/contributors/code/e2e/overusing-snapshots.md)


## Cross-browser testing

By default, tests are only run in chromium. You can _tag_ tests to run them in different browsers. Use `@browser` anywhere in the test title to run it in that browser. Tests will always run in chromium by default, append `-chromium` to disable testing in chromium. Available browsers are `chromium`, `firefox`, and `webkit`.
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved

```js
test( 'I will run in @firefox and @webkit (and chromium by default)', async ( { page } ) => {
// ...
} );

test( 'I will only run in @firefox but not -chromium', async ( { page } ) => {
// ...
} );

test.describe( 'Grouping tests (@webkit, -chromium)', () => {
test( 'I will only run in webkit', async ( { page } ) => {
// ...
} );
} );
```
125 changes: 125 additions & 0 deletions docs/contributors/code/e2e/overusing-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Overusing snapshots

Take a look at the below code. Could you understand what the test is trying to do at first glance?

```js
await editor.insertBlock( { name: 'core/quote' } );
await page.keyboard.type( '1' );
await page.keyboard.press( 'Enter' );
await page.keyboard.press( 'Enter' );

expect( await editor.getEditedPostContent() ).toMatchSnapshot();

await page.keyboard.press( 'Backspace' );
await page.keyboard.type( '2' );

expect( await editor.getEditedPostContent() ).toMatchSnapshot();
```

This is borrowed from the real code in gutenberg, with the test title and the comments removed and refactored into Playwright. Ideally, E2E tests should be self-documented and readable to end users; in the end, they are trying to resemble how end users interact with the app. However, there are a couple of red flags in the code.

## Problems with snapshot testing

Popularized by Jest, [snapshot testing](https://jestjs.io/docs/snapshot-testing) is a great tool to help test our app _when it makes sense_. However, probably because it's so powerful, it's often overused by developers. There are already multiple [articles](https://kentcdodds.com/blog/effective-snapshot-testing) about this. In this particular case, snapshot testing fails to reflect the developer's intention. It's not clear what the assertions are about without looking into other information. This makes the code harder to understand and creates a mental overhead for all the other readers other than the one who wrote it. As readers, we have to jump around the code to fully understand them. The added complexity of the code discourages contributors from changing the test to fit their needs. It could sometimes even confuse the authors and make them accidentally [commit the wrong snapshots](https://github.com/WordPress/gutenberg/pull/42780#discussion_r949865612).

Here's the same test with the test title and comments. Now you know what these assertions are actually about.

```js
it( 'can be split at the end', async () => {
// ...

// Expect empty paragraph outside quote block.
expect( await getEditedPostContent() ).toMatchSnapshot();

// ...

// Expect the paragraph to be merged into the quote block.
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
```

The developer's intention is a bit more readable, but it still feels disconnected from the test. You might be tempted to try [inline snapshots](https://jestjs.io/docs/snapshot-testing#inline-snapshots), which do solve the issue of having to jump around files, but they're still not self-documented nor explicit. We can do better.
kevin940726 marked this conversation as resolved.
Show resolved Hide resolved

## The solution

Instead of writing the assertions in comments, we can try directly writing them out explicitly. With the help of `editor.getBlocks`, we can rewrite them into simpler and atomic assertions.

```js
// ...

// Expect empty paragraph outside quote block.
await expect.poll( editor.getBlocks ).toMatchObject( [
{
name: 'core/quote',
innerBlocks: [
{
name: 'core/paragraph',
attributes: { content: '1' },
},
],
},
{
name: 'core/paragraph',
attributes: { content: '' },
}
] );

// ...

// Expect the paragraph to be merged into the quote block.
await expect.poll( editor.getBlocks ).toMatchObject( [ {
name: 'core/quote',
innerBlocks: [
{
name: 'core/paragraph',
attributes: { content: '1' },
},
{
name: 'core/paragraph',
attributes: { content: '2' },
},
],
} ] );
```

These assertions are more readable and explicit. You can add additional assertions or split existing ones into multiple ones to highlight their importance. Whether to keep the comments is up to you, but it's usually fine to omit them when the code is already readable without them.

## Snapshot variants

Due to the lack of inline snapshots in Playwright, some migrated tests are using string assertions (`toBe`) to simulate similar effects without having to create dozens of snapshot files.

```js
expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:paragraph -->
<p>Paragraph</p>
<!-- /wp:paragraph -->` );
```

We can consider this pattern as a variant of snapshot testing, and we should follow the same rule when writing them. It's often better to rewrite them using `editor.getBlocks` or other methods to make explicit assertions.

```js
await expect.poll( editor.getBlocks ).toMatchObject( [ {
name: 'core/paragraph',
attributes: { content: 'Paragraph' },
} ] );
```

## What about test coverage?

Comparing the explicit assertions to snapshot testing, we're definitely losing some test coverage in this test. Snapshot testing is still useful when we want to assert the full serialized content of the block. Fortunately, though, some tests in the integration test already assert the [full content](https://github.com/WordPress/gutenberg/blob/trunk/test/integration/fixtures/blocks/README.md) of each core block. They run in Node.js, making them way faster than repeating the same test in Playwright. Running 273 test cases in my machine only costs about 5.7 seconds. These sorts of tests work great at the unit or integration level, and we can run them much faster without losing test coverage.

## Best practices

Snapshot testing should rarely be required in E2E tests, often there are better alternatives that leverage explicit assertions. For times when there isn't any other suitable alternative, we should follow the best practices when using them.

### Avoid huge snapshots

Huge snapshots are hard to read and difficult to review. Moreover, when everything is important then nothing is important. Huge snapshots prevent us from focusing on the important parts of the snapshots.

### Avoid repetitive snapshots

If you find yourself creating multiple snapshots of similar contents in the same test, then it's probably a sign that you want to make more atomic assertions instead. Rethink what you want to test, if the first snapshot is only just a reference for the second one, then what you want is likely the **difference** between the snapshots. Store the first result in a variable and assert the difference between the results instead.

## Further readings

- [Effective Snapshot Testing - Kent C. Dodds](https://kentcdodds.com/blog/effective-snapshot-testing)
- [Common Testing Mistakes - Kent C. Dodds](https://kentcdodds.com/blog/common-testing-mistakes)
4 changes: 2 additions & 2 deletions docs/contributors/code/testing-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,9 @@ There is an ongoing effort to add integration tests to the native mobile project

## End-to-end Testing

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.
Most existing 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.
There's an ongoing [project](https://github.com/WordPress/gutenberg/issues/38851) to migrate them from Puppeteer to Playwright. **It's recommended to write new e2e tests in Playwright whenever possible**. The sections below mostly apply to the old Jest + Puppeteer framework. See the dedicated [guide](/docs/contributors/code/e2e/README.md) if you're writing tests with Playwright.**

### Using wp-env

Expand Down
21 changes: 21 additions & 0 deletions packages/e2e-test-utils-playwright/src/editor/get-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Internal dependencies
*/
import type { Editor } from './index';

/**
* Returns the edited blocks.
*
* @param this
*
* @return The blocks.
*/
export async function getBlocks( this: Editor ) {
return await this.page.evaluate( () => {
// The editor might still contain an unmodified empty block even when it's technically "empty".
if ( window.wp.data.select( 'core/editor' ).isEditedPostEmpty() ) {
return [];
}
return window.wp.data.select( 'core/block-editor' ).getBlocks();
} );
}
2 changes: 2 additions & 0 deletions packages/e2e-test-utils-playwright/src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Browser, Page, BrowserContext, Frame } from '@playwright/test';
*/
import { clickBlockOptionsMenuItem } from './click-block-options-menu-item';
import { clickBlockToolbarButton } from './click-block-toolbar-button';
import { getBlocks } from './get-blocks';
import { getEditedPostContent } from './get-edited-post-content';
import { insertBlock } from './insert-block';
import { openDocumentSettingsSidebar } from './open-document-settings-sidebar';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class Editor {
}
clickBlockOptionsMenuItem = clickBlockOptionsMenuItem.bind( this );
clickBlockToolbarButton = clickBlockToolbarButton.bind( this );
getBlocks = getBlocks.bind( this );
getEditedPostContent = getEditedPostContent.bind( this );
insertBlock = insertBlock.bind( this );
openDocumentSettingsSidebar = openDocumentSettingsSidebar.bind( this );
Expand Down
Loading