.claude/skills/test-setup/SKILL.md
Initialize testing infrastructure for a project. Syncs Vitest + Playwright configs from upstream.
npx skillsauth add lucidlabs-hq/agent-kit test-setupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Set up Vitest + Playwright testing infrastructure from upstream configs. Run this once per project to enable TDD workflow.
cd frontend
# Check if already set up
if [ -f "vitest.config.ts" ]; then
echo "Test infrastructure already exists."
echo " vitest.config.ts: EXISTS"
echo " playwright.config.ts: $([ -f playwright.config.ts ] && echo EXISTS || echo MISSING)"
echo " src/test/setup.ts: $([ -f src/test/setup.ts ] && echo EXISTS || echo MISSING)"
echo ""
echo "Run /test to execute tests."
exit 0
fi
# Find upstream directory
UPSTREAM=""
if [ -d "../../lucidlabs-agent-kit/frontend" ]; then
UPSTREAM="../../lucidlabs-agent-kit/frontend"
elif [ -d "../lucidlabs-agent-kit/frontend" ]; then
UPSTREAM="../lucidlabs-agent-kit/frontend"
fi
if [ -z "$UPSTREAM" ]; then
echo "Upstream not found. Creating configs from template."
fi
From upstream (preferred) or from inline templates:
vitest.config.ts:
/**
* Vitest Configuration
*
* Key choices:
* - vite-tsconfig-paths: resolves @ alias from tsconfig (recommended by Next.js docs)
* - restoreMocks: auto-restores mocks between tests (prevents state leakage)
* - jsdom: browser-like environment for React component tests
*/
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.test.{ts,tsx}", "lib/**/*.test.{ts,tsx}"],
restoreMocks: true,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules", "src/test", "e2e"],
},
},
});
NOTE: Do NOT use manual
path.resolvealiases. Usevite-tsconfig-pathsinstead - it reads the@alias fromtsconfig.jsonautomatically, keeping a single source of truth.
playwright.config.ts:
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e/specs",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "mobile", use: { ...devices["Pixel 5"] } },
],
webServer: {
command: "pnpm run dev --port 3001",
url: "http://localhost:3001",
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
src/test/setup.ts:
import "@testing-library/jest-dom";
import { vi } from "vitest";
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false, media: query, onchange: null,
addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(),
})),
});
class MockIntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
}
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
value: MockIntersectionObserver,
});
Add to devDependencies in package.json:
{
"vitest": "^4.0.17",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.17",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"jsdom": "^27.4.0",
"vite-tsconfig-paths": "^4.3.2",
"@playwright/test": "^1.57.0",
"agent-browser": "^0.6.0"
}
Add to scripts in package.json:
{
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed"
}
mkdir -p lib/__tests__
mkdir -p e2e/specs
mkdir -p e2e/pages
mkdir -p src/test
Create lib/__tests__/utils.test.ts as a canary:
import { describe, expect, it } from "vitest";
import { cn } from "../utils";
describe("cn", () => {
it("merges class names correctly", () => {
expect(cn("foo", "bar")).toBe("foo bar");
});
});
pnpm install
pnpm run test
┌─────────────────────────────────────────────────────────────────┐
│ TEST INFRASTRUCTURE READY │
│ ───────────────────────── │
│ │
│ Created: │
│ vitest.config.ts │
│ playwright.config.ts │
│ src/test/setup.ts │
│ lib/__tests__/utils.test.ts (canary) │
│ │
│ Directories: │
│ lib/__tests__/ │
│ e2e/specs/ │
│ e2e/pages/ │
│ │
│ Verification: 1 test passed │
│ │
│ Next: Run /test to see coverage gaps │
│ Run /test:watch for TDD workflow │
│ │
└─────────────────────────────────────────────────────────────────┘
development
Deploy invoice-accounting-assistant to HQ server. Runs tests first (TDD), then builds and deploys. Use when ready to push changes to staging/production.
testing
Visual UI verification with agent-browser. Use after implementing UI components to take screenshots, verify interactions, and self-check your work. FASTER than E2E tests.
documentation
Update README with current project status and features. Use after completing features.
tools
--- name: time-report description: Cross-project time report. Aggregates all session data from ~/.claude-time/sessions/. Use to see how much time was spent across all projects. disable-model-invocation: true allowed-tools: Bash, Read argument-hint: [all | this-week | this-month | last-month | {project-name}] --- # Time Report: Cross-Project Session Overview ## Objective Read ALL session files from `~/.claude-time/sessions/*.json` and produce an aggregated time report. Supports filtering by pe