1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164/**
* OpenNext Compat: next.config.js redirects, rewrites, and custom headers.
*
* Ported from:
* https://github.com/opennextjs/opennextjs-cloudflare/blob/main/examples/e2e/app-router/e2e/config.redirect.test.ts
* https://github.com/opennextjs/opennextjs-cloudflare/blob/main/examples/e2e/app-router/e2e/headers.test.ts
* Tests: ON-12, ON-15 in TRACKING.md
*/
import { test, expect } from "@playwright/test";
const BASE = "http://localhost:4174";
test.describe("Config Redirects (OpenNext compat)", () => {
// Ref: opennextjs-cloudflare config.redirect.test.ts โ simple redirect
test("simple redirect from config source to destination", async ({
page,
}) => {
await page.goto(`${BASE}/config-redirect-source`);
await page.waitForURL(/\/about$/);
const el = page.getByText("About", { exact: true });
await expect(el).toBeVisible();
});
// Ref: opennextjs-cloudflare config.redirect.test.ts โ permanent redirect status
test("permanent redirect returns 308", async ({ request }) => {
const res = await request.get(`${BASE}/config-redirect-source`, {
maxRedirects: 0,
});
expect(res.status()).toBe(308);
expect(res.headers()["location"]).toMatch(/\/about$/);
});
// Ref: opennextjs-cloudflare config.redirect.test.ts โ temporary redirect
test("non-permanent redirect returns 307", async ({ request }) => {
const res = await request.get(`${BASE}/config-redirect-query`, {
maxRedirects: 0,
});
expect(res.status()).toBe(307);
expect(res.headers()["location"]).toContain("/about?from=config");
});
// Ref: opennextjs-cloudflare config.redirect.test.ts โ parameterized redirect
test("parameterized redirect preserves slug", async ({ request }) => {
const res = await request.get(`${BASE}/old-blog/hello-world`, {
maxRedirects: 0,
});
// permanent: false โ 307
expect(res.status()).toBe(307);
expect(res.headers()["location"]).toMatch(/\/blog\/hello-world$/);
});
// Ref: opennextjs-cloudflare config.redirect.test.ts โ cookie conditions
test("redirect with has cookie condition only fires when cookie present", async ({ request }) => {
// Without the cookie โ should NOT redirect (200 or 404, not 3xx)
const noRedirect = await request.get(`${BASE}/has-cookie-redirect`, {
maxRedirects: 0,
});
// Should NOT be a redirect (no cookie present)
expect(noRedirect.status()).not.toBe(307);
expect(noRedirect.status()).not.toBe(308);
// With the cookie โ should redirect
const withRedirect = await request.get(`${BASE}/has-cookie-redirect`, {
maxRedirects: 0,
headers: { Cookie: "redirect-me=1" },
});
expect(withRedirect.status()).toBe(307);
expect(withRedirect.headers()["location"]).toMatch(/\/about$/);
});
test("redirect with missing cookie condition only fires when cookie absent", async ({ request }) => {
// Without the cookie โ should redirect (cookie is missing โ condition met)
const shouldRedirect = await request.get(`${BASE}/missing-cookie-redirect`, {
maxRedirects: 0,
});
expect(shouldRedirect.status()).toBe(307);
expect(shouldRedirect.headers()["location"]).toMatch(/\/about$/);
// With the cookie โ should NOT redirect (cookie is present โ missing condition fails)
const noRedirect = await request.get(`${BASE}/missing-cookie-redirect`, {
maxRedirects: 0,
headers: { Cookie: "stay-here=1" },
});
expect(noRedirect.status()).not.toBe(307);
expect(noRedirect.status()).not.toBe(308);
});
});
test.describe("Config Rewrites (OpenNext compat)", () => {
// Config rewrite: /config-rewrite โ / (URL stays, content from /)
test("config rewrite serves / content at /config-rewrite URL", async ({
page,
}) => {
await page.goto(`${BASE}/config-rewrite`);
// URL should stay as /config-rewrite
expect(page.url()).toMatch(/\/config-rewrite$/);
// Content should be from / (home page)
const el = page.getByText("Welcome to App Router", { exact: true });
await expect(el).toBeVisible();
});
});
test.describe("Config Custom Headers (OpenNext compat)", () => {
// Ref: opennextjs-cloudflare headers.test.ts โ "Headers"
test("custom header from next.config headers() is present on pages", async ({
request,
}) => {
const res = await request.get(`${BASE}/about`);
expect(res.status()).toBe(200);
// The /(.*) catch-all header applies to all routes
expect(res.headers()["x-e2e-header"]).toBe("vinext-e2e");
// The /about-specific header also applies
expect(res.headers()["x-page-header"]).toBe("about-page");
});
test("custom header applied to API routes", async ({ request }) => {
const res = await request.get(`${BASE}/api/hello`);
expect(res.status()).toBe(200);
// The /api/(.*) header
expect(res.headers()["x-custom-header"]).toBe("vinext-app");
// The /(.*) catch-all header
expect(res.headers()["x-e2e-header"]).toBe("vinext-e2e");
});
// Ref: opennextjs-cloudflare headers.test.ts โ "x-powered-by should be absent"
// vinext never sends X-Powered-By (matching Next.js poweredByHeader: false behavior).
// Tests: ON-6 #7, ON-8 #3 in TRACKING.md
test("x-powered-by header is absent from responses", async ({ request }) => {
const pageRes = await request.get(`${BASE}/about`);
expect(pageRes.headers()["x-powered-by"]).toBeUndefined();
const apiRes = await request.get(`${BASE}/api/hello`);
expect(apiRes.headers()["x-powered-by"]).toBeUndefined();
});
// Ref: opennextjs-cloudflare headers.test.ts โ "Middleware headers override next.config.js headers"
// In Next.js, `dangerous.middlewareHeadersOverrideNextConfigHeaders` lets middleware
// overwrite config headers for the same key. vinext does not implement this config flag.
// Tests: ON-8 #2 in TRACKING.md
test.fixme(
"middleware headers override config headers for same key",
async () => {
// Would test: middleware sets e2e-headers=middleware, config sets e2e-headers=next.config.js
// With dangerous.middlewareHeadersOverrideNextConfigHeaders enabled, middleware wins.
// Needs: config flag support + fixture with conflicting header keys
},
);
// Ref: opennextjs-cloudflare headers.test.ts โ has/missing conditions
// vinext matchHeaders() in config-matchers.ts only checks source pattern.
// Tests: ON-15 #6 in TRACKING.md
test.fixme(
"config headers with has/missing conditions",
async () => {
// Would test: header rule with has: [{ type: "cookie", key: "logged-in" }]
// only applies when the cookie is present in the request.
// Needs: has/missing support in matchHeaders(), matchRedirect(), matchRewrite()
},
);
});