@@ -2208,5 +2208,324 @@ implementations.forEach((implementation) => {
22082208 validateRSCHtml ( await page . content ( ) ) ;
22092209 } ) ;
22102210 } ) ;
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+ } ) ;
22112530 } ) ;
22122531} ) ;
0 commit comments