Skip to content

Commit 852da0d

Browse files
committed
refactor e2e scaffolding to be able to setup nextjs apps to test different scenarios
1 parent c658e2c commit 852da0d

File tree

2 files changed

+201
-83
lines changed

2 files changed

+201
-83
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as assert from "assert";
2+
import { posix } from "path";
3+
import { getAdapterMetadata } from "../dist/utils.js";
4+
export const host = process.env.HOST;
5+
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
let adapterMetadata: any;
11+
12+
before(() => {
13+
adapterMetadata = getAdapterMetadata();
14+
});
15+
16+
describe("middleware", () => {
17+
it("should have x-fah-adapter header and x-fah-middleware header on all routes", async () => {
18+
const routes = [
19+
"/",
20+
"/ssg",
21+
"/ssr",
22+
"/ssr/streaming",
23+
"/isr/time",
24+
"/isr/demand",
25+
"/nonexistent-route",
26+
];
27+
28+
for (const route of routes) {
29+
const response = await fetch(posix.join(host, route));
30+
assert.equal(
31+
response.headers.get("x-fah-adapter"),
32+
`nextjs-${adapterMetadata.adapterVersion}`,
33+
`Route ${route} missing x-fah-adapter header`,
34+
);
35+
assert.equal(
36+
response.headers.get("x-fah-middleware"),
37+
"true",
38+
`Route ${route} missing x-fah-middleware header`,
39+
);
40+
}
41+
});
42+
});

packages/@apphosting/adapter-nextjs/e2e/run-local.ts

Lines changed: 159 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,100 +12,176 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
1212

1313
const starterTemplateDir = "../../../starters/nextjs/basic";
1414

15+
// Define scenarios to test
16+
interface Scenario {
17+
name: string; // Name of the scenario
18+
setup?: (cwd: string) => Promise<void>; // Optional setup function before building the app
19+
tests?: string[]; // List of test files to run
20+
}
21+
22+
const scenarios: Scenario[] = [
23+
{
24+
name: "basic",
25+
// No setup needed for basic scenario
26+
tests: ["app.spec.ts"],
27+
},
28+
{
29+
name: "with-middleware",
30+
setup: async (cwd: string) => {
31+
// Create a middleware.ts file
32+
const middlewareContent = `
33+
import type { NextRequest } from 'next/server'
34+
35+
export function middleware(request: NextRequest) {
36+
// This is a simple middleware that doesn't modify the request
37+
console.log('Middleware executed', request.nextUrl.pathname);
38+
}
39+
40+
export const config = {
41+
matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
42+
};
43+
`;
44+
45+
await fsExtra.writeFile(join(cwd, "src", "middleware.ts"), middlewareContent);
46+
console.log(`Created middleware.ts file`);
47+
},
48+
tests: ["middleware.spec.ts"], // Only run middleware-specific tests
49+
},
50+
];
51+
1552
const errors: any[] = [];
1653

1754
await rmdir(join(__dirname, "runs"), { recursive: true }).catch(() => undefined);
1855

