# Webapp Testing Skill

**Version:** 1.0.0
**Purpose:** Visual regression testing with screenshot capture, comparison, and baseline management
**Integration:** Playwright + SQLite + Redis coordination

---

## Overview

This skill provides comprehensive web application testing with visual regression detection, screenshot management, and CFN Loop integration. Adapted from Anthropic's webapp-testing skill for Claude Flow Novice's distributed architecture.

### Key Features

- 📸 **Screenshot Capture** - Multi-viewport, multi-state capture with Playwright
- 🔍 **Visual Comparison** - Pixel-level diff detection with similarity scoring
- 📦 **Baseline Management** - Version-controlled reference images with audit trail
- 🔄 **CFN Loop Integration** - Automatic validation in Loop 2, Product Owner review
- 🗄️ **SQLite Storage** - Metadata persistence with efficient querying
- ⚡ **Redis Coordination** - Zero-token agent synchronization
- 🎯 **Naming Convention** - Hierarchical organization for discoverability

---

## Architecture

### Components

```
webapp-testing/
├── SKILL.md                         # This file
├── SCREENSHOT_NAMING_CONVENTION.md  # Naming and storage strategy
├── capture-screenshot.sh            # Playwright screenshot capture
├── compare-screenshots.sh           # Visual diff generation
├── set-baseline.sh                  # Baseline management
├── update-baseline.sh               # Baseline updates with audit
├── init-storage.sh                  # SQLite schema initialization
├── query-screenshots.sh             # Screenshot search/retrieval
├── cleanup-ephemeral.sh             # Remove temporary files
└── test-webapp-testing.sh           # Skill validation tests
```

### Data Flow

```
1. Agent requests screenshot capture
   ↓
2. capture-screenshot.sh
   - Launch Playwright
   - Capture viewport/state
   - Save to .screenshots/current/
   - Store metadata in SQLite
   - Push Redis notification
   ↓
3. compare-screenshots.sh (if baseline exists)
   - Retrieve baseline from SQLite
   - Calculate pixel diff
   - Generate diff image (.screenshots/diffs/)
   - Store comparison result in Redis (TTL: 1h)
   ↓
4. CFN Loop 2 Validator
   - Retrieve comparison results from Redis
   - Review visual diffs
   - Report consensus (approve/iterate)
   ↓
5. Product Owner Decision
   - If changes approved: update-baseline.sh
   - If changes rejected: iterate Loop 3
```

---

## Usage

### 1. Initialize Storage

```bash
# Create SQLite schema and directory structure
./.claude/skills/webapp-testing/init-storage.sh
```

**Output:**
- SQLite table: `webapp_screenshots`
- Directories: `.screenshots/{baselines,current,diffs,archive}/`
- Redis keys initialized

### 2. Capture Screenshot

```bash
./.claude/skills/webapp-testing/capture-screenshot.sh \
  --project "auth-system" \
  --component "login-form" \
  --viewport "1920x1080" \
  --state "default" \
  --variant "light-mode" \
  --url "http://localhost:3000/login" \
  --task-id "cfn-task-123" \
  --agent-id "playwright-tester-456"
```

**Parameters:**

| Parameter | Required | Description | Example |
|-----------|----------|-------------|---------|
| `--project` | Yes | Application/feature namespace | `auth-system` |
| `--component` | Yes | UI component or page | `login-form` |
| `--viewport` | Yes | Screen dimensions | `1920x1080`, `375x667` |
| `--state` | Yes | Interaction state | `default`, `hover`, `error` |
| `--variant` | No | Theme/A-B variant (default: `default`) | `light-mode`, `dark-mode` |
| `--url` | Yes | Page URL | `http://localhost:3000/login` |
| `--task-id` | No | CFN task ID | `cfn-task-123` |
| `--agent-id` | No | Agent identifier | `playwright-tester-456` |
| `--selector` | No | Specific element to capture | `.login-form` |
| `--wait-for` | No | Selector to wait for before capture | `.submit-button` |
| `--timeout` | No | Playwright timeout (ms, default: 30000) | `60000` |

**Output:**

