99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { test as base, expect, Page } from "@playwright/test";
|
|
import { spawn, ChildProcess } from "child_process";
|
|
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
|
|
/**
|
|
* Starts a fresh Sustenance server against a unique, throwaway database on a
|
|
* unique port for each test, and tears it down afterwards. This gives every
|
|
* test a clean DB with no shared state between tests.
|
|
*/
|
|
export const test = base.extend<{ server: { baseURL: string }; page: Page }>({
|
|
server: [
|
|
async ({}, use) => {
|
|
const dbPath = path.join(
|
|
os.tmpdir(),
|
|
`sustenance-e2e-${process.pid}-${Date.now()}-${Math.random()
|
|
.toString(36)
|
|
.slice(2)}.db`,
|
|
);
|
|
const port = 3200 + Math.floor(Math.random() * 2000);
|
|
const baseURL = `http://127.0.0.1:${port}`;
|
|
|
|
const child = spawn(
|
|
path.resolve(__dirname, "..", "target", "debug", "sustenance"),
|
|
[],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
DATABASE_PATH: dbPath,
|
|
REGISTRATION_MODE: "open",
|
|
BIND_ADDRESS: `127.0.0.1:${port}`,
|
|
// Point SEED_CONFIG at a nonexistent file so no default user is created.
|
|
SEED_CONFIG: path.join(os.tmpdir(), "sustenance-e2e-no-seed.json"),
|
|
},
|
|
stdio: "ignore",
|
|
// Run in its own process group so we can kill the whole tree.
|
|
detached: true,
|
|
},
|
|
);
|
|
|
|
await waitForServer(baseURL, child);
|
|
|
|
await use({ baseURL });
|
|
|
|
await killTree(child);
|
|
// Clean up the DB files (including -wal / -shm).
|
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
fs.rmSync(dbPath + suffix, { force: true });
|
|
}
|
|
},
|
|
{ scope: "test", auto: true },
|
|
],
|
|
|
|
// Provide a page whose baseURL points at this test's server.
|
|
page: async ({ browser, server }, use) => {
|
|
const context = await browser.newContext({ baseURL: server.baseURL });
|
|
const page = await context.newPage();
|
|
await use(page);
|
|
await context.close();
|
|
},
|
|
});
|
|
|
|
async function waitForServer(baseURL: string, child: ChildProcess) {
|
|
const deadline = Date.now() + 60_000;
|
|
while (Date.now() < deadline) {
|
|
if (child.exitCode !== null) {
|
|
throw new Error(`server exited early with code ${child.exitCode}`);
|
|
}
|
|
try {
|
|
const res = await fetch(baseURL + "/login");
|
|
if (res.ok) return;
|
|
} catch {
|
|
// not up yet
|
|
}
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
}
|
|
throw new Error("timed out waiting for server to start");
|
|
}
|
|
|
|
async function killTree(child: ChildProcess) {
|
|
try {
|
|
process.kill(-child.pid!, "SIGTERM");
|
|
} catch {
|
|
child.kill("SIGTERM");
|
|
}
|
|
// Give it a moment to shut down gracefully, then force-kill if needed.
|
|
const exited = new Promise((resolve) => child.once("exit", resolve));
|
|
const timeout = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
await Promise.race([exited, timeout]);
|
|
try {
|
|
process.kill(-child.pid!, "SIGKILL");
|
|
} catch {
|
|
/* already gone */
|
|
}
|
|
}
|
|
|
|
export { expect };
|