So, you’re building Gutenberg blocks and want to make sure they actually, you know, work. Good call! When it comes to testing, you’ve got a couple of powerful allies: Jest for your unit and integration tests, and @wordpress/e2e-test-utils for those crucial end-to-end scenarios. This article will walk you through how to set up and write effective tests using both, focusing on actual code and practical advice.
Let’s dive in.
Before you can write any tests, you need to get your project ready. This involves installing some packages and configuring Jest.
Installing Necessary Packages
First things first, open your terminal in your block’s root directory and run these commands. We’ll grab Jest, the WordPress testing utilities, and a few other helpful bits.
“`bash
npm install –save-dev jest @wordpress/scripts @testing-library/react @testing-library/jest-dom
“`
A quick breakdown of what these are for:
jest: The JavaScript testing framework itself. This is your foundation.@wordpress/scripts: A collection of scripts that WordPress uses for development, including Jest configurations. This saves you a ton of setup time.@testing-library/react: Provides utility functions to test React components in a way that encourages good testing practices by focusing on user interactions.@testing-library/jest-dom: Offers custom Jest matchers to make assertions about the DOM more readable and powerful.
Configuring Jest
@wordpress/scripts does a lot of the heavy lifting here. The easiest way to get Jest running with the WordPress-specific configurations is to update your package.json file.
Open up your package.json and add these scripts:
“`json
{
“name”: “your-gutenberg-block”,
“version”: “1.0.0”,
“description”: “My awesome Gutenberg block”,
“main”: “build/index.js”,
“scripts”: {
“test”: “wp-scripts test”,
“test:watch”: “wp-scripts test –watch”,
“test:coverage”: “wp-scripts test –coverage”
},
“devDependencies”: {
“jest”: “^29.7.0”,
“@wordpress/scripts”: “^26.0.0”,
“@testing-library/react”: “^14.0.0”,
“@testing-library/jest-dom”: “^6.0.0”
}
}
“`
Now, when you run npm test, wp-scripts will
automatically find your tests, apply the necessary Babel transformations, and run Jest with the appropriate environment. It’s a huge time-saver.
Creating a Test File
Jest looks for files named .test.js or .spec.js. A good practice is to place your test files right alongside the code they’re testing, or in a __tests__ subdirectory.
For example, if you have src/edit.js, its unit tests might live in src/edit.test.js.
If you’re looking to deepen your understanding of testing Gutenberg blocks using Jest and @wordpress/e2e-test-utils, you might find it helpful to explore related resources that cover best practices and advanced techniques. For instance, an insightful article on setting up a robust testing environment for WordPress development can provide you with additional context and tips. You can check it out here: How to Write Jest and @wordpress/e2e-test-utils Tests for Gutenberg Blocks.
Writing Unit and Integration Tests with Jest
Unit tests focus on the smallest testable parts of your code – individual functions or components – in isolation. Integration tests check how different units work together. For Gutenberg blocks, this often means testing your edit and save functions, and any helper components.
Testing Your Block’s edit Function
The edit function is where your block’s UI lives. We want to ensure it renders correctly and responds to user interactions. @testing-library/react is your best friend here.
Let’s say you have a simple edit.js for a “Hello World” block:
“`jsx
// src/edit.js
import { useBlockProps } from ‘@wordpress/block-editor’;
import { TextControl } from ‘@wordpress/components’;
import { __ } from ‘@wordpress/i18n’;
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps();
const { content } = attributes;
return (
label={__(‘Content’, ‘my-block’)} value={content} onChange={(newContent) => setAttributes({ content: newContent })} /> {__(‘Preview:’, ‘my-block’)} {content}
);
}
“`
Now, let’s write src/edit.test.js:
“`jsx
// src/edit.test.js
import { render, screen, fireEvent } from ‘@testing-library/react’;
import ‘@testing-library/jest-dom’; // For extended matchers
import Edit from ‘./edit’;
describe(‘Edit component’, () => {
// Mock necessary Gutenberg APIs
const mockSetAttributes = jest.fn();
const blockProps = {
className: ‘wp-block-my-block’,
};
beforeEach(() => {
// Clear mocks before each test to ensure isolation
mockSetAttributes.mockClear();
});
it(‘renders correctly with default attributes’, () => {
const attributes = { content: ‘Default Content’ };
render(
);
// Check if the TextControl is rendered with the correct value
expect(screen.getByLabelText(‘Content’)).toHaveValue(‘Default Content’);
// Check if the preview paragraph is rendered
expect(screen.getByText(/Preview: Default Content/i)).toBeInTheDocument();
});
it(‘updates content when TextControl value changes’, () => {
const attributes = { content: ‘Initial Content’ };
render(
);
// Find the TextControl by its label
const textInput = screen.getByLabelText(‘Content’);
// Simulate a user typing into the input
fireEvent.change(textInput, { target: { value: ‘New Content’ } });
// Expect setAttributes to have been called with the new content
expect(mockSetAttributes).toHaveBeenCalledTimes(1);
expect(mockSetAttributes).toHaveBeenCalledWith({ content: ‘New Content’ });
});
it(‘reflects attribute changes in the preview’, () => {
const attributes = { content: ‘Dynamic Content’ };
// We’ll simulate setAttributes causing a re-render for this test
// In a real application, React handles this, but for unit testing, we can control it.
let currentAttributes = { …attributes };
const fakeSetAttributes = (newAttrs) => {
currentAttributes = { …currentAttributes, …newAttrs };
rerender(
};
const { rerender } = render(
);
const textInput = screen.getByLabelText(‘Content’);
fireEvent.change(textInput, { target: { value: ‘Updated Preview’ } });
expect(screen.getByText(/Preview: Updated Preview/i)).toBeInTheDocument();
});
});
“`
Here’s why this approach is effective:
- Mocking: We mock
setAttributesandblockPropsbecause we’re testing our component, not the entire Gutenberg editor. This isolates the test. - User-centric queries:
@testing-library/reactencourages querying elements the way a user would (e.g.,getByLabelText,getByText). This makes your tests more robust to cosmetic changes. fireEvent: We simulate actual user interactions, like typing in an input.expect().toBeInTheDocument()andtoHaveValue(): These are custom matchers from@testing-library/jest-domthat make assertions about DOM elements more expressive.
If you’re looking to enhance your testing skills for Gutenberg blocks, you might find it helpful to explore a related article that delves into best practices for writing effective Jest and @wordpress/e2e-test-utils tests. This resource provides valuable insights and examples that can complement your understanding of the testing process. For more information, check out this informative guide on testing strategies at The Sheryar.
Testing Your Block’s save Function
The save function determines the HTML output of your block. This is crucial for consistency and front-end rendering. We typically render the save output and assert on the resulting HTML.
Let’s assume your save.js looks like this:
“`jsx
// src/save.js
import { useBlockProps } from ‘@wordpress/block-editor’;
export default function Save({ attributes }) {
const blockProps = useBlockProps.save();
const { content } = attributes;
return (
Block Content: {content}
);
}
“`
And its src/save.test.js:
“`jsx
// src/save.test.js
import { render } from ‘@testing-library/react’;
import Save from ‘./save’;
describe(‘Save component’, () => {
it(‘renders content correctly with provided attributes’, () => {
const attributes = { content: ‘Saved Block Text’ };
// Render the Save component, which outputs the HTML for the front end.
const { container } = render(
// We expect the rendered HTML to contain our paragraph with the content.
expect(container.querySelector(‘div.wp-block-my-block p’)).toHaveTextContent(‘Block Content: Saved Block Text’);
// You can also snapshot the output for robust regression testing
expect(container.innerHTML).toMatchSnapshot();
});
it(‘renders without content if attribute is empty’, () => {
const attributes = { content: ” };
const { container } = render(
expect(container.querySelector(‘div.wp-block-my-block p’)).toHaveTextContent(‘Block Content: ‘);
expect(container.innerHTML).toMatchSnapshot();
});
});
“`
Key takeaways for save tests:
render: We still userenderto get a DOM representation of the output.container: Thecontainerobject fromrenderholds the root DOM element, allowing you to query the raw HTML output.toMatchSnapshot(): This is a powerful Jest feature. The first time you run this test, Jest creates a “snapshot” file (e.g.,src/__snapshots__/save.test.js.snap) containing the rendered HTML. Subsequent runs compare the current output against this snapshot. If they differ, the test fails, alerting you to unexpected changes in your block’s HTML. This is excellent for preventing accidental markup changes.
Testing Helper Functions and Components
If your block has smaller, reusable functions or React components, test them individually. This follows the true “unit testing” philosophy.
For example, if you had a ButtonComponent.js:
“`jsx
// src/components/ButtonComponent.js
import React from ‘react’;
import { Button } from ‘@wordpress/components’;
const ButtonComponent = ({ label, onClick, className }) => {
return (
{label}
);
};
export default ButtonComponent;
“`
And its src/components/ButtonComponent.test.js:
“`jsx
// src/components/ButtonComponent.test.js
import { render, screen, fireEvent } from ‘@testing-library/react’;
import ‘@testing-library/jest-dom’;
import ButtonComponent from ‘./ButtonComponent’;
describe(‘ButtonComponent’, () => {
it(‘renders with the correct label’, () => {
render(
expect(screen.getByRole(‘button’, { name: /Click Me/i })).toBeInTheDocument();
});
it(‘calls onClick handler when clicked’, () => {
const handleClick = jest.fn(); // Create a mock function
render(
const button = screen.getByRole(‘button’, { name: /Test Button/i });
fireEvent.click(button); // Simulate a click
expect(handleClick).toHaveBeenCalledTimes(1); // Expect our mock to have been called once
});
it(‘applies custom class name’, () => {
render(
const button = screen.getByRole(‘button’, { name: /Styled Button/i });
expect(button).toHaveClass(‘my-custom-button’);
});
});
“`
This ensures each small piece of your block is robust before integrating them.
End-to-End Testing with @wordpress/e2e-test-utils
Unit and integration tests are great, but they don’t catch everything. You need to simulate a real user interacting with your block in the actual WordPress editor. That’s where end-to-end (E2E) tests come in, powered by @wordpress/e2e-test-utils and Playwright (under the hood).
Important: For E2E tests, you need a running WordPress instance with your block installed. Typically, this is done via a local development environment (like local, DDEV, or a custom Docker setup).
Setting Up E2E Tests
First, you’ll likely need to create a dedicated E2E test file, often in a e2e or tests-e2e directory in your block’s root. For example, e2e/basic-block.test.js.
The @wordpress/scripts package comes with an E2E testing configuration. You’ll need to update your package.json to include an E2E test script:
“`json
{
“name”: “your-gutenberg-block”,
“version”: “1.0.0”,
“description”: “My awesome Gutenberg block”,
“main”: “build/index.js”,
“scripts”: {
“test”: “wp-scripts test”,
“test:watch”: “wp-scripts test –watch”,
“test:coverage”: “wp-scripts test –coverage”,
“test:e2e”: “wp-scripts test-e2e”,
“test:e2e:debug”: “wp-scripts test-e2e –debug”
},
“devDependencies”: {
“jest”: “^29.7.0”,
“@wordpress/scripts”: “^26.0.0”,
“@testing-library/react”: “^14.0.0”,
“@testing-library/jest-dom”: “^6.0.0”
}
}
“`
Now, running npm run test:e2e will launch Playwright, open a browser, and run your tests.
It expects a WordPress site running, usually at http://localhost:8888. You can configure the URL via environment variables (like WP_BASE_URL or a jest-puppeteer.config.js if you were using puppeteer directly). @wordpress/scripts will typically try http://localhost:8888 by default or use a configured test:e2e host.
Essential @wordpress/e2e-test-utils Functions
@wordpress/e2e-test-utils provides a suite of helpers to interact with the Gutenberg editor. These are usually globally available within your E2E test context.
Let’s look at a few common ones:
visitAdminPage( path, query ): Navigates to a WordPress admin page. E.g.,page.visitAdminPage('post-new.php')to create a new post.insertBlock( blockNameOrSlug, attributes ): Inserts a Gutenberg block.clickBlockToolbarButton( labelOrTitle ): Clicks a button in the block toolbar.page.type( selector, text ): Types text into an input field (Playwright native).page.click( selector ): Clicks an element (Playwright native).getBlock( blockNameOrSlug ): Selects a block in the editor.getCurrentPostContent(): Retrieves the inner content of the post editor.getEditedPostContent(): Retrieves the saved post content, how it would appear on the front end.
Writing Your First E2E Test
Let’s test our “Hello World” block from earlier. This test will:
- Navigate to the post editor.
- Insert our block.
- Type some text into the TextControl.
- Assert that the text is correctly reflected in the editor.
- Save the post.
- Assert that the front-end content is correct.
“`jsx
// e2e/basic-block.test.js
const {
insertBlock,
getEditedPostContent,
visitAdminPage,
// Other useful functions:
// clickBlockToolbarButton,
// getBlock,
// getCurrentPostContent,
// toggleBlockSettings,
// fillTextControl,
// pressKeys,
} = require(‘@wordpress/e2e-