monocart-reporter 1.6.21 → 1.6.23

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
@@ -725,6 +725,27 @@ const report = await attachCoverageReport(coverageList, test.info(), {
725
725
  | Minified code | N/A | ✅ | ❌ |
726
726
  | Code formatting | N/A | ✅ | ❌ |
727
727
 
728
+ ## Global Coverage Report
729
+ If you want to generate a global coverage report, you can use the API `addCoverageReport(v8list, testInfo)`. When all the tests are finished, all added reports will be automatically merged into a global report. Currently supported `V8` only. Here is an example for Playwright Component Testing [playwright-ct-vue](https://github.com/cenfun/playwright-ct-vue).
730
+ ```js
731
+ // playwright.config.js
732
+ module.exports = {
733
+ reporter: [
734
+ ['monocart-reporter', {
735
+ name: "My Test Report",
736
+ outputFile: './test-results/report.html',
737
+ // global coverage report options
738
+ coverage: {
739
+ unpackSourceMap: true,
740
+ excludeDistFile: true,
741
+ sourceFilter: (sourceName) => sourceName.search(/\/src\/.+/) !== -1,
742
+ entryFilter: (entry) => {}
743
+ }
744
+ }]
745
+ ]
746
+ };
747
+ ```
748
+
728
749
  ## Attach Network Report
729
750
  Attach a network report with API `attachNetworkReport(har, testInfo)`. Arguments:
730
751
  - `har` HAR path (String) or HAR file buffer (Buffer). see [HAR 1.2 Spec](http://www.softwareishard.com/blog/har-12-spec/)
@@ -954,10 +975,9 @@ Please create an [Incoming Webhooks](https://learn.microsoft.com/en-us/microsoft
954
975
  - Lightweight UI Components [vine-ui](https://github.com/cenfun/vine-ui)
955
976
  - High Performance Grid [turbogrid](https://github.com/cenfun/turbogrid)
956
977
  - String compress/decompress [lz-utils](https://github.com/cenfun/lz-utils)
957
- - Formatter and Code Viewer [monocart-components](https://github.com/cenfun/monocart-components)
958
978
  - Packages
959
979
  - `packages/app` Monocart report UI
960
- - `packages/istanbul` Istanbul report libs
980
+ - `packages/coverage` Coverage report libs
961
981
  - `packages/network` Network HAR report libs
962
982
  - `packages/v8` V8 HTML report UI
963
983
  - `packages/vendor` Third-party libs
@@ -9,6 +9,14 @@ module.exports = {
9
9
  attachmentPath: null,
10
10
  // attachmentPath: (currentPath, extras) => `https://cenfun.github.io/monocart-reporter/${currentPath}`,
11
11
 
12
+ // global coverage settings for addCoverageReport API
13
+ coverage: null,
14
+ // coverage: {
15
+ // unpackSourceMap: true,
16
+ // sourceFilter: (sourceName) => sourceName.search(/\/src\/.+/) !== -1,
17
+ // entryFilter: (entry) => {}
18
+ // },
19
+
12
20
  // trend data handler
13
21
  trend: null,
14
22
  // trend: () => './test-results/report.json',
@@ -4,6 +4,7 @@ const Util = require('./utils/util.js');
4
4
  const { getTickInfo } = require('./utils/system.js');
5
5
  const Visitor = require('./visitor.js');
6
6
  const { calculateSummary } = require('./common.js');
7
+ const { generateCoverageReport } = require('./plugins/coverage/coverage.js');
7
8
 
8
9
  const getReportName = (options, config, metadata) => {
9
10
  const reportName = options.name || config.name || metadata.name;
@@ -13,6 +14,18 @@ const getReportName = (options, config, metadata) => {
13
14
  return 'Test Report';
14
15
  };
15
16
 
17
+ const artifactsHandler = async (visitor, options) => {
18
+ const artifacts = [];
19
+ const { coverage } = visitor.artifacts;
20
+ visitor.artifacts = null;
21
+ if (coverage) {
22
+ const report = await generateCoverageReport(coverage, options);
23
+ report.name = 'coverage';
24
+ artifacts.push(report);
25
+ }
26
+ return artifacts;
27
+ };
28
+
16
29
  const generateData = async (results) => {
17
30
 
18
31
  console.log('[MCR] generating report data ...');
@@ -28,7 +41,7 @@ const generateData = async (results) => {
28
41
  // console.log(config);
29
42
  const cwd = Util.formatPath(process.cwd());
30
43
 
31
- const outputFile = options.outputFile;
44
+ const outputFile = await Util.resolveOutputFile(options.outputFile);
32
45
  // init outputDir
33
46
  const outputDir = path.dirname(outputFile);
34
47
  if (!fs.existsSync(outputDir)) {
@@ -74,6 +87,9 @@ const generateData = async (results) => {
74
87
  });
75
88
 
76
89
  const reportName = getReportName(options, config, metadata);
90
+ options.name = reportName;
91
+
92
+ const artifacts = await artifactsHandler(visitor, options);
77
93
 
78
94
  system.cwd = cwd;
79
95
 
@@ -118,6 +134,8 @@ const generateData = async (results) => {
118
134
 
119
135
  metadata,
120
136
  system,
137
+
138
+ artifacts,
121
139
  trends,
122
140
 
123
141
  // columns, rows, formatters
@@ -5,7 +5,7 @@ const CG = require('console-grid');
5
5
  const { deflateSync } = require('lz-utils');
6
6
  const { nodemailer } = require('./runtime/monocart-vendor.js');
7
7
  const Util = require('./utils/util.js');
8
- const emailHelper = require('./helper/email.js');
8
+ const emailPlugin = require('./plugins/email.js');
9
9
 
10
10
  // ===========================================================================
11
11
 
@@ -148,7 +148,15 @@ const generateReport = (reportData, onEnd) => {
148
148
  showTestResults(reportData);
149
149
 
150
150
  // console.log(config);
151
- const { outputFile, outputDir } = reportData;
151
+ const {
152
+ outputFile, outputDir, artifacts
153
+ } = reportData;
154
+
155
+ if (artifacts) {
156
+ artifacts.forEach((report) => {
157
+ console.log(`[MCR] ${report.name} report: ${EC.cyan(report.htmlPath)}`);
158
+ });
159
+ }
152
160
 
153
161
  // console.log(reportData);
154
162
  const filename = path.basename(outputFile, '.html');
@@ -170,7 +178,7 @@ const generateReport = (reportData, onEnd) => {
170
178
  });
171
179
 
172
180
  // generate email data
173
- emailHelper(reportData);
181
+ emailPlugin(reportData);
174
182
 
175
183
  // forEach rows API
176
184
  const forEach = (callback) => {
package/lib/index.js CHANGED
@@ -6,9 +6,9 @@ const defaultOptions = require('./default/options.js');
6
6
  const { getTrends } = require('./common.js');
7
7
 
8
8
  const merge = require('./merge-data.js');
9
- const { attachAuditReport } = require('./helper/audit.js');
10
- const { attachCoverageReport } = require('./helper/coverage.js');
11
- const { attachNetworkReport } = require('./helper/network.js');
9
+ const { attachAuditReport } = require('./plugins/audit/audit.js');
10
+ const { addCoverageReport, attachCoverageReport } = require('./plugins/coverage/coverage.js');
11
+ const { attachNetworkReport } = require('./plugins/network/network.js');
12
12
 
13
13
  // custom reporter
14
14
  // https://playwright.dev/docs/test-reporters#custom-reporters
@@ -16,8 +16,7 @@ class Reporter {
16
16
 
17
17
  static merge = merge;
18
18
 
19
- // Deprecated
20
- static takeCoverage = attachCoverageReport;
19
+ static addCoverageReport = addCoverageReport;
21
20
 
22
21
  static attachAuditReport = attachAuditReport;
23
22
  static attachCoverageReport = attachCoverageReport;
package/lib/index.mjs CHANGED
@@ -5,8 +5,7 @@ export { MonocartReporter };
5
5
 
6
6
  export const merge = MonocartReporter.merge;
7
7
 
8
- // Deprecated
9
- export const takeCoverage = MonocartReporter.attachCoverageReport;
8
+ export const addCoverageReport = MonocartReporter.addCoverageReport;
10
9
 
11
10
  export const attachAuditReport = MonocartReporter.attachAuditReport;
12
11
  export const attachCoverageReport = MonocartReporter.attachCoverageReport;
package/lib/merge-data.js CHANGED
@@ -76,7 +76,7 @@ const mergeDataList = async (dataList, options) => {
76
76
 
77
77
  const trends = await getTrends(options.trend);
78
78
 
79
- const outputFile = options.outputFile;
79
+ const outputFile = await Util.resolveOutputFile(options.outputFile);
80
80
  // init outputDir
81
81
  const outputDir = path.dirname(outputFile);
82
82
  if (!fs.existsSync(outputDir)) {
@@ -0,0 +1,74 @@
1
+ class Concurrency {
2
+
3
+ constructor(maxCount = 10) {
4
+ this.maxCount = maxCount;
5
+ this.list = [];
6
+ }
7
+
8
+ addItem(item) {
9
+ this.list.push(item);
10
+ }
11
+
12
+ addList(list) {
13
+ this.list = this.list.concat(list);
14
+ }
15
+
16
+ start(handler) {
17
+ // must be async function
18
+ if (typeof handler !== 'function') {
19
+ return;
20
+ }
21
+ this.handler = handler;
22
+ this.count = 0;
23
+ return new Promise((resolve) => {
24
+ this.resolve = resolve;
25
+ this.next();
26
+ });
27
+ }
28
+
29
+ next() {
30
+ // console.log(`list: ${this.list.length} count: ${this.count}`);
31
+
32
+ // if has clear
33
+ if (!this.resolve) {
34
+ return;
35
+ }
36
+
37
+ if (!this.list.length) {
38
+ // no list but has in progress count
39
+ if (this.count > 0) {
40
+ return;
41
+ }
42
+ // all finish
43
+ this.resolve();
44
+ this.clear();
45
+ return;
46
+ }
47
+
48
+ // out of concurrency count, just wait
49
+ if (this.count >= this.maxCount) {
50
+ return;
51
+ }
52
+
53
+ const item = this.list.shift();
54
+ this.count += 1;
55
+
56
+ // async handler
57
+ this.handler(item).finally(() => {
58
+ this.count -= 1;
59
+ this.next();
60
+ });
61
+
62
+ this.next();
63
+ }
64
+
65
+ clear() {
66
+ this.list = [];
67
+ this.handler = null;
68
+ this.count = 0;
69
+ this.resolve = null;
70
+ }
71
+
72
+ }
73
+
74
+ module.exports = Concurrency;
@@ -19,6 +19,11 @@ const Util = {
19
19
  name: 'network',
20
20
  contentType: 'text/html',
21
21
  reportFile: 'network-report.json'
22
+ },
23
+ // artifact will be removed finally
24
+ artifact: {
25
+ name: 'artifact',
26
+ contentType: 'application/json'
22
27
  }
23
28
  },
24
29
 
@@ -164,7 +169,15 @@ const Util = {
164
169
  });
165
170
  }
166
171
  });
167
- flatRanges.sort((a, b) => a.startOffset - b.startOffset);
172
+ flatRanges.sort((a, b) => {
173
+ if (a.startOffset === b.startOffset) {
174
+ if (a.endOffset === b.endOffset) {
175
+ return a.count - b.count;
176
+ }
177
+ return a.endOffset - b.endOffset;
178
+ }
179
+ return a.startOffset - b.startOffset;
180
+ });
168
181
  }
169
182
  return flatRanges;
170
183
  },
@@ -186,6 +199,25 @@ const Util = {
186
199
  });
187
200
  },
188
201
 
202
+ // =============================================================================
203
+
204
+ generatePercentChart: function(percent) {
205
+ return `<div style="--mcr-percent:${percent}%;" class="mcr-percent-chart"></div>`;
206
+ },
207
+
208
+ getStatus: (value, watermarks) => {
209
+ if (!watermarks) {
210
+ return 'unknown';
211
+ }
212
+ if (value < watermarks[0]) {
213
+ return 'low';
214
+ }
215
+ if (value < watermarks[1]) {
216
+ return 'medium';
217
+ }
218
+ return 'high';
219
+ },
220
+
189
221
  // =============================================================================
190
222
  // svg
191
223
 
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const EC = require('eight-colors');
4
- const Util = require('../utils/util.js');
4
+ const Util = require('../../utils/util.js');
5
5
 
6
6
  const getStatus = (s) => {
7
7
  if (s < 0.5) {
@@ -71,7 +71,7 @@ const attachAuditReport = async (runnerResult, testInfo, options = {}) => {
71
71
  }
72
72
 
73
73
  options = {
74
- outputDir: Util.getOutputDir(testInfo),
74
+ outputDir: Util.resolveOutputDir(testInfo),
75
75
  outputName: `audit-${Util.shortTestId(testInfo.testId)}`,
76
76
 
77
77
  ... options
@@ -0,0 +1,267 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const EC = require('eight-colors');
4
+
5
+ const Util = require('../../utils/util.js');
6
+
7
+ const { saveIstanbulReport, saveV8ToIstanbulReport } = require('./istanbul/istanbul.js');
8
+
9
+ const {
10
+ initV8List, mergeV8List, saveV8Report
11
+ } = require('./v8/v8.js');
12
+
13
+ // ========================================================================================================
14
+
15
+ // high performance
16
+ // str.search(reg) -> index
17
+ // reg.test(str) -> boolean
18
+ const defaultSourceFilter = (sourceName) => {
19
+ // sourceName.search(/\/src\/.+/) !== -1
20
+ return true;
21
+ };
22
+
23
+ const defaultV8Options = {
24
+ toIstanbul: false,
25
+
26
+ unpackSourceMap: true,
27
+ excludeDistFile: true,
28
+ sourceFilter: defaultSourceFilter,
29
+ entryFilter: null
30
+
31
+ // watermarks: [50, 80],
32
+ // inline: false
33
+ };
34
+
35
+ const defaultIstanbulOptions = {
36
+ entryFilter: null,
37
+ sourceFinder: null,
38
+ lcov: false
39
+
40
+ // watermarks: {},
41
+ };
42
+
43
+ // ========================================================================================================
44
+
45
+ const saveReportAttachment = (testInfo, report, htmlDir) => {
46
+
47
+ const definition = Util.attachments.coverage;
48
+ // save report
49
+ const reportPath = path.resolve(htmlDir, definition.reportFile);
50
+ Util.writeJSONSync(reportPath, report);
51
+
52
+ testInfo.attachments.push({
53
+ name: definition.name,
54
+ contentType: definition.contentType,
55
+ path: path.resolve(htmlDir, 'index.html')
56
+ });
57
+ };
58
+
59
+ // ========================================================================================================
60
+
61
+ const generateIstanbulReport = (istanbulCoverage, testInfo, options) => {
62
+
63
+ options = {
64
+ ... defaultIstanbulOptions,
65
+ ... options
66
+ };
67
+
68
+ const report = saveIstanbulReport(istanbulCoverage, options);
69
+
70
+ saveReportAttachment(testInfo, report, options.htmlDir);
71
+
72
+ return report;
73
+ };
74
+
75
+ // ========================================================================================================
76
+
77
+ const toIstanbulReport = async (v8list, testInfo, options) => {
78
+
79
+ options = {
80
+ ... defaultIstanbulOptions,
81
+ ... options
82
+ };
83
+
84
+ const report = await saveV8ToIstanbulReport(v8list, options);
85
+
86
+ saveReportAttachment(testInfo, report, options.htmlDir);
87
+
88
+ return report;
89
+ };
90
+
91
+ // ========================================================================================================
92
+
93
+
94
+ const generateV8Report = async (v8list, testInfo, options) => {
95
+
96
+ options.title = `Coverage Report - ${testInfo.title}`;
97
+
98
+ const report = await saveV8Report(v8list, options);
99
+
100
+ saveReportAttachment(testInfo, report, options.htmlDir);
101
+
102
+ return report;
103
+ };
104
+
105
+ // ========================================================================================================
106
+
107
+ const generateV8Coverage = async (v8list, testInfo, options) => {
108
+
109
+ // v8list options, also for init / source map handler
110
+ options = {
111
+ ... defaultV8Options,
112
+ ... options
113
+ };
114
+
115
+ v8list = await initV8List(v8list, options);
116
+
117
+ if (options.toIstanbul) {
118
+ return toIstanbulReport(v8list, testInfo, options);
119
+ }
120
+
121
+ return generateV8Report(v8list, testInfo, options);
122
+ };
123
+
124
+ /**
125
+ * @parameters
126
+ * (@input istanbul.__coverage__: Object, testInfo, options: { watermarks: {} }) -> @output istanbul report
127
+ * (@input v8list: Array, testInfo, options: { toIstanbul: true, watermarks: {} }) -> @output istanbul report (without css supported)
128
+ * (@input v8list: Array, testInfo, options: { watermarks: [], inline: Boolean }) -> @output v8 report (css supported and minified code formatting)
129
+ */
130
+ const attachCoverageReport = (coverageInput, testInfo, options = {}) => {
131
+
132
+ if (!coverageInput) {
133
+ EC.logRed('[MCR] invalid coverage input');
134
+ return;
135
+ }
136
+
137
+ options = {
138
+ outputDir: Util.resolveOutputDir(testInfo),
139
+ outputName: `coverage-${Util.shortTestId(testInfo.testId)}`,
140
+ ... options
141
+ };
142
+
143
+ const htmlDir = path.resolve(options.outputDir, options.outputName);
144
+ if (!fs.existsSync(htmlDir)) {
145
+ fs.mkdirSync(htmlDir, {
146
+ recursive: true
147
+ });
148
+ }
149
+ options.htmlDir = htmlDir;
150
+
151
+ if (Array.isArray(coverageInput)) {
152
+ return generateV8Coverage(coverageInput, testInfo, options);
153
+ }
154
+
155
+ return generateIstanbulReport(coverageInput, testInfo, options);
156
+ };
157
+
158
+ // ========================================================================================================
159
+
160
+ // add coverage report to global
161
+ const addCoverageReport = async (v8list, testInfo) => {
162
+
163
+ if (!Util.isList(v8list)) {
164
+ EC.logRed('[MCR] invalid coverage input');
165
+ return;
166
+ }
167
+
168
+ const reporterOptions = Util.resolveReporterOptions(testInfo);
169
+ const coverageOptions = reporterOptions.coverage || {};
170
+
171
+ // reporter outputFile
172
+ const outputFile = await Util.resolveOutputFile(reporterOptions.outputFile);
173
+ const outputDir = path.dirname(outputFile);
174
+
175
+ const options = {
176
+ // use reporter dir as output dir, NOT test output dir
177
+ outputDir,
178
+ outputName: 'coverage',
179
+
180
+ ... defaultV8Options,
181
+ ... coverageOptions
182
+ };
183
+
184
+ // The source map must be fetched before page closed
185
+ // or it may be deleted
186
+ v8list = await initV8List(v8list, options);
187
+
188
+ const id = Util.shortTestId(testInfo.testId);
189
+ const filename = `artifact-${id}.json`;
190
+ const jsonDir = path.resolve(options.outputDir, options.outputName);
191
+ const jsonPath = path.resolve(jsonDir, filename);
192
+
193
+ const report = {
194
+ id,
195
+ title: testInfo.title,
196
+ outputFile: Util.relativePath(jsonPath),
197
+ // current v8 only
198
+ type: 'v8',
199
+ data: v8list
200
+ };
201
+
202
+ const artifactContent = JSON.stringify({
203
+ type: 'coverage',
204
+ data: report
205
+ });
206
+ await Util.writeFile(jsonPath, artifactContent);
207
+
208
+ const definition = Util.attachments.artifact;
209
+ testInfo.attachments.push({
210
+ name: definition.name,
211
+ contentType: definition.contentType,
212
+ path: jsonPath
213
+ });
214
+
215
+ return report;
216
+ };
217
+
218
+ // global coverage report, run different process with addCoverageReport
219
+ const generateCoverageReport = async (dataList, reporterOptions) => {
220
+
221
+ // reporter outputFile
222
+ const outputFile = await Util.resolveOutputFile(reporterOptions.outputFile);
223
+ const outputDir = path.dirname(outputFile);
224
+
225
+ const coverageOptions = reporterOptions.coverage || {};
226
+ const options = {
227
+ outputDir,
228
+ outputName: 'coverage',
229
+ ... coverageOptions
230
+ };
231
+
232
+ const htmlDir = path.resolve(options.outputDir, options.outputName);
233
+ if (!fs.existsSync(htmlDir)) {
234
+ fs.mkdirSync(htmlDir, {
235
+ recursive: true
236
+ });
237
+ }
238
+ options.htmlDir = htmlDir;
239
+
240
+ let v8list = [];
241
+ dataList.forEach((item) => {
242
+ v8list = v8list.concat(item.data);
243
+ });
244
+
245
+ // merge list, maybe collected multiple times
246
+ v8list = await mergeV8List(v8list, options);
247
+
248
+ // const v8Path = path.resolve(__dirname, '../../.temp/v8-data.js');
249
+ // const v8Data = `module.exports = ${JSON.stringify(v8list, null, 4)};`;
250
+ // fs.writeFileSync(v8Path, v8Data);
251
+
252
+ options.title = `Coverage Report - ${reporterOptions.name}`;
253
+
254
+ const report = await saveV8Report(v8list, options);
255
+
256
+ return {
257
+ type: report.type,
258
+ htmlPath: report.htmlPath,
259
+ summary: report.summary
260
+ };
261
+ };
262
+
263
+ module.exports = {
264
+ addCoverageReport,
265
+ generateCoverageReport,
266
+ attachCoverageReport
267
+ };
@@ -0,0 +1,49 @@
1
+ const { istanbulLibReport } = require('../../../runtime/monocart-coverage.js');
2
+
3
+ const ReportBase = istanbulLibReport.ReportBase;
4
+ class IstanbulSummary extends ReportBase {
5
+
6
+ onStart(root, context) {
7
+ this.context = context;
8
+ this.summary = {};
9
+ this.files = [];
10
+ }
11
+
12
+ addStatus(data) {
13
+ Object.keys(data).forEach((k) => {
14
+ const item = data[k];
15
+ // low, medium, high, unknown
16
+ item.status = this.context.classForPercent(k, item.pct);
17
+ });
18
+ }
19
+
20
+ onSummary(node) {
21
+ if (!node.isRoot()) {
22
+ return;
23
+ }
24
+ this.summary = node.getCoverageSummary().data;
25
+ this.addStatus(this.summary);
26
+ }
27
+
28
+ onDetail(node) {
29
+ const fileSummary = node.getCoverageSummary().data;
30
+ this.addStatus(fileSummary);
31
+ fileSummary.name = node.getRelativeName();
32
+ this.files.push(fileSummary);
33
+ }
34
+
35
+ onEnd() {
36
+ // console.log('onEnd');
37
+ }
38
+
39
+ getReport() {
40
+ return {
41
+ type: 'istanbul',
42
+ summary: this.summary,
43
+ files: this.files
44
+ };
45
+ }
46
+ }
47
+
48
+
49
+ module.exports = IstanbulSummary;