Skip to content

Commit ff812b5

Browse files
authored
chore: Refactor and improve unit testing (#48)
* chore: Refactor and improve unit testing * chore: Refactor most RouterEngine tests to be run for every routing universe * test: Enhance addRoutes() test util
1 parent 13d7409 commit ff812b5

File tree

3 files changed

+999
-530
lines changed

3 files changed

+999
-530
lines changed

docs/testing-guide.md

Lines changed: 219 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,8 @@ beforeAll(() => {
199199
Use data-driven testing across **all 5 routing universes**:
200200

201201
```typescript
202-
export const ROUTING_UNIVERSES: {
203-
hash: Hash | undefined;
204-
implicitMode: RoutingOptions['implicitMode'];
205-
hashMode: Exclude<RoutingOptions['hashMode'], undefined>;
206-
text: string;
207-
name: string;
208-
}[] = [
209-
{ hash: undefined, implicitMode: 'path', hashMode: 'single', text: "IMP", name: "Implicit Path Routing" },
210-
{ hash: undefined, implicitMode: 'hash', hashMode: 'single', text: "IMH", name: "Implicit Hash Routing" },
211-
{ hash: false, implicitMode: 'path', hashMode: 'single', text: "PR", name: "Path Routing" },
212-
{ hash: true, implicitMode: 'path', hashMode: 'single', text: "HR", name: "Hash Routing" },
213-
{ hash: 'p1', implicitMode: 'path', hashMode: 'multi', text: "MHR", name: "Multi Hash Routing" },
214-
] as const;
202+
// Import the complete universe definitions
203+
import { ROUTING_UNIVERSES } from "../testing/test-utils.js";
215204

216205
ROUTING_UNIVERSES.forEach((ru) => {
217206
describe(`Component - ${ru.text}`, () => {
@@ -239,6 +228,8 @@ ROUTING_UNIVERSES.forEach((ru) => {
239228
});
240229
```
241230

231+
See `src/testing/test-utils.ts` for the complete `ROUTING_UNIVERSES` array definition with all universe configurations.
232+
242233
### Context Setup
243234

244235
```typescript
@@ -299,10 +290,16 @@ addNonMatchingRoute(router, 'optionalRouteName');
299290
// Add multiple routes at once
300291
addRoutes(router, {
301292
matching: 2, // Adds 2 matching routes
302-
nonMatching: 1, // Adds 1 non-matching route
303-
ignoreForFallback: 1 // Adds 1 route that ignores fallback
293+
nonMatching: 1 // Adds 1 non-matching route
304294
});
305295

296+
// Add explicit custom routes using rest parameters
297+
addRoutes(router,
298+
{ matching: 1 },
299+
{ pattern: "/api/:id", name: "api-route" },
300+
{ regex: /^\/test$/ } // Auto-generated name
301+
);
302+
306303
// Manual route addition
307304
router.routes["routeName"] = {
308305
pattern: "/some/path",
@@ -341,7 +338,7 @@ import {
341338
createRouterTestSetup,
342339
createTestSnippet,
343340
ROUTING_UNIVERSES
344-
} from "$lib/testing/test-utils.js";
341+
} from "../testing/test-utils.js"; // Note: moved outside $lib
345342
```
346343

347344
### Snippet Creation for Testing
@@ -452,6 +449,211 @@ afterAll(() => {
452449
7. **Reactivity**: Remember to call `flushSync()` after changing reactive state
453450
8. **Prop vs State Reactivity**: Test both prop changes AND reactive dependency changes
454451

452+
## Advanced Testing Infrastructure
453+
454+
### Browser API Mocking
455+
456+
For testing components that rely on `window.location` and `window.history` (like `RouterEngine`), use the comprehensive browser mocking utilities:
457+
458+
```typescript
459+
import { setupBrowserMocks } from "../testing/test-utils.js";
460+
461+
describe("Component requiring browser APIs", () => {
462+
beforeEach(() => {
463+
// Automatically mocks window.location, window.history, and integrates with library Location
464+
setupBrowserMocks("/initial/path");
465+
});
466+
467+
test("Should respond to location changes", () => {
468+
// Browser APIs are now mocked and integrated with library
469+
window.history.pushState({}, "", "/new/path");
470+
// Test component behavior
471+
});
472+
});
473+
```
474+
475+
**What `setupBrowserMocks()` provides**:
476+
- Complete `window.location` mock with all properties (href, pathname, hash, search, etc.)
477+
- Full `window.history` mock with `pushState`, `replaceState`, and state management
478+
- Automatic `popstate` event triggering on location changes
479+
- Integration with library's `LocationLite` for synchronized state
480+
- Proper cleanup between tests
481+
482+
### Enhanced Route Management
483+
484+
The `addRoutes()` utility supports multiple approaches for flexible route setup:
485+
486+
```typescript
487+
// Simple route counts
488+
addRoutes(router, { matching: 2, nonMatching: 1 });
489+
490+
// RouteSpecs approach for custom route definitions
491+
addRoutes(router, {
492+
matching: { count: 2, specs: { pattern: "/custom/:id" } },
493+
nonMatching: { count: 1, specs: { pattern: "/other" } }
494+
});
495+
496+
// Rest parameters for explicit route definitions (NEW)
497+
addRoutes(router,
498+
{ matching: 1, nonMatching: 0 },
499+
{ pattern: "/api/users/:id", name: "user-detail" },
500+
{ regex: /^\/products\/\d+$/, name: "product" },
501+
{ pattern: "/settings" } // Name auto-generated if not provided
502+
);
503+
504+
// Combined approach
505+
addRoutes(router,
506+
{ matching: 2 }, // Generate 2 matching routes
507+
{ pattern: "/custom", name: "custom-route" }, // Add specific route
508+
{ pattern: "/another" } // Add another with auto-generated name
509+
);
510+
```
511+
512+
**Rest Parameters Benefits:**
513+
- **Explicit control**: Define exact routes with specific patterns/regex
514+
- **Named routes**: Optional `name` property for predictable route keys
515+
- **Type safety**: Full IntelliSense support for `RouteInfo` properties
516+
- **Flexible mixing**: Combine generated routes with explicit definitions
517+
518+
Refer to `src/testing/test-utils.ts` for complete function signatures and type definitions.
519+
520+
### Universe-Based Testing Pattern
521+
522+
**Complete test coverage across all 5 routing universes** using the standardized pattern:
523+
524+
```typescript
525+
import { ROUTING_UNIVERSES } from "../testing/test-utils.js";
526+
527+
// ✅ Recommended: Test ALL universes with single loop
528+
ROUTING_UNIVERSES.forEach((universe) => {
529+
describe(`Component (${universe.text})`, () => {
530+
let cleanup: () => void;
531+
let setup: ReturnType<typeof createRouterTestSetup>;
532+
533+
beforeAll(() => {
534+
cleanup = init({
535+
implicitMode: universe.implicitMode,
536+
hashMode: universe.hashMode
537+
});
538+
setup = createRouterTestSetup(universe.hash);
539+
});
540+
541+
afterAll(() => {
542+
cleanup();
543+
setup.dispose();
544+
});
545+
546+
beforeEach(() => {
547+
setup.init(); // Fresh router per test
548+
setupBrowserMocks("/"); // Fresh browser state
549+
});
550+
551+
test(`Should behave correctly in ${universe.text}`, () => {
552+
// Test logic that works across all universes
553+
const { hash, context, router } = setup;
554+
555+
// Use universe.text for concise test descriptions
556+
expect(universe.text).toMatch(/^(IMP|IMH|PR|HR|MHR)$/);
557+
});
558+
});
559+
});
560+
```
561+
562+
**Benefits**:
563+
- **100% Universe Coverage**: Ensures behavior works across all routing modes
564+
- **Consistent Test Structure**: Standardized setup and teardown patterns
565+
- **Efficient Execution**: Vitest's dynamic skipping capabilities maintain performance
566+
- **Clear Reporting**: Each universe shows as separate test suite with meaningful names
567+
568+
### Self-Documenting Test Constants
569+
570+
Use dictionary-based constants for better maintainability:
571+
572+
```typescript
573+
// Import self-documenting hash values
574+
import { ALL_HASHES } from "../testing/test-utils.js";
575+
576+
// Usage in tests
577+
test("Should validate hash compatibility", () => {
578+
expect(() => {
579+
new RouterEngine({ hash: ALL_HASHES.single });
580+
}).not.toThrow();
581+
});
582+
```
583+
584+
See `src/testing/test-utils.ts` for the complete `ALL_HASHES` dictionary definition.
585+
586+
**Dictionary Benefits**:
587+
- **Self-Documentation**: `ALL_HASHES.single` is clearer than `true`
588+
- **Single Source of Truth**: Change values in one place
589+
- **Type Safety**: TypeScript can validate usage
590+
- **Discoverability**: IDE autocomplete shows available options
591+
592+
### Constructor Validation Testing
593+
594+
For components with runtime validation, test all error scenarios systematically:
595+
596+
```typescript
597+
describe("Constructor hash validation", () => {
598+
test.each([
599+
{ parent: ALL_HASHES.path, child: ALL_HASHES.single, desc: 'path parent vs hash child' },
600+
{ parent: ALL_HASHES.single, child: ALL_HASHES.path, desc: 'hash parent vs path child' },
601+
{ parent: ALL_HASHES.multi, child: ALL_HASHES.path, desc: 'multi-hash parent vs path child' },
602+
{ parent: ALL_HASHES.path, child: ALL_HASHES.multi, desc: 'path parent vs multi-hash child' }
603+
])("Should throw error when parent and child have different hash modes: '$desc'", ({ parent, child }) => {
604+
expect(() => {
605+
const parentRouter = new RouterEngine({ hash: parent });
606+
new RouterEngine(parentRouter, { hash: child });
607+
}).toThrow("Parent and child routers must use the same hash mode");
608+
});
609+
610+
test.each([
611+
{ parent: ALL_HASHES.path, desc: 'path parent' },
612+
{ parent: ALL_HASHES.single, desc: 'hash parent' },
613+
{ parent: ALL_HASHES.multi, desc: 'multi-hash parent' }
614+
])("Should allow child router without explicit hash to inherit parent's hash: '$desc'", ({ parent }) => {
615+
expect(() => {
616+
const parentRouter = new RouterEngine({ hash: parent });
617+
new RouterEngine(parentRouter);
618+
}).not.toThrow();
619+
});
620+
});
621+
```
622+
623+
### Performance Optimizations
624+
625+
**Browser Mock State Synchronization**:
626+
```typescript
627+
// ✅ Automatic state sync - setupBrowserMocks handles this
628+
// Best practice: pass the library's location object for full integration
629+
setupBrowserMocks("/initial", libraryLocationObject);
630+
window.history.pushState({}, "", "/new"); // Automatically triggers popstate
631+
632+
// ❌ Manual sync required (old approach)
633+
mockLocation.pathname = "/new";
634+
window.dispatchEvent(new PopStateEvent('popstate')); // Manual event trigger
635+
```
636+
637+
**Efficient Test Assertions**:
638+
```typescript
639+
// ✅ Fast negative assertions
640+
expect(queryByText("should not exist")).toBeNull();
641+
642+
// ❌ Slow - waits for timeout
643+
await expect(findByText("should not exist")).rejects.toThrow();
644+
645+
// ✅ Use findByText for elements that should exist
646+
const element = await findByText("should exist");
647+
expect(element).toBeInTheDocument();
648+
```
649+
455650
## Test Utilities Location
456651

457-
Test utilities are located in `src/lib/testing/` and excluded from the published package via the `"files"` property in `package.json`. During development, they build to `dist/testing/` but are not included in `npm pack`.
652+
Test utilities are centralized in `src/testing/test-utils.ts` (moved from `src/lib/testing/` for better organization) and excluded from the published package via the `"files"` property in `package.json`. During development, they build to `dist/testing/` but are not included in `npm pack`.
653+
654+
**Key utilities**:
655+
- `setupBrowserMocks()`: Complete browser API mocking with library integration
656+
- `addRoutes()`: Enhanced route management with RouteSpecs support
657+
- `createRouterTestSetup()`: Standardized router setup with proper lifecycle
658+
- `ROUTING_UNIVERSES`: Complete universe definitions for comprehensive testing
659+
- `ALL_HASHES`: Self-documenting hash value constants

0 commit comments

Comments
 (0)