スキル一覧に戻る

testing-patterns

vassovass / scl-v3

1🍴 0📅 2026年1月18日

Testing patterns for Next.js App Router with Supabase. Use when adding tests, verifying fixes, or preventing regressions. Keywords: test, testing, jest, vitest, unit test, integration test, mock, Supabase mock, playwright, e2e.

SKILL.md

---
name: testing-patterns
description: Testing patterns for Next.js App Router with Supabase. Use when adding tests, verifying fixes, or preventing regressions. Keywords: test, testing, jest, vitest, unit test, integration test, mock, Supabase mock, playwright, e2e.
compatibility: Antigravity, Claude Code, Cursor
metadata:
  version: "4.0"
  project: "stepleague"
  last_updated: "2026-01-18"
---

# Testing Patterns Skill

## Overview

StepLeague has two test suites:
- **Vitest** (unit/integration): 1,101+ tests for hooks, components, utilities
- **Playwright** (E2E): 63 tests for full user flows against live site

---

## Quick Start

```bash
# Unit/Integration Tests (Vitest)
npm run test              # Run all
npm run test:watch        # Watch mode
npm run test:coverage     # Coverage report

# E2E Tests (Playwright)
npm run test:e2e          # Run all (headless)
npm run test:e2e:headed   # Visible browser
npm run test:e2e:ui       # Playwright UI
```

---

## User Guidance (Show This When Asked About Tests)

> [!NOTE]
> When the user asks "what can tests do?", "how do I use tests?", or similar questions about testing capabilities, show them this section.

### Is it Automatic?

**Currently: No.** Tests don't run automatically on changes. Options:
1. **Watch Mode** (`npm run test:watch`) - Leave running while coding; tests re-run on save
2. **CI/CD** (not yet configured) - Could add GitHub Actions to run on push

### When to Ask for Tests

**Ask to RUN tests when:**
- Finished implementing a feature
- Debugging an issue (catch regressions)
- Before committing/deploying

**Ask to WRITE tests when:**
- Fixed a bug (prevents recurrence)
- Added new API route or complex component
- Want to verify specific behavior

### Example Prompts

| Prompt | What happens |
|--------|--------------|
| "Run the tests" | Runs `npm test` and shows results |
| "Write a test for [feature]" | Creates test file following skill patterns |
| "Check test coverage" | Runs coverage report |
| "Add tests for proxy claim" | Writes comprehensive tests for that feature |

---

## Project Testing Stack

| Tool | Purpose | Installed |
|------|---------| --------- |
| **Vitest** | Test runner (faster than Jest for Vite/Next) | ✅ |
| **React Testing Library** | Component testing | ✅ |
| **MSW** | API mocking | ✅ |
| **jsdom** | Browser environment simulation | ✅ |

---

## Test File Structure

```
src/
├── __mocks__/
│   └── supabase.ts          # Mock factories (use this!)
├── lib/
│   └── auth/
│       └── __tests__/
│           └── sessionCache.test.ts
├── app/
│   └── api/
│       └── proxy-claim/
│           └── __tests__/
│               └── route.test.ts
```

---

## Pattern 1: Session Cache Testing (REAL EXAMPLE)

From `src/lib/auth/__tests__/sessionCache.test.ts`:

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setCachedSession, getCachedSession, clearCachedSession } from '../sessionCache';

describe('sessionCache', () => {
  beforeEach(() => {
    clearCachedSession();
  });

  it('stores session when all params provided', () => {
    const futureTime = Math.floor(Date.now() / 1000) + 3600;
    setCachedSession('token-123', 'user-456', futureTime);
    
    const cached = getCachedSession();
    expect(cached).not.toBeNull();
    expect(cached?.accessToken).toBe('token-123');
  });

  it('returns null when session expires within 60s buffer', () => {
    const soonTime = Math.floor(Date.now() / 1000) + 30;
    setCachedSession('token-123', 'user-456', soonTime);
    
    expect(getCachedSession()).toBeNull();
  });
});
```

---

## Pattern 2: Using Mock Factories (REAL EXAMPLE)

From `src/__mocks__/supabase.ts`:

```typescript
import { vi } from 'vitest';

// Create mock user
export const createMockUser = (overrides = {}) => ({
  id: 'user-123',
  email: 'test@example.com',
  created_at: '2026-01-01T00:00:00Z',
  ...overrides,
});

// Create mock proxy
export const createMockProxy = (overrides = {}) => ({
  id: 'proxy-456',
  display_name: 'Proxy User',
  is_proxy: true,
  managed_by: 'user-123',
  invite_code: 'CLAIM123',
  claims_remaining: 1,
  ...overrides,
});

