monocart-reporter 2.8.4 → 2.9.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.
package/README.md CHANGED
@@ -59,6 +59,7 @@
59
59
  ![](/docs/report.gif)
60
60
 
61
61
  ![](/docs/cli.png)
62
+ (For Github actions, we can enforce color with env: `FORCE_COLOR: true`)
62
63
 
63
64
  ## Install
64
65
  ```sh
@@ -1064,6 +1065,12 @@ The default config files (In order of priority)
1064
1065
  - mr.config.json
1065
1066
  - mr.config.ts
1066
1067
 
1068
+ Preload for TypeScript config file:
1069
+ - It requires node 18.19.0+
1070
+ - Installing tsx: `npm i -D tsx`
1071
+ - Using the `--import tsx` flag
1072
+ - see [comment](https://github.com/cenfun/monocart-reporter/issues/145#issuecomment-2365460013)
1073
+
1067
1074
 
1068
1075
  ## onEnd Hook
1069
1076
  The `onEnd` function will be executed after report generated. Arguments:
@@ -8,6 +8,9 @@ module.exports = () => ({
8
8
  // the output html file path (relative process.cwd)
9
9
  outputFile: './monocart-report/index.html',
10
10
 
11
+ json: true,
12
+ zip: false,
13
+
11
14
  // whether to copy attachments to the reporter output dir, defaults to true
12
15
  // it is useful when there are multiple html reports being output.
13
16
  copyAttachments: true,
@@ -22,6 +25,9 @@ module.exports = () => ({
22
25
  // timezone offset in minutes, GMT+0800 = -480
23
26
  timezoneOffset: 0,
24
27
 
28
+ // normal or exclude-idle
29
+ durationStrategy: null,
30
+
25
31
  // global coverage settings for addCoverageReport API
26
32
  coverage: null,
27
33
  // coverage: {
@@ -1,26 +1,92 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
3
  const EC = require('eight-colors');
3
4
  const nodemailer = require('nodemailer');
4
5
  const Util = require('./utils/util.js');
5
6
  const emailPlugin = require('./plugins/email.js');
6
7
  const Assets = require('./assets.js');
7
-
8
+ const { ZipFile } = require('./packages/monocart-reporter-vendor.js');
8
9
  // ===========================================================================
9
10
 
10
- const generateJson = (outputDir, htmlFile, reportData) => {
11
- const filename = path.basename(htmlFile, '.html');
12
- let jsonPath = path.resolve(outputDir, `${filename}.json`);
11
+ const generateJson = (outputDir, filename, reportData, options) => {
12
+ if (!options.json) {
13
+ return;
14
+ }
15
+
16
+ const jsonPath = path.resolve(outputDir, `${filename}.json`);
13
17
  Util.writeJSONSync(jsonPath, reportData);
14
- jsonPath = Util.relativePath(jsonPath);
15
- return jsonPath;
18
+ reportData.jsonPath = Util.relativePath(jsonPath);
19
+ };
20
+
21
+ const forEachFile = (dir, callback) => {
22
+ if (!fs.existsSync(dir)) {
23
+ return;
24
+ }
25
+ const dirs = [];
26
+ fs.readdirSync(dir, {
27
+ withFileTypes: true
28
+ }).forEach((it) => {
29
+
30
+ if (it.isFile()) {
31
+ callback(it.name, dir);
32
+ return;
33
+ }
34
+
35
+ if (it.isDirectory()) {
36
+ dirs.push(path.resolve(dir, it.name));
37
+ }
38
+ });
39
+
40
+ for (const subDir of dirs) {
41
+ forEachFile(subDir, callback);
42
+ }
43
+
44
+ };
45
+
46
+ const generateZip = (outputDir, filename, reportData, options) => {
47
+
48
+ if (!options.zip) {
49
+ return;
50
+ }
51
+
52
+ const zipPath = path.resolve(outputDir, `${filename}.zip`);
53
+ const reportFiles = [];
54
+ return new Promise((resolve) => {
55
+ const zipFile = new ZipFile();
56
+ zipFile.outputStream.pipe(fs.createWriteStream(zipPath)).on('close', function() {
57
+
58
+ // remove files after zip
59
+ reportData.reportFiles = reportFiles;
60
+ reportData.zipPath = Util.relativePath(zipPath);
61
+
62
+ resolve();
63
+ });
64
+
65
+ forEachFile(outputDir, (name, dir) => {
66
+ const absPath = path.resolve(dir, name);
67
+ const relPath = Util.relativePath(absPath, outputDir);
68
+ reportFiles.push(relPath);
69
+ // console.log(relPath);
70
+ zipFile.addFile(absPath, relPath);
71
+ });
72
+
73
+ zipFile.end();
74
+ });
16
75
  };
17
76
 
18
- const generateHtml = async (outputDir, htmlFile, reportData, inline) => {
77
+ const generateHtml = async (outputDir, filename, reportData, options) => {
78
+
79
+ // generate html
80
+ let inline = true;
81
+ if (typeof options.inline === 'boolean') {
82
+ inline = options.inline;
83
+ }
19
84
 
20
85
  // deps
21
86
  const jsFiles = ['monocart-reporter-app'];
87
+ const htmlFile = `${filename}.html`;
22
88
 
23
- const options = {
89
+ const htmlPath = await Assets.saveHtmlReport({
24
90
  inline,
25
91
  reportData,
26
92
  jsFiles,
@@ -29,11 +95,10 @@ const generateHtml = async (outputDir, htmlFile, reportData, inline) => {
29
95
  htmlFile,
30
96
 
31
97
  reportDataFile: 'report-data.js'
32
- };
98
+ });
33
99
 
34
- const htmlPath = await Assets.saveHtmlReport(options);
100
+ reportData.htmlPath = htmlPath;
35
101
 
36
- return htmlPath;
37
102
  };
38
103
 
39
104
  const showTestResults = (reportData) => {
@@ -221,30 +286,38 @@ const generateReport = async (reportData, options, rawData) => {
221
286
  await onDataHandler(reportData, options, rawData);
222
287
 
223
288
  // console.log(reportData);
224
- const htmlFile = path.basename(outputFile);
225
-
226
- // generate json
227
- const jsonPath = await generateJson(outputDir, htmlFile, reportData);
228
-
229
- // generate html
230
- let inline = true;
231
- if (typeof options.inline === 'boolean') {
232
- inline = options.inline;
233
- }
234
- const htmlPath = await generateHtml(outputDir, htmlFile, reportData, inline);
289
+ const filename = path.basename(outputFile, '.html');
235
290
 
236
- // for onEnd after saved
237
- Object.assign(reportData, {
238
- htmlPath,
239
- jsonPath
240
- });
291
+ await generateHtml(outputDir, filename, reportData, options);
292
+ await generateJson(outputDir, filename, reportData, options);
293
+ await generateZip(outputDir, filename, reportData, options);
241
294
 
242
295
  await onEndHandler(reportData, options);
243
296
 
244
297
  // after onEnd for summary changes
245
298
  showTestResults(reportData);
246
299
 
247
- Util.logInfo(`html report: ${EC.cyan(htmlPath)} (json: ${jsonPath})`);
300
+ // clean .cache for merge
301
+ if (options.cacheDir) {
302
+ Util.rmSync(options.cacheDir);
303
+ }
304
+
305
+ const {
306
+ htmlPath, jsonPath, zipPath
307
+ } = reportData;
308
+
309
+ const assets = [];
310
+ if (jsonPath) {
311
+ assets.push(`json: ${EC.cyan(jsonPath)}`);
312
+ }
313
+ if (zipPath) {
314
+ assets.push(`zip: ${EC.cyan(zipPath)}`);
315
+ }
316
+
317
+ if (assets.length) {
318
+ Util.logInfo(assets.join(' '));
319
+ }
320
+
248
321
  Util.logInfo(`view report: ${EC.cyan(`npx monocart show-report ${htmlPath}`)}`);
249
322
 
250
323
  };
package/lib/index.d.ts CHANGED
@@ -54,6 +54,11 @@ export type MonocartReporterOptions = {
54
54
  /** the output file path (relative process.cwd) */
55
55
  outputFile?: string;
56
56
 
57
+ /** output json file for data only */
58
+ json?: boolean;
59
+ /** output zip file for all report files */
60
+ zip?: boolean;
61
+
57
62
  /**
58
63
  * whether to copy attachments to the reporter output dir, defaults to true
59
64
  * it is useful when there are multiple html reports being output
@@ -73,6 +78,9 @@ export type MonocartReporterOptions = {
73
78
  /** timezone offset in minutes, For example: GMT+0800 = -480 */
74
79
  timezoneOffset?: number;
75
80
 
81
+ /** normal or exclude-idle */
82
+ durationStrategy?: 'normal' | 'exclude-idle',
83
+
76
84
  /** global coverage options: https://github.com/cenfun/monocart-reporter?#code-coverage-report
77
85
  * ```js
