بنقرة واحدة
testability
// Make features testable by design. Testing pyramid from fast (local) to slow (UI). Expose APIs securely for testing.
// Make features testable by design. Testing pyramid from fast (local) to slow (UI). Expose APIs securely for testing.
Debug production issues on Vercel using logs, database inspection, and proper deployment waiting
Manage DNS records for domains hosted on Vercel using the Vercel CLI
Workspace guide to introduce OpenWork and onboard new users.
Access and update company administrative information stored in Notion
Create and register new OpenCode skills in this repo
Keep the 0 Finance CLI aligned with product capabilities.
| name | testability |
| description | Make features testable by design. Testing pyramid from fast (local) to slow (UI). Expose APIs securely for testing. |
| license | MIT |
| compatibility | opencode |
| metadata | {"audience":"developers","workflow":"development","trigger":"before-writing-code"} |
Ensure every feature you build is testable from the start. This skill teaches:
"If you can't test it locally, you can't test it."
Every feature should be testable at multiple levels. Design for testability, don't bolt it on later.
From fastest (run constantly) to slowest (run occasionally):
▲
/U\ UI Tests (E2E)
/ I \ - Browser automation
/-----\ - Run on staging only
/ API \ API/Integration Tests
/ TESTS \ - tRPC procedures
/-----------\ - Can run locally
/ UNIT \ Unit Tests
/ TESTS \ - Pure functions
/------------------\ - Fastest, run always
| Layer | Speed | Where | When to Use |
|---|---|---|---|
| Unit | <1s | Local | Pure logic, utils, calculations |
| API/Integration | 1-10s | Local + CI | tRPC, DB operations, business logic |
| Staging | 30s-2m | Vercel preview | Full flow verification |
| UI/E2E | 2-5m | Staging only | Critical user journeys |
// packages/web/src/lib/utils/calculate-fee.ts
export function calculateFee(amount: number, feePercent: number): number {
return amount * (feePercent / 100);
}
// packages/web/src/lib/utils/calculate-fee.test.ts
import { describe, it, expect } from 'vitest';
import { calculateFee } from './calculate-fee';
describe('calculateFee', () => {
it('calculates 1% fee correctly', () => {
expect(calculateFee(1000, 1)).toBe(10);
});
it('handles zero amount', () => {
expect(calculateFee(0, 5)).toBe(0);
});
});
cd packages/web
pnpm test # Run all tests (watch mode)
pnpm test -- --run # Run once and exit
pnpm test:watch # Watch mode
pnpm test -- --run --grep "fee" # Filter by name
Repo note:
@zero-finance/webVitest discovers tests underpackages/web/src/test/**/*.test.ts. Put new tests there (or update Vitest config) so they get picked up.
// packages/web/src/server/routers/earn/get-balance.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createTestContext } from '@/test/context';
import { earnRouter } from './index';
describe('earn.getBalance', () => {
let ctx: ReturnType<typeof createTestContext>;
beforeEach(() => {
ctx = createTestContext({
user: { privyDid: 'test-user-did' },
workspaceId: 'test-workspace-id',
});
});
it('returns balance for valid user', async () => {
const caller = earnRouter.createCaller(ctx);
const result = await caller.getBalance({ chainId: 8453 });
expect(result).toHaveProperty('balance');
expect(typeof result.balance).toBe('string');
});
});
// Mock Privy
vi.mock('@privy-io/server-auth', () => ({
PrivyClient: vi.fn().mockImplementation(() => ({
getUser: vi.fn().mockResolvedValue({ id: 'test-user' }),
})),
}));
// Mock Database
vi.mock('@/db', () => ({
db: {
query: {
userSafes: {
findFirst: vi.fn().mockResolvedValue({
safeAddress: '0x1234...',
chainId: 8453,
}),
},
},
},
}));
For tests that need a real database:
// packages/web/src/test/setup-db.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
export async function createTestDb() {
// Use a test-specific database
const sql = neon(process.env.TEST_DATABASE_URL!);
return drizzle(sql);
}
# 1. Push your branch
git push -u origin feat/my-feature
# 2. Wait for deployment
LATEST=$(vercel ls --scope prologe 2>/dev/null | head -1)
vercel inspect "$LATEST" --scope prologe --wait --timeout 5m
# 3. Test on preview URL
# Use Chrome MCP or manual testing
Load the test-staging-branch skill for:
skill("test-staging-branch")
// packages/web/tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test('user can view dashboard balance', async ({ page }) => {
// Login would use test fixtures
await page.goto('/dashboard');
await expect(page.getByText('Total Balance')).toBeVisible();
await expect(page.getByTestId('balance-amount')).toBeVisible();
});
cd packages/web
pnpm exec playwright test
pnpm exec playwright test --ui # Interactive mode
// BAD - Hard to test
export async function getBalance() {
const safe = await db.query.userSafes.findFirst({...});
const balance = await fetch(`https://api.example.com/balance/${safe.address}`);
return balance;
}
// GOOD - Testable
export async function getBalance(
deps: {
getSafe: () => Promise<UserSafe>,
fetchBalance: (address: string) => Promise<string>,
}
) {
const safe = await deps.getSafe();
const balance = await deps.fetchBalance(safe.address);
return balance;
}
// BAD - Logic mixed with I/O
export async function processTransfer(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
await db.insert(transfers).values({ amount, fee, total });
return { amount, fee, total };
}
// GOOD - Pure function extractable
export function calculateTransferFees(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
return { amount, fee, total };
}
export async function processTransfer(amount: number) {
const calculated = calculateTransferFees(amount);
await db.insert(transfers).values(calculated);
return calculated;
}
// Now calculateTransferFees is easily unit testable!
// Add data-testid for E2E tests
<div data-testid="balance-card">
<span data-testid="balance-amount">{balance}</span>
</div>
Expose internal state via API routes that are:
// packages/web/src/app/api/test/state/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
// Only in development
if (process.env.NODE_ENV === 'production') {
return NextResponse.json({ error: 'Not available' }, { status: 403 });
}
// Return internal state for testing
return NextResponse.json({
safesCount: await db.select().from(userSafes).count(),
// ... other debug info
});
}
# .env.test - Test-specific config
DATABASE_URL="postgres://test:test@localhost:5432/test_db"
PRIVY_APP_ID="test-app-id"
# ... mocked values
Create reusable test helpers:
// packages/web/src/test/fixtures.ts
export const testUser = {
privyDid: 'did:privy:test-user',
email: 'test@example.com',
};
export const testSafe = {
address: '0x1234567890123456789012345678901234567890',
chainId: 8453,
};
// packages/web/src/test/context.ts
export function createTestContext(overrides = {}) {
return {
user: testUser,
workspaceId: 'test-workspace',
db: mockDb,
...overrides,
};
}
Before considering a feature "done":
[ ] Unit tests for pure functions
[ ] Integration tests for tRPC procedures
[ ] Mocks for external services
[ ] Test IDs in UI components
[ ] Manual test on staging (if applicable)
[ ] E2E test for critical paths only
// BAD - Tests internal state
expect(component.state.isLoading).toBe(false);
// GOOD - Tests observable behavior
expect(screen.getByText('Loading...')).not.toBeVisible();
// BAD - Mock everything
vi.mock('@/db');
vi.mock('@/lib/api');
vi.mock('@/hooks/use-user');
// ... 10 more mocks
// GOOD - Mock only external boundaries
vi.mock('@/lib/external-api'); // Third-party only
// BAD - E2E for simple validation
test('email validation shows error', async ({ page }) => {
// This should be a unit test!
});
// GOOD - E2E for critical flows only
test('user can complete payment flow', async ({ page }) => {
// Multi-step, multi-service flow
});
| Scenario | Skill to Use |
|---|---|
| Testing fails on staging | test-staging-branch |
| Need to debug prod data | debug prod issues |
| After completing tests | skill-reinforcement |
| Need Chrome automation | chrome-devtools-mcp |
Add new learnings here as they're discovered
params as a Promise; await params before reading slug to avoid 404s in local CLI testing.wallet_index and create_direct_signer; omit defaults and only send those fields when explicitly provided.# Unit tests
pnpm --filter @zero-finance/web test
# Watch mode
pnpm --filter @zero-finance/web test:watch
# E2E tests
pnpm --filter @zero-finance/web exec playwright test
# Type check (catches many bugs)
pnpm typecheck
# Lint
pnpm lint