Skip to content

Commit f46fcae

Browse files
authored
test: sync e2e with latest aws (#540)
1 parent ca7df8e commit f46fcae

File tree

9 files changed

+327
-1
lines changed

9 files changed

+327
-1
lines changed

.changeset/rich-bears-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
test: sync e2e with aws
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
2+
export const dynamicParams = false; // or true, to make it try SSR unknown paths
3+
4+
const POSTS = Array.from({ length: 20 }, (_, i) => ({
5+
id: String(i + 1),
6+
title: `Post ${i + 1}`,
7+
content: `This is post ${i + 1}`,
8+
}));
9+
10+
async function fakeGetPostsFetch() {
11+
return POSTS.slice(0, 10);
12+
}
13+
14+
async function fakeGetPostFetch(id: string) {
15+
return POSTS.find((post) => post.id === id);
16+
}
17+
18+
export async function generateStaticParams() {
19+
const fakePosts = await fakeGetPostsFetch();
20+
return fakePosts.map((post) => ({
21+
id: post.id,
22+
}));
23+
}
24+
25+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
26+
const { id } = await params;
27+
const post = await fakeGetPostFetch(id);
28+
return (
29+
<main>
30+
<h1 data-testid="title">{post?.title}</h1>
31+
<p data-testid="content">{post?.content}</p>
32+
</main>
33+
);
34+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { notFound } from "next/navigation";
2+
3+
// We'll prerender only the params from `generateStaticParams` at build time.
4+
// If a request comes in for a path that hasn't been generated,
5+
// Next.js will server-render the page on-demand.
6+
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
7+
export const dynamicParams = true; // or false, to 404 on unknown paths
8+
9+
const POSTS = Array.from({ length: 20 }, (_, i) => ({
10+
id: String(i + 1),
11+
title: `Post ${i + 1}`,
12+
content: `This is post ${i + 1}`,
13+
}));
14+
15+
async function fakeGetPostsFetch() {
16+
return POSTS.slice(0, 10);
17+
}
18+
19+
async function fakeGetPostFetch(id: string) {
20+
return POSTS.find((post) => post.id === id);
21+
}
22+
23+
export async function generateStaticParams() {
24+
const fakePosts = await fakeGetPostsFetch();
25+
return fakePosts.map((post) => ({
26+
id: post.id,
27+
}));
28+
}
29+
30+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
31+
const { id } = await params;
32+
const post = await fakeGetPostFetch(id);
33+
if (Number(id) === 1337) {
34+
throw new Error("This is an error!");
35+
}
36+
if (!post) {
37+
notFound();
38+
}
39+
return (
40+
<main>
41+
<h1 data-testid="title">{post.title}</h1>
42+
<p data-testid="content">{post.content}</p>
43+
</main>
44+
);
45+
}

examples/e2e/app-router/e2e/isr.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,59 @@ test("Incremental Static Regeneration with data cache", async ({ page }) => {
9898
expect(originalCachedDate).toEqual(finalCachedDate);
9999
expect(originalFetchedDate).toEqual(finalFetchedDate);
100100
});
101+
102+
test.describe("dynamicParams set to true", () => {
103+
test("should be HIT on a path that was prebuilt", async ({ page }) => {
104+
const res = await page.goto("/isr/dynamic-params-true/1");
105+
expect(res?.status()).toEqual(200);
106+
expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT");
107+
const title = await page.getByTestId("title").textContent();
108+
const content = await page.getByTestId("content").textContent();
109+
expect(title).toEqual("Post 1");
110+
expect(content).toEqual("This is post 1");
111+
});
112+
113+
// In `next start` this test would fail on subsequent requests because `x-nextjs-cache` would be `HIT`
114+
// However, once deployed to AWS, Cloudfront will cache `MISS`
115+
// We are gonna skip this one for now, turborepo caching can cause this page to be STALE once deployed
116+
test.skip("should SSR on a path that was not prebuilt", async ({ page }) => {
117+
const res = await page.goto("/isr/dynamic-params-true/11");
118+
expect(res?.headers()["x-nextjs-cache"]).toEqual("MISS");
119+
const title = await page.getByTestId("title").textContent();
120+
const content = await page.getByTestId("content").textContent();
121+
expect(title).toEqual("Post 11");
122+
expect(content).toEqual("This is post 11");
123+
});
124+
125+
test("should 404 when you call notFound", async ({ page }) => {
126+
const res = await page.goto("/isr/dynamic-params-true/21");
127+
expect(res?.status()).toEqual(404);
128+
expect(res?.headers()["cache-control"]).toBe("private, no-cache, no-store, max-age=0, must-revalidate");
129+
await expect(page.getByText("404")).toBeAttached();
130+
});
131+
132+
test("should 500 for a path that throws an error", async ({ page }) => {
133+
const res = await page.goto("/isr/dynamic-params-true/1337");
134+
expect(res?.status()).toEqual(500);
135+
expect(res?.headers()["cache-control"]).toBe("private, no-cache, no-store, max-age=0, must-revalidate");
136+
});
137+
});
138+
139+
test.describe("dynamicParams set to false", () => {
140+
test("should be HIT on a path that was prebuilt", async ({ page }) => {
141+
const res = await page.goto("/isr/dynamic-params-false/1");
142+
expect(res?.status()).toEqual(200);
143+
expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT");
144+
const title = await page.getByTestId("title").textContent();
145+
const content = await page.getByTestId("content").textContent();
146+
expect(title).toEqual("Post 1");
147+
expect(content).toEqual("This is post 1");
148+
});
149+
150+
test("should 404 for a path that is not found", async ({ page }) => {
151+
const res = await page.goto("/isr/dynamic-params-false/11");
152+
expect(res?.status()).toEqual(404);
153+
expect(res?.headers()["cache-control"]).toBe("private, no-cache, no-store, max-age=0, must-revalidate");
154+
await expect(page.getByText("404")).toBeAttached();
155+
});
156+
});

