wordpress-jwt-auth

Playground tests

Real WordPress and real WooCommerce, running on WASM PHP via @wp-playground/cli. No Docker, no database server, no wp-env.

This is a separate npm package with its own lockfile. @wp-playground/cli and playwright-core are large, and the root package-lock.json is consumed by the Nix build — keeping them apart means the plugin build never has to resolve them.

Prerequisites

composer install          # repo root — the plugin fatals without vendor/autoload.php
npm ci && npm run build   # repo root — produces build/woo-login.js
npm --prefix tests/playground ci

run-browser.mjs drives system Chrome rather than downloading a browser. It defaults to /run/current-system/sw/bin/google-chrome-stable; override with CHROME_PATH. Set WP_VERSION to test against something other than latest.

What each test covers

Test WordPress? Browser? Guards
run-assets.mjs no no The rolldown → PHP seam: build/woo-login.asset.php has the shape enqueueAssets() requires, and the bundle keeps the properties the source promises (no innerHTML, login-form selector only).
run-e2e.mjs yes no The server side: logged-out My Account returns exactly one button, pointing at wp-login.php, with the script and its inline config enqueued at the manifest’s version.
run-security.mjs yes no That password login is genuinely dead against real core: the authenticate filter ordering, wp_authenticate() with a valid password, wp-login.php, XML-RPC, and application passwords — plus the passthroughs (cron, empty submission) that must survive.
run-browser.mjs yes yes The rendered result, after the script has run.
run-exclusive.mjs yes yes JWT_AUTH_EXCLUSIVE: that WooCommerce renders the plugin’s templates in place of its own, that the handlers behind them are unhooked from real WooCommerce, that a real HTML parser finds no credential field in wp_login_form()’s output, and that wp-login.php still does everything that is not a login.

run-security.mjs exists because a hand-rolled fake WordPress structurally cannot catch its bug. authenticate is a filter: every callback receives the previous one’s return value, and the last one to run decides. Core registers wp_authenticate_username_password, wp_authenticate_email_password and wp_authenticate_application_password at priority 20. The plugin originally filtered at priority 1, so core ran afterwards, resolved the password, and handed back a WP_User — silently overwriting the WP_Error. Password login worked the entire time. Only real core, with real callbacks in real order, can show that.

run-browser.mjs is the only test that can see the bug this suite was built for. The duplicate “Sign in with SSO” button on /my-account was added client-side: PHP emitted one via woocommerce_login_form_start, the fallback script prepended a second, and every server-side assertion still counted exactly one. It also covers the block checkout, where an earlier selector list matched .wc-block-components-form — the checkout fields form — and dropped a button among the address inputs.

run-exclusive.mjs boots its own server from mu-plugins-exclusive/, which requires the shared config and adds JWT_AUTH_EXCLUSIVE. A second directory rather than a flag because constants are this plugin’s configuration surface and PHP cannot redefine one — exclusive mode has to be a differently-booted site, not a differently-configured request.

Three of its checks cannot be written any other way:

To confirm those checks still bite, restore the old behaviour in assets/src/sso-button.ts — swap the .jwt-auth-sso marker guard in injectInto() for a data-jwt-auth-done attribute flag, and widen LOGIN_FORM_SELECTOR back to .woocommerce-form-login, .wc-block-components-form, .wp-block-woocommerce-customer-account form — then npm run build and re-run. Four checks fail, found 2 among them.

Notes