wp-self-test: use cases
What wp-self-test does and how people use it, from the product's readme and the docs in its repository.
Running it
| From | Command |
|---|---|
| wp-admin | Tools › Self-test (?page=mmwp-self-test), non-production only |
| WP-CLI | wp mmwp self-test <slug> --format=table runs one plugin's cases; without <slug> it runs every case. Actions: list, preview, run [--cleanup], cleanup, report. Exits 1 when a case fails. |
| REST | GET mmwp/v1/self-test/cases, GET .../preview, POST .../run, POST .../cleanup, GET .../report (cookie + X-WP-Nonce, manage_options) |
| Abilities | mmwp/self-test-list-cases, -preview, -run, -cleanup, -get-report; run and cleanup need confirm: true |
| Pre-push | bin/pre-push via bin/install-hooks, or php bin/run.php (see below) |
<slug> is the plugin slug passed to Registry::register(). A plugin whose slug equals an action name (list, run, ...) has to use --plugin=<slug>.
Source: README.md(opens in a new tab)
Register a manifest
add_action('mmwp_self_test_register', function (\MammothWP\SelfTest\Registry $registry) {
$registry->register('my-plugin', __DIR__ . '/self-test/manifest.json', [
'label' => 'My Plugin',
'requirements' => ['abilities', 'logger'], // optional: valid `requires` keys
'cli_loaders' => [['My_Plugin_Cli_Shim', 'load']], // optional, see "CLI wrappers"
]);
});
self-test/manifest.json:
{
"version": 1,
"cases": [
{
"id": "content-crud",
"label": "Custom content CRUD",
"category": "content",
"layers": ["rest", "cli", "ability"],
"class": "My\\Plugin\\SelfTest\\Content_Crud_Case",
"requires": [],
"args": {},
"covers": {
"rest": ["my-plugin/v1/items", "my-plugin/v1/items/{id}"],
"cli": ["my-plugin items list"],
"ability": ["my-plugin/list-items"]
}
}
]
}
Case ids must be unique across every registered plugin. covers is documentation the package carries along (your own tests can assert every route, command and ability you register is claimed by a case).
Source: README.md(opens in a new tab)
Write a case
use MammothWP\SelfTest\Case_Base;
class Content_Crud_Case extends Case_Base
{
public function preview() { return ['Create an item over REST', 'Read it back over the CLI', '…']; }
public function run()
{
$o = [];
$o['create'] = $this->rest('POST', '/my-plugin/v1/items', ['title' => 'Self-test']);
$o['cli'] = $this->cli(['My_Cli', 'get'], [$o['create']['data']['id']], []);
$o['get'] = $this->ability('my-plugin/get-item', ['id' => $o['create']['data']['id']]);
return ['observed' => $o, 'id' => $o['create']['data']['id']];
}
public function validate(array $context)
{
$o = $context['observed'];
return [
$this->check('rest', 'POST /items returns 201', $this->status($o, 'create') === 201, $this->detail($o, 'create')),
$this->cli_check($o, 'cli', 'wp my-plugin items get prints the item'),
$this->ability_check($o, 'get', 'my-plugin/get-item returns the item', fn($data) => !empty($data['id'])),
];
}
public function cleanup(array $context)
{
delete_item((int) $context['id']);
return ['Deleted the test item.'];
}
}
Four phases, each callable on its own: preview() (no side effects), run() (returns a JSON-serialisable context, may create data), validate($context) (turns observations into check() / skip() rows), cleanup($context) (removes what run() created; the runner stores the context so cleanup can happen in a later request). Helpers: rest(), cli(), ability(), status(), detail(), cli_check(), cli_check_printed(), ability_check(), availability().
Source: README.md(opens in a new tab)
CLI wrappers
Cases call CLI command handlers in-process. In a real wp process that is WP-CLI itself; in PHPUnit, the dashboard and bin/run.php it is the shim in src/Wp_Cli_Shim.php, a minimal WP_CLI that records output and throws instead of exiting. Command classes that return early when WP_CLI is missing are declared again by the cli_loaders callables a plugin passes to register(); the runner calls them once before a run.
Source: README.md(opens in a new tab)
Browser layer
The rest, cli, ability and core layers never load wp-admin in a browser, so JavaScript errors, screens that fail to render and broken form flows go unnoticed. The browser layer runs Playwright (opens in a new tab) against the real site through the same manifest, runner and pre-push hook. It never runs on production and nothing of it loads on front-end requests.
Add it to a plugin
- Add the Node package straight from this git repo (no npm registry), plus Playwright, next to the plugin's
manifest.json(the folder with thenode_modulesthe runner finds is searched from the manifest upwards):
{
"devDependencies": {
"@mammothwp/wp-self-test-e2e": "git+https://github.com/mammothwp/wp-self-test.git#main",
"@playwright/test": "^1.50.0"
}
}
Then npm install and npx playwright install chromium. The package.json sits at the repo root with a files whitelist (only e2e/), and every Node file is export-ignored, so the Composer copy in lib/ never contains it.
- Declare browser checks on a case in
manifest.json:
{
"id": "admin-smoke",
"label": "Admin smoke",
"class": "MammothWP\\SelfTest\\Browser_Case",
"category": "admin",
"browser": {
"crawl": true,
"specs": ["self-test/e2e/coupon-flow.spec.js"],
"allow": {
"console": ["Failed to load resource.*gravatar"],
"requests": ["/wp-json/some/v1/known-404"],
"pages": ["^admin\\.php\\?page=legacy-screen$"]
},
"timeout": 300
}
}
| Key | Meaning |
|---|---|
crawl | true opens the front page once and every page of the admin menu inventory (GET only) and requires HTTP 200, no Fatal error/Warning/Notice/Deprecated/WordPress database error/"There has been a critical error" markup, no console errors, no uncaught page errors (pageerror), no >= 500 response on any request and no >= 400 wp-json or admin-ajax response. |
specs | Playwright spec files, relative to the folder with node_modules (no ..). |
allow | Per-plugin allow-lists of regular-expression sources: console (messages), requests ("STATUS URL"), pages (inventory paths to skip). |
timeout | Seconds the Node runner may take (default 300). |
A case with browser automatically lists the browser layer (add "layers": ["rest"] as before for the other layers). Use Browser_Case for browser-only cases, or your own Case_Base subclass to combine PHP phases with browser checks. Put crawl in one case only; every case with browser starts its own Node run.