Web Ui Test

Dev Tools

Environment-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".

Install

openclaw skills install web-ui-test

Web UI Tester

Web UI testing skill. Detects the runtime environment and routes to the appropriate browser backend.

CRITICAL — user visibility is the top priority (HARD STOP)

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'tDo
1Launch with chromium.launch({ headless: true }) and only attach a screenshot in chatchromium.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
3wmux/cmux/Playwright MCP disconnected → fall back to headless CLIEven 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" mindsetSpeed costs user visibility. If the user says "show me", visibility wins
5Playwright MCP disconnected → CLI fallback auto-selects headlessCLI fallback is also headless: false. headless is only for explicit non-interactive cases (e.g., CI assertion)

Self-check (every time before launching Playwright/chromium)

  1. Did the user use a visibility request keyword such as "show me", "open it", "web-ui-test", or "browser test"? → If yes, force headless: false
  2. Is this work interactive verification or diagnosis for the user? → If yes, headless: false
  3. headless is justified only when (a) CI assertion (b) the user explicitly said "in headless" (c) Playwright MCP is used (the UI shows itself)
  4. screenshot is supporting evidence — it can be attached to a chat report, but it does not replace user visibility

Violation case (2026-05-28, 1st)

During 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.


Step 0: Environment Detection (MANDATORY — before any browser action)

Check environment variables to determine the browser backend:

echo $WMUX
echo $CMUX_SESSION

Do & Don't — Browser Backend Selection

EnvironmentDetectDo (use this)Don't (forbidden)
wmux$WMUX is set (e.g. 1)wmux browser open/snapshot/click/type commands via BashPlaywright MCP — user cannot see the invisible Playwright window
cmux$CMUX_SESSION is setcmux browser panel commandsPlaywright MCP — same reason
Plain / tmuxNeither $WMUX nor $CMUX_SESSION setPlaywright MCP (Step 1 below)

wmux Browser Commands Reference

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 @eNbrowser snapshot again.

Refs (@e1, @e2...) expire after page changes — always re-snapshot.

Do & Don't — wmux vs Playwright Mapping

