# Visual Test

> Visually verify a component by launching its Storybook story and taking a screenshot with playwright-cli. Use after making visual changes to a component.

- Skill: `microsoft/visual-test` (Agent Skill)
- Install (CLI): `npx skillmds@latest add microsoft/visual-test`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/visual-test/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/microsoft/visual-test

---


# Visual Test a Component

Visually verify **$ARGUMENTS** by launching Storybook and capturing a screenshot with `playwright-cli`.

## Prerequisites

Run `playwright-cli` via `npx` so nothing is installed globally on the user's box. The first invocation downloads `@playwright/cli@0.1.1` into the npx cache; subsequent calls are cached. Every command below uses this form:

```bash
npx -y @playwright/cli@0.1.1 <command>
```

## Critical: use the per-component Storybook only

Always boot the **per-component stories package** (`react-<component>-stories`) via nx `storybook` target, which only imports its own component's stories and dependencies.

## Steps

1. **Find the component's stories package.** Each v9 component has a dedicated stories package named `react-<component>-stories`:

   ```bash
   yarn nx show project react-<lowercase-component-name>-stories --json
   ```

   If nx returns nothing with output of `Could not find project react-<component>-stories`, the component doesn't have its own stories package — check for a preview package (`react-<component>-preview-stories`) or ask before proceeding.

2. **Start the component's Storybook dev server.** Use the `storybook` target on the stories project directly — it's the most portable, since library aliases like `react-<component>:start` were only added in April 2026 and may not exist in older workspace snapshots:

   ```bash
   yarn nx run react-<component>-stories:storybook &
   ```

3. **Find the storybook port.** Three quirks to know:

   - Storybook picks a **random high port** on first boot (e.g. `49360`), not the Storybook default `6006`. Don't assume.
   - The nx wrapper process often exits 0 after delegating to storybook, leaving the actual server running as a child. So the nx PID isn't the storybook PID.
   - The storybook child opens **two** listening sockets: one for HTTP content, one for the webpack HMR event-stream. They are not ordered — either one can be numerically lower. Picking by port number is unreliable; pick by `Content-Type`.

   Reliable detection — target the storybook node child (not the yarn wrapper), then probe each listening socket until one returns `text/html`:

   ```bash
   # Wait up to 180s for the storybook child to bind an HTTP port.
   # Pattern matches the node child specifically, not `yarn storybook dev` (the wrapper has no sockets).
   for i in $(seq 1 180); do
     SB_CHILD=$(pgrep -f "node.*\.bin/storybook dev" | head -1)
     if [ -n "$SB_CHILD" ]; then
       for port in $(lsof -a -p "$SB_CHILD" -i -P -sTCP:LISTEN 2>/dev/null | awk 'NR>1 {print $9}' | sed 's/.*://'); do
         CT=$(curl -sI --max-time 2 "http://localhost:$port/" 2>/dev/null | grep -i '^content-type:' | grep -i 'text/html')
         if [ -n "$CT" ]; then SB_PORT=$port; break; fi
       done
       if [ -n "$SB_PORT" ]; then break; fi
     fi
     sleep 1
   done
   echo "Storybook child PID=$SB_CHILD on port $SB_PORT"
   ```

   Then wait for Storybook to finish compiling stories — the HTTP port answers before `index.json` is populated:

   ```bash
   for i in $(seq 1 60); do
     N=$(curl -s --max-time 2 "http://localhost:$SB_PORT/index.json" 2>/dev/null \
       | python3 -c "import json,sys; print(len(json.load(sys.stdin).get('entries', {})))" 2>/dev/null || echo 0)
     if [ "$N" -gt 0 ]; then break; fi
     sleep 2
   done
   ```

   If no port turns up, or `index.json` never populates — **do not** fall back to the workspace-wide Storybook; read the nx output log and debug the per-component boot. The most common real failure is missing build artifacts for unstable re-export deps (see troubleshooting below).

4. **Open the page with playwright-cli:**

   ```bash
   npx -y @playwright/cli@0.1.1 open "http://localhost:$SB_PORT"
   ```

5. **Navigate to the specific story iframe** and capture a screenshot.
   Use the iframe URL for a clean render without Storybook chrome:

   ```bash
   npx -y @playwright/cli@0.1.1 goto "http://localhost:$SB_PORT/iframe.html?id=components-<component>--default&viewMode=story"
   npx -y @playwright/cli@0.1.1 screenshot --filename=/tmp/visual-test-$ARGUMENTS.png
   ```

6. **View the screenshot** using the Read tool to visually inspect the rendered component.

7. **Use `snapshot`** to get the accessibility tree and find interactive element refs:

   ```bash
   npx -y @playwright/cli@0.1.1 snapshot
   ```

   Then interact with elements by ref (e.g., click, hover) before taking more screenshots.

8. **If the component doesn't look right**, go back to the code, fix the issue, and repeat from step 4 (Storybook hot-reloads changes).

9. **Clean up** when done:
   ```bash
   npx -y @playwright/cli@0.1.1 close
   # Kill storybook — the nx wrapper may already be gone, so target the child
   [ -n "$SB_CHILD" ] && kill "$SB_CHILD" 2>/dev/null
   lsof -i :$SB_PORT -t 2>/dev/null | xargs kill 2>/dev/null
   ```

## Troubleshooting

**`yarn nx run react-<component>-stories:storybook` says the target doesn't exist.**
The workspace graph may be stale (recent reparent). Run `yarn nx reset` then retry. If `stroybook` aliases still don't exist, use the direct yarn invocation:

```bash
cd packages/react-components/react-<component>/stories && yarn storybook dev --port 0 &
# --port 0 asks Storybook to pick a free port; detect it via the pgrep/lsof pattern above
```

## Story ID Pattern

Story IDs follow the pattern `<category>-<component>--<story>`:

```
# Default story for Button
components-button--default

# Appearance variant
components-button--appearance

# Default story for Menu
components-menu--default
```

To discover exact story IDs, open the Storybook sidebar and use `snapshot` to find navigation links,
or check the story file's `export default { title: '...' }` metadata.

## Iframe URL Format

```
# Local storybook (replace $SB_PORT with the actual port)
http://localhost:$SB_PORT/iframe.html?id=components-button--default&viewMode=story

# Dark theme
http://localhost:$SB_PORT/iframe.html?id=components-button--default&viewMode=story&globals=theme:webDarkTheme
```

The `/iframe.html` URL gives a clean render without Storybook chrome — always prefer this for screenshots.

## Tips

- Use `npx -y @playwright/cli@0.1.1 snapshot` to get an accessibility tree — useful for verifying ARIA attributes and finding interactive elements.
- Use `npx -y @playwright/cli@0.1.1 click <ref>` to interact with the component (test hover states, open menus, etc.) before taking a screenshot.
- Use `npx -y @playwright/cli@0.1.1 resize <width> <height>` to test responsive behavior.
- For multiple story variants, take a screenshot of each: Default, Appearance, Size, Disabled, etc.

