| name | mx-test-page |
| description | Drive a Playwright browser test against a page on the running TestOSApp3 testbed (port 8082). Auto-asserts the testbed fingerprints (URL :8082 + "Test Project" ribbon) before any further assertion -- this is a HARD safety gate against accidentally driving the live OneSource project. Uses the Playwright MCP server (auto-attached to Claude Code as `playwright`). Captures screenshot + accessibility snapshot. Auto-invoke after a /mx-handoff completes, after any page change, and as part of any verification flow that needs to prove the rendered UI works. Also invocable as /mx-test-page [page-path-or-name]. |
mx-test-page
Playwright orchestration skill scoped to the testbed. The "did the page actually work" primitive that closes the loop on every other Mendix-write skill.
North star
No page change is "done" until Playwright says it works on the testbed.
Every Tier 3 / Tier 4 Mendix write skill (/mx-scaffold-microflow, /mx-entity-add, future /mx-page-from-prototype, /mx-page-clone-from-template) ends with this skill verifying the rendered output. No exceptions.
Hard safety gate -- testbed fingerprints
Every Playwright run MUST assert both:
- The URL contains
:8082 (the testbed runtime port; the live OneSource project runs on :8080).
- The page contains the visible text
Test Project (the ribbon Neo added top-right of every testbed page).
If either assertion fails, the skill aborts immediately with a TESTBED-MISMATCH error. Do NOT continue to other assertions, do NOT take a screenshot of an unverified app, do NOT log to test history.
Source: ~/.claude/rules/mendix-testbed.md (see "Verification helpers" section).
When to invoke
Auto-invoke when:
- A
/mx-handoff completed and Neo replied "done" -- verify the handed-off page renders correctly.
- A
/mx-scaffold-microflow or /mx-entity-add succeeded and the change might affect a page.
- Phase D page-orchestration skills produced a generated page.
- Neo says "test the testbed" / "is the page working" / similar.
Do not invoke for:
- Pages on a non-testbed project. The project allowlist refuses these by default; Playwright is not the right escape hatch.
- Microflow-internal logic (no UI). Use mxcli read-only inspection instead.
What it does
Phase 0 -- prerequisites
- Playwright transport selection. Two paths supported:
- Preferred: Playwright MCP server. Check
claude mcp list shows playwright: ... Connected AND that the mcp__playwright__browser_* tools are surfaced in this session (try ToolSearch query="select:mcp__playwright__browser_navigate"). If both hold, use the MCP tools.
- Fallback: direct Node-driven Playwright when the MCP tools aren't surfaced in-session despite the server being Connected (a real failure mode observed 2026-04-27). Use the reusable driver at
<MendixToolkit>/sandbox/playwright-driver/test-page.mjs -- it loads the cached playwright module from ~/AppData/Local/npm-cache/_npx/<hash>/node_modules/ via a junction. Same fingerprint-guard logic; same artifact paths. If the cache hash drifts after an mxcli/mcp upgrade, re-run the junction-create step.
- Setup gap: if
playwright install chromium-headless-shell was never run, the first attempt fails with "Executable doesn't exist at ...chromium_headless_shell-XXX...". Run the install once (~150MB, persists in ~/AppData/Local/ms-playwright/).
- Testbed runtime up? Verify port 8082 is listening:
Get-NetTCPConnection -LocalPort 8082 -State Listen -ErrorAction SilentlyContinue
If not, ask Neo to start the testbed (the runtime button in Studio Pro). Do not proceed.
- Resolve target URL. If Neo passed a page name like
CustomerDetail, build http://localhost:8082/p/CustomerDetail (Mendix's URL pattern). If Neo passed a path, use it. If nothing, default to http://localhost:8082/. With the corrected ribbon detection (Phase 1 step 3), the root URL works on both anonymous-redirect-to-login AND authenticated pages — the ribbon is body::after and applies globally.
Phase 1 -- testbed fingerprint guard (MANDATORY)
Use Playwright tools in this order. If the MCP path is available, use the browser_* tool names below. If on the direct-driver fallback, the equivalent Playwright API calls run inside test-page.mjs — same checks, same order.
browser_navigate(url=<target>) (MCP) / page.goto(url, {waitUntil:'networkidle'}) (direct)
- Assert URL contains
:8082. If the URL got rewritten (auto-https, redirect to live), ABORT with TESTBED-MISMATCH.
- Assert ribbon via
getComputedStyle — NOT via accessibility tree. The ribbon is a body::after CSS pseudo-element (themesource/onesource_theme/web/onesource-overrides.scss); pseudo-element generated content is excluded from the accessibility tree by browser spec, so getByText('TEST APP') and accessibility.snapshot() will silently miss it on every page including authenticated ones. The right check is:
const ribbon = await page.evaluate(
() => getComputedStyle(document.body, '::after').content
);
if (!ribbon.includes('TEST APP')) ABORT TESTBED-MISMATCH;
This works on both the login page AND authenticated pages — the ribbon is body::after, scoped globally.
- Optionally take an
accessibility.snapshot() AFTER the guard passes, for user assertions in Phase 2.
ABORT means: report the mismatch with the failing details, take NO screenshot (it would create a misleading artifact), do NOT proceed to user-supplied assertions.
The 2026-04-27 "root URL ribbon gap" was a misdiagnosis (corrected 2026-04-28). The ribbon was always on the login page; the AX-tree detection method was the bug. With getComputedStyle, root URL works fine.
Phase 2 -- user assertions (optional)
If Neo passed assertions in $ARGUMENTS (e.g., "verify the customer table shows 5 rows" or "click the Save button and confirm a success toast"), run them:
- Use
browser_snapshot() for accessibility-tree assertions (preferred -- deterministic).
- Use
browser_take_screenshot() only AFTER the fingerprint guard passed.
- Use
browser_click(), browser_fill_form(), browser_press_key() for interactions.
- Use
browser_wait_for() for state changes.
- Avoid
browser_evaluate() unless absolutely necessary (it's powerful but bypasses accessibility-tree determinism).
Phase 3 -- artifact capture
- Take ONE screenshot via
browser_take_screenshot(). Save to ~/.claude/logs/mx-test-page/<timestamp>-<sanitized-page-name>.png (create dir if missing).
- Save the accessibility snapshot to a sibling
.json for diffing future runs.
- Capture
browser_console_messages() -- any console errors get reported alongside the test result.
browser_close() to release the browser context.
Phase 4 -- report
## /mx-test-page: PASS | FAIL | TESTBED-MISMATCH
**Target:** <URL>
**Testbed fingerprint:** <PASS / FAIL detail>
**User assertions:** <count passed / total>
**Console errors:** <count, with first 3>
**Screenshot:** <path>
**A11y snapshot:** <path>
**Failures (if any):**
- <assertion> -- <expected> -- <got>
**Suggested next step:**
- (PASS) merge / commit / move on
- (FAIL) re-run /mx-handoff with corrections, or open Studio Pro to inspect
- (TESTBED-MISMATCH) check the runtime is on 8082 and the ribbon is rendered
Hard rules
- Refuse on non-testbed projects. Run
~/.claude/rules/mendix-testbed.md resolution first; refuse with the standard testbed-refusal block on a miss.
- Refuse on a non-localhost URL. Even if the testbed-port assertion would pass, refuse anything that's not
http://localhost: or http://127.0.0.1:. We never drive Playwright against a remote host from this skill.
- NEVER skip the fingerprint guard. No flag, no edge case lets phase 1 be bypassed. If Neo says "skip the guard, I trust the URL," refuse and explain that this is the rule we built specifically because of the live-vs-test confusion risk.
- NEVER use browser_evaluate to bypass accessibility-tree limits. If the page can't be tested via the snapshot, the page has an accessibility problem -- report that as a finding, don't paper over it with JS evaluation.
- One Playwright session per skill invocation. Open, do work, close. Don't leave browser contexts running.
- Screenshots only after fingerprint pass. A screenshot of the wrong app misleads everyone who reads it later.
Cross-reference
- Testbed allowlist + fingerprints:
~/.claude/rules/mendix-testbed.md
- MCP setup:
~/.claude/lib/Setup-PlaywrightMcp.ps1
- Maia HTTP variant launcher:
~/.claude/lib/Start-PlaywrightHttp.ps1
- Direct-driver fallback:
<MendixToolkit>/sandbox/playwright-driver/test-page.mjs (proven-working 2026-04-27 when MCP tools aren't surfaced in-session)
- Companion skills:
/mx-handoff (typically the immediately-previous step), /mx-doctor (model-side health), /mx-impact (blast-radius read).