Testing and standards
Everything below runs in CI on every pull request, and the relevant subset runs on staged files before each commit.
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 goPHP
| Tool | Config | Notes |
|---|---|---|
| PHP_CodeSniffer + WPCS | phpcs.xml.dist | WordPress-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-wordpress | phpstan.neon.dist | Level 8 over the theme and must-use plugins. |
| Rector | rector.php | PHP 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 Monkey | phpunit.xml.dist | Unit 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
| Tool | Config | Scope |
|---|---|---|
| ESLint 10 | wp-content/themes/whalepress/eslint.config.js | Theme JS. Formatting rules are left to Prettier. |
| Stylelint | wp-content/themes/whalepress/stylelint.config.js | stylelint-config-standard, aware of Tailwind 4 at-rules. |
| Prettier + Twig plugin | .prettierrc.json | JS, 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:
{# 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
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=frontendplaywright.config.js defines four projects:
| Project | What it covers |
|---|---|
frontend | Desktop Chrome: layout, assets, skip link, 404s, search, single posts. |
frontend-mobile | The same suite on a Pixel 7, plus the mobile navigation toggle and submenus. |
setup | Logs in with WP_ADMIN_USER / WP_ADMIN_PASSWORD and saves the session. |
admin | Dashboard, 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!:orBREAKING CHANGE:→ major).
Continuous integration
.github/workflows/ci.yml runs three jobs in parallel on every pull request and push to main:
- PHP quality: PHPCS (annotated inline with cs2pr), PHPStan, Rector, PHPUnit.
- 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).
- 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:
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 -vAlways 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.
