npx claudepluginhub yves-s/just-ship --plugin just-shipThis skill uses the workspace's default tool permissions.
Testing strategy, framework selection, and execution for web applications. Covers the full testing stack — from unit tests to visual verification.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Testing strategy, framework selection, and execution for web applications. Covers the full testing stack — from unit tests to visual verification.
Not all tests are equal. Choose the right level for what you're testing.
╱ E2E ╲ Few — slow, brittle, high confidence
╱───────╲
╱Integration╲ Some — test real boundaries
╱─────────────╲
╱ Unit Tests ╲ Many — fast, isolated, focused
╰─────────────────╯
| Level | Test When... | Examples |
|---|---|---|
| Unit | Pure functions, business logic, data transformations, validation rules, utilities | formatDate(), calculateDiscount(), validateEmail(), schema parsing |
| Integration | Components interact with real boundaries (DB, API, auth, file system) | API endpoint returns correct data, RLS policy blocks unauthorized access, webhook processes payload |
| Component | UI components render correctly and respond to interaction | Button disables on click, form shows validation errors, modal opens/closes |
| E2E | Critical user journeys that span multiple pages/systems | Checkout flow, signup → onboarding, auth → dashboard redirect |
Choose the framework based on the project's stack. Read project.json for the stack.
Stack?
├── Next.js / React → Vitest + @testing-library/react
├── Remix / React Router → Vitest + @testing-library/react
├── Vue / Nuxt → Vitest + @vue/test-utils
├── Svelte / SvelteKit → Vitest + @testing-library/svelte
├── Node.js / Express / Hono → Vitest (or Jest if already configured)
├── TypeScript (no framework) → Vitest
├── Playwright already in project → Playwright for E2E
└── Jest already configured → Keep Jest (don't migrate mid-ticket)
Default choice: Vitest. Faster than Jest, native ESM/TypeScript support, compatible API.
Exception: If the project already uses Jest with significant test infrastructure, keep Jest. Don't migrate frameworks within a feature ticket.
utils.ts → utils.test.tstests/ directory if project.json specifies paths.tests{filename}.test.ts or {filename}.spec.ts — match existing convention in the projectMocking is a tool, not a default. Every mock hides a real interaction.
| What | Why | How |
|---|---|---|
| External HTTP APIs | Slow, unreliable, costs money | msw (Mock Service Worker) or Vitest vi.mock |
| Database in unit tests | Slow, needs setup/teardown | Mock the repository/data layer, not the DB client directly |
| File system | Side effects, cleanup needed | memfs or mock the fs module |
| Timers / Dates | Non-deterministic | vi.useFakeTimers(), vi.setSystemTime() |
| Environment variables | Test isolation | vi.stubEnv() |
| Third-party SDKs (Stripe, SendGrid) | External dependency, costs money | Mock at the SDK boundary |
| What | Why |
|---|---|
| Your own utility functions | They're fast, deterministic — test them for real |
| Framework primitives (React hooks, Svelte stores) | Mocking them tests nothing real |
| Anything that runs in < 50ms | No performance reason to mock |
| The thing you're actually testing | Mocking the SUT = testing nothing |
| Database in integration tests | The whole point is testing the real query |
"If I remove this mock, does the test still make sense?"
- Yes → The mock is hiding a real dependency. Consider removing it.
- No → The mock is simulating an external boundary. Keep it.
Announce at start: "Starting visual verification with Playwright."
Playwright must be installed:
pip install playwright && playwright install chromium
Task -> Static HTML?
|-- Yes -> Read HTML file, identify selectors
| |-- Playwright script with file:// URL
|
|-- No (dynamic app) -> Server already running?
|-- No -> Use with_server.py (see below)
|-- Yes -> Reconnaissance-then-Action:
1. Navigate + wait for networkidle
2. Screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with found selectors
The framework includes .claude/scripts/with_server.py — starts server, waits for port readiness, runs automation, cleans up.
# Run --help first to see options
python .claude/scripts/with_server.py --help
# Single Server
python .claude/scripts/with_server.py \
--server "npm run dev" --port 5173 \
-- python test_script.py
# Multi-Server (Backend + Frontend)
python .claude/scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test_script.py
Automation scripts contain only Playwright logic — servers are managed by with_server.py:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # CRITICAL: Wait until JS is loaded
# ... automation logic ...
browser.close()
# Take screenshot
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Inspect DOM
content = page.content()
# Discover elements
buttons = page.locator('button').all()
links = page.locator('a[href]').all()
inputs = page.locator('input, textarea, select').all()
Derive correct selectors from screenshot or DOM.
page.click('text=Dashboard')
page.fill('#email', 'test@example.com')
page.click('button[type="submit"]')
console_logs = []
def handle_console(msg):
console_logs.append(f"[{msg.type}] {msg.text}")
page.on("console", handle_console)
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# Evaluate logs after interactions
for log in console_logs:
if log.startswith("[error]"):
print(f"CONSOLE ERROR: {log}")
headless=True — no GUI neededwait_for_load_state('networkidle') before DOM inspection on dynamic appsbrowser.close())text=, role=, CSS selectors, IDs/tmp/ and verify via Read toolDo not inspect the DOM before networkidle is reached — on dynamic apps the initial DOM is empty/incomplete.