Skip to content

Commit 7d8979d

Browse files
committed
Add e2e tests to detect layout shift when server rendering
1 parent 2ef1cab commit 7d8979d

File tree

7 files changed

+205
-27
lines changed

7 files changed

+205
-27
lines changed

integrations/next/app/page.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,62 @@
11
import { cookies } from "next/headers";
22
import { type Layout, Panel, Separator } from "react-resizable-panels";
33
import Group from "./components/Group";
4+
import { LayoutShiftDetecter } from "../../tests";
45

56
export default async function Home() {
6-
const defaultLayoutA = await getDefaultLayout("group-one");
7-
const defaultLayoutB = await getDefaultLayout("group-two");
7+
const defaultLayoutOne = await getDefaultLayout("group-one");
8+
const defaultLayoutTwo = await getDefaultLayout("group-two");
89

910
return (
1011
<div className="p-2 flex flex-col gap-2">
12+
<LayoutShiftDetecter />
1113
<Group
1214
className="h-25 gap-2"
13-
defaultLayout={defaultLayoutA}
15+
defaultLayout={defaultLayoutOne}
1416
id="group-one"
1517
>
1618
<Panel
1719
className="bg-slate-800 rounded rounded-md p-2"
1820
id="left"
1921
minSize={50}
2022
>
21-
left
23+
id: left
2224
</Panel>
2325
<Panel
2426
className="bg-slate-800 rounded rounded-md p-2"
2527
id="center"
2628
minSize={50}
2729
>
28-
center
30+
id: center
2931
</Panel>
3032
<Panel
3133
className="bg-slate-800 rounded rounded-md p-2"
3234
id="right"
3335
minSize={50}
3436
>
35-
right
37+
id: right
3638
</Panel>
3739
</Group>
3840
<Group
39-
className="h-25 gap-2"
40-
defaultLayout={defaultLayoutB}
41+
className="min-h-35 gap-2"
42+
defaultLayout={defaultLayoutTwo}
4143
id="group-two"
44+
orientation="vertical"
4245
>
4346
<Panel
4447
className="bg-slate-800 rounded rounded-md p-2"
45-
id="left"
46-
minSize={50}
48+
id="top"
49+
minSize={30}
4750
>
48-
left
51+
id: top
4952
</Panel>
50-
<Separator className="w-2 rounded rounded-md bg-slate-700" />
53+
<Separator className="h-2 rounded rounded-md bg-slate-700" />
5154
<Panel
5255
className="bg-slate-800 rounded rounded-md p-2"
53-
id="right"
54-
minSize={50}
56+
id="bottom"
57+
minSize={30}
5558
>
56-
right
59+
id: bottom
5760
</Panel>
5861
</Group>
5962
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
import { useEffect, useInsertionEffect, useState } from "react";
4+
5+
type PerformanceEntry = {
6+
hadRecentInput: boolean;
7+
sources: unknown[];
8+
value: number;
9+
};
10+
11+
export function LayoutShiftDetecter() {
12+
const [state, setState] = useState<number | null>(null);
13+
14+
useInsertionEffect(() => {
15+
const observer = new PerformanceObserver((list) => {
16+
for (const entry of list.getEntries() as unknown as PerformanceEntry[]) {
17+
if (!entry.hadRecentInput) {
18+
setState(entry.value);
19+
}
20+
}
21+
});
22+
23+
observer.observe({ type: "layout-shift", buffered: true });
24+
}, []);
25+
26+
useEffect(() => {
27+
const timeout = setTimeout(() => {
28+
setState((prevState) => (prevState === null ? 0 : prevState));
29+
}, 250);
30+
31+
return () => {
32+
clearTimeout(timeout);
33+
};
34+
}, []);
35+
36+
switch (state) {
37+
case null: {
38+
return (
39+
<div className="text-xs text-slate-500">Measuring layout shift ...</div>
40+
);
41+
}
42+
case 0: {
43+
return <div className="text-xs text-green-400">✅ No layout shift</div>;
44+
}
45+
default: {
46+
return (
47+
<div className="text-xs text-red-400">
48+
❌ Layout shift detected: {state}
49+
</div>
50+
);
51+
}
52+
}
53+
}

integrations/tests/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { Decoder } from "./components/Decoder";
2+
export { LayoutShiftDetecter } from "./components/LayoutShiftDetecter";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { expect, test } from "@playwright/test";
2+
import { resizeHelper } from "../src/utils/pointer-interactions/resizeHelper";
3+
4+
// High level tests; more nuanced scenarios are covered by unit tests
5+
test.describe("default layouts", () => {
6+
test("should not cause layout shift for client components", async ({
7+
page
8+
}) => {
9+
await page.goto("http://localhost:3012/");
10+
await expect(page.getByText("No layout shift")).toBeVisible();
11+
12+
await resizeHelper(page, ["top", "bottom"], 0, -25);
13+
await page.waitForTimeout(1000); // Wait for saved layout to be flushed
14+
15+
await page.reload({ waitUntil: "domcontentloaded" });
16+
await expect(page.getByText("No layout shift")).toBeVisible();
17+
});
18+
19+
test("should not cause layout shift for server-rendered client components", async ({
20+
page
21+
}) => {
22+
await page.goto("http://localhost:3011/");
23+
await expect(page.getByText("No layout shift")).toBeVisible();
24+
25+
await resizeHelper(page, ["top", "bottom"], 0, -25);
26+
await page.waitForTimeout(1000); // Wait for saved layout to be flushed
27+
28+
await page.reload({ waitUntil: "domcontentloaded" });
29+
await expect(page.getByText("No layout shift")).toBeVisible();
30+
});
31+
32+
test("should not cause layout shift for server components", async ({
33+
page
34+
}) => {
35+
await page.goto("http://localhost:3010/");
36+
await expect(page.getByText("No layout shift")).toBeVisible();
37+
38+
await resizeHelper(page, ["top", "bottom"], 0, -25);
39+
await page.waitForTimeout(1000); // Wait for saved layout to be flushed
40+
41+
await page.reload({ waitUntil: "domcontentloaded" });
42+
await expect(page.getByText("No layout shift")).toBeVisible();
43+
});
44+
});

integrations/vike/pages/index/+Page.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Separator,
55
useDefaultLayout
66
} from "react-resizable-panels";
7+
import { LayoutShiftDetecter } from "../../../tests";
78
import { useCookieStorage } from "./useCookieStorage";
89

910
export default function Page() {
@@ -21,44 +22,50 @@ export default function Page() {
2122

2223
return (
2324
<div className="p-2 flex flex-col gap-2">
24-
<Group className="h-25 gap-2" id="group-one" {...groupOneProps}>
25+
<LayoutShiftDetecter />
26+
27+
<Group className="h-25 gap-2" {...groupOneProps}>
2528
<Panel
2629
className="bg-slate-800 rounded rounded-md p-2"
2730
id="left"
2831
minSize={50}
2932
>
30-
left
33+
id: left
3134
</Panel>
3235
<Panel
3336
className="bg-slate-800 rounded rounded-md p-2"
3437
id="center"
3538
minSize={50}
3639
>
37-
center
40+
id: center
3841
</Panel>
3942
<Panel
4043
className="bg-slate-800 rounded rounded-md p-2"
4144
id="right"
4245
minSize={50}
4346
>
44-
right
47+
id: right
4548
</Panel>
4649
</Group>
47-
<Group className="h-25 gap-2" id="group-two" {...groupTwoProps}>
50+
<Group
51+
className="min-h-35 gap-2"
52+
orientation="vertical"
53+
{...groupTwoProps}
54+
>
4855
<Panel
4956
className="bg-slate-800 rounded rounded-md p-2"
50-
id="left"
51-
minSize={50}
57+
id="top"
58+
minSize={30}
5259
>
53-
left
60+
id: top
5461
</Panel>
55-
<Separator className="w-2 rounded rounded-md bg-slate-600" />
62+
<Separator className="h-2 rounded rounded-md bg-slate-700" />
5663
<Panel
5764
className="bg-slate-800 rounded rounded-md p-2"
58-
id="right"
59-
minSize={50}
65+
id="bottom"
66+
minSize={30}
6067
>
61-
right
68+
id: bottom
6269
</Panel>
6370
</Group>
6471
</div>

integrations/vite/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33
import { BrowserRouter, Route, Routes } from "react-router";
44
import { DecoderRoute } from "./routes/Decoder";
5+
import { HomeRoute } from "./routes/Home";
56

67
import "./tailwind.css";
78

89
createRoot(document.getElementById("root")!).render(
910
<StrictMode>
1011
<BrowserRouter>
1112
<Routes>
13+
<Route path="/" element={<HomeRoute />} />
1214
<Route path="/decoder/:encoded" element={<DecoderRoute />} />
1315
</Routes>
1416
</BrowserRouter>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
Group,
3+
Panel,
4+
Separator,
5+
useDefaultLayout
6+
} from "react-resizable-panels";
7+
import { LayoutShiftDetecter } from "../../../tests";
8+
9+
export function HomeRoute() {
10+
const groupOneProps = useDefaultLayout({
11+
id: "group-one"
12+
});
13+
14+
const groupTwoProps = useDefaultLayout({
15+
id: "group-two"
16+
});
17+
18+
return (
19+
<div className="p-2 flex flex-col gap-2">
20+
<LayoutShiftDetecter />
21+
22+
<Group className="h-25 gap-2" {...groupOneProps}>
23+
<Panel
24+
className="bg-slate-800 rounded rounded-md p-2"
25+
id="left"
26+
minSize={50}
27+
>
28+
id: left
29+
</Panel>
30+
<Panel
31+
className="bg-slate-800 rounded rounded-md p-2"
32+
id="center"
33+
minSize={50}
34+
>
35+
id: center
36+
</Panel>
37+
<Panel
38+
className="bg-slate-800 rounded rounded-md p-2"
39+
id="right"
40+
minSize={50}
41+
>
42+
id: right
43+
</Panel>
44+
</Group>
45+
<Group
46+
className="min-h-35 gap-2"
47+
orientation="vertical"
48+
{...groupTwoProps}
49+
>
50+
<Panel
51+
className="bg-slate-800 rounded rounded-md p-2"
52+
id="top"
53+
minSize={30}
54+
>
55+
id: top
56+
</Panel>
57+
<Separator className="h-2 rounded rounded-md bg-slate-700" />
58+
<Panel
59+
className="bg-slate-800 rounded rounded-md p-2"
60+
id="bottom"
61+
minSize={30}
62+
>
63+
id: bottom
64+
</Panel>
65+
</Group>
66+
</div>
67+
);
68+
}

0 commit comments

Comments
 (0)