78
86
  * coverage: {
package/lib/index.js CHANGED
@@ -55,6 +55,7 @@ class MonocartReporter {
55
55
  this.tickTime = this.options.tickTime || 1000;
56
56
  this.tickStart();
57
57
 
58
+ this.trends = [];
58
59
  // read trends from json before clean dir
59
60
  getTrends(this.options.trend).then((trends) => {
60
61
  // console.log('=========================== ', 'trends', trends.length);
@@ -65,12 +66,12 @@ class MonocartReporter {
65
66
 
66
67
  }
67
68
 
68
- async init() {
69
+ init() {
69
70
 
70
71
  this.options.cwd = Util.formatPath(process.cwd());
71
72
 
72
73
  // init outputDir
73
- const outputFile = await Util.resolveOutputFile(this.options.outputFile);
74
+ const outputFile = Util.resolveOutputFile(this.options.outputFile);
74
75
  this.options.outputFile = outputFile;
75
76
 
76
77
  const outputDir = path.dirname(outputFile);
package/lib/merge-data.js CHANGED
@@ -5,6 +5,7 @@ const generateReport = require('./generate-report.js');
5
5
  const getDefaultOptions = require('./default/options.js');
6
6
  const { calculateSummary, getTrends } = require('./common.js');
7
7
  const { mergeGlobalCoverageReport } = require('./plugins/coverage/coverage.js');
8
+ const { StreamZip } = require('./packages/monocart-reporter-vendor.js');
8
9
 
9
10
  const checkReportData = (item) => {
10
11
  if (item && typeof item === 'object') {
@@ -14,7 +15,71 @@ const checkReportData = (item) => {
14
15
  }
15
16
  };
16
17
 
17
- const initDataList = (reportDataList) => {
18
+ const unzipDataFile = async (item, num, options) => {
19
+
20
+ if (!options.cacheDir) {
21
+ options.cacheDir = path.resolve(options.outputDir, '.cache');
22
+ }
23
+
24
+ const unzipDir = path.resolve(options.cacheDir, `extracted-${num}`);
25
+
26
+ fs.mkdirSync(unzipDir, {
27
+ recursive: true
28
+ });
29
+
30
+ const zip = new StreamZip({
31
+ file: item
32
+ });
33
+
34
+ await zip.extract(null, unzipDir);
35
+
36
+ // Do not forget to close the file once you're done
37
+ await zip.close();
38
+
39
+ const filename = path.basename(item, '.zip');
40
+ return path.resolve(unzipDir, `${filename}.json`);
41
+ };
42
+
43
+ const getReportData = async (item, num, options) => {
44
+ if (typeof item === 'string') {
45
+
46
+ // json or zip path
47
+ const extname = path.extname(item);
48
+ if (extname === '.zip') {
49
+ item = await unzipDataFile(item, num, options);
50
+ // console.log(item);
51
+ }
52
+
53
+ // json path
54
+ const data = Util.readJSONSync(item);
55
+ if (!data) {
56
+ Util.logError(`failed to read report data file: ${item}`);
57
+ return;
58
+ }
59
+
60
+ // for finding attachments
61
+ const jsonDir = Util.relativePath(path.dirname(item));
62
+
63
+ Util.logInfo(`report data loaded: ${item}`);
64
+ return {
65
+ jsonDir,
66
+ data
67
+ };
68
+ }
69
+
70
+ if (!checkReportData(item)) {
71
+ Util.logError(`unmatched report data format: ${item}`);
72
+ return;
73
+ }
74
+
75
+ // json
76
+ return {
77
+ jsonDir: item.outputDir,
78
+ data: item
79
+ };
80
+ };
81
+
82
+ const initDataList = async (reportDataList, options) => {
18
83
  if (!Util.isList(reportDataList)) {
19
84
  Util.logError(`invalid report data list: ${reportDataList}`);
20
85
  return;
@@ -23,31 +88,17 @@ const initDataList = (reportDataList) => {
23
88
  let hasError = false;
24
89
  const list = [];
25
90
 
91
+ let num = 1;
26
92
  for (const item of reportDataList) {
27
- if (typeof item === 'string') {
28
- const data = Util.readJSONSync(item);
29
- if (!data) {
30
- hasError = true;
31
- Util.logError(`failed to read report data file: ${item}`);
32
- continue;
33
- }
34
-
35
- // for finding attachments
36
- data.jsonDir = Util.relativePath(path.dirname(item));
37
93
 
38
- list.push(data);
39
- Util.logInfo(`report data loaded: ${item}`);
40
- continue;
94
+ const data = await getReportData(item, num, options);
95
+ if (!data) {
96
+ hasError = true;
97
+ break;
41
98
  }
99
+ num += 1;
42
100
 
43
- if (!checkReportData(item)) {
44
- Util.logError(`unmatched report data format: ${item}`);
45
- return;
46
- }
47
-
48
- item.jsonDir = item.outputDir;
49
-
50
- list.push(item);
101
+ list.push(data);
51
102
  }
52
103
 
53
104
  if (hasError) {
@@ -82,12 +133,10 @@ const copyTarget = (targetPath, fromDir, toDir) => {
82
133
  }
83
134
  };
84
135
 
85
- const attachmentsHandler = (item, outputDir, attachmentPathHandler) => {
136
+ const attachmentsHandler = (data, jsonDir, outputDir, attachmentPathHandler) => {
86
137
 
87
- const jsonDir = item.jsonDir;
88
-
89
- const extras = Util.getAttachmentPathExtras(item);
90
- Util.forEach(item.rows, (row) => {
138
+ const extras = Util.getAttachmentPathExtras(data);
139
+ Util.forEach(data.rows, (row) => {
91
140
  if (row.type !== 'case' || !row.attachments) {
92
141
  return;
93
142
  }
@@ -195,62 +244,51 @@ const mergeArtifacts = async (artifactsList, options) => {
195
244
 
196
245
  const mergeDataList = async (dataList, options) => {
197
246
 
198
- const trends = await getTrends(options.trend);
199
-
200
- const outputFile = await Util.resolveOutputFile(options.outputFile);
201
- // init outputDir
202
- const outputDir = path.dirname(outputFile);
203
- // clean
204
- if (fs.existsSync(outputDir)) {
205
- fs.rmSync(outputDir, {
206
- recursive: true,
207
- force: true,
208
- maxRetries: 10
209
- });
210
- }
247
+ const { outputFile, outputDir } = options;
211
248
 
212
- fs.mkdirSync(outputDir, {
213
- recursive: true
214
- });
215
- // for attachment path handler
216
- options.outputDir = outputDir;
249
+ const trends = await getTrends(options.trend);
217
250
 
218
251
  const attachmentPathHandler = typeof options.attachmentPath === 'function' ? options.attachmentPath : null;
219
252
 
220
- const dates = [];
221
- const endDates = [];
253
+ const startDates = [];
254
+ const dateRanges = [];
222
255
  const metadata = {};
223
256
  const system = [];
224
257
  const rows = [];
225
258
  const artifactsList = [];
226
259
 
227
- for (const item of dataList) {
260
+ for (const dataItem of dataList) {
228
261
 
229
- attachmentsHandler(item, outputDir, attachmentPathHandler);
262
+ const { data, jsonDir } = dataItem;
230
263
 
231
- dates.push(item.date);
232
- endDates.push(item.date + item.duration);
264
+ attachmentsHandler(data, jsonDir, outputDir, attachmentPathHandler);
265
+
266
+ startDates.push(data.date);
267
+ dateRanges.push({
268
+ start: data.date,
269
+ end: data.date + data.duration
270
+ });
233
271
 
234
272
  // merge metadata (may collect from diff shard)
235
- Object.assign(metadata, item.metadata);
273
+ Object.assign(metadata, data.metadata);
236
274
 
237
275
  // merge system
238
- system.push(item.system);
276
+ system.push(data.system);
239
277
 
240
278
  // merge rows
241
279
  rows.push({
242
280
  // add shard level suite
243
- title: item.system.hostname,
281
+ title: data.system.hostname,
244
282
  type: 'suite',
245
283
  suiteType: 'shard',
246
- caseNum: item.summary.tests.value,
247
- subs: item.rows
284
+ caseNum: data.summary.tests.value,
285
+ subs: data.rows
248
286
  });
249
287
 
250
- if (item.artifacts) {
288
+ if (data.artifacts) {
251
289
  artifactsList.push({
252
- jsonDir: item.jsonDir,
253
- artifacts: item.artifacts
290
+ jsonDir,
291
+ artifacts: data.artifacts
254
292
  });
255
293
  }
256
294
 
@@ -260,7 +298,7 @@ const mergeDataList = async (dataList, options) => {
260
298
 
261
299
  // base on first one, do not change dataList (need for onData)
262
300
  const mergedData = {
263
- ... dataList[0]
301
+ ... dataList[0].data
264
302
  };
265
303
 
266
304
  // merge new options
@@ -272,11 +310,10 @@ const mergeDataList = async (dataList, options) => {
272
310
 
273
311
  const reportName = options.name || mergedData.name;
274
312
 
275
- const date = Math.min.apply(null, dates);
313
+ const date = Math.min.apply(null, startDates);
276
314
  const dateH = new Date(date).toLocaleString();
277
315
 
278
- const endDate = Math.max.apply(null, endDates);
279
- const duration = endDate - date;
316
+ const duration = Util.getDuration(dateRanges, options.durationStrategy);
280
317
  const durationH = Util.TF(duration);
281
318
 
282
319
  Object.assign(mergedData, {
@@ -315,19 +352,30 @@ module.exports = async (reportDataList, userOptions = {}) => {
315
352
 
316
353
  Util.logInfo('merging report data ...');
317
354
 
318
- const dataList = await initDataList(reportDataList);
319
- if (!dataList) {
320
- return;
321
- }
322
-
323
355
  const options = {
324
356
  ... getDefaultOptions(),
325
357
  ... userOptions
326
358
  };
327
359
 
360
+ // init options
361
+ const outputFile = Util.resolveOutputFile(options.outputFile);
362
+ options.outputFile = outputFile;
363
+ // init outputDir
364
+ const outputDir = path.dirname(outputFile);
365
+ // clean
366
+ Util.initDir(outputDir);
367
+ // for attachment path handler
368
+ options.outputDir = outputDir;
369
+
370
+ const dataList = await initDataList(reportDataList, options);
371
+ if (!dataList) {
372
+ return;
373
+ }
374
+
328
375
  const reportData = await mergeDataList(dataList, options);
329
376
  // console.log(reportData.artifacts);
330
377
 
331
- return generateReport(reportData, options, dataList);
378
+ const rawData = dataList.map((it) => it.data);
379
+ return generateReport(reportData, options, rawData);
332
380
 
333
381
  };