@ranimontagna/agent-toolkit 0.1.10 → 0.1.12

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.
@@ -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`