examples/e2e/app-router/e2e/middleware.redirect.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import { expect, test } from "@playwright/test";
2+
import { validateMd5 } from "../../utils";
3+
4+
/*
5+
* `curl -s https://opennext.js.org/share.png | md5sum`
6+
* This is the MD5 hash of the image. It is used to validate the image content.
7+
*/
8+
const OPENNEXT_PNG_MD5 = "405f45cc3397b09717a13ebd6f1e027b";
29

310
test("Middleware Redirect", async ({ page, context }) => {
411
await page.goto("/");
@@ -18,3 +25,14 @@ test("Middleware Redirect", async ({ page, context }) => {
1825
el = page.getByText("Redirect Destination", { exact: true });
1926
await expect(el).toBeVisible();
2027
});
28+
29+
test("Middleware Rewrite External Image", async ({ page }) => {
30+
await page.goto("/rewrite-external");
31+
page.on("response", async (response) => {
32+
expect(response.status()).toBe(200);
33+
expect(response.headers()["content-type"]).toBe("image/png");
34+
expect(response.headers()["cache-control"]).toBe("max-age=600");
35+
const bodyBuffer = await response.body();
36+
expect(validateMd5(bodyBuffer, OPENNEXT_PNG_MD5)).toBe(true);
37+
});
38+
});

examples/e2e/app-router/middleware.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ export function middleware(request: NextRequest) {
2424
},
2525
});
2626
}
27-
const requestHeaders = new Headers();
27+
if (path === "/rewrite-external") {
28+
const u = new URL("https://opennext.js.org/share.png");
29+
return NextResponse.rewrite(u);
30+
}
31+
const requestHeaders = new Headers(request.headers);
2832
// Setting the Request Headers, this should be available in RSC
2933
requestHeaders.set("request-header", "request-header");
3034
requestHeaders.set("search-params", `mw/${request.nextUrl.searchParams.get("searchParams") || ""}`);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
const SADE_SMOOTH_OPERATOR_LYRIC = `Diamond life, lover boy
4+
He move in space with minimum waste and maximum joy
5+
City lights and business nights
6+
When you require streetcar desire for higher heights
7+
No place for beginners or sensitive hearts
8+
When sentiment is left to chance
9+
No place to be ending but somewhere to start
10+
No need to ask, he's a smooth operator
11+
Smooth operator, smooth operator
12+
Smooth operator`;
13+
14+
test("streaming should work in api route", async ({ page }) => {
15+
await page.goto("/sse");
16+
17+
// wait for first line to be present
18+
await page.getByTestId("line").first().waitFor();
19+
const initialLines = await page.getByTestId("line").count();
20+
// fail if all lines appear at once
21+
// this is a safeguard to ensure that the response is streamed and not buffered all at once
22+
expect(initialLines).toBe(1);
23+
24+
const seenLines: Array<{ line: string; time: number }> = [];
25+
const startTime = Date.now();
26+
27+
// we loop until we see all lines
28+
while (seenLines.length < SADE_SMOOTH_OPERATOR_LYRIC.split("\n").length) {
29+
const lines = await page.getByTestId("line").all();
30+
if (lines.length > seenLines.length) {
31+
expect(lines.length).toBe(seenLines.length + 1);
32+
const newLine = lines[lines.length - 1];
33+
seenLines.push({
34+
line: await newLine.innerText(),
35+
time: Date.now() - startTime,
36+
});
37+
}
38+
// wait for a bit before checking again
39+
await page.waitForTimeout(200);
40+
}
41+
42+
expect(seenLines.map((n) => n.line)).toEqual(SADE_SMOOTH_OPERATOR_LYRIC.split("\n"));
43+
for (let i = 1; i < seenLines.length; i++) {
44+
expect(seenLines[i].time - seenLines[i - 1].time).toBeGreaterThan(500);
45+
}
46+
47+
await expect(page.getByTestId("video")).toBeVisible();
48+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
const SADE_SMOOTH_OPERATOR_LYRIC = `Diamond life, lover boy
4+
He move in space with minimum waste and maximum joy
5+
City lights and business nights
6+
When you require streetcar desire for higher heights
7+
No place for beginners or sensitive hearts
8+
When sentiment is left to chance
9+
No place to be ending but somewhere to start
10+
No need to ask, he's a smooth operator
11+
Smooth operator, smooth operator
12+
Smooth operator`;
13+
14+
function sleep(ms: number) {
15+
return new Promise((resolve) => {
16+
setTimeout(resolve, ms);
17+
});
18+
}
19+
20+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
21+
if (req.method !== "GET") {
22+
return res.status(405).json({ message: "Method not allowed" });
23+
}
24+
25+
res.setHeader("Content-Type", "text/event-stream");
26+
res.setHeader("Connection", "keep-alive");
27+
res.setHeader("Cache-Control", "no-cache, no-transform");
28+
res.setHeader("Transfer-Encoding", "chunked");
29+
30+
res.write(`data: ${JSON.stringify({ type: "start", model: "ai-lyric-model" })}\n\n`);
31+
await sleep(1000);
32+
33+
const lines = SADE_SMOOTH_OPERATOR_LYRIC.split("\n");
34+
for (const line of lines) {
35+
res.write(`data: ${JSON.stringify({ type: "content", body: line })}\n\n`);
36+
await sleep(1000);
37+
}
38+
39+
res.write(`data: ${JSON.stringify({ type: "complete" })}\n\n`);
40+
41+
res.end();
42+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
type Event = {
6+
type: "start" | "content" | "complete";
7+
model?: string;
8+
body?: string;
9+
};
10+
11+
export default function SSE() {
12+
const [events, setEvents] = useState<Event[]>([]);
13+
const [finished, setFinished] = useState(false);
14+
15+
useEffect(() => {
16+
const e = new EventSource("/api/streaming");
17+
18+
e.onmessage = (msg) => {
19+
console.log(msg);
20+
try {
21+
const data = JSON.parse(msg.data) as Event;
22+
if (data.type === "complete") {
23+
e.close();
24+
setFinished(true);
25+
}
26+
if (data.type === "content") {
27+
setEvents((prev) => prev.concat(data));
28+
}
29+
} catch (err) {
30+
console.error(err, msg);
31+
}
32+
};
33+
}, []);
34+
35+
return (
36+
<div
37+
style={{
38+
padding: "20px",
39+
marginBottom: "20px",
40+
display: "flex",
41+
flexDirection: "column",
42+
gap: "40px",
43+
}}
44+
>
45+
<h1
46+
style={{
47+
fontSize: "2rem",
48+
marginBottom: "20px",
49+
}}
50+
>
51+
Sade - Smooth Operator
52+
</h1>
53+
<div>
54+
{events.map((e, i) => (
55+
<p data-testid="line" key={i}>
56+
{e.body}
57+
</p>
58+
))}
59+
</div>
60+
{finished && (
61+
<iframe
62+
data-testid="video"
63+
width="560"
64+
height="315"
65+
src="https://www.youtube.com/embed/4TYv2PhG89A?si=e1fmpiXZZ1PBKPE5"
66+
title="YouTube video player"
67+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
68+
referrerPolicy="strict-origin-when-cross-origin"
69+
allowFullScreen
70+
></iframe>
71+
)}
72+
</div>
73+
);
74+
}

0 commit comments

Comments
 (0)