```json
{
  "screenshot_key": "auth-system/login-form/1920x1080/default/light-mode",
  "file_path": ".screenshots/current/auth-system/login-form/1920x1080/default/light-mode_20251024143022.png",
  "file_hash": "sha256:abc123...",
  "viewport_actual": { "width": 1920, "height": 1080 },
  "captured_at": 1729781422,
  "baseline_exists": false
}
```

### 3. Compare to Baseline

```bash
./.claude/skills/webapp-testing/compare-screenshots.sh \
  --screenshot-key "auth-system/login-form/1920x1080/default/light-mode" \
  --task-id "cfn-task-123"
```

**Parameters:**

| Parameter | Required | Description |
|-----------|----------|-------------|
| `--screenshot-key` | Yes | Screenshot identifier (from capture output) |
| `--task-id` | Yes | CFN task ID |
| `--threshold` | No | Similarity threshold 0-1 (default: 0.95) |
| `--ignore-regions` | No | JSON array of regions to ignore |

**Output:**

```json
{
  "screenshot_key": "auth-system/login-form/1920x1080/default/light-mode",
  "similarity_score": 0.987,
  "diff_pixels": 1234,
  "total_pixels": 2073600,
  "diff_percentage": 0.06,
  "status": "passed",
  "diff_path": ".screenshots/diffs/cfn-task-123/auth-system_login-form_1920x1080_default_light-mode_diff.png",
  "baseline_path": ".screenshots/baselines/auth-system/login-form/1920x1080/default/light-mode.png",
  "current_path": ".screenshots/current/auth-system/login-form/1920x1080/default/light-mode_20251024143022.png"
}
```

**Status Values:**
- `passed` - Similarity ≥ threshold
- `failed` - Similarity < threshold
- `no-baseline` - No baseline exists for comparison

### 4. Set Baseline

```bash
./.claude/skills/webapp-testing/set-baseline.sh \
  --screenshot-key "auth-system/login-form/1920x1080/default/light-mode" \
  --reason "Initial baseline for login form component"
```

**Effect:**
1. Copy from `current/` to `baselines/` (remove timestamp)
2. Update SQLite: `baseline = 1`
3. Update Redis cache
4. Archive any previous baseline

### 5. Update Baseline

```bash
./.claude/skills/webapp-testing/update-baseline.sh \
  --screenshot-key "auth-system/login-form/1920x1080/default/light-mode" \
  --new-file ".screenshots/current/auth-system/login-form/1920x1080/default/light-mode_20251024150000.png" \
  --reason "Updated button style per design review (PR #456)" \
  --approved-by "product-owner-agent-789"
```

**Effect:**
1. Archive old baseline to `.screenshots/archive/{YYYY-MM}/`
2. Copy new file to `baselines/`
3. Update SQLite record
4. Clear Redis cache
5. Log to audit trail

---

## CFN Loop Integration

### Loop 3: Implementation

**Frontend developer agent:**

```bash
# Capture screenshots during development
for STATE in default hover error; do
  ./.claude/skills/webapp-testing/capture-screenshot.sh \
    --project "auth-system" \
    --component "login-form" \
    --viewport "1920x1080" \
    --state "$STATE" \
    --variant "light-mode" \
    --url "http://localhost:3000/login" \
    --task-id "$TASK_ID" \
    --agent-id "$AGENT_ID"
done

# Report confidence
CONFIDENCE=0.85
./.claude/skills/redis-coordination/invoke-waiting-mode.sh report \
  --task-id "$TASK_ID" \
  --agent-id "$AGENT_ID" \
  --confidence "$CONFIDENCE" \
  --iteration 1
```

### Loop 2: Validation

**Reviewer agent:**

```bash
# Compare all screenshots to baselines
COMPARISONS=$(redis-cli keys "screenshot:queue:${TASK_ID}:*" | while read KEY; do
  SCREENSHOT_KEY=$(redis-cli get "$KEY")
  ./.claude/skills/webapp-testing/compare-screenshots.sh \
    --screenshot-key "$SCREENSHOT_KEY" \
    --task-id "$TASK_ID" \
    --threshold 0.95
done)

# Calculate consensus
FAILED_COUNT=$(echo "$COMPARISONS" | jq -r 'select(.status == "failed") | .screenshot_key' | wc -l)
TOTAL_COUNT=$(echo "$COMPARISONS" | jq -s 'length')

if [ "$FAILED_COUNT" -eq 0 ]; then
  CONFIDENCE=0.95
  FEEDBACK="All visual regression tests passed"
else
  CONFIDENCE=0.70
  FEEDBACK="Visual regressions detected in $FAILED_COUNT/$TOTAL_COUNT screenshots. Review diffs in .screenshots/diffs/${TASK_ID}/"
fi

# Report consensus
./.claude/skills/redis-coordination/invoke-waiting-mode.sh report \
  --task-id "$TASK_ID" \
  --agent-id "$AGENT_ID" \
  --confidence "$CONFIDENCE" \
  --iteration 1
```

