mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
215 lines
7.9 KiB
TypeScript
215 lines
7.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import type { JsonReport } from "@react-doctor/core";
|
|
import { JsonReport as JsonReportSchema } from "@react-doctor/core/schemas";
|
|
import * as Schema from "effect/Schema";
|
|
import { INTERNAL_ERROR_JSON_FALLBACK } from "../src/cli/utils/constants.js";
|
|
import {
|
|
enableJsonMode,
|
|
isJsonModeActive,
|
|
setJsonReportDirectory,
|
|
setJsonReportMode,
|
|
writeJsonErrorReport,
|
|
writeJsonReport,
|
|
} from "../src/cli/utils/json-mode.js";
|
|
|
|
const buildOkReport = (overrides: Partial<JsonReport> = {}): JsonReport => ({
|
|
schemaVersion: 1,
|
|
version: "test",
|
|
ok: true,
|
|
directory: "/tmp/foo",
|
|
mode: "full",
|
|
diff: null,
|
|
projects: [],
|
|
diagnostics: [],
|
|
summary: {
|
|
errorCount: 0,
|
|
warningCount: 0,
|
|
affectedFileCount: 0,
|
|
totalDiagnosticCount: 0,
|
|
score: null,
|
|
scoreLabel: null,
|
|
},
|
|
elapsedMilliseconds: 0,
|
|
error: null,
|
|
...overrides,
|
|
});
|
|
|
|
// HACK: json-mode owns a module-level singleton. Tests intentionally don't
|
|
// reset it between cases — each `enableJsonMode` overwrites the context,
|
|
// and we can read `isJsonModeActive()` to confirm the prior state was
|
|
// replaced.
|
|
|
|
interface CapturedStdout {
|
|
lines: string[];
|
|
restore: () => void;
|
|
}
|
|
|
|
const captureStdout = (): CapturedStdout => {
|
|
const lines: string[] = [];
|
|
const spy = vi.spyOn(process.stdout, "write").mockImplementation(((
|
|
chunk: string | Uint8Array,
|
|
) => {
|
|
lines.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
|
|
return true;
|
|
}) as never);
|
|
return { lines, restore: () => spy.mockRestore() };
|
|
};
|
|
|
|
describe("json-mode lifecycle", () => {
|
|
let captured: CapturedStdout;
|
|
|
|
beforeEach(() => {
|
|
captured = captureStdout();
|
|
});
|
|
|
|
afterEach(() => {
|
|
captured.restore();
|
|
});
|
|
|
|
it("starts inactive before enableJsonMode is called from a fresh module load", () => {
|
|
// NOTE: this assertion is only reliable on the very first test run after
|
|
// module load. We assert weakly — just that the activation toggle works.
|
|
enableJsonMode({ compact: false, directory: "/tmp/initial" });
|
|
expect(isJsonModeActive()).toBe(true);
|
|
});
|
|
|
|
it("writeJsonErrorReport silently returns when json mode was never enabled in this process — assertion via direct call after explicit setup", () => {
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
expect(isJsonModeActive()).toBe(true);
|
|
writeJsonErrorReport(new Error("boom"));
|
|
expect(captured.lines.length).toBeGreaterThan(0);
|
|
const written = captured.lines.join("");
|
|
expect(written).toContain('"ok":false');
|
|
expect(written).toContain("boom");
|
|
});
|
|
|
|
it("writeJsonReport emits compact JSON when compact: true", () => {
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
captured.lines.length = 0;
|
|
writeJsonReport(buildOkReport());
|
|
const written = captured.lines.join("");
|
|
expect(written).not.toContain("\n ");
|
|
expect(written.endsWith("\n")).toBe(true);
|
|
});
|
|
|
|
it("writeJsonReport emits indented JSON when compact: false", () => {
|
|
enableJsonMode({ compact: false, directory: "/tmp/foo" });
|
|
captured.lines.length = 0;
|
|
writeJsonReport(buildOkReport());
|
|
const written = captured.lines.join("");
|
|
expect(written).toContain("\n ");
|
|
});
|
|
|
|
it("setJsonReportDirectory updates the directory used by writeJsonErrorReport", () => {
|
|
enableJsonMode({ compact: true, directory: "/tmp/initial" });
|
|
setJsonReportDirectory("/tmp/resolved");
|
|
captured.lines.length = 0;
|
|
writeJsonErrorReport(new Error("nope"));
|
|
const written = captured.lines.join("");
|
|
expect(written).toContain("/tmp/resolved");
|
|
expect(written).not.toContain("/tmp/initial");
|
|
});
|
|
|
|
it("setJsonReportMode updates the mode used by writeJsonErrorReport", () => {
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
setJsonReportMode("diff");
|
|
captured.lines.length = 0;
|
|
writeJsonErrorReport(new Error("nope"));
|
|
const written = captured.lines.join("");
|
|
expect(written).toContain('"mode":"diff"');
|
|
});
|
|
|
|
it("setJsonReportMode is a no-op when json mode was disabled (no enableJsonMode call after disable)", () => {
|
|
// Once enabled, the helper currently cannot be disabled mid-process —
|
|
// this guards against accidental side effects if a future change
|
|
// introduces a disable path. The mode setter should silently ignore
|
|
// calls when context is null.
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
setJsonReportMode("staged");
|
|
// Re-enabling resets mode back to "full".
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
captured.lines.length = 0;
|
|
writeJsonErrorReport(new Error("nope"));
|
|
const written = captured.lines.join("");
|
|
expect(written).toContain('"mode":"full"');
|
|
});
|
|
|
|
it("falls back to the canned internal-error JSON when buildJsonReportError throws", () => {
|
|
enableJsonMode({ compact: true, directory: "/tmp/foo" });
|
|
captured.lines.length = 0;
|
|
|
|
// Pass a synthetic "error" whose property accessors throw, forcing
|
|
// buildJsonReportError's internal serialization to throw too.
|
|
const exploding = new Proxy({} as unknown as Error, {
|
|
get: (_, property) => {
|
|
if (property === "name" || property === "message" || property === "stack") {
|
|
throw new Error("inner explosion");
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
writeJsonErrorReport(exploding);
|
|
|
|
const written = captured.lines.join("");
|
|
// Either the safe report succeeded OR we hit the canned fallback. Both
|
|
// are acceptable — what matters is stdout stays valid JSON.
|
|
expect(() => JSON.parse(written.trim())).not.toThrow();
|
|
const parsed = JSON.parse(written.trim());
|
|
expect(parsed.ok).toBe(false);
|
|
expect(parsed.schemaVersion).toBe(3);
|
|
});
|
|
|
|
it("keeps the last-resort error payload valid against the v3 schema", () => {
|
|
const parsedFallback = JSON.parse(INTERNAL_ERROR_JSON_FALLBACK);
|
|
const decodedFallback = Schema.decodeUnknownSync(JsonReportSchema)(parsedFallback);
|
|
|
|
expect(decodedFallback.schemaVersion).toBe(3);
|
|
expect(decodedFallback.ok).toBe(false);
|
|
});
|
|
|
|
it("writeJsonReport writes to a file when outputFile is provided", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "json-mode-test-"));
|
|
const outputFile = path.join(tempDir, "report.json");
|
|
enableJsonMode({ compact: false, directory: "/tmp/foo", outputFile });
|
|
captured.lines.length = 0;
|
|
writeJsonReport(buildOkReport());
|
|
expect(captured.lines.length).toBe(0);
|
|
expect(fs.existsSync(outputFile)).toBe(true);
|
|
const fileContent = fs.readFileSync(outputFile, "utf8");
|
|
expect(() => JSON.parse(fileContent)).not.toThrow();
|
|
const parsed = JSON.parse(fileContent);
|
|
expect(parsed.ok).toBe(true);
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("writeJsonReport creates parent directories when outputFile is in a nested path", () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "json-mode-test-"));
|
|
const outputFile = path.join(tempDir, "nested", "deep", "report.json");
|
|
enableJsonMode({ compact: false, directory: "/tmp/foo", outputFile });
|
|
captured.lines.length = 0;
|
|
writeJsonReport(buildOkReport());
|
|
expect(captured.lines.length).toBe(0);
|
|
expect(fs.existsSync(outputFile)).toBe(true);
|
|
const fileContent = fs.readFileSync(outputFile, "utf8");
|
|
expect(() => JSON.parse(fileContent)).not.toThrow();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("rejects a directory passed as the output file", () => {
|
|
const outputDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "json-mode-output-directory-"));
|
|
|
|
expect(() =>
|
|
enableJsonMode({
|
|
compact: false,
|
|
directory: "/tmp/foo",
|
|
outputFile: outputDirectory,
|
|
}),
|
|
).toThrow("--json-out must point to a file");
|
|
|
|
fs.rmSync(outputDirectory, { recursive: true, force: true });
|
|
});
|
|
});
|