/** * Tests for `check-i18n-keys.ts`. * * Build a fixtures filesystem on disk under `mkdtemp` and call the * exported helpers (`extractKeysFromSource`, `loadBundleKeys`, * `diff`) directly. The script's `main()` integrates with * `process.argv` / `process.exit` so it is exercised end-to-end via * a `Bun.spawn` round-trip in the last test. */ import { afterEach, beforeEach, describe, expect, it } from "@suaveplan/testing/runner"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { diff, extractKeysFromSource, loadBundleKeys, } from "./check-i18n-keys.js"; let workDir: string; beforeEach(async () => { workDir = await mkdtemp(join(tmpdir(), "i18n-keys-")); }); afterEach(async () => { await rm(workDir, { recursive: true, force: true }); }); async function writeFixture(rel: string, content: string): Promise { const full = join(workDir, rel); await mkdir(join(full, ".."), { recursive: true }); await writeFile(full, content, "utf8"); return full; } describe("extractKeysFromSource", () => { it("captures t() calls with double quotes", { timeout: 5000 }, async () => { const f = await writeFixture( "a.tsx", `const out = t("react-ui.foo.bar");` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.has("react-ui.foo.bar")).toBe(true); }); it("captures t() calls with single quotes", { timeout: 5000 }, async () => { const f = await writeFixture( "a.tsx", `const out = t('react-ui.foo.bar');` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.has("react-ui.foo.bar")).toBe(true); }); it("captures i18nKey JSX attribute", { timeout: 5000 }, async () => { const f = await writeFixture( "a.tsx", `` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.has("react-ui.greeting")).toBe(true); }); it("captures the DEFAULT_*_KEY const idiom", { timeout: 5000 }, async () => { const f = await writeFixture( "a.tsx", `const DEFAULT_LABEL_KEY = "react-ui.thing.label";` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.has("react-ui.thing.label")).toBe(true); }); it("captures `?? \"react-ui.…\"` fallback shorthand", { timeout: 5000, }, async () => { const f = await writeFixture( "a.tsx", `const k = labelKey ?? "react-ui.fallback.label";` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.has("react-ui.fallback.label")).toBe(true); }); it("records the source location for each key", { timeout: 5000, }, async () => { const f = await writeFixture( "deep/nested.tsx", `// header\nconst k = t("react-ui.line.two");` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.get("react-ui.line.two")).toMatch(/deep\/nested\.tsx:2$/); }); it("ignores non-namespace strings", { timeout: 5000 }, async () => { const f = await writeFixture( "a.tsx", `const k = t("other.namespace.key");` ); const { keys } = await extractKeysFromSource([f], workDir); expect(keys.size).toBe(0); }); }); describe("loadBundleKeys", () => { it("loads only keys in the react-ui namespace", { timeout: 5000, }, async () => { const path = join(workDir, "bundle.json"); await writeFile( path, JSON.stringify({ "react-ui.foo": "Foo", "react-ui.bar": "Bar", "other.namespace.key": "other", }), "utf8" ); const keys = await loadBundleKeys(path); expect(keys.has("react-ui.foo")).toBe(true); expect(keys.has("react-ui.bar")).toBe(true); expect(keys.has("other.namespace.key")).toBe(false); }); }); describe("diff", () => { it("reports MISSING for code-only keys", { timeout: 5000 }, () => { const codeKeys = new Map([["react-ui.code.only", "src/x.tsx:1"]]); const bundleKeys = new Set(["react-ui.bundled"]); const diags = diff(codeKeys, bundleKeys); expect( diags.find((d) => d.kind === "MISSING")?.key ).toBe("react-ui.code.only"); }); it("reports ORPHAN for bundle-only keys", { timeout: 5000 }, () => { const codeKeys = new Map(); const bundleKeys = new Set(["react-ui.bundle.only"]); const diags = diff(codeKeys, bundleKeys); expect( diags.find((d) => d.kind === "ORPHAN")?.key ).toBe("react-ui.bundle.only"); }); it("returns empty diagnostics on a clean source/bundle pair", { timeout: 5000, }, () => { const codeKeys = new Map([["react-ui.k", "src/x.tsx:1"]]); const bundleKeys = new Set(["react-ui.k"]); expect(diff(codeKeys, bundleKeys)).toEqual([]); }); it("orders MISSING before ORPHAN, then alphabetically", { timeout: 5000, }, () => { const codeKeys = new Map([ ["react-ui.b", "x:1"], ["react-ui.a", "x:2"], ]); const bundleKeys = new Set(["react-ui.zzz", "react-ui.aaa"]); const diags = diff(codeKeys, bundleKeys); expect(diags.map((d) => `${d.kind}:${d.key}`)).toEqual([ "MISSING:react-ui.a", "MISSING:react-ui.b", "ORPHAN:react-ui.aaa", "ORPHAN:react-ui.zzz", ]); }); });