Skip to content

Testing and Quality

Jon Imms edited this page Jun 25, 2026 · 1 revision

Testing & Quality

How to test a StrataWP theme end to end: Vitest unit tests, Playwright E2E flows, accessibility (axe) scans, and Lighthouse performance budgets — plus the CI workflows that enforce them on every pull request.

StrataWP ships a dedicated @stratawp/testing package with WordPress-aware mocks, custom matchers, and shareable Vitest/Playwright configs, so you can write meaningful tests without wiring up the WordPress JavaScript APIs by hand.

Prerequisites

  • Node.js 18 or higher (engines requires node >=18.18) and pnpm (see Installation & Quick Start)
  • A StrataWP theme scaffolded with npx create-stratawp my-theme
  • For E2E and accessibility tests: a running local WordPress (e.g. Local, MAMP, Docker, or @wordpress/env)
  • PHP 8.1+ if you also run the PHP test suite in @stratawp/core

Overview

StrataWP testing has four layers. The first three are things you run as a theme author; the fourth is enforced automatically in CI.

Layer Tool Command What it checks
Unit Vitest pnpm test Block registration, edit/save rendering, attribute logic
E2E Playwright pnpm test:e2e Real editor workflows (insert, edit, publish)
Accessibility Playwright + axe CI workflow (a11y.yml) WCAG 2.1 A/AA violations on rendered pages
Performance Lighthouse CI pnpm test:perf (repo root) Performance/a11y/best-practices/SEO budgets + Core Web Vitals

Note The theme-author commands pnpm test, pnpm test:e2e, and pnpm test:coverage are provided by the @stratawp/testing package (vitest run, playwright test, and vitest run --coverage respectively). The bundled example theme (examples/basic-theme) defines only dev/build/preview/typecheck, so in that theme these test commands run through the testing package rather than a theme-local script. At the monorepo root, pnpm test is wired through Turborepo (turbo test) and pnpm test:perf (lhci autorun) is a root-only script used by CI — see the workflows below.


1. Write your first unit test

Vitest is the unit runner. The @stratawp/testing/vitest entry point gives you WordPress API mocks, render helpers, and custom matchers so a block test reads declaratively.

Steps

  1. Add the testing package to your theme (already present in scaffolded themes):

    pnpm add -D @stratawp/testing
  2. Create a Vitest config that extends the shared preset, vitest.config.ts:

    import { defineConfig } from 'vitest/config'
    import { vitestConfig } from '@stratawp/testing/vitest'
    
    export default defineConfig({
      ...vitestConfig,
      test: {
        ...vitestConfig.test,
        // Your custom test configuration
      },
    })
  3. Create a test file, __tests__/my-block.test.tsx:

    import { describe, it, expect, beforeAll } from 'vitest'
    import {
      renderBlockEdit,
      testBlockRegistration,
      setupWordPressMocks,
      setupCustomMatchers,
    } from '@stratawp/testing/vitest'
    
    beforeAll(() => {
      setupWordPressMocks()
      setupCustomMatchers()
    })
    
    describe('My Block', () => {
      it('should register correctly', () => {
        testBlockRegistration('my-theme/my-block', {
          title: 'My Block',
          category: 'common',
        })
      })
    
      it('should render edit component', () => {
        const EditComponent = ({ attributes }: any) => (
          <div className="wp-block-my-theme-my-block">{attributes.content}</div>
        )
    
        const { getByText } = renderBlockEdit(EditComponent, {
          attributes: { content: 'Hello World' },
        })
    
        expect(getByText('Hello World')).toBeInTheDocument()
      })
    })
  4. Run the unit tests:

    pnpm test

    Expected output: Vitest discovers files under __tests__/, reports each describe/it block, and exits 0 when all pass.

Tip Always call setupWordPressMocks() and setupCustomMatchers() in beforeAll. The first stubs the WordPress JS APIs; the second registers the StrataWP block matchers.

If this fails with wp is not defined, you skipped setupWordPressMocks(). If custom matchers like toHaveBlockClass are "not a function", you skipped setupCustomMatchers().

Unit testing toolkit

These are exported from @stratawp/testing/vitest.

Export Purpose
setupWordPressMocks() Mock @wordpress/blocks, data, i18n, components, block-editor, element, api-fetch
setupCustomMatchers() Register WordPress-specific matchers
renderBlockEdit(Component, props) Render a block's edit component
renderBlockSave(Component, props) Render a block's save component
testBlockRegistration(name, config) Assert a block registers with the expected config
createMockAttributes(name, overrides) Build mock attributes for a block type

