factorio-test-cli 3.3.0 → 3.4.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.
@@ -118,7 +118,7 @@ function createOutputComponents(options) {
118
118
  collector.handleEvent(event);
119
119
  progress.handleEvent(event);
120
120
  if (options.verbose) {
121
- progress.withPermanentOutput(() => console.log(JSON.stringify(event)));
121
+ progress.withPermanentOutput(() => printer.printEvent(event));
122
122
  }
123
123
  });
124
124
  handler.on("log", (line) => {
@@ -132,6 +132,9 @@ function createOutputComponents(options) {
132
132
  progress.handleTestFinished(test);
133
133
  progress.withPermanentOutput(() => printer.printTestResult(test));
134
134
  });
135
+ collector.on("describeBlockFailed", (block) => {
136
+ progress.withPermanentOutput(() => printer.printTestResult(block));
137
+ });
135
138
  handler.on("result", () => {
136
139
  progress.finish();
137
140
  printer.resetMessage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factorio-test-cli",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "A CLI to run FactorioTest.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/test-output.js CHANGED
@@ -87,16 +87,19 @@ export class OutputFormatter {
87
87
  formatTestResult(test) {
88
88
  if (this.options.quiet)
89
89
  return;
90
- const showLogs = test.result === "failed" || this.options.showPassedLogs;
90
+ const prefix = this.getPrefix(test.result);
91
+ const duration = test.durationMs !== undefined ? ` (${formatDuration(test.durationMs)})` : "";
92
+ console.log(`${prefix} ${test.path}${duration}`);
93
+ const showLogs = this.options.showLogs !== false &&
94
+ (test.result === "failed" || test.result === "error" || this.options.showPassedLogs);
91
95
  if (showLogs && test.logs.length > 0) {
96
+ console.log("Log messages:");
92
97
  for (const log of test.logs) {
93
98
  console.log(" " + log);
94
99
  }
95
100
  }
96
- const prefix = this.getPrefix(test.result);
97
- const duration = test.durationMs !== undefined ? ` (${formatDuration(test.durationMs)})` : "";
98
- console.log(`${prefix} ${test.path}${duration}`);
99
- if (test.result === "failed") {
101
+ if ((test.result === "failed" || test.result === "error") && test.errors.length > 0) {
102
+ console.log("Errors:");
100
103
  for (const error of test.errors) {
101
104
  console.log(" " + error);
102
105
  }
@@ -107,6 +110,7 @@ export class OutputFormatter {
107
110
  return;
108
111
  if (!this.options.quiet) {
109
112
  this.printRecapSection(data.tests, "failed", "Failures:");
113
+ this.printRecapSection(data.tests, "error", "Describe block errors:");
110
114
  this.printRecapSection(data.tests, "todo", "Todo:");
111
115
  }
112
116
  this.printCountsLine(data.summary);
@@ -126,6 +130,8 @@ export class OutputFormatter {
126
130
  const segments = [];
127
131
  if (summary.failed > 0)
128
132
  segments.push(chalk.red(`${summary.failed} failed`));
133
+ if (summary.describeBlockErrors > 0)
134
+ segments.push(chalk.red(`${summary.describeBlockErrors} errors`));
129
135
  if (summary.todo > 0)
130
136
  segments.push(chalk.magenta(`${summary.todo} todo`));
131
137
  if (summary.skipped > 0)
@@ -134,12 +140,28 @@ export class OutputFormatter {
134
140
  const total = summary.passed + summary.failed + summary.skipped + summary.todo;
135
141
  console.log(`Tests: ${segments.join(", ")} (${total} total)`);
136
142
  }
143
+ formatEvent(event) {
144
+ switch (event.type) {
145
+ case "testRunStarted":
146
+ return chalk.dim(`Running ${event.total} tests...`);
147
+ case "testStarted":
148
+ return chalk.dim(`Starting: ${event.test.path}`);
149
+ case "loadError":
150
+ return chalk.red(`Load error: ${event.error}`);
151
+ case "testRunCancelled":
152
+ return chalk.yellow("Test run cancelled");
153
+ default:
154
+ return undefined;
155
+ }
156
+ }
137
157
  getPrefix(result) {
138
158
  switch (result) {
139
159
  case "passed":
140
160
  return chalk.green("PASS");
141
161
  case "failed":
142
162
  return chalk.red("FAIL");
163
+ case "error":
164
+ return chalk.red("ERROR");
143
165
  case "skipped":
144
166
  return chalk.yellow("SKIP");
145
167
  case "todo":
@@ -157,6 +179,7 @@ export class OutputPrinter {
157
179
  verbose: options.verbose,
158
180
  quiet: options.quiet,
159
181
  showPassedLogs: options.verbose,
182
+ showLogs: !options.verbose,
160
183
  });
161
184
  }
162
185
  printTestResult(test) {
@@ -180,6 +203,12 @@ export class OutputPrinter {
180
203
  resetMessage() {
181
204
  this.isMessageFirstLine = true;
182
205
  }
206
+ printEvent(event) {
207
+ const formatted = this.formatter.formatEvent(event);
208
+ if (formatted !== undefined) {
209
+ console.log(formatted);
210
+ }
211
+ }
183
212
  printVerbose(line) {
184
213
  if (this.options.verbose) {
185
214
  console.log(line);
@@ -150,12 +150,19 @@ describe("OutputFormatter", () => {
150
150
  expect(output.some((line) => line.includes("assertion failed"))).toBe(true);
151
151
  expect(output.some((line) => line.includes("at test.ts:10"))).toBe(true);
152
152
  });
153
- it("shows logs before failed test result", () => {
153
+ it("shows status line first, then logs and errors with headers", () => {
154
154
  const formatter = new OutputFormatter({});
155
155
  formatter.formatTestResult(failedTest);
156
- const logIndex = output.findIndex((line) => line.includes("debug output"));
157
156
  const failIndex = output.findIndex((line) => line.includes("FAIL"));
158
- expect(logIndex).toBeLessThan(failIndex);
157
+ const logHeaderIndex = output.findIndex((line) => line.includes("Log messages:"));
158
+ const logIndex = output.findIndex((line) => line.includes("debug output"));
159
+ const errorHeaderIndex = output.findIndex((line) => line.includes("Errors:"));
160
+ const errorIndex = output.findIndex((line) => line.includes("assertion failed"));
161
+ expect(failIndex).toBe(0);
162
+ expect(logHeaderIndex).toBeGreaterThan(failIndex);
163
+ expect(logIndex).toBeGreaterThan(logHeaderIndex);
164
+ expect(errorHeaderIndex).toBeGreaterThan(logIndex);
165
+ expect(errorIndex).toBeGreaterThan(errorHeaderIndex);
159
166
  });
160
167
  it("hides logs for passed tests by default", () => {
161
168
  const formatter = new OutputFormatter({});
@@ -199,6 +206,120 @@ describe("OutputFormatter", () => {
199
206
  expect(output[0]).toContain("TODO");
200
207
  expect(output[0]).toContain("todo test");
201
208
  });
209
+ it("formats describe block error with ERROR prefix", () => {
210
+ const formatter = new OutputFormatter({});
211
+ const errorTest = {
212
+ path: "root > block",
213
+ result: "error",
214
+ errors: ["Error running afterAll: Oh no"],
215
+ logs: ["hook log"],
216
+ };
217
+ formatter.formatTestResult(errorTest);
218
+ expect(output[0]).toContain("ERROR");
219
+ expect(output[0]).toContain("root > block");
220
+ expect(output.some((line) => line.includes("Log messages:"))).toBe(true);
221
+ expect(output.some((line) => line.includes("hook log"))).toBe(true);
222
+ expect(output.some((line) => line.includes("Errors:"))).toBe(true);
223
+ expect(output.some((line) => line.includes("Oh no"))).toBe(true);
224
+ });
225
+ });
226
+ describe("OutputFormatter.formatEvent", () => {
227
+ it("formats testRunStarted", () => {
228
+ const formatter = new OutputFormatter({});
229
+ const result = formatter.formatEvent({ type: "testRunStarted", total: 42 });
230
+ expect(result).toContain("Running 42 tests...");
231
+ });
232
+ it("formats testStarted", () => {
233
+ const formatter = new OutputFormatter({});
234
+ const result = formatter.formatEvent({ type: "testStarted", test: { path: "root > my test" } });
235
+ expect(result).toContain("Starting: root > my test");
236
+ });
237
+ it("formats loadError", () => {
238
+ const formatter = new OutputFormatter({});
239
+ const result = formatter.formatEvent({ type: "loadError", error: "syntax error in foo.lua" });
240
+ expect(result).toContain("Load error: syntax error in foo.lua");
241
+ });
242
+ it("formats testRunCancelled", () => {
243
+ const formatter = new OutputFormatter({});
244
+ const result = formatter.formatEvent({ type: "testRunCancelled" });
245
+ expect(result).toContain("Test run cancelled");
246
+ });
247
+ it("returns undefined for testPassed", () => {
248
+ const formatter = new OutputFormatter({});
249
+ const result = formatter.formatEvent({ type: "testPassed", test: { path: "test" } });
250
+ expect(result).toBeUndefined();
251
+ });
252
+ it("returns undefined for testFailed", () => {
253
+ const formatter = new OutputFormatter({});
254
+ const result = formatter.formatEvent({ type: "testFailed", test: { path: "test" }, errors: ["err"] });
255
+ expect(result).toBeUndefined();
256
+ });
257
+ it("returns undefined for testRunFinished", () => {
258
+ const formatter = new OutputFormatter({});
259
+ const result = formatter.formatEvent({
260
+ type: "testRunFinished",
261
+ results: {
262
+ ran: 1,
263
+ passed: 1,
264
+ failed: 0,
265
+ skipped: 0,
266
+ todo: 0,
267
+ cancelled: 0,
268
+ describeBlockErrors: 0,
269
+ status: "passed",
270
+ },
271
+ });
272
+ expect(result).toBeUndefined();
273
+ });
274
+ });
275
+ describe("OutputFormatter showLogs option", () => {
276
+ let consoleSpy;
277
+ let output;
278
+ beforeEach(() => {
279
+ output = [];
280
+ consoleSpy = vi.spyOn(console, "log").mockImplementation((...args) => {
281
+ output.push(args.join(" "));
282
+ });
283
+ });
284
+ afterEach(() => {
285
+ consoleSpy.mockRestore();
286
+ });
287
+ it("showLogs: false suppresses logs for failed tests", () => {
288
+ const formatter = new OutputFormatter({ showLogs: false });
289
+ const test = {
290
+ path: "test",
291
+ result: "failed",
292
+ errors: ["assertion failed"],
293
+ logs: ["debug output"],
294
+ };
295
+ formatter.formatTestResult(test);
296
+ expect(output.some((l) => l.includes("Log messages:"))).toBe(false);
297
+ expect(output.some((l) => l.includes("debug output"))).toBe(false);
298
+ });
299
+ it("showLogs: false still shows errors", () => {
300
+ const formatter = new OutputFormatter({ showLogs: false });
301
+ const test = {
302
+ path: "test",
303
+ result: "failed",
304
+ errors: ["assertion failed"],
305
+ logs: ["debug output"],
306
+ };
307
+ formatter.formatTestResult(test);
308
+ expect(output.some((l) => l.includes("Errors:"))).toBe(true);
309
+ expect(output.some((l) => l.includes("assertion failed"))).toBe(true);
310
+ });
311
+ it("showLogs: false suppresses logs for error results", () => {
312
+ const formatter = new OutputFormatter({ showLogs: false });
313
+ const test = {
314
+ path: "block",
315
+ result: "error",
316
+ errors: ["hook error"],
317
+ logs: ["hook log"],
318
+ };
319
+ formatter.formatTestResult(test);
320
+ expect(output.some((l) => l.includes("hook log"))).toBe(false);
321
+ expect(output.some((l) => l.includes("hook error"))).toBe(true);
322
+ });
202
323
  });
203
324
  describe("OutputFormatter.formatSummary", () => {
204
325
  let consoleSpy;
@@ -298,6 +419,37 @@ describe("OutputFormatter.formatSummary", () => {
298
419
  expect(countsLine).toContain("1 passed");
299
420
  expect(countsLine).toContain("(4 total)");
300
421
  });
422
+ it("shows describe block errors in separate section from failures", () => {
423
+ const formatter = new OutputFormatter({});
424
+ const data = {
425
+ tests: [
426
+ { path: "test", result: "failed", errors: ["test err"], logs: [] },
427
+ { path: "block", result: "error", errors: ["block err"], logs: [] },
428
+ ],
429
+ summary: makeSummary({ ran: 1, failed: 1, describeBlockErrors: 1, status: "failed" }),
430
+ };
431
+ formatter.formatSummary(data);
432
+ expect(output.some((l) => l.includes("Failures:"))).toBe(true);
433
+ expect(output.some((l) => l.includes("Describe block errors:"))).toBe(true);
434
+ const failuresIdx = output.findIndex((l) => l.includes("Failures:"));
435
+ const errorsIdx = output.findIndex((l) => l.includes("Describe block errors:"));
436
+ const failLine = output.findIndex((l) => l.includes("FAIL"));
437
+ const errorLine = output.findIndex((l) => l.includes("ERROR"));
438
+ expect(failLine).toBeGreaterThan(failuresIdx);
439
+ expect(failLine).toBeLessThan(errorsIdx);
440
+ expect(errorLine).toBeGreaterThan(errorsIdx);
441
+ });
442
+ it("includes describe block errors count in summary line", () => {
443
+ const formatter = new OutputFormatter({});
444
+ const data = {
445
+ tests: [{ path: "block", result: "error", errors: ["err"], logs: [] }],
446
+ summary: makeSummary({ passed: 1, describeBlockErrors: 2, status: "failed" }),
447
+ };
448
+ formatter.formatSummary(data);
449
+ const countsLine = output.find((l) => l.includes("Tests:"));
450
+ expect(countsLine).toContain("2 errors");
451
+ expect(countsLine).toContain("1 passed");
452
+ });
301
453
  it("does nothing when summary is undefined", () => {
302
454
  const formatter = new OutputFormatter({});
303
455
  formatter.formatSummary({ tests: [] });
@@ -349,6 +501,29 @@ describe("OutputPrinter", () => {
349
501
  expect(output.some((line) => line.includes("FAIL"))).toBe(true);
350
502
  expect(output.some((line) => line.includes("TODO"))).toBe(true);
351
503
  });
504
+ it("verbose mode: printEvent prints formatted events", () => {
505
+ const printer = new OutputPrinter({ verbose: true });
506
+ printer.printEvent({ type: "testStarted", test: { path: "root > test" } });
507
+ expect(output.some((l) => l.includes("Starting: root > test"))).toBe(true);
508
+ });
509
+ it("verbose mode: printEvent skips events that return undefined", () => {
510
+ const printer = new OutputPrinter({ verbose: true });
511
+ printer.printEvent({ type: "testPassed", test: { path: "test" } });
512
+ expect(output).toHaveLength(0);
513
+ });
514
+ it("verbose mode: printTestResult does not include logs", () => {
515
+ const printer = new OutputPrinter({ verbose: true });
516
+ const test = {
517
+ path: "test",
518
+ result: "failed",
519
+ errors: ["err"],
520
+ logs: ["debug log"],
521
+ };
522
+ printer.printTestResult(test);
523
+ expect(output.some((l) => l.includes("FAIL"))).toBe(true);
524
+ expect(output.some((l) => l.includes("debug log"))).toBe(false);
525
+ expect(output.some((l) => l.includes("err"))).toBe(true);
526
+ });
352
527
  it("hides all tests in quiet mode", () => {
353
528
  const printer = new OutputPrinter({ quiet: true });
354
529
  printer.printTestResult(passedTest);
package/test-results.js CHANGED
@@ -5,11 +5,13 @@ export class TestRunCollector extends EventEmitter {
5
5
  data = { tests: [] };
6
6
  currentTest;
7
7
  currentLogs = [];
8
+ pendingLogs = [];
8
9
  testStartTime;
9
10
  handleEvent(event) {
10
11
  switch (event.type) {
11
12
  case "testStarted":
12
13
  this.flushCurrentTest();
14
+ this.pendingLogs = [];
13
15
  this.testStartTime = performance.now();
14
16
  this.currentTest = {
15
17
  path: event.test.path,
@@ -61,6 +63,20 @@ export class TestRunCollector extends EventEmitter {
61
63
  logs: [],
62
64
  });
63
65
  break;
66
+ case "describeBlockFailed": {
67
+ this.flushCurrentTest();
68
+ const captured = {
69
+ path: event.block.path,
70
+ source: event.block.source,
71
+ result: "error",
72
+ errors: event.errors,
73
+ logs: [...this.pendingLogs],
74
+ };
75
+ this.data.tests.push(captured);
76
+ this.emit("describeBlockFailed", captured);
77
+ this.pendingLogs = [];
78
+ break;
79
+ }
64
80
  case "testRunFinished":
65
81
  this.flushCurrentTest();
66
82
  this.data.summary = event.results;
@@ -75,6 +91,9 @@ export class TestRunCollector extends EventEmitter {
75
91
  if (this.currentTest) {
76
92
  this.currentLogs.push(line);
77
93
  }
94
+ else {
95
+ this.pendingLogs.push(line);
96
+ }
78
97
  }
79
98
  getData() {
80
99
  return this.data;
@@ -38,12 +38,69 @@ describe("TestRunCollector", () => {
38
38
  const data = collector.getData();
39
39
  expect(data.tests[0].logs).toEqual(["log line 1", "log line 2"]);
40
40
  });
41
- it("does not capture logs when no test is running", () => {
41
+ it("does not attach pending logs to skipped tests", () => {
42
42
  collector.captureLog("orphan log");
43
43
  collector.handleEvent({ type: "testSkipped", test: { path: "test" } });
44
44
  const data = collector.getData();
45
45
  expect(data.tests[0].logs).toEqual([]);
46
46
  });
47
+ it("handles describeBlockFailed with errors and pending logs", () => {
48
+ collector.captureLog("hook output");
49
+ collector.handleEvent({
50
+ type: "describeBlockFailed",
51
+ block: { path: "root > block", source: { file: "test.ts", line: 5 } },
52
+ errors: ["Error running afterAll: Oh no"],
53
+ });
54
+ const data = collector.getData();
55
+ expect(data.tests).toHaveLength(1);
56
+ expect(data.tests[0]).toMatchObject({
57
+ path: "root > block",
58
+ result: "error",
59
+ errors: ["Error running afterAll: Oh no"],
60
+ logs: ["hook output"],
61
+ source: { file: "test.ts", line: 5 },
62
+ });
63
+ });
64
+ it("emits describeBlockFailed event", () => {
65
+ const handler = vi.fn();
66
+ collector.on("describeBlockFailed", handler);
67
+ collector.handleEvent({
68
+ type: "describeBlockFailed",
69
+ block: { path: "block" },
70
+ errors: ["err"],
71
+ });
72
+ expect(handler).toHaveBeenCalledOnce();
73
+ expect(handler.mock.calls[0][0].path).toBe("block");
74
+ });
75
+ it("clears pending logs when a new test starts", () => {
76
+ collector.captureLog("before test");
77
+ collector.handleEvent({ type: "testStarted", test: { path: "test" } });
78
+ collector.handleEvent({ type: "testPassed", test: { path: "test" } });
79
+ collector.handleEvent({
80
+ type: "describeBlockFailed",
81
+ block: { path: "block" },
82
+ errors: ["err"],
83
+ });
84
+ const data = collector.getData();
85
+ const block = data.tests.find((t) => t.path === "block");
86
+ expect(block.logs).toEqual([]);
87
+ });
88
+ it("captures logs after test finishes for describe block failure", () => {
89
+ collector.handleEvent({ type: "testStarted", test: { path: "test" } });
90
+ collector.captureLog("test log");
91
+ collector.handleEvent({ type: "testPassed", test: { path: "test" } });
92
+ collector.captureLog("after_all log");
93
+ collector.handleEvent({
94
+ type: "describeBlockFailed",
95
+ block: { path: "block" },
96
+ errors: ["hook error"],
97
+ });
98
+ const data = collector.getData();
99
+ expect(data.tests[0].logs).toEqual(["test log"]);
100
+ const block = data.tests[1];
101
+ expect(block.logs).toEqual(["after_all log"]);
102
+ expect(block.errors).toEqual(["hook error"]);
103
+ });
47
104
  it("handles testSkipped without prior testStarted", () => {
48
105
  collector.handleEvent({ type: "testSkipped", test: { path: "skipped test" } });
49
106
  const data = collector.getData();