cloudflare/vinext

Public

mirrored from https://github.com/cloudflare/vinextAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.0.9

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

tests/ecosystem.test.ts

326lines · modecode

1/**
2 * Ecosystem integration tests — verifies popular third-party libraries
3 * work correctly with vinext.
4 *
5 * Uses subprocess-based testing: starts Vite dev server as a child process,
6 * waits for it to be ready, makes HTTP requests, and asserts SSR output.
7 * This approach is necessary because the RSC module runner in programmatic
8 * createServer() bypasses Vite's resolveId for `next` package resolution.
9 *
10 * Run with: npx vitest run tests/ecosystem.test.ts
11 */
12import { describe, it, expect, beforeAll, afterAll } from "vitest";
13import { spawn, type ChildProcess } from "node:child_process";
14import path from "node:path";
15
16const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "ecosystem");
17
18/**
19 * Start a Vite dev server as a child process and wait for it to be ready.
20 */
21async function startFixture(
22 name: string,
23 port: number,
24): Promise<{
25 process: ChildProcess;
26 baseUrl: string;
27 fetchPage: (pathname: string) => Promise<{ html: string; status: number }>;
28}> {
29 const root = path.join(FIXTURES_DIR, name);
30 const baseUrl = `http://localhost:${port}`;
31
32 const proc = spawn("npx", ["vite", "--port", String(port), "--strictPort"], {
33 cwd: root,
34 stdio: ["pipe", "pipe", "pipe"],
35 env: { ...process.env },
36 });
37
38 // Wait for the server to be ready
39 await new Promise<void>((resolve, reject) => {
40 const timeoutId = setTimeout(() => {
41 reject(new Error(`Fixture "${name}" did not start within 30s`));
42 }, 30000);
43
44 let output = "";
45 const onData = (data: Buffer) => {
46 output += data.toString();
47 if (output.includes("ready in") || output.includes("Local:")) {
48 clearTimeout(timeoutId);
49 resolve();
50 }
51 };
52
53 proc.stdout?.on("data", onData);
54 proc.stderr?.on("data", onData);
55 proc.on("error", (err) => {
56 clearTimeout(timeoutId);
57 reject(err);
58 });
59 proc.on("exit", (code) => {
60 if (code !== null && code !== 0) {
61 clearTimeout(timeoutId);
62 reject(
63 new Error(`Fixture "${name}" exited with code ${code}: ${output}`),
64 );
65 }
66 });
67 });
68
69 // Give the server a moment to be fully ready for requests
70 await new Promise((r) => setTimeout(r, 500));
71
72 async function fetchPage(pathname: string) {
73 const res = await fetch(`${baseUrl}${pathname}`, {
74 signal: AbortSignal.timeout(10000),
75 });
76 const html = await res.text();
77 return { html, status: res.status };
78 }
79
80 return { process: proc, baseUrl, fetchPage };
81}
82
83function killProcess(proc: ChildProcess | null) {
84 if (proc && !proc.killed) {
85 proc.kill("SIGTERM");
86 }
87}
88
89// ─── next-themes ──────────────────────────────────────────────────────────────
90describe("next-themes", () => {
91 let proc: ChildProcess | null = null;
92 let fetchPage: (path: string) => Promise<{ html: string; status: number }>;
93
94 beforeAll(async () => {
95 const fixture = await startFixture("next-themes", 4400);
96 proc = fixture.process;
97 fetchPage = fixture.fetchPage;
98 }, 30000);
99
100 afterAll(() => killProcess(proc));
101
102 it("renders SSR content", async () => {
103 const { html, status } = await fetchPage("/");
104 expect(status).toBe(200);
105 expect(html).toContain("next-themes test");
106 expect(html).toContain('data-testid="ssr-content"');
107 expect(html).toContain("Server-rendered content");
108 });
109
110 it("injects theme detection script", async () => {
111 const { html } = await fetchPage("/");
112 expect(html).toContain("prefers-color-scheme");
113 expect(html).toContain("localStorage");
114 });
115
116 it("sets html lang attribute", async () => {
117 const { html } = await fetchPage("/");
118 expect(html).toContain('<html lang="en"');
119 });
120
121 it("renders theme toggle buttons", async () => {
122 const { html } = await fetchPage("/");
123 expect(html).toContain('data-testid="theme-loading"');
124 });
125});
126
127// ─── next-view-transitions ────────────────────────────────────────────────────
128describe("next-view-transitions", () => {
129 let proc: ChildProcess | null = null;
130 let fetchPage: (path: string) => Promise<{ html: string; status: number }>;
131
132 beforeAll(async () => {
133 const fixture = await startFixture("next-view-transitions", 4401);
134 proc = fixture.process;
135 fetchPage = fixture.fetchPage;
136 }, 30000);
137
138 afterAll(() => killProcess(proc));
139
140 it("renders home page with view transition styles", async () => {
141 const { html, status } = await fetchPage("/");
142 expect(status).toBe(200);
143 expect(html).toContain("Home Page");
144 expect(html).toContain("view-transition-name:title");
145 });
146
147 it("renders Link component from next-view-transitions", async () => {
148 const { html } = await fetchPage("/");
149 expect(html).toContain('data-testid="about-link"');
150 expect(html).toContain('href="/about"');
151 });
152
153 it("renders about page", async () => {
154 const { html, status } = await fetchPage("/about");
155 expect(status).toBe(200);
156 expect(html).toContain("About Page");
157 expect(html).toContain("view-transition-name:title");
158 });
159
160 it("renders navigation links", async () => {
161 const { html } = await fetchPage("/");
162 expect(html).toContain('<a href="/">Home</a>');
163 expect(html).toContain('<a href="/about">About</a>');
164 });
165});
166
167// ─── nuqs ─────────────────────────────────────────────────────────────────────
168describe("nuqs", () => {
169 let proc: ChildProcess | null = null;
170 let fetchPage: (path: string) => Promise<{ html: string; status: number }>;
171
172 beforeAll(async () => {
173 const fixture = await startFixture("nuqs", 4402);
174 proc = fixture.process;
175 fetchPage = fixture.fetchPage;
176 }, 30000);
177
178 afterAll(() => killProcess(proc));
179
180 it("renders SSR content", async () => {
181 const { html, status } = await fetchPage("/");
182 expect(status).toBe(200);
183 expect(html).toContain("nuqs test");
184 expect(html).toContain('data-testid="ssr-content"');
185 });
186
187 it("renders search input with default value", async () => {
188 const { html } = await fetchPage("/");
189 expect(html).toContain('data-testid="search-input"');
190 expect(html).toContain('placeholder="Type a query..."');
191 });
192
193 it("renders default query state", async () => {
194 const { html } = await fetchPage("/");
195 expect(html).toContain("(empty)");
196 expect(html).toContain("Page:");
197 });
198
199 it("renders pagination buttons", async () => {
200 const { html } = await fetchPage("/");
201 expect(html).toContain('data-testid="prev-page"');
202 expect(html).toContain('data-testid="next-page"');
203 });
204});
205
206// ─── better-auth ──────────────────────────────────────────────────────────────
207describe("better-auth", () => {
208 let proc: ChildProcess | null = null;
209 let baseUrl: string;
210 let fetchPage: (path: string) => Promise<{ html: string; status: number }>;
211
212 /** Strip Set-Cookie attributes — keep only name=value pairs for Cookie header */
213 function toCookieHeader(setCookies: string[]): string {
214 return setCookies.map((c) => c.split(";")[0]).join("; ");
215 }
216
217 /** Sign up a user, return session cookies as a Cookie header string */
218 async function signUpUser(email: string, password: string, name: string) {
219 const res = await fetch(`${baseUrl}/api/auth/sign-up/email`, {
220 method: "POST",
221 headers: { "Content-Type": "application/json" },
222 body: JSON.stringify({ email, password, name }),
223 signal: AbortSignal.timeout(10000),
224 });
225 return { res, cookieHeader: toCookieHeader(res.headers.getSetCookie()) };
226 }
227
228 beforeAll(async () => {
229 const fixture = await startFixture("better-auth", 4403);
230 proc = fixture.process;
231 baseUrl = fixture.baseUrl;
232 fetchPage = fixture.fetchPage;
233 }, 30000);
234
235 afterAll(() => killProcess(proc));
236
237 it("renders SSR content", async () => {
238 const { html, status } = await fetchPage("/");
239 expect(status).toBe(200);
240 expect(html).toContain("better-auth test");
241 expect(html).toContain('data-testid="ssr-content"');
242 });
243
244 it("renders client-side auth status component", async () => {
245 const { html } = await fetchPage("/");
246 // The AuthStatus client component should be SSR-rendered in its loading
247 // or signed-out state
248 expect(html).toMatch(/data-testid="auth-(loading|signed-out)"/);
249 });
250
251 it("auth API catch-all route responds to GET", async () => {
252 // better-auth exposes GET /api/auth/get-session
253 const res = await fetch(`${baseUrl}/api/auth/get-session`, {
254 signal: AbortSignal.timeout(10000),
255 });
256 expect(res.status).toBe(200);
257 });
258
259 it("sign-up flow creates user and returns session", async () => {
260 const { res } = await signUpUser(
261 "signup-test@example.com",
262 "password123456",
263 "Signup Test",
264 );
265 expect(res.status).toBe(200);
266 const data = await res.json();
267 expect(data.user).toBeDefined();
268 expect(data.user.email).toBe("signup-test@example.com");
269 expect(data.user.name).toBe("Signup Test");
270 });
271
272 it("session is accessible after sign-in with cookie", async () => {
273 // Create a user first (self-contained — no dependency on other tests)
274 await signUpUser("signin-test@example.com", "password123456", "Signin Test");
275
276 // Sign in to get a session cookie
277 const signinRes = await fetch(`${baseUrl}/api/auth/sign-in/email`, {
278 method: "POST",
279 headers: { "Content-Type": "application/json" },
280 body: JSON.stringify({
281 email: "signin-test@example.com",
282 password: "password123456",
283 }),
284 signal: AbortSignal.timeout(10000),
285 });
286 expect(signinRes.status).toBe(200);
287
288 const cookieHeader = toCookieHeader(signinRes.headers.getSetCookie());
289
290 // Fetch session using the cookie
291 const sessionRes = await fetch(`${baseUrl}/api/auth/get-session`, {
292 headers: { cookie: cookieHeader },
293 signal: AbortSignal.timeout(10000),
294 });
295 expect(sessionRes.status).toBe(200);
296 const session = await sessionRes.json();
297 expect(session.user.email).toBe("signin-test@example.com");
298 });
299
300 it("server component can access session via headers()", async () => {
301 // Create and sign in a user (self-contained)
302 await signUpUser("protected-test@example.com", "password123456", "Protected Test");
303
304 const signinRes = await fetch(`${baseUrl}/api/auth/sign-in/email`, {
305 method: "POST",
306 headers: { "Content-Type": "application/json" },
307 body: JSON.stringify({
308 email: "protected-test@example.com",
309 password: "password123456",
310 }),
311 signal: AbortSignal.timeout(10000),
312 });
313 const cookieHeader = toCookieHeader(signinRes.headers.getSetCookie());
314
315 // Fetch the protected server component page with the session cookie.
316 // This exercises: headers() shim -> auth.api.getSession() -> SSR render
317 const res = await fetch(`${baseUrl}/protected`, {
318 headers: { cookie: cookieHeader },
319 signal: AbortSignal.timeout(15000),
320 });
321 expect(res.status).toBe(200);
322 const html = await res.text();
323 expect(html).toContain('data-testid="protected-heading"');
324 expect(html).toContain("Logged in as protected-test@example.com");
325 });
326});
327