Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/alordiel/mandragora-qa-test-manager/archive/refs/heads/main.zipManual QA test runs for a small internal team. Testers open a page in wp-admin, pick a run, work through a list of test cases, and mark each one pass / fail / blocked / skipped. Failures get a comment and a link to a GitHub issue, and issues stay attached to the case, so the next person to test it sees what is currently broken.
No automated testing, no code execution, no screenshots. Everything is human-entered.
Requires PHP 8.2+, WordPress 6.4+, MySQL 8.
The three concepts
| Case | A permanent instruction, written once and edited rarely. Belongs to a suite. Carries no status. |
| Run | One pass through a chosen subset of cases, at a point in time, against a named environment and version. |
| Result | The intersection of a run and a case: status, who set it, when. One row per case per run. |
Putting a status on a case is the mistake this schema exists to prevent — a case has no status, only results do, and that is what makes run #12 comparable to run #9.
Runs are independent. Several are open at once and completing one never touches another.
Completion is always an explicit action, never automatic when the last case is set, because
testers revisit results. Once a run is not open, the API rejects result, comment and lock
writes with a 409.
Issues attach to the case, not the run. That is the one cross-run link: when anyone opens that case in any later run, they see a banner for each open issue. Resolved issues disappear from the test flow entirely and live on only in the case library's audit view.
Install
npm install && npm run build # build/ is committed, so this is only needed after edits
composer install # optional: dev tooling only, no runtime dependency
Activate the plugin. Activation creates the tables, registers the mqatm_tester role, writes
the default options.
The runtime has no Composer dependency — classes autoload from includes/ via a PSR-4
autoloader in the main plugin file, so an rsync deploy needs no install step on the server.
Verify the REST namespace first
Before debugging anything in the app, confirm the API is reachable. Open the Mandragora QA Test Manager screen and run this in the browser console:
fetch(mqatm.root + 'ping', {headers: {'X-WP-Nonce': mqatm.nonce}, credentials: 'same-origin'})
.then((r) => r.json())
.then(console.log);
A 200 with {ok: true, ...} means the stack is wired correctly. A 403 is a server-side
filter on the REST API, not plugin code — resolve that before going further.
Capabilities
Always checked as capabilities, never role names.
| Capability | Grants |
|---|---|
mqatm_view |
See the QA screens |
mqatm_run_tests |
Set results, comment, raise and resolve issues, create runs |
mqatm_manage_cases |
Edit the case library and suites, delete any comment, abandon runs, change settings |
Two roles ship with the plugin, both cloned from subscriber so their users can reach wp-admin at all:
| Role | Capabilities | Owns |
|---|---|---|
mqatm_admin (QA Admin) |
all three | The library: creates, edits and archives suites and cases, abandons runs, changes settings |
mqatm_tester (QA Tester) |
mqatm_view, mqatm_run_tests |
The work in flight: creates and runs runs, adds and removes their cases, claims cases, comments, raises and resolves issues |
The line between them is mqatm_manage_cases. A tester sees the whole library but cannot
create or archive anything in it; the case library, suites and settings screens are hidden
from them in the router as well as refused by the REST layer. Testers can create runs — that
is deliberate.
Roles::ELEVATED_ROLES names the WordPress roles that also receive the full set
(administrator). It is the source of truth: on install the caps are granted to the roles
listed and taken back from every role that is not, the two QA roles aside. Bump
Roles::VERSION after changing it so maybe_install() reconciles on the next admin
request, or deactivate and reactivate the plugin.
Run assignment is informational: it sends an email and shows an avatar. It does not restrict
who may set a result. Case assignment within a run is a claim testers make on themselves —
it needs no capability beyond mqatm_run_tests plus a place on the run.
Build
vite.config.js emits an IIFE bundle at fixed paths — build/mqatm-admin-page.js and
build/mqatm-admin-page.css — rather than a hashed ES module with a manifest. Assets.php
enqueues those directly as a classic script, so there is no manifest to read and no
script_loader_tag filter adding type="module". Cache busting comes from
MQATM_VERSION.
build/ is committed on purpose: deployment is rsync-based with no build step on the server.
npm run dev # rebuild on change, unminified, with devtools
npm run build # production bundle
npm run lint # eslint
npm run format # prettier
composer lint # phpcs, WordPress ruleset
Notifications
Two emails, both plain HTML tables, and both only when somebody assigns another person — assigning yourself sends nothing:
- Run assignment, sent when someone is added to a run.
notified_atis stamped on success, so re-saving a run never double-sends. - Case assignment, sent when someone is given a case within a run. Re-assigning a person already on the case does not re-send.
Both are sent with the wp_mail_content_type filter attached immediately before the send
and removed immediately after, so other plugins' mail is untouched.
"Pause notifications" in Settings stops both.
Translations
Text domain mandragora-qa-test-manager. PHP uses the usual __() family. The Vue app uses WordPress's
wp-i18n script, in two forms that both survive minification — the only way strings reach
wp i18n make-pot, which reads the built bundle (it cannot parse .vue files):
- Templates:
{{ __('Text', 'mandragora-qa-test-manager') }}, plus_n,_xandsprintf. These are app-wide globals (src/main.js) and compile to_ctx.__(…). - Script code:
wp.i18n.__('Text', 'mandragora-qa-test-manager'). Never import or destructure__: the minifier renames it, and its strings silently drop out of the POT.
Always pass the text domain as a literal, and use sprintf placeholders, never string
concatenation, so translators can reorder words.
npm run make-pot # regenerate languages/mandragora-qa-test-manager.pot (needs WP-CLI); release does this too
Assets.php calls wp_set_script_translations() with languages/ as the path, so a
mandragora-qa-test-manager-{locale}-{md5}.json built there with wp i18n make-json is picked up first.
WordPress.org language packs are the fallback.
Uninstall
Removing the plugin always removes the role, its capabilities and the plugin options. The tables are only dropped when "delete data on uninstall" is enabled in Settings, which is off by default. QA history should not vanish because someone removed the plugin to troubleshoot something else.
Layout
mandragora-qa-test-manager.php bootstrap, constants, PSR-4 autoloader, activation hooks
uninstall.php role + options always; tables only behind the opt-in
includes/
Plugin.php singleton, wires hooks
Install/ Schema (dbDelta + migrations), Roles, Seeder
Admin/ Menu (mount point), Assets (enqueue + window.mqatm)
Repository/ one per table, every query via $wpdb->prepare()
Rest/ Controller base + one controller per resource
Notification/ Mailer
Support/ Enum, Sanitize, Dates, Settings
templates/emails/ assignment.php, case-assignment.php
src/ Vue 3 source
build/ Vite output — committed
Notes for future work
- No ENUM columns. Every status and priority field is
VARCHAR(20), becausedbDeltamangles ENUM on subsequent migrations. The allowed values live inSupport/Enum.phpand are shared by the repositories and the RESTvalidate_callbacks. - Timezones. Everything is stored and emitted as UTC.
current_time('mysql')is never mixed withgmdate(); the client formats for display. - Soft locks are compared against a 15-minute TTL on read. There is no cron clearing them, and they never block a second tester from submitting.
$wpdb->prefix, notbase_prefix— single site. Table names are built in one place,Schema::table(), so a multisite move is a one-line change.