@@ -383,5 +383,396 @@ describe(
383
383
// FIXME:
384
384
// expect(nestedQuery).toHaveBeenCalledTimes(2)
385
385
} )
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
+ } )
386
777
}
387
778
)
0 commit comments