Move tests out of src/ into a top-level tests/ directory
src/ now contains only source code; tests live alongside it under
tests/. Bun's test runner discovers '**/*.test.ts' so no test-runner
config change is needed.
Changes:
- tests/config.test.ts (was src/config.test.ts)
- tests/configFile.test.ts (was src/configFile.test.ts)
- Imports updated: ./config.ts -> ../src/config.ts (likewise for
configFile.ts and errors.ts).
- tsconfig.json include adds 'tests/**/*.ts' so tsc type-checks
the test files too.
All 25 tests still pass; tsc clean.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* configFile.test.ts
|
||||
* ------------------
|
||||
* Unit tests for the JSON config-file loader.
|
||||
* Run via `bun test` (or `bun run test`).
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { loadConfigFile } from "../src/configFile.ts";
|
||||
import { CliError } from "../src/errors.ts";
|
||||
|
||||
let TMP: string;
|
||||
|
||||
beforeAll(() => {
|
||||
TMP = mkdtempSync(join(tmpdir(), "move-cfg-test-"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(TMP, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeFixture(name: string, body: string): string {
|
||||
const p = join(TMP, name);
|
||||
writeFileSync(p, body);
|
||||
return p;
|
||||
}
|
||||
|
||||
describe("loadConfigFile (explicit path)", () => {
|
||||
test("returns parsed overrides for a valid file", () => {
|
||||
const path = writeFixture(
|
||||
"valid.json",
|
||||
JSON.stringify({ moveInterval: 60, verbose: true }),
|
||||
);
|
||||
const result = loadConfigFile(path);
|
||||
expect(result).not.toBeNull();
|
||||
// The bang is justified by the not-null assertion above.
|
||||
expect(result!.moveInterval).toBe(60);
|
||||
expect(result!.verbose).toBe(true);
|
||||
// Fields not in the file are undefined.
|
||||
expect(result!.checkInterval).toBeUndefined();
|
||||
expect(result!.stepDelay).toBeUndefined();
|
||||
expect(result!.stepCount).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns all-undefined overrides for an empty object", () => {
|
||||
const path = writeFixture("empty.json", "{}");
|
||||
const result = loadConfigFile(path);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.moveInterval).toBeUndefined();
|
||||
expect(result!.verbose).toBeUndefined();
|
||||
});
|
||||
|
||||
test("throws CliError when explicit path does not exist", () => {
|
||||
expect(() => loadConfigFile(join(TMP, "missing.json"))).toThrow(CliError);
|
||||
});
|
||||
|
||||
test("throws on malformed JSON, mentioning the file path", () => {
|
||||
const path = writeFixture("bad-json.json", "this is not json");
|
||||
expect(() => loadConfigFile(path)).toThrow(/is not valid JSON/);
|
||||
expect(() => loadConfigFile(path)).toThrow(new RegExp(path.replace(/[.]/g, "\\.")));
|
||||
});
|
||||
|
||||
test("throws when root is not an object (e.g. array)", () => {
|
||||
const path = writeFixture("array.json", "[1, 2, 3]");
|
||||
expect(() => loadConfigFile(path)).toThrow(/JSON object at the root/);
|
||||
});
|
||||
|
||||
test("throws when root is not an object (e.g. string)", () => {
|
||||
const path = writeFixture("string.json", "\"hello\"");
|
||||
expect(() => loadConfigFile(path)).toThrow(/JSON object at the root/);
|
||||
});
|
||||
|
||||
test("throws on an unknown key, naming the typo and the allowed set", () => {
|
||||
const path = writeFixture("typo.json", JSON.stringify({ movInterval: 60 }));
|
||||
expect(() => loadConfigFile(path)).toThrow(/unknown key 'movInterval'/);
|
||||
expect(() => loadConfigFile(path)).toThrow(/moveInterval/);
|
||||
});
|
||||
|
||||
test("throws on non-positive numeric values", () => {
|
||||
const negative = writeFixture("neg.json", JSON.stringify({ stepCount: -1 }));
|
||||
expect(() => loadConfigFile(negative)).toThrow(/'stepCount'.*positive number/);
|
||||
|
||||
const zero = writeFixture("zero.json", JSON.stringify({ stepDelay: 0 }));
|
||||
expect(() => loadConfigFile(zero)).toThrow(/'stepDelay'.*positive number/);
|
||||
});
|
||||
|
||||
test("throws when a numeric field has the wrong type", () => {
|
||||
const path = writeFixture("type.json", JSON.stringify({ moveInterval: "60" }));
|
||||
expect(() => loadConfigFile(path)).toThrow(/'moveInterval'.*positive number/);
|
||||
});
|
||||
|
||||
test("throws when verbose is the wrong type", () => {
|
||||
const path = writeFixture("verbose.json", JSON.stringify({ verbose: "yes" }));
|
||||
expect(() => loadConfigFile(path)).toThrow(/'verbose'.*boolean/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadConfigFile (default path)", () => {
|
||||
let savedXdg: string | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
savedXdg = process.env.XDG_CONFIG_HOME;
|
||||
// Point the default path under the test tmpdir so a missing file is
|
||||
// guaranteed (we never create $TMP/move/config.json).
|
||||
process.env.XDG_CONFIG_HOME = TMP;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (savedXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
||||
else process.env.XDG_CONFIG_HOME = savedXdg;
|
||||
});
|
||||
|
||||
test("returns null when no file exists at the default path", () => {
|
||||
expect(loadConfigFile(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user