Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .claude/skills/ui-browser/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
name: UI Browser
description: >
This skill should be used when the user asks to "verify UI changes",
"check the UI", "take a screenshot", "browse the app", "inspect a page",
"test how it looks", "visually verify", "screenshot for PR", or
"update documentation screenshots". Also use after editing any Vue component,
Vuetify template, CSS, or layout code to confirm the result visually.
version: 0.1.0
---

# UI Browser

Browse, inspect, and screenshot the running zwave-js-ui dev server using Playwright MCP tools. Verify UI changes visually after editing components, debug layout issues interactively, and capture screenshots for PRs and documentation.

## Prerequisites

Ensure the dev servers are running before browsing:
- Frontend: `npm run dev` (port 8092)
- Backend: `npm run dev:server` (port 8091)
- Fake stick: `npm run fake-stick` (port 5555)

The Playwright MCP server is configured in `.mcp.json` at the project root.

## App Navigation

The app uses Vue Router with hash history. All URLs follow this pattern:
```
http://localhost:8092/#/<route>
```

### Route Quick Reference

| Route | Page |
|-------|------|
| `/control-panel` | Control Panel (main page, node list + details) |
| `/settings` | Settings (config, gateway values, Home Assistant) |
| `/scenes` | Scenes |
| `/debug` | Debug (log viewer) |
| `/store` | Store (MQTT/gateway explorer) |
| `/mesh` | Network Graph (mesh visualization) |
| `/smart-start` | Smart Start |
| `/controller-chart` | Controller Chart |
| `/zniffer` | Zniffer (traffic sniffer) |
| `/configuration-templates` | Configuration Templates |

### Layout Structure

- **Navigation drawer** (left sidebar): `v-navigation-drawer` — toggle via hamburger icon
- **App bar** (top): `v-app-bar` — nav toggle, page title, action buttons
- **Main content**: below app bar, fills remaining viewport

### Auth Handling

Auth is controlled by `store/settings.json` → `gateway.authEnabled`:
- **When `false` or unset**: no login needed, `/` redirects to `/control-panel`
- **When `true`**: login required. Fill `input[name="username"]` with `admin`, `input[name="password"]` with `zwave`, click `button[type="submit"]`

## Interactive Browsing with Playwright MCP

### Navigate to a Page

Use the browser navigate tool with `http://localhost:8092/#/<route>`. Wait 2-3 seconds after navigation for Vuetify components to fully render.

### Inspect Page Content

Take a **snapshot** (accessibility tree) to see page structure, text, and interactive elements without a visual screenshot. Use this to find selectors, verify text content, or debug missing elements.

### Capture Screenshots

Take a **screenshot** to capture the visual state. Save to `/tmp/screenshots/` for ad-hoc use or `docs/_images/` for documentation updates.

### Interact with Elements

- **Click** buttons, tabs, list items, navigation links
- **Fill** text inputs and form fields
- **Select** dropdown options
- **Hover** to trigger tooltips or dropdown menus

## Verification Workflows

### After Editing a Vue Component

1. Navigate to the page containing the component
2. Take a screenshot to verify the visual result
3. If something looks off, take a snapshot to inspect the DOM structure

### After Changing Styles or Layout

1. Navigate to the affected page
2. Screenshot at default viewport (1280x720)
3. Optionally resize the viewport and screenshot again to test responsive behavior

### After Modifying a Dialog or Modal

1. Navigate to the page
2. Click the button or action that opens the dialog
3. Screenshot the dialog in its open state

### Verifying Dark Mode

The app reads `localStorage.dark`. Set it via the browser console or toggle the theme switch in the UI. The batch screenshot script sets dark mode by default.

## Batch Screenshots

For capturing multiple pages at once, run the template script:

```bash
node .claude/skills/ui-browser/screenshot-template.js <url> <output-path> [width] [height]
```

Default viewport is 1280x720. The script auto-handles auth and sets dark mode.

```bash
node .claude/skills/ui-browser/screenshot-template.js "http://localhost:8092/#/settings" /tmp/screenshots/settings.png
node .claude/skills/ui-browser/screenshot-template.js "http://localhost:8092/#/mesh" /tmp/screenshots/mesh.png 1920 1080
```

For the full route-to-documentation image mapping and troubleshooting tips, see `references/screenshots.md`.
38 changes: 38 additions & 0 deletions .claude/skills/ui-browser/references/screenshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Screenshot Reference

## Output Locations

- **Ad-hoc screenshots:** `/tmp/screenshots/`
- **Documentation updates:** `docs/_images/` in the project root

## Route-to-Documentation Mapping

These files in `docs/_images/` correspond to specific routes and states:

