Our e2e test suite was getting painful. Every test run meant waiting for Docker to spin up a WordPress container, run the tests, and tear it down. In CI, the wait times added up quickly. When I finally sat down to modernise the infrastructure, WP Playground turned out to be the game changer I didn’t know I needed.

The old setup

The original test suite was JavaScript-based Playwright, running against WordPress in Docker. It worked, but had some issues:

  • Slow startup - Docker needs to pull images, start services, wait for MySQL to be ready
  • Heavy resource usage - Running a full WordPress + MySQL stack just to click some buttons
  • Flaky in CI - Container networking issues, race conditions on service readiness
  • Hard to debug - When tests failed, the WordPress environment was already gone

The tests themselves were fine. But the infrastructure around them was holding us back.

Enter WP Playground

WP Playground is WordPress compiled to WebAssembly, running entirely in the browser or via Node.js. No PHP installation, no MySQL, no Docker. Just a single process that boots a full WordPress instance in memory.

The CLI version (@wp-playground/cli) is what makes this work for e2e testing. You can:

  • Start a WordPress instance with one command
  • Pre-configure it via blueprints (JSON files that define plugins, settings, users)
  • Run it in the background while Playwright does its thing
  • Tear it down instantly when done

The startup time difference is dramatic - roughly 4x faster. Docker needs to pull images, start MySQL, wait for service readiness. WP Playground just boots.

Blueprint configuration

Blueprints let you define the WordPress state declaratively. Here’s a simplified version of what I use:

{
  "$schema": "https://playground.wordpress.net/blueprint-schema.json",
  "landingPage": "/wp-admin/",
  "login": true,
  "steps": [
    {
      "step": "defineWpConfigConsts",
      "consts": {
        "WP_DEBUG": true
      }
    },
    {
      "step": "setSiteOptions",
      "options": {
        "my_plugin_test_token": "abc123",
        "my_plugin_license_key": "test-license"
      }
    },
    {
      "step": "wp-cli",
      "command": "wp plugin activate my-plugin"
    }
  ]
}

The blueprint sets up WordPress options, activates plugins, and logs you in - all before the first test runs. No manual setup, no database fixtures, no cleanup scripts.

The TypeScript migration

While I was at it, I migrated all 9 test files from JavaScript to TypeScript. Not just for type safety (though that helps), but to implement the Page Object Model properly.

The old tests looked like this:

test('can complete task', async ({ page }) => {
  await page.goto('/wp-admin/admin.php?page=my-plugin');
  await page.click('[data-testid="task-checkbox"]');
  await expect(page.locator('.task-completed')).toBeVisible();
});

Now they look like this:

test('can complete task', async ({ dashboardPage }) => {
  await dashboardPage.completeTask(0);
  await dashboardPage.expectTaskCompleted(0);
});

The dashboardPage comes from a custom Playwright fixture that injects page objects. Each page object encapsulates the selectors and interactions for a specific part of the UI. When the DOM changes, I update one file instead of hunting through every test.

Custom fixtures

Playwright fixtures are dependency injection for tests. Instead of creating page objects in each test, you declare what you need and the framework provides it:

type TestFixtures = {
  dashboard: DashboardPage;
  dashboardPage: DashboardPage;  // Without auto-navigation
  tasksApi: TasksApi;
};

export const test = base.extend<TestFixtures>({
  dashboard: async ({ page }, use) => {
    const dashboard = new DashboardPage(page);
    await dashboard.goto();
    await use(dashboard);
  },

  tasksApi: async ({ page, request }, use) => {
    const api = new TasksApi(page, request);
    await use(api);
  },
});

Tests just declare their dependencies in the function signature. The fixture handles navigation and setup. I also added a tasksApi fixture for direct REST API access when tests need to set up state without clicking through the UI.

Parallel CI jobs

The other improvement was restructuring CI. Some integration tests still require Docker - for example, testing third-party premium plugin integrations that require Composer installation.

The solution: run them in parallel.

  1. Main e2e job - Uses WP Playground, runs fast, covers core plugin functionality
  2. Integration tests job - Uses Docker with MySQL, installs premium plugins via Composer

Both jobs start simultaneously. While Docker spins up MySQL and pulls images, the Playground tests are already running. The main tests typically finish before Docker is even ready.

The tradeoffs

WP Playground isn’t perfect for every scenario:

  • No persistence - Data doesn’t survive restarts. Fine for tests, not for debugging stateful issues.
  • No real MySQL - It uses SQLite. Most WordPress code works fine, but if you’re testing raw SQL queries, watch out.
  • Plugin compatibility - Some plugins that rely on specific PHP extensions or filesystem operations might not work.

For testing UI interactions, API calls, and user flows, none of these matter. The tests verify that clicking buttons does the right thing, and WP Playground handles that perfectly.

Results

After the migration:

  • CI time improved noticeably
  • Tests are more maintainable (TypeScript + Page Object Model)
  • Debugging is easier (the WordPress instance sticks around if you need it)
  • Fewer flaky tests (no more container networking issues)

The combination of WP Playground for speed and proper test architecture for maintainability made the e2e suite something I actually want to run, rather than something I tolerate.


If you’re running WordPress e2e tests with Docker, give WP Playground a look. The CLI documentation covers the basics, and the blueprint system is surprisingly capable once you dig into it.