19-
console.log("\nBuilding and starting test project...");
20-
21-
const runId = Math.random().toString().split(".")[1];
22-
const cwd = join(__dirname, "runs", runId);
23-
await mkdirp(cwd);
24-
25-
console.log(`[${runId}] Copying ${starterTemplateDir} to working directory`);
26-
await cp(starterTemplateDir, cwd, { recursive: true });
27-
28-
console.log(`[${runId}] > npm ci --silent --no-progress`);
29-
await promiseSpawn("npm", ["ci", "--silent", "--no-progress"], {
30-
cwd,
31-
stdio: "inherit",
32-
shell: true,
33-
});
34-
35-
const buildScript = relative(cwd, join(__dirname, "../dist/bin/build.js"));
36-
console.log(`[${runId}] > node ${buildScript}`);
37-
38-
const packageJson = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
39-
const frameworkVersion = packageJson.dependencies.next.replace("^", "");
40-
await promiseSpawn("node", [buildScript], {
41-
cwd,
42-
stdio: "inherit",
43-
shell: true,
44-
env: {
45-
...process.env,
46-
FRAMEWORK_VERSION: frameworkVersion,
47-
},
48-
});
49-
50-
const bundleYaml = parseYaml(readFileSync(join(cwd, ".apphosting/bundle.yaml")).toString());
56+
// Run each scenario
57+
for (const scenario of scenarios) {
58+
console.log(`\n\nRunning scenario: ${scenario.name}`);
5159

52-
const runCommand = bundleYaml.runConfig.runCommand;
60+
const runId = `${scenario.name}-${Math.random().toString().split(".")[1]}`;
61+
const cwd = join(__dirname, "runs", runId);
62+
await mkdirp(cwd);
5363

54-
if (typeof runCommand !== "string") {
55-
throw new Error("runCommand must be a string");
56-
}
64+
console.log(`[${runId}] Copying ${starterTemplateDir} to working directory`);
65+
await cp(starterTemplateDir, cwd, { recursive: true });
5766

58-
const [runScript, ...runArgs] = runCommand.split(" ");
59-
let resolveHostname: (it: string) => void;
60-
let rejectHostname: () => void;
61-
const hostnamePromise = new Promise<string>((resolve, reject) => {
62-
resolveHostname = resolve;
63-
rejectHostname = reject;
64-
});
65-
const port = 8080 + Math.floor(Math.random() * 1000);
66-
console.log(`[${runId}] > PORT=${port} ${runCommand}`);
67-
const run = spawn(runScript, runArgs, {
68-
cwd,
69-
shell: true,
70-
env: {
71-
NODE_ENV: "production",
72-
PORT: port.toString(),
73-
PATH: process.env.PATH,
74-
},
75-
});
76-
run.stderr.on("data", (data) => console.error(data.toString()));
77-
run.stdout.on("data", (data) => {
78-
console.log(data.toString());
79-
// Check for the "Ready in" message to determine when the server is fully started
80-
if (data.toString().includes(`Ready in`)) {
81-
// We use 0.0.0.0 instead of localhost to avoid issues when ipv6 is not available (Node 18)
82-
resolveHostname(`http://0.0.0.0:${port}`);
83-
}
84-
});
85-
run.on("close", (code) => {
86-
if (code) {
87-
rejectHostname();
67+
// Run scenario-specific setup if provided
68+
if (scenario.setup) {
69+
console.log(`[${runId}] Running setup for ${scenario.name}`);
70+
await scenario.setup(cwd);
8871
}
89-
});
90-
const host = await hostnamePromise;
9172

92-
console.log("\n\n");
93-
94-
try {
95-
console.log(`> HOST=${host} ts-mocha -p tsconfig.json e2e/*.spec.ts`);
96-
await promiseSpawn("ts-mocha", ["-p", "tsconfig.json", "e2e/*.spec.ts"], {
97-
shell: true,
73+
console.log(`[${runId}] > npm ci --silent --no-progress`);
74+
await promiseSpawn("npm", ["ci", "--silent", "--no-progress"], {
75+
cwd,
9876
stdio: "inherit",
99-
env: {
100-
...process.env,
101-
HOST: host,
102-
},
103-
}).finally(() => {
104-
run.stdin.end();
105-
run.kill("SIGKILL");
77+
shell: true,
10678
});
107-
} catch (e) {
108-
errors.push(e);
79+
80+
const buildScript = relative(cwd, join(__dirname, "../dist/bin/build.js"));
81+
const buildLogPath = join(cwd, "build.log");
82+
console.log(`[${runId}] > node ${buildScript} (output written to ${buildLogPath})`);
83+
84+
const packageJson = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
85+
const frameworkVersion = packageJson.dependencies.next.replace("^", "");
86+
87+
try {
88+
await promiseSpawn("node", [buildScript], {
89+
cwd,
90+
stdioString: true,
91+
stdio: "pipe",
92+
shell: true,
93+
env: {
94+
...process.env,
95+
FRAMEWORK_VERSION: frameworkVersion,
96+
},
97+
}).then((result) => {
98+
// Write stdout and stderr to the log file
99+
fsExtra.writeFileSync(buildLogPath, result.stdout + result.stderr);
100+
});
101+
102+
const bundleYaml = parseYaml(readFileSync(join(cwd, ".apphosting/bundle.yaml")).toString());
103+
104+
const runCommand = bundleYaml.runConfig.runCommand;
105+
106+
if (typeof runCommand !== "string") {
107+
throw new Error("runCommand must be a string");
108+
}
109+
110+
const [runScript, ...runArgs] = runCommand.split(" ");
111+
let resolveHostname: (it: string) => void;
112+
let rejectHostname: () => void;
113+
const hostnamePromise = new Promise<string>((resolve, reject) => {
114+
resolveHostname = resolve;
115+
rejectHostname = reject;
116+
});
117+
const port = 8080 + Math.floor(Math.random() * 1000);
118+
const runLogPath = join(cwd, "run.log");
119+
console.log(`[${runId}] > PORT=${port} ${runCommand} (output written to ${runLogPath})`);
120+
const runLogStream = fsExtra.createWriteStream(runLogPath);
121+
122+
const run = spawn(runScript, runArgs, {
123+
cwd,
124+
shell: true,
125+
env: {
126+
NODE_ENV: "production",
127+
PORT: port.toString(),
128+
PATH: process.env.PATH,
129+
},
130+
});
131+
132+
run.stderr.on("data", (data) => {
133+
const output = data.toString();
134+
runLogStream.write(output);
135+
});
136+
137+
run.stdout.on("data", (data) => {
138+
const output = data.toString();
139+
runLogStream.write(output);
140+
// Check for the "Ready in" message to determine when the server is fully started
141+
if (output.includes(`Ready in`)) {
142+
// We use 0.0.0.0 instead of localhost to avoid issues when ipv6 is not available (Node 18)
143+
resolveHostname(`http://0.0.0.0:${port}`);
144+
}
145+
});
146+
147+
run.on("close", (code) => {
148+
runLogStream.end();
149+
if (code) {
150+
rejectHostname();
151+
}
152+
});
153+
const host = await hostnamePromise;
154+
155+
console.log("\n\n");
156+
157+
try {
158+
// Determine which test files to run
159+
const testPattern = scenario.tests
160+
? scenario.tests.map((test) => `e2e/${test}`).join(" ")
161+
: "e2e/*.spec.ts";
162+
163+
console.log(
164+
`> HOST=${host} SCENARIO=${scenario.name} ts-mocha -p tsconfig.json ${testPattern}`,
165+
);
166+
await promiseSpawn("ts-mocha", ["-p", "tsconfig.json", ...testPattern.split(" ")], {
167+
shell: true,
168+
stdio: "inherit",
169+
env: {
170+
...process.env,
171+
HOST: host,
172+
SCENARIO: scenario.name,
173+
},
174+
}).finally(() => {
175+
run.stdin.end();
176+
run.kill("SIGKILL");
177+
});
178+
} catch (e) {
179+
errors.push(e);
180+
}
181+
} catch (e) {
182+
console.error(`Error in scenario ${scenario.name}:`, e);
183+
errors.push(e);
184+
}
109185
}
110186

111187
if (errors.length) {

0 commit comments

Comments
 (0)