Files
kandev/apps/web/scripts/vitest-project-selection.test.ts
T
2026-09-13 12:06:08 +01:00

151 lines
5.9 KiB
TypeScript

import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
const WEB_ROOT = path.resolve(import.meta.dirname, "..");
const CONFIG_PATH = path.join(WEB_ROOT, "vitest.config.ts");
const SELECTION_PATH = path.join(import.meta.dirname, "vitest-project-selection.ts");
const NODE_PROJECT = "node" as const;
const BROWSER_PROJECT = "browser" as const;
const BROWSER_LOCALES_PROJECT = "browser-locales" as const;
const FIXTURE_TEST_SOURCE = "export const testValue = true;\n";
const FIXTURE_PLAYWRIGHT_SOURCE = "export const shouldNotRun = true;\n";
type SelectionModule = typeof import("./vitest-project-selection");
async function loadSelection(): Promise<SelectionModule> {
const config = readFileSync(CONFIG_PATH, "utf8");
expect(config).toContain("projects:");
expect(config).toContain("projectDefinitions.browser.include");
expect(config).toContain('projectDefinitions["browser-locales"].include');
expect(existsSync(SELECTION_PATH)).toBe(true);
return import(/* @vite-ignore */ pathToFileURL(SELECTION_PATH).href);
}
function withFixture<T>(files: Record<string, string>, callback: (root: string) => T): T {
const root = mkdtempSync(path.join(tmpdir(), "kandev-vitest-selection-"));
try {
for (const [relativeFile, contents] of Object.entries(files)) {
const file = path.join(root, relativeFile);
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, contents);
}
return callback(root);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
describe("Vitest project selection", () => {
it("keeps every test file in exactly one project", async () => {
const { discoverTestFiles, matchingProjects } = await loadSelection();
const files = discoverTestFiles(WEB_ROOT);
expect(files.length).toBeGreaterThan(1000);
expect(files.some((file) => file.startsWith("e2e/") && file.endsWith(".test.ts"))).toBe(true);
for (const file of files) {
expect(matchingProjects(file), file).toHaveLength(1);
}
});
it("keeps the projects disjoint while retaining the complete file identity set", async () => {
const { discoverTestFiles, matchingProjects, projectFiles } = await loadSelection();
const files = discoverTestFiles(WEB_ROOT);
const selected = new Set(files.flatMap((file) => matchingProjects(file)));
expect(selected).toEqual(new Set([NODE_PROJECT, BROWSER_PROJECT, BROWSER_LOCALES_PROJECT]));
expect(new Set(projectFiles.node)).toEqual(
new Set(files.filter((file) => matchingProjects(file)[0] === NODE_PROJECT)),
);
expect(new Set(projectFiles.browser)).toEqual(
new Set(files.filter((file) => matchingProjects(file)[0] === BROWSER_PROJECT)),
);
expect(new Set(projectFiles["browser-locales"])).toEqual(
new Set(files.filter((file) => matchingProjects(file)[0] === BROWSER_LOCALES_PROJECT)),
);
});
it("matches configured project include and exclude rules for fixture identities", async () => {
const {
PROJECT_NAMES,
buildProjectSelection,
discoverTestFiles,
matchingProjectsForSelection,
matchesConfiguredProject,
matchesTestFileGlob,
} = await loadSelection();
withFixture(
{
"scripts/unclassified.test.ts": FIXTURE_TEST_SOURCE,
"components/indirect-locale.test.ts":
'import "./indirect-locale-helper";\nexport const testValue = true;\n',
"components/indirect-locale-helper.ts":
'export function setLocale() { return changeLanguage("en"); }\n',
"components/unknown.spec.cjsx": FIXTURE_TEST_SOURCE,
"components/unknown.spec.ctsx": FIXTURE_TEST_SOURCE,
"components/unknown.test.cts": FIXTURE_TEST_SOURCE,
"components/unknown.test.mjsx": FIXTURE_TEST_SOURCE,
"e2e/ignored.spec.ts": FIXTURE_PLAYWRIGHT_SOURCE,
"e2e/fixtures/ignored.test.ts": FIXTURE_PLAYWRIGHT_SOURCE,
"e2e/pages/ignored.test.ts": FIXTURE_PLAYWRIGHT_SOURCE,
},
(root) => {
const selection = buildProjectSelection(root);
const files = discoverTestFiles(root);
expect(files).toEqual([
"components/indirect-locale.test.ts",
"components/unknown.spec.cjsx",
"components/unknown.spec.ctsx",
"components/unknown.test.cts",
"components/unknown.test.mjsx",
"scripts/unclassified.test.ts",
]);
for (const file of [
"components/unknown.spec.cjsx",
"components/unknown.spec.ctsx",
"components/unknown.test.cts",
"components/unknown.test.mjsx",
]) {
expect(matchesTestFileGlob(file)).toBe(true);
}
for (const file of files) {
const configuredProjects = PROJECT_NAMES.filter((project) =>
matchesConfiguredProject(file, project, selection),
);
expect(configuredProjects).toEqual(matchingProjectsForSelection(file, selection));
expect(configuredProjects, file).toHaveLength(1);
}
expect(matchingProjectsForSelection("scripts/unclassified.test.ts", selection)).toEqual([
BROWSER_LOCALES_PROJECT,
]);
expect(
matchingProjectsForSelection("components/indirect-locale.test.ts", selection),
).toEqual([BROWSER_LOCALES_PROJECT]);
expect(matchesConfiguredProject("e2e/ignored.spec.ts", BROWSER_PROJECT, selection)).toBe(
false,
);
expect(
matchesConfiguredProject("e2e/ignored.spec.ts", BROWSER_LOCALES_PROJECT, selection),
).toBe(false);
},
);
});
it("gives node-only configuration tests enough time under project contention", () => {
const config = readFileSync(CONFIG_PATH, "utf8");
expect(config).toContain('name: "node"');
expect(config).toContain("testTimeout: 15_000");
});
});