factorio-test-cli 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import logUpdate from "log-update";
3
+ import { ProgressRenderer } from "./progress-renderer.js";
4
+ vi.mock("log-update", () => ({
5
+ default: Object.assign(vi.fn(), { clear: vi.fn() }),
6
+ }));
7
+ describe("ProgressRenderer", () => {
8
+ beforeEach(() => {
9
+ vi.mocked(logUpdate).mockClear();
10
+ vi.mocked(logUpdate.clear).mockClear();
11
+ });
12
+ describe("withPermanentOutput", () => {
13
+ it("clears and re-renders around permanent output when active", () => {
14
+ const renderer = new ProgressRenderer(true);
15
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
16
+ renderer.handleEvent({ type: "testStarted", test: { path: "test" } });
17
+ vi.mocked(logUpdate).mockClear();
18
+ vi.mocked(logUpdate.clear).mockClear();
19
+ let callbackExecuted = false;
20
+ renderer.withPermanentOutput(() => {
21
+ callbackExecuted = true;
22
+ });
23
+ expect(logUpdate.clear).toHaveBeenCalledTimes(1);
24
+ expect(callbackExecuted).toBe(true);
25
+ expect(logUpdate).toHaveBeenCalledTimes(1);
26
+ });
27
+ it("executes callback without clear when not active", () => {
28
+ const renderer = new ProgressRenderer(true);
29
+ let callbackExecuted = false;
30
+ renderer.withPermanentOutput(() => {
31
+ callbackExecuted = true;
32
+ });
33
+ expect(callbackExecuted).toBe(true);
34
+ expect(logUpdate.clear).not.toHaveBeenCalled();
35
+ });
36
+ it("executes callback without clear when not TTY", () => {
37
+ const renderer = new ProgressRenderer(false);
38
+ let callbackExecuted = false;
39
+ renderer.withPermanentOutput(() => {
40
+ callbackExecuted = true;
41
+ });
42
+ expect(callbackExecuted).toBe(true);
43
+ expect(logUpdate.clear).not.toHaveBeenCalled();
44
+ expect(logUpdate).not.toHaveBeenCalled();
45
+ });
46
+ });
47
+ describe("render", () => {
48
+ it("renders nothing when not TTY", () => {
49
+ const renderer = new ProgressRenderer(false);
50
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
51
+ renderer.handleEvent({ type: "testStarted", test: { path: "test" } });
52
+ expect(logUpdate).not.toHaveBeenCalled();
53
+ });
54
+ it("renders progress bar when TTY via withPermanentOutput", () => {
55
+ const renderer = new ProgressRenderer(true);
56
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
57
+ renderer.handleEvent({ type: "testStarted", test: { path: "test" } });
58
+ vi.mocked(logUpdate).mockClear();
59
+ renderer.handleTestFinished({ path: "test", result: "passed", errors: [], logs: [] });
60
+ renderer.withPermanentOutput(() => { });
61
+ expect(logUpdate).toHaveBeenCalled();
62
+ const output = vi.mocked(logUpdate).mock.calls[0][0];
63
+ expect(output).toContain("10%");
64
+ expect(output).toContain("1/10");
65
+ });
66
+ it("includes current test when running", () => {
67
+ const renderer = new ProgressRenderer(true);
68
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
69
+ renderer.handleEvent({ type: "testStarted", test: { path: "describe > my test" } });
70
+ const output = vi.mocked(logUpdate).mock.calls[0][0];
71
+ expect(output).toContain("Running: describe > my test");
72
+ });
73
+ });
74
+ describe("finish", () => {
75
+ it("clears on finish when active", () => {
76
+ const renderer = new ProgressRenderer(true);
77
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
78
+ renderer.handleEvent({ type: "testStarted", test: { path: "test" } });
79
+ renderer.finish();
80
+ expect(logUpdate.clear).toHaveBeenCalled();
81
+ });
82
+ it("does not clear when not active", () => {
83
+ const renderer = new ProgressRenderer(true);
84
+ renderer.finish();
85
+ expect(logUpdate.clear).not.toHaveBeenCalled();
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,30 @@
1
+ import * as fsp from "fs/promises";
2
+ import * as path from "path";
3
+ export async function writeResultsFile(outputPath, modName, data) {
4
+ const content = {
5
+ timestamp: new Date().toISOString(),
6
+ modName,
7
+ summary: data.summary,
8
+ tests: data.tests.map((t) => ({
9
+ path: t.path,
10
+ result: t.result,
11
+ ...(t.durationMs !== undefined && { durationMs: t.durationMs }),
12
+ ...(t.errors.length > 0 && { errors: t.errors }),
13
+ })),
14
+ };
15
+ await fsp.mkdir(path.dirname(outputPath), { recursive: true });
16
+ await fsp.writeFile(outputPath, JSON.stringify(content, null, 2));
17
+ }
18
+ export async function readPreviousFailedTests(outputPath) {
19
+ try {
20
+ const content = await fsp.readFile(outputPath, "utf-8");
21
+ const parsed = JSON.parse(content);
22
+ return parsed.tests.filter((t) => t.result === "failed").map((t) => t.path);
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ export function getDefaultOutputPath(dataDir) {
29
+ return path.join(dataDir, "test-results.json");
30
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as fsp from "fs/promises";
3
+ import { writeResultsFile, readPreviousFailedTests, getDefaultOutputPath, } from "./results-writer.js";
4
+ vi.mock("fs/promises");
5
+ describe("writeResultsFile", () => {
6
+ beforeEach(() => {
7
+ vi.mocked(fsp.mkdir).mockResolvedValue(undefined);
8
+ vi.mocked(fsp.writeFile).mockResolvedValue();
9
+ });
10
+ afterEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+ it("writes results with correct structure", async () => {
14
+ const data = {
15
+ tests: [
16
+ { path: "test1", result: "passed", errors: [], logs: [], durationMs: 1 },
17
+ { path: "test2", result: "failed", errors: ["error"], logs: [], durationMs: 2 },
18
+ ],
19
+ summary: {
20
+ ran: 2,
21
+ passed: 1,
22
+ failed: 1,
23
+ skipped: 0,
24
+ todo: 0,
25
+ cancelled: 0,
26
+ describeBlockErrors: 0,
27
+ status: "failed",
28
+ },
29
+ };
30
+ await writeResultsFile("/out/results.json", "test-mod", data);
31
+ expect(fsp.mkdir).toHaveBeenCalledWith("/out", { recursive: true });
32
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
33
+ expect(written.modName).toBe("test-mod");
34
+ expect(written.tests).toHaveLength(2);
35
+ expect(written.tests[0].errors).toBeUndefined();
36
+ expect(written.tests[1].errors).toEqual(["error"]);
37
+ });
38
+ it("omits duration when not present", async () => {
39
+ const data = {
40
+ tests: [{ path: "test1", result: "skipped", errors: [], logs: [] }],
41
+ };
42
+ await writeResultsFile("/out/results.json", "test-mod", data);
43
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
44
+ expect(written.tests[0].durationMs).toBeUndefined();
45
+ });
46
+ it("omits errors array when empty", async () => {
47
+ const data = {
48
+ tests: [{ path: "test1", result: "passed", errors: [], logs: [], durationMs: 5 }],
49
+ };
50
+ await writeResultsFile("/out/results.json", "test-mod", data);
51
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
52
+ expect(written.tests[0].errors).toBeUndefined();
53
+ });
54
+ });
55
+ describe("readPreviousFailedTests", () => {
56
+ afterEach(() => {
57
+ vi.clearAllMocks();
58
+ });
59
+ it("returns failed test paths", async () => {
60
+ const content = {
61
+ timestamp: "2026-01-24T00:00:00Z",
62
+ modName: "test",
63
+ summary: undefined,
64
+ tests: [
65
+ { path: "passing", result: "passed" },
66
+ { path: "failing1", result: "failed" },
67
+ { path: "failing2", result: "failed" },
68
+ ],
69
+ };
70
+ vi.mocked(fsp.readFile).mockResolvedValue(JSON.stringify(content));
71
+ const result = await readPreviousFailedTests("/path/to/results.json");
72
+ expect(result).toEqual(["failing1", "failing2"]);
73
+ });
74
+ it("returns empty array on file not found", async () => {
75
+ vi.mocked(fsp.readFile).mockRejectedValue(new Error("ENOENT"));
76
+ const result = await readPreviousFailedTests("/path/to/results.json");
77
+ expect(result).toEqual([]);
78
+ });
79
+ it("returns empty array on invalid JSON", async () => {
80
+ vi.mocked(fsp.readFile).mockResolvedValue("not valid json");
81
+ const result = await readPreviousFailedTests("/path/to/results.json");
82
+ expect(result).toEqual([]);
83
+ });
84
+ });
85
+ describe("getDefaultOutputPath", () => {
86
+ it("returns path in data directory", () => {
87
+ expect(getDefaultOutputPath("/data/dir")).toBe("/data/dir/test-results.json");
88
+ });
89
+ });