Skip to content

Commit be50c6a

Browse files
committed
Merge branch 'docs-v2' into state-refactor
2 parents 8b6fb70 + 15537ae commit be50c6a

29 files changed

Lines changed: 2342 additions & 91 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Sveltest Helper
2+
3+
**Comprehensive testing tools and examples for building robust Svelte
4+
5 applications**
5+
6+
This skill provides a complete testing framework for SvelteKit
7+
projects using vitest-browser-svelte. It includes patterns, best
8+
practices, and real-world examples for client-side, server-side, and
9+
SSR testing.
10+
11+
## What You'll Find Here
12+
13+
-**Foundation First methodology** - Plan comprehensive test
14+
coverage before coding
15+
-**Real browser testing** - Using vitest-browser-svelte with
16+
Playwright
17+
-**Client-Server alignment** - Test with real FormData/Request
18+
objects
19+
-**Svelte 5 runes patterns** - Proper use of untrack(), $derived,
20+
and $effect
21+
-**Common pitfalls solved** - Strict mode, form submission,
22+
accessibility
23+
-**Production-ready examples** - Copy-paste test templates
24+
25+
## For Developers
26+
27+
Use this as a quick reference guide when writing tests:
28+
29+
- Browse `SKILL.md` for quick patterns and reminders
30+
- Check `references/detailed-guide.md` for comprehensive examples
31+
- Follow the "Unbreakable Rules" to avoid common mistakes
32+
33+
## For AI Assistants
34+
35+
This skill is automatically invoked by Claude Code when working with
36+
tests in SvelteKit projects. It provides context-aware guidance for
37+
creating and maintaining high-quality tests.
38+
39+
## Structure
40+
41+
- **SKILL.md** - Quick reference for common patterns
42+
- **references/detailed-guide.md** - Complete testing guide with
43+
examples
44+
45+
## Quick Start
46+
47+
```bash
48+
# Run all tests
49+
pnpm test
50+
51+
# Run specific test types
52+
pnpm test:client # Browser component tests
53+
pnpm test:server # API and server logic tests
54+
pnpm test:ssr # Server-side rendering tests
55+
56+
# Generate coverage
57+
pnpm coverage
58+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
name: svelte-testing
3+
# prettier-ignore
4+
description: Fix and create Svelte 5 tests with vitest-browser-svelte and Playwright. Use when fixing broken tests, debugging failures, writing unit/SSR/e2e tests, or working with vitest/Playwright.
5+
---
6+
7+
# Svelte Testing
8+
9+
## Quick Start
10+
11+
```typescript
12+
// Client-side component test (.svelte.test.ts)
13+
import { render } from 'vitest-browser-svelte';
14+
import { expect } from 'vitest';
15+
import Button from './button.svelte';
16+
17+
test('button click increments counter', async () => {
18+
const { page } = render(Button);
19+
const button = page.getByRole('button', { name: /click me/i });
20+
21+
await button.click();
22+
await expect.element(button).toHaveTextContent('Clicked: 1');
23+
});
24+
```
25+
26+
## Core Principles
27+
28+
- **Always use locators**: `page.getBy*()` methods, never containers
29+
- **Multiple elements**: Use `.first()`, `.nth()`, `.last()` to avoid
30+
strict mode violations
31+
- **Use untrack()**: When accessing `$derived` values in tests
32+
- **Real API objects**: Test with FormData/Request, minimal mocking
33+
34+
## Reference Files
35+
36+
- [core-principles](references/core-principles.md) |
37+
[foundation-first](references/foundation-first.md) |
38+
[client-examples](references/client-examples.md)
39+
- [server-ssr-examples](references/server-ssr-examples.md) |
40+
[critical-patterns](references/critical-patterns.md)
41+
- [client-server-alignment](references/client-server-alignment.md) |
42+
[troubleshooting](references/troubleshooting.md)
43+
44+
## Notes
45+
46+
- Never click SvelteKit form submit buttons - Always use
47+
`await expect.element()`
48+
- Test files: `.svelte.test.ts` (client), `.ssr.test.ts` (SSR),
49+
`server.test.ts` (API)
50+
51+
<!--
52+
PROGRESSIVE DISCLOSURE GUIDELINES:
53+
- Keep this file ~50 lines total (max ~150 lines)
54+
- Use 1-2 code blocks only (recommend 1)
55+
- Keep description <200 chars for Level 1 efficiency
56+
- Move detailed docs to references/ for Level 3 loading
57+
- This is Level 2 - quick reference ONLY, not a manual
58+
-->
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
## Complete Examples
2+
3+
### Example 1: Client-Side Component Test
4+
5+
Real browser testing with user interactions:
6+
7+
```typescript
8+
// button.svelte.test.ts
9+
import { render } from 'vitest-browser-svelte';
10+
import { test, expect, describe } from 'vitest';
11+
import { userEvent } from '@vitest/browser/context';
12+
import Button from './button.svelte';
13+
14+
describe('Button Component', () => {
15+
test('increments counter on click', async () => {
16+
const { page } = render(Button, { props: { label: 'Click me' } });
17+
18+
const button = page.getByRole('button', { name: /click me/i });
19+
20+
await userEvent.click(button);
21+
await expect.element(button).toHaveTextContent('Clicked: 1');
22+
23+
await userEvent.click(button);
24+
await expect.element(button).toHaveTextContent('Clicked: 2');
25+
});
26+
27+
test('supports keyboard interaction', async () => {
28+
render(Button, { props: { label: 'Press me' } });
29+
30+
const button = page.getByRole('button', { name: /press me/i });
31+
await button.focus();
32+
await userEvent.keyboard('{Enter}');
33+
34+
await expect.element(button).toHaveTextContent('Clicked: 1');
35+
});
36+
37+
test('handles multiple buttons with .first()', async () => {
38+
render(ButtonGroup); // Renders multiple buttons
39+
40+
// Handle multiple buttons explicitly
41+
const firstButton = page.getByRole('button').first();
42+
const secondButton = page.getByRole('button').nth(1);
43+
44+
await firstButton.click();
45+
await expect.element(firstButton).toHaveTextContent('Clicked: 1');
46+
47+
await secondButton.click();
48+
await expect
49+
.element(secondButton)
50+
.toHaveTextContent('Clicked: 1');
51+
});
52+
});
53+
```
54+
55+
### Example 2: Testing Svelte 5 Runes
56+
57+
```typescript
58+
// counter.svelte.test.ts
59+
import { render } from 'vitest-browser-svelte';
60+
import { test, expect } from 'vitest';
61+
import { untrack, flushSync } from 'svelte';
62+
import Counter from './counter.svelte';
63+
64+
test('$state and $derived reactivity', async () => {
65+
const { component } = render(Counter);
66+
67+
// Access $state value directly
68+
expect(component.count).toBe(0);
69+
70+
// Update state
71+
component.increment();
72+
73+
// Force synchronous update
74+
flushSync(() => {});
75+
76+
// Access $derived value with untrack
77+
const doubled = untrack(() => component.doubled);
78+
expect(doubled).toBe(2);
79+
});
80+
81+
test('form validation lifecycle', async () => {
82+
const { component } = render(FormComponent);
83+
84+
// Initially valid (no validation run yet)
85+
expect(untrack(() => component.isFormValid())).toBe(true);
86+
87+
// Trigger validation
88+
component.validateAllFields();
89+
90+
// Now invalid (empty required fields)
91+
expect(untrack(() => component.isFormValid())).toBe(false);
92+
93+
// Fix validation errors
94+
component.email.value = 'test@example.com';
95+
component.validateAllFields();
96+
97+
// Valid again
98+
expect(untrack(() => component.isFormValid())).toBe(true);
99+
});
100+
```
101+
102+
### Example 3: Server-Side API Test
103+
104+
Test with real FormData/Request objects:
105+
106+
```typescript
107+
// api/users/server.test.ts
108+
import { test, expect, describe, vi } from 'vitest';
109+
import { POST } from './+server';
110+
import * as database from '$lib/server/database';
111+
112+
vi.mock('$lib/server/database');
113+
114+
describe('POST /api/users', () => {
115+
test('creates user with valid data', async () => {
116+
// Mock only external services
117+
vi.mocked(database.createUser).mockResolvedValue({
118+
id: '123',
119+
email: 'user@example.com',
120+
});
121+
122+
// Use real FormData
123+
const formData = new FormData();
124+
formData.append('email', 'user@example.com');
125+
formData.append('password', 'securepass123');
126+
127+
// Use real Request object
128+
const request = new Request('http://localhost/api/users', {
129+
method: 'POST',
130+
body: formData,
131+
});
132+
133+
const response = await POST({ request });
134+
const data = await response.json();
135+
136+
expect(response.status).toBe(201);
137+
expect(data.email).toBe('user@example.com');
138+
expect(database.createUser).toHaveBeenCalledWith({
139+
email: 'user@example.com',
140+
password: 'securepass123',
141+
});
142+
});
143+
144+
test('rejects invalid email format', async () => {
145+
const formData = new FormData();
146+
formData.append('email', 'invalid-email');
147+
formData.append('password', 'pass123');
148+
149+
const request = new Request('http://localhost/api/users', {
150+
method: 'POST',
151+
body: formData,
152+
});
153+
154+
const response = await POST({ request });
155+
const data = await response.json();
156+
157+
expect(response.status).toBe(400);
158+
expect(data.errors.email).toBeDefined();
159+
expect(database.createUser).not.toHaveBeenCalled();
160+
});
161+
162+
test('handles missing required fields', async () => {
163+
const formData = new FormData();
164+
// Missing email and password
165+
166+
const request = new Request('http://localhost/api/users', {
167+
method: 'POST',
168+
body: formData,
169+
});
170+
171+
const response = await POST({ request });
172+
const data = await response.json();
173+
174+
expect(response.status).toBe(400);
175+
expect(data.errors.email).toBeDefined();
176+
expect(data.errors.password).toBeDefined();
177+
});
178+
});
179+
```
180+
181+
---
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
## Client-Server Alignment
2+
3+
### The Problem
4+
5+
Heavy mocking in server tests can hide client-server mismatches that
6+
only appear in production.
7+
8+
### The Solution
9+
10+
Use real `FormData` and `Request` objects. Only mock external services
11+
(database, APIs).
12+
13+
```typescript
14+
// ❌ BRITTLE APPROACH
15+
const mockRequest = {
16+
formData: vi.fn().mockResolvedValue({
17+
get: vi.fn((key) => {
18+
if (key === 'email') return 'test@example.com';
19+
if (key === 'password') return 'pass123';
20+
}),
21+
}),
22+
};
23+
// This passes even if real FormData API differs!
24+
25+
// ✅ ROBUST APPROACH
26+
const formData = new FormData();
27+
formData.append('email', 'test@example.com');
28+
formData.append('password', 'pass123');
29+
30+
const request = new Request('http://localhost/register', {
31+
method: 'POST',
32+
body: formData,
33+
});
34+
35+
// Only mock external services
36+
vi.mocked(database.createUser).mockResolvedValue({
37+
id: '123',
38+
email: 'test@example.com',
39+
});
40+
41+
const response = await POST({ request });
42+
```
43+
44+
### Shared Validation Logic
45+
46+
Use the same validation on client and server:
47+
48+
```typescript
49+
// lib/validation.ts
50+
export function validateEmail(email: string) {
51+
if (!email) return 'Email is required';
52+
if (!email.includes('@')) return 'Invalid email format';
53+
return null;
54+
}
55+
56+
// Component test
57+
import { validateEmail } from '$lib/validation';
58+
test('validates email', () => {
59+
expect(validateEmail('')).toBe('Email is required');
60+
expect(validateEmail('invalid')).toBe('Invalid email format');
61+
expect(validateEmail('test@example.com')).toBe(null);
62+
});
63+
64+
// Server test - same validation!
65+
const emailError = validateEmail(formData.get('email'));
66+
if (emailError) {
67+
return json({ errors: { email: emailError } }, { status: 400 });
68+
}
69+
```
70+
71+
---

0 commit comments

Comments
 (0)