@@ -846,6 +846,121 @@ test.describe("Client Data", () => {
846846 expect ( html ) . not . toMatch ( "Should not see me" ) ;
847847 console . error = _consoleError ;
848848 } ) ;
849+
850+ test ( "bubbled server loader errors are persisted for hydrating routes" , async ( {
851+ page,
852+ } ) => {
853+ let _consoleError = console . error ;
854+ console . error = ( ) => { } ;
855+ appFixture = await createAppFixture (
856+ await createFixture (
857+ {
858+ files : {
859+ ...getFiles ( {
860+ parentClientLoader : false ,
861+ parentClientLoaderHydrate : false ,
862+ childClientLoader : false ,
863+ childClientLoaderHydrate : false ,
864+ } ) ,
865+ "app/routes/parent.tsx" : js `
866+ import { json } from '@remix-run/node'
867+ import { Outlet, useLoaderData, useRouteLoaderData, useRouteError } from '@remix-run/react'
868+
869+ export function loader() {
870+ return json({ message: 'Parent Server Loader'});
871+ }
872+
873+ export async function clientLoader({ serverLoader }) {
874+ console.log('running parent client loader')
875+ // Need a small delay to ensure we capture the server-rendered
876+ // fallbacks for assertions
877+ await new Promise(r => setTimeout(r, 100));
878+ let data = await serverLoader();
879+ return { message: data.message + " (mutated by client)" };
880+ }
881+
882+ clientLoader.hydrate = true;
883+
884+ export default function Component() {
885+ let data = useLoaderData();
886+ return (
887+ <>
888+ <p id="parent-data">{data.message}</p>
889+ <Outlet/>
890+ </>
891+ );
892+ }
893+
894+ export function ErrorBoundary() {
895+ let data = useRouteLoaderData("routes/parent")
896+ let error = useRouteError();
897+ return (
898+ <>
899+ <h1>Parent Error</h1>
900+ <p id="parent-data">{data?.message}</p>
901+ <p id="parent-error">{error?.data?.message}</p>
902+ </>
903+ );
904+ }
905+ ` ,
906+ "app/routes/parent.child.tsx" : js `
907+ import { json } from '@remix-run/node'
908+ import { useRouteError, useLoaderData } from '@remix-run/react'
909+
910+ export function loader() {
911+ throw json({ message: 'Child Server Error'});
912+ }
913+
914+ export function clientLoader() {
915+ console.log('running child client loader')
916+ return "Should not see me";
917+ }
918+
919+ clientLoader.hydrate = true;
920+
921+ export default function Component() {
922+ let data = useLoaderData()
923+ return (
924+ <>
925+ <p>Should not see me</p>
926+ <p>{data}</p>;
927+ </>
928+ );
929+ }
930+ ` ,
931+ } ,
932+ } ,
933+ ServerMode . Development // Avoid error sanitization
934+ ) ,
935+ ServerMode . Development // Avoid error sanitization
936+ ) ;
937+ let app = new PlaywrightFixture ( appFixture , page ) ;
938+
939+ let logs : string [ ] = [ ] ;
940+ page . on ( "console" , ( msg ) => logs . push ( msg . text ( ) ) ) ;
941+
942+ await app . goto ( "/parent/child" , false ) ;
943+ let html = await app . getHtml ( "main" ) ;
944+ expect ( html ) . toMatch ( "Parent Server Loader</p>" ) ;
945+ expect ( html ) . toMatch ( "Child Server Error" ) ;
946+ expect ( html ) . not . toMatch ( "Should not see me" ) ;
947+
948+ // Ensure we hydrate and remain on the boundary
949+ await page . waitForSelector (
950+ ":has-text('Parent Server Loader (mutated by client)')"
951+ ) ;
952+ html = await app . getHtml ( "main" ) ;
953+ expect ( html ) . toMatch ( "Parent Server Loader (mutated by client)</p>" ) ;
954+ expect ( html ) . toMatch ( "Child Server Error" ) ;
955+ expect ( html ) . not . toMatch ( "Should not see me" ) ;
956+
957+ expect ( logs ) . toEqual ( [
958+ expect . stringContaining ( "Download the React DevTools" ) ,
959+ "running parent client loader" ,
960+ ] ) ;
961+
962+ console . error = _consoleError ;
963+ } ) ;
849964 } ) ;
850965
851966 test . describe ( "clientLoader - lazy route module" , ( ) => {
0 commit comments