-
Notifications
You must be signed in to change notification settings - Fork 0
Testing and 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 (
enginesrequiresnode >=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
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, andpnpm test:coverageare provided by the@stratawp/testingpackage (vitest run,playwright test, andvitest run --coveragerespectively). The bundled example theme (examples/basic-theme) defines onlydev/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 testis wired through Turborepo (turbo test) andpnpm test:perf(lhci autorun) is a root-only script used by CI — see the workflows below.
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.
-
Add the testing package to your theme (already present in scaffolded themes):
pnpm add -D @stratawp/testing
-
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 }, })
-
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() }) })
-
Run the unit tests:
pnpm testExpected output: Vitest discovers files under
__tests__/, reports eachdescribe/itblock, and exits0when all pass.
Tip Always call
setupWordPressMocks()andsetupCustomMatchers()inbeforeAll. The first stubs the WordPress JS APIs; the second registers the StrataWP block matchers.If this fails with
wp is not defined, you skippedsetupWordPressMocks(). If custom matchers liketoHaveBlockClassare "not a function", you skippedsetupCustomMatchers().
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 asvitestConfigin step 2 above), and the shareable Playwright preset asplaywright.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() |
The testing package documents coverage targets for theme suites. Run:
pnpm test:coverageThen open the HTML report:
open coverage/index.htmlDocumented 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 shipsvitestas a dependency).
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.
-
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 }, })
-
Install the Playwright browsers (once per machine/CI):
pnpm exec playwright install --with-deps -
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') }) })
-
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
baseURLyou configured. The shared Playwright preset defaults toprocess.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
timeoutinplaywright.config.ts(e.g.timeout: 60 * 1000).
Note The repo-root
pnpm test:e2escript 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, runpnpm test:e2evia the@stratawp/testingpackage (playwright test) or invoke Playwright directly as shown above.
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 |
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:
-
Builds the
@stratawp/vite-pluginand@stratawp/testingpackages. -
Builds the example theme (
examples/basic-theme), copying the path-repo core intovendor/viaCOMPOSER_MIRROR_PATH_REPOS=1. -
Boots WordPress with
pnpm exec wp-env start, activatesbasic-theme, and enables pretty permalinks (wp rewrite structure '/%postname%/' --hard+ flush). -
Installs Playwright Chromium (
pnpm exec playwright install --with-deps chromium) and waits forhttp://localhost:8888/to respond (node scripts/wait-for-http.mjs http://localhost:8888/ 120000). -
Runs the axe scan:
pnpm exec playwright test --config playwright.a11y.config.ts
-
Uploads the Playwright report as the
playwright-a11y-reportartifact (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
StrataWP enforces a Lighthouse CI budget defined in .lighthouserc.cjs. Run it from the repo root with:
pnpm test:perfUnder 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.
The main quality gate is .github/workflows/ci.yml, which runs on pushes to main and on every pull request. It has four jobs:
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) |
Runs inside packages/core:
composer install --no-interaction --no-progress
composer phpcs
composer phpstan
composer testThis lints (phpcs), statically analyses (phpstan), and unit-tests (PHPUnit) the @stratawp/core PHP framework.
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 syncInstalls the core QA toolchain (phpcs + WordPress Coding Standards) and runs:
pnpm lint:php # node scripts/lint-php.mjs — WPCS across all theme PHP| 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 | 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 |
| 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 |
- Blocks, Patterns & Design Systems — what you're testing
-
Architecture & Packages — the
@stratawp/testingpackage internals -
AI, Agent Skills & MCP — the contract checks the
contractsCI job enforces - Deployment — shipping once tests are green
- Contributing & Releases — CI expectations for contributors
StrataWP v2.0.0 · GPL-3.0-or-later · Built by Jon Imms Repository · README
Start here
Building themes
Shipping
Extending & contributing
Help