Install
openclaw skills install web-ui-testEnvironment-aware web UI testing. Detects wmux/cmux/tmux and routes to the appropriate browser backend: wmux → wmux browser commands (user-visible panel), plain → Playwright MCP. Analyze browser snapshots, click elements, fill forms, and return summarized results. sso-verify - Verify Authentik SSO login flow, confirm redirect targets, detect Blueprint drift [sso-verify.md]. cdp-trace - CDP-based closed shadow DOM cascade diagnosis (DOM.getDocument pierce:true + CSS.getMatchedStylesForNode) [cdp-trace.md]. Use for: "UI check", "browser test", "screen verify", "Playwright test", "UI verification", "verify with playwright", "SSO verification", "SSO test", "login flow verification", "shadow DOM cascade", "::part not working", "CDP trace", "closed shadow DOM diagnosis".
openclaw skills install web-ui-testWeb UI testing skill. Detects the runtime environment and routes to the appropriate browser backend.
The primary purpose of browser diagnosis/verification is "the user sees it on their own screen". Screenshot capture is supporting evidence, not a substitute for visibility.
| # | Don't | Do |
|---|---|---|
| 1 | Launch with chromium.launch({ headless: true }) and only attach a screenshot in chat | chromium.launch({ headless: false, slowMo: 500 }) — let the user follow in real time |
| 2 | "I showed the user a screenshot, so it's fine" | screenshot ≠ visible to the user. If the user says "show me", open a visible browser + slowMo |
| 3 | wmux/cmux/Playwright MCP disconnected → fall back to headless CLI | Even on CLI fallback, force headless: false. On a Windows desktop OS, a chromium GUI is available |
| 4 | "headless is faster and more stable by default" mindset | Speed costs user visibility. If the user says "show me", visibility wins |
| 5 | Playwright MCP disconnected → CLI fallback auto-selects headless | CLI fallback is also headless: false. headless is only for explicit non-interactive cases (e.g., CI assertion) |
headless: falseheadless: falseDuring a closed shadow DOM ak-library cascade investigation, used a npx playwright Bash invocation + chromium.launch({ headless: true }) and only attached a screenshot in chat. The user requested "show it via web-ui-test" and no visible browser was provided. The user reacted angrily that the Chromium UI never appeared.
Check environment variables to determine the browser backend:
echo $WMUX
echo $CMUX_SESSION
| Environment | Detect | Do (use this) | Don't (forbidden) |
|---|---|---|---|
| wmux | $WMUX is set (e.g. 1) | wmux browser open/snapshot/click/type commands via Bash | Playwright MCP — user cannot see the invisible Playwright window |
| cmux | $CMUX_SESSION is set | cmux browser panel commands | Playwright MCP — same reason |
| Plain / tmux | Neither $WMUX nor $CMUX_SESSION set | Playwright MCP (Step 1 below) | — |
When $WMUX is set, use these instead of Playwright MCP.
Invocation form: the rest of this document uses the bare wmux browser … form, which is what runs when wmux is on PATH (the common case). If wmux is not on PATH in the current environment, substitute node "$WMUX_CLI" for wmux in every command below — $WMUX_CLI points to the same entry point. The two forms are interchangeable; pick whichever resolves on the current shell and use it consistently.
wmux browser open <url> # navigate (= playwright navigate)
wmux browser snapshot # get accessibility tree with @eN refs
wmux browser click @eN # click element
wmux browser type @eN <text> # type into element
wmux browser fill @eN <value> # set input value
wmux browser get-text # get page text
wmux browser screenshot # capture screenshot
wmux browser eval <js> # run JavaScript
wmux browser back # go back
wmux browser forward # go forward
wmux browser reload # reload page
Workflow: browser open <url> → browser snapshot → read tree → browser click/type @eN → browser snapshot again.
Refs (@e1, @e2...) expire after page changes — always re-snapshot.
| Action | wmux (Do) | Playwright MCP (Don't in wmux) |
|---|---|---|
| Navigate | Bash("wmux browser open <url>") | mcp__playwright__browser_navigate |
| Snapshot | Bash("wmux browser snapshot") | mcp__playwright__browser_snapshot |
| Click | Bash("wmux browser click @eN") | mcp__playwright__browser_click |
| Type | Bash("wmux browser type @eN text") | mcp__playwright__browser_type |
| Screenshot | Bash("wmux browser screenshot") | mcp__playwright__browser_take_screenshot |
| Evaluate JS | Bash("wmux browser eval <js>") | mcp__playwright__browser_evaluate |
| Wait for text | Re-snapshot + check | mcp__playwright__browser_wait_for |
Key difference: wmux browser is visible to the user in real-time on the right panel. Playwright opens an invisible window the user cannot see.
This procedure must be run before all tasks — only when Step 0 determined Playwright backend.
Check whether the plugin is already installed first — if mcp__playwright__* tools already exist, code-mode registration is unnecessary:
ToolSearch("select:mcp__playwright__browser_navigate")
If the result returns the mcp__playwright__browser_navigate schema → plugin is installed. Skip Step 2, go to Step 3 (use the mcp__playwright__* tools directly).
If not returned → try registration via code-mode:
mcp__code-mode__list_tools()
If the result contains playwright → go to Step 3 (via code-mode call_tool_chain)
Otherwise → run Step 2
mcp__code-mode__register_manual({
manual_call_template: {
name: "playwright",
call_template_type: "mcp",
config: {
mcpServers: {
"playwright": {
transport: "stdio",
command: "npx",
args: ["@playwright/mcp@latest"]
}
}
}
}
})
After registration, verify that playwright tools appear via mcp__code-mode__list_tools().
Do not fall back to alternatives. Diagnose the problem, fix it, then retry:
Diagnostic steps:
# 1. Check if npx is available
npx --version
# 2. Check package accessibility
npx @playwright/mcp@latest --version 2>&1 | head -5
# 3. Check for network issues
npm ping 2>&1
Common failure causes and fixes:
| Error | Cause | Fix |
|---|---|---|
transport undefined | Missing config | Add "transport": "stdio" |
NODE_MODULE_VERSION mismatch | Node version conflict | Run npx clear-npx-cache then retry |
command not found: npx | Node not installed | Check npx path, use absolute path |
| Package download failure | Network/registry issue | Check npm registry connectivity |
EACCES permission error | Permission issue | Check cache directory permissions |
| "Another program is using the profile" / Chrome exits immediately | Previous Playwright Chrome occupying mcp-chrome profile | Run Chrome Profile Lock Recovery procedure below |
If Chrome only shows about:blank or exits immediately when launching Playwright:
# 1. Kill existing mcp-chrome process (via command-line match)
powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" | Where-Object { $_.CommandLine -like '*mcp-chrome*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
# 2. Delete profile lock files
cmd /c "del /F /Q \"%LOCALAPPDATA%\\ms-playwright\\mcp-chrome\\SingletonLock\" 2>nul"
cmd /c "del /F /Q \"%LOCALAPPDATA%\\ms-playwright\\mcp-chrome\\SingletonCookie\" 2>nul"
cmd /c "del /F /Q \"%LOCALAPPDATA%\\ms-playwright\\mcp-chrome\\SingletonSocket\" 2>nul"
# 3. If still failing, delete entire profile directory
cmd /c "rmdir /S /Q \"%LOCALAPPDATA%\\ms-playwright\\mcp-chrome\""
# 4. If all 3 steps fail → close any open about:blank windows manually and retry
Auto-detection: If you see browserType.launchPersistentContext: Failed to launch error + process did exit: exitCode=0 pattern, this is the issue.
If recovery fails, register Playwright with a temporary profile instead of mcp-chrome:
mcp__code-mode__register_manual({
manual_call_template: {
name: "playwright",
call_template_type: "mcp",
config: {
mcpServers: {
"playwright": {
transport: "stdio",
command: "npx",
args: ["@playwright/mcp@latest", "--user-data-dir", "%LOCALAPPDATA%/ms-playwright/mcp-chrome-" + Date.now()]
}
}
}
}
})
Timestamp-based profile → no lock conflicts. Note: cookies/session are reset each time.
Diagnose → fix → re-register. Always resolve before proceeding.
The registered Playwright is called via mcp__code-mode__call_tool_chain:
// Navigate to page
mcp__code-mode__call_tool_chain({
code: `
const result = await playwright.playwright_browser_navigate({ url: 'http://...' });
return result;
`
})
// Snapshot (primary use)
mcp__code-mode__call_tool_chain({
code: `
const snapshot = await playwright.playwright_browser_snapshot();
return snapshot;
`
})
// Screenshot
mcp__code-mode__call_tool_chain({
code: `
const screenshot = await playwright.playwright_browser_take_screenshot();
return screenshot;
`
})
// Click
mcp__code-mode__call_tool_chain({
code: `
const result = await playwright.playwright_browser_click({ ref: 'e123' });
return result;
`
})
// Form input
mcp__code-mode__call_tool_chain({
code: `
const result = await playwright.playwright_browser_type({ ref: 'e456', text: 'input text' });
return result;
`
})
// Wait
mcp__code-mode__call_tool_chain({
code: `
const result = await playwright.playwright_browser_wait_for({ text: 'expected text' });
return result;
`
})
// Console messages
mcp__code-mode__call_tool_chain({
code: `
const logs = await playwright.playwright_browser_console_messages();
return logs;
`
})
For user-authentication/permission operations that cannot be automated via API/SDK (issuing a GitHub PAT, granting OAuth permission, setting up 2FA, etc.), do not stop at "please go to the GitHub UI yourself". Apply the semi-automated pattern: open the page in Playwright + user interaction + collect the result.
mcp__playwright__browser_navigate)gh secret set)| Task | API | Playwright fallback |
|---|---|---|
| Issue a GitHub PAT | ❌ Not possible (security) | https://github.com/settings/personal-access-tokens/new |
| Create an OAuth App | ❌ Not possible | https://github.com/settings/developers |
| Register a GitHub Secret | ✅ gh secret set | (not needed) |
| User sign-in for an IdP (browser session required) | ❌ Auth flow | navigate the IdP page |
| Create an environment on the automation server | ✅ API call | (not needed) |
A workflow's PAT lacked permissions → the response only said "please issue a fine-grained PAT from the GitHub UI" and stopped. The user pointed out: "always open Playwright and request login." The Playwright fallback pattern had not been encoded as a rule, leading to repeated "guidance-only" responses.
All verification must go through Playwright UI interaction. The purpose of this skill is to test what users actually see and do in the browser.
curl, fetch, httpie or any direct API call is prohibited for verificationbrowser_click + browser_type$WMUX / $CMUX_SESSION / $TMUX (Step 0 above)wmux browser snapshot / plain: call_tool_chain)CRITICAL: Never return raw snapshot data. Always summarize findings.
Verified URL must be shared: print the tested URL as text so the user can open it in their own browser. Example: "Open directly: http://{host}:{port}/{path}"
## UI Verification Result ✅
**Page:** [page title/URL]
**Status:** OK
### Verified URLs (open directly)
- [URL 1]
- [URL 2]
### Confirmed Findings
- [key finding 1]
- [key finding 2]
### Actions Taken
- [action taken, if any]
## UI Verification Result ❌
**Page:** [page title/URL]
**Issue Found**
### Error Details
- [error 1]
- [error 2]
### Console Errors
[relevant console errors only]
### Recommended Action
- [fix suggestion]
When analyzing snapshots:
❌ Bad (too long):
- generic [ref=e1]: ...
- button [ref=e2]: ...
(hundreds of lines)
✅ Good (concise):
Page: Claude Sessions (localhost:5173)
Status: Loaded successfully
Key Elements:
- Project list (10 projects)
- Session viewer (35 messages)
- Tabs: Messages (selected), Agents, Todos
Issues found: None
1. snapshot → Find element ref
2. browser_click using ref
3. Wait for state change
4. snapshot again → Verify result
5. Report summary
1. snapshot → Identify form fields
2. browser_type each field
3. Submit if requested
4. Report result
1. browser_navigate to URL
2. Wait for load (browser_wait_for)
3. snapshot
4. Report page state
If the snapshot result is too large:
mcp__code-mode__call_tool_chain({
code: `
const result = await playwright.playwright_browser_evaluate({
code: "document.querySelectorAll('button').length"
});
return result;
`
})
mcp__code-mode__call_tool_chain({
code: `
const shot = await playwright.playwright_browser_take_screenshot({ element: 'specific area' });
return shot;
`
})
If element not found:
If action fails: