web: smoke-test the built standalone app against a mock graphql cms
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// Builds the app against the mock CMS and serves the standalone output,
|
||||
// reproducing the Dockerfile's layout (server.js + .next/static + public).
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { cpSync, existsSync, rmSync } from "node:fs";
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { TestProject } from "vitest/node";
|
||||
|
||||
import { startMockCms } from "./mock-cms";
|
||||
|
||||
const WEB = fileURLToPath(new URL("../..", import.meta.url));
|
||||
|
||||
declare module "vitest" {
|
||||
interface ProvidedContext {
|
||||
appUrl: string;
|
||||
mockUrl: string;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const srv = http.createServer();
|
||||
srv.listen(0, "127.0.0.1", () => {
|
||||
const { port } = srv.address() as AddressInfo;
|
||||
srv.close((err) => (err ? reject(err) : resolve(port)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForOk(url: string, timeoutMs: number): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError: unknown = null;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return;
|
||||
lastError = new Error(`GET ${url} -> ${res.status}`);
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
throw new Error(`timed out waiting for ${url}: ${lastError}`);
|
||||
}
|
||||
|
||||
export default async function setup(project: TestProject) {
|
||||
const mock = await startMockCms();
|
||||
const appPort = await getFreePort();
|
||||
const appUrl = `http://127.0.0.1:${appPort}`;
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
// process env beats web/.env, steering the build away from cms.neuf.no
|
||||
WAGTAIL_BASE_URL: mock.url,
|
||||
// production builds refuse local-IP image sources without this override
|
||||
SMOKE_ALLOW_LOCAL_IMAGES: "1",
|
||||
REVALIDATE_WEBHOOK_SECRET: "smoke-secret",
|
||||
URL: appUrl,
|
||||
NEXT_TELEMETRY_DISABLED: "1",
|
||||
};
|
||||
|
||||
rmSync(join(WEB, ".next", "standalone"), { recursive: true, force: true });
|
||||
// async spawn: the mock CMS lives in this process, so the event loop must
|
||||
// stay free to answer the build's GraphQL requests
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const build = spawn("node", ["node_modules/next/dist/bin/next", "build"], {
|
||||
cwd: WEB,
|
||||
env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
build.on("error", reject);
|
||||
build.on("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`next build exited with code ${code}`))
|
||||
);
|
||||
});
|
||||
|
||||
if (mock.graphqlRequests === 0) {
|
||||
throw new Error(
|
||||
"next build made no GraphQL requests to the mock CMS — is WAGTAIL_BASE_URL being overridden?"
|
||||
);
|
||||
}
|
||||
|
||||
// mirror the Dockerfile: standalone lacks static assets and public/
|
||||
const standalone = join(WEB, ".next", "standalone");
|
||||
if (!existsSync(join(standalone, "server.js"))) {
|
||||
throw new Error(`no server.js in ${standalone} — standalone layout changed?`);
|
||||
}
|
||||
cpSync(join(WEB, ".next", "static"), join(standalone, ".next", "static"), {
|
||||
recursive: true,
|
||||
});
|
||||
cpSync(join(WEB, "public"), join(standalone, "public"), { recursive: true });
|
||||
|
||||
const app: ChildProcess = spawn("node", ["server.js"], {
|
||||
cwd: standalone,
|
||||
env: { ...env, PORT: String(appPort), HOSTNAME: "127.0.0.1" },
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
await waitForOk(`${appUrl}/`, 60_000);
|
||||
|
||||
project.provide("appUrl", appUrl);
|
||||
project.provide("mockUrl", mock.url);
|
||||
|
||||
return async () => {
|
||||
app.kill();
|
||||
await mock.close();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user