factorio-test-cli 3.0.2 → 3.0.3

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,256 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import logUpdate from "log-update";
3
+ import { ProgressRenderer, OutputFormatter, OutputPrinter } from "./test-output.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
+ it("handles ran exceeding total without error", () => {
74
+ const renderer = new ProgressRenderer(true);
75
+ renderer.handleEvent({ type: "testRunStarted", total: 2 });
76
+ for (let i = 0; i < 5; i++) {
77
+ renderer.handleTestFinished({ path: `test${i}`, result: "passed", errors: [], logs: [] });
78
+ }
79
+ expect(() => {
80
+ renderer.handleEvent({ type: "testStarted", test: { path: "extra test" } });
81
+ }).not.toThrow();
82
+ });
83
+ it("does not count skipped or todo tests in ran", () => {
84
+ const renderer = new ProgressRenderer(true);
85
+ renderer.handleEvent({ type: "testRunStarted", total: 2 });
86
+ renderer.handleTestFinished({ path: "test1", result: "passed", errors: [], logs: [] });
87
+ renderer.handleTestFinished({ path: "skipped", result: "skipped", errors: [], logs: [] });
88
+ renderer.handleTestFinished({ path: "todo", result: "todo", errors: [], logs: [] });
89
+ renderer.handleTestFinished({ path: "test2", result: "failed", errors: [], logs: [] });
90
+ vi.mocked(logUpdate).mockClear();
91
+ renderer.handleEvent({ type: "testStarted", test: { path: "next" } });
92
+ const output = vi.mocked(logUpdate).mock.calls[0][0];
93
+ expect(output).toContain("2/2");
94
+ expect(output).toContain("100%");
95
+ });
96
+ });
97
+ describe("finish", () => {
98
+ it("clears on finish when active", () => {
99
+ const renderer = new ProgressRenderer(true);
100
+ renderer.handleEvent({ type: "testRunStarted", total: 10 });
101
+ renderer.handleEvent({ type: "testStarted", test: { path: "test" } });
102
+ renderer.finish();
103
+ expect(logUpdate.clear).toHaveBeenCalled();
104
+ });
105
+ it("does not clear when not active", () => {
106
+ const renderer = new ProgressRenderer(true);
107
+ renderer.finish();
108
+ expect(logUpdate.clear).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ });
112
+ describe("OutputFormatter", () => {
113
+ let consoleSpy;
114
+ let output;
115
+ beforeEach(() => {
116
+ output = [];
117
+ consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
118
+ output.push(args.join(" "));
119
+ });
120
+ });
121
+ afterEach(() => {
122
+ consoleSpy.mockRestore();
123
+ });
124
+ const passedTest = {
125
+ path: "root > test1",
126
+ result: "passed",
127
+ errors: [],
128
+ logs: [],
129
+ durationMs: 1.23,
130
+ };
131
+ const failedTest = {
132
+ path: "root > failing",
133
+ result: "failed",
134
+ errors: ["assertion failed", "at test.ts:10"],
135
+ logs: ["debug output"],
136
+ durationMs: 0.5,
137
+ };
138
+ it("formats passed test with duration", () => {
139
+ const formatter = new OutputFormatter({});
140
+ formatter.formatTestResult(passedTest);
141
+ expect(output).toHaveLength(1);
142
+ expect(output[0]).toContain("PASS");
143
+ expect(output[0]).toContain("root > test1");
144
+ expect(output[0]).toContain("1.2ms");
145
+ });
146
+ it("formats failed test with errors", () => {
147
+ const formatter = new OutputFormatter({});
148
+ formatter.formatTestResult(failedTest);
149
+ expect(output.some((line) => line.includes("FAIL"))).toBe(true);
150
+ expect(output.some((line) => line.includes("assertion failed"))).toBe(true);
151
+ expect(output.some((line) => line.includes("at test.ts:10"))).toBe(true);
152
+ });
153
+ it("shows logs before failed test result", () => {
154
+ const formatter = new OutputFormatter({});
155
+ formatter.formatTestResult(failedTest);
156
+ const logIndex = output.findIndex((line) => line.includes("debug output"));
157
+ const failIndex = output.findIndex((line) => line.includes("FAIL"));
158
+ expect(logIndex).toBeLessThan(failIndex);
159
+ });
160
+ it("hides logs for passed tests by default", () => {
161
+ const formatter = new OutputFormatter({});
162
+ const testWithLogs = { ...passedTest, logs: ["should not appear"] };
163
+ formatter.formatTestResult(testWithLogs);
164
+ expect(output.some((line) => line.includes("should not appear"))).toBe(false);
165
+ });
166
+ it("shows logs for passed tests when showPassedLogs is true", () => {
167
+ const formatter = new OutputFormatter({ showPassedLogs: true });
168
+ const testWithLogs = { ...passedTest, logs: ["visible log"] };
169
+ formatter.formatTestResult(testWithLogs);
170
+ expect(output.some((line) => line.includes("visible log"))).toBe(true);
171
+ });
172
+ it("suppresses all output when quiet is true", () => {
173
+ const formatter = new OutputFormatter({ quiet: true });
174
+ formatter.formatTestResult(passedTest);
175
+ formatter.formatTestResult(failedTest);
176
+ expect(output).toHaveLength(0);
177
+ });
178
+ it("formats skipped test", () => {
179
+ const formatter = new OutputFormatter({});
180
+ const skippedTest = {
181
+ path: "skipped test",
182
+ result: "skipped",
183
+ errors: [],
184
+ logs: [],
185
+ };
186
+ formatter.formatTestResult(skippedTest);
187
+ expect(output[0]).toContain("SKIP");
188
+ expect(output[0]).toContain("skipped test");
189
+ });
190
+ it("formats todo test", () => {
191
+ const formatter = new OutputFormatter({});
192
+ const todoTest = {
193
+ path: "todo test",
194
+ result: "todo",
195
+ errors: [],
196
+ logs: [],
197
+ };
198
+ formatter.formatTestResult(todoTest);
199
+ expect(output[0]).toContain("TODO");
200
+ expect(output[0]).toContain("todo test");
201
+ });
202
+ });
203
+ describe("OutputPrinter", () => {
204
+ let consoleSpy;
205
+ let output;
206
+ beforeEach(() => {
207
+ output = [];
208
+ consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
209
+ output.push(args.join(" "));
210
+ });
211
+ });
212
+ afterEach(() => {
213
+ consoleSpy.mockRestore();
214
+ });
215
+ const skippedTest = { path: "skipped", result: "skipped", errors: [], logs: [] };
216
+ const todoTest = { path: "todo", result: "todo", errors: [], logs: [] };
217
+ const passedTest = { path: "passed", result: "passed", errors: [], logs: [] };
218
+ const failedTest = { path: "failed", result: "failed", errors: ["err"], logs: [] };
219
+ it("hides skipped tests without verbose", () => {
220
+ const printer = new OutputPrinter({});
221
+ printer.printTestResult(skippedTest);
222
+ expect(output).toHaveLength(0);
223
+ });
224
+ it("shows todo tests without verbose", () => {
225
+ const printer = new OutputPrinter({});
226
+ printer.printTestResult(todoTest);
227
+ expect(output.some((line) => line.includes("TODO"))).toBe(true);
228
+ });
229
+ it("shows skipped tests with verbose", () => {
230
+ const printer = new OutputPrinter({ verbose: true });
231
+ printer.printTestResult(skippedTest);
232
+ expect(output.some((line) => line.includes("SKIP"))).toBe(true);
233
+ });
234
+ it("shows todo tests with verbose", () => {
235
+ const printer = new OutputPrinter({ verbose: true });
236
+ printer.printTestResult(todoTest);
237
+ expect(output.some((line) => line.includes("TODO"))).toBe(true);
238
+ });
239
+ it("shows passed, failed, and todo tests without verbose", () => {
240
+ const printer = new OutputPrinter({});
241
+ printer.printTestResult(passedTest);
242
+ printer.printTestResult(failedTest);
243
+ printer.printTestResult(todoTest);
244
+ expect(output.some((line) => line.includes("PASS"))).toBe(true);
245
+ expect(output.some((line) => line.includes("FAIL"))).toBe(true);
246
+ expect(output.some((line) => line.includes("TODO"))).toBe(true);
247
+ });
248
+ it("hides all tests in quiet mode", () => {
249
+ const printer = new OutputPrinter({ quiet: true });
250
+ printer.printTestResult(passedTest);
251
+ printer.printTestResult(failedTest);
252
+ printer.printTestResult(skippedTest);
253
+ printer.printTestResult(todoTest);
254
+ expect(output).toHaveLength(0);
255
+ });
256
+ });
@@ -1,4 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
+ import * as fsp from "fs/promises";
3
+ import * as path from "path";
2
4
  export class TestRunCollector extends EventEmitter {
3
5
  data = { tests: [] };
4
6
  currentTest;
@@ -90,3 +92,31 @@ export class TestRunCollector extends EventEmitter {
90
92
  this.emit("testFinished", test);
91
93
  }
92
94
  }
