---
name: verify-ui
description: UI verification workflow — open modals, query DOM contract selectors, interact with inputs, take screenshots, and validate UI rendering via the operability facade.
triggers:
    - verify ui
    - check ui
    - test modal
    - does it render
    - publication center
    - onboarding wizard
    - quartz hub
    - settings tab
argument-hint: '<modal-name>'
---

# Verify UI Skill

## Purpose

After modifying view code (PublicationCenter, OnboardingWizard, QuartzHub, DiffModal, StatusBar, settings), verify the UI renders correctly by querying DOM contract selectors, interacting with inputs, and taking screenshots.

## Prerequisites

- Obsidian must be running with the test vault open.
- Plugin must be built with `npm run build:dev` (copies to test vault, enables facade via `__DEV__` flag).
- Facade must be available (`obsidian eval code="typeof window.__QS__" 2>/dev/null` returns `=> object`).
- Console capture requires `obsidian dev:debug on 2>/dev/null` (once per session).

## When to Activate

Activate when:
- Changes were made to files in `src/views/`
- CSS changes in `styles.css` that affect plugin UI
- TreeRenderer, TreeState, or PublicationTree changes
- Settings tab or declarative settings changes
- Quartz Hub changes

## Important: Interaction Patterns

**Suppress CLI noise.** Always append `2>/dev/null` to every `obsidian` CLI command.

**Async eval pattern.** Use IIFE + `console.log` for async calls:
```bash
obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'pub.open'});console.log(JSON.stringify(r))})()" 2>/dev/null
```

**Missing eval output is not failure.** `obsidian eval` only captures what is logged within ~5–15 ms. Modal-opening actions frequently outlast that — `pub.open` prints `{"success":true}` on a warm status and prints nothing on a cold tree. The DOM query that follows is the real check, which is why every workflow below pairs an action with a `dev:dom` assertion.

**Setting input values.** DOM `.value` assignment does NOT trigger event listeners. Always dispatch an input event:
```bash
obsidian eval code="const el=document.querySelector('[data-qs=\"hub-setup-clone-url\"]');el.value='https://example.com/repo.git';el.dispatchEvent(new Event('input',{bubbles:true}))" 2>/dev/null
```

**Wait after operations.** Approximate wait times:
- Modal open: 2 seconds
- Button click with async effect: 3-5 seconds
- Build/install commands: 10-60 seconds
- Plugin reload: 3 seconds

**Verify actions took effect.** After triggering an action, always query the DOM to confirm:
```bash
obsidian eval code="document.querySelector('[data-qs=\"hub-action\"][data-qs-value=\"build\"]')?.click()" 2>/dev/null
sleep 3 && obsidian dev:dom selector='.qs-terminal-output' total 2>/dev/null
```

**Stacked modals.** When a modal opens another (e.g., Hub → Plugin Browser), close in reverse order:
```bash
# Close Plugin Browser first (topmost)
obsidian eval code="document.querySelector('.quartz-syncer-plugin-browser .modal-close-button')?.click()" 2>/dev/null
sleep 1
# Then close the Hub
obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'hub.close'});console.log(JSON.stringify(r))})()" 2>/dev/null
```

**TerminalOutputModal.** Has no `data-qs` attributes. Close with:
```bash
obsidian eval code="document.querySelector('.qs-terminal-output .qs-terminal-output-actions button:last-child')?.click()" 2>/dev/null
```

## DOM Contract Reference

All queryable elements use `data-qs` attributes. Never use raw CSS classes for verification.

| Selector | Element |
|---|---|
| `[data-qs="pub-center"]` | Publication center modal |
| `[data-qs="pub-row"]` | File row (has `data-qs-path`) |
| `[data-qs="pub-checkbox"]` | Checkbox (has `data-qs-path` or `data-qs-category`) |
| `[data-qs="pub-category"]` | Category header (has `data-qs-value`) |
| `[data-qs="pub-tab"]` | Tab button (has `data-qs-value`) |
| `[data-qs="pub-publish-btn"]` | Publish button |
| `[data-qs="pub-delete-btn"]` | Delete button |
| `[data-qs="pub-search"]` | Filter input |
| `[data-qs="pub-progress"]` | Progress bar |
| `[data-qs="wizard"]` | Onboarding wizard |
| `[data-qs="wizard-step"]` | Step indicator (has `data-qs-value`) |
| `[data-qs="wizard-next"]` | Next/continue button |
| `[data-qs="wizard-input"]` | Input field (has `data-qs-field`) |
| `[data-qs="wizard-error"]` | Error display |
| `[data-qs="statusbar"]` | Status bar (has `data-qs-state`) |
| `[data-qs="diff-view"]` | Diff viewer |
| `[data-qs="hub"]` | Quartz Hub modal |
| `[data-qs="hub-tab"]` | Hub tab button (has `data-qs-value`) |
| `[data-qs="hub-status"]` | Hub status panel |
| `[data-qs="hub-action"]` | Hub action button (has `data-qs-value`) |
| `[data-qs="hub-setup-link-path"]` | Hub setup link path input |
| `[data-qs="hub-setup-link"]` | Hub setup link button |
| `[data-qs="hub-setup-clone-url"]` | Hub setup clone URL input |
| `[data-qs="hub-setup-clone-dest"]` | Hub setup clone destination input |
| `[data-qs="hub-setup-clone"]` | Hub setup clone button |

