Skip to content

Commit ba7b59c

Browse files
authored
Minor client data edge case fixes (#8253)
1 parent eaa9cec commit ba7b59c

File tree

1 file changed

+319
-4
lines changed

1 file changed

+319
-4
lines changed

integration/client-data-test.ts

Lines changed: 319 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,6 @@ test.describe("Client Data", () => {
315315
childClientLoader: false,
316316
childClientLoaderHydrate: false,
317317
}),
318-
// Blow away parent.child.tsx with our own deferred version
319318
"app/routes/parent.child.tsx": js`
320319
import * as React from 'react';
321320
import { defer, json } from '@remix-run/node'
@@ -415,7 +414,6 @@ test.describe("Client Data", () => {
415414
childClientLoader: false,
416415
childClientLoaderHydrate: false,
417416
}),
418-
// Blow away parent.child.tsx with our own version
419417
"app/routes/parent.child.tsx": js`
420418
import * as React from 'react';
421419
import { json } from '@remix-run/node';
@@ -470,7 +468,6 @@ test.describe("Client Data", () => {
470468
childClientLoader: false,
471469
childClientLoaderHydrate: false,
472470
}),
473-
// Blow away parent.child.tsx with our own version without a server loader
474471
"app/routes/parent.child.tsx": js`
475472
import * as React from 'react';
476473
import { useLoaderData } from '@remix-run/react';
@@ -514,7 +511,6 @@ test.describe("Client Data", () => {
514511
childClientLoader: false,
515512
childClientLoaderHydrate: false,
516513
}),
517-
// Blow away parent.child.tsx with our own version without a server loader
518514
"app/routes/parent.child.tsx": js`
519515
import * as React from 'react';
520516
import { useLoaderData } from '@remix-run/react';
@@ -545,6 +541,189 @@ test.describe("Client Data", () => {
545541
html = await app.getHtml("main");
546542
expect(html).toMatch("Loader Data (clientLoader only)");
547543
});
544+
545+
test("throws a 400 if you call serverLoader without a server loader", async ({
546+
page,
547+
}) => {
548+
appFixture = await createAppFixture(
549+
await createFixture({
550+
files: {
551+
...getFiles({
552+
parentClientLoader: false,
553+
parentClientLoaderHydrate: false,
554+
childClientLoader: false,
555+
childClientLoaderHydrate: false,
556+
}),
557+
"app/routes/parent.child.tsx": js`
558+
import * as React from 'react';
559+
import { useLoaderData, useRouteError } from '@remix-run/react';
560+
export async function clientLoader({ serverLoader }) {
561+
return await serverLoader();
562+
}
563+
export default function Component() {
564+
return <p>Child</p>;
565+
}
566+
export function HydrateFallback() {
567+
return <p>Loading...</p>;
568+
}
569+
export function ErrorBoundary() {
570+
let error = useRouteError();
571+
return <p id="child-error">{error.status} {error.data}</p>;
572+
}
573+
`,
574+
},
575+
})
576+
);
577+
let app = new PlaywrightFixture(appFixture, page);
578+
579+
await app.goto("/parent/child");
580+
await page.waitForSelector("#child-error");
581+
let html = await app.getHtml("#child-error");
582+
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
583+
"400 Error: You are trying to call serverLoader() on a route that does " +
584+
'not have a server loader (routeId: "routes/parent.child")'
585+
);
586+
});
587+
588+
test("initial hydration data check functions properly", async ({
589+
page,
590+
}) => {
591+
appFixture = await createAppFixture(
592+
await createFixture({
593+
files: {
594+
...getFiles({
595+
parentClientLoader: false,
596+
parentClientLoaderHydrate: false,
597+
childClientLoader: false,
598+
childClientLoaderHydrate: false,
599+
}),
600+
"app/routes/parent.child.tsx": js`
601+
import * as React from 'react';
602+
import { json } from '@remix-run/node';
603+
import { useLoaderData, useRevalidator } from '@remix-run/react';
604+
let isFirstCall = true;
605+
export async function loader({ serverLoader }) {
606+
if (isFirstCall) {
607+
isFirstCall = false
608+
return json({
609+
message: "Child Server Loader Data (1)",
610+
});
611+
}
612+
return json({
613+
message: "Child Server Loader Data (2+)",
614+
});
615+
}
616+
export async function clientLoader({ serverLoader }) {
617+
await new Promise(r => setTimeout(r, 100));
618+
let serverData = await serverLoader();
619+
return {
620+
message: serverData.message + " (mutated by client)",
621+
};
622+
}
623+
clientLoader.hydrate=true;
624+
export default function Component() {
625+
let data = useLoaderData();
626+
let revalidator = useRevalidator();
627+
return (
628+
<>
629+
<p id="child-data">{data.message}</p>
630+
<button onClick={() => revalidator.revalidate()}>Revalidate</button>
631+
</>
632+
);
633+
}
634+
export function HydrateFallback() {
635+
return <p>Loading...</p>
636+
}
637+
`,
638+
},
639+
})
640+
);
641+
let app = new PlaywrightFixture(appFixture, page);
642+
643+
await app.goto("/parent/child");
644+
await page.waitForSelector("#child-data");
645+
let html = await app.getHtml();
646+
expect(html).toMatch("Child Server Loader Data (1) (mutated by client)");
647+
app.clickElement("button");
648+
await page.waitForSelector(':has-text("Child Server Loader Data (2+)")');
649+
html = await app.getHtml("main");
650+
expect(html).toMatch("Child Server Loader Data (2+) (mutated by client)");
651+
});
652+
653+
test("initial hydration data check functions properly even if serverLoader isn't called on hydration", async ({
654+
page,
655+
}) => {
656+
appFixture = await createAppFixture(
657+
await createFixture({
658+
files: {
659+
...getFiles({
660+
parentClientLoader: false,
661+
parentClientLoaderHydrate: false,
662+
childClientLoader: false,
663+
childClientLoaderHydrate: false,
664+
}),
665+
"app/routes/parent.child.tsx": js`
666+
import * as React from 'react';
667+
import { json } from '@remix-run/node';
668+
import { useLoaderData, useRevalidator } from '@remix-run/react';
669+
let isFirstCall = true;
670+
export async function loader({ serverLoader }) {
671+
if (isFirstCall) {
672+
isFirstCall = false
673+
return json({
674+
message: "Child Server Loader Data (1)",
675+
});
676+
}
677+
return json({
678+
message: "Child Server Loader Data (2+)",
679+
});
680+
}
681+
let isFirstClientCall = true;
682+
export async function clientLoader({ serverLoader }) {
683+
await new Promise(r => setTimeout(r, 100));
684+
if (isFirstClientCall) {
685+
isFirstClientCall = false;
686+
// First time through - don't even call serverLoader
687+
return {
688+
message: "Child Client Loader Data",
689+
};
690+
}
691+
// Only call the serverLoader on subsequent calls and this
692+
// should *not* return us the initialData any longer
693+
let serverData = await serverLoader();
694+
return {
695+
message: serverData.message + " (mutated by client)",
696+
};
697+
}
698+
clientLoader.hydrate=true;
699+
export default function Component() {
700+
let data = useLoaderData();
701+
let revalidator = useRevalidator();
702+
return (
703+
<>
704+
<p id="child-data">{data.message}</p>
705+
<button onClick={() => revalidator.revalidate()}>Revalidate</button>
706+
</>
707+
);
708+
}
709+
export function HydrateFallback() {
710+
return <p>Loading...</p>
711+
}
712+
`,
713+
},
714+
})
715+
);
716+
let app = new PlaywrightFixture(appFixture, page);
717+
718+
await app.goto("/parent/child");
719+
await page.waitForSelector("#child-data");
720+
let html = await app.getHtml();
721+
expect(html).toMatch("Child Client Loader Data");
722+
app.clickElement("button");
723+
await page.waitForSelector(':has-text("Child Server Loader Data (2+)")');
724+
html = await app.getHtml("main");
725+
expect(html).toMatch("Child Server Loader Data (2+) (mutated by client)");
726+
});
548727
});
549728

