1
- import React , { Suspense , useMemo } from "rehackt" ;
1
+ import React , { Suspense , use , useMemo } from "rehackt" ;
2
2
import { outsideOf } from "../util/runInConditions.js" ;
3
3
import assert from "node:assert" ;
4
4
import test , { afterEach , describe } from "node:test" ;
@@ -24,17 +24,21 @@ const {
24
24
InMemoryCache,
25
25
WrapApolloProvider,
26
26
DataTransportContext,
27
+ resetApolloSingletons,
27
28
} = await import ( "#bundled" ) ;
28
29
29
- await describe (
30
+ describe (
30
31
"tests with DOM access" ,
31
32
{ skip : outsideOf ( "node" , "browser" ) } ,
32
33
async ( ) => {
33
34
// @ts -expect-error seems to have a wrong type?
34
35
await import ( "global-jsdom/register" ) ;
35
- const { render, cleanup } = await import ( "@testing-library/react" ) ;
36
+ const { render, cleanup, getQueriesForElement } = await import (
37
+ "@testing-library/react"
38
+ ) ;
36
39
37
40
afterEach ( cleanup ) ;
41
+ afterEach ( resetApolloSingletons ) ;
38
42
39
43
const QUERY_ME : TypedDocumentNode < { me : string } > = gql `
40
44
query {
@@ -215,9 +219,9 @@ await describe(
215
219
await findByText ( "User" ) ;
216
220
217
221
assert . ok ( attemptedRenderCount > 0 ) ;
218
- // one render to rehydrate the server value
222
+ // will try with server value and immediately restart with client value
219
223
// one rerender with the actual client value (which is hopefull equal)
220
- assert . equal ( finishedRenderCount , 2 ) ;
224
+ assert . equal ( finishedRenderCount , 1 ) ;
221
225
222
226
assert . deepStrictEqual ( JSON . parse ( JSON . stringify ( client . extract ( ) ) ) , {
223
227
ROOT_QUERY : {
@@ -227,10 +231,127 @@ await describe(
227
231
} ) ;
228
232
}
229
233
) ;
234
+
235
+ test (
236
+ "race condition: client ahead of server renders without hydration mismatch" ,
237
+ { skip : outsideOf ( "browser" ) } ,
238
+ async ( ) => {
239
+ const { $RC, $RS, setBody, hydrateBody, appendToBody } = await import (
240
+ "../util/hydrationTest.js"
241
+ ) ;
242
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
243
+ let useStaticValueRefStub = < T extends unknown > ( ) : { current : T } => {
244
+ throw new Error ( "Should not be called yet!" ) ;
245
+ } ;
246
+
247
+ const client = new ApolloClient ( {
248
+ connectToDevTools : false ,
249
+ cache : new InMemoryCache ( ) ,
250
+ } ) ;
251
+ const simulateRequestStart = client . onQueryStarted ! ;
252
+ const simulateRequestData = client . onQueryProgress ! ;
253
+
254
+ const Provider = WrapApolloProvider ( ( { children } ) => {
255
+ return (
256
+ < DataTransportContext . Provider
257
+ value = { useMemo (
258
+ ( ) => ( {
259
+ useStaticValueRef ( ) {
260
+ return useStaticValueRefStub ( ) ;
261
+ } ,
262
+ } ) ,
263
+ [ ]
264
+ ) }
265
+ >
266
+ { children }
267
+ </ DataTransportContext . Provider >
268
+ ) ;
269
+ } ) ;
270
+
271
+ const finishedRenders : any [ ] = [ ] ;
272
+
273
+ function Child ( ) {
274
+ const { data } = useSuspenseQuery ( QUERY_ME ) ;
275
+ finishedRenders . push ( data ) ;
276
+ return < div id = "user" > { data . me } </ div > ;
277
+ }
278
+
279
+ const promise = Promise . resolve ( ) ;
280
+ // suspends on the server, immediately resolved in browser
281
+ function ParallelSuspending ( ) {
282
+ use ( promise ) ;
283
+ return < div id = "parallel" > suspending in parallel</ div > ;
284
+ }
285
+
286
+ const { findByText } = getQueriesForElement ( document . body ) ;
287
+
288
+ // server starts streaming
289
+ setBody `<!--$?--><template id="B:0"></template>Fallback<!--/$-->` ;
290
+ // request started on the server
291
+ simulateRequestStart ( EVENT_STARTED ) ;
292
+
293
+ hydrateBody (
294
+ < Provider makeClient = { ( ) => client } >
295
+ < Suspense fallback = { "Fallback" } >
296
+ < Child />
297
+ < ParallelSuspending />
298
+ </ Suspense >
299
+ </ Provider >
300
+ ) ;
301
+
302
+ await findByText ( "Fallback" ) ;
303
+ // this is the div for the suspense boundary
304
+ appendToBody `<div hidden id="S:0"><template id="P:1"></template><template id="P:2"></template></div>` ;
305
+ // request has finished on the server
306
+ simulateRequestData ( EVENT_DATA ) ;
307
+ simulateRequestData ( EVENT_COMPLETE ) ;
308
+ // `Child` component wants to transport data from SSR render to the browser
309
+ useStaticValueRefStub = ( ) => ( { current : FIRST_HOOK_RESULT as any } ) ;
310
+ // `Child` finishes rendering on the server
311
+ appendToBody `<div hidden id="S:1"><div id="user">User</div></div>` ;
312
+ $RS ( "S:1" , "P:1" ) ;
313
+
314
+ // meanwhile, in the browser, the cache is modified
315
+ client . cache . writeQuery ( {
316
+ query : QUERY_ME ,
317
+ data : {
318
+ me : "Future me." ,
319
+ } ,
320
+ } ) ;
321
+
322
+ // `ParallelSuspending` finishes rendering
323
+ appendToBody `<div hidden id="S:2"><div id="parallel">suspending in parallel</div></div>` ;
324
+ $RS ( "S:2" , "P:2" ) ;
325
+
326
+ // everything in the suspense boundary finished rendering, so assemble HTML and take up React rendering again
327
+ $RC ( "B:0" , "S:0" ) ;
328
+
329
+ // we expect the *new* value to appear after hydration finished, not the old value from the server
330
+ await findByText ( "Future me." ) ;
331
+
332
+ // one render to rehydrate the server value
333
+ // one rerender with the actual client value (which is hopefull equal)
334
+ assert . deepStrictEqual ( finishedRenders , [
335
+ { me : "User" } ,
336
+ { me : "Future me." } ,
337
+ ] ) ;
338
+
339
+ assert . deepStrictEqual ( JSON . parse ( JSON . stringify ( client . extract ( ) ) ) , {
340
+ ROOT_QUERY : {
341
+ __typename : "Query" ,
342
+ me : "Future me." ,
343
+ } ,
344
+ } ) ;
345
+ assert . equal (
346
+ document . body . innerHTML ,
347
+ `<!--$--><div id="user">Future me.</div><div id="parallel">suspending in parallel</div><!--/$-->`
348
+ ) ;
349
+ }
350
+ ) ;
230
351
}
231
352
) ;
232
353
233
- await describe ( "document transforms are applied correctly" , async ( ) => {
354
+ describe ( "document transforms are applied correctly" , async ( ) => {
234
355
const untransformedQuery = gql `
235
356
query Test {
236
357
user {
0 commit comments