Actionwmux (Do)Playwright MCP (Don't in wmux)
NavigateBash("wmux browser open <url>")mcp__playwright__browser_navigate
SnapshotBash("wmux browser snapshot")mcp__playwright__browser_snapshot
ClickBash("wmux browser click @eN")mcp__playwright__browser_click
TypeBash("wmux browser type @eN text")mcp__playwright__browser_type
ScreenshotBash("wmux browser screenshot")mcp__playwright__browser_take_screenshot
Evaluate JSBash("wmux browser eval <js>")mcp__playwright__browser_evaluate
Wait for textRe-snapshot + checkmcp__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.


Required Setup: Playwright Registration (skip if wmux/cmux detected)

This procedure must be run before all tasks — only when Step 0 determined Playwright backend.

Step 1: Check Registration

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 playwrightgo to Step 3 (via code-mode call_tool_chain) Otherwise → run Step 2

Step 2: Register Playwright

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().

Registration Failure - Must diagnose and resolve the root cause

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:

ErrorCauseFix
transport undefinedMissing configAdd "transport": "stdio"
NODE_MODULE_VERSION mismatchNode version conflictRun npx clear-npx-cache then retry
command not found: npxNode not installedCheck npx path, use absolute path
Package download failureNetwork/registry issueCheck npm registry connectivity
EACCES permission errorPermission issueCheck cache directory permissions
"Another program is using the profile" / Chrome exits immediatelyPrevious Playwright Chrome occupying mcp-chrome profileRun Chrome Profile Lock Recovery procedure below

Chrome Profile Lock Recovery (Windows)

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.

Fallback: Launch with a new profile path

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.

Step 3: Using Playwright Tools

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;
  `
})

Auth-Required Operations: Playwright Fallback (HARD STOP)

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.

Procedure (automation priority)

  1. Try API/SDK (1st): fully automate via API/SDK when possible
  2. Confirm API impossibility: verify with one of
    • the official docs show no such endpoint
    • the API call is blocked for security reasons (e.g., PAT issuance is UI-only)
    • the SDK does not expose the auth flow
  3. Playwright fallback (2nd):
    • Navigate the target page URL with Playwright (mcp__playwright__browser_navigate)
    • Inspect current state via page snapshot
    • Notify the user: "I opened {URL}. Please sign in and complete {required step}, then let me know." + provide the direct URL
    • Wait for the user's completion signal (AskUserQuestion or user message)
    • Receive the result (generated token / ID / etc.) from the user and continue with follow-up automation (e.g., gh secret set)
  4. Forbidden: ending with only an instruction like "Please go to https://github.com/settings/... and issue it yourself."

Scenarios (examples)

TaskAPIPlaywright fallback
Issue a GitHub PAT❌ Not possible (security)https://github.com/settings/personal-access-tokens/new
Create an OAuth App❌ Not possiblehttps://github.com/settings/developers
Register a GitHub Secretgh secret set(not needed)
User sign-in for an IdP (browser session required)❌ Auth flownavigate the IdP page
Create an environment on the automation server✅ API call(not needed)

Violation case (2026-05-04)

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.

Forbidden: API Direct Calls

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 verification
  • If a test requires form submission, use Playwright browser_click + browser_type
  • If a test requires data deletion, do it through the UI (click delete button, confirm dialog)
  • API calls bypass UI bugs (disabled buttons, missing dialogs, broken event handlers)

Core Responsibilities

1. Page State Analysis

  • Take browser snapshots to understand current UI
  • Check for errors in console messages
  • Identify key interactive elements

2. Interaction Testing

  • Click buttons, links, and other elements
  • Fill forms and submit data
  • Navigate between pages
  • Wait for dynamic content

3. Error Detection

  • Check console for JavaScript errors
  • Identify missing elements or broken UI
  • Verify expected content is present

Workflow

  1. Detect Environment - Check $WMUX / $CMUX_SESSION / $TMUX (Step 0 above)
  2. Setup Backend - wmux: ready immediately / plain: Register Playwright via UTCP
  3. Snapshot - Acquire snapshot (wmux: wmux browser snapshot / plain: call_tool_chain)
  4. Analyze - Identify relevant elements and state from snapshot
  5. Execute - Perform requested interactions using the detected backend
  6. Verify - Confirm results and detect issues
  7. Report - Return concise summary (raw snapshot data prohibited)

Output Format

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}"

Success Response

## 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]

Error Response

## 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]

Snapshot Analysis Rules

When analyzing snapshots:

  1. Summarize structure - "Main panel shows 35 messages with tabs for Messages/Agents/Todos"
  2. Report key elements - List important buttons, forms, or content areas
  3. Identify issues - Note missing elements, unexpected text like "No messages", error states
  4. Skip irrelevant details - Don't list every element, focus on what matters for the task

Example Summary

❌ 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

Interaction Patterns

Click Element

1. snapshot → Find element ref
2. browser_click using ref
3. Wait for state change
4. snapshot again → Verify result
5. Report summary

Fill Form

1. snapshot → Identify form fields
2. browser_type each field
3. Submit if requested
4. Report result

Navigate

1. browser_navigate to URL
2. Wait for load (browser_wait_for)
3. snapshot
4. Report page state

Large Page Handling

If the snapshot result is too large:

1. Query specific elements via call_tool_chain

mcp__code-mode__call_tool_chain({
  code: `
    const result = await playwright.playwright_browser_evaluate({
      code: "document.querySelectorAll('button').length"
    });
    return result;
  `
})

2. Screenshot a specific area

mcp__code-mode__call_tool_chain({
  code: `
    const shot = await playwright.playwright_browser_take_screenshot({ element: 'specific area' });
    return shot;
  `
})

Error Handling

If element not found:

  • Check if page is still loading
  • Try browser_wait_for
  • Report specific missing element

If action fails:

  • Check console for errors
  • Take screenshot for debugging
  • Report failure with context

Language Guidelines

  • Respond primarily in English
  • Keep technical terms (URLs, element names) in English
  • Use emojis for status: ✅ Success, ❌ Error, ⚠️ Warning, 🔄 In progress