Note The shareable Vitest preset is shipped as the package's vitest.config.ts (imported as vitestConfig in step 2 above), and the shareable Playwright preset as playwright.config.ts (playwrightConfig).

Custom matchers:

Matcher Example
toHaveBlockClass expect(element).toHaveBlockClass('my-theme/my-block')
toBeRegisteredBlock expect('my-theme/my-block').toBeRegisteredBlock()
toHaveBlockAttributes expect(blockType).toHaveBlockAttributes(['content', 'align'])
toBeValidWordPressBlock expect(element).toBeValidWordPressBlock()
toBeValidBlockMarkup expect(markup).toBeValidBlockMarkup()

2. Run tests with coverage

The testing package documents coverage targets for theme suites. Run:

pnpm test:coverage

Then open the HTML report:

open coverage/index.html

Documented coverage targets (@stratawp/testing):

Metric Target
Lines 80%
Functions 80%
Statements 80%
Branches 75%

Note For an instant re-run loop while developing, run Vitest in watch mode directly with pnpm exec vitest (the package ships vitest as a dependency).


3. Write your first E2E test

Playwright drives a real browser against a running WordPress install to exercise full editor workflows. The @stratawp/testing/playwright entry point provides login and block-editor helpers.

Steps

  1. Create a Playwright config that extends the shared preset, playwright.config.ts:

    import { defineConfig } from '@playwright/test'
    import { playwrightConfig } from '@stratawp/testing/playwright'
    
    export default defineConfig({
      ...playwrightConfig,
      use: {
        ...playwrightConfig.use,
        baseURL: 'http://localhost:8888', // Your WordPress URL
      },
    })
  2. Install the Playwright browsers (once per machine/CI):

    pnpm exec playwright install --with-deps
  3. Create an E2E spec, e2e/my-block.spec.ts:

    import {
      test,
      expect,
      wpLogin,
      openBlockEditor,
      insertBlock,
      publishPost,
    } from '@stratawp/testing/playwright'
    
    test.describe('My Block E2E', () => {
      test.beforeEach(async ({ page }) => {
        await wpLogin(page)
      })
    
      test('should insert and publish block', async ({ page }) => {
        await openBlockEditor(page, 'post')
        await insertBlock(page, 'My Block')
    
        await page.fill('[data-type="my-theme/my-block"] input', 'Test content')
    
        await publishPost(page)
    
        await expect(page.locator('.components-snackbar')).toContainText('published')
      })
    })
  4. Run the E2E suite:

    pnpm test:e2e

    Expected output: Playwright launches the browser, runs each test, and prints a pass/fail summary.

Warning E2E tests need WordPress actually running and reachable at the baseURL you configured. The shared Playwright preset defaults to process.env.WP_BASE_URL || 'http://localhost:8888'. If the editor never loads, confirm the site is up and the URL is correct.

If tests time out, raise timeout in playwright.config.ts (e.g. timeout: 60 * 1000).

Note The repo-root pnpm test:e2e script is a special case: it is pinned to the accessibility suite (cd examples/basic-theme && pnpm exec playwright test --config playwright.a11y.config.ts). To run a generic E2E suite for your own theme, run pnpm test:e2e via the @stratawp/testing package (playwright test) or invoke Playwright directly as shown above.

E2E helpers

Exported from @stratawp/testing/playwright:

Helper Purpose
wpLogin(page, user?, pass?) Log in to wp-admin
openBlockEditor(page, type) Open editor for 'post', 'page', or a CPT
insertBlock(page, name) Insert a block by title
publishPost(page) Publish the current post
previewPost(page) Open the front-end preview in a new tab
selectBlock / deleteBlock Select or delete a block by name
moveBlockUp / moveBlockDown Reorder blocks
updateBlockAttribute(page, label, value) Change an attribute via the inspector
setupConsoleErrorTracking(page) Collect console errors to assert against

4. Accessibility checks (axe, WCAG 2.1 A/AA)

StrataWP runs an automated accessibility scan with axe via Playwright against a fully rendered theme. This runs in CI on every push to main and every pull request — see .github/workflows/a11y.yml.

What the workflow does, in order:

  1. Builds the @stratawp/vite-plugin and @stratawp/testing packages.

  2. Builds the example theme (examples/basic-theme), copying the path-repo core into vendor/ via COMPOSER_MIRROR_PATH_REPOS=1.

  3. Boots WordPress with pnpm exec wp-env start, activates basic-theme, and enables pretty permalinks (wp rewrite structure '/%postname%/' --hard + flush).

  4. Installs Playwright Chromium (pnpm exec playwright install --with-deps chromium) and waits for http://localhost:8888/ to respond (node scripts/wait-for-http.mjs http://localhost:8888/ 120000).

  5. Runs the axe scan:

    pnpm exec playwright test --config playwright.a11y.config.ts
  6. Uploads the Playwright report as the playwright-a11y-report artifact (retained 7 days).

