| name | verify-ui |
| context | fork |
| description | Frontend testing and web UI verification via DOM inspection in a Playwright browser. Verify component behavior, check layout, and test interactive elements after implementing or modifying features. Checks visibility, clickability, alignment, overflow, and state transitions — without screenshots. Use when verifying UI, investigating bugs, or testing acceptance criteria against a live app. Use when user reports a bug or asks to investigate how a feature works. Use BEFORE custom Playwright usage.
|
| version | 1.0.0 |
Verify UI
Follow this process for every verification. Use the bundled scripts — do not write
custom page.evaluate code unless the scripts cannot answer the question.
Step 1: Understand what to verify
Read the minimum source code to identify:
- How to reach the feature (route, buttons to click)
- Key selectors (CSS classes, roles, test-ids)
- Expected layout rules from CSS (flex direction, gaps, alignment)
Stop reading after these three. Do NOT explore the full component tree.
- NEVER use ref for snapshots. Target by css class, role, or whatever else
here what happens when you try to get page snapshot by ref:
⏺ playwright - Page snapshot (MCP)(target: "[ref=e31]", depth: 6)
⎿ Error: ### Error
Error: "[ref=e31]" does not match any elements.
it happens all the time, please do not waste tokens on that
Step 2: Navigate to the feature
- Use page.locator('selector').click() for all interactions
- Never take page snapshots
- Never use ref targets (see Step 1)
Step 3: Load and run verification scripts
For EVERY measurement, follow this exact sequence:
After-action state checks (THIS IS THE MOST COMMON CHECK YOU SHOULD USE IN MOST CASES)
This is a 3-stage check: setup observer, perform click with Playwright, collect results.
- Read:
Read .claude/skills/verify-ui/scripts/check-after-action.js
- Inject into page:
page.evaluate(scriptContent)
- Setup:
page.evaluate((opts) => setupAfterAction(opts), { scope: '[role="dialog"]', expect: [{ selector: '.success-message', visible: true }, { selector: '.loading-spinner', visible: false }], timeout: 5000 })
- Click with Playwright:
page.locator('.submit-button').click()
- Collect:
page.evaluate(() => collectAfterAction())
- Check overall
pass and each result's reason
Use this for: selection state changes, popup open/close, tab switching,
form submission results — any click that should change the UI permanently.
Use checkVisibilityTransition instead when tracking temporary elements
(spinners, toasts, animations).
Alignment checks
- Read the script:
Read .claude/skills/verify-ui/scripts/check-alignment.js
- Inject into page:
page.evaluate(scriptContent) where scriptContent is the
full file you just read
- Call:
page.evaluate(() => checkAlignment({ containerSelector: '...', scopeSelector: '...' }))
- Interpret the result: check
behavior, alignment, gaps.uniform, gaps.value
Clickability checks
- Read:
Read .claude/skills/verify-ui/scripts/check-clickable.js
- Inject into page
- Call:
page.evaluate(() => checkClickable({ selectors: [...], scope: '...' }))
- Check each result's
clickable and reason
Visibility transition checks
This is a 3-stage check: setup observer, perform click with Playwright, collect results.
- Read:
Read .claude/skills/verify-ui/scripts/check-visibility-transition.js
- Inject into page:
page.evaluate(scriptContent)
- Setup:
page.evaluate((opts) => setupVisibilityTransition(opts), { target: '.spinner', scope: '[role="dialog"]', expected: [false, true, false], timeout: 5000 })
- Click with Playwright:
page.locator('.submit-button').click()
- Collect:
page.evaluate(() => collectVisibilityTransition())
- Check
matches and reason
Container inventory checks
Use this after opening a popover, dialog, or dropdown to discover everything inside in one call
instead of writing custom discovery code each time.
- Read:
Read .claude/skills/verify-ui/scripts/check-inventory.js
- Inject into page
- Call:
page.evaluate(() => checkInventory({ scope: '[data-example]', // e.g.: [data-popover-content], [data-radix-popper-content-wrapper], [role="dialog"] include: ['button', '[role="option"]', '[role="radio"]', '[role="group"]'] }))
- Inspect
summary for counts, sections for labeled groups, scrollState for scroll info,
and elements for the full list with text/role/ariaState/visible/inViewport per element.
Options:
scope (required): CSS selector for the container to inventory
include: array of selectors to search for (defaults to all common interactive roles)
exclude: array of selectors to skip
maxDepth: limit DOM traversal depth
Overflow and reachability checks
Use this to check whether a container or its children overflow the viewport,
whether footer content is clipped, and whether interactive elements are still reachable.
- Read:
Read .claude/skills/verify-ui/scripts/check-overflow.js
- Inject into page
- Call:
page.evaluate(() => checkOverflow({ scope: '[data-side]', checkFooter: true }))
- Check
summary for the quick verdict (overflows, scrollable, footerClipped,
unreachableCount), overflow/overflowPx for directional detail, footer for
last-child clipping, and unreachable for any interactive elements that can't be clicked.
Options:
scope (required): CSS selector for the container to check
checkFooter: check if the last visible child is clipped (default: true)
checkInteractive: hit-test all interactive elements for reachability (default: true)
margin: pixel tolerance for overflow detection (default: 0)
How to inject scripts
Scripts live on disk, NOT accessible from the browser. To inject:
- Use the Read tool:
Read .claude/skills/verify-ui/scripts/check-alignment.js
- The file content is now in YOUR context (not the browser's)
- Inject via:
page.evaluate(fileContent) — this defines the function in browser scope
- Then call:
page.evaluate(() => checkAlignment({...}))
NEVER use fetch() or import() to load scripts — the browser cannot access local files
Step 4: Compile results
Populate all check in a md format table, with the columns "Elements", "Check", "Result", "Details", "Suggested fix" for instance:
| Elements | Check | Result | Details | Suggested fix |
|----------|-------|--------|---------|---------------|
| .search-input | visible | ✅ | Visible at the very top on the search form | N/A |
| .search-input | clickable | ✅ | elementFromPoint returns self | N/A |
| .dropdown | appears on focus | ❌ | MutationObserver: element never became visible within 3s | Add visibility state change on search input focus |
Important Notes Towards Details And Summaries
Instead of providing numeric information, explain what is going on semantically. Imagine that user does not see the skill at all and you have to explain to him being in complete blindness
General rules
- For clicking elements, ALWAYS use Playwright's native click via page.locator('selector').click() — never use dispatchEvent or element.click() from page.evaluate. Components often require real pointer events.
- For selector, prioritize text which element contains or accessibility attributes (e.g. aria-role)
- To make selection more precise, instead of selecting element on the whole page, first extract feature box element from page (use text feature contains, classes, test-ids, consider mixing those approaches if you struggle to select single item, example: .locator('[role="form"]:has-text("Register Account")')), then use locator from feature element instead of page
- For tab switching, radio selection, checkbox filling, dropdown element picking and all other user interactions use native clicks: .locator(<selector>).click()
- use .fill() for text input and .press() or page.keyboard.press() for key presses, do not edit DOM value field directly
- avoid gathering getComputedStyle information other than about visibility/opacity: that is redundant, getBoundingClientRect provides full data about shape and positioning
Custom scripts
Use custom skill ONLY when you tried to utilize scripts provided above and you still lack some information/low on confidence
Navigation efficiency
- After reading source code, you already know the selectors. Do NOT take page snapshots
just to "see" the page — go directly to clicking and measuring.
- Only use page snapshot when you genuinely don't know what's on the page
(e.g., investigating an unknown bug with no code context).
- When you need to inspect a specific area of the page, use page.locator('css-selector').evaluate()
- NEVER use ref targets for Playwright, neither for snapshots nor for clicks
browser_snapshot rules
- The
target parameter MUST be a CSS selector or role query, NEVER a [ref=...] value
- you MUST map snapshot which contains ref values to source code, to transform ref to css class, text or whatever else
- If you need a subtree snapshot, use a CSS selector like '[role="listbox"]' or '.className'
- If you don't know the CSS selector, DON'T take a targeted snapshot — use page.evaluate with getBoundingClientRect or DOM queries instead
What skill does not do
- Skill does not verify visual appearance (colors, fonts, spacing and contrast aesthetics), it verifies only if the feature elements behave as it planned by spec and codebase
- Skill does not test cross-browser behavior and uses only Playwright Chromium browser
- Skill does not verify accessibility features use different skill for that
- Skill avoids using screenshots and fallbacks to them only in case of low confidence after usage of tools