Skip to content

Testing and standards ​

Everything below runs in CI on every pull request, and the relevant subset runs on staged files before each commit.

sh
npm run lint        # PHPCS + ESLint + Stylelint + Prettier
npm run analyse     # PHPStan
npm run rector      # Rector (dry run)
npm run test        # PHPUnit, then Playwright
npm run composer -- qa   # All PHP checks in one go

PHP ​

ToolConfigNotes
PHP_CodeSniffer + WPCSphpcs.xml.distWordPress-Extra and WordPress-Docs, with the whalepress text domain and prefixes. PSR-4 file names are allowed in src/. npm run composer -- lint:fix runs PHPCBF.
PHPStan + phpstan-wordpressphpstan.neon.distLevel 8 over the theme and must-use plugins.
Rectorrector.phpPHP 8.3 sets plus dead code, code quality, type declarations and early return. Long array syntax and array( $this, 'method' ) callbacks are kept, as WordPress conventions (and remove_action()) need them. Apply with npm run composer -- rector:fix.
PHPUnit + Brain Monkeyphpunit.xml.distUnit tests in tests/php/Unit, with no WordPress or database needed. Stub WordPress functions per test.

Unit tests extend WhalePress\Tests\TestCase, which sets up Brain Monkey. Because services take their dependencies through constructors (paths, URLs, environment), most logic can be tested with plain values. See ViteTest.

Front end ​

ToolConfigScope
ESLint 10wp-content/themes/whalepress/eslint.config.jsTheme JS. Formatting rules are left to Prettier.
Stylelintwp-content/themes/whalepress/stylelint.config.jsstylelint-config-standard, aware of Tailwind 4 at-rules.
Prettier + Twig plugin.prettierrc.jsonJS, CSS, Twig, JSON, YAML and Markdown across the repository. PHP is PHPCBF's job.

The Twig formatter can't parse tags inside an HTML tag. Use conditional attribute values instead:

twig
{# Not parseable #}
<a href="{{ item.link }}" {% if item.current %}aria-current="page"{% endif %}>

{# Use instead #}
<a href="{{ item.link }}" aria-current="{{ item.current ? 'page' : 'false' }}">

End-to-end: Playwright ​

sh
npm run test:e2e               # Against WP_HOME from .env
npm run test:e2e:ui            # Interactive UI mode
BASE_URL=https://staging.example.com npx playwright test --project=frontend

playwright.config.js defines four projects:

ProjectWhat it covers
frontendDesktop Chrome: layout, assets, skip link, 404s, search, single posts.
frontend-mobileThe same suite on a Pixel 7, plus the mobile navigation toggle and submenus.
setupLogs in with WP_ADMIN_USER / WP_ADMIN_PASSWORD and saves the session.
adminDashboard, the block editor with theme styles in its canvas, a post round trip via the REST API, and theme patterns containing only valid, allowed blocks.

The expectAccessible() fixture (in tests/e2e/fixtures.js) runs axe-core against WCAG 2.2 A/AA and fails on serious or critical violations. Use it in any new page test.

Install browsers once with npx playwright install chromium.

Commits and hooks ​

Husky installs two hooks when you run npm install:

  • pre-commit: lint-staged runs ESLint, Stylelint, Prettier and PHPCBF/PHPCS on staged files only (lint-staged.config.js).
  • commit-msg: commitlint enforces Conventional Commits, which semantic-release uses to choose the next version (fix: → patch, feat: → minor, feat!: or BREAKING CHANGE: → major).

Continuous integration ​

.github/workflows/ci.yml runs three jobs in parallel on every pull request and push to main:

  1. PHP quality: PHPCS (annotated inline with cs2pr), PHPStan, Rector, PHPUnit.
  2. Assets and formatting: commitlint for the PR's commits, ESLint, Stylelint, Prettier, a production build of the theme, and a build of this documentation site (which fails on dead links).
  3. Image and end-to-end tests: builds the production image, scans it with Trivy (fails on fixable critical CVEs), runs it with docker-compose.test.yml, installs WordPress, seeds test content (tests/e2e/seed.sh) and runs the full Playwright suite against it. On failure, the Playwright report and container logs are attached.

Pushes to main that pass then release and deploy.

Two manual workflows test deployed environments: E2E (deployed site) runs the front-end Playwright projects, and OWASP ZAP baseline runs a passive security scan. Both read HOST_URL from the chosen GitHub environment.

To run the CI end-to-end setup locally:

sh
docker compose -f docker-compose.test.yml up -d --build --wait
docker compose -f docker-compose.test.yml exec -T -u www-data wordpress bash -s < local/bin/install.sh
docker compose -f docker-compose.test.yml exec -T -u www-data wordpress bash -s < tests/e2e/seed.sh
BASE_URL=http://localhost:8099 WP_ADMIN_USER=admin WP_ADMIN_PASSWORD=password npx playwright test
docker compose -f docker-compose.test.yml down -v

Always finish with down -v. The image declares /var/www/html as a volume, and Compose reuses it when it recreates the container, so without -v a rebuilt image keeps running the previous theme files.

Test content ​

Some tests need content a fresh install doesn't have, and skip without it: the submenu tests need a menu item with children, and the featured image test needs a post with one. tests/e2e/seed.sh adds them (a page under the first primary menu item, and a newest post with the theme's screenshot as its featured image, a category and a tag). It is idempotent and meant only for throwaway sites; never run it against a real one. When a new test needs content, seed it there rather than letting the test skip in CI.

The only skips left in CI are deliberate: the mobile navigation toggle test runs on the mobile project only, and the desktop navigation test on the desktop project only.