@@ -16,6 +16,7 @@ import {
16
16
createRouter ,
17
17
createStaticHandler ,
18
18
defer ,
19
+ UNSAFE_DEFERRED_SYMBOL ,
19
20
ErrorResponse ,
20
21
IDLE_FETCHER ,
21
22
IDLE_NAVIGATION ,
@@ -30,6 +31,7 @@ import type {
30
31
AgnosticIndexRouteObject ,
31
32
AgnosticNonIndexRouteObject ,
32
33
AgnosticRouteObject ,
34
+ DeferredData ,
33
35
TrackedPromise ,
34
36
} from "../utils" ;
35
37
import {
@@ -157,8 +159,13 @@ function isRedirect(result: any) {
157
159
) ;
158
160
}
159
161
160
- interface CustomMatchers < R = unknown > {
162
+ interface CustomMatchers < R = jest . Expect > {
161
163
trackedPromise ( data ?: any , error ?: any , aborted ?: boolean ) : R ;
164
+ deferredData (
165
+ done : boolean ,
166
+ status ?: number ,
167
+ headers ?: Record < string , string >
168
+ ) : R ;
162
169
}
163
170
164
171
declare global {
@@ -169,12 +176,40 @@ declare global {
169
176
}
170
177
}
171
178
172
- // Custom matcher for asserting deferred promise results inside of `toEqual()`
173
- // - expect.trackedPromise() => pending promise
174
- // - expect.trackedPromise(value) => promise resolved with `value`
175
- // - expect.trackedPromise(null, error) => promise rejected with `error`
176
- // - expect.trackedPromise(null, null, true) => promise aborted
177
179
expect . extend ( {
180
+ // Custom matcher for asserting deferred promise results for static handler
181
+ // - expect(val).deferredData(false) => Unresolved promise
182
+ // - expect(val).deferredData(false) => Resolved promise
183
+ // - expect(val).deferredData(false, 201, { 'x-custom': 'yes' })
184
+ // => Unresolved promise with status + headers
185
+ // - expect(val).deferredData(true, 201, { 'x-custom': 'yes' })
186
+ // => Resolved promise with status + headers
187
+ deferredData ( received , done , status = 200 , headers = { } ) {
188
+ let deferredData = received as DeferredData ;
189
+
190
+ return {
191
+ message : ( ) =>
192
+ `expected done=${ String (
193
+ done
194
+ ) } /status=${ status } /headers=${ JSON . stringify ( headers ) } , ` +
195
+ `instead got done=${ String ( deferredData . done ) } /status=${
196
+ deferredData . init ! . status || 200
197
+ } /headers=${ JSON . stringify (
198
+ Object . fromEntries ( new Headers ( deferredData . init ! . headers ) . entries ( ) )
199
+ ) } `,
200
+ pass :
201
+ deferredData . done === done &&
202
+ ( deferredData . init ! . status || 200 ) === status &&
203
+ JSON . stringify (
204
+ Object . fromEntries ( new Headers ( deferredData . init ! . headers ) . entries ( ) )
205
+ ) === JSON . stringify ( headers ) ,
206
+ } ;
207
+ } ,
208
+ // Custom matcher for asserting deferred promise results inside of `toEqual()`
209
+ // - expect.trackedPromise() => pending promise
210
+ // - expect.trackedPromise(value) => promise resolved with `value`
211
+ // - expect.trackedPromise(null, error) => promise rejected with `error`
212
+ // - expect.trackedPromise(null, null, true) => promise aborted
178
213
trackedPromise ( received , data , error , aborted = false ) {
179
214
let promise = received as TrackedPromise ;
180
215
let isTrackedPromise =
@@ -10948,14 +10983,32 @@ describe("a router", () => {
10948
10983
{
10949
10984
id : "deferred" ,
10950
10985
path : "deferred" ,
10951
- loader : ( ) =>
10952
- defer ( {
10986
+ loader : ( { request } ) => {
10987
+ if ( new URL ( request . url ) . searchParams . has ( "reject" ) ) {
10988
+ return defer ( {
10989
+ critical : "loader" ,
10990
+ lazy : new Promise ( ( _ , r ) =>
10991
+ setTimeout ( ( ) => r ( new Error ( "broken!" ) ) , 10 )
10992
+ ) ,
10993
+ } ) ;
10994
+ }
10995
+ if ( new URL ( request . url ) . searchParams . has ( "status" ) ) {
10996
+ return defer (
10997
+ {
10998
+ critical : "loader" ,
10999
+ lazy : new Promise ( ( r ) => setTimeout ( ( ) => r ( "lazy" ) , 10 ) ) ,
11000
+ } ,
11001
+ { status : 201 , headers : { "X-Custom" : "yes" } }
11002
+ ) ;
11003
+ }
11004
+ return defer ( {
10953
11005
critical : "loader" ,
10954
11006
lazy : new Promise ( ( r ) => setTimeout ( ( ) => r ( "lazy" ) , 10 ) ) ,
10955
- } ) ,
11007
+ } ) ;
11008
+ } ,
10956
11009
action : ( ) =>
10957
11010
defer ( {
10958
- critical : "action " ,
11011
+ critical : "critical " ,
10959
11012
lazy : new Promise ( ( r ) => setTimeout ( ( ) => r ( "lazy" ) , 10 ) ) ,
10960
11013
} ) ,
10961
11014
} ,
@@ -11112,8 +11165,7 @@ describe("a router", () => {
11112
11165
} ) ;
11113
11166
} ) ;
11114
11167
11115
- // Note: this is only until we wire up the remix streaming
11116
- it ( "should abort deferred data on load navigations (for now)" , async ( ) => {
11168
+ it ( "should support document load navigations returning deferred" , async ( ) => {
11117
11169
let { query } = createStaticHandler ( SSR_ROUTES ) ;
11118
11170
let context = await query ( createRequest ( "/parent/deferred" ) ) ;
11119
11171
expect ( context ) . toMatchObject ( {
@@ -11122,19 +11174,29 @@ describe("a router", () => {
11122
11174
parent : "PARENT LOADER" ,
11123
11175
deferred : {
11124
11176
critical : "loader" ,
11125
- lazy : expect . trackedPromise ( null , null , true ) ,
11177
+ lazy : expect . trackedPromise ( ) ,
11126
11178
} ,
11127
11179
} ,
11180
+ activeDeferreds : {
11181
+ deferred : expect . deferredData ( false ) ,
11182
+ } ,
11128
11183
errors : null ,
11129
11184
location : { pathname : "/parent/deferred" } ,
11130
11185
matches : [ { route : { id : "parent" } } , { route : { id : "deferred" } } ] ,
11131
11186
} ) ;
11132
11187
11133
11188
await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
11134
- expect (
11135
- ( context as StaticHandlerContext ) . loaderData . deferred . lazy instanceof
11136
- Promise
11137
- ) . toBe ( true ) ;
11189
+
11190
+ expect ( context ) . toMatchObject ( {
11191
+ loaderData : {
11192
+ deferred : {
11193
+ lazy : expect . trackedPromise ( "lazy" ) ,
11194
+ } ,
11195
+ } ,
11196
+ activeDeferreds : {
11197
+ deferred : expect . deferredData ( true ) ,
11198
+ } ,
11199
+ } ) ;
11138
11200
} ) ;
11139
11201
11140
11202
it ( "should support document submit navigations" , async ( ) => {
@@ -11685,6 +11747,127 @@ describe("a router", () => {
11685
11747
expect ( arg ( childStub ) . context . sessionId ) . toBe ( "12345" ) ;
11686
11748
} ) ;
11687
11749
11750
+ describe ( "deferred" , ( ) => {
11751
+ let { query } = createStaticHandler ( SSR_ROUTES ) ;
11752
+
11753
+ it ( "should return DeferredData on symbol" , async ( ) => {
11754
+ let context = ( await query (
11755
+ createRequest ( "/parent/deferred" )
11756
+ ) ) as StaticHandlerContext ;
11757
+ expect ( context ) . toMatchObject ( {
11758
+ loaderData : {
11759
+ parent : "PARENT LOADER" ,
11760
+ deferred : {
11761
+ critical : "loader" ,
11762
+ lazy : expect . trackedPromise ( ) ,
11763
+ } ,
11764
+ } ,
11765
+ activeDeferreds : {
11766
+ deferred : expect . deferredData ( false ) ,
11767
+ } ,
11768
+ } ) ;
11769
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
11770
+ expect ( context ) . toMatchObject ( {
11771
+ loaderData : {
11772
+ parent : "PARENT LOADER" ,
11773
+ deferred : {
11774
+ critical : "loader" ,
11775
+ lazy : expect . trackedPromise ( "lazy" ) ,
11776
+ } ,
11777
+ } ,
11778
+ activeDeferreds : {
11779
+ deferred : expect . deferredData ( true ) ,
11780
+ } ,
11781
+ } ) ;
11782
+ } ) ;
11783
+
11784
+ it ( "should return rejected DeferredData on symbol" , async ( ) => {
11785
+ let context = ( await query (
11786
+ createRequest ( "/parent/deferred?reject" )
11787
+ ) ) as StaticHandlerContext ;
11788
+ expect ( context ) . toMatchObject ( {
11789
+ loaderData : {
11790
+ parent : "PARENT LOADER" ,
11791
+ deferred : {
11792
+ critical : "loader" ,
11793
+ lazy : expect . trackedPromise ( ) ,
11794
+ } ,
11795
+ } ,
11796
+ activeDeferreds : {
11797
+ deferred : expect . deferredData ( false ) ,
11798
+ } ,
11799
+ } ) ;
11800
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
11801
+ expect ( context ) . toMatchObject ( {
11802
+ loaderData : {
11803
+ parent : "PARENT LOADER" ,
11804
+ deferred : {
11805
+ critical : "loader" ,
11806
+ lazy : expect . trackedPromise ( undefined , new Error ( "broken!" ) ) ,
11807
+ } ,
11808
+ } ,
11809
+ activeDeferreds : {
11810
+ deferred : expect . deferredData ( true ) ,
11811
+ } ,
11812
+ } ) ;
11813
+ } ) ;
11814
+
11815
+ it ( "should return DeferredData on symbol with status + headers" , async ( ) => {
11816
+ let context = ( await query (
11817
+ createRequest ( "/parent/deferred?status" )
11818
+ ) ) as StaticHandlerContext ;
11819
+ expect ( context ) . toMatchObject ( {
11820
+ loaderData : {
11821
+ parent : "PARENT LOADER" ,
11822
+ deferred : {
11823
+ critical : "loader" ,
11824
+ lazy : expect . trackedPromise ( ) ,
11825
+ } ,
11826
+ } ,
11827
+ activeDeferreds : {
11828
+ deferred : expect . deferredData ( false , 201 , {
11829
+ "x-custom" : "yes" ,
11830
+ } ) ,
11831
+ } ,
11832
+ } ) ;
11833
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
11834
+ expect ( context ) . toMatchObject ( {
11835
+ loaderData : {
11836
+ parent : "PARENT LOADER" ,
11837
+ deferred : {
11838
+ critical : "loader" ,
11839
+ lazy : expect . trackedPromise ( "lazy" ) ,
11840
+ } ,
11841
+ } ,
11842
+ activeDeferreds : {
11843
+ deferred : expect . deferredData ( true , 201 , {
11844
+ "x-custom" : "yes" ,
11845
+ } ) ,
11846
+ } ,
11847
+ } ) ;
11848
+ } ) ;
11849
+
11850
+ it ( "does not support deferred on submissions" , async ( ) => {
11851
+ let context = ( await query (
11852
+ createSubmitRequest ( "/parent/deferred" )
11853
+ ) ) as StaticHandlerContext ;
11854
+ expect ( context . actionData ) . toEqual ( null ) ;
11855
+ expect ( context . loaderData ) . toEqual ( {
11856
+ parent : null ,
11857
+ deferred : null ,
11858
+ } ) ;
11859
+ expect ( context . activeDeferreds ) . toEqual ( null ) ;
11860
+ expect ( context . errors ) . toEqual ( {
11861
+ parent : new ErrorResponse (
11862
+ 400 ,
11863
+ "Bad Request" ,
11864
+ new Error ( "defer() is not supported in actions" ) ,
11865
+ true
11866
+ ) ,
11867
+ } ) ;
11868
+ } ) ;
11869
+ } ) ;
11870
+
11688
11871
describe ( "statusCode" , ( ) => {
11689
11872
it ( "should expose a 200 status code by default" , async ( ) => {
11690
11873
let { query } = createStaticHandler ( [
@@ -12661,6 +12844,80 @@ describe("a router", () => {
12661
12844
expect ( arg ( actionStub ) . context . sessionId ) . toBe ( "12345" ) ;
12662
12845
} ) ;
12663
12846
12847
+ describe ( "deferred" , ( ) => {
12848
+ let { queryRoute } = createStaticHandler ( SSR_ROUTES ) ;
12849
+
12850
+ it ( "should return DeferredData on symbol" , async ( ) => {
12851
+ let result = await queryRoute ( createRequest ( "/parent/deferred" ) ) ;
12852
+ expect ( result ) . toMatchObject ( {
12853
+ critical : "loader" ,
12854
+ lazy : expect . trackedPromise ( ) ,
12855
+ } ) ;
12856
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( false ) ;
12857
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
12858
+ expect ( result ) . toMatchObject ( {
12859
+ critical : "loader" ,
12860
+ lazy : expect . trackedPromise ( "lazy" ) ,
12861
+ } ) ;
12862
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( true ) ;
12863
+ } ) ;
12864
+
12865
+ it ( "should return rejected DeferredData on symbol" , async ( ) => {
12866
+ let result = await queryRoute (
12867
+ createRequest ( "/parent/deferred?reject" )
12868
+ ) ;
12869
+ expect ( result ) . toMatchObject ( {
12870
+ critical : "loader" ,
12871
+ lazy : expect . trackedPromise ( ) ,
12872
+ } ) ;
12873
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( false ) ;
12874
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
12875
+ expect ( result ) . toMatchObject ( {
12876
+ critical : "loader" ,
12877
+ lazy : expect . trackedPromise ( null , new Error ( "broken!" ) ) ,
12878
+ } ) ;
12879
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( true ) ;
12880
+ } ) ;
12881
+
12882
+ it ( "should return DeferredData on symbol with status + headers" , async ( ) => {
12883
+ let result = await queryRoute (
12884
+ createRequest ( "/parent/deferred?status" )
12885
+ ) ;
12886
+ expect ( result ) . toMatchObject ( {
12887
+ critical : "loader" ,
12888
+ lazy : expect . trackedPromise ( ) ,
12889
+ } ) ;
12890
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( false , 201 , {
12891
+ "x-custom" : "yes" ,
12892
+ } ) ;
12893
+ await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
12894
+ expect ( result ) . toMatchObject ( {
12895
+ critical : "loader" ,
12896
+ lazy : expect . trackedPromise ( "lazy" ) ,
12897
+ } ) ;
12898
+ expect ( result [ UNSAFE_DEFERRED_SYMBOL ] ) . deferredData ( true , 201 , {
12899
+ "x-custom" : "yes" ,
12900
+ } ) ;
12901
+ } ) ;
12902
+
12903
+ it ( "does not support deferred on submissions" , async ( ) => {
12904
+ try {
12905
+ await queryRoute ( createSubmitRequest ( "/parent/deferred" ) ) ;
12906
+ expect ( false ) . toBe ( true ) ;
12907
+ } catch ( e ) {
12908
+ // eslint-disable-next-line jest/no-conditional-expect
12909
+ expect ( e ) . toEqual (
12910
+ new ErrorResponse (
12911
+ 400 ,
12912
+ "Bad Request" ,
12913
+ new Error ( "defer() is not supported in actions" ) ,
12914
+ true
12915
+ )
12916
+ ) ;
12917
+ }
12918
+ } ) ;
12919
+ } ) ;
12920
+
12664
12921
describe ( "Errors with Status Codes" , ( ) => {
12665
12922
/* eslint-disable jest/no-conditional-expect */
12666
12923
let { queryRoute } = createStaticHandler ( [
0 commit comments