550729
test.describe("clientLoader - lazy route module", () => {
@@ -632,6 +811,50 @@ test.describe("Client Data", () => {
632811
expect(html).toMatch("Parent Server Loader (mutated by client)");
633812
expect(html).toMatch("Child Server Loader (mutated by client");
634813
});
814+
815+
test("throws a 400 if you call serverLoader without a server loader", async ({
816+
page,
817+
}) => {
818+
appFixture = await createAppFixture(
819+
await createFixture({
820+
files: {
821+
...getFiles({
822+
parentClientLoader: false,
823+
parentClientLoaderHydrate: false,
824+
childClientLoader: false,
825+
childClientLoaderHydrate: false,
826+
}),
827+
"app/routes/parent.child.tsx": js`
828+
import * as React from 'react';
829+
import { useLoaderData, useRouteError } from '@remix-run/react';
830+
export async function clientLoader({ serverLoader }) {
831+
return await serverLoader();
832+
}
833+
export default function Component() {
834+
return <p>Child</p>;
835+
}
836+
export function HydrateFallback() {
837+
return <p>Loading...</p>;
838+
}
839+
export function ErrorBoundary() {
840+
let error = useRouteError();
841+
return <p id="child-error">{error.status} {error.data}</p>;
842+
}
843+
`,
844+
},
845+
})
846+
);
847+
let app = new PlaywrightFixture(appFixture, page);
848+
849+
await app.goto("/");
850+
await app.clickLink("/parent/child");
851+
await page.waitForSelector("#child-error");
852+
let html = await app.getHtml("#child-error");
853+
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
854+
"400 Error: You are trying to call serverLoader() on a route that does " +
855+
'not have a server loader (routeId: "routes/parent.child")'
856+
);
857+
});
635858
});
636859

