@@ -322,6 +322,243 @@ await expect(findByText(content)).rejects.toThrow();
322322expect (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(() => {
4486856 . ** File Naming** : Use ` .svelte.test.ts ` for files that need Svelte runes support
4496867 . ** Reactivity** : Remember to call ` flushSync() ` after changing reactive state
4506878 . ** 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