### Product Owner Decision

**After Loop 2 consensus ≥ 0.90:**

```bash
# Product Owner reviews visual diffs
DIFF_PATHS=$(redis-cli keys "screenshot:diff:${TASK_ID}:*" | while read KEY; do
  redis-cli get "$KEY" | jq -r '.diff_path'
done)

# Decision logic
if [[ $(echo "$DIFF_PATHS" | wc -l) -eq 0 ]]; then
  # No diffs, proceed
  DECISION="PROCEED"
else
  # Review diffs (Product Owner logic here)
  # For now: if diffs exist, require approval
  echo "Visual changes detected. Review diffs:"
  echo "$DIFF_PATHS"

  # Simulate approval (in production, this would be manual review)
  DECISION="ITERATE"  # or "PROCEED" after approval
fi

# If approved, update baselines
if [ "$DECISION" = "PROCEED" ]; then
  redis-cli keys "screenshot:diff:${TASK_ID}:*" | while read KEY; do
    SCREENSHOT_KEY=$(echo "$KEY" | sed 's/screenshot:diff:[^:]*://')
    CURRENT_FILE=$(redis-cli get "$KEY" | jq -r '.current_path')

    ./.claude/skills/webapp-testing/update-baseline.sh \
      --screenshot-key "$SCREENSHOT_KEY" \
      --new-file "$CURRENT_FILE" \
      --reason "Approved visual changes from task $TASK_ID" \
      --approved-by "product-owner"
  done
fi
```

---

## Redis Coordination

### Keys and Patterns

**Screenshot Capture Queue:**

```bash
# Add screenshot to capture queue
redis-cli lpush "screenshot:queue:${TASK_ID}" \
  "auth-system/login-form/1920x1080/default/light-mode"

# Wait for captures to complete (orchestrator)
redis-cli blpop "screenshot:queue:${TASK_ID}:done" 0
```

**Comparison Results (TTL: 1 hour):**

```bash
# Store comparison result
redis-cli setex "screenshot:diff:${TASK_ID}:${SCREENSHOT_KEY}" 3600 \
  '{"similarity_score": 0.987, "status": "passed", "diff_path": "..."}'

# Retrieve all comparisons for task
redis-cli keys "screenshot:diff:${TASK_ID}:*" | while read KEY; do
  redis-cli get "$KEY"
done | jq -s '.'
```

**Baseline Cache (TTL: 24 hours):**

```bash
# Cache baseline file hash
redis-cli setex "screenshot:baseline:${SCREENSHOT_KEY}" 86400 \
  "sha256:abc123..."

# Retrieve cached hash
CACHED_HASH=$(redis-cli get "screenshot:baseline:${SCREENSHOT_KEY}")
```

**Agent Coordination:**

```bash
# Signal screenshot capture complete
redis-cli lpush "swarm:${TASK_ID}:${AGENT_ID}:screenshot-complete" "1"

# Wait for all agents to complete captures
redis-cli blpop "swarm:${TASK_ID}:all-screenshots-complete" 0
```

---

## SQLite Schema

### Table: `webapp_screenshots`

```sql
CREATE TABLE IF NOT EXISTS webapp_screenshots (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  screenshot_key TEXT UNIQUE NOT NULL,
  project TEXT NOT NULL,
  component TEXT NOT NULL,
  viewport TEXT NOT NULL,
  state TEXT NOT NULL,
  variant TEXT NOT NULL DEFAULT 'default',
  file_path TEXT NOT NULL,
  file_hash TEXT NOT NULL,
  baseline BOOLEAN DEFAULT 0,
  captured_at INTEGER NOT NULL,
  task_id TEXT,
  agent_id TEXT,
  metadata TEXT,  -- JSON
  UNIQUE(project, component, viewport, state, variant, baseline)
);

CREATE INDEX idx_baseline ON webapp_screenshots(project, component, viewport, state, variant, baseline);
CREATE INDEX idx_task ON webapp_screenshots(task_id);
CREATE INDEX idx_component ON webapp_screenshots(project, component);
CREATE INDEX idx_captured ON webapp_screenshots(captured_at DESC);
```

