@jsenv/test 3.2.1 → 3.3.2

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,121 @@
1
+ import { assertAndNormalizeFileUrl, writeFileSync } from "@jsenv/filesystem";
2
+ import { urlToFileSystemPath } from "@jsenv/urls";
3
+
4
+ import { replaceUrls } from "./replace_urls.js";
5
+
6
+ export const reportAsJson = (
7
+ testPlanResult,
8
+ fileUrl,
9
+ { mockFluctuatingValues } = {},
10
+ ) => {
11
+ fileUrl = assertAndNormalizeFileUrl(fileUrl);
12
+
13
+ if (!mockFluctuatingValues) {
14
+ writeFileSync(fileUrl, JSON.stringify(testPlanResult, null, " "));
15
+ return;
16
+ }
17
+ const testPlanResultCopy = deepCopy(testPlanResult, {});
18
+ testPlanResultCopy.os.name = "<mock>";
19
+ testPlanResultCopy.os.version = "<mock>";
20
+ testPlanResultCopy.os.availableCpu = "<mock>";
21
+ testPlanResultCopy.os.availableMemory = "<mock>";
22
+ testPlanResultCopy.process.version = "<mock>";
23
+ testPlanResultCopy.memoryUsage = "<mock>";
24
+ testPlanResultCopy.cpuUsage = "<mock>";
25
+ testPlanResultCopy.rootDirectoryUrl = "/mock/";
26
+ testPlanResultCopy.timings = "<mock>";
27
+ for (const group of Object.keys(testPlanResultCopy.groups)) {
28
+ testPlanResultCopy.groups[group].runtimeVersion = "<mock>";
29
+ }
30
+
31
+ for (const relativeUrl of Object.keys(testPlanResultCopy.results)) {
32
+ const fileResults = testPlanResultCopy.results[relativeUrl];
33
+ for (const groupName of Object.keys(fileResults)) {
34
+ const executionResult = fileResults[groupName];
35
+ const { consoleCalls } = executionResult;
36
+ if (consoleCalls) {
37
+ for (const consoleCall of consoleCalls) {
38
+ consoleCall.text = mockFluctuatingValuesInMessage(consoleCall.text, {
39
+ rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
40
+ });
41
+ consoleCall.text = consoleCall.text.replace(/\r\n/g, "\n");
42
+ }
43
+ }
44
+ const { errors } = executionResult;
45
+ if (errors) {
46
+ for (const error of errors) {
47
+ if (error.message) {
48
+ error.message = mockFluctuatingValuesInMessage(error.message, {
49
+ rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
50
+ });
51
+ }
52
+ if (error.stack) {
53
+ error.stack = mockFluctuatingValuesInMessage(error.stack, {
54
+ rootDirectoryUrl: testPlanResult.rootDirectoryUrl,
55
+ });
56
+ }
57
+ }
58
+ }
59
+ if (executionResult.timings) {
60
+ executionResult.timings = "<mock>";
61
+ }
62
+ if (Object.hasOwn(executionResult, "memoryUsage")) {
63
+ executionResult.memoryUsage = "<mock>";
64
+ }
65
+ if (executionResult.performance) {
66
+ executionResult.performance = "<mock>";
67
+ }
68
+ if (executionResult.coverageFileUrl) {
69
+ executionResult.coverageFileUrl = "<mock>";
70
+ }
71
+ }
72
+ }
73
+ writeFileSync(fileUrl, JSON.stringify(testPlanResultCopy, null, " "));
74
+ };
75
+
76
+ const mockFluctuatingValuesInMessage = (message, { rootDirectoryUrl }) => {
77
+ return replaceUrls(message, ({ url, line, column }) => {
78
+ let urlAsPath = String(url).startsWith("file:")
79
+ ? urlToFileSystemPath(url)
80
+ : url;
81
+ const rootDirectoryPath = urlToFileSystemPath(rootDirectoryUrl);
82
+ urlAsPath = urlAsPath.replace(rootDirectoryPath, "/mock/");
83
+ if (process.platform === "win32") {
84
+ urlAsPath = urlAsPath.replace(/\\/g, "/");
85
+ }
86
+ if (typeof line === "number" && typeof column === "number") {
87
+ return `${urlAsPath}:${line}:${column}`;
88
+ }
89
+ if (typeof line === "number") {
90
+ return `${urlAsPath}:${line}`;
91
+ }
92
+ return urlAsPath;
93
+ });
94
+ };
95
+
96
+ const deepCopy = (from, into) => {
97
+ const copyValue = (value) => {
98
+ if (value === null) {
99
+ return null;
100
+ }
101
+ if (typeof value === "object") {
102
+ if (Array.isArray(value)) {
103
+ return deepCopy(value, []);
104
+ }
105
+ return deepCopy(value, {});
106
+ }
107
+ return value;
108
+ };
109
+
110
+ if (Array.isArray(from)) {
111
+ let i = 0;
112
+ while (i < from.length) {
113
+ into[i] = copyValue(from[i]);
114
+ i++;
115
+ }
116
+ }
117
+ for (const key of Object.keys(from)) {
118
+ into[key] = copyValue(from[key]);
119
+ }
120
+ return into;
121
+ };
@@ -86,9 +86,15 @@ export const reporterList = ({
86
86
  Object.keys(testPlanResult.results).length === 0
87
87
  ) {
88
88
  return {
89
- afterEachInOrder: (execution) => {
90
- const log = renderExecutionLog(execution, logOptions);
91
- write(log);
89
+ afterEachInOrder: (execution, testPlanResult, testPlanHelpers) => {
90
+ const log = renderExecutionLog(
91
+ execution,
92
+ logOptions,
93
+ testPlanHelpers,
94
+ );
95
+ if (log) {
96
+ write(log);
97
+ }
92
98
  },
93
99
  afterAll: async () => {
94
100
  await write(renderOutro(testPlanResult, logOptions));
@@ -156,12 +162,18 @@ export const reporterList = ({
156
162
  console.warn(warning.message);
157
163
  });
158
164
  },
159
- afterEachInOrder: (execution) => {
165
+ afterEachInOrder: (execution, testPlanResult, testPlanHelpers) => {
160
166
  oneExecutionWritten = true;
161
167
  dynamicLog.clearDuringFunctionCall(
162
168
  () => {
163
- const log = renderExecutionLog(execution, logOptions);
164
- write(log);
169
+ const log = renderExecutionLog(
170
+ execution,
171
+ logOptions,
172
+ testPlanHelpers,
173
+ );
174
+ if (log) {
175
+ write(log);
176
+ }
165
177
  },
166
178
  // regenerate the dynamic log to put the leading "\n"
167
179
  // because of oneExecutionWritten becoming true
@@ -218,11 +230,11 @@ const renderIntro = (testPlanResult, logOptions) => {
218
230
 
219
231
  let title;
220
232
  if (planified === 0) {
221
- title = `no file to execute`;
233
+ title = `nothing to execute`;
222
234
  } else if (planified === 1) {
223
- title = `1 execution planified`;
235
+ title = `1 execution ready`;
224
236
  } else {
225
- title = `${planified} executions planified`;
237
+ title = `${planified} executions ready`;
226
238
  }
227
239
  const lines = [];
228
240
  lines.push(`directory: ${directory}`);
@@ -244,6 +256,11 @@ const renderIntro = (testPlanResult, logOptions) => {
244
256
  testPlanLog += "}";
245
257
  lines.push(`testPlan: ${testPlanLog}`);
246
258
  }
259
+ if (testPlanResult.fragment) {
260
+ lines.push(
261
+ `fragment: ${testPlanResult.fragment}, executing only ${testPlanResult.fragmentStart}:${testPlanResult.fragmentEnd}`,
262
+ );
263
+ }
247
264
  if (logOptions.platformInfo) {
248
265
  os_line: {
249
266
  let osLine = `os: `;
@@ -280,7 +297,36 @@ const renderIntro = (testPlanResult, logOptions) => {
280
297
  })}\n`;
281
298
  };
282
299
 
283
- const renderExecutionLog = (execution, logOptions) => {
300
+ const renderExecutionLog = (
301
+ execution,
302
+ logOptions,
303
+ { getPreviousExecution, getNextExecution },
304
+ ) => {
305
+ if (execution.skipped) {
306
+ const skipReason = execution.skipReason;
307
+ const prev = getPreviousExecution(execution);
308
+ if (prev && prev.skipped && prev.skipReason === skipReason) {
309
+ return "";
310
+ }
311
+ let nextExecutionSkippedWithSameReason = 0;
312
+ let next = getNextExecution(execution);
313
+ while (next) {
314
+ if (!next.skipped || next.skipReason !== skipReason) {
315
+ break;
316
+ }
317
+ nextExecutionSkippedWithSameReason++;
318
+ next = getNextExecution(next);
319
+ }
320
+ if (nextExecutionSkippedWithSameReason) {
321
+ const skippedGroupLog = descriptionFormatters.skippedGroup({
322
+ from: execution.index,
323
+ to: execution.index + nextExecutionSkippedWithSameReason,
324
+ skipReason,
325
+ });
326
+ return `${skippedGroupLog}\n`;
327
+ }
328
+ }
329
+
284
330
  let log = "";
285
331
  // label
286
332
  {
@@ -361,15 +407,38 @@ const descriptionFormatters = {
361
407
  executing: ({ fileRelativeUrl }) => {
362
408
  return ANSI.color(`${fileRelativeUrl}`, COLOR_EXECUTING);
363
409
  },
410
+ skippedGroup: ({ from, to, skipReason }) => {
411
+ let description = `${UNICODE.CIRCLE_DOTTED_RAW} ${from + 1}:${to + 1} skipped`;
412
+ if (skipReason) {
413
+ description += ` (${skipReason})`;
414
+ }
415
+ return ANSI.color(description, COLOR_SKIPPED);
416
+ },
417
+ skipped: ({ index, countersInOrder, fileRelativeUrl, skipReason }) => {
418
+ const total = countersInOrder.planified;
419
+ const number = fillLeft(index + 1, total, "0");
420
+ let description = `${UNICODE.CIRCLE_DOTTED_RAW} ${number}/${total} skipped ${fileRelativeUrl}`;
421
+ if (skipReason) {
422
+ description += ` (${skipReason})`;
423
+ }
424
+ return ANSI.color(description, COLOR_SKIPPED);
425
+ },
364
426
  aborted: ({ index, countersInOrder, fileRelativeUrl }) => {
365
427
  const total = countersInOrder.planified;
366
428
  const number = fillLeft(index + 1, total, "0");
367
-
368
429
  return ANSI.color(
369
430
  `${UNICODE.FAILURE_RAW} ${number}/${total} ${fileRelativeUrl}`,
370
431
  COLOR_ABORTED,
371
432
  );
372
433
  },
434
+ cancelled: ({ index, countersInOrder, fileRelativeUrl }) => {
435
+ const total = countersInOrder.planified;
436
+ const number = fillLeft(index + 1, total, "0");
437
+ return ANSI.color(
438
+ `${UNICODE.FAILURE_RAW} ${number}/${total} ${fileRelativeUrl}`,
439
+ COLOR_CANCELLED,
440
+ );
441
+ },
373
442
  timedout: ({ index, countersInOrder, fileRelativeUrl, params }) => {
374
443
  const total = countersInOrder.planified;
375
444
  const number = fillLeft(index + 1, total, "0");
@@ -397,23 +466,15 @@ const descriptionFormatters = {
397
466
  COLOR_COMPLETED,
398
467
  );
399
468
  },
400
- cancelled: ({ index, countersInOrder, fileRelativeUrl }) => {
401
- const total = countersInOrder.planified;
402
- const number = fillLeft(index + 1, total, "0");
403
-
404
- return ANSI.color(
405
- `${UNICODE.FAILURE_RAW} ${number}/${total} ${fileRelativeUrl}`,
406
- COLOR_CANCELLED,
407
- );
408
- },
409
469
  };
410
470
 
411
471
  const COLOR_EXECUTING = ANSI.BLUE;
472
+ const COLOR_SKIPPED = ANSI.GREY;
412
473
  const COLOR_ABORTED = ANSI.MAGENTA;
474
+ const COLOR_CANCELLED = ANSI.GREY;
413
475
  const COLOR_TIMEOUT = ANSI.MAGENTA;
414
476
  const COLOR_FAILED = ANSI.RED;
415
477
  const COLOR_COMPLETED = ANSI.GREEN;
416
- const COLOR_CANCELLED = ANSI.GREY;
417
478
  const fillLeft = (value, biggestValue, char = " ") => {
418
479
  const width = String(value).length;
419
480
  const biggestWidth = String(biggestValue).length;
@@ -691,9 +752,15 @@ const renderStatusRepartition = (counters, { showProgression } = {}) => {
691
752
  if (counters.planified === 0) {
692
753
  return ``;
693
754
  }
755
+ if (counters.skipped === counters.planified) {
756
+ return `all ${ANSI.color(`skipped`, COLOR_SKIPPED)}`;
757
+ }
694
758
  if (counters.aborted === counters.planified) {
695
759
  return `all ${ANSI.color(`aborted`, COLOR_ABORTED)}`;
696
760
  }
761
+ if (counters.cancelled === counters.planified) {
762
+ return `all ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`;
763
+ }
697
764
  if (counters.timedout === counters.planified) {
698
765
  return `all ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`;
699
766
  }
@@ -703,10 +770,15 @@ const renderStatusRepartition = (counters, { showProgression } = {}) => {
703
770
  if (counters.completed === counters.planified) {
704
771
  return `all ${ANSI.color(`completed`, COLOR_COMPLETED)}`;
705
772
  }
706
- if (counters.cancelled === counters.planified) {
707
- return `all ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`;
708
- }
709
773
  const parts = [];
774
+ if (showProgression) {
775
+ if (counters.executing) {
776
+ parts.push(`${counters.executing} executing`);
777
+ }
778
+ if (counters.waiting) {
779
+ parts.push(`${counters.waiting} waiting`);
780
+ }
781
+ }
710
782
  if (counters.timedout) {
711
783
  parts.push(
712
784
  `${counters.timedout} ${ANSI.color(`timed out`, COLOR_TIMEOUT)}`,
@@ -728,13 +800,8 @@ const renderStatusRepartition = (counters, { showProgression } = {}) => {
728
800
  `${counters.cancelled} ${ANSI.color(`cancelled`, COLOR_CANCELLED)}`,
729
801
  );
730
802
  }
731
- if (showProgression) {
732
- if (counters.executing) {
733
- parts.push(`${counters.executing} executing`);
734
- }
735
- if (counters.waiting) {
736
- parts.push(`${counters.waiting} waiting`);
737
- }
803
+ if (counters.skipped) {
804
+ parts.push(`${counters.skipped} ${ANSI.color(`skipped`, COLOR_SKIPPED)}`);
738
805
  }
739
806
  return `${parts.join(", ")}`;
740
807
  };
package/src/main.js CHANGED
@@ -14,6 +14,7 @@ export { reportCoverageAsHtml } from "./coverage/report_coverage_as_html.js";
14
14
  export { reporterList } from "./execution/reporters/reporter_list.js";
15
15
  // other
16
16
  export { reportAsJunitXml } from "./execution/junit_xml_file/report_as_junit_xml.js";
17
+ export { reportAsJson } from "./execution/report_as_json.js";
17
18
  // advanced
18
19
  export { execute } from "./execution/execute.js";
19
20
 
@@ -19,7 +19,7 @@ const createFirefoxRuntime = ({
19
19
  if (disableOnWindowsBecauseFlaky === undefined) {
20
20
  // https://github.com/microsoft/playwright/issues/1396
21
21
  console.warn(
22
- `Windows + firefox detected: executions on firefox will be ignored (firefox is flaky on windows).
22
+ `Windows + firefox detected: executions on firefox will be ignored (firefox is flaky on windows).
23
23
  To disable this warning, use disableOnWindowsBecauseFlaky: true
24
24
  To ignore potential flakyness, use disableOnWindowsBecauseFlaky: false`,
25
25
  );
@@ -28,6 +28,7 @@ To ignore potential flakyness, use disableOnWindowsBecauseFlaky: false`,
28
28
  if (disableOnWindowsBecauseFlaky) {
29
29
  return {
30
30
  disabled: true,
31
+ disabledReason: "flaky on windows",
31
32
  };
32
33
  }
33
34
  }
@@ -1,8 +1,7 @@
1
1
  import { fork } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
- import supportsColor from "supports-color";
4
3
  import { Abort, raceCallbacks } from "@jsenv/abort";
5
- import { createDetailedMessage } from "@jsenv/humanize";
4
+ import { UNICODE, ANSI, createDetailedMessage } from "@jsenv/humanize";
6
5
  import { memoize } from "@jsenv/utils/src/memoize/memoize.js";
7
6
 
8
7
  import { createChildExecOptions } from "./child_exec_options.js";
@@ -37,11 +36,8 @@ export const nodeChildProcess = ({
37
36
  env = {
38
37
  ...env,
39
38
  JSENV: true,
40
- FORCE_COLOR:
41
- supportsColor.stdout ||
42
- // GitHub workflow does support ANSI but "supports-color" returns false
43
- // because stream.isTTY returns false, see https://github.com/actions/runner/issues/241
44
- process.env.GITHUB_WORKFLOW,
39
+ ...(ANSI.supported ? { FORCE_COLOR: "1" } : {}),
40
+ ...(UNICODE.supported ? { FORCE_UNICODE: "1" } : {}),
45
41
  };
46
42
 
47
43
  return {
@@ -3,8 +3,8 @@
3
3
  // https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/worker/base.js
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { fileURLToPath } from "node:url";
6
- import supportsColor from "supports-color";
7
6
  import { Abort, raceCallbacks } from "@jsenv/abort";
7
+ import { UNICODE, ANSI } from "@jsenv/humanize";
8
8
  import { memoize } from "@jsenv/utils/src/memoize/memoize.js";
9
9
 
10
10
  import { createChildExecOptions } from "./child_exec_options.js";
@@ -33,11 +33,8 @@ export const nodeWorkerThread = ({
33
33
  env = {
34
34
  ...env,
35
35
  JSENV: true,
36
- FORCE_COLOR:
37
- supportsColor.stdout ||
38
- // GitHub workflow does support ANSI but "supports-color" returns false
39
- // because stream.isTTY returns false, see https://github.com/actions/runner/issues/241
40
- process.env.GITHUB_WORKFLOW,
36
+ ...(ANSI.supported ? { FORCE_COLOR: "1" } : {}),
37
+ ...(UNICODE.supported ? { FORCE_UNICODE: "1" } : {}),
41
38
  };
42
39
 
43
40
  return {