test/scripts/test-subjects.test.ts
Rama main · réplica de lectura del repositorio original.
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
cachingReader,
collectTestSubjects,
resolveProjectImportOrNull,
} from "#scripts/test-subjects.ts";
const IMPORT_MAP = {
"#cli/": "./cli/",
"#db/": "./src/shared/db/",
"#fp": "./src/fp.ts",
"#routes": "./src/features/index.ts",
"#routes/": "./src/features/",
"#scripts/": "./scripts/",
"#shared/": "./src/shared/",
"#test-utils/": "./test/test-utils/",
"#test/": "./test/",
"#ui/": "./src/ui/",
// A non-alias key that still points into src: only `#` specifiers count.
"legacy/": "./src/legacy/",
};
/** Run the subject walk over an in-memory tree. */
const walkTree = (files: Record<string, string>, testFile: string) =>
collectTestSubjects(
testFile,
readerFor(files),
IMPORT_MAP,
testTreeOf(files),
);
/** Reads from an in-memory tree; an unknown path throws, as a real read would. */
const readerFor =
(files: Record<string, string>) =>
(path: string): Promise<string> => {
const text = files[path];
if (text === undefined) throw new Error(`No such file: ${path}`);
return Promise.resolve(text);
};
/** The test-tree paths of an in-memory file set — what the walk may follow. */
const testTreeOf = (files: Record<string, string>): Set<string> =>
new Set(Object.keys(files).filter((path) => path.startsWith("test/")));
describe("test subjects", () => {
describe("resolveProjectImportOrNull", () => {
test("resolves a directory alias to its target path", () => {
expect(
resolveProjectImportOrNull(
"#shared/csrf.ts",
IMPORT_MAP,
"test/shared/csrf.test.ts",
),
).toBe("src/shared/csrf.ts");
});
test("resolves an exact alias to the whole target", () => {
expect(
resolveProjectImportOrNull("#fp", IMPORT_MAP, "test/fp.test.ts"),
).toBe("src/fp.ts");
});
test("resolves an alias that points into the test tree", () => {
expect(
resolveProjectImportOrNull(
"#test/ui/shared.ts",
IMPORT_MAP,
"test/shared/a.test.ts",
),
).toBe("test/ui/shared.ts");
});
test("resolves a sibling relative import against the importing file", () => {
expect(
resolveProjectImportOrNull(
"./helpers.ts",
IMPORT_MAP,
"test/shared/merge/cleanup.test.ts",
),
).toBe("test/shared/merge/helpers.ts");
});
test("walks up for a parent relative import", () => {
expect(
resolveProjectImportOrNull(
"../../shared.ts",
IMPORT_MAP,
"test/ui/templates/table/component.test.tsx",
),
).toBe("test/ui/shared.ts");
});
test("resolves an alias that points at a tooling script", () => {
expect(
resolveProjectImportOrNull(
"#scripts/path.ts",
IMPORT_MAP,
"test/scripts/path.test.ts",
),
).toBe("scripts/path.ts");
});
test("resolves an alias that points at a command-line entry", () => {
expect(
resolveProjectImportOrNull(
"#cli/backup.ts",
IMPORT_MAP,
"test/scripts/backup.test.ts",
),
).toBe("cli/backup.ts");
});
test("ignores a bare import-map key that is not a # alias", () => {
expect(
resolveProjectImportOrNull(
"legacy/thing.ts",
IMPORT_MAP,
"test/shared/a.test.ts",
),
).toBeNull();
});
test("returns null for a module outside the project", () => {
expect(
resolveProjectImportOrNull(
"@std/expect",
IMPORT_MAP,
"test/shared/a.test.ts",
),
).toBeNull();
});
test("returns null for an alias the import map does not define", () => {
expect(
resolveProjectImportOrNull(
"#nope/thing.ts",
IMPORT_MAP,
"test/shared/a.test.ts",
),
).toBeNull();
});
});
describe("collectTestSubjects", () => {
test("collects the sources the test imports itself", async () => {
const files = {
"test/shared/csrf.test.ts": `import { signCsrf } from "#shared/csrf.ts";`,
};
expect(
(await walkTree(files, "test/shared/csrf.test.ts")).subjects,
).toEqual(["src/shared/csrf.ts"]);
});
test("collects the source a shared helper imports on the test's behalf", async () => {
const files = {
"test/ui/table/component.test.tsx": [
`import { getCurrentCsrfToken } from "#shared/csrf.ts";`,
`import { render } from "./shared.ts";`,
].join("\n"),
"test/ui/table/shared.ts": `import { AttendeeTable } from "#ui/templates/table/component.tsx";`,
};
expect(
(
await walkTree(files, "test/ui/table/component.test.tsx")
).subjects.sort(),
).toEqual(["src/shared/csrf.ts", "src/ui/templates/table/component.tsx"]);
});
test("names no subject for what a test-utils helper reaches", async () => {
// Shared helpers under test/test-utils/ are plumbing nearly every test
// needs — a database row, a config value — so nothing they import says
// what the test is about.
const files = {
"test/shared/email.test.ts": [
`import { sendEmail } from "#shared/email.ts";`,
`import { describeWithEnv } from "#test-utils/db.ts";`,
].join("\n"),
"test/test-utils/db.ts": [
`import { getDb } from "#db/client.ts";`,
`import { withEnv } from "#test-utils/env.ts";`,
].join("\n"),
"test/test-utils/env.ts": `import { config } from "#shared/config.ts";`,
};
expect(
(await walkTree(files, "test/shared/email.test.ts")).subjects,
).toEqual(["src/shared/email.ts"]);
});
test("names no subject for a test-utils helper an absolute root reached", async () => {
// A scan given an absolute test root walks absolute paths, and a
// helper a test reaches by a relative import resolves absolute — the
// utils exclusion must still leave the test naming its one subject.
const files = {
"/proj/test/shared/email.test.ts": [
`import { sendEmail } from "#shared/email.ts";`,
`import { setup } from "../test-utils/db.ts";`,
].join("\n"),
"/proj/test/test-utils/db.ts": `import { getDb } from "#db/client.ts";`,
};
expect(
(
await collectTestSubjects(
"/proj/test/shared/email.test.ts",
readerFor(files),
IMPORT_MAP,
new Set(Object.keys(files)),
)
).subjects,
).toEqual(["src/shared/email.ts"]);
});
test("follows helpers through more than one hop", async () => {
const files = {
"test/a.test.ts": `import { one } from "#test/first.ts";`,
"test/first.ts": `import { two } from "#test/second.ts";`,
"test/second.ts": `import { deep } from "#shared/deep.ts";`,
};
expect((await walkTree(files, "test/a.test.ts")).subjects).toEqual([
"src/shared/deep.ts",
]);
});
test("does not follow a source's own imports", async () => {
const files = {
// Never read: following it would make every test exercise the tree.
"src/shared/a.ts": `import { b } from "#shared/b.ts";`,
"test/shared/a.test.ts": `import { a } from "#shared/a.ts";`,
};
expect((await walkTree(files, "test/shared/a.test.ts")).subjects).toEqual(
["src/shared/a.ts"],
);
});
test("survives helpers that import each other in a cycle", async () => {
const files = {
"test/a.test.ts": `import { one } from "#test/first.ts";`,
"test/first.ts": [
`import { two } from "#test/second.ts";`,
`import { a } from "#shared/a.ts";`,
].join("\n"),
"test/second.ts": `import { one } from "#test/first.ts";`,
};
expect((await walkTree(files, "test/a.test.ts")).subjects).toEqual([
"src/shared/a.ts",
]);
});
test("reports no subjects for a test that imports nothing of ours", async () => {
const files = {
"test/pure.test.ts": `import { expect } from "@std/expect";`,
};
expect((await walkTree(files, "test/pure.test.ts")).subjects).toEqual([]);
});
test("marks a test app-loading when a helper reaches the app entry", async () => {
// A suite that drives pages through session.ts is an integration suite
// however it reaches the app — the misplaced-test list must not move it
// onto the mirror of an import it only checks along the way (issue
// #2312).
const files = {
"test/features/maintenance.test.ts": [
`import { resetDb } from "#test-utils/db.ts";`,
`import { adminFormPost } from "#test-utils/session.ts";`,
].join("\n"),
"test/test-utils/db.ts": `import { getDb } from "#db/client.ts";`,
"test/test-utils/mocks.ts": `import { handleRequest } from "#routes";`,
"test/test-utils/session.ts": `import { awaitTestRequest } from "#test-utils/mocks.ts";`,
};
const walked = await walkTree(files, "test/features/maintenance.test.ts");
expect(walked.loadsApp).toBe(true);
expect(walked.subjects).toEqual([]);
});
test("marks a test app-loading when a helper reaches a route module", async () => {
const files = {
"test/features/maintenance.test.ts": `import { postSetting } from "#test-utils/session.ts";`,
"test/test-utils/session.ts": `const { handleRequest } = await import("#routes/admin/settings.ts");`,
};
expect(
(await walkTree(files, "test/features/maintenance.test.ts")).loadsApp,
).toBe(true);
});
test("keeps a route module the test imports itself a plain subject", async () => {
// The test file's own import names its subject on purpose, so it stays
// a move candidate — only a helper's reach makes a test integration.
const files = {
"test/ui/templates/checkin/routes.test.ts": `const { routeCheckin } = await import("#routes/checkin.ts");`,
};
const walked = await walkTree(
files,
"test/ui/templates/checkin/routes.test.ts",
);
expect(walked.subjects).toEqual(["src/features/checkin.ts"]);
expect(walked.loadsApp).toBe(false);
});
});
describe("cachingReader", () => {
test("reads each path once however often it is asked for", async () => {
const reads: string[] = [];
const read = cachingReader((path) => {
reads.push(path);
return Promise.resolve(`text of ${path}`);
});
expect(await read("test/helpers.ts")).toBe("text of test/helpers.ts");
expect(await read("test/helpers.ts")).toBe("text of test/helpers.ts");
expect(await read("test/other.ts")).toBe("text of test/other.ts");
expect(reads).toEqual(["test/helpers.ts", "test/other.ts"]);
});
});
});