## Workflow

### Verifying the Publication Center

```bash
obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'pub.open'});console.log(JSON.stringify(r))})()" 2>/dev/null
sleep 2

obsidian dev:dom selector='[data-qs="pub-center"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="pub-row"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="pub-category"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="pub-publish-btn"]' text 2>/dev/null
obsidian dev:dom selector='[data-qs="pub-tab"]' total 2>/dev/null

obsidian dev:screenshot path=/tmp/pub-center.png 2>/dev/null

obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'pub.close'});console.log(JSON.stringify(r))})()" 2>/dev/null
```

### Verifying the Quartz Hub

```bash
obsidian command id=quartz-syncer:open-hub 2>/dev/null
sleep 2

obsidian dev:dom selector='[data-qs="hub"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="hub-tab"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="hub-status"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="hub-action"]' total 2>/dev/null

obsidian dev:screenshot path=/tmp/hub.png 2>/dev/null

# Switch to Setup tab
obsidian eval code="document.querySelector('[data-qs=\"hub-tab\"][data-qs-value=\"setup\"]')?.click()" 2>/dev/null
sleep 1
obsidian dev:dom selector='[data-qs="hub-setup-link"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="hub-setup-clone"]' total 2>/dev/null

obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'hub.close'});console.log(JSON.stringify(r))})()" 2>/dev/null
```

### Verifying Hub Setup — Link via DOM

```bash
obsidian command id=quartz-syncer:open-hub 2>/dev/null
sleep 2
obsidian eval code="document.querySelector('[data-qs=\"hub-tab\"][data-qs-value=\"setup\"]')?.click()" 2>/dev/null
sleep 1

# Set path and trigger validation
obsidian eval code="const el=document.querySelector('[data-qs=\"hub-setup-link-path\"]');el.value='/path/to/quartz';el.dispatchEvent(new Event('input',{bubbles:true}))" 2>/dev/null
sleep 1

obsidian dev:screenshot path=/tmp/hub-link.png 2>/dev/null
# Check validation showed "Quartz repo detected" before clicking Link
obsidian eval code="document.querySelector('[data-qs=\"hub-setup-link\"]')?.click()" 2>/dev/null
```

### Verifying Hub Setup — Clone via operability (recommended)

```bash
obsidian eval code="(async()=>{const r=await window.__QS__.act({name:'hub.setup.clone',params:{url:'https://github.com/user/quartz.git',dest:'/path/to/dest',confirm:true}});console.log(JSON.stringify(r))})()" 2>/dev/null
```

### Verifying the Onboarding Wizard

```bash
obsidian command id=quartz-syncer:setup-wizard 2>/dev/null
sleep 2

obsidian dev:dom selector='[data-qs="wizard"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="wizard-step"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="wizard-input"]' total 2>/dev/null
obsidian dev:dom selector='[data-qs="wizard-next"]' text 2>/dev/null
obsidian dev:dom selector='[data-qs="wizard-error"]' total 2>/dev/null

obsidian dev:screenshot path=/tmp/wizard.png 2>/dev/null
```

### Verifying the Status Bar

```bash
obsidian dev:dom selector='[data-qs="statusbar"]' attr=data-qs-state 2>/dev/null
```

## MUST DO

- Always use `[data-qs="..."]` selectors — never raw CSS classes.
- Always append `2>/dev/null` to all CLI commands.
- Always wait after opening modals (2s) and clicking buttons (3-5s).
- Always verify element exists (`total`) before querying content (`text`, `attr`).
- Always use `dispatchEvent` after setting input `.value`.
- Always take screenshots for visual changes.
- Always use the IIFE pattern for async eval calls.

## MUST NOT DO

- Do NOT use CSS class selectors for verification — exception: TerminalOutputModal (`.qs-terminal-output`) which lacks `data-qs`.
- Do NOT omit `2>/dev/null` — GTK warnings break output parsing.
- Do NOT use top-level `await` in eval — use the IIFE pattern.
- Do NOT set `.value` without `dispatchEvent` — event listeners won't fire.
- Do NOT assume element counts — always query first.
