Skip to content

Commit 79a9647

Browse files
authored
add e2e for streaming in pages-router (#792)
* add routehandler and api route * add streaming to pages-router * add e2e for both * fix e2e * make typescript happy * rm from app router * add comment * review fix
1 parent 9bc06e4 commit 79a9647

File tree

5 files changed

+184
-1
lines changed

5 files changed

+184
-1
lines changed

examples/pages-router/open-next.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
const config = {
2-
default: {},
2+
default: {
3+
override: {
4+
wrapper: "aws-lambda-streaming",
5+
},
6+
},
37
functions: {},
48
buildCommand: "npx turbo build",
59
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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(
21+
req: NextApiRequest,
22+
res: NextApiResponse,
23+
) {
24+
if (req.method !== "GET") {
25+
return res.status(405).json({ message: "Method not allowed" });
26+
}
27+
28+
res.setHeader("Content-Type", "text/event-stream");
29+
res.setHeader("Connection", "keep-alive");
30+
res.setHeader("Cache-Control", "no-cache, no-transform");
31+
res.setHeader("Transfer-Encoding", "chunked");
32+
33+
res.write(
34+
`data: ${JSON.stringify({ type: "start", model: "ai-lyric-model" })}\n\n`,
35+
);
36+
await sleep(1000);
37+
38+
const lines = SADE_SMOOTH_OPERATOR_LYRIC.split("\n");
39+
for (const line of lines) {
40+
res.write(`data: ${JSON.stringify({ type: "content", body: line })}\n\n`);
41+
await sleep(1000);
42+
}
43+
44+
res.write(`data: ${JSON.stringify({ type: "complete" })}\n\n`);
45+
46+
res.end();
47+
}
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+
}

examples/sst/stacks/PagesRouter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplement
33
export function PagesRouter({ stack }) {
44
const site = new OpenNextCdkReferenceImplementation(stack, "pagesrouter", {
55
path: "../pages-router",
6+
/*
7+
* We need to set this environment variable to not break other E2E tests that have an empty body. (i.e: /redirect)
8+
* https://opennext.js.org/aws/common_issues#empty-body-in-response-when-streaming-in-aws-lambda
9+
*
10+
*/
11+
environment: {
12+
OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true",
13+
},
614
});
715
// const site = new NextjsSite(stack, "pagesrouter", {
816
// path: "../pages-router",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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(
43+
SADE_SMOOTH_OPERATOR_LYRIC.split("\n"),
44+
);
45+
for (let i = 1; i < seenLines.length; i++) {
46+
expect(seenLines[i].time - seenLines[i - 1].time).toBeGreaterThan(500);
47+
}
48+
49+
await expect(page.getByTestId("video")).toBeVisible();
50+
});

0 commit comments

Comments
 (0)