name: sg-visual-discover description: Explore a web project's codebase to discover testable user journeys, then generate YAML test manifests mirroring the UI navigation tree. Use when setting up Visual tests for a new project, or after structural UI changes (new routes, removed pages, navigation updates). Trigger on "sg-visual-discover", "visual discover", "generate visual tests", "discover test routes", "update test manifests", "scan UI for tests". context: conversation argument-hint: "[project-path] [--all] [--diff=ref] [--refresh-existing]"
/sg-visual-discover — Discover & Generate Visual Test Manifests
Explore the codebase of any web application, detect all user-facing routes and interactions, and generate a YAML test manifest tree that mirrors the UI navigation structure.
Recommended model: Sonnet 4.6. This skill crawls routes + generates YAML manifests — mechanical work where Opus 4.7 provides no measurable quality gain. Use
/model sonnetbefore invoking to save Opus weekly quota.
Invocations
| Command | Behavior |
|---|---|
/sg-visual-discover |
Interactive — detect changes, ask scope |
/sg-visual-discover <path> |
Discover routes in specific project path |
/sg-visual-discover --diff=main |
Generate manifests only for routes impacted by changes since main |
/sg-visual-discover --all |
Discover all routes (skip scope question) |
/sg-visual-discover --refresh-existing |
In diff mode, also regenerate existing manifests for impacted routes |
Scope Detection
Before scanning the project, determine scope:
- Check for
--allflag → skip to Phase 1 with full scope. - Check for
--diff=<ref>flag → use that ref. - If BOTH
--alland--diff→ error:Cannot use --all and --diff together. - If neither flag:
a. Detect base reference:
b. Runcurrent_branch=$(git rev-parse --abbrev-ref HEAD) if [ "$current_branch" != "main" ] && [ "$current_branch" != "master" ]; then if git show-ref --verify --quiet refs/heads/main; then base=$(git merge-base HEAD main) elif git show-ref --verify --quiet refs/heads/master; then base=$(git merge-base HEAD master) else base="HEAD~1" fi else base="HEAD~1" figit diff --name-only {base}→ changed files. c. If changes detected, map to routes using the same generic route detection described insg-code-audit. d. Ask user:"I detected {N} files changed since
{base}, impacting {R} routes. What scope?"- Only impacted routes — generate manifests for new routes only
- Full app — discover all routes
- Different base e. If no changes detected, offer same fallback as sg-code-audit: last commit, full app, or different base.
- Store
scope_mode("diff" or "full") andimpacted_routes[]for Phase 4.
Flag combination: /sg-visual-discover <project-path> --diff=<ref>
Both apply: discover within project-path, but only generate manifests for routes impacted by the diff. The diff is computed on the whole repo, but only routes within project-path are considered.
--refresh-existing only applies in diff mode. In full discovery, existing manifests are still skipped by default.
Prerequisites
Before running, verify:
agent-browser --version— must be installed- The project has a web frontend (check for package.json with a framework)
- Identify the
visual-tests/directory (create if missing)
Phase 1: Detect Project Structure
Explore the codebase to identify:
1.1 Frontend Framework
Search for framework indicators in this order:
next.config.*orapp/layout.tsx→ Next.js (App Router)pages/_app.tsxorpages/index.tsx→ Next.js (Pages Router)src/App.tsx+react-routerin package.json → React Routersrc/router/index.tsorvue.config.*→ Vueangular.json→ Angular- Fallback: grep for route patterns (
<Route,path:,router.get)
1.2 Static HTML Fallback
If NO framework is detected in Phase 1.1:
- Scan the project root,
src/, andpublic/directories for*.htmlfiles - Each
.htmlfile becomes a test manifest in apages/category - Derive the URL from the file path with these rules:
- Files inside
public/→ strip thepublic/prefix (e.g.,public/about.html→{base_url}/about.html) index.htmlat any level → map to the directory URL (e.g.,public/index.html→{base_url}/,public/help/index.html→{base_url}/help/)- Other files → use the relative path as-is (e.g.,
pages/contact.html→{base_url}/pages/contact.html)
- Files inside
- Screenshot names must be unique per page — derive from the relative URL path, slugified (e.g.,
public/help/index.html→pages-help-index.png,about.html→pages-about.png) - Generate a minimal manifest per page:
name: "<filename without extension>"
description: "Auto-generated from static HTML file"
priority: medium
requires_auth: false
timeout: 30s
tags: [auto-generated, static-html]
steps:
- action: open
url: "{base_url}/<derived-url-path>"
- action: llm-check
description: "Page loads and renders content"
criteria: "Page content is visible, no blank screen, no broken images or missing resources"
severity: critical
screenshot: "pages-<slugified-path>.png"
Element-aware manifest generation: Before generating the minimal manifest, parse each HTML file for interactive elements and enrich the steps:
- If the file contains
<video>or<iframe>→ add:- action: llm-checkwith criteria "Media element loads and is playable/visible, no broken embed" - If the file contains
<form>→ add:- action: fill+- action: click(submit button) +- action: llm-checkwith criteria "Form submission feedback is visible" - If the file contains
<img>(3+ images) → add:- action: llm-checkwith criteria "All images loaded, no broken image icons" - If the file contains
role="tab"or class containingtab→ add tab click + assert steps (see SPA Tab Detection below)
- If the file contains
Batch mode: When invoked with
--allon a static site, generate all manifests in a single pass without step-by-step user interaction. Print a summary at the end instead of asking confirmation per file.Log detection: "No framework detected — falling back to static HTML scan"
If no
.htmlfiles found either, ask the user to specify the route source
1.2b SPA Tab Detection
If no JS framework is detected AND only 1-3 HTML files are found, the site may be a single-page app with tabs:
- Scan each HTML file for tab indicators:
role="tab"orrole="tablist"attributes- Elements with class containing
tab(e.g.,nav-tabs,fr-tabs,tab-button) <a href="#section">hash links that act as tab navigation
- Scan JS files for:
- Files named
tab-*.js,*-tab.js, or*tabs*.js - Hash route handling:
window.location.hash,hashchangeevent
- Files named
- For backend projects (FastAPI/Flask/Django), scan
server.py,app.py,main.pyfor:@app.get("...")or@app.route("...")decorators → each is a route- Hash routes in JS files (
#import,#clean, etc.) → each is a tab/view
- For each detected tab/section:
- Generate a manifest with:
open→clickon the tab →llm-check"Tab content is visible and correct" - Category:
tabs/subdirectory - Name derived from tab text or hash
- Generate a manifest with:
1.2c Auto-Detect Dev Server Command
Before asking the user for base_url, auto-detect the dev server command:
- Check
playwright.config.jsorplaywright.config.ts→ look forwebServer.commandfield - Check
package.json→ look forscripts.devorscripts.start - Check for Python dev scripts:
scripts/*.py,run.py,server.pywithuvicornorflaskpatterns - Check for
docker-compose.ymlordocker-compose.yaml→ proposedocker compose up -d
If found, propose the result as build_command in _config.yaml:
build_command: "<detected command>" # auto-detected from {source}
If nothing detected, leave as build_command: null.
1.3 Route Definitions
Based on the detected framework:
| Framework | Where to look |
|---|---|
| Next.js App Router | app/**/page.tsx — each directory = route |
| Next.js Pages | pages/**/*.tsx — file path = route |
| React Router | Router config files, <Route path=...> patterns |
| Vue Router | router/index.ts, routes: [...] |
| Angular | *-routing.module.ts |
| Generic | Grep for path:, route:, URL patterns in config |
Collect: route path, page component file, any associated layout.
1.4 Navigation Structure
Find navigation components that define the UI hierarchy:
- Search for files named
navigation.ts,nav-*.ts,sidebar-*.tsx,menu-*.tsx - Search for
dashboard-data.ts,route-registry.tsor similar - Look for arrays of navigation items with labels, paths, icons
- This defines the test directory structure
1.5 Feature Flags
Search for feature flag systems:
NEXT_PUBLIC_FEATURE_*,FEATURE_*env varsisFeatureEnabled(),featureFlagpatterns- Record which features are flagged — tests for disabled features get
priority: low
1.6 Interactive Components
For each route, scan the page component for:
- Forms (
<form,onSubmit, input fields) - Modals (
Dialog,Modal,Sheet) - File uploads (
<input type="file",Upload,Dropzone) - Chat interfaces (
onSend,message, chat input patterns) - Data tables, lists with actions
- These become the steps in the manifest
1.7 Test Data
Search the project for usable test files:
test/fixtures/,data-sample/,__fixtures__/*.pdf,*.docx,*.csvin data directories- Seed scripts, sample data generators
- Record paths for use in manifest
data:sections
1.8 Credentials
Look for dev credentials in:
CLAUDE.md,README.md— search for "credentials", "login", "username", "password".env.example— search for auth-related vars- Test files — search for login helpers
Phase 2: Generate Config
If visual-tests/_config.yaml does not exist, create it:
# visual-tests/_config.yaml — Generated by /sg-visual-discover
base_url: "<detected_url>" # from dev server config, docker-compose, or README
credentials:
username: "<detected>"
password: "<detected>"
screenshots_dir: "visual-tests/_results/screenshots"
report_path: "visual-tests/_results/report.md"
agent_browser_path: "agent-browser"
build_command: null # set to rebuild command (e.g. "docker compose up -d --build frontend") or leave null if no rebuild needed
If it already exists, do NOT overwrite.
Phase 3: Generate Shared Bricks
_shared/login.yaml
If authentication is detected, create (if not exists):
name: "Login"
description: "Authenticate with dev credentials"
steps:
- action: open
url: "{base_url}"
- action: click
target: "<detected_login_button>"
- action: fill
target: "<detected_username_field>"
value: "{credentials.username}"
- action: fill
target: "<detected_password_field>"
value: "{credentials.password}"
- action: click
target: "<detected_submit_button>"
- action: wait
duration: 3s
- action: assert_url
expected: "<detected_post_login_url>"
Fill in the targets by reading the actual login component or by running agent-browser open {base_url} and doing a snapshot to identify the real labels.
Phase 4: Generate Test Manifests
For each discovered route, organized by the navigation hierarchy:
4.1 Directory Structure
Mirror the navigation tree:
visual-tests/
_config.yaml
_regressions.yaml # create empty if not exists
_shared/
login.yaml
<nav-group>/
<page>.yaml
<sub-group>/
<page>.yaml
4.2 Manifest Generation Rules
For each route:
- If
scope_mode == "diff"AND route is NOT inimpacted_routes→ SKIP (not impacted by changes) - If a manifest already exists AND
--refresh-existingis NOT set → SKIP (never overwrite without explicit flag) - If a manifest already exists AND
--refresh-existingIS set → REGENERATE (re-scan route components, overwrite manifest). Warn:Refreshing {N} existing manifests. - If no manifest exists → CREATE new manifest
Skeleton manifest (minimum viable test):
name: "<Page Name>"
description: "Auto-generated — customize with real test steps"
priority: medium
requires_auth: true
timeout: 30s
tags: [auto-generated]
steps:
- action: open
url: "{base_url}<route_path>"
- action: llm-check
description: "Page loads correctly"
criteria: "Page content is visible, no error messages, no blank screen"
severity: critical
screenshot: "<page-name>-load.png"
Enhanced manifest (when interactive components are detected):
If the page has forms, uploads, chat, etc., generate steps that exercise them:
- Forms →
fill+clicksubmit +llm-checkresult - Uploads →
upload+llm-waitfor processing +llm-checkresult - Chat →
fillmessage +press Enter+wait+llm-checkresponse - Tables →
llm-check"data is displayed, rows visible"
Pre-fill data: section with discovered test files when relevant.
Report:
scope_mode == "diff":Created {N} new manifests. Skipped {S} routes (manifests exist). {U} uncovered routes (no component match).scope_mode == "full":Created {N} new manifests. Skipped {S} routes (manifests exist). {D} routes deprecated.
4.3 Deprecated Handling
For each existing manifest whose route no longer exists in the codebase:
- Add
deprecated: trueto the manifest frontmatter - Do NOT delete the file
Phase 5: Output Summary
After generation, output:
## /sg-visual-discover Summary
**Framework:** Next.js App Router (or "Static HTML fallback — no framework detected")
**Routes found:** 24
**Navigation groups:** 4
### Generated
- _config.yaml (new)
- _shared/login.yaml (new)
- auth/login.yaml (new)
- dashboard/home.yaml (new)
- ...
### Skipped (already exist)
- dashboard/home.yaml
- ...
### Deprecated
- dashboard/old-feature.yaml (route removed)
### Test Data Found
- data-sample/clement acte.pdf
- data/notarial-corpus/acte_vente/ (15 PDFs)
Run `/sg-visual-run` to execute tests.
Run `/sg-visual-run --regressions` to run only known failures.
agent-browser Reference
| Command | Usage | Example |
|---|---|---|
open <url> |
Navigate to URL | agent-browser open http://localhost:3000 |
snapshot |
Accessibility tree with refs (for AI) | agent-browser snapshot |
click <ref> |
Click element by ref | agent-browser click @e12 |
fill <ref> <text> |
Clear and fill input | agent-browser fill @e10 "alex" |
upload <sel> <files> |
Upload file to input | agent-browser upload "#file-input" ./test.md |
eval <js> |
Run JavaScript in page | agent-browser eval 'document.querySelector("input").id' |
screenshot <path> |
Take screenshot | agent-browser screenshot /tmp/capture.png |
get url |
Get current URL | agent-browser get url |
close |
Close browser | agent-browser close |
Key Rules
- NEVER overwrite existing manifests UNLESS
--refresh-existingis passed. When refreshing, preserve user-added comments and custom steps. By default, only create new ones. - NEVER delete manifests — mark deprecated
- Always verify agent-browser is installed before running
- Use real element labels — do a snapshot of each page to find actual button/input text
- Pre-fill test data when fixtures are found
- Ask the user if framework or route detection fails
Final Checklist
Before considering the discovery complete, verify:
- agent-browser installed and functional
- Framework detected (or generic fallback documented)
- At least one route collected
-
_config.yamlcreated or existing one left untouched -
_regressions.yamlcreated (empty) if absent - No existing manifest overwritten
- Summary displayed (generated / skipped / deprecated)