### Table: `screenshot_audit_log`

```sql
CREATE TABLE IF NOT EXISTS screenshot_audit_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  screenshot_key TEXT NOT NULL,
  action TEXT NOT NULL,  -- 'created', 'updated', 'archived', 'deleted'
  reason TEXT,
  old_file_hash TEXT,
  new_file_hash TEXT,
  approved_by TEXT,
  created_at INTEGER NOT NULL,
  task_id TEXT,
  metadata TEXT  -- JSON
);

CREATE INDEX idx_audit_screenshot ON screenshot_audit_log(screenshot_key);
CREATE INDEX idx_audit_created ON screenshot_audit_log(created_at DESC);
CREATE INDEX idx_audit_task ON screenshot_audit_log(task_id);
```

---

## Skill Scripts

### `capture-screenshot.sh`

**Purpose:** Capture screenshot with Playwright

**Implementation:**

```bash
#!/bin/bash
set -e

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --project) PROJECT="$2"; shift 2 ;;
    --component) COMPONENT="$2"; shift 2 ;;
    --viewport) VIEWPORT="$2"; shift 2 ;;
    --state) STATE="$2"; shift 2 ;;
    --variant) VARIANT="${2:-default}"; shift 2 ;;
    --url) URL="$2"; shift 2 ;;
    --task-id) TASK_ID="$2"; shift 2 ;;
    --agent-id) AGENT_ID="$2"; shift 2 ;;
    --selector) SELECTOR="$2"; shift 2 ;;
    --wait-for) WAIT_FOR="$2"; shift 2 ;;
    --timeout) TIMEOUT="${2:-30000}"; shift 2 ;;
    *) echo "Unknown parameter: $1"; exit 1 ;;
  esac
done

# Validate required parameters
if [ -z "$PROJECT" ] || [ -z "$COMPONENT" ] || [ -z "$VIEWPORT" ] || [ -z "$STATE" ] || [ -z "$URL" ]; then
  echo "Error: Missing required parameters"
  echo "Usage: $0 --project <project> --component <component> --viewport <viewport> --state <state> --url <url>"
  exit 1
fi

# Generate screenshot key and paths
VARIANT="${VARIANT:-default}"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
SCREENSHOT_KEY="${PROJECT}/${COMPONENT}/${VIEWPORT}/${STATE}/${VARIANT}"
OUTPUT_DIR=".screenshots/current/${PROJECT}/${COMPONENT}/${VIEWPORT}/${STATE}"
OUTPUT_FILE="${OUTPUT_DIR}/${VARIANT}_${TIMESTAMP}.png"

mkdir -p "$OUTPUT_DIR"

# Parse viewport dimensions
IFS='x' read -r WIDTH HEIGHT <<< "$VIEWPORT"

# Capture screenshot with Playwright (inline Node.js script)
node -e "
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    viewport: { width: ${WIDTH}, height: ${HEIGHT} }
  });
  const page = await context.newPage();

  await page.goto('${URL}', { waitUntil: 'networkidle', timeout: ${TIMEOUT:-30000} });

  ${WAIT_FOR:+await page.waitForSelector('${WAIT_FOR}', { timeout: ${TIMEOUT:-30000} });}

  // Simulate state if needed
  ${SELECTOR:+const element = await page.locator('${SELECTOR}');}
  if ('${STATE}' === 'hover') {
    await ${SELECTOR:+element}${SELECTOR:-.locator('body')}.hover();
  } else if ('${STATE}' === 'focus') {
    await ${SELECTOR:+element}${SELECTOR:-.locator('input').first()}.focus();
  }

  await page.screenshot({
    path: '${OUTPUT_FILE}',
    ${SELECTOR:+clip: await ${SELECTOR:+element}.boundingBox(),}
    fullPage: ${SELECTOR:+false}${SELECTOR:-true}
  });

  await browser.close();

  console.log(JSON.stringify({
    screenshot_key: '${SCREENSHOT_KEY}',
    file_path: '${OUTPUT_FILE}',
    viewport_actual: { width: ${WIDTH}, height: ${HEIGHT} },
    captured_at: Math.floor(Date.now() / 1000)
  }));
})();
"

# Calculate file hash
FILE_HASH=$(sha256sum "$OUTPUT_FILE" | awk '{print $1}')

# Check if baseline exists
BASELINE_EXISTS=$(sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT COUNT(*) FROM webapp_screenshots WHERE screenshot_key = '${SCREENSHOT_KEY}' AND baseline = 1")

# Store metadata in SQLite
METADATA=$(cat <<EOF
{
  "browser": "chromium",
  "viewport_actual": { "width": ${WIDTH}, "height": ${HEIGHT} },
  "url": "${URL}",
  "task_id": "${TASK_ID}",
  "agent_id": "${AGENT_ID}",
  "git_commit": "$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')",
  "git_branch": "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
}
EOF
)

sqlite3 ~/.claude/memory/adaptive-context.db <<EOF
INSERT INTO webapp_screenshots (
  screenshot_key, project, component, viewport, state, variant,
  file_path, file_hash, baseline, captured_at, task_id, agent_id, metadata
) VALUES (
  '${SCREENSHOT_KEY}', '${PROJECT}', '${COMPONENT}', '${VIEWPORT}', '${STATE}', '${VARIANT}',
  '${OUTPUT_FILE}', 'sha256:${FILE_HASH}', 0, $(date +%s), '${TASK_ID}', '${AGENT_ID}', '${METADATA}'
);
EOF

# Push to Redis queue (for orchestrator tracking)
if [ -n "$TASK_ID" ]; then
  redis-cli lpush "screenshot:queue:${TASK_ID}" "$SCREENSHOT_KEY" > /dev/null
fi

# Output result
cat <<EOF
{
  "screenshot_key": "${SCREENSHOT_KEY}",
  "file_path": "${OUTPUT_FILE}",
  "file_hash": "sha256:${FILE_HASH}",
  "viewport_actual": { "width": ${WIDTH}, "height": ${HEIGHT} },
  "captured_at": $(date +%s),
  "baseline_exists": $([[ "$BASELINE_EXISTS" -eq 1 ]] && echo "true" || echo "false")
}
EOF
```

