Skip to content

Commit df73b56

Browse files
committed
chore: Add more unit testing
1 parent 05a7d18 commit df73b56

File tree

11 files changed

+3116
-177
lines changed

11 files changed

+3116
-177
lines changed

docs/testing-guide.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,243 @@ await expect(findByText(content)).rejects.toThrow();
322322
expect(queryByText(content)).toBeNull();
323323
```
324324

325+
### Testing Bindable Properties
326+
327+
**Bindable properties** (declared with `export let` and used with `bind:` in Svelte) require special testing patterns since they involve two-way data binding between parent and child components.
328+
329+
#### **The Getter/Setter Pattern (Recommended)**
330+
331+
The most effective way to test bindable properties is using getter/setter functions in the `render()` props:
332+
333+
```typescript
334+
test("Should bind property correctly", async () => {
335+
// Arrange: Set up binding capture
336+
let capturedValue: any;
337+
const propertySetter = vi.fn((value) => { capturedValue = value; });
338+
339+
// Act: Render component with bindable property
340+
render(Component, {
341+
props: {
342+
// Other props...
343+
get bindableProperty() { return capturedValue; },
344+
set bindableProperty(value) { propertySetter(value); }
345+
},
346+
context
347+
});
348+
349+
// Trigger the binding (component-specific logic)
350+
// e.g., navigate to a route, trigger an event, etc.
351+
await triggerBindingUpdate();
352+
353+
// Assert: Verify binding occurred
354+
expect(propertySetter).toHaveBeenCalled();
355+
expect(capturedValue).toEqual(expectedValue);
356+
});
357+
```
358+
359+
#### **Testing Across Routing Modes**
360+
361+
For components that work across multiple routing universes, test bindable properties for each mode:
362+
363+
```typescript
364+
function bindablePropertyTests(setup: ReturnType<typeof createRouterTestSetup>, ru: RoutingUniverse) {
365+
test("Should bind property when condition is met", async () => {
366+
const { hash, context } = setup;
367+
let capturedValue: any;
368+
const propertySetter = vi.fn((value) => { capturedValue = value; });
369+
370+
render(Component, {
371+
props: {
372+
hash,
373+
get boundProperty() { return capturedValue; },
374+
set boundProperty(value) { propertySetter(value); },
375+
// Other component-specific props
376+
},
377+
context
378+
});
379+
380+
// Trigger binding based on routing mode
381+
const shouldUseHash = (ru.implicitMode === 'hash') || (hash === true) || (typeof hash === 'string');
382+
const url = shouldUseHash ? "http://example.com/#/test" : "http://example.com/test";
383+
location.url.href = url;
384+
await vi.waitFor(() => {});
385+
386+
expect(propertySetter).toHaveBeenCalled();
387+
expect(capturedValue).toEqual(expectedValue);
388+
});
389+
390+
test("Should update binding when conditions change", async () => {
391+
// Test binding updates during navigation or state changes
392+
let capturedValue: any;
393+
const propertySetter = vi.fn((value) => { capturedValue = value; });
394+
395+
render(Component, {
396+
props: {
397+
hash,
398+
get boundProperty() { return capturedValue; },
399+
set boundProperty(value) { propertySetter(value); }
400+
},
401+
context
402+
});
403+
404+
// First state
405+
await triggerFirstState();
406+
expect(capturedValue).toEqual(firstExpectedValue);
407+
408+
// Second state
409+
await triggerSecondState();
410+
expect(capturedValue).toEqual(secondExpectedValue);
411+
});
412+
}
413+
414+
// Apply to all routing universes
415+
ROUTING_UNIVERSES.forEach((ru) => {
416+
describe(`Component - ${ru.text}`, () => {
417+
const setup = createRouterTestSetup(ru.hash);
418+
// ... setup code ...
419+
420+
describe("Bindable Properties", () => {
421+
bindablePropertyTests(setup, ru);
422+
});
423+
});
424+
});
425+
```
426+
427+
#### **Handling Mode-Specific Limitations**
428+
429+
Some routing modes may have different behavior or limitations. Handle these gracefully:
430+
431+
```typescript
432+
test("Should handle complex binding scenarios", async () => {
433+
let capturedValue: any;
434+
const propertySetter = vi.fn((value) => { capturedValue = value; });
435+
436+
render(Component, {
437+
props: {
438+
get boundProperty() { return capturedValue; },
439+
set boundProperty(value) { propertySetter(value); }
440+
},
441+
context
442+
});
443+
444+
await triggerBinding();
445+
446+
expect(propertySetter).toHaveBeenCalled();
447+
448+
// Handle mode-specific edge cases
449+
if (ru.text === 'MHR') {
450+
// Multi Hash Routing may require different URL format or setup
451+
// Skip complex assertions that aren't supported yet
452+
return;
453+
}
454+
455+
expect(capturedValue).toEqual(expectedComplexValue);
456+
});
457+
```
458+
459+
#### **Type Conversion Awareness**
460+
461+
When testing components that perform automatic type conversion (like RouterEngine), account for expected type changes:
462+
463+
```typescript
464+
test("Should bind with correct type conversion", async () => {
465+
let capturedParams: any;
466+
const paramsSetter = vi.fn((value) => { capturedParams = value; });
467+
468+
render(RouteComponent, {
469+
props: {
470+
path: "/user/:userId/post/:postId",
471+
get params() { return capturedParams; },
472+
set params(value) { paramsSetter(value); }
473+
},
474+
context
475+
});
476+
477+
// Navigate to URL with string parameters
478+
location.url.href = "http://example.com/user/123/post/456";
479+
await vi.waitFor(() => {});
480+
481+
expect(paramsSetter).toHaveBeenCalled();
482+
// Expect automatic string-to-number conversion
483+
expect(capturedParams).toEqual({
484+
userId: 123, // number, not "123"
485+
postId: 456 // number, not "456"
486+
});
487+
});
488+
```
489+
490+
#### **Anti-Patterns to Avoid**
491+
492+
**Don't use wrapper components for simple binding tests**:
493+
```typescript
494+
// Bad - overcomplicated
495+
const WrapperComponent = () => {
496+
let boundValue = $state();
497+
return `<Component bind:property={boundValue} />`;
498+
};
499+
```
500+
501+
**Don't test binding implementation details**:
502+
```typescript
503+
// Bad - testing internal mechanics
504+
expect(component.$$.callbacks.boundProperty).toHaveBeenCalled();
505+
```
506+
507+
**Don't forget routing mode compatibility**:
508+
```typescript
509+
// Bad - hardcoded to one routing mode
510+
location.url.href = "http://example.com/#/test"; // Only works for hash routing
511+
```
512+
513+
**Use the getter/setter pattern for clean, direct testing**:
514+
```typescript
515+
// Good - direct, simple, effective
516+
render(Component, {
517+
props: {
518+
get boundProperty() { return capturedValue; },
519+
set boundProperty(value) { propertySetter(value); }
520+
}
521+
});
522+
```
523+
524+
#### **Real-World Example: Route Parameter Binding**
525+
526+
```typescript
527+
test("Should bind route parameters correctly", async () => {
528+
// Arrange
529+
const { hash, context } = setup;
530+
let capturedParams: any;
531+
const paramsSetter = vi.fn((value) => { capturedParams = value; });
532+
533+
// Act
534+
render(TestRouteWithRouter, {
535+
props: {
536+
hash,
537+
routePath: "/user/:userId",
538+
get params() { return capturedParams; },
539+
set params(value) { paramsSetter(value); },
540+
children: createTestSnippet('<div>User {params?.userId}</div>')
541+
},
542+
context
543+
});
544+
545+
// Navigate to matching route
546+
const shouldUseHash = (ru.implicitMode === 'hash') || (hash === true) || (typeof hash === 'string');
547+
location.url.href = shouldUseHash ? "http://example.com/#/user/42" : "http://example.com/user/42";
548+
await vi.waitFor(() => {});
549+
550+
// Assert
551+
expect(paramsSetter).toHaveBeenCalled();
552+
expect(capturedParams).toEqual({ userId: 42 }); // Note: number due to auto-conversion
553+
});
554+
```
555+
556+
This pattern provides:
557+
- **Clear test intent**: What binding behavior is being tested
558+
- **Routing mode compatibility**: Works across all universe types
559+
- **Type safety**: Captures actual bound values for verification
560+
- **Maintainability**: Simple, readable test structure
561+
325562
### Required Imports
326563

327564
```typescript
@@ -448,6 +685,7 @@ afterAll(() => {
448685
6. **File Naming**: Use `.svelte.test.ts` for files that need Svelte runes support
449686
7. **Reactivity**: Remember to call `flushSync()` after changing reactive state
450687
8. **Prop vs State Reactivity**: Test both prop changes AND reactive dependency changes
688+
9. **Bindable Properties**: Use getter/setter pattern in `render()` props instead of wrapper components for testing two-way binding
451689

452690
## Advanced Testing Infrastructure
453691

0 commit comments

Comments
 (0)