@@ -383,5 +383,396 @@ describe(
383383 // FIXME:
384384 // expect(nestedQuery).toHaveBeenCalledTimes(2)
385385 } )
386+
387+ it ( "handles query function errors gracefully" , async ( ) => {
388+ const error = new Error ( "Query failed" )
389+ const query = vi . fn ( ) . mockRejectedValue ( error )
390+ const useData = defineColadaLoader ( {
391+ query,
392+ key : ( ) => [ "error-test" ] ,
393+ } )
394+
395+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
396+
397+ await router . push ( "/fetch" )
398+ const { error : loaderError , isLoading } = useDataFn ( )
399+
400+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
401+ expect ( loaderError . value ) . toBe ( error )
402+ expect ( isLoading . value ) . toBe ( false )
403+ } )
404+
405+ it ( "handles dynamic key changes correctly" , async ( ) => {
406+ const query = vi . fn ( ) . mockImplementation ( async ( to ) => `data-${ to . params . id } ` )
407+ const useData = defineColadaLoader ( {
408+ query,
409+ key : ( to ) => [ "dynamic" , to . params . id as string ] ,
410+ } )
411+
412+ let useDataResult : ReturnType < typeof useData > | undefined
413+ const component = defineComponent ( {
414+ setup ( ) {
415+ useDataResult = useData ( )
416+ const { data, error, isLoading } = useDataResult
417+ return { data, error, isLoading }
418+ } ,
419+ template : `<p/>` ,
420+ } )
421+
422+ const router = getRouter ( )
423+ router . addRoute ( {
424+ name : "dynamic-test" ,
425+ path : "/items/:id" ,
426+ meta : { loaders : [ useData ] } ,
427+ component,
428+ } )
429+
430+ mount ( RouterViewMock , {
431+ global : {
432+ plugins : [ [ DataLoaderPlugin , { router } ] , createPinia ( ) , PiniaColada ] ,
433+ } ,
434+ } )
435+
436+ await router . push ( "/items/1" )
437+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
438+ expect ( useDataResult ! . data . value ) . toBe ( "data-1" )
439+
440+ await router . push ( "/items/2" )
441+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
442+ expect ( useDataResult ! . data . value ) . toBe ( "data-2" )
443+ } )
444+
445+ it ( "handles concurrent navigation correctly" , async ( ) => {
446+ const query = vi . fn ( ) . mockImplementation (
447+ async ( to ) => {
448+ await new Promise ( resolve => setTimeout ( resolve , 10 ) )
449+ return `data-${ to . query . id } `
450+ }
451+ )
452+ const useData = defineColadaLoader ( {
453+ query,
454+ key : ( to ) => [ "concurrent" , to . query . id as string ] ,
455+ } )
456+
457+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
458+
459+ // Start multiple concurrent navigations
460+ const navigation1 = router . push ( "/fetch?id=1" )
461+ const navigation2 = router . push ( "/fetch?id=2" )
462+ const navigation3 = router . push ( "/fetch?id=3" )
463+
464+ await Promise . all ( [ navigation1 , navigation2 , navigation3 ] )
465+ await vi . runAllTimersAsync ( )
466+
467+ const { data } = useDataFn ( )
468+ // Should have the data from the last navigation
469+ expect ( data . value ) . toBe ( "data-3" )
470+ // Query should be called for each unique key
471+ expect ( query ) . toHaveBeenCalledTimes ( 3 )
472+ } )
473+
474+ it ( "properly manages loading state transitions" , async ( ) => {
475+ let resolveQuery : ( value : string ) => void
476+ const query = vi . fn ( ) . mockImplementation (
477+ ( ) => new Promise ( resolve => { resolveQuery = resolve } )
478+ )
479+
480+ const useData = defineColadaLoader ( {
481+ query,
482+ key : ( ) => [ "loading-test" ] ,
483+ } )
484+
485+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
486+
487+ const navigationPromise = router . push ( "/fetch" )
488+ const { isLoading, data } = useDataFn ( )
489+
490+ // Should be loading initially
491+ expect ( isLoading . value ) . toBe ( true )
492+ expect ( data . value ) . toBeUndefined ( )
493+
494+ // Resolve the query
495+ resolveQuery ! ( "loaded-data" )
496+ await navigationPromise
497+ await nextTick ( )
498+
499+ // Should no longer be loading
500+ expect ( isLoading . value ) . toBe ( false )
501+ expect ( data . value ) . toBe ( "loaded-data" )
502+ } )
503+
504+ it ( "handles error recovery scenarios" , async ( ) => {
505+ const error = new Error ( "Network error" )
506+ const query = vi . fn ( )
507+ . mockRejectedValueOnce ( error )
508+ . mockResolvedValueOnce ( "recovery-data" )
509+
510+ const useData = defineColadaLoader ( {
511+ query,
512+ key : ( ) => [ "error-recovery" ] ,
513+ } )
514+
515+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
516+
517+ await router . push ( "/fetch" )
518+ const { data, error : loaderError , reload } = useDataFn ( )
519+
520+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
521+ expect ( loaderError . value ) . toBe ( error )
522+ expect ( data . value ) . toBeUndefined ( )
523+
524+ // Attempt recovery
525+ await reload ( )
526+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
527+ expect ( loaderError . value ) . toBe ( null )
528+ expect ( data . value ) . toBe ( "recovery-data" )
529+ } )
530+
531+ it ( "handles different data types correctly" , async ( ) => {
532+ const complexData = {
533+ users : [ { id : 1 , name : "John" } , { id : 2 , name : "Jane" } ] ,
534+ meta : { total : 2 , page : 1 } ,
535+ nested : { deep : { value : "test" } }
536+ }
537+
538+ const query = vi . fn ( ) . mockResolvedValue ( complexData )
539+ const useData = defineColadaLoader ( {
540+ query,
541+ key : ( ) => [ "complex-data" ] ,
542+ } )
543+
544+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
545+
546+ await router . push ( "/fetch" )
547+ const { data } = useDataFn ( )
548+
549+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
550+ expect ( data . value ) . toEqual ( complexData )
551+ expect ( data . value ?. users ) . toHaveLength ( 2 )
552+ expect ( data . value ?. nested . deep . value ) . toBe ( "test" )
553+ } )
554+
555+ it ( "handles null and undefined data correctly" , async ( ) => {
556+ const query = vi . fn ( ) . mockResolvedValue ( null )
557+ const useData = defineColadaLoader ( {
558+ query,
559+ key : ( ) => [ "null-data" ] ,
560+ } )
561+
562+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
563+
564+ await router . push ( "/fetch" )
565+ const { data } = useDataFn ( )
566+
567+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
568+ expect ( data . value ) . toBe ( null )
569+ } )
570+
571+ it ( "handles route parameter changes in key function" , async ( ) => {
572+ const query = vi . fn ( ) . mockImplementation ( async ( to ) => `user-${ to . params . userId } ` )
573+ const useData = defineColadaLoader ( {
574+ query,
575+ key : ( to ) => [ "user" , to . params . userId as string , to . query . version as string ] ,
576+ } )
577+
578+ let useDataResult : ReturnType < typeof useData > | undefined
579+ const component = defineComponent ( {
580+ setup ( ) {
581+ useDataResult = useData ( )
582+ return { ...useDataResult }
583+ } ,
584+ template : `<p/>` ,
585+ } )
586+
587+ const router = getRouter ( )
588+ router . addRoute ( {
589+ name : "user-profile" ,
590+ path : "/users/:userId" ,
591+ meta : { loaders : [ useData ] } ,
592+ component,
593+ } )
594+
595+ mount ( RouterViewMock , {
596+ global : {
597+ plugins : [ [ DataLoaderPlugin , { router } ] , createPinia ( ) , PiniaColada ] ,
598+ } ,
599+ } )
600+
601+ await router . push ( "/users/123?version=v1" )
602+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
603+ expect ( useDataResult ! . data . value ) . toBe ( "user-123" )
604+
605+ // Change version - should fetch again due to key change
606+ await router . push ( "/users/123?version=v2" )
607+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
608+ expect ( useDataResult ! . data . value ) . toBe ( "user-123" )
609+
610+ // Change user ID - should fetch again
611+ await router . push ( "/users/456?version=v2" )
612+ expect ( query ) . toHaveBeenCalledTimes ( 3 )
613+ expect ( useDataResult ! . data . value ) . toBe ( "user-456" )
614+ } )
615+
616+ it ( "handles cache invalidation correctly" , async ( ) => {
617+ const query = vi . fn ( )
618+ . mockResolvedValueOnce ( "cached-data" )
619+ . mockResolvedValueOnce ( "fresh-data" )
620+
621+ const useData = defineColadaLoader ( {
622+ query,
623+ key : ( ) => [ "cache-invalidation" ] ,
624+ } )
625+
626+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
627+
628+ await router . push ( "/fetch" )
629+ const { data } = useDataFn ( )
630+
631+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
632+ expect ( data . value ) . toBe ( "cached-data" )
633+
634+ // Create a new mount with same cache to test cache persistence
635+ const wrapper = mount (
636+ defineComponent ( {
637+ setup ( ) {
638+ const caches = useQueryCache ( )
639+ return { caches }
640+ } ,
641+ template : `<div></div>` ,
642+ } ) ,
643+ {
644+ global : {
645+ plugins : [ getActivePinia ( ) ! , PiniaColada ] ,
646+ } ,
647+ }
648+ )
649+
650+ // Invalidate cache
651+ await wrapper . vm . caches . invalidateQueries ( { key : [ "cache-invalidation" ] } )
652+
653+ const { data : freshData , reload } = useDataFn ( )
654+ await reload ( )
655+
656+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
657+ expect ( freshData . value ) . toBe ( "fresh-data" )
658+ } )
659+
660+ it ( "handles multiple loaders with same key correctly" , async ( ) => {
661+ const sharedQuery = vi . fn ( ) . mockResolvedValue ( "shared-data" )
662+
663+ const useData1 = defineColadaLoader ( {
664+ query : sharedQuery ,
665+ key : ( ) => [ "shared" ] ,
666+ } )
667+
668+ const useData2 = defineColadaLoader ( {
669+ query : sharedQuery ,
670+ key : ( ) => [ "shared" ] ,
671+ } )
672+
673+ const { router : router1 , useData : useDataFn1 } = singleLoaderOneRoute ( useData1 )
674+ const { router : router2 , useData : useDataFn2 } = singleLoaderOneRoute ( useData2 )
675+
676+ await router1 . push ( "/fetch" )
677+ await router2 . push ( "/fetch" )
678+
679+ const { data : data1 } = useDataFn1 ( )
680+ const { data : data2 } = useDataFn2 ( )
681+
682+ // Should only call query once due to shared cache
683+ expect ( sharedQuery ) . toHaveBeenCalledTimes ( 1 )
684+ expect ( data1 . value ) . toBe ( "shared-data" )
685+ expect ( data2 . value ) . toBe ( "shared-data" )
686+ } )
687+
688+ it ( "handles empty key arrays" , async ( ) => {
689+ const query = vi . fn ( ) . mockResolvedValue ( "empty-key-data" )
690+ const useData = defineColadaLoader ( {
691+ query,
692+ key : ( ) => [ ] ,
693+ } )
694+
695+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
696+
697+ await router . push ( "/fetch" )
698+ const { data } = useDataFn ( )
699+
700+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
701+ expect ( data . value ) . toBe ( "empty-key-data" )
702+ } )
703+
704+ it ( "handles special characters in keys" , async ( ) => {
705+ const specialKey = [ "special" , "key-with/slashes" , "key with spaces" , "key@with#symbols" ]
706+ const query = vi . fn ( ) . mockResolvedValue ( "special-key-data" )
707+ const useData = defineColadaLoader ( {
708+ query,
709+ key : ( ) => specialKey ,
710+ } )
711+
712+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
713+
714+ await router . push ( "/fetch" )
715+ const { data } = useDataFn ( )
716+
717+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
718+ expect ( data . value ) . toBe ( "special-key-data" )
719+ } )
720+
721+ it ( "supports manual reload functionality" , async ( ) => {
722+ const query = vi . fn ( )
723+ . mockResolvedValueOnce ( "initial-data" )
724+ . mockResolvedValueOnce ( "reloaded-data" )
725+
726+ const useData = defineColadaLoader ( {
727+ query,
728+ key : ( ) => [ "reload-test" ] ,
729+ } )
730+
731+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
732+
733+ await router . push ( "/fetch" )
734+ const { data, reload } = useDataFn ( )
735+
736+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
737+ expect ( data . value ) . toBe ( "initial-data" )
738+
739+ await reload ( )
740+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
741+ expect ( data . value ) . toBe ( "reloaded-data" )
742+ } )
743+
744+ it ( "handles stale data scenarios correctly" , async ( ) => {
745+ let resolveQuery : ( value : string ) => void
746+ let queryCount = 0
747+ const query = vi . fn ( ) . mockImplementation (
748+ ( ) => new Promise ( resolve => {
749+ queryCount ++
750+ resolveQuery = ( value ) => resolve ( `${ value } -${ queryCount } ` )
751+ } )
752+ )
753+ const useData = defineColadaLoader ( {
754+ query,
755+ key : ( to ) => [ "stale" , to . query . v as string ] ,
756+ } )
757+
758+ const { router, useData : useDataFn } = singleLoaderOneRoute ( useData )
759+
760+ // Start first navigation
761+ const firstNavigation = router . push ( "/fetch?v=1" )
762+ expect ( query ) . toHaveBeenCalledTimes ( 1 )
763+
764+ // Start second navigation before first completes
765+ const secondNavigation = router . push ( "/fetch?v=2" )
766+ expect ( query ) . toHaveBeenCalledTimes ( 2 )
767+
768+ // Complete second query first
769+ resolveQuery ! ( "data" )
770+ await secondNavigation
771+ await vi . runAllTimersAsync ( )
772+
773+ const { data } = useDataFn ( )
774+ // Should have data from the second query
775+ expect ( data . value ) . toBe ( "data-2" )
776+ } )
386777 }
387778)
0 commit comments