637860
test.describe("clientAction - critical route module", () => {
@@ -796,6 +1019,51 @@ test.describe("Client Data", () => {
7961019
expect(html).toMatch("Child Server Loader (mutated by client)");
7971020
expect(html).toMatch("Child Server Action (mutated by client)");
7981021
});
1022+
1023+
test("throws a 400 if you call serverAction without a server action", async ({
1024+
page,
1025+
}) => {
1026+
appFixture = await createAppFixture(
1027+
await createFixture({
1028+
files: {
1029+
...getFiles({
1030+
parentClientLoader: false,
1031+
parentClientLoaderHydrate: false,
1032+
childClientLoader: false,
1033+
childClientLoaderHydrate: false,
1034+
}),
1035+
"app/routes/parent.child.tsx": js`
1036+
import * as React from 'react';
1037+
import { json } from '@remix-run/node';
1038+
import { Form, useRouteError } from '@remix-run/react';
1039+
export async function clientAction({ serverAction }) {
1040+
return await serverAction();
1041+
}
1042+
export default function Component() {
1043+
return (
1044+
<Form method="post">
1045+
<button type="submit">Submit</button>
1046+
</Form>
1047+
);
1048+
}
1049+
export function ErrorBoundary() {
1050+
let error = useRouteError();
1051+
return <p id="child-error">{error.status} {error.data}</p>;
1052+
}
1053+
`,
1054+
},
1055+
})
1056+
);
1057+
let app = new PlaywrightFixture(appFixture, page);
1058+
await app.goto("/parent/child");
1059+
app.clickSubmitButton("/parent/child");
1060+
await page.waitForSelector("#child-error");
1061+
let html = await app.getHtml("#child-error");
1062+
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
1063+
"400 Error: You are trying to call serverAction() on a route that does " +
1064+
'not have a server action (routeId: "routes/parent.child")'
1065+
);
1066+
});
7991067
});
8001068

8011069
test.describe("clientAction - lazy route module", () => {
@@ -968,5 +1236,52 @@ test.describe("Client Data", () => {
9681236
expect(html).toMatch("Child Server Loader (mutated by client)");
9691237
expect(html).toMatch("Child Server Action (mutated by client)");
9701238
});
1239+
1240+
test("throws a 400 if you call serverAction without a server action", async ({
1241+
page,
1242+
}) => {
1243+
appFixture = await createAppFixture(
1244+
await createFixture({
1245+
files: {
1246+
...getFiles({
1247+
parentClientLoader: false,
1248+
parentClientLoaderHydrate: false,
1249+
childClientLoader: false,
1250+
childClientLoaderHydrate: false,
1251+
}),
1252+
"app/routes/parent.child.tsx": js`
1253+
import * as React from 'react';
1254+
import { json } from '@remix-run/node';
1255+
import { Form, useRouteError } from '@remix-run/react';
1256+
export async function clientAction({ serverAction }) {
1257+
return await serverAction();
1258+
}
1259+
export default function Component() {
1260+
return (
1261+
<Form method="post">
1262+
<button type="submit">Submit</button>
1263+
</Form>
1264+
);
1265+
}
1266+
export function ErrorBoundary() {
1267+
let error = useRouteError();
1268+
return <p id="child-error">{error.status} {error.data}</p>;
1269+
}
1270+
`,
1271+
},
1272+
})
1273+
);
1274+
let app = new PlaywrightFixture(appFixture, page);
1275+
await app.goto("/");
1276+
await app.goto("/parent/child");
1277+
await page.waitForSelector("form");
1278+
app.clickSubmitButton("/parent/child");
1279+
await page.waitForSelector("#child-error");
1280+
let html = await app.getHtml("#child-error");
1281+
expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch(
1282+
"400 Error: You are trying to call serverAction() on a route that does " +
1283+
'not have a server action (routeId: "routes/parent.child")'
1284+
);
1285+
});
9711286
});
9721287
});

0 commit comments

Comments
 (0)