@@ -2208,5 +2208,324 @@ implementations.forEach((implementation) => {
2208
2208
validateRSCHtml ( await page . content ( ) ) ;
2209
2209
} ) ;
2210
2210
} ) ;
2211
+
2212
+ test . describe ( "Route Client Component Props" , ( ) => {
2213
+ test ( "Passes props to client route component" , async ( { page } ) => {
2214
+ let port = await getPort ( ) ;
2215
+ stop = await setupRscTest ( {
2216
+ implementation,
2217
+ port,
2218
+ files : {
2219
+ "src/routes/home.tsx" : js `
2220
+ export { default, clientLoader, clientAction } from "./home.client";
2221
+ ` ,
2222
+ "src/routes/home.client.tsx" : js `
2223
+ "use client";
2224
+
2225
+ import { Form } from "react-router";
2226
+
2227
+ export async function clientLoader() {
2228
+ return { message: "Hello from client loader!" };
2229
+ }
2230
+
2231
+ export async function clientAction({ request }) {
2232
+ const formData = await request.formData();
2233
+ const name = formData.get("name") as string;
2234
+ return { actionResult: "Hello " + name + " from client action!" };
2235
+ }
2236
+
2237
+ export default function HomeRoute({ loaderData, actionData, matches, params }) {
2238
+ return (
2239
+ <div>
2240
+ <h2 data-home>Home Route</h2>
2241
+ {loaderData && (
2242
+ <p data-loader-data>{loaderData.message}</p>
2243
+ )}
2244
+ {actionData && (
2245
+ <p data-action-data>{actionData.actionResult}</p>
2246
+ )}
2247
+ {matches && (
2248
+ <div data-matches>
2249
+ <p data-matches-ids>matches ids: {matches.map(match => match.id).join(", ")}</p>
2250
+ </div>
2251
+ )}
2252
+ {params && (
2253
+ <div data-params>
2254
+ <p data-params-type>typeof params: {typeof params}</p>
2255
+ <p data-params-count>params count: {Object.keys(params).length}</p>
2256
+ </div>
2257
+ )}
2258
+ <Form method="post">
2259
+ <input name="name" data-name-input />
2260
+ <button type="submit" data-submit-button>
2261
+ Submit Action
2262
+ </button>
2263
+ </Form>
2264
+ </div>
2265
+ );
2266
+ }
2267
+ ` ,
2268
+ } ,
2269
+ } ) ;
2270
+
2271
+ await page . goto ( `http://localhost:${ port } /` ) ;
2272
+
2273
+ // Verify loader data is passed
2274
+ await page . waitForSelector ( "[data-loader-data]" ) ;
2275
+ expect ( await page . locator ( "[data-loader-data]" ) . textContent ( ) ) . toBe (
2276
+ "Hello from client loader!"
2277
+ ) ;
2278
+
2279
+ // Verify params are passed (empty for home route)
2280
+ await page . waitForSelector ( "[data-params]" ) ;
2281
+ await page . waitForSelector ( "[data-params-type]" ) ;
2282
+ await page . waitForSelector ( "[data-params-count]" ) ;
2283
+ expect ( await page . locator ( "[data-params-type]" ) . textContent ( ) ) . toBe (
2284
+ "typeof params: object"
2285
+ ) ;
2286
+ expect ( await page . locator ( "[data-params-count]" ) . textContent ( ) ) . toBe (
2287
+ "params count: 0"
2288
+ ) ;
2289
+
2290
+ // Verify matches are passed
2291
+ await page . waitForSelector ( "[data-matches]" ) ;
2292
+ await page . waitForSelector ( "[data-matches-ids]" ) ;
2293
+ expect ( await page . locator ( "[data-matches-ids]" ) . textContent ( ) ) . toBe (
2294
+ "matches ids: root, home"
2295
+ ) ;
2296
+
2297
+ // Submit the form to trigger the client action
2298
+ await page . fill ( "[data-name-input]" , "World" ) ;
2299
+ await page . click ( "[data-submit-button]" ) ;
2300
+
2301
+ // Verify the action data is displayed
2302
+ await page . waitForSelector ( "[data-action-data]" ) ;
2303
+ expect ( await page . locator ( "[data-action-data]" ) . textContent ( ) ) . toBe (
2304
+ "Hello World from client action!"
2305
+ ) ;
2306
+
2307
+ // Ensure this is using RSC
2308
+ validateRSCHtml ( await page . content ( ) ) ;
2309
+ } ) ;
2310
+
2311
+ test ( "Passes props to client ErrorBoundary when error is thrown in client loader" , async ( {
2312
+ page,
2313
+ } ) => {
2314
+ let port = await getPort ( ) ;
2315
+ stop = await setupRscTest ( {
2316
+ implementation,
2317
+ port,
2318
+ files : {
2319
+ "src/routes/home.tsx" : js `
2320
+ export { default, clientLoader, ErrorBoundary } from "./home.client";
2321
+ ` ,
2322
+ "src/routes/home.client.tsx" : js `
2323
+ "use client";
2324
+
2325
+ export async function clientLoader() {
2326
+ throw new Error("Intentional error from client loader");
2327
+ }
2328
+
2329
+ export function ErrorBoundary({ error, params }) {
2330
+ return (
2331
+ <div>
2332
+ <h2 data-error-title>Error Caught!</h2>
2333
+ <p data-error-message>{error.message}</p>
2334
+ {params && (
2335
+ <div data-error-params>
2336
+ <p data-error-params-type>typeof params: {typeof params}</p>
2337
+ <p data-error-params-count>params count: {Object.keys(params).length}</p>
2338
+ </div>
2339
+ )}
2340
+ </div>
2341
+ );
2342
+ }
2343
+
2344
+ export default function HomeRoute() {
2345
+ return (
2346
+ <div>
2347
+ <h2>Home Route</h2>
2348
+ </div>
2349
+ );
2350
+ }
2351
+ ` ,
2352
+ } ,
2353
+ } ) ;
2354
+
2355
+ await page . goto ( `http://localhost:${ port } /` ) ;
2356
+
2357
+ // Verify error boundary is shown
2358
+ await page . waitForSelector ( "[data-error-title]" ) ;
2359
+ await page . waitForSelector ( "[data-error-message]" ) ;
2360
+ expect ( await page . locator ( "[data-error-title]" ) . textContent ( ) ) . toBe (
2361
+ "Error Caught!"
2362
+ ) ;
2363
+ expect ( await page . locator ( "[data-error-message]" ) . textContent ( ) ) . toBe (
2364
+ "Intentional error from client loader"
2365
+ ) ;
2366
+
2367
+ // Verify params are passed to error boundary
2368
+ await page . waitForSelector ( "[data-error-params]" ) ;
2369
+ await page . waitForSelector ( "[data-error-params-type]" ) ;
2370
+ await page . waitForSelector ( "[data-error-params-count]" ) ;
2371
+ expect (
2372
+ await page . locator ( "[data-error-params-type]" ) . textContent ( )
2373
+ ) . toBe ( "typeof params: object" ) ;
2374
+ expect (
2375
+ await page . locator ( "[data-error-params-count]" ) . textContent ( )
2376
+ ) . toBe ( "params count: 0" ) ;
2377
+
2378
+ // Ensure this is using RSC
2379
+ validateRSCHtml ( await page . content ( ) ) ;
2380
+ } ) ;
2381
+
2382
+ test ( "Passes props to client ErrorBoundary when error is thrown in server loader" , async ( {
2383
+ page,
2384
+ } ) => {
2385
+ let port = await getPort ( ) ;
2386
+ stop = await setupRscTest ( {
2387
+ implementation,
2388
+ port,
2389
+ dev : true ,
2390
+ files : {
2391
+ "src/routes/home.tsx" : js `
2392
+ export function loader() {
2393
+ throw new Error("Intentional error from server loader");
2394
+ }
2395
+
2396
+ export default function HomeRoute() {
2397
+ return <h2>This should not be rendered</h2>;
2398
+ }
2399
+
2400
+ export { ErrorBoundary } from "./home.client";
2401
+ ` ,
2402
+ "src/routes/home.client.tsx" : js `
2403
+ "use client";
2404
+
2405
+ export function ErrorBoundary({ error, params }) {
2406
+ return (
2407
+ <div>
2408
+ <h2 data-error-title>Error Caught!</h2>
2409
+ <p data-error-message>{error.message}</p>
2410
+ {params && (
2411
+ <div data-error-params>
2412
+ <p data-error-params-type>typeof params: {typeof params}</p>
2413
+ <p data-error-params-count>params count: {Object.keys(params).length}</p>
2414
+ </div>
2415
+ )}
2416
+ </div>
2417
+ );
2418
+ }
2419
+ ` ,
2420
+ } ,
2421
+ } ) ;
2422
+
2423
+ await page . goto ( `http://localhost:${ port } /` ) ;
2424
+
2425
+ // Verify error boundary is shown
2426
+ await page . waitForSelector ( "[data-error-title]" ) ;
2427
+ await page . waitForSelector ( "[data-error-message]" ) ;
2428
+ expect ( await page . locator ( "[data-error-title]" ) . textContent ( ) ) . toBe (
2429
+ "Error Caught!"
2430
+ ) ;
2431
+ expect ( await page . locator ( "[data-error-message]" ) . textContent ( ) ) . toBe (
2432
+ "Intentional error from server loader"
2433
+ ) ;
2434
+
2435
+ // Verify params are passed to error boundary
2436
+ await page . waitForSelector ( "[data-error-params]" ) ;
2437
+ await page . waitForSelector ( "[data-error-params-type]" ) ;
2438
+ await page . waitForSelector ( "[data-error-params-count]" ) ;
2439
+ expect (
2440
+ await page . locator ( "[data-error-params-type]" ) . textContent ( )
2441
+ ) . toBe ( "typeof params: object" ) ;
2442
+ expect (
2443
+ await page . locator ( "[data-error-params-count]" ) . textContent ( )
2444
+ ) . toBe ( "params count: 0" ) ;
2445
+
2446
+ // Ensure this is using RSC
2447
+ validateRSCHtml ( await page . content ( ) ) ;
2448
+ } ) ;
2449
+
2450
+ test ( "Passes props to client HydrateFallback" , async ( { page } ) => {
2451
+ let port = await getPort ( ) ;
2452
+ stop = await setupRscTest ( {
2453
+ implementation,
2454
+ port,
2455
+ files : {
2456
+ "src/routes/home.tsx" : js `
2457
+ export { default, clientLoader, HydrateFallback } from "./home.client";
2458
+ ` ,
2459
+ "src/routes/home.client.tsx" : js `
2460
+ "use client";
2461
+
2462
+ export async function clientLoader() {
2463
+ const pollingPromise = (async () => {
2464
+ while (globalThis.unblockClientLoader !== true) {
2465
+ await new Promise((resolve) => setTimeout(resolve, 0));
2466
+ }
2467
+ })();
2468
+ const timeoutPromise = new Promise((_, reject) => {
2469
+ setTimeout(() => reject(new Error("Client loader wasn't unblocked after 5s")), 5000);
2470
+ });
2471
+ await Promise.race([pollingPromise, timeoutPromise]);
2472
+ return { message: "Hello from client loader!" };
2473
+ }
2474
+
2475
+ export function HydrateFallback({ params }) {
2476
+ return (
2477
+ <div>
2478
+ <h2 data-hydrate-fallback>Hydrate Fallback</h2>
2479
+ {params && (
2480
+ <div data-hydrate-params>
2481
+ <p data-hydrate-params-type>typeof params: {typeof params}</p>
2482
+ <p data-hydrate-params-count>params count: {Object.keys(params).length}</p>
2483
+ </div>
2484
+ )}
2485
+ </div>
2486
+ );
2487
+ }
2488
+
2489
+ export default function HomeRoute() {
2490
+ return (
2491
+ <div>
2492
+ <h2 data-home>Home Route</h2>
2493
+ </div>
2494
+ );
2495
+ }
2496
+ ` ,
2497
+ } ,
2498
+ } ) ;
2499
+
2500
+ await page . goto ( `http://localhost:${ port } /` ) ;
2501
+
2502
+ // Verify the hydrate fallback is shown initially
2503
+ await page . waitForSelector ( "[data-hydrate-fallback]" ) ;
2504
+ expect (
2505
+ await page . locator ( "[data-hydrate-fallback]" ) . textContent ( )
2506
+ ) . toBe ( "Hydrate Fallback" ) ;
2507
+
2508
+ // Verify params are passed to hydrate fallback
2509
+ await page . waitForSelector ( "[data-hydrate-params]" ) ;
2510
+ await page . waitForSelector ( "[data-hydrate-params-type]" ) ;
2511
+ await page . waitForSelector ( "[data-hydrate-params-count]" ) ;
2512
+ expect (
2513
+ await page . locator ( "[data-hydrate-params-type]" ) . textContent ( )
2514
+ ) . toBe ( "typeof params: object" ) ;
2515
+ expect (
2516
+ await page . locator ( "[data-hydrate-params-count]" ) . textContent ( )
2517
+ ) . toBe ( "params count: 0" ) ;
2518
+
2519
+ // Unblock the client loader to allow it to complete
2520
+ await page . evaluate ( ( ) => {
2521
+ ( globalThis as any ) . unblockClientLoader = true ;
2522
+ } ) ;
2523
+
2524
+ await page . waitForSelector ( "[data-home]" ) ;
2525
+
2526
+ // Ensure this is using RSC
2527
+ validateRSCHtml ( await page . content ( ) ) ;
2528
+ } ) ;
2529
+ } ) ;
2211
2530
} ) ;
2212
2531
} ) ;
0 commit comments