test/scripts/unit-tests-report-imports.test.ts
Rama main · réplica de lectura del repositorio original.
import { assert } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import {
findMisplacedTests,
formatMisplacedSection,
type MisplacedTest,
parseImportSpecifiers,
resolveImportToSourceOrNull,
resolveTestImports,
} from "#scripts/unit-tests-report-imports.ts";
import { options } from "./unit-tests-report-fixtures.ts";
/** A small import map covering an exact alias, nested and general directory
* aliases, and a non-source alias, so resolution's branches are all exercised. */
const importMap: Record<string, string> = {
"#db/": "./src/shared/db/",
"#fp": "./src/fp.ts",
"#shared/": "./src/shared/",
"#test-utils/": "./test/test-utils/",
valibot: "npm:valibot@^1.4.1",
};
/** Build a test-with-imports record. */
const imp = (path: string, imports: string[], loadsApp = false) => ({
imports,
loadsApp,
path,
});
describe("parseImportSpecifiers", () => {
test("extracts import and re-export specifiers, including multiline", () => {
const text = [
'import { a } from "#fp";',
"import {",
" b,",
'} from "#db/x.ts";',
'export { c } from "#shared/y.ts";',
].join("\n");
expect(parseImportSpecifiers(text)).toEqual([
"#fp",
"#db/x.ts",
"#shared/y.ts",
]);
});
test("ignores side-effect imports with no from clause", () => {
expect(parseImportSpecifiers('import "./setup.ts";')).toEqual([]);
});
test("ignores specifiers that appear inside line or block comments", () => {
const text = [
'import { a } from "#fp";',
'// import { old } from "#shared/gone.ts";',
"/*",
' export { b } from "#shared/also-gone.ts";',
"*/",
].join("\n");
expect(parseImportSpecifiers(text)).toEqual(["#fp"]);
});
test("still finds an import when a comment sits between `from` and the path", () => {
// Comment stripping must remove the comment, not replace it with text: a
// leftover word between `from` and the specifier would stop the specifier
// being recognised (`from mutated "x"` matches nothing).
expect(
parseImportSpecifiers('import { x } from /* note */ "#shared/real.ts";'),
).toEqual(["#shared/real.ts"]);
expect(
parseImportSpecifiers('import { y } from // note\n"#shared/other.ts";'),
).toEqual(["#shared/other.ts"]);
});
test("captures dynamic import() specifiers alongside static ones", () => {
const text = [
'import { settings } from "#db/settings.ts";',
'const mod = await import("#routes/wallet/google.ts");',
].join("\n");
expect(parseImportSpecifiers(text)).toEqual([
"#db/settings.ts",
"#routes/wallet/google.ts",
]);
});
test("skips type-query dynamic imports (typeof and member access)", () => {
// Both forms are erased at runtime, so neither names code under test.
const text = [
'type M = typeof import("#db/table.ts");',
'const row = {} as import("#db/attendee-types.ts").Row;',
'const mod = await import("#routes/wallet/google.ts");',
].join("\n");
expect(parseImportSpecifiers(text)).toEqual(["#routes/wallet/google.ts"]);
});
test("skips type-only import and export statements", () => {
// A type import is erased at runtime, so it doesn't name the code under test.
const text = [
'import type { Listing } from "#types";',
'export type { Foo } from "#shared/foo.ts";',
'import { real } from "#shared/real.ts";',
].join("\n");
expect(parseImportSpecifiers(text)).toEqual(["#shared/real.ts"]);
});
test("returns nothing for text with no imports", () => {
expect(parseImportSpecifiers("const x = 1;")).toEqual([]);
});
});
describe("resolveImportToSourceOrNull", () => {
test("maps a directory alias's tail onto the source root", () => {
expect(
resolveImportToSourceOrNull("#shared/email.ts", importMap, "src"),
).toBe("src/shared/email.ts");
});
test("prefers the longest matching alias so a nested dir wins", () => {
// Both "#shared/" and "#db/" match; the specific one must win.
expect(resolveImportToSourceOrNull("#db/client.ts", importMap, "src")).toBe(
"src/shared/db/client.ts",
);
});
test("maps an exact alias to its whole target", () => {
expect(resolveImportToSourceOrNull("#fp", importMap, "src")).toBe(
"src/fp.ts",
);
});
test("does not treat an exact alias as a directory prefix", () => {
// "#fp" is exact, so a deeper specifier under it resolves to nothing rather
// than being glued onto the file target ("src/fp.ts/x.ts").
expect(
resolveImportToSourceOrNull("#fp/x.ts", importMap, "src"),
).toBeNull();
});
test("the longest directory alias wins even when targets diverge", () => {
// Nested alias points somewhere unrelated to its parent, so picking the
// shorter alias would resolve to a different file — only longest-first is
// correct.
// Targets are different lengths from their aliases, so ordering by alias
// length (correct) and by target length (a mutation) give different winners.
const diverging = { "#a/": "./src/xx/", "#a/b/": "./src/y/" };
expect(resolveImportToSourceOrNull("#a/b/c.ts", diverging, "src")).toBe(
"src/y/c.ts",
);
});
test("returns null for a non-source specifier", () => {
expect(resolveImportToSourceOrNull("valibot", importMap, "src")).toBeNull();
expect(
resolveImportToSourceOrNull("#test-utils/db.ts", importMap, "src"),
).toBeNull();
expect(
resolveImportToSourceOrNull("#unknown", importMap, "src"),
).toBeNull();
});
});
describe("resolveTestImports", () => {
test("resolves, de-duplicates, and drops non-source imports", () => {
const text = [
'import { a } from "#db/client.ts";',
'import { b } from "#db/client.ts";', // duplicate
'import { c } from "#fp";',
'import { d } from "valibot";', // non-source, dropped
].join("\n");
expect(resolveTestImports(text, importMap, "src")).toEqual([
"src/shared/db/client.ts",
"src/fp.ts",
]);
});
});
describe("findMisplacedTests", () => {
const sources = ["src/shared/email.ts", "src/shared/db/client.ts"];
const appEntry = "src/features/index.ts";
test("flags a single-source test that lives off its source's mirror", () => {
const result = findMisplacedTests(
[imp("test/lib/mailer.test.ts", ["src/shared/email.ts"])],
sources,
options,
appEntry,
);
expect(result).toEqual([
{
basenameMatch: false,
source: "src/shared/email.ts",
suggestedPrefix: "test/shared/email",
test: "test/lib/mailer.test.ts",
},
]);
});
test("marks a basename match as high confidence", () => {
const result = findMisplacedTests(
[imp("test/lib/email.test.ts", ["src/shared/email.ts"])],
sources,
options,
appEntry,
);
expect(result[0]).toMatchObject({ basenameMatch: true });
});
test("skips a test already sitting at its source's direct mirror", () => {
expect(
findMisplacedTests(
[imp("test/shared/email.test.ts", ["src/shared/email.ts"])],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("skips a test inside its source's mirror directory (suite convention)", () => {
expect(
findMisplacedTests(
[imp("test/shared/email/bounces.test.ts", ["src/shared/email.ts"])],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("skips a test on an exempt tree even if it imports one source", () => {
// test/e2e/ is exempt in the fixture options, so it never mirrors a source.
expect(
findMisplacedTests(
[imp("test/e2e/flow.test.ts", ["src/shared/email.ts"])],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("skips an integration test that imports the app entry", () => {
expect(
findMisplacedTests(
[imp("test/lib/route.test.ts", [appEntry, "src/shared/email.ts"])],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("skips a test that reaches the app through a helper", () => {
// The one subject is an import the test only checks along the way; the
// pages came in through a helper the walk followed. That suite is
// integration, and the list must not move it onto a database mirror
// (issue #2312).
expect(
findMisplacedTests(
[
imp(
"test/features/maintenance.test.ts",
["src/shared/db/client.ts"],
true,
),
],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("skips a test importing more than one source", () => {
expect(
findMisplacedTests(
[
imp("test/lib/multi.test.ts", [
"src/shared/email.ts",
"src/shared/db/client.ts",
]),
],
sources,
options,
appEntry,
),
).toEqual([]);
});
test("ignores imports of exempt (non-testable) sources", () => {
// A locale table is exempt, so a test importing only it has no subject.
const result = findMisplacedTests(
[imp("test/lib/copy.test.ts", ["src/locales/en/index.ts"])],
[...sources, "src/locales/en/index.ts"],
options,
appEntry,
);
expect(result).toEqual([]);
});
test("orders basename matches first, then by test path", () => {
const result = findMisplacedTests(
[
imp("test/lib/z-mailer.test.ts", ["src/shared/email.ts"]),
imp("test/lib/email.test.ts", ["src/shared/email.ts"]),
imp("test/lib/a-mailer.test.ts", ["src/shared/email.ts"]),
],
sources,
options,
appEntry,
);
expect(result.map((entry) => entry.test)).toEqual([
"test/lib/email.test.ts", // basename match, first
"test/lib/a-mailer.test.ts", // then alphabetical
"test/lib/z-mailer.test.ts",
]);
});
test("promotes a match ahead of a non-match given in the wrong order", () => {
// The non-match is listed first, so only a comparator that sinks it (the
// basename-mismatch branch) yields the match-first order.
const result = findMisplacedTests(
[
imp("test/lib/mailer.test.ts", ["src/shared/email.ts"]), // non-match
imp("test/lib/email.test.ts", ["src/shared/email.ts"]), // match
],
sources,
options,
appEntry,
);
expect(result.map((entry) => entry.test)).toEqual([
"test/lib/email.test.ts",
"test/lib/mailer.test.ts",
]);
});
});
describe("formatMisplacedSection", () => {
const entry = (
test: string,
suggestedPrefix: string,
basenameMatch: boolean,
): MisplacedTest => ({
basenameMatch,
source: "src/shared/email.ts",
suggestedPrefix,
test,
});
test("reports a friendly note when nothing is misplaced", () => {
expect(formatMisplacedSection([], null)).toEqual([
" (none — every unit test sits at its source's mirror)",
]);
});
test("marks basename matches with a tick and names the target and source", () => {
const lines = formatMisplacedSection(
[entry("test/lib/email.test.ts", "test/shared/email", true)],
null,
);
expect(lines[0]).toBe(
" ✓ test/lib/email.test.ts\n → test/shared/email (imports src/shared/email.ts)",
);
});
test("uses a blank marker (not a tick) for a non-matching row", () => {
const lines = formatMisplacedSection(
[entry("test/lib/mailer.test.ts", "test/shared/email", false)],
null,
);
// Two indent spaces, then the blank marker space, then a space before path.
expect(lines[0]).toBe(
" test/lib/mailer.test.ts\n → test/shared/email (imports src/shared/email.ts)",
);
});
test("truncates to the limit and reports how many more remain", () => {
const rows = [
entry("test/lib/a.test.ts", "test/shared/a", false),
entry("test/lib/b.test.ts", "test/shared/b", false),
entry("test/lib/c.test.ts", "test/shared/c", false),
];
const lines = formatMisplacedSection(rows, 2);
expect(lines).toHaveLength(3);
expect(lines[2]).toBe(" … and 1 more (use --all to list)");
});
test("shows every row when under the limit with no truncation note", () => {
const lines = formatMisplacedSection(
[entry("test/lib/a.test.ts", "test/shared/a", false)],
25,
);
expect(lines).toHaveLength(1);
assert(!(lines[0] ?? "").includes("more"));
});
});