@ranimontagna/agent-toolkit 0.1.10 → 0.1.11
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 +16 -2
- package/package.json +1 -1
- package/skills/backend/api/api-design/LICENSE +21 -0
- package/skills/backend/api/api-design/NOTICE.md +7 -0
- package/skills/backend/api/api-design/SKILL.md +523 -0
- package/skills/frontend/astro/astro-developer/LICENSE +59 -0
- package/skills/frontend/astro/astro-developer/NOTICE.md +7 -0
- package/skills/frontend/astro/astro-developer/SKILL.md +132 -0
- package/skills/frontend/astro/astro-developer/architecture.md +435 -0
- package/skills/frontend/astro/astro-developer/constraints.md +559 -0
- package/skills/frontend/astro/astro-developer/debugging.md +513 -0
- package/skills/frontend/astro/astro-developer/testing.md +973 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to writing and debugging tests in the Astro monorepo.
|
|
4
|
+
|
|
5
|
+
## Testing Philosophy
|
|
6
|
+
|
|
7
|
+
**Prefer unit tests over integration tests.** The codebase is being refactored to be more unit-testable.
|
|
8
|
+
|
|
9
|
+
**Guidelines:**
|
|
10
|
+
|
|
11
|
+
- **Default to unit tests**: Test pure functions and business logic with unit tests
|
|
12
|
+
- **Use integration tests sparingly**: Only for features that cannot be unit tested (e.g., virtual modules, full build pipeline)
|
|
13
|
+
- **Write unit-testable code**: Extract business logic from infrastructure, avoid tight coupling, use dependency injection
|
|
14
|
+
|
|
15
|
+
**When writing new code:**
|
|
16
|
+
|
|
17
|
+
1. Design code to be unit-testable (pure functions, minimal side effects)
|
|
18
|
+
2. Write unit tests first
|
|
19
|
+
3. Only use integration tests when absolutely necessary
|
|
20
|
+
|
|
21
|
+
## CRITICAL: Test Isolation Requirement
|
|
22
|
+
|
|
23
|
+
**Every test fixture MUST have a unique `outDir`**. This is the #1 cause of mysterious test failures.
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// BAD - Will cause cache pollution
|
|
27
|
+
await loadFixture({
|
|
28
|
+
root: './fixtures/my-test/',
|
|
29
|
+
// No outDir specified - uses default, shared with other tests
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// GOOD - Isolated output
|
|
33
|
+
await loadFixture({
|
|
34
|
+
root: './fixtures/my-test/',
|
|
35
|
+
outDir: './dist/my-test/', // Unique per test
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Why**: Build artifacts are cached and shared via ESM between test runs. Without unique `outDir`, tests contaminate each other.
|
|
40
|
+
|
|
41
|
+
**Reference**: `/CONTRIBUTING.md:203-214`
|
|
42
|
+
|
|
43
|
+
## Test Types (Prefer Unit Tests)
|
|
44
|
+
|
|
45
|
+
### Unit Tests (node:test) - PREFERRED
|
|
46
|
+
|
|
47
|
+
**Location**: `packages/astro/test/*.test.js`
|
|
48
|
+
|
|
49
|
+
**Runner**: `node:test` via `astro-scripts test`
|
|
50
|
+
|
|
51
|
+
**When to use** (default for most code):
|
|
52
|
+
|
|
53
|
+
- Testing pure functions and business logic
|
|
54
|
+
- Testing utilities and helpers
|
|
55
|
+
- Testing data transformations
|
|
56
|
+
- Testing configuration parsing
|
|
57
|
+
- Testing any code that can be isolated from infrastructure
|
|
58
|
+
|
|
59
|
+
**Run**:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# All tests in package
|
|
63
|
+
pnpm -C packages/astro exec astro-scripts test "test/**/*.test.js"
|
|
64
|
+
|
|
65
|
+
# Single test file
|
|
66
|
+
pnpm -C packages/astro exec astro-scripts test "test/actions.test.js"
|
|
67
|
+
|
|
68
|
+
# Filter by pattern
|
|
69
|
+
pnpm -C packages/astro exec astro-scripts test "test/**/*.test.js" --match "CSS"
|
|
70
|
+
|
|
71
|
+
# Multiple files
|
|
72
|
+
pnpm -C packages/astro exec astro-scripts test "test/{actions,css,middleware}.test.js"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Flags**:
|
|
76
|
+
|
|
77
|
+
- `--match` / `-m` → Filter tests by name pattern (regex)
|
|
78
|
+
- `--only` / `-o` → Run only tests marked with `.only`
|
|
79
|
+
- `--parallel` / `-p` → Run tests in parallel (default: sequential)
|
|
80
|
+
- `--timeout` / `-t` → Set timeout in milliseconds
|
|
81
|
+
- `--watch` / `-w` → Watch mode
|
|
82
|
+
|
|
83
|
+
**Writing unit-testable code:**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// BAD - tightly coupled, hard to test
|
|
87
|
+
export function processConfig(configPath) {
|
|
88
|
+
const fs = require('node:fs');
|
|
89
|
+
const config = JSON.parse(fs.readFileSync(configPath));
|
|
90
|
+
const result = transformConfig(config);
|
|
91
|
+
fs.writeFileSync(configPath, JSON.stringify(result));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// GOOD - pure function, easy to test
|
|
95
|
+
export function transformConfig(config) {
|
|
96
|
+
// Pure business logic, no side effects
|
|
97
|
+
return {
|
|
98
|
+
...config,
|
|
99
|
+
transformed: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Infrastructure layer (test with integration test if needed)
|
|
104
|
+
export function processConfigFile(configPath) {
|
|
105
|
+
const fs = require('node:fs');
|
|
106
|
+
const config = JSON.parse(fs.readFileSync(configPath));
|
|
107
|
+
const result = transformConfig(config); // Unit-tested function
|
|
108
|
+
fs.writeFileSync(configPath, JSON.stringify(result));
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Integration Tests - USE SPARINGLY
|
|
113
|
+
|
|
114
|
+
**When to use** (only when unit tests are insufficient):
|
|
115
|
+
|
|
116
|
+
- Testing virtual modules (cannot be unit tested)
|
|
117
|
+
- Testing full build pipeline integration
|
|
118
|
+
- Testing Vite plugin integration
|
|
119
|
+
- Testing adapter integration
|
|
120
|
+
- Testing features that require full Astro context
|
|
121
|
+
|
|
122
|
+
**Location**: `packages/astro/test/*.test.js` (same location, different purpose)
|
|
123
|
+
|
|
124
|
+
**Pattern**: Uses `loadFixture()` and builds full Astro projects
|
|
125
|
+
|
|
126
|
+
**⚠️ Avoid when possible**: Integration tests are slower and harder to debug. Extract business logic into unit-testable functions.
|
|
127
|
+
|
|
128
|
+
### E2E Tests (Playwright) - USE FOR BROWSER ONLY
|
|
129
|
+
|
|
130
|
+
**Location**: `packages/astro/e2e/*.test.js`
|
|
131
|
+
|
|
132
|
+
**When to use**:
|
|
133
|
+
|
|
134
|
+
- Testing client-side hydration
|
|
135
|
+
- Verifying HMR behavior
|
|
136
|
+
- Browser-specific interactions
|
|
137
|
+
- Testing dev server behavior in real browser
|
|
138
|
+
|
|
139
|
+
**When NOT to use**:
|
|
140
|
+
|
|
141
|
+
- Testing `astro build` output (use unit tests)
|
|
142
|
+
- Testing server-side logic (use unit tests)
|
|
143
|
+
- Static HTML validation (use unit tests)
|
|
144
|
+
|
|
145
|
+
**Run**:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# All E2E tests
|
|
149
|
+
pnpm run test:e2e
|
|
150
|
+
|
|
151
|
+
# Filter by pattern
|
|
152
|
+
pnpm run test:e2e:match "Tailwind CSS"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Integration Package Tests
|
|
156
|
+
|
|
157
|
+
**Location**: `packages/integrations/*/test/`
|
|
158
|
+
|
|
159
|
+
**Pattern**: Each integration has its own test suite
|
|
160
|
+
|
|
161
|
+
**Run**:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Single integration
|
|
165
|
+
pnpm -C packages/integrations/react run test
|
|
166
|
+
|
|
167
|
+
# All integrations
|
|
168
|
+
pnpm run test:integrations
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Writing Unit-Testable Code
|
|
172
|
+
|
|
173
|
+
**Goal**: Make business logic testable without infrastructure dependencies.
|
|
174
|
+
|
|
175
|
+
### Principles
|
|
176
|
+
|
|
177
|
+
1. **Separate business logic from infrastructure**
|
|
178
|
+
- Business logic: Pure functions, data transformations, algorithms
|
|
179
|
+
- Infrastructure: File I/O, network calls, Vite plugins, Astro context
|
|
180
|
+
|
|
181
|
+
2. **Use pure functions**
|
|
182
|
+
- Same input → same output
|
|
183
|
+
- No side effects
|
|
184
|
+
- Easy to test
|
|
185
|
+
|
|
186
|
+
3. **Inject dependencies**
|
|
187
|
+
- Don't hardcode dependencies
|
|
188
|
+
- Pass them as parameters
|
|
189
|
+
|
|
190
|
+
4. **Avoid tight coupling**
|
|
191
|
+
- Don't import concrete implementations
|
|
192
|
+
- Use interfaces/contracts
|
|
193
|
+
|
|
194
|
+
### Patterns
|
|
195
|
+
|
|
196
|
+
#### Pattern 1: Extract Business Logic
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// BAD - business logic mixed with infrastructure
|
|
200
|
+
export async function processMarkdown(filePath: string) {
|
|
201
|
+
const fs = await import('node:fs/promises');
|
|
202
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
203
|
+
|
|
204
|
+
// Business logic buried in infrastructure
|
|
205
|
+
const withFrontmatter = content.split('---')[2];
|
|
206
|
+
const processed = withFrontmatter.replace(/TODO:/g, 'NOTE:');
|
|
207
|
+
|
|
208
|
+
await fs.writeFile(filePath, processed);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// GOOD - business logic extracted (unit testable)
|
|
212
|
+
export function transformMarkdownContent(content: string): string {
|
|
213
|
+
const withFrontmatter = content.split('---')[2];
|
|
214
|
+
return withFrontmatter.replace(/TODO:/g, 'NOTE:');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Infrastructure layer (integration test if needed)
|
|
218
|
+
export async function processMarkdownFile(filePath: string) {
|
|
219
|
+
const fs = await import('node:fs/promises');
|
|
220
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
221
|
+
const processed = transformMarkdownContent(content);
|
|
222
|
+
await fs.writeFile(filePath, processed);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Unit test (fast, no file I/O)
|
|
226
|
+
it('transforms markdown content', () => {
|
|
227
|
+
const input = '---\ntitle: Test\n---\nTODO: Fix this';
|
|
228
|
+
const result = transformMarkdownContent(input);
|
|
229
|
+
assert.equal(result, 'NOTE: Fix this');
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Pattern 2: Dependency Injection
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// BAD - hardcoded dependency
|
|
237
|
+
export function buildRoutes(config: AstroConfig) {
|
|
238
|
+
const pages = scanFilesystem('./src/pages'); // Hardcoded
|
|
239
|
+
return pages.map((page) => createRoute(page, config));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// GOOD - inject dependency (unit testable)
|
|
243
|
+
export function buildRoutes(pages: string[], config: AstroConfig): Route[] {
|
|
244
|
+
return pages.map((page) => createRoute(page, config));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Unit test (no filesystem access)
|
|
248
|
+
it('builds routes from pages', () => {
|
|
249
|
+
const pages = ['index.astro', 'about.astro'];
|
|
250
|
+
const config = { base: '/' };
|
|
251
|
+
const routes = buildRoutes(pages, config);
|
|
252
|
+
assert.equal(routes.length, 2);
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Pattern 3: Use Interfaces
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// BAD - tight coupling to concrete type
|
|
260
|
+
export function validateConfig(viteConfig: ViteUserConfig) {
|
|
261
|
+
// Requires full Vite config object
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// GOOD - accept minimal interface
|
|
265
|
+
interface ConfigLike {
|
|
266
|
+
build?: { outDir?: string };
|
|
267
|
+
server?: { port?: number };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function validateConfig(config: ConfigLike) {
|
|
271
|
+
// Only needs what it uses, easy to mock
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Unit test (simple mock)
|
|
275
|
+
it('validates config', () => {
|
|
276
|
+
const config = { build: { outDir: './dist' } };
|
|
277
|
+
const result = validateConfig(config);
|
|
278
|
+
assert.ok(result);
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Pattern 4: Return Data, Don't Mutate
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// BAD - mutation, side effects
|
|
286
|
+
export function updateManifest(manifest: Manifest, route: Route) {
|
|
287
|
+
manifest.routes.push(route);
|
|
288
|
+
manifest.version++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// GOOD - pure function returning new data
|
|
292
|
+
export function addRouteToManifest(manifest: Manifest, route: Route): Manifest {
|
|
293
|
+
return {
|
|
294
|
+
...manifest,
|
|
295
|
+
routes: [...manifest.routes, route],
|
|
296
|
+
version: manifest.version + 1,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Unit test (predictable, no side effects)
|
|
301
|
+
it('adds route to manifest', () => {
|
|
302
|
+
const manifest = { routes: [], version: 1 };
|
|
303
|
+
const route = { path: '/test' };
|
|
304
|
+
const result = addRouteToManifest(manifest, route);
|
|
305
|
+
|
|
306
|
+
assert.equal(result.routes.length, 1);
|
|
307
|
+
assert.equal(result.version, 2);
|
|
308
|
+
assert.equal(manifest.routes.length, 0); // Original unchanged
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### When Integration Tests Are Necessary
|
|
313
|
+
|
|
314
|
+
Use integration tests only when unit tests are insufficient:
|
|
315
|
+
|
|
316
|
+
1. **Virtual modules**: Cannot mock Vite's virtual module system
|
|
317
|
+
2. **Full build pipeline**: Testing end-to-end build process
|
|
318
|
+
3. **Vite plugin behavior**: Testing actual Vite plugin execution
|
|
319
|
+
4. **Adapter integration**: Testing how adapters work with Astro
|
|
320
|
+
|
|
321
|
+
**Even then**, extract as much business logic as possible into unit-testable functions.
|
|
322
|
+
|
|
323
|
+
## Test Utilities
|
|
324
|
+
|
|
325
|
+
**Location**: `packages/astro/test/test-utils.js`
|
|
326
|
+
|
|
327
|
+
### loadFixture(config)
|
|
328
|
+
|
|
329
|
+
Load a test fixture with configuration.
|
|
330
|
+
|
|
331
|
+
```javascript
|
|
332
|
+
import { loadFixture } from './test-utils.js';
|
|
333
|
+
|
|
334
|
+
const fixture = await loadFixture({
|
|
335
|
+
root: './fixtures/my-test/',
|
|
336
|
+
outDir: './dist/my-test/', // REQUIRED
|
|
337
|
+
adapter: testAdapter(),
|
|
338
|
+
integrations: [react()],
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Returns**: Fixture object with methods
|
|
343
|
+
|
|
344
|
+
### Fixture Methods
|
|
345
|
+
|
|
346
|
+
#### fixture.build()
|
|
347
|
+
|
|
348
|
+
Build the fixture (runs `astro build`).
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
await fixture.build();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### fixture.startDevServer(options)
|
|
355
|
+
|
|
356
|
+
Start dev server.
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
const devServer = await fixture.startDevServer();
|
|
360
|
+
// Use server
|
|
361
|
+
await devServer.stop();
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### fixture.preview(options)
|
|
365
|
+
|
|
366
|
+
Start preview server (serves build output).
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
await fixture.build();
|
|
370
|
+
const previewServer = await fixture.preview();
|
|
371
|
+
// Use server
|
|
372
|
+
await previewServer.stop();
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### fixture.fetch(url, init)
|
|
376
|
+
|
|
377
|
+
Fetch from dev/preview server.
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
const res = await fixture.fetch('/about');
|
|
381
|
+
const html = await res.text();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### fixture.readFile(path)
|
|
385
|
+
|
|
386
|
+
Read file from build output.
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
const html = await fixture.readFile('/index.html');
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### fixture.pathExists(path)
|
|
393
|
+
|
|
394
|
+
Check if file exists in build output.
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
const exists = await fixture.pathExists('/index.html');
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### testAdapter()
|
|
401
|
+
|
|
402
|
+
Test adapter for SSR testing.
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
import testAdapter from './test-adapter.js';
|
|
406
|
+
|
|
407
|
+
const fixture = await loadFixture({
|
|
408
|
+
adapter: testAdapter(),
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Test Structure Pattern
|
|
413
|
+
|
|
414
|
+
### Standard Test Structure
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
import { describe, it, before, after } from 'node:test';
|
|
418
|
+
import assert from 'node:assert/strict';
|
|
419
|
+
import { loadFixture } from './test-utils.js';
|
|
420
|
+
|
|
421
|
+
describe('Feature Name', () => {
|
|
422
|
+
let fixture;
|
|
423
|
+
|
|
424
|
+
before(async () => {
|
|
425
|
+
fixture = await loadFixture({
|
|
426
|
+
root: './fixtures/feature-name/',
|
|
427
|
+
outDir: './dist/feature-name/', // Unique!
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('dev', () => {
|
|
432
|
+
let devServer;
|
|
433
|
+
|
|
434
|
+
before(async () => {
|
|
435
|
+
devServer = await fixture.startDevServer();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
after(async () => {
|
|
439
|
+
await devServer.stop();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should work in dev', async () => {
|
|
443
|
+
const res = await fixture.fetch('/');
|
|
444
|
+
assert.equal(res.status, 200);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('build', () => {
|
|
449
|
+
before(async () => {
|
|
450
|
+
await fixture.build();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should work in build', async () => {
|
|
454
|
+
const html = await fixture.readFile('/index.html');
|
|
455
|
+
assert.match(html, /expected content/);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Using .only for Focused Testing
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
// Run only this test
|
|
465
|
+
it.only('focused test', async () => {
|
|
466
|
+
// ...
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Run only this describe block
|
|
470
|
+
describe.only('focused suite', () => {
|
|
471
|
+
// All tests here will run
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Run with**:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
node --test --test-only test/my-test.test.js
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Warning**:
|
|
482
|
+
|
|
483
|
+
- All parent `describe` blocks must also have `.only`
|
|
484
|
+
- `--test-only` flag must come before file path
|
|
485
|
+
|
|
486
|
+
## Fixture Structure
|
|
487
|
+
|
|
488
|
+
### Minimal Fixture
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
test/fixtures/my-test/
|
|
492
|
+
├── package.json # REQUIRED
|
|
493
|
+
├── astro.config.mjs # Optional
|
|
494
|
+
└── src/
|
|
495
|
+
└── pages/
|
|
496
|
+
└── index.astro
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Fixture package.json
|
|
500
|
+
|
|
501
|
+
**REQUIRED**: Use workspace dependencies
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"name": "@test/my-test",
|
|
506
|
+
"version": "0.0.0",
|
|
507
|
+
"private": true,
|
|
508
|
+
"dependencies": {
|
|
509
|
+
"astro": "workspace:*",
|
|
510
|
+
"@astrojs/react": "workspace:*",
|
|
511
|
+
"react": "catalog:",
|
|
512
|
+
"react-dom": "catalog:"
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Pattern**:
|
|
518
|
+
|
|
519
|
+
- `"astro": "workspace:*"` → Links to local astro package
|
|
520
|
+
- `"@astrojs/*": "workspace:*"` → Links to local integrations
|
|
521
|
+
- `"react": "catalog:"` → Uses version from catalog in root package.json
|
|
522
|
+
|
|
523
|
+
### Fixture astro.config.mjs
|
|
524
|
+
|
|
525
|
+
```javascript
|
|
526
|
+
import { defineConfig } from 'astro/config';
|
|
527
|
+
import react from '@astrojs/react';
|
|
528
|
+
|
|
529
|
+
export default defineConfig({
|
|
530
|
+
integrations: [react()],
|
|
531
|
+
outDir: './dist', // Can be overridden by test
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Test Isolation Best Practices
|
|
536
|
+
|
|
537
|
+
### 1. Unique Output Directories
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
// Pattern: Use test name in outDir
|
|
541
|
+
describe('Actions', () => {
|
|
542
|
+
const fixture = await loadFixture({
|
|
543
|
+
root: './fixtures/actions/',
|
|
544
|
+
outDir: './dist/actions/', // Matches test name
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe('Actions with adapter', () => {
|
|
549
|
+
const fixture = await loadFixture({
|
|
550
|
+
root: './fixtures/actions/',
|
|
551
|
+
outDir: './dist/actions-adapter/', // Different from above
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 2. Clean Build Artifacts
|
|
557
|
+
|
|
558
|
+
If tests still fail with unique outDir:
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
# Manual cleanup
|
|
562
|
+
rm -rf test/fixtures/my-test/.astro
|
|
563
|
+
rm -rf test/fixtures/my-test/dist
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### 3. Avoid Parallel Execution for Flaky Tests
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
# Sequential execution (default)
|
|
570
|
+
pnpm -C packages/astro exec astro-scripts test "test/**/*.test.js"
|
|
571
|
+
|
|
572
|
+
# Parallel (faster but can cause issues)
|
|
573
|
+
pnpm -C packages/astro exec astro-scripts test "test/**/*.test.js" --parallel
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Testing Patterns
|
|
577
|
+
|
|
578
|
+
### Testing Vite Plugins
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
describe('Vite Plugin', () => {
|
|
582
|
+
it('should transform .astro files', async () => {
|
|
583
|
+
const fixture = await loadFixture({
|
|
584
|
+
root: './fixtures/astro-components/',
|
|
585
|
+
outDir: './dist/astro-components/',
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
await fixture.build();
|
|
589
|
+
const html = await fixture.readFile('/index.html');
|
|
590
|
+
assert.match(html, /<h1>.*<\/h1>/);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Testing Virtual Modules
|
|
596
|
+
|
|
597
|
+
```javascript
|
|
598
|
+
describe('Virtual Modules', () => {
|
|
599
|
+
it('should load virtual:astro:middleware', async () => {
|
|
600
|
+
const fixture = await loadFixture({
|
|
601
|
+
root: './fixtures/middleware/',
|
|
602
|
+
outDir: './dist/middleware/',
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const devServer = await fixture.startDevServer();
|
|
606
|
+
const res = await fixture.fetch('/');
|
|
607
|
+
assert.equal(res.headers.get('x-middleware'), 'true');
|
|
608
|
+
await devServer.stop();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Testing SSR
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
import testAdapter from './test-adapter.js';
|
|
617
|
+
|
|
618
|
+
describe('SSR', () => {
|
|
619
|
+
let fixture;
|
|
620
|
+
|
|
621
|
+
before(async () => {
|
|
622
|
+
fixture = await loadFixture({
|
|
623
|
+
root: './fixtures/ssr/',
|
|
624
|
+
outDir: './dist/ssr/',
|
|
625
|
+
output: 'server',
|
|
626
|
+
adapter: testAdapter(),
|
|
627
|
+
});
|
|
628
|
+
await fixture.build();
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('should render dynamically', async () => {
|
|
632
|
+
const app = await fixture.loadTestAdapterApp();
|
|
633
|
+
const request = new Request('http://example.com/');
|
|
634
|
+
const response = await app.render(request);
|
|
635
|
+
const html = await response.text();
|
|
636
|
+
assert.match(html, /dynamic content/);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Testing Content Collections
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
describe('Content Collections', () => {
|
|
645
|
+
it('should generate types', async () => {
|
|
646
|
+
const fixture = await loadFixture({
|
|
647
|
+
root: './fixtures/content-collections/',
|
|
648
|
+
outDir: './dist/content-collections/',
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await fixture.build();
|
|
652
|
+
|
|
653
|
+
// Check data store
|
|
654
|
+
const dataStore = await fixture.readFile('../.astro/data-store.json');
|
|
655
|
+
const parsed = JSON.parse(dataStore);
|
|
656
|
+
assert.ok(parsed.collections.blog);
|
|
657
|
+
|
|
658
|
+
// Check types
|
|
659
|
+
const types = await fixture.readFile('../.astro/types.d.ts');
|
|
660
|
+
assert.match(types, /declare module 'astro:content'/);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Testing Errors
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
describe('Error Handling', () => {
|
|
669
|
+
it('should throw on invalid config', async () => {
|
|
670
|
+
await assert.rejects(async () => {
|
|
671
|
+
await loadFixture({
|
|
672
|
+
root: './fixtures/invalid-config/',
|
|
673
|
+
outDir: './dist/invalid-config/',
|
|
674
|
+
});
|
|
675
|
+
}, /Expected configuration error/);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## Business Logic vs Infrastructure
|
|
681
|
+
|
|
682
|
+
**Pattern**: Separate pure logic from side effects for testability
|
|
683
|
+
|
|
684
|
+
### Bad: Hard to Test
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
// create-key.ts
|
|
688
|
+
import { logger } from '../utils.js';
|
|
689
|
+
|
|
690
|
+
export async function createKey() {
|
|
691
|
+
const key = await crypto.subtle.generateKey(/* ... */);
|
|
692
|
+
logger.info(`Key: ${key}`);
|
|
693
|
+
return key;
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Issues**:
|
|
698
|
+
|
|
699
|
+
- Global logger dependency
|
|
700
|
+
- Crypto API hardcoded
|
|
701
|
+
- Can't easily mock
|
|
702
|
+
|
|
703
|
+
### Good: Testable
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// create-key.ts
|
|
707
|
+
import type { Logger, KeyGenerator } from './types.js';
|
|
708
|
+
|
|
709
|
+
interface Options {
|
|
710
|
+
logger: Logger;
|
|
711
|
+
keyGenerator: KeyGenerator;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export async function createKey({ logger, keyGenerator }: Options) {
|
|
715
|
+
const key = await keyGenerator.generate();
|
|
716
|
+
logger.info(`Key: ${key}`);
|
|
717
|
+
return key;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// test/create-key.test.js
|
|
721
|
+
import { SpyLogger } from './test-utils.js';
|
|
722
|
+
import { FakeKeyGenerator } from './test-utils.js';
|
|
723
|
+
|
|
724
|
+
it('logs the generated key', async () => {
|
|
725
|
+
const logger = new SpyLogger();
|
|
726
|
+
const keyGenerator = new FakeKeyGenerator('test-key');
|
|
727
|
+
|
|
728
|
+
await createKey({ logger, keyGenerator });
|
|
729
|
+
|
|
730
|
+
assert.equal(logger.logs[0].message, 'Key: test-key');
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Test-Specific Abstractions
|
|
735
|
+
|
|
736
|
+
```javascript
|
|
737
|
+
// test/test-utils.js
|
|
738
|
+
|
|
739
|
+
export class SpyLogger {
|
|
740
|
+
logs = [];
|
|
741
|
+
|
|
742
|
+
info(message) {
|
|
743
|
+
this.logs.push({ level: 'info', message });
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
error(message) {
|
|
747
|
+
this.logs.push({ level: 'error', message });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export class FakeKeyGenerator {
|
|
752
|
+
constructor(key) {
|
|
753
|
+
this.key = key;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async generate() {
|
|
757
|
+
return this.key;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Debugging Test Failures
|
|
763
|
+
|
|
764
|
+
### 1. Run Test in Isolation
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
# Single test file
|
|
768
|
+
node --test test/my-test.test.js
|
|
769
|
+
|
|
770
|
+
# With focused test
|
|
771
|
+
node --test --test-only test/my-test.test.js
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### 2. Check Fixture Setup
|
|
775
|
+
|
|
776
|
+
```javascript
|
|
777
|
+
// Add logging
|
|
778
|
+
before(async () => {
|
|
779
|
+
console.log('Loading fixture:', fixturePath);
|
|
780
|
+
fixture = await loadFixture({
|
|
781
|
+
root: fixturePath,
|
|
782
|
+
outDir: './dist/unique/',
|
|
783
|
+
});
|
|
784
|
+
console.log('Fixture loaded');
|
|
785
|
+
});
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### 3. Inspect Build Output
|
|
789
|
+
|
|
790
|
+
```javascript
|
|
791
|
+
// After build, inspect files
|
|
792
|
+
await fixture.build();
|
|
793
|
+
const files = await fs.readdir(fixture.config.outDir);
|
|
794
|
+
console.log('Built files:', files);
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 4. Check for Cache Issues
|
|
798
|
+
|
|
799
|
+
```bash
|
|
800
|
+
# Clean fixture manually
|
|
801
|
+
rm -rf test/fixtures/my-test/.astro
|
|
802
|
+
rm -rf test/fixtures/my-test/dist
|
|
803
|
+
rm -rf test/fixtures/my-test/node_modules/.vite
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### 5. Verify outDir Uniqueness
|
|
807
|
+
|
|
808
|
+
```javascript
|
|
809
|
+
// Check if another test uses same outDir
|
|
810
|
+
grep -r "outDir.*dist/my-test" test/**/*.test.js
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### 6. Debug Timeouts in CI
|
|
814
|
+
|
|
815
|
+
**Symptom**: Tests pass locally but timeout in CI
|
|
816
|
+
|
|
817
|
+
**Fix**: Add `--parallel` to see which file times out
|
|
818
|
+
|
|
819
|
+
```json
|
|
820
|
+
// package.json
|
|
821
|
+
{
|
|
822
|
+
"test": "astro-scripts test --parallel \"test/**/*.test.js\""
|
|
823
|
+
}
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
**After identifying problematic file**, remove `--parallel` and fix the test.
|
|
827
|
+
|
|
828
|
+
## Running Tests
|
|
829
|
+
|
|
830
|
+
### Local Development
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
# Fast iteration
|
|
834
|
+
pnpm -C packages/astro exec astro-scripts test "test/my-feature.test.js" --watch
|
|
835
|
+
|
|
836
|
+
# Filter by pattern
|
|
837
|
+
pnpm -C packages/astro exec astro-scripts test -m "should handle errors"
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### Full Test Suite
|
|
841
|
+
|
|
842
|
+
```bash
|
|
843
|
+
# All tests (slow)
|
|
844
|
+
pnpm run test
|
|
845
|
+
|
|
846
|
+
# Just astro package
|
|
847
|
+
pnpm run test:astro
|
|
848
|
+
|
|
849
|
+
# Just integrations
|
|
850
|
+
pnpm run test:integrations
|
|
851
|
+
|
|
852
|
+
# E2E tests
|
|
853
|
+
pnpm run test:e2e
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### CI Testing
|
|
857
|
+
|
|
858
|
+
```bash
|
|
859
|
+
# Run in CI mode (no cache)
|
|
860
|
+
pnpm run build:ci:no-cache
|
|
861
|
+
pnpm run test
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Common Test Failures
|
|
865
|
+
|
|
866
|
+
### "Test timeout exceeded"
|
|
867
|
+
|
|
868
|
+
**Cause**: Test takes too long (default: 30s)
|
|
869
|
+
|
|
870
|
+
**Fix**:
|
|
871
|
+
|
|
872
|
+
```bash
|
|
873
|
+
# Increase timeout
|
|
874
|
+
pnpm -C packages/astro exec astro-scripts test --timeout 60000 "test/slow.test.js"
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### "Port already in use"
|
|
878
|
+
|
|
879
|
+
**Cause**: Previous dev server not stopped
|
|
880
|
+
|
|
881
|
+
**Fix**:
|
|
882
|
+
|
|
883
|
+
```javascript
|
|
884
|
+
// Always stop servers in after() hook
|
|
885
|
+
after(async () => {
|
|
886
|
+
await devServer?.stop();
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### "ENOENT: no such file"
|
|
891
|
+
|
|
892
|
+
**Cause**: File path wrong or build didn't complete
|
|
893
|
+
|
|
894
|
+
**Fix**:
|
|
895
|
+
|
|
896
|
+
1. Check file path relative to outDir
|
|
897
|
+
2. Verify `await fixture.build()` completed
|
|
898
|
+
3. Check if file should exist
|
|
899
|
+
|
|
900
|
+
### "Fixture contamination"
|
|
901
|
+
|
|
902
|
+
**Cause**: Shared outDir between tests
|
|
903
|
+
|
|
904
|
+
**Fix**: Ensure unique outDir per test (see top of this guide)
|
|
905
|
+
|
|
906
|
+
## Test Organization
|
|
907
|
+
|
|
908
|
+
### Group Related Tests
|
|
909
|
+
|
|
910
|
+
```javascript
|
|
911
|
+
describe('Feature', () => {
|
|
912
|
+
describe('dev mode', () => {
|
|
913
|
+
// Dev-specific tests
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
describe('build mode', () => {
|
|
917
|
+
// Build-specific tests
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
describe('SSR mode', () => {
|
|
921
|
+
// SSR-specific tests
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
### Share Setup Between Tests
|
|
927
|
+
|
|
928
|
+
```javascript
|
|
929
|
+
describe('Feature', () => {
|
|
930
|
+
let fixture;
|
|
931
|
+
|
|
932
|
+
// Shared setup
|
|
933
|
+
before(async () => {
|
|
934
|
+
fixture = await loadFixture({
|
|
935
|
+
root: './fixtures/shared/',
|
|
936
|
+
outDir: './dist/shared/',
|
|
937
|
+
});
|
|
938
|
+
await fixture.build();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Multiple tests use same fixture
|
|
942
|
+
it('test 1', async () => {
|
|
943
|
+
const html = await fixture.readFile('/page1.html');
|
|
944
|
+
// ...
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('test 2', async () => {
|
|
948
|
+
const html = await fixture.readFile('/page2.html');
|
|
949
|
+
// ...
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
## Testing Checklist
|
|
955
|
+
|
|
956
|
+
Before considering a feature complete:
|
|
957
|
+
|
|
958
|
+
- [ ] Unit tests cover happy path
|
|
959
|
+
- [ ] Unit tests cover error cases
|
|
960
|
+
- [ ] E2E tests if browser interaction needed
|
|
961
|
+
- [ ] Tests use unique outDir
|
|
962
|
+
- [ ] Tests clean up servers/resources
|
|
963
|
+
- [ ] Tests pass locally
|
|
964
|
+
- [ ] Tests pass with `--parallel` (if applicable)
|
|
965
|
+
- [ ] Fixture has proper package.json
|
|
966
|
+
- [ ] Business logic separated from infrastructure
|
|
967
|
+
|
|
968
|
+
## Further Reading
|
|
969
|
+
|
|
970
|
+
- CONTRIBUTING.md: Testing section
|
|
971
|
+
- CONTRIBUTING.md: Making code testable (lines 347-531)
|
|
972
|
+
- Fixtures: `packages/astro/test/fixtures/`
|
|
973
|
+
- Test utils: `packages/astro/test/test-utils.js`
|