Skip to content

Theme Testing

The theme ships 41 tests — 23 PHP (PHPUnit 12) and 18 JS (Jest 29 + jsdom). Both run in CI on every push.

Layout

theme/tests/
├── ThemeTestCase.php            # Base TestCase — Brain\Monkey setup/teardown
├── bootstrap.php                # Composer autoload + Brain\Monkey kickoff
├── unit/
│   ├── AppPassword/             # _restart_api_key creation hook
│   ├── Filters/                 # wp_enqueue_scripts, font enqueue, etc.
│   └── Shortcodes/              # The five theme shortcodes
└── js/
    ├── auth.test.js
    ├── contact-modal.test.js
    └── start-registry.test.js

Running

cd theme

# PHP
composer install
./vendor/bin/phpunit               # or: phpunit --testsuite=unit
./vendor/bin/phpunit --filter Login

# JS
npm ci
npm test                           # all Jest tests
npm test -- auth                   # only auth.test.js

The Make targets:

make theme-test           # PHP + JS
make theme-test-php
make theme-test-js

PHP test patterns

Brain\Monkey for WP functions

ThemeTestCase runs Brain\Monkey's setUp() / tearDown() around each test. Inside a test you stub WP functions you intend to use:

\Brain\Monkey\Functions\when('is_user_logged_in')->justReturn(true);
\Brain\Monkey\Functions\when('wp_get_current_user')->justReturn($user);

include instead of require_once

functions.php defines its shortcodes inside add_shortcode() callbacks. To re-execute the file per test (so each test gets a clean callback registry), use include rather than require_once. PHPUnit caches included files, but include re-runs the top-level statements:

public function test_login_shortcode_renders(): void {
    \Brain\Monkey\Functions\when('is_user_logged_in')->justReturn(false);
    include __DIR__ . '/../../../functions.php';
    $output = do_shortcode('[restart_login_form]');
    $this->assertStringContainsString('<form', $output);
}

This pattern is used across the shortcode tests.

JS test patterns

jQuery Deferred for AJAX mocking

auth.js and friends use $.post(...).done(...).fail(...) chains. Jest tests stub $.post to return a real jQuery Deferred so the chain behaves exactly like production:

const dfd = $.Deferred();
$.post = jest.fn().mockReturnValue(dfd);
// trigger the form submit
$('#rr-register-form').submit();
// resolve to drive the .done() branch
dfd.resolve({ success: true, data: { redirect: '/my-account/' } });

This catches subtle bugs that a generic jest.fn().mockResolvedValue(...) would mask — for example, that the code expected .done() semantics, not Promise semantics.

setTimeout instead of setImmediate

jsdom does not provide setImmediate. The tests use setTimeout(fn, 0) to schedule "after this tick" assertions:

setTimeout(() => {
    expect(window.location.href).toBe('/my-account/');
    done();
}, 0);

include equivalent in JS

JS files run their setup at module load. To re-run that setup per test, the suite re-requires the module after clearing the require cache:

beforeEach(() => {
    jest.resetModules();
    document.body.innerHTML = fixture;
    require('../../assets/js/auth.js');
});

This is the JS twin of the PHP include pattern.

What is covered

Suite Coverage
unit/Shortcodes/ All five shortcodes — logged-out branch, logged-in branch, error states
unit/Filters/ wp_enqueue_scripts callback, font enqueue, start-registry script gating
unit/AppPassword/ The auto-create-application-password hook on /start-a-registry/
js/auth.test.js Register form happy/failure, profile toggle, profile update happy/failure
js/start-registry.test.js Submit happy/failure, private toggle, validation errors
js/contact-modal.test.js Open, close on escape/backdrop/button, body scroll lock

CI

make theme-test runs in ci.yml on every push. There is no theme-specific deploy workflow; theme releases are currently manual zip uploads built with make theme-pack.