Skip to content

Commit 5d46e4a

Browse files
authored
Merge branch 'main' into copilot/collapse-yesterdays-schedule
2 parents ec84eb0 + e1c9318 commit 5d46e4a

97 files changed

Lines changed: 2756 additions & 2766 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/main.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ const config: StorybookConfig = {
4444
},
4545
typescript: {
4646
check: false,
47-
reactDocgen: 'react-docgen-typescript',
48-
reactDocgenTypescriptOptions: {
49-
shouldExtractLiteralValuesFromEnum: true,
50-
propFilter: (prop) =>
51-
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
47+
reactDocgen: 'react-docgen',
48+
},
49+
build: {
50+
test: {
51+
disabledAddons: ['@storybook/addon-docs'],
5252
},
5353
},
5454
core: {

AGENTS.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,90 @@ When refactoring code:
342342
- Document any complex or non-obvious props
343343

344344
This structure ensures the codebase remains maintainable, testable, and scalable as the project grows.
345+
346+
## Testing Guidelines
347+
348+
The project uses **Vitest** with `@testing-library/react` for unit and integration tests, and **Storybook interaction tests** for component behavior.
349+
350+
### Test Philosophy
351+
352+
- **Test behavior, not implementation.** Assert on observable outcomes (return values, rendered output, side effects) rather than internal state or private methods.
353+
- **Every test must exercise real source code.** A test that only asserts on values defined within the test file itself is worthless — delete it. If a test doesn't import from `src/`, it's not testing the application.
354+
- **Prefer integration tests over isolated unit tests.** Test modules working together through their public APIs. Reserve heavy mocking for true external boundaries (network, database, third-party services).
355+
- **Tests are documentation.** A reader should understand what the code does by reading the test names and assertions.
356+
357+
### What to Test
358+
359+
- **Domain logic and data transformations** (e.g., validation functions, formatters, business rules).
360+
- **API route handlers** — test request/response contracts with realistic payloads.
361+
- **React components** — test user-visible behavior: rendering, interactions, conditional display. Use `@testing-library/react` queries (`getByRole`, `getByText`) over implementation details like CSS classes or component internals.
362+
- **Error handling paths** — verify that errors produce correct user-facing messages or status codes.
363+
- **Edge cases and boundary conditions** — empty arrays, null values, missing optional fields, maximum lengths.
364+
365+
### What NOT to Test
366+
367+
- **Type definitions and interfaces** — TypeScript already validates these at compile time.
368+
- **Simple pass-through functions** — if a function just calls another with the same arguments, the value of testing it is near zero.
369+
- **Third-party library internals** — trust that Sanity, NextAuth, tRPC work correctly. Mock them at the boundary and test your integration logic.
370+
- **Styling and layout** — use Storybook visual testing and Chromatic for visual regression instead.
371+
372+
### Writing Tests
373+
374+
```typescript
375+
// Test file location: __tests__/{path matching src/}
376+
// e.g., src/lib/proposal/validation.ts → __tests__/lib/proposal/validation.test.ts
377+
378+
// Use descriptive test names that explain the scenario and expected outcome
379+
describe('validateProposal', () => {
380+
it('should reject proposals without a title', () => { ... })
381+
it('should accept proposals with all required fields', () => { ... })
382+
})
383+
```
384+
385+
- **File naming:** `{module}.test.ts` or `{Component}.test.tsx`, mirroring the source path under `__tests__/`.
386+
- **Structure:** Use `describe` blocks to group by function or feature, `it` blocks for individual scenarios.
387+
- **Setup:** Use factory functions or test fixtures over complex `beforeEach` setup. Keep test data close to where it's used.
388+
- **Assertions:** Be specific. Prefer `toEqual` over `toBeTruthy`. Assert on error messages, not just that an error was thrown.
389+
- **Async:** Always `await` async operations. Never use `done` callbacks.
390+
391+
### Mocking
392+
393+
- **Mock at boundaries, not internally.** Mock external services (Sanity client, email provider, Slack API), not internal utility functions.
394+
- **Use `vi.mock()` for module-level mocks.** Vitest auto-hoists these, so standard ESM `import` works — no need for `require()` workarounds.
395+
- **Prefer dependency injection** where possible over module mocking.
396+
- **Avoid `Object.defineProperty` on `process.env`** — Vitest makes env properties non-configurable. Use direct assignment: `process.env.MY_VAR = 'value'`.
397+
- **Clean up mocks** with `vi.restoreAllMocks()` in `afterEach` or `beforeEach` to prevent test pollution.
398+
399+
### Environment Directives
400+
401+
Tests run in `node` environment by default. For component tests that need DOM APIs, add a docblock at the top of the file:
402+
403+
```typescript
404+
/**
405+
* @vitest-environment jsdom
406+
*/
407+
```
408+
409+
### Storybook Interaction Tests
410+
411+
For component behavior testing in context, prefer Storybook `play` functions using `storybook/test`:
412+
413+
```typescript
414+
import { expect, fn, userEvent, within } from 'storybook/test'
415+
416+
export const ClickTest: Story = {
417+
args: { onClick: fn() },
418+
play: async ({ canvasElement }) => {
419+
const canvas = within(canvasElement)
420+
await userEvent.click(canvas.getByRole('button'))
421+
},
422+
}
423+
```
424+
425+
These run in CI via `pnpm run storybook:test-ci` and complement unit tests by verifying components in a realistic rendering context.
426+
427+
### Performance
428+
429+
- **Avoid creating large data structures in tests.** Use `new Uint8Array(size)` instead of `new Array(size).fill('a').join('')` for large binary blobs — the latter is orders of magnitude slower.
430+
- **Keep tests fast.** The full suite should run in under 15 seconds. If a test needs more than 5 seconds, reconsider the approach.
431+
- **Use `vi.resetModules()` + dynamic `import()` for tests that need fresh module state** (e.g., environment variable–dependent config). Avoid `vi.isolateModules` which doesn't exist in Vitest.

__tests__/api/badge/badge-e2e.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { describe, it, expect } from '@jest/globals'
21
import { generateBadgeCredential } from '@/lib/badge/generator'
32
import { createTestConfiguration } from '@/lib/badge/config'
43
import { generateBadgeSVG } from '@/lib/badge/svg'

__tests__/api/cron/cleanup-orphaned-blobs.test.ts

Lines changed: 0 additions & 147 deletions
This file was deleted.

__tests__/api/cron/contract-reminders.test.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
11
/**
2-
* @jest-environment node
2+
* @vitest-environment node
33
*/
4-
import {
5-
describe,
6-
it,
7-
expect,
8-
jest,
9-
beforeEach,
10-
afterEach,
11-
} from '@jest/globals'
124
import { NextRequest } from 'next/server'
135

146
/* eslint-disable @typescript-eslint/no-explicit-any */
15-
const mockSanityFetch = jest.fn<(...args: any[]) => any>()
16-
const mockPatch = jest.fn<(...args: any[]) => any>()
17-
const mockSet = jest.fn<(...args: any[]) => any>()
18-
const mockCommit = jest.fn<(...args: any[]) => any>()
19-
const mockCreate = jest.fn<(...args: any[]) => any>()
20-
const mockResendSend = jest.fn<(...args: any[]) => any>()
21-
22-
jest.mock('@/lib/sanity/client', () => ({
7+
const mockSanityFetch = vi.fn<(...args: any[]) => any>()
8+
const mockPatch = vi.fn<(...args: any[]) => any>()
9+
const mockSet = vi.fn<(...args: any[]) => any>()
10+
const mockCommit = vi.fn<(...args: any[]) => any>()
11+
const mockCreate = vi.fn<(...args: any[]) => any>()
12+
const mockResendSend = vi.fn<(...args: any[]) => any>()
13+
14+
vi.mock('@/lib/sanity/client', () => ({
2315
clientWrite: {
2416
fetch: (...args: unknown[]) => mockSanityFetch(...args),
2517
patch: (...args: unknown[]) => mockPatch(...args),
2618
create: (...args: unknown[]) => mockCreate(...args),
2719
},
2820
}))
2921

30-
jest.mock('@/lib/time', () => ({
22+
vi.mock('@/lib/time', () => ({
3123
getCurrentDateTime: () => '2026-01-15T10:00:00Z',
3224
formatConferenceDateLong: (date: string) => date,
3325
}))
3426

35-
jest.mock('@/lib/email/config', () => ({
27+
vi.mock('@/lib/email/config', () => ({
3628
resend: {
3729
emails: {
3830
send: (...args: unknown[]) => mockResendSend(...args),
@@ -41,8 +33,8 @@ jest.mock('@/lib/email/config', () => ({
4133
retryWithBackoff: async (fn: () => Promise<unknown>) => fn(),
4234
}))
4335

44-
jest.mock('@/lib/email/contract-email', () => ({
45-
renderContractEmail: jest.fn<(...args: any[]) => any>().mockResolvedValue({
36+
vi.mock('@/lib/email/contract-email', () => ({
37+
renderContractEmail: vi.fn<(...args: any[]) => any>().mockResolvedValue({
4638
subject: 'Reminder: Sponsorship Agreement',
4739
react: null,
4840
}),
@@ -53,13 +45,13 @@ jest.mock('@/lib/email/contract-email', () => ({
5345
},
5446
}))
5547

56-
jest.mock('next/cache', () => ({
57-
unstable_noStore: jest.fn(),
48+
vi.mock('next/cache', () => ({
49+
unstable_noStore: vi.fn(),
5850
}))
5951

6052
describe('api/cron/contract-reminders', () => {
6153
beforeEach(() => {
62-
jest.clearAllMocks()
54+
vi.clearAllMocks()
6355
process.env.CRON_SECRET = 'test-cron-secret'
6456

6557
mockPatch.mockReturnValue({ set: mockSet })

0 commit comments

Comments
 (0)