95
+ export async function writeResultsFile(outputPath, modName, data) {
96
+ const content = {
97
+ timestamp: new Date().toISOString(),
98
+ modName,
99
+ summary: data.summary,
100
+ tests: data.tests.map((t) => ({
101
+ path: t.path,
102
+ result: t.result,
103
+ ...(t.durationMs !== undefined && { durationMs: t.durationMs }),
104
+ ...(t.errors.length > 0 && { errors: t.errors }),
105
+ })),
106
+ };
107
+ await fsp.mkdir(path.dirname(outputPath), { recursive: true });
108
+ await fsp.writeFile(outputPath, JSON.stringify(content, null, 2));
109
+ }
110
+ export async function readPreviousFailedTests(outputPath) {
111
+ try {
112
+ const content = await fsp.readFile(outputPath, "utf-8");
113
+ const parsed = JSON.parse(content);
114
+ return parsed.tests.filter((t) => t.result === "failed").map((t) => t.path);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ export function getDefaultOutputPath(dataDir) {
121
+ return path.join(dataDir, "test-results.json");
122
+ }
@@ -1,5 +1,7 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { TestRunCollector } from "./test-run-collector.js";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as fsp from "fs/promises";
3
+ import { TestRunCollector, writeResultsFile, readPreviousFailedTests, getDefaultOutputPath, } from "./test-results.js";
4
+ vi.mock("fs/promises");
3
5
  describe("TestRunCollector", () => {
4
6
  let collector;
5
7
  beforeEach(() => {
@@ -99,3 +101,88 @@ describe("TestRunCollector", () => {
99
101
  expect(data.tests[0].source).toEqual({ file: "test.ts", line: 42 });
100
102
  });
101
103
  });
104
+ describe("writeResultsFile", () => {
105
+ beforeEach(() => {
106
+ vi.mocked(fsp.mkdir).mockResolvedValue(undefined);
107
+ vi.mocked(fsp.writeFile).mockResolvedValue();
108
+ });
109
+ afterEach(() => {
110
+ vi.clearAllMocks();
111
+ });
112
+ it("writes results with correct structure", async () => {
113
+ const data = {
114
+ tests: [
115
+ { path: "test1", result: "passed", errors: [], logs: [], durationMs: 1 },
116
+ { path: "test2", result: "failed", errors: ["error"], logs: [], durationMs: 2 },
117
+ ],
118
+ summary: {
119
+ ran: 2,
120
+ passed: 1,
121
+ failed: 1,
122
+ skipped: 0,
123
+ todo: 0,
124
+ cancelled: 0,
125
+ describeBlockErrors: 0,
126
+ status: "failed",
127
+ },
128
+ };
129
+ await writeResultsFile("/out/results.json", "test-mod", data);
130
+ expect(fsp.mkdir).toHaveBeenCalledWith("/out", { recursive: true });
131
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
132
+ expect(written.modName).toBe("test-mod");
133
+ expect(written.tests).toHaveLength(2);
134
+ expect(written.tests[0].errors).toBeUndefined();
135
+ expect(written.tests[1].errors).toEqual(["error"]);
136
+ });
137
+ it("omits duration when not present", async () => {
138
+ const data = {
139
+ tests: [{ path: "test1", result: "skipped", errors: [], logs: [] }],
140
+ };
141
+ await writeResultsFile("/out/results.json", "test-mod", data);
142
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
143
+ expect(written.tests[0].durationMs).toBeUndefined();
144
+ });
145
+ it("omits errors array when empty", async () => {
146
+ const data = {
147
+ tests: [{ path: "test1", result: "passed", errors: [], logs: [], durationMs: 5 }],
148
+ };
149
+ await writeResultsFile("/out/results.json", "test-mod", data);
150
+ const written = JSON.parse(vi.mocked(fsp.writeFile).mock.calls[0][1]);
151
+ expect(written.tests[0].errors).toBeUndefined();
152
+ });
153
+ });
154
+ describe("readPreviousFailedTests", () => {
155
+ afterEach(() => {
156
+ vi.clearAllMocks();
157
+ });
158
+ it("returns failed test paths", async () => {
159
+ const content = {
160
+ timestamp: "2026-01-24T00:00:00Z",
161
+ modName: "test",
162
+ summary: undefined,
163
+ tests: [
164
+ { path: "passing", result: "passed" },
165
+ { path: "failing1", result: "failed" },
166
+ { path: "failing2", result: "failed" },
167
+ ],
168
+ };
169
+ vi.mocked(fsp.readFile).mockResolvedValue(JSON.stringify(content));
170
+ const result = await readPreviousFailedTests("/path/to/results.json");
171
+ expect(result).toEqual(["failing1", "failing2"]);
172
+ });
173
+ it("returns empty array on file not found", async () => {
174
+ vi.mocked(fsp.readFile).mockRejectedValue(new Error("ENOENT"));
175
+ const result = await readPreviousFailedTests("/path/to/results.json");
176
+ expect(result).toEqual([]);
177
+ });
178
+ it("returns empty array on invalid JSON", async () => {
179
+ vi.mocked(fsp.readFile).mockResolvedValue("not valid json");
180
+ const result = await readPreviousFailedTests("/path/to/results.json");
181
+ expect(result).toEqual([]);
182
+ });
183
+ });
184
+ describe("getDefaultOutputPath", () => {
185
+ it("returns path in data directory", () => {
186
+ expect(getDefaultOutputPath("/data/dir")).toBe("/data/dir/test-results.json");
187
+ });
188
+ });
@@ -1,31 +0,0 @@
1
- import { EventEmitter } from "events";
2
- export default class BufferLineSplitter extends EventEmitter {
3
- buf;
4
- constructor(instream) {
5
- super();
6
- this.buf = "";
7
- instream.on("close", () => {
8
- if (this.buf.length > 0)
9
- this.emit("line", this.buf);
10
- this.emit("close");
11
- });
12
- instream.on("end", () => {
13
- if (this.buf.length > 0)
14
- this.emit("line", this.buf);
15
- this.emit("end");
16
- });
17
- instream.on("data", (chunk) => {
18
- this.buf += chunk.toString();
19
- while (this.buf.length > 0) {
20
- const index = this.buf.search(/\r?\n/);
21
- if (index === -1)
22
- break;
23
- this.emit("line", this.buf.slice(0, index));
24
- this.buf = this.buf.slice(index + 1);
25
- }
26
- });
27
- }
28
- on(event, listener) {
29
- return super.on(event, listener);
30
- }
31
- }
@@ -1,53 +0,0 @@
1
- import * as os from "os";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import { spawnSync } from "child_process";
5
- import { CliError } from "./cli-error.js";
6
- export function getFactorioPlayerDataPath() {
7
- const platform = os.platform();
8
- if (platform === "win32") {
9
- return path.join(process.env.APPDATA, "Factorio", "player-data.json");
10
- }
11
- if (platform === "darwin") {
12
- return path.join(os.homedir(), "Library", "Application Support", "factorio", "player-data.json");
13
- }
14
- return path.join(os.homedir(), ".factorio", "player-data.json");
15
- }
16
- function factorioIsInPath() {
17
- const result = spawnSync("factorio", ["--version"], { stdio: "ignore" });
18
- return result.status === 0;
19
- }
20
- export function autoDetectFactorioPath() {
21
- if (factorioIsInPath()) {
22
- return "factorio";
23
- }
24
- let pathsToTry;
25
- if (os.platform() === "linux" || os.platform() === "darwin") {
26
- pathsToTry = [
27
- "~/.local/share/Steam/steamapps/common/Factorio/bin/x64/factorio",
28
- "~/Library/Application Support/Steam/steamapps/common/Factorio/factorio.app/Contents/MacOS/factorio",
29
- "~/.factorio/bin/x64/factorio",
30
- "/Applications/factorio.app/Contents/MacOS/factorio",
31
- "/usr/share/factorio/bin/x64/factorio",
32
- "/usr/share/games/factorio/bin/x64/factorio",
33
- ];
34
- }
35
- else if (os.platform() === "win32") {
36
- pathsToTry = [
37
- "factorio.exe",
38
- process.env["ProgramFiles(x86)"] + "\\Steam\\steamapps\\common\\Factorio\\bin\\x64\\factorio.exe",
39
- process.env["ProgramFiles"] + "\\Factorio\\bin\\x64\\factorio.exe",
40
- ];
41
- }
42
- else {
43
- throw new CliError(`Cannot auto-detect factorio path on platform ${os.platform()}`);
44
- }
45
- pathsToTry = pathsToTry.map((p) => p.replace(/^~\//, os.homedir() + "/"));
46
- for (const testPath of pathsToTry) {
47
- if (fs.statSync(testPath, { throwIfNoEntry: false })?.isFile()) {
48
- return path.resolve(testPath);
49
- }
50
- }
51
- throw new CliError(`Could not auto-detect factorio executable. Tried: ${pathsToTry.join(", ")}. ` +
52
- "Either add the factorio bin to your path, or specify the path with --factorio-path");
53
- }
@@ -1,24 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- vi.mock("child_process", () => ({
3
- spawnSync: vi.fn(() => ({ status: 1 })),
4
- }));
5
- describe("autoDetectFactorioPath", () => {
6
- beforeEach(() => {
7
- vi.resetModules();
8
- });
9
- afterEach(() => {
10
- vi.restoreAllMocks();
11
- });
12
- it("returns 'factorio' if in PATH", async () => {
13
- const { spawnSync } = await import("child_process");
14
- vi.mocked(spawnSync).mockReturnValue({ status: 0 });
15
- const { autoDetectFactorioPath } = await import("./factorio-discovery.js");
16
- expect(autoDetectFactorioPath()).toBe("factorio");
17
- });
18
- it("throws if no path found and factorio not in PATH", async () => {
19
- const { spawnSync } = await import("child_process");
20
- vi.mocked(spawnSync).mockReturnValue({ status: 1 });
21
- const { autoDetectFactorioPath } = await import("./factorio-discovery.js");
22
- expect(() => autoDetectFactorioPath()).toThrow(/Could not auto-detect/);
23
- });
24
- });
@@ -1,137 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { OutputFormatter, OutputPrinter } from "./output-formatter.js";
3
- describe("OutputFormatter", () => {
4
- let consoleSpy;
5
- let output;
6
- beforeEach(() => {
7
- output = [];
8
- consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
9
- output.push(args.join(" "));
10
- });
11
- });
12
- afterEach(() => {
13
- consoleSpy.mockRestore();
14
- });
15
- const passedTest = {
16
- path: "root > test1",
17
- result: "passed",
18
- errors: [],
19
- logs: [],
20
- durationMs: 1.23,
21
- };
22
- const failedTest = {
23
- path: "root > failing",
24
- result: "failed",
25
- errors: ["assertion failed", "at test.ts:10"],
26
- logs: ["debug output"],
27
- durationMs: 0.5,
28
- };
29
- it("formats passed test with duration", () => {
30
- const formatter = new OutputFormatter({});
31
- formatter.formatTestResult(passedTest);
32
- expect(output).toHaveLength(1);
33
- expect(output[0]).toContain("PASS");
34
- expect(output[0]).toContain("root > test1");
35
- expect(output[0]).toContain("1.2ms");
36
- });
37
- it("formats failed test with errors", () => {
38
- const formatter = new OutputFormatter({});
39
- formatter.formatTestResult(failedTest);
40
- expect(output.some((line) => line.includes("FAIL"))).toBe(true);
41
- expect(output.some((line) => line.includes("assertion failed"))).toBe(true);
42
- expect(output.some((line) => line.includes("at test.ts:10"))).toBe(true);
43
- });
44
- it("shows logs before failed test result", () => {
45
- const formatter = new OutputFormatter({});
46
- formatter.formatTestResult(failedTest);
47
- const logIndex = output.findIndex((line) => line.includes("debug output"));
48
- const failIndex = output.findIndex((line) => line.includes("FAIL"));
49
- expect(logIndex).toBeLessThan(failIndex);
50
- });
51
- it("hides logs for passed tests by default", () => {
52
- const formatter = new OutputFormatter({});
53
- const testWithLogs = { ...passedTest, logs: ["should not appear"] };
54
- formatter.formatTestResult(testWithLogs);
55
- expect(output.some((line) => line.includes("should not appear"))).toBe(false);
56
- });
57
- it("shows logs for passed tests when showPassedLogs is true", () => {
58
- const formatter = new OutputFormatter({ showPassedLogs: true });
59
- const testWithLogs = { ...passedTest, logs: ["visible log"] };
60
- formatter.formatTestResult(testWithLogs);
61
- expect(output.some((line) => line.includes("visible log"))).toBe(true);
62
- });
63
- it("suppresses all output when quiet is true", () => {
64
- const formatter = new OutputFormatter({ quiet: true });
65
- formatter.formatTestResult(passedTest);
66
- formatter.formatTestResult(failedTest);
67
- expect(output).toHaveLength(0);
68
- });
69
- it("formats skipped test", () => {
70
- const formatter = new OutputFormatter({});
71
- const skippedTest = {
72
- path: "skipped test",
73
- result: "skipped",
74
- errors: [],
75
- logs: [],
76
- };
77
- formatter.formatTestResult(skippedTest);
78
- expect(output[0]).toContain("SKIP");
79
- expect(output[0]).toContain("skipped test");
80
- });
81
- it("formats todo test", () => {
82
- const formatter = new OutputFormatter({});
83
- const todoTest = {
84
- path: "todo test",
85
- result: "todo",
86
- errors: [],
87
- logs: [],
88
- };
89
- formatter.formatTestResult(todoTest);
90
- expect(output[0]).toContain("TODO");
91
- expect(output[0]).toContain("todo test");
92
- });
93
- });
94
- describe("OutputPrinter", () => {
95
- let consoleSpy;
96
- let output;
97
- beforeEach(() => {
98
- output = [];
99
- consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
100
- output.push(args.join(" "));
101
- });
102
- });
103
- afterEach(() => {
104
- consoleSpy.mockRestore();
105
- });
106
- const skippedTest = { path: "skipped", result: "skipped", errors: [], logs: [] };
107
- const todoTest = { path: "todo", result: "todo", errors: [], logs: [] };
108
- const passedTest = { path: "passed", result: "passed", errors: [], logs: [] };
109
- const failedTest = { path: "failed", result: "failed", errors: ["err"], logs: [] };
110
- it("hides skipped tests without verbose", () => {
111
- const printer = new OutputPrinter({});
112
- printer.printTestResult(skippedTest);
113
- expect(output).toHaveLength(0);
114
- });
115
- it("hides todo tests without verbose", () => {
116
- const printer = new OutputPrinter({});
117
- printer.printTestResult(todoTest);
118
- expect(output).toHaveLength(0);
119
- });
120
- it("shows skipped tests with verbose", () => {
121
- const printer = new OutputPrinter({ verbose: true });
122
- printer.printTestResult(skippedTest);
123
- expect(output.some((line) => line.includes("SKIP"))).toBe(true);
124
- });
125
- it("shows todo tests with verbose", () => {
126
- const printer = new OutputPrinter({ verbose: true });
127
- printer.printTestResult(todoTest);
128
- expect(output.some((line) => line.includes("TODO"))).toBe(true);
129
- });
130
- it("shows passed and failed tests without verbose", () => {
131
- const printer = new OutputPrinter({});
132
- printer.printTestResult(passedTest);
133
- printer.printTestResult(failedTest);
134
- expect(output.some((line) => line.includes("PASS"))).toBe(true);
135
- expect(output.some((line) => line.includes("FAIL"))).toBe(true);
136
- });
137
- });