| File | Route | Notes |
|------|-------|-------|
| `control_panel_dark.png` | `/#/control-panel` | Dark mode |
| `settings.png` | `/#/settings` | Light mode override |
| `settings_dark.png` | `/#/settings` | Dark mode |
| `debug.png` | `/#/debug` | Needs backend |
| `mesh_diagram.png` | `/#/mesh` | Needs backend + fake-stick |
| `mesh-selected.png` | `/#/mesh` | With a node selected |
| `scenes.png` | `/#/scenes` | |
| `nodes_manager.png` | `/#/control-panel` | Nodes table view |
| `unknown-device.png` | `/#/control-panel` | Specific unknown node |
| `hass_devices.png` | `/#/settings` | Home Assistant tab |
| `gateway_values_table.png` | `/#/settings` | Gateway values section |

## Documentation Update Workflow

1. Take screenshots for each route in the mapping table above
2. Save directly to `docs/_images/` overwriting existing files
3. Stage and commit: `git add docs/_images/ && git commit -m "docs: update UI screenshots"`

## Troubleshooting

- **Login screen keeps appearing**: Check `store/settings.json` for `gateway.authEnabled`. If true, handle login first via Playwright MCP or let the batch script handle it automatically.
- **Blank page**: Ensure all three dev servers are running (frontend, backend, fake-stick). Take a snapshot to check for error messages in the DOM.
- **Components not rendered**: Vuetify needs 2-3s after `networkidle` to mount. Increase wait time before screenshotting.
- **Stale content**: The app uses Socket.IO for real-time updates. If data looks stale, the backend may not be connected to the fake stick.
- **Navigation drawer missing**: It may be collapsed. Click the hamburger icon (`v-app-bar-nav-icon`) in the top-left to toggle it open.
109 changes: 109 additions & 0 deletions .claude/skills/ui-browser/screenshot-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node

/**
* Screenshot template for zwave-js-ui
*
* Usage: node screenshot-template.js <url> <output-path> [width] [height]
*
* Examples:
* node screenshot-template.js "http://localhost:8092/#/settings" /tmp/screenshots/settings.png
* node screenshot-template.js "http://localhost:8092/#/mesh" /tmp/screenshots/mesh.png 1920 1080
*/

const { chromium } = require('playwright')
const path = require('path')
const fs = require('fs')

const url = process.argv[2]
const outputPath = process.argv[3]
const viewportWidth = parseInt(process.argv[4]) || 1280
const viewportHeight = parseInt(process.argv[5]) || 720

if (!url || !outputPath) {
console.error(
'Usage: node screenshot-template.js <url> <output-path> [width] [height]',
)
process.exit(1)
}

// Check if auth is enabled by reading store/settings.json
function isAuthEnabled() {
const cwdSettingsPath = path.resolve(process.cwd(), 'store/settings.json')
const scriptSettingsPath = path.resolve(
__dirname,
'../../../store/settings.json',
)

for (const p of [cwdSettingsPath, scriptSettingsPath]) {
try {
const settings = JSON.parse(fs.readFileSync(p, 'utf8'))
return settings?.gateway?.authEnabled === true
} catch {
// continue to next path
}
}
return false
}

async function takeScreenshot() {
const authEnabled = isAuthEnabled()
const outputDir = path.dirname(outputPath)
fs.mkdirSync(outputDir, { recursive: true })

const browser = await chromium.launch({ headless: true })
const context = await browser.newContext({
viewport: { width: viewportWidth, height: viewportHeight },
})

// Set dark mode in localStorage before navigation
await context.addInitScript(() => {
localStorage.setItem('dark', 'true')
})

const page = await context.newPage()

if (authEnabled) {
// Navigate to the app root first — router will redirect to login
await page.goto('http://localhost:8092/', {
waitUntil: 'networkidle',
})
await page.waitForTimeout(1000)

// Fill login form
try {
await page.fill(
'input[type="text"], input[name="username"]',
'admin',
{ timeout: 5000 },
)
await page.fill('input[type="password"]', 'zwave', {
timeout: 5000,
})
await page.click('button[type="submit"]', { timeout: 5000 })
await page.waitForTimeout(2000)
} catch (e) {
console.warn(
'Login form not found or login failed, continuing...',
e.message,
)
}
}

// Navigate to the target URL
await page.goto(url, { waitUntil: 'networkidle' })

// Wait for Vuetify rendering
await page.waitForTimeout(2000)

await page.screenshot({ path: outputPath, fullPage: false })
console.log(
`Screenshot saved to ${outputPath} (${viewportWidth}x${viewportHeight})`,
)

await browser.close()
}

takeScreenshot().catch((err) => {
console.error('Screenshot failed:', err)
process.exit(1)
})
13 changes: 13 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--headless",
"--viewport-size",
"1280,720"
]
}
}
}
Loading