Three of the six Vitest projects ran on Vitest's own testTimeout: 5000 and five on its hookTimeout/teardownTimeout: 10000, and all 788 Testing Library waits in the web unit project ran on asyncUtilTimeout: 1000. None of those numbers was chosen here, and all of them were sized for an idle machine rather than for three or four concurrent worktree sessions each running a full local:gate on eight cores. #2292, #2278, #2250, #1942 and #1742 were each that class, diagnosed one site at a time. This is the complement of #1596, not a retreat from it: every fix that issue produced stands, and no race is hidden here. What changes is that a correct, deterministic test is no longer cut off by a budget nobody picked. - vitest.shared.mts exports frozen TIMEOUTS / INTEGRATION_TIMEOUTS; all six projects spread one of them, clients/launcher importing the shared module for the first time to do so. - Both web projects configure Testing Library's asyncUtilTimeout. The storybook one needs its own setup file, deliberately outside .storybook/ and free of setProjectAnnotations so the automatic preview-annotation provisioning (#1898) is untouched. - 28 per-suite restatements of the integration project's own budget are gone, so raising the shared value now moves every site under it. The handful that genuinely differ — cross-process lock contention, a full OAuth round trip — keep their budget as a named constant that says why. - AppRenderer's waitFor no longer asks for exactly its enclosing budget, which it could never win (#2292's shape); cli's e2e child timer drops below the project budget so the bound carrying the diagnostic is the one that fires. - The transition auto-settle's slack term is named RAF_SLACK_MS and raised; the term derived from HEADER_ANIM_MS is untouched. The two answer to different things and must not be scaled as one number. - The 19 Playwright budgets scattered across three web smokes become four named constants in scripts/lib/browser-timeouts.mjs. scripts/verify-test-timeouts.mjs resolves each project through Vitest itself and asserts the budgets, the asyncUtilTimeout sites, and that retry stays unset. It was observed red against the pre-change config: unit/tui/launcher at testTimeout 5000, five projects at hookTimeout 10000, both asyncUtilTimeout sites unmet. Closes #2323 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp Signed-off-by: cliffhall <cliff@futurescale.com>
150 lines
5.2 KiB
JavaScript
150 lines
5.2 KiB
JavaScript
/**
|
|
* Unit tests for `verify-test-timeouts.mjs`'s pure decision logic.
|
|
*
|
|
* The guard's own resolution half needs a real Vitest and four real configs, so
|
|
* it is exercised by running it (it is in `validate`). What is tested here is
|
|
* everything that decides PASS or FAIL once a project is resolved — including
|
|
* the two failure modes a green repo can never produce on its own: an unknown
|
|
* project, and a `retry` someone added.
|
|
*
|
|
* ⚠️ Keep the filename exactly `verify-test-timeouts.test.mjs`. `node --test`
|
|
* silently SKIPS a file its glob misses and still exits 0, so a typo here reads
|
|
* as a passing suite.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
ASYNC_UTIL_SITES,
|
|
CONFIG_ROOTS,
|
|
EXPECTED_INTEGRATION_TIMEOUTS,
|
|
EXPECTED_PROJECTS,
|
|
EXPECTED_TIMEOUTS,
|
|
checkAsyncUtilSource,
|
|
checkProject,
|
|
identifyProject,
|
|
} from "./verify-test-timeouts.mjs";
|
|
|
|
const ok = { ...EXPECTED_TIMEOUTS };
|
|
|
|
test("a project on the stated budgets passes", () => {
|
|
assert.deepEqual(checkProject("unit", ok), []);
|
|
assert.deepEqual(checkProject("cli", ok), []);
|
|
assert.deepEqual(
|
|
checkProject("integration", { ...EXPECTED_INTEGRATION_TIMEOUTS }),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test("each budget is checked independently", () => {
|
|
for (const key of Object.keys(EXPECTED_TIMEOUTS)) {
|
|
const failures = checkProject("unit", { ...ok, [key]: 1234 });
|
|
assert.equal(failures.length, 1, `${key} was not checked`);
|
|
assert.match(failures[0], new RegExp(`${key} to 1234, expected`));
|
|
}
|
|
});
|
|
|
|
test("a budget left on a library default is a failure, not an omission", () => {
|
|
// The exact shape this guard exists for: Vitest's own 5000/10000, which is
|
|
// what five of the six projects resolved to before #2323.
|
|
const failures = checkProject("tui", {
|
|
testTimeout: 5000,
|
|
hookTimeout: 10000,
|
|
teardownTimeout: 10000,
|
|
});
|
|
assert.equal(failures.length, 3);
|
|
});
|
|
|
|
test("an absent budget fails rather than being treated as fine", () => {
|
|
// `undefined !== 15000`. Worth pinning: a config that stopped being loaded
|
|
// resolves every key to undefined, and that must not read as a pass.
|
|
assert.equal(checkProject("unit", {}).length, 3);
|
|
});
|
|
|
|
test("a project with no row fails loudly", () => {
|
|
const failures = checkProject("brand-new", ok);
|
|
assert.equal(failures.length, 1);
|
|
assert.match(failures[0], /no row in EXPECTED_PROJECTS/);
|
|
});
|
|
|
|
test("retry is allowed to be unset or 0, and nothing else", () => {
|
|
assert.deepEqual(checkProject("unit", { ...ok }), []);
|
|
assert.deepEqual(checkProject("unit", { ...ok, retry: 0 }), []);
|
|
for (const retry of [1, 2]) {
|
|
const failures = checkProject("unit", { ...ok, retry });
|
|
assert.equal(failures.length, 1);
|
|
assert.match(failures[0], /retry/);
|
|
}
|
|
});
|
|
|
|
test("every project in the table is one the configs are asked for", () => {
|
|
const declared = CONFIG_ROOTS.flatMap((c) => c.projects).sort();
|
|
assert.deepEqual(declared, Object.keys(EXPECTED_PROJECTS).sort());
|
|
});
|
|
|
|
test("a nameless single project takes its name from the config entry", () => {
|
|
// The node clients set no `name`, so their one project resolves as "".
|
|
assert.equal(identifyProject({ name: "" }, ["cli"]), "cli");
|
|
});
|
|
|
|
test("a browser project is matched past its instance suffix", () => {
|
|
assert.equal(
|
|
identifyProject({ name: "storybook (chromium)" }, [
|
|
"unit",
|
|
"integration",
|
|
"storybook",
|
|
]),
|
|
"storybook",
|
|
);
|
|
});
|
|
|
|
test("an unrecognized project name is not silently matched", () => {
|
|
assert.equal(
|
|
identifyProject({ name: "e2e" }, ["unit", "storybook"]),
|
|
undefined,
|
|
);
|
|
// Nameless only resolves when the entry expects exactly one project —
|
|
// otherwise the guard cannot tell which row it belongs to.
|
|
assert.equal(identifyProject({ name: "" }, ["unit", "storybook"]), undefined);
|
|
});
|
|
|
|
test("a setup file that configures asyncUtilTimeout passes", () => {
|
|
const source = `import { cleanup, configure } from "@testing-library/react";
|
|
configure({ asyncUtilTimeout: 5000 });`;
|
|
assert.deepEqual(checkAsyncUtilSource(source, "@testing-library/react"), []);
|
|
});
|
|
|
|
test("configure() without the timeout is caught", () => {
|
|
const source = `import { configure } from "storybook/test";
|
|
configure({ testIdAttribute: "data-testid" });`;
|
|
assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [
|
|
"calls configure() without an asyncUtilTimeout",
|
|
]);
|
|
});
|
|
|
|
test("the import is checked, not just the call", () => {
|
|
// Storybook instruments its own Testing Library copy, so configuring
|
|
// `@testing-library/*` from a story setup configures a copy no play function
|
|
// ever calls — green by inspection, dead in practice.
|
|
const source = `import { configure } from "@testing-library/dom";
|
|
configure({ asyncUtilTimeout: 5000 });`;
|
|
assert.deepEqual(checkAsyncUtilSource(source, "storybook/test"), [
|
|
'does not import from "storybook/test"',
|
|
]);
|
|
});
|
|
|
|
test("a setup file with no configure() at all is caught", () => {
|
|
const failures = checkAsyncUtilSource(
|
|
"export const nothing = 1;\n",
|
|
"storybook/test",
|
|
);
|
|
assert.equal(failures.length, 2);
|
|
});
|
|
|
|
test("both web projects have an asyncUtilTimeout site", () => {
|
|
assert.deepEqual(ASYNC_UTIL_SITES.map((s) => s.project).sort(), [
|
|
"storybook",
|
|
"unit",
|
|
]);
|
|
});
|