// Create chainable Supabase client mock
export const createMockSupabaseClient = () => ({
  from: vi.fn(() => ({
    select: vi.fn().mockReturnThis(),
    eq: vi.fn().mockReturnThis(),
    single: vi.fn().mockResolvedValue({ data: null, error: null }),
  })),
  auth: {
    getUser: vi.fn().mockResolvedValue({ data: { user: null }, error: null }),
    signOut: vi.fn().mockResolvedValue({ error: null }),
  },
});

// Usage in test:
// import { createMockSupabaseClient, createMockProxy } from '@/__mocks__/supabase';
```

---

## Pattern 3: Auth Level Testing (REAL EXAMPLE)

From `src/lib/api/__tests__/handler.test.ts`:

```typescript
describe('League-level auth', () => {
  type Role = 'member' | 'admin' | 'owner';
  
  const roleHasAccess = (userRole: Role, requiredLevel: string): boolean => {
    const roleHierarchy = { member: 1, admin: 2, owner: 3 };
    const requiredHierarchy = { 
      league_member: 1, league_admin: 2, league_owner: 3 
    };
    return roleHierarchy[userRole] >= requiredHierarchy[requiredLevel];
  };

  it('member cannot access league_admin routes', () => {
    expect(roleHasAccess('member', 'league_admin')).toBe(false);
  });

  it('owner can access all league routes', () => {
    expect(roleHasAccess('owner', 'league_member')).toBe(true);
    expect(roleHasAccess('owner', 'league_admin')).toBe(true);
    expect(roleHasAccess('owner', 'league_owner')).toBe(true);
  });
});
```

---

## Pattern 4: Proxy Claim Testing (REAL EXAMPLE)

From `src/app/api/proxy-claim/__tests__/route.test.ts`:

```typescript
describe('POST - Execute Claim', () => {
  it('prevents user from claiming their own proxy', () => {
    const managerId = 'user-123';
    const claimingUserId = 'user-123';
    
    const isOwnProxy = managerId === claimingUserId;
    expect(isOwnProxy).toBe(true);
    // Route should return 400
  });

  describe('Merge Strategy', () => {
    it('uses proxy display_name with keep_proxy_profile strategy', () => {
      const proxy = createMockProxy({ display_name: 'Proxy Name' });
      const user = createMockProfile({ display_name: 'User Name' });
      const strategy = 'keep_proxy_profile';
      
      const finalName = strategy === 'keep_proxy_profile' 
        ? proxy.display_name 
        : user.display_name;
      
      expect(finalName).toBe('Proxy Name');
    });
  });
});
```

---

## Testing Priorities (Updated)

Based on 10-day commit analysis, priority testing areas:

1. **Auth Session Cache** - Bypasses Web Locks deadlocks ⚠️ Critical
2. **Proxy Claims** - Complex data transfer logic ⚠️ Critical
3. **API Handler Auth** - Role-based access
4. **Hooks with URL state** - Prevent infinite loops

---

## Configuration Files

| File | Purpose |
|------|---------|
| `vitest.config.ts` | Vitest setup with path aliases, coverage thresholds |
| `vitest.setup.ts` | Global mocks (next/navigation, next/headers, matchMedia) |
| `src/__mocks__/supabase.ts` | Supabase client and data factories |

---

## Test-Driven Bug Fixing

When fixing a bug:

1. **Write failing test first** - Reproduces the bug
2. **Fix the code** - Make test pass
3. **Commit both** - Test prevents regression

```typescript
// Example: Testing the 60-second token expiry buffer
it('returns null when session expires within 60s buffer', () => {
  // This test catches the bug where tokens were used too close to expiry
  const soonTime = Math.floor(Date.now() / 1000) + 30;
  setCachedSession('token-123', 'user-456', soonTime);
  expect(getCachedSession()).toBeNull();
});
```

---

## Quick Links to Test Files

### API Route Tests (Phase A)
| Test File | Purpose | Tests |
|-----------|---------|-------|
| `src/app/api/submissions/__tests__/route.test.ts` | Submissions CRUD, proxy context | 98 |
| `src/app/api/leagues/[id]/__tests__/route.test.ts` | League CRUD, authorization | 52 |
| `src/app/api/admin/branding/__tests__/route.test.ts` | Brand customization | 36 |
| `src/app/api/high-fives/__tests__/route.test.ts` | High-five toggle | 29 |
| `src/app/api/admin/settings/__tests__/route.test.ts` | Feature flags | 16 |

### Hook Tests (Phase B)
| Test File | Purpose | Tests |
|-----------|---------|-------|
| `src/hooks/__tests__/useExport.test.ts` | CSV export | 47 |
| `src/hooks/__tests__/useImport.test.ts` | CSV import | 46 |
| `src/hooks/__tests__/useOfflineSync.test.ts` | Queue sync | 43 |
| `src/hooks/__tests__/useSubmissionStatus.test.ts` | Status tracking | 41 |
| `src/hooks/__tests__/useFetch.test.ts` | Data fetching | 38 |
| `src/hooks/__tests__/useConflictCheck.test.ts` | Duplicate detection | 38 |
| `src/hooks/__tests__/usePreferences.test.ts` | User settings | 35 |
| `src/hooks/__tests__/useFeatureFlag.test.ts` | Flag evaluation | 27 |

### Utility Tests (Phase C)
| Test File | Purpose | Tests |
|-----------|---------|-------|
| `src/lib/__tests__/badges.test.ts` | Badge config | 73 |
| `src/lib/cache/__tests__/serverCache.test.ts` | Cache with circuit breaker | 49 |
| `src/lib/__tests__/errors.test.ts` | Error handling | 45 |
| `src/lib/export/__tests__/csvParser.test.ts` | CSV parsing | 41 |

### Component Tests (Phase D)
| Test File | Purpose |
|-----------|---------|
| `src/components/encouragement/__tests__/HighFiveButton.test.tsx` | High-five UI |
| `src/components/forms/__tests__/SubmissionForm.test.tsx` | Submission form |
| `src/components/analytics/__tests__/CookieConsent.test.tsx` | Cookie consent |
| `src/components/auth/__tests__/ProfileSwitcher.test.tsx` | Profile switcher |
| `src/components/admin/__tests__/KanbanBoard.test.tsx` | Kanban board |

### Original Tests
| Test File | Purpose | Tests |
|-----------|---------|-------|
| `src/lib/__tests__/analytics.test.ts` | Dual-tracking (GA4+PostHog) | 44 |
| `src/lib/__tests__/auth-middleware.test.ts` | Route protection | 35 |
| `src/lib/__tests__/comparisons.test.ts` | SEO comparison pages | 20 |
| `src/lib/auth/__tests__/sessionCache.test.ts` | Token caching | 11 |
| `src/lib/api/__tests__/handler.test.ts` | Auth levels | 22 |

---

## Playwright E2E Testing (NEW)

E2E tests run against live `stepleague.app`. Uses credentials from `.env.local`.

### Test Files (9 suites, 63 tests)

| File | Tests | Purpose |
|------|-------|---------|
| `e2e/auth.spec.ts` | 6 | Login/logout, protected redirects |
| `e2e/homepage.spec.ts` | 3 | Public pages load correctly |
| `e2e/league.spec.ts` | 3 | Create, verify, delete leagues |
| `e2e/protected-routes.spec.ts` | 13 | Auth gating, reset page |
| `e2e/navigation.spec.ts` | 6 | Header, footer, dashboard nav |
| `e2e/form-validation.spec.ts` | 7 | Form validation, maxlength |
| `e2e/error-handling.spec.ts` | 5 | 404s, console errors |
| `e2e/ui-interactions.spec.ts` | 8 | Mobile, theme, focus |
| `e2e/user-flows.spec.ts` | 12 | Session persistence |

### Key Patterns

```typescript
// e2e/fixtures/auth.ts - Login helper
import { test as base, expect } from '@playwright/test';

