@girardmedia/bootspring 1.2.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# Playwright E2E Testing Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for end-to-end testing with Playwright.
|
|
4
|
-
|
|
5
|
-
## Basic Test
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// tests/e2e/login.spec.ts
|
|
9
|
-
import { test, expect } from '@playwright/test'
|
|
10
|
-
|
|
11
|
-
test('user can log in', async ({ page }) => {
|
|
12
|
-
await page.goto('/login')
|
|
13
|
-
|
|
14
|
-
await page.fill('[name="email"]', 'test@example.com')
|
|
15
|
-
await page.fill('[name="password"]', 'password123')
|
|
16
|
-
await page.click('button[type="submit"]')
|
|
17
|
-
|
|
18
|
-
await expect(page).toHaveURL('/dashboard')
|
|
19
|
-
await expect(page.locator('h1')).toContainText('Welcome')
|
|
20
|
-
})
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Page Object Model
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
// tests/e2e/pages/login.page.ts
|
|
27
|
-
import { Page, Locator } from '@playwright/test'
|
|
28
|
-
|
|
29
|
-
export class LoginPage {
|
|
30
|
-
readonly page: Page
|
|
31
|
-
readonly emailInput: Locator
|
|
32
|
-
readonly passwordInput: Locator
|
|
33
|
-
readonly submitButton: Locator
|
|
34
|
-
readonly errorMessage: Locator
|
|
35
|
-
|
|
36
|
-
constructor(page: Page) {
|
|
37
|
-
this.page = page
|
|
38
|
-
this.emailInput = page.locator('[name="email"]')
|
|
39
|
-
this.passwordInput = page.locator('[name="password"]')
|
|
40
|
-
this.submitButton = page.locator('button[type="submit"]')
|
|
41
|
-
this.errorMessage = page.locator('[role="alert"]')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async goto() {
|
|
45
|
-
await this.page.goto('/login')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async login(email: string, password: string) {
|
|
49
|
-
await this.emailInput.fill(email)
|
|
50
|
-
await this.passwordInput.fill(password)
|
|
51
|
-
await this.submitButton.click()
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Usage in test
|
|
56
|
-
import { LoginPage } from './pages/login.page'
|
|
57
|
-
|
|
58
|
-
test('login with page object', async ({ page }) => {
|
|
59
|
-
const loginPage = new LoginPage(page)
|
|
60
|
-
await loginPage.goto()
|
|
61
|
-
await loginPage.login('test@example.com', 'password123')
|
|
62
|
-
|
|
63
|
-
await expect(page).toHaveURL('/dashboard')
|
|
64
|
-
})
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Authentication State
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// tests/e2e/auth.setup.ts
|
|
71
|
-
import { test as setup, expect } from '@playwright/test'
|
|
72
|
-
|
|
73
|
-
const authFile = 'playwright/.auth/user.json'
|
|
74
|
-
|
|
75
|
-
setup('authenticate', async ({ page }) => {
|
|
76
|
-
await page.goto('/login')
|
|
77
|
-
await page.fill('[name="email"]', process.env.TEST_EMAIL!)
|
|
78
|
-
await page.fill('[name="password"]', process.env.TEST_PASSWORD!)
|
|
79
|
-
await page.click('button[type="submit"]')
|
|
80
|
-
|
|
81
|
-
await expect(page).toHaveURL('/dashboard')
|
|
82
|
-
|
|
83
|
-
// Save auth state
|
|
84
|
-
await page.context().storageState({ path: authFile })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// playwright.config.ts
|
|
88
|
-
export default defineConfig({
|
|
89
|
-
projects: [
|
|
90
|
-
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
|
91
|
-
{
|
|
92
|
-
name: 'chromium',
|
|
93
|
-
use: {
|
|
94
|
-
storageState: 'playwright/.auth/user.json'
|
|
95
|
-
},
|
|
96
|
-
dependencies: ['setup']
|
|
97
|
-
}
|
|
98
|
-
]
|
|
99
|
-
})
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## API Mocking
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
// tests/e2e/dashboard.spec.ts
|
|
106
|
-
test('displays mocked data', async ({ page }) => {
|
|
107
|
-
// Mock API response
|
|
108
|
-
await page.route('/api/users', async (route) => {
|
|
109
|
-
await route.fulfill({
|
|
110
|
-
status: 200,
|
|
111
|
-
contentType: 'application/json',
|
|
112
|
-
body: JSON.stringify([
|
|
113
|
-
{ id: '1', name: 'Test User' }
|
|
114
|
-
])
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
await page.goto('/dashboard')
|
|
119
|
-
await expect(page.locator('text=Test User')).toBeVisible()
|
|
120
|
-
})
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Visual Regression
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// tests/e2e/visual.spec.ts
|
|
127
|
-
test('homepage visual regression', async ({ page }) => {
|
|
128
|
-
await page.goto('/')
|
|
129
|
-
|
|
130
|
-
// Full page screenshot
|
|
131
|
-
await expect(page).toHaveScreenshot('homepage.png', {
|
|
132
|
-
fullPage: true
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// Element screenshot
|
|
136
|
-
await expect(page.locator('header')).toHaveScreenshot('header.png')
|
|
137
|
-
})
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Mobile Testing
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// tests/e2e/mobile.spec.ts
|
|
144
|
-
import { devices } from '@playwright/test'
|
|
145
|
-
|
|
146
|
-
test.use({ ...devices['iPhone 13'] })
|
|
147
|
-
|
|
148
|
-
test('mobile navigation', async ({ page }) => {
|
|
149
|
-
await page.goto('/')
|
|
150
|
-
|
|
151
|
-
// Open mobile menu
|
|
152
|
-
await page.click('[aria-label="Open menu"]')
|
|
153
|
-
await expect(page.locator('nav')).toBeVisible()
|
|
154
|
-
})
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## When to Use
|
|
158
|
-
|
|
159
|
-
- User flows (login, checkout)
|
|
160
|
-
- Critical paths
|
|
161
|
-
- Cross-browser testing
|
|
162
|
-
- Visual regression
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# Snapshot Testing Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for snapshot testing with Vitest.
|
|
4
|
-
|
|
5
|
-
## Basic Snapshot
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// tests/components/Button.test.tsx
|
|
9
|
-
import { render } from '@testing-library/react'
|
|
10
|
-
import { Button } from '@/components/ui/Button'
|
|
11
|
-
|
|
12
|
-
it('renders correctly', () => {
|
|
13
|
-
const { container } = render(<Button>Click me</Button>)
|
|
14
|
-
expect(container).toMatchSnapshot()
|
|
15
|
-
})
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Inline Snapshots
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
// More readable for small outputs
|
|
22
|
-
it('formats currency', () => {
|
|
23
|
-
expect(formatCurrency(1234.56)).toMatchInlineSnapshot(`"$1,234.56"`)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('generates user display name', () => {
|
|
27
|
-
expect(getDisplayName({ firstName: 'John', lastName: 'Doe' }))
|
|
28
|
-
.toMatchInlineSnapshot(`"John Doe"`)
|
|
29
|
-
})
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Object Snapshots
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
// Snapshot complex objects
|
|
36
|
-
it('transforms API response', () => {
|
|
37
|
-
const response = {
|
|
38
|
-
id: '123',
|
|
39
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
40
|
-
user_name: 'johndoe'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
expect(transformResponse(response)).toMatchInlineSnapshot(`
|
|
44
|
-
{
|
|
45
|
-
"id": "123",
|
|
46
|
-
"createdAt": "2024-01-01T00:00:00.000Z",
|
|
47
|
-
"userName": "johndoe"
|
|
48
|
-
}
|
|
49
|
-
`)
|
|
50
|
-
})
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Serializers
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// vitest.config.ts
|
|
57
|
-
export default defineConfig({
|
|
58
|
-
test: {
|
|
59
|
-
snapshotSerializers: ['./tests/serializers/date.ts']
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
// tests/serializers/date.ts
|
|
64
|
-
export const serialize = (val: Date) => `Date<${val.toISOString()}>`
|
|
65
|
-
export const test = (val: unknown) => val instanceof Date
|
|
66
|
-
|
|
67
|
-
// Output in snapshots:
|
|
68
|
-
// Date<2024-01-01T00:00:00.000Z>
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Property Matchers
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
// Ignore dynamic values
|
|
75
|
-
it('creates user with generated id', async () => {
|
|
76
|
-
const user = await createUser({ email: 'test@example.com' })
|
|
77
|
-
|
|
78
|
-
expect(user).toMatchSnapshot({
|
|
79
|
-
id: expect.any(String),
|
|
80
|
-
createdAt: expect.any(Date)
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## File Snapshots
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
// Large output to separate files
|
|
89
|
-
it('generates config file', () => {
|
|
90
|
-
const config = generateConfig({
|
|
91
|
-
name: 'my-app',
|
|
92
|
-
features: ['auth', 'api', 'db']
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
expect(config).toMatchFileSnapshot('./snapshots/config.json')
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Update Snapshots
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
# Update all snapshots
|
|
103
|
-
npm test -- --update
|
|
104
|
-
|
|
105
|
-
# Update specific test file snapshots
|
|
106
|
-
npm test -- Button.test.tsx --update
|
|
107
|
-
|
|
108
|
-
# Interactive update (choose which to update)
|
|
109
|
-
npm test -- --watch
|
|
110
|
-
# Then press 'u' to update
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Snapshot Best Practices
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// DON'T: Snapshot too much
|
|
117
|
-
it('renders page', () => {
|
|
118
|
-
const { container } = render(<ComplexPage />)
|
|
119
|
-
expect(container).toMatchSnapshot() // Too large, hard to review
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// DO: Snapshot specific elements
|
|
123
|
-
it('renders header correctly', () => {
|
|
124
|
-
const { getByRole } = render(<ComplexPage />)
|
|
125
|
-
expect(getByRole('banner')).toMatchSnapshot()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// DO: Use inline snapshots for small outputs
|
|
129
|
-
it('formats date', () => {
|
|
130
|
-
expect(formatDate(new Date('2024-01-15')))
|
|
131
|
-
.toMatchInlineSnapshot(`"January 15, 2024"`)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// DON'T: Include dynamic data
|
|
135
|
-
it('renders user', () => {
|
|
136
|
-
const { container } = render(<User id={Math.random()} />) // Will always fail
|
|
137
|
-
expect(container).toMatchSnapshot()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// DO: Use stable test data
|
|
141
|
-
it('renders user', () => {
|
|
142
|
-
const { container } = render(<User id="test-id" createdAt={new Date(0)} />)
|
|
143
|
-
expect(container).toMatchSnapshot()
|
|
144
|
-
})
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## Component Snapshots
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// Snapshot multiple variants
|
|
151
|
-
describe('Button', () => {
|
|
152
|
-
const variants = ['primary', 'secondary', 'danger'] as const
|
|
153
|
-
const sizes = ['sm', 'md', 'lg'] as const
|
|
154
|
-
|
|
155
|
-
variants.forEach(variant => {
|
|
156
|
-
sizes.forEach(size => {
|
|
157
|
-
it(`renders ${variant} ${size}`, () => {
|
|
158
|
-
const { container } = render(
|
|
159
|
-
<Button variant={variant} size={size}>
|
|
160
|
-
Button
|
|
161
|
-
</Button>
|
|
162
|
-
)
|
|
163
|
-
expect(container.firstChild).toMatchSnapshot()
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
})
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## When to Use
|
|
171
|
-
|
|
172
|
-
- UI component output
|
|
173
|
-
- Serialized data structures
|
|
174
|
-
- Generated code/config
|
|
175
|
-
- API response transformations
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
# Vitest Testing Patterns
|
|
2
|
-
|
|
3
|
-
Battle-tested patterns for testing with Vitest in Next.js.
|
|
4
|
-
|
|
5
|
-
## Setup Configuration
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// vitest.config.ts
|
|
9
|
-
import { defineConfig } from 'vitest/config'
|
|
10
|
-
import react from '@vitejs/plugin-react'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
|
|
13
|
-
export default defineConfig({
|
|
14
|
-
plugins: [react()],
|
|
15
|
-
test: {
|
|
16
|
-
environment: 'jsdom',
|
|
17
|
-
globals: true,
|
|
18
|
-
setupFiles: ['./vitest.setup.ts'],
|
|
19
|
-
include: ['**/*.test.{ts,tsx}'],
|
|
20
|
-
coverage: {
|
|
21
|
-
provider: 'v8',
|
|
22
|
-
reporter: ['text', 'json', 'html'],
|
|
23
|
-
exclude: ['node_modules/', '*.config.*']
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
resolve: {
|
|
27
|
-
alias: {
|
|
28
|
-
'@': path.resolve(__dirname, './src')
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
// vitest.setup.ts
|
|
36
|
-
import '@testing-library/jest-dom/vitest'
|
|
37
|
-
import { cleanup } from '@testing-library/react'
|
|
38
|
-
import { afterEach, vi } from 'vitest'
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
cleanup()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// Mock next/navigation
|
|
45
|
-
vi.mock('next/navigation', () => ({
|
|
46
|
-
useRouter: () => ({
|
|
47
|
-
push: vi.fn(),
|
|
48
|
-
replace: vi.fn(),
|
|
49
|
-
back: vi.fn()
|
|
50
|
-
}),
|
|
51
|
-
usePathname: () => '/',
|
|
52
|
-
useSearchParams: () => new URLSearchParams()
|
|
53
|
-
}))
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Unit Testing Functions
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
// lib/utils.test.ts
|
|
60
|
-
import { describe, it, expect } from 'vitest'
|
|
61
|
-
import { formatPrice, calculateDiscount, validateEmail } from './utils'
|
|
62
|
-
|
|
63
|
-
describe('formatPrice', () => {
|
|
64
|
-
it('formats cents to dollars', () => {
|
|
65
|
-
expect(formatPrice(1000)).toBe('$10.00')
|
|
66
|
-
expect(formatPrice(1599)).toBe('$15.99')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('handles zero', () => {
|
|
70
|
-
expect(formatPrice(0)).toBe('$0.00')
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('calculateDiscount', () => {
|
|
75
|
-
it('applies percentage discount', () => {
|
|
76
|
-
expect(calculateDiscount(100, 10)).toBe(90)
|
|
77
|
-
expect(calculateDiscount(50, 50)).toBe(25)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('returns original price for zero discount', () => {
|
|
81
|
-
expect(calculateDiscount(100, 0)).toBe(100)
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
describe('validateEmail', () => {
|
|
86
|
-
it('accepts valid emails', () => {
|
|
87
|
-
expect(validateEmail('user@example.com')).toBe(true)
|
|
88
|
-
expect(validateEmail('user+tag@domain.co.uk')).toBe(true)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('rejects invalid emails', () => {
|
|
92
|
-
expect(validateEmail('invalid')).toBe(false)
|
|
93
|
-
expect(validateEmail('no@domain')).toBe(false)
|
|
94
|
-
expect(validateEmail('')).toBe(false)
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Testing React Components
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// components/button.test.tsx
|
|
103
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
104
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
105
|
-
import { Button } from './button'
|
|
106
|
-
|
|
107
|
-
describe('Button', () => {
|
|
108
|
-
it('renders with children', () => {
|
|
109
|
-
render(<Button>Click me</Button>)
|
|
110
|
-
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('calls onClick when clicked', () => {
|
|
114
|
-
const handleClick = vi.fn()
|
|
115
|
-
render(<Button onClick={handleClick}>Click</Button>)
|
|
116
|
-
|
|
117
|
-
fireEvent.click(screen.getByText('Click'))
|
|
118
|
-
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('is disabled when loading', () => {
|
|
122
|
-
render(<Button loading>Submit</Button>)
|
|
123
|
-
expect(screen.getByRole('button')).toBeDisabled()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('applies variant classes', () => {
|
|
127
|
-
render(<Button variant="destructive">Delete</Button>)
|
|
128
|
-
expect(screen.getByRole('button')).toHaveClass('bg-red-500')
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Testing Async Components
|
|
134
|
-
|
|
135
|
-
```typescript
|
|
136
|
-
// components/user-profile.test.tsx
|
|
137
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
138
|
-
import { render, screen, waitFor } from '@testing-library/react'
|
|
139
|
-
import { UserProfile } from './user-profile'
|
|
140
|
-
|
|
141
|
-
// Mock the fetch function
|
|
142
|
-
vi.mock('@/lib/api', () => ({
|
|
143
|
-
fetchUser: vi.fn()
|
|
144
|
-
}))
|
|
145
|
-
|
|
146
|
-
import { fetchUser } from '@/lib/api'
|
|
147
|
-
|
|
148
|
-
describe('UserProfile', () => {
|
|
149
|
-
it('shows loading state initially', () => {
|
|
150
|
-
vi.mocked(fetchUser).mockImplementation(() => new Promise(() => {}))
|
|
151
|
-
|
|
152
|
-
render(<UserProfile userId="123" />)
|
|
153
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('displays user data after loading', async () => {
|
|
157
|
-
vi.mocked(fetchUser).mockResolvedValue({
|
|
158
|
-
name: 'John Doe',
|
|
159
|
-
email: 'john@example.com'
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
render(<UserProfile userId="123" />)
|
|
163
|
-
|
|
164
|
-
await waitFor(() => {
|
|
165
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
166
|
-
expect(screen.getByText('john@example.com')).toBeInTheDocument()
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('shows error state on failure', async () => {
|
|
171
|
-
vi.mocked(fetchUser).mockRejectedValue(new Error('Failed'))
|
|
172
|
-
|
|
173
|
-
render(<UserProfile userId="123" />)
|
|
174
|
-
|
|
175
|
-
await waitFor(() => {
|
|
176
|
-
expect(screen.getByText('Error loading profile')).toBeInTheDocument()
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Testing Server Actions
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
// actions/posts.test.ts
|
|
186
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
187
|
-
import { createPost, deletePost } from './posts'
|
|
188
|
-
import { prisma } from '@/lib/db'
|
|
189
|
-
import { auth } from '@/lib/auth'
|
|
190
|
-
|
|
191
|
-
vi.mock('@/lib/db', () => ({
|
|
192
|
-
prisma: {
|
|
193
|
-
post: {
|
|
194
|
-
create: vi.fn(),
|
|
195
|
-
delete: vi.fn(),
|
|
196
|
-
findUnique: vi.fn()
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}))
|
|
200
|
-
|
|
201
|
-
vi.mock('@/lib/auth', () => ({
|
|
202
|
-
auth: vi.fn()
|
|
203
|
-
}))
|
|
204
|
-
|
|
205
|
-
vi.mock('next/cache', () => ({
|
|
206
|
-
revalidatePath: vi.fn()
|
|
207
|
-
}))
|
|
208
|
-
|
|
209
|
-
describe('createPost', () => {
|
|
210
|
-
beforeEach(() => {
|
|
211
|
-
vi.clearAllMocks()
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('creates post for authenticated user', async () => {
|
|
215
|
-
vi.mocked(auth).mockResolvedValue({ userId: 'user-123' })
|
|
216
|
-
vi.mocked(prisma.post.create).mockResolvedValue({
|
|
217
|
-
id: 'post-1',
|
|
218
|
-
title: 'Test Post',
|
|
219
|
-
authorId: 'user-123'
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
const formData = new FormData()
|
|
223
|
-
formData.set('title', 'Test Post')
|
|
224
|
-
|
|
225
|
-
const result = await createPost(formData)
|
|
226
|
-
|
|
227
|
-
expect(result.success).toBe(true)
|
|
228
|
-
expect(prisma.post.create).toHaveBeenCalledWith({
|
|
229
|
-
data: expect.objectContaining({
|
|
230
|
-
title: 'Test Post',
|
|
231
|
-
authorId: 'user-123'
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('returns error for unauthenticated user', async () => {
|
|
237
|
-
vi.mocked(auth).mockResolvedValue({ userId: null })
|
|
238
|
-
|
|
239
|
-
const formData = new FormData()
|
|
240
|
-
formData.set('title', 'Test')
|
|
241
|
-
|
|
242
|
-
const result = await createPost(formData)
|
|
243
|
-
|
|
244
|
-
expect(result.success).toBe(false)
|
|
245
|
-
expect(result.error).toBe('Not authenticated')
|
|
246
|
-
})
|
|
247
|
-
})
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## Testing Hooks
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
// hooks/use-debounce.test.ts
|
|
254
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
255
|
-
import { renderHook, act } from '@testing-library/react'
|
|
256
|
-
import { useDebounce } from './use-debounce'
|
|
257
|
-
|
|
258
|
-
describe('useDebounce', () => {
|
|
259
|
-
beforeEach(() => {
|
|
260
|
-
vi.useFakeTimers()
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
afterEach(() => {
|
|
264
|
-
vi.useRealTimers()
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('returns initial value immediately', () => {
|
|
268
|
-
const { result } = renderHook(() => useDebounce('hello', 500))
|
|
269
|
-
expect(result.current).toBe('hello')
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('debounces value changes', () => {
|
|
273
|
-
const { result, rerender } = renderHook(
|
|
274
|
-
({ value }) => useDebounce(value, 500),
|
|
275
|
-
{ initialProps: { value: 'hello' } }
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
rerender({ value: 'world' })
|
|
279
|
-
expect(result.current).toBe('hello') // Still old value
|
|
280
|
-
|
|
281
|
-
act(() => {
|
|
282
|
-
vi.advanceTimersByTime(500)
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
expect(result.current).toBe('world') // Now updated
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Running Tests
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
# Run all tests
|
|
294
|
-
npm test
|
|
295
|
-
|
|
296
|
-
# Watch mode
|
|
297
|
-
npm test -- --watch
|
|
298
|
-
|
|
299
|
-
# Coverage report
|
|
300
|
-
npm test -- --coverage
|
|
301
|
-
|
|
302
|
-
# Run specific file
|
|
303
|
-
npm test -- button.test.tsx
|
|
304
|
-
|
|
305
|
-
# Run tests matching pattern
|
|
306
|
-
npm test -- --grep "createPost"
|
|
307
|
-
```
|