Files
SnapOtter/tests/unit/infra/docs-dev-ports.test.ts
T

88 lines
3.5 KiB
TypeScript
Raw Normal View History

// @vitest-environment node
import { globSync, readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
/**
* The build-from-source docs told contributors to open port 1349 for five
* releases. 1349 is the container port; `pnpm dev` binds Vite to 1351, so
* everyone following those pages hit a dead port.
*
* Nothing checked it, so this reads the two ports back out of the code that
* sets them and holds the docs to that. Restating 1351 as a literal here would
* just move the drift one file over, so both numbers are parsed.
*/
const ROOT = path.resolve(__dirname, "../../..");
function devServerPort(): number {
// apps/web/package.json runs a bare `vite`, so the fallback in the config is
// the port a contributor actually gets.
const config = readFileSync(path.join(ROOT, "apps/web/vite.config.ts"), "utf8");
const match = config.match(/port:\s*Number\(process\.env\.PORT\)\s*\|\|\s*(\d+)/);
if (!match)
throw new Error("could not read the Vite dev-server port from apps/web/vite.config.ts");
return Number(match[1]);
}
function apiPort(): number {
const pkg = JSON.parse(readFileSync(path.join(ROOT, "apps/api/package.json"), "utf8"));
const match = String(pkg.scripts.dev).match(/\bPORT=(\d+)/);
if (!match) throw new Error("could not read PORT from the @snapotter/api dev script");
return Number(match[1]);
}
const WEB = devServerPort();
const API = apiPort();
function docs(pattern: string): string[] {
const files = globSync(pattern, { cwd: ROOT }).sort();
if (files.length === 0) throw new Error(`no docs matched ${pattern}`);
return files;
}
describe("dev-server ports in the build-from-source docs", () => {
it("reads both ports out of the code that binds them", () => {
// If either parse silently returned NaN the per-file cases below would
// compare NaN to NaN and pass on an empty match set.
expect(Number.isInteger(WEB)).toBe(true);
expect(Number.isInteger(API)).toBe(true);
expect(WEB).not.toBe(API);
});
// developer.md documents `pnpm dev` end to end, so every localhost port on the
// page is a dev-server port. All 21 locales carry the same table.
it.each(docs("apps/docs/**/guide/developer.md"))("%s names only the two dev ports", (file) => {
const text = readFileSync(path.join(ROOT, file), "utf8");
const ports = [...text.matchAll(/localhost:(\d+)/g)].map((m) => Number(m[1]));
expect(ports.length).toBeGreaterThan(0);
expect([...new Set(ports)].sort()).toEqual([WEB, API].sort());
});
// getting-started.md is mostly about the container, so only the bare-URL
// bullet pair under "Build from Source" is in scope.
it.each(docs("apps/docs/**/guide/getting-started.md"))(
"%s points the build-from-source bullets at the dev ports",
(file) => {
const lines = readFileSync(path.join(ROOT, file), "utf8").split("\n");
const bullets = lines
.map((line) =>
line.match(/^[-*]\s.*\[http:\/\/localhost:(\d+)\]\(http:\/\/localhost:\1\)\s*$/),
)
.filter((m): m is RegExpMatchArray => m !== null)
.map((m) => Number(m[1]));
expect(bullets).toEqual([WEB, API]);
},
);
it.each([...docs("apps/docs/**/guide/contributing.md"), "CONTRIBUTING.md"])(
"%s comments the `pnpm dev` block with the dev ports",
(file) => {
const text = readFileSync(path.join(ROOT, file), "utf8");
const match = text.match(/web on :(\d+), API on :(\d+)/);
expect(match).not.toBeNull();
expect([Number(match?.[1]), Number(match?.[2])]).toEqual([WEB, API]);
},
);
});