### `compare-screenshots.sh`

**Purpose:** Compare captured screenshot to baseline

**Implementation:**

```bash
#!/bin/bash
set -e

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --screenshot-key) SCREENSHOT_KEY="$2"; shift 2 ;;
    --task-id) TASK_ID="$2"; shift 2 ;;
    --threshold) THRESHOLD="${2:-0.95}"; shift 2 ;;
    *) echo "Unknown parameter: $1"; exit 1 ;;
  esac
done

# Validate
if [ -z "$SCREENSHOT_KEY" ] || [ -z "$TASK_ID" ]; then
  echo "Error: Missing required parameters"
  exit 1
fi

# Get baseline and current paths from SQLite
BASELINE_PATH=$(sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT file_path FROM webapp_screenshots WHERE screenshot_key = '${SCREENSHOT_KEY}' AND baseline = 1")

CURRENT_PATH=$(sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT file_path FROM webapp_screenshots WHERE screenshot_key = '${SCREENSHOT_KEY}' AND baseline = 0 ORDER BY captured_at DESC LIMIT 1")

if [ -z "$BASELINE_PATH" ]; then
  # No baseline exists
  echo '{"status": "no-baseline", "screenshot_key": "'$SCREENSHOT_KEY'"}'
  exit 0
fi

if [ -z "$CURRENT_PATH" ]; then
  echo "Error: No current screenshot found for key: $SCREENSHOT_KEY"
  exit 1
fi

# Generate diff path
DIFF_DIR=".screenshots/diffs/${TASK_ID}"
mkdir -p "$DIFF_DIR"
SCREENSHOT_KEY_FLAT=$(echo "$SCREENSHOT_KEY" | tr '/' '_')
DIFF_PATH="${DIFF_DIR}/${SCREENSHOT_KEY_FLAT}_diff.png"

# Compare using pixelmatch (Node.js)
COMPARISON=$(node -e "
const fs = require('fs');
const PNG = require('pngjs').PNG;
const pixelmatch = require('pixelmatch');

const baseline = PNG.sync.read(fs.readFileSync('${BASELINE_PATH}'));
const current = PNG.sync.read(fs.readFileSync('${CURRENT_PATH}'));
const { width, height } = baseline;
const diff = new PNG({ width, height });

const numDiffPixels = pixelmatch(
  baseline.data, current.data, diff.data, width, height,
  { threshold: 0.1 }
);

fs.writeFileSync('${DIFF_PATH}', PNG.sync.write(diff));

const totalPixels = width * height;
const diffPercentage = (numDiffPixels / totalPixels) * 100;
const similarityScore = 1 - (numDiffPixels / totalPixels);
const status = similarityScore >= ${THRESHOLD} ? 'passed' : 'failed';

console.log(JSON.stringify({
  screenshot_key: '${SCREENSHOT_KEY}',
  similarity_score: similarityScore.toFixed(4),
  diff_pixels: numDiffPixels,
  total_pixels: totalPixels,
  diff_percentage: diffPercentage.toFixed(2),
  status: status,
  diff_path: '${DIFF_PATH}',
  baseline_path: '${BASELINE_PATH}',
  current_path: '${CURRENT_PATH}'
}));
")

# Store in Redis (TTL: 1 hour)
redis-cli setex "screenshot:diff:${TASK_ID}:${SCREENSHOT_KEY}" 3600 "$COMPARISON" > /dev/null

# Output result
echo "$COMPARISON"
```

