Files
kandev/apps/web/scripts/lib/e2e-wait-inventory.test.ts
T
2026-08-13 22:50:13 +01:00

164 lines
5.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { inventorySource, readDwellCategories, summarize } from "./e2e-wait-inventory.mjs";
const CAUSAL_WAITS = path.resolve(import.meta.dirname, "../../e2e/helpers/causal-waits.ts");
const CATEGORIES = readDwellCategories(fs.readFileSync(CAUSAL_WAITS, "utf8")) as string[];
/** Extracted because it is the one category that is permanent rather than debt. */
const NEGATIVE = "negative-assertion";
const inventory = (code: string) => inventorySource(code, "fixture.ts", CATEGORIES);
describe("readDwellCategories", () => {
/**
* Read from the helper rather than restated here. If `causal-waits.ts` grows a
* category, this picks it up; if the export is renamed or reshaped, the parse
* throws instead of silently reporting every dwell as uncategorised.
*/
it("reads the live category list out of causal-waits.ts", () => {
expect(CATEGORIES).toEqual([
NEGATIVE,
"product-timer",
"library-timer",
"clock-separation",
"poll-interval",
"browser-chrome",
"unverified",
]);
});
it("throws rather than guessing when the export cannot be found", () => {
expect(() => readDwellCategories("export const SOMETHING_ELSE = [];")).toThrow(
/could not find/i,
);
});
it("throws when the list parses to nothing", () => {
expect(() => readDwellCategories("export const DWELL_CATEGORIES = [] as const;")).toThrow(
/empty list/i,
);
});
});
describe("inventorySource", () => {
/**
* The two overloads put `category` at a different index — 1 without a page, 2
* with one. Both must land in the same bucket, or the report splits one
* category across two rows depending on which form the author used.
*/
it("reads the category from the page-less form", () => {
const found = inventory('await dwell(500, "poll-interval", "backend health poll");');
expect(found.dwells).toEqual([{ category: "poll-interval", line: 1 }]);
});
it("reads the category from the page form", () => {
const found = inventory('await dwell(page, 300, "library-timer", "Radix open delay");');
expect(found.dwells).toEqual([{ category: "library-timer", line: 1 }]);
});
it("counts every category separately across a file", () => {
const found = inventory(`
await dwell(page, 300, "${NEGATIVE}", "asserting the toast never appears");
await dwell(100, "unverified", "pre-existing spacing, no timer identified");
await dwell(page, 50, "clock-separation", "keep the two writes distinguishable");
`);
expect(found.dwells.map((entry: { category: string }) => entry.category)).toEqual([
NEGATIVE,
"unverified",
"clock-separation",
]);
});
it("flags a category that is not in the live list rather than dropping it", () => {
const found = inventory('await dwell(500, "made-up-category", "why");');
expect(found.dwells[0].category).toBe("«unknown: made-up-category»");
});
it("flags a dwell whose category is not a literal", () => {
const found = inventory("await dwell(500, category, reason);");
expect(found.dwells[0].category).toBe("«no literal category»");
});
it("counts injectLatency apart from dwell", () => {
const found = inventory(`
await injectLatency(800, "make the spinner observable");
await dwell(100, "unverified", "no timer identified");
`);
expect(found.injectLatency).toBe(1);
expect(found.dwells).toHaveLength(1);
});
it("parses TypeScript syntax without a type checker", () => {
const found = inventory(`
const wait = async (page: Page): Promise<void> => {
await dwell(page, 10, "browser-chrome", "native print dialog");
};
`);
expect(found.dwells).toHaveLength(1);
});
});
describe("summarize", () => {
const build = (categories: string[], injectLatency = 0) => [
{
filePath: "fixture.ts",
dwells: categories.map((category) => ({ category, line: 1 })),
injectLatency,
},
];
/**
* The load-bearing assertion of the whole report: `negative-assertion` is
* permanent and must never appear in the number a conversion effort is trying
* to reduce. A test asserting something never happens has no event to wait
* for, and deleting its wait leaves a test that passes for free.
*/
it("excludes negative-assertion from debt", () => {
const summary = summarize(build([NEGATIVE, NEGATIVE]), CATEGORIES);
expect(summary.totalDwells).toBe(2);
expect(summary.permanent).toBe(2);
expect(summary.debt).toBe(0);
});
it("counts every other category as debt", () => {
const summary = summarize(
build([NEGATIVE, "unverified", "library-timer", "poll-interval"]),
CATEGORIES,
);
expect(summary.totalDwells).toBe(4);
expect(summary.permanent).toBe(1);
expect(summary.debt).toBe(3);
expect(summary.unverified).toBe(1);
});
/**
* injectLatency is deliberate fixture configuration, correct by construction.
* Folding it in would make adding a legitimate latency simulation read as a
* regression in a number that is supposed to be shrinking.
*/
it("keeps injectLatency out of both dwell totals", () => {
const summary = summarize(build(["unverified"], 5), CATEGORIES);
expect(summary.injectLatency).toBe(5);
expect(summary.totalDwells).toBe(1);
expect(summary.debt).toBe(1);
});
it("surfaces uncategorised dwells instead of silently dropping them", () => {
const summary = summarize(build(["«unknown: nope»", "unverified"]), CATEGORIES);
expect(summary.unknown).toBe(1);
expect(summary.totalDwells).toBe(2);
expect(summary.debt).toBe(2);
});
it("reports zeros for a suite with no dwells at all", () => {
const summary = summarize([], CATEGORIES);
expect(summary.totalDwells).toBe(0);
expect(summary.debt).toBe(0);
expect(summary.byCategory.unverified).toBe(0);
});
});