factorio-test-cli 3.2.0 → 3.3.1

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.
@@ -102,6 +102,9 @@ export function parseResultMessage(message) {
102
102
  hasFocusedTests: hasFocused,
103
103
  };
104
104
  }
105
+ function factorioLogHint(dataDir) {
106
+ return `\nCheck Factorio log for details: ${path.join(dataDir, "factorio-current.log")}`;
107
+ }
105
108
  function createOutputComponents(options) {
106
109
  const handler = new FactorioOutputHandler();
107
110
  const collector = new TestRunCollector();
@@ -129,6 +132,9 @@ function createOutputComponents(options) {
129
132
  progress.handleTestFinished(test);
130
133
  progress.withPermanentOutput(() => printer.printTestResult(test));
131
134
  });
135
+ collector.on("describeBlockFailed", (block) => {
136
+ progress.withPermanentOutput(() => printer.printTestResult(block));
137
+ });
132
138
  handler.on("result", () => {
133
139
  progress.finish();
134
140
  printer.resetMessage();
@@ -206,16 +212,16 @@ export async function runFactorioTestsHeadless(factorioPath, dataDir, savePath,
206
212
  resolve();
207
213
  }
208
214
  else if (outputTimedOut) {
209
- reject(new CliError(`Factorio process stuck: no output received for ${outputTimeout} seconds`));
215
+ reject(new CliError(`Factorio process stuck: no output received for ${outputTimeout} seconds${factorioLogHint(dataDir)}`));
210
216
  }
211
217
  else if (startupTimedOut) {
212
- reject(new CliError("Factorio unresponsive: no test run started within 10 seconds"));
218
+ reject(new CliError(`Factorio unresponsive: no test run started within 10 seconds${factorioLogHint(dataDir)}`));
213
219
  }
214
220
  else if (handler.getResultMessage() !== undefined) {
215
221
  resolve();
216
222
  }
217
223
  else {
218
- reject(new CliError(`Factorio exited with code ${code}, signal ${signal}, no result received`));
224
+ reject(new CliError(`Factorio exited with code ${code}, signal ${signal}, no result received${factorioLogHint(dataDir)}`));
219
225
  }
220
226
  });
221
227
  });
@@ -253,7 +259,7 @@ export async function runFactorioTestsGraphics(factorioPath, dataDir, savePath,
253
259
  resolve();
254
260
  }
255
261
  else {
256
- reject(new CliError(`Factorio exited with code ${code}, signal ${signal}`));
262
+ reject(new CliError(`Factorio exited with code ${code}, signal ${signal}${factorioLogHint(dataDir)}`));
257
263
  }
258
264
  });
259
265
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factorio-test-cli",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "A CLI to run FactorioTest.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/test-output.js CHANGED
@@ -87,16 +87,18 @@ 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 = test.result === "failed" || test.result === "error" || this.options.showPassedLogs;
91
94
  if (showLogs && test.logs.length > 0) {
95
+ console.log("Log messages:");
92
96
  for (const log of test.logs) {
93
97
  console.log(" " + log);
94
98
  }
95
99
  }
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") {
100
+ if ((test.result === "failed" || test.result === "error") && test.errors.length > 0) {
101
+ console.log("Errors:");
100
102
  for (const error of test.errors) {
101
103
  console.log(" " + error);
102
104
  }
@@ -107,6 +109,7 @@ export class OutputFormatter {
107
109
  return;
108
110
  if (!this.options.quiet) {
109
111
  this.printRecapSection(data.tests, "failed", "Failures:");
112
+ this.printRecapSection(data.tests, "error", "Describe block errors:");
110
113
  this.printRecapSection(data.tests, "todo", "Todo:");
111
114
  }
112
115
  this.printCountsLine(data.summary);
@@ -126,6 +129,8 @@ export class OutputFormatter {
126
129
  const segments = [];
127
130
  if (summary.failed > 0)
128
131
  segments.push(chalk.red(`${summary.failed} failed`));
132
+ if (summary.describeBlockErrors > 0)
133
+ segments.push(chalk.red(`${summary.describeBlockErrors} errors`));
129
134
  if (summary.todo > 0)
130
135
  segments.push(chalk.magenta(`${summary.todo} todo`));
131
136
  if (summary.skipped > 0)
@@ -140,6 +145,8 @@ export class OutputFormatter {
140
145
  return chalk.green("PASS");
141
146
  case "failed":
142
147
  return chalk.red("FAIL");
148
+ case "error":
149
+ return chalk.red("ERROR");
143
150
  case "skipped":
144
151
  return chalk.yellow("SKIP");
145
152
  case "todo":
@@ -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,22 @@ 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
+ });
202
225
  });
203
226
  describe("OutputFormatter.formatSummary", () => {
204
227
  let consoleSpy;
@@ -298,6 +321,37 @@ describe("OutputFormatter.formatSummary", () => {
298
321
  expect(countsLine).toContain("1 passed");
299
322
  expect(countsLine).toContain("(4 total)");
300
323
  });
324
+ it("shows describe block errors in separate section from failures", () => {
325
+ const formatter = new OutputFormatter({});
326
+ const data = {
327
+ tests: [
328
+ { path: "test", result: "failed", errors: ["test err"], logs: [] },
329
+ { path: "block", result: "error", errors: ["block err"], logs: [] },
330
+ ],
331
+ summary: makeSummary({ ran: 1, failed: 1, describeBlockErrors: 1, status: "failed" }),
332
+ };
333
+ formatter.formatSummary(data);
334
+ expect(output.some((l) => l.includes("Failures:"))).toBe(true);
335
+ expect(output.some((l) => l.includes("Describe block errors:"))).toBe(true);
336
+ const failuresIdx = output.findIndex((l) => l.includes("Failures:"));
337
+ const errorsIdx = output.findIndex((l) => l.includes("Describe block errors:"));
338
+ const failLine = output.findIndex((l) => l.includes("FAIL"));
339
+ const errorLine = output.findIndex((l) => l.includes("ERROR"));
340
+ expect(failLine).toBeGreaterThan(failuresIdx);
341
+ expect(failLine).toBeLessThan(errorsIdx);
342
+ expect(errorLine).toBeGreaterThan(errorsIdx);
343
+ });
344
+ it("includes describe block errors count in summary line", () => {
345
+ const formatter = new OutputFormatter({});
346
+ const data = {
347
+ tests: [{ path: "block", result: "error", errors: ["err"], logs: [] }],
348
+ summary: makeSummary({ passed: 1, describeBlockErrors: 2, status: "failed" }),
349
+ };
350
+ formatter.formatSummary(data);
351
+ const countsLine = output.find((l) => l.includes("Tests:"));
352
+ expect(countsLine).toContain("2 errors");
353
+ expect(countsLine).toContain("1 passed");
354
+ });
301
355
  it("does nothing when summary is undefined", () => {
302
356
  const formatter = new OutputFormatter({});
303
357
  formatter.formatSummary({ tests: [] });
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();