### `set-baseline.sh`

**Purpose:** Set a screenshot as baseline

**Implementation:**

```bash
#!/bin/bash
set -e

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --screenshot-key) SCREENSHOT_KEY="$2"; shift 2 ;;
    --reason) REASON="$2"; shift 2 ;;
    *) echo "Unknown parameter: $1"; exit 1 ;;
  esac
done

# Validate
if [ -z "$SCREENSHOT_KEY" ]; then
  echo "Error: Missing --screenshot-key"
  exit 1
fi

# Parse screenshot key
IFS='/' read -r PROJECT COMPONENT VIEWPORT STATE VARIANT <<< "$SCREENSHOT_KEY"

# Get current screenshot from SQLite
CURRENT_PATH=$(sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT file_path FROM webapp_screenshots WHERE screenshot_key = '${SCREENSHOT_KEY}' AND baseline = 0 ORDER BY captured_at DESC LIMIT 1")

if [ -z "$CURRENT_PATH" ]; then
  echo "Error: No current screenshot found for key: $SCREENSHOT_KEY"
  exit 1
fi

# Create baseline directory
BASELINE_DIR=".screenshots/baselines/${PROJECT}/${COMPONENT}/${VIEWPORT}/${STATE}"
mkdir -p "$BASELINE_DIR"
BASELINE_PATH="${BASELINE_DIR}/${VARIANT}.png"

# Copy to baselines (remove timestamp from filename)
cp "$CURRENT_PATH" "$BASELINE_PATH"

# Get file hash
FILE_HASH=$(sha256sum "$BASELINE_PATH" | awk '{print $1}')

# Update SQLite (set as baseline)
sqlite3 ~/.claude/memory/adaptive-context.db <<EOF
INSERT OR REPLACE INTO webapp_screenshots (
  screenshot_key, project, component, viewport, state, variant,
  file_path, file_hash, baseline, captured_at, task_id, agent_id, metadata
)
SELECT
  screenshot_key, project, component, viewport, state, variant,
  '${BASELINE_PATH}', 'sha256:${FILE_HASH}', 1, $(date +%s), task_id, agent_id, metadata
FROM webapp_screenshots
WHERE screenshot_key = '${SCREENSHOT_KEY}' AND baseline = 0
ORDER BY captured_at DESC LIMIT 1;

-- Audit log
INSERT INTO screenshot_audit_log (
  screenshot_key, action, reason, new_file_hash, created_at
) VALUES (
  '${SCREENSHOT_KEY}', 'created', '${REASON}', 'sha256:${FILE_HASH}', $(date +%s)
);
EOF

# Clear Redis cache
redis-cli del "screenshot:baseline:${SCREENSHOT_KEY}" > /dev/null

echo '{"status": "baseline-set", "screenshot_key": "'$SCREENSHOT_KEY'", "baseline_path": "'$BASELINE_PATH'"}'
```