export async function login(page: Page) {
  await page.goto('/sign-in');
  await page.fill('#email', process.env.SL_PW_LOGIN_TEST_USERNAME!);
  await page.fill('#password', process.env.SL_PW_LOGIN_TEST_PASSWORD!);
  await page.click('button[type="submit"]');
  await page.waitForURL(/\/dashboard/);
}

// Using in tests
import { test, expect, login } from './fixtures/auth';

test('can access dashboard', async ({ page }) => {
  await login(page);
  await page.goto('/dashboard');
  await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});
```

### Credentials Setup

```bash
# .env.local (DO NOT COMMIT)
SL_PW_LOGIN_TEST_USERNAME=your-test-email@example.com
SL_PW_LOGIN_TEST_PASSWORD=your-test-password
```

### Configuration

See [playwright.config.ts](file:///d:/Vasso/coding%20projects/SCL%20v3%20AG/scl-v3/playwright.config.ts):
- `baseURL`: stepleague.app
- `colorScheme`: dark
- `retries`: 2 on CI, 0 locally

---

## Related Skills

- `auth-patterns` - getUser vs getSession, deadlock avoidance
- `api-handler` - withApiHandler middleware patterns
- `react-debugging` - Use tests to prevent infinite render loops
- `analytics-tracking` - Event tracking tests in analytics.test.ts