Tip To reproduce locally, start your WordPress environment, then run the a11y Playwright config from your theme directory the same way the workflow does:

pnpm exec playwright test --config playwright.a11y.config.ts

5. Performance budgets (Lighthouse CI)

StrataWP enforces a Lighthouse CI budget defined in .lighthouserc.cjs. Run it from the repo root with:

pnpm test:perf

Under the hood this is lhci autorun, which collects 3 runs against http://localhost:8888/ using the desktop preset and asserts the budget below.

The enforced budget:

Audit Assertion
Performance category >= 0.95
Accessibility category >= 0.90
Best Practices category >= 0.90
SEO category >= 0.90
Largest Contentful Paint (LCP) <= 2000 ms
Cumulative Layout Shift (CLS) <= 0.1
Total Blocking Time (TBT) <= 200 ms
Modern image formats warn
Responsive images warn
Unused CSS rules warn

Results are written to ./.lighthouseci.

The CI side lives in .github/workflows/perf.yml: it builds the Vite plugin and example theme, starts wp-env, activates the theme, extracts critical CSS (node scripts/extract-critical.mjs http://localhost:8888/ examples/basic-theme/dist), then runs pnpm test:perf.

Note The performance workflow is currently marked continue-on-error: true (non-blocking) while the budget is calibrated against real CI numbers. Treat a Lighthouse failure as a signal to investigate, not yet a hard gate.


6. How CI enforces quality on pull requests

The main quality gate is .github/workflows/ci.yml, which runs on pushes to main and on every pull request. It has four jobs:

js — JavaScript/TypeScript (Node 18, 20, 22, 24 matrix)

Runs, in order:

pnpm install --frozen-lockfile
pnpm build
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test
Script What it does
build turbo build across all packages
typecheck turbo typecheck (TypeScript validation)
lint eslint .
format:check prettier --check "**/*.{ts,tsx,md,json}"
test turbo test (Vitest unit suites)

php — PHP framework (PHP 8.1–8.4 matrix)

Runs inside packages/core:

composer install --no-interaction --no-progress
composer phpcs
composer phpstan
composer test

This lints (phpcs), statically analyses (phpstan), and unit-tests (PHPUnit) the @stratawp/core PHP framework.

contracts — MCP tool-schema stability

Verifies the AI/MCP tool surface hasn't drifted (see AI, Agent Skills & MCP):

pnpm contracts:check       # MCP tool-contract snapshot in sync
pnpm contracts:validate    # declaration files validate against schema
pnpm contracts:types:check # generated block types in sync

php-themes — Theme PHP coding standards (WPCS)

Installs the core QA toolchain (phpcs + WordPress Coding Standards) and runs:

pnpm lint:php   # node scripts/lint-php.mjs — WPCS across all theme PHP

Companion workflows

Workflow File Gate
Accessibility (axe) a11y.yml WCAG 2.1 A/AA — blocking
Performance (Lighthouse) perf.yml Budget — non-blocking (calibrating)

Tip Run the same checks locally before opening a PR to get a green build first time:

pnpm build && pnpm typecheck && pnpm lint && pnpm format:check && pnpm test

Command reference

Command Layer Notes
pnpm test Unit Vitest (turbo test at the repo root)
pnpm exec vitest Unit Vitest watch mode while developing
pnpm test:coverage Unit Coverage run (@stratawp/testing)
pnpm test:e2e E2E Playwright (root script runs the a11y config)
pnpm test:perf Performance lhci autorun against the budget (repo root)
pnpm typecheck Quality TypeScript (turbo typecheck)
pnpm lint Quality eslint .
pnpm format:check Quality Prettier check
pnpm lint:php Quality WPCS over theme PHP
pnpm contracts:check / :validate / :types:check Quality MCP + types contracts

Troubleshooting

Symptom Fix
wp is not defined in unit tests Call setupWordPressMocks() in beforeAll
toHaveBlockClass is not a function Call setupCustomMatchers() in beforeAll
Playwright tests time out Raise timeout in playwright.config.ts (e.g. 60 * 1000)
Block editor never loads in E2E Ensure WordPress is running and baseURL (WP_BASE_URL) is correct
Lighthouse budget fails Investigate LCP/CLS/TBT; the perf job is non-blocking while calibrating

Related pages

Clone this wiki locally