---

## Configuration

### `.claude/skills/webapp-testing/config.json`

```json
{
  "storage": {
    "baselines_dir": ".screenshots/baselines",
    "current_dir": ".screenshots/current",
    "diffs_dir": ".screenshots/diffs",
    "archive_dir": ".screenshots/archive",
    "db_path": "~/.claude/memory/adaptive-context.db"
  },
  "defaults": {
    "viewport": "1920x1080",
    "threshold": 0.95,
    "timeout": 30000,
    "variant": "default"
  },
  "viewports": {
    "desktop": "1920x1080",
    "laptop": "1366x768",
    "tablet": "768x1024",
    "mobile": "375x667",
    "mobile-large": "390x844"
  },
  "redis": {
    "ttl_comparison": 3600,
    "ttl_baseline_cache": 86400
  },
  "cleanup": {
    "archive_retention_days": 90,
    "current_retention_hours": 24,
    "diff_retention_hours": 48
  }
}
```

---

## Best Practices

### 1. Screenshot Naming

✅ **DO:**
- Use consistent project/component names
- Follow kebab-case convention
- Include all relevant states

❌ **DON'T:**
- Use spaces or special characters
- Mix naming conventions
- Skip variant specification

### 2. Baseline Management

✅ **DO:**
- Review visual changes before updating baselines
- Document baseline change reasons
- Version control baselines directory
- Archive old baselines before updates

❌ **DON'T:**
- Auto-approve baseline changes without review
- Update baselines without audit trail
- Delete old baselines immediately
- Skip Product Owner approval for visual changes

### 3. CFN Loop Integration

✅ **DO:**
- Capture screenshots in Loop 3 (implementation)
- Compare in Loop 2 (validation)
- Require Product Owner approval for baseline updates
- Store comparison results in Redis for validator review

❌ **DON'T:**
- Update baselines in Loop 3 without validation
- Skip visual regression checks in Loop 2
- Proceed with failed visual tests without iteration
- Mix capture and comparison in same loop

### 4. Performance

✅ **DO:**
- Compress screenshots (pngquant, oxipng)
- Set appropriate Redis TTLs
- Clean up ephemeral files regularly
- Use Redis BLPOP for zero-token coordination

❌ **DON'T:**
- Store uncompressed screenshots
- Keep all test runs indefinitely
- Poll Redis for completion
- Duplicate baselines across tasks

---

## Troubleshooting

### Playwright Installation

```bash
# Install Playwright browsers
npx playwright install chromium

# Verify installation
npx playwright --version
```

### SQLite Schema Issues

```bash
# Reinitialize schema
./.claude/skills/webapp-testing/init-storage.sh --force
```

### Missing Screenshots

```bash
# Query for screenshot
sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT * FROM webapp_screenshots WHERE screenshot_key = 'your-key-here'"

# Check filesystem
ls -lah .screenshots/current/your-project/your-component/
```

### Comparison Failures

```bash
# Check if baseline exists
sqlite3 ~/.claude/memory/adaptive-context.db \
  "SELECT file_path FROM webapp_screenshots WHERE screenshot_key = 'your-key' AND baseline = 1"

# Manually set baseline
./.claude/skills/webapp-testing/set-baseline.sh \
  --screenshot-key "your-key" \
  --reason "Initial baseline"
```

---

## Future Enhancements

### Phase 2
- [ ] Multi-viewport capture (single command)
- [ ] Animation frame capture (GIF/video)
- [ ] Component isolation (auto-crop)
- [ ] Playwright trace integration

### Phase 3
- [ ] ML-based anomaly detection
- [ ] Smart diff highlighting
- [ ] Figma → screenshot generation
- [ ] Natural language screenshot queries

---

**Status:** Ready for Implementation
**Dependencies:** Playwright, pngjs, pixelmatch, SQLite, Redis
**Documentation:** See `SCREENSHOT_NAMING_CONVENTION.md`
