monocart-reporter 2.9.6 → 2.9.8
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/LICENSE +21 -21
- package/README.md +1180 -1180
- package/lib/cli.js +372 -372
- package/lib/common.js +244 -244
- package/lib/default/columns.js +79 -79
- package/lib/default/options.js +95 -95
- package/lib/default/summary.js +80 -80
- package/lib/default/template.html +47 -47
- package/lib/generate-data.js +174 -174
- package/lib/generate-report.js +360 -360
- package/lib/index.d.ts +268 -268
- package/lib/index.js +253 -253
- package/lib/index.mjs +19 -19
- package/lib/merge-data.js +405 -405
- package/lib/packages/monocart-reporter-assets.js +3 -3
- package/lib/packages/monocart-reporter-vendor.js +22 -23
- package/lib/platform/concurrency.js +74 -74
- package/lib/platform/share.js +369 -369
- package/lib/plugins/audit/audit.js +119 -119
- package/lib/plugins/comments.js +124 -124
- package/lib/plugins/coverage/coverage.js +169 -169
- package/lib/plugins/email.js +76 -76
- package/lib/plugins/metadata/metadata.js +25 -25
- package/lib/plugins/network/network.js +186 -186
- package/lib/plugins/state/client.js +152 -152
- package/lib/plugins/state/state.js +194 -194
- package/lib/utils/pie.js +148 -148
- package/lib/utils/system.js +145 -145
- package/lib/utils/util.js +512 -511
- package/lib/visitor.js +915 -915
- package/package.json +10 -10
package/lib/visitor.js
CHANGED
|
@@ -1,915 +1,915 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const EC = require('eight-colors');
|
|
4
|
-
const {
|
|
5
|
-
StackUtils, codeFrameColumns, sanitize
|
|
6
|
-
} = require('./packages/monocart-reporter-vendor.js');
|
|
7
|
-
const Util = require('./utils/util.js');
|
|
8
|
-
const commentsPlugin = require('./plugins/comments.js');
|
|
9
|
-
const getDefaultColumns = require('./default/columns.js');
|
|
10
|
-
|
|
11
|
-
class Visitor {
|
|
12
|
-
constructor(root, options) {
|
|
13
|
-
this.root = root;
|
|
14
|
-
this.options = options;
|
|
15
|
-
|
|
16
|
-
// console.log(options);
|
|
17
|
-
|
|
18
|
-
if (typeof options.visitor === 'function') {
|
|
19
|
-
this.customCommonVisitor = options.visitor;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async start() {
|
|
25
|
-
|
|
26
|
-
const columns = getDefaultColumns();
|
|
27
|
-
// default columns not detailed in report
|
|
28
|
-
columns.forEach((item) => {
|
|
29
|
-
item.detailed = false;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// custom column formatters with string passed to JSON
|
|
33
|
-
this.formatters = {};
|
|
34
|
-
|
|
35
|
-
// user defined custom columns
|
|
36
|
-
const handler = this.options.columns;
|
|
37
|
-
if (!this.columnsUpdated && typeof handler === 'function') {
|
|
38
|
-
// prevent repeated execution
|
|
39
|
-
this.columnsUpdated = true;
|
|
40
|
-
|
|
41
|
-
// update default columns by user
|
|
42
|
-
handler.call(this, columns);
|
|
43
|
-
|
|
44
|
-
// maybe a tree
|
|
45
|
-
const customVisitors = [];
|
|
46
|
-
this.initCustomHandler(columns, customVisitors, this.formatters);
|
|
47
|
-
if (customVisitors.length) {
|
|
48
|
-
this.customVisitors = customVisitors;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// console.log(customFormatters);
|
|
53
|
-
|
|
54
|
-
this.columns = columns;
|
|
55
|
-
this.rows = [];
|
|
56
|
-
this.jobs = [];
|
|
57
|
-
this.artifacts = [];
|
|
58
|
-
|
|
59
|
-
await this.visit(this.root, this.rows);
|
|
60
|
-
|
|
61
|
-
this.duplicatedErrorsHandler(this.rows);
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ==============================================================================================
|
|
66
|
-
|
|
67
|
-
initCustomHandler(list, visitors, formatters) {
|
|
68
|
-
|
|
69
|
-
list.forEach((column) => {
|
|
70
|
-
if (column.id) {
|
|
71
|
-
|
|
72
|
-
// custom visitor
|
|
73
|
-
if (typeof column.visitor === 'function') {
|
|
74
|
-
|
|
75
|
-
visitors.push({
|
|
76
|
-
id: column.id,
|
|
77
|
-
visitor: column.visitor
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// remove function (can not be in JSON)
|
|
81
|
-
delete column.visitor;
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// custom formatter
|
|
86
|
-
if (typeof column.formatter === 'function') {
|
|
87
|
-
|
|
88
|
-
formatters[column.id] = column.formatter.toString();
|
|
89
|
-
|
|
90
|
-
// remove function (can not be in JSON)
|
|
91
|
-
delete column.formatter;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// drill down
|
|
97
|
-
if (Util.isList(column.subs)) {
|
|
98
|
-
this.initCustomHandler(column.subs, visitors, formatters);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// generate the column data from playwright metadata
|
|
104
|
-
// data.type is suite, metadata is Suite, https://playwright.dev/docs/api/class-suite
|
|
105
|
-
// data.type is case, metadata is TestCase, https://playwright.dev/docs/api/class-testcase
|
|
106
|
-
// data.type is step, metadata is TestStep, https://playwright.dev/docs/api/class-teststep
|
|
107
|
-
async customVisitorsHandler(data, metadata) {
|
|
108
|
-
|
|
109
|
-
if (this.options.customFieldsInComments) {
|
|
110
|
-
const customData = commentsPlugin(metadata);
|
|
111
|
-
Object.assign(data, customData);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// for all data
|
|
115
|
-
if (this.customCommonVisitor) {
|
|
116
|
-
await this.customCommonVisitor.call(this, data, metadata);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// for single column data (high priority)
|
|
120
|
-
if (this.customVisitors) {
|
|
121
|
-
for (const item of this.customVisitors) {
|
|
122
|
-
const res = await item.visitor.call(this, data, metadata);
|
|
123
|
-
if (typeof res !== 'undefined') {
|
|
124
|
-
data[item.id] = res;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ==============================================================================================
|
|
131
|
-
|
|
132
|
-
async visit(suite, list) {
|
|
133
|
-
if (!suite._entries) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
// suite -> tests/test case -> test result -> test step
|
|
137
|
-
for (const entry of suite._entries) {
|
|
138
|
-
// only case has results
|
|
139
|
-
if (entry.results) {
|
|
140
|
-
await this.testCaseHandler(entry, list);
|
|
141
|
-
} else {
|
|
142
|
-
await this.testSuiteHandler(entry, list);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ==============================================================================================
|
|
148
|
-
|
|
149
|
-
/*
|
|
150
|
-
Project suite #1. Has a child suite for each test file in the project.
|
|
151
|
-
File suite #1
|
|
152
|
-
TestCase #1
|
|
153
|
-
Suite corresponding to a test.describe(title, callback) group
|
|
154
|
-
TestCase #1 in a group
|
|
155
|
-
TestStep
|
|
156
|
-
*/
|
|
157
|
-
async testSuiteHandler(suite, list) {
|
|
158
|
-
|
|
159
|
-
// sometimes project title is empty
|
|
160
|
-
const suiteType = suite._type;
|
|
161
|
-
let suiteTitle = Util.formatPath(suite.title);
|
|
162
|
-
if (!suiteTitle) {
|
|
163
|
-
suiteTitle = suiteType;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// suite uid for report
|
|
167
|
-
const suiteStr = [suite._fileId].concat(suite.titlePath()).filter((it) => it).join(' ');
|
|
168
|
-
// console.log(suiteStr);
|
|
169
|
-
const suiteId = Util.calculateId(suiteStr);
|
|
170
|
-
|
|
171
|
-
const group = {
|
|
172
|
-
id: suiteId,
|
|
173
|
-
title: suiteTitle,
|
|
174
|
-
type: 'suite',
|
|
175
|
-
// root, project, file, describe
|
|
176
|
-
suiteType: suiteType,
|
|
177
|
-
// all test cases in this suite and its descendants
|
|
178
|
-
caseNum: suite.allTests().length,
|
|
179
|
-
subs: []
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
if (suiteType === 'project') {
|
|
183
|
-
this.projectMetadataHandler(group, suite);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (suite.location) {
|
|
187
|
-
group.location = this.locationHandler(suite.location);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
await this.customVisitorsHandler(group, suite);
|
|
191
|
-
|
|
192
|
-
list.push(group);
|
|
193
|
-
// drill down
|
|
194
|
-
await this.visit(suite, group.subs);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
projectMetadataHandler(project, suite) {
|
|
198
|
-
const sp = suite._fullProject;
|
|
199
|
-
if (!sp) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const projectMetadata = sp.project && sp.project.metadata;
|
|
204
|
-
if (!projectMetadata) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const config = sp.fullConfig && sp.fullConfig.config;
|
|
209
|
-
const configMetadata = config && config.metadata;
|
|
210
|
-
if (configMetadata && configMetadata === projectMetadata) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
project.metadata = projectMetadata;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ==============================================================================================
|
|
218
|
-
|
|
219
|
-
async testCaseHandler(testCase, list) {
|
|
220
|
-
|
|
221
|
-
// duration
|
|
222
|
-
// total of testResult.duration is not exact, it will cost time before/between/after result
|
|
223
|
-
const caseTimestamps = [].concat(testCase.timestamps);
|
|
224
|
-
const duration = caseTimestamps.pop() - caseTimestamps.shift();
|
|
225
|
-
|
|
226
|
-
// Unique test ID that is computed based on the test file name, test title and project name
|
|
227
|
-
|
|
228
|
-
// 6113402d7bc11a0fb7a9-281a9986cca0dfd6fa4b
|
|
229
|
-
// const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
|
|
230
|
-
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
|
|
231
|
-
// const testIdExpression = `[project=${project._internal.id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
|
|
232
|
-
// const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
|
|
233
|
-
|
|
234
|
-
const caseId = Util.calculateId(testCase.id);
|
|
235
|
-
|
|
236
|
-
const caseItem = {
|
|
237
|
-
id: caseId,
|
|
238
|
-
title: testCase.title,
|
|
239
|
-
type: 'case',
|
|
240
|
-
caseType: '',
|
|
241
|
-
|
|
242
|
-
// Whether the test is considered running fine. Non-ok tests fail the test run with non-zero exit code.
|
|
243
|
-
ok: testCase.ok(),
|
|
244
|
-
|
|
245
|
-
// Testing outcome for this test. Note that outcome is not the same as testResult.status:
|
|
246
|
-
// returns: <"skipped"|"expected"|"unexpected"|"flaky">
|
|
247
|
-
outcome: testCase.outcome(),
|
|
248
|
-
|
|
249
|
-
expectedStatus: testCase.expectedStatus,
|
|
250
|
-
location: this.locationHandler(testCase.location),
|
|
251
|
-
|
|
252
|
-
// custom collection
|
|
253
|
-
logs: testCase.logs,
|
|
254
|
-
timestamps: testCase.timestamps,
|
|
255
|
-
|
|
256
|
-
duration,
|
|
257
|
-
|
|
258
|
-
// annotations, string or array
|
|
259
|
-
annotations: this.getCaseAnnotations(testCase.annotations),
|
|
260
|
-
// new syntax in playwright v1.42
|
|
261
|
-
tags: testCase.tags,
|
|
262
|
-
|
|
263
|
-
// repeatEachIndex: testCase.repeatEachIndex,
|
|
264
|
-
|
|
265
|
-
// The maximum number of retries given to this test in the configuration
|
|
266
|
-
// retries: testCase.retries,
|
|
267
|
-
|
|
268
|
-
// The timeout given to the test.
|
|
269
|
-
// Affected by testConfig.timeout, testProject.timeout, test.setTimeout(timeout), test.slow() and testInfo.setTimeout(timeout).
|
|
270
|
-
timeout: testCase.timeout,
|
|
271
|
-
|
|
272
|
-
// ===============================================================
|
|
273
|
-
// merge all results (retry multiple times)
|
|
274
|
-
|
|
275
|
-
attachments: [],
|
|
276
|
-
|
|
277
|
-
// errors thrown during the test execution.
|
|
278
|
-
// error is first errors
|
|
279
|
-
errors: [],
|
|
280
|
-
|
|
281
|
-
retry: 0,
|
|
282
|
-
|
|
283
|
-
// <"passed"|"failed"|"timedOut"|"skipped">
|
|
284
|
-
status: '',
|
|
285
|
-
|
|
286
|
-
// all results steps
|
|
287
|
-
stepNum: 0,
|
|
288
|
-
stepFailed: 0,
|
|
289
|
-
stepSubs: false,
|
|
290
|
-
subs: []
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const resultsTimestamps = [].concat(testCase.timestamps);
|
|
294
|
-
|
|
295
|
-
for (const testResult of testCase.results) {
|
|
296
|
-
|
|
297
|
-
const retry = testResult.retry;
|
|
298
|
-
|
|
299
|
-
caseItem.retry = retry;
|
|
300
|
-
caseItem.status = testResult.status;
|
|
301
|
-
|
|
302
|
-
const attachments = this.initAttachmentsRetry(testResult.attachments, retry);
|
|
303
|
-
caseItem.attachments = caseItem.attachments.concat(attachments);
|
|
304
|
-
caseItem.errors = caseItem.errors.concat(testResult.errors);
|
|
305
|
-
|
|
306
|
-
// The worker index is used to reference a specific browser instance
|
|
307
|
-
// The parallel index coordinates the parallel execution of tests across multiple worker instances.
|
|
308
|
-
// https://playwright.dev/docs/test-parallel#worker-index-and-parallel-index
|
|
309
|
-
|
|
310
|
-
// result duration
|
|
311
|
-
const time_start = resultsTimestamps.shift();
|
|
312
|
-
const time_end = resultsTimestamps.shift();
|
|
313
|
-
const resultDuration = time_end - time_start;
|
|
314
|
-
|
|
315
|
-
// console.log(resultDuration, testResult.duration);
|
|
316
|
-
|
|
317
|
-
this.jobs.push({
|
|
318
|
-
caseId,
|
|
319
|
-
// worker
|
|
320
|
-
parallelIndex: testResult.parallelIndex,
|
|
321
|
-
// job
|
|
322
|
-
workerIndex: testResult.workerIndex,
|
|
323
|
-
timestamp: time_start,
|
|
324
|
-
duration: resultDuration
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
// concat all steps
|
|
328
|
-
if (caseItem.subs.length) {
|
|
329
|
-
caseItem.subs.push(this.getRetryStep(retry));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const steps = await this.testStepHandler(testResult.steps, caseItem);
|
|
333
|
-
|
|
334
|
-
caseItem.subs = caseItem.subs.concat(steps);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// 'passed', 'flaky', 'skipped', 'failed'
|
|
338
|
-
// after all required status in results
|
|
339
|
-
caseItem.caseType = this.getCaseType(caseItem);
|
|
340
|
-
|
|
341
|
-
// will no steps if someone skipped
|
|
342
|
-
if (!caseItem.subs.length) {
|
|
343
|
-
delete caseItem.subs;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
this.attachmentsHandler(caseItem, caseId);
|
|
347
|
-
this.caseErrorsHandler(caseItem);
|
|
348
|
-
|
|
349
|
-
await this.customVisitorsHandler(caseItem, testCase);
|
|
350
|
-
|
|
351
|
-
list.push(caseItem);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
getCaseType(item) {
|
|
355
|
-
// ok includes outcome === 'expected' || 'flaky' || 'skipped'
|
|
356
|
-
if (item.ok) {
|
|
357
|
-
if (item.outcome === 'skipped' || item.status === 'skipped') {
|
|
358
|
-
return 'skipped';
|
|
359
|
-
}
|
|
360
|
-
if (item.outcome === 'flaky') {
|
|
361
|
-
return 'flaky';
|
|
362
|
-
}
|
|
363
|
-
return 'passed';
|
|
364
|
-
}
|
|
365
|
-
return 'failed';
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
getCaseAnnotations(annotations) {
|
|
369
|
-
// array
|
|
370
|
-
if (Util.isList(annotations)) {
|
|
371
|
-
return annotations;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// string from comments
|
|
375
|
-
if (typeof annotations === 'string' && annotations) {
|
|
376
|
-
return annotations;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
initAttachmentsRetry(attachments, retry) {
|
|
382
|
-
attachments.forEach((item) => {
|
|
383
|
-
item.retry = retry;
|
|
384
|
-
});
|
|
385
|
-
return attachments;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ==============================================================================================
|
|
389
|
-
|
|
390
|
-
getRetryStep(retry) {
|
|
391
|
-
const stepId = Util.uid();
|
|
392
|
-
return {
|
|
393
|
-
id: stepId,
|
|
394
|
-
title: `Retry #${retry}`,
|
|
395
|
-
type: 'step',
|
|
396
|
-
stepType: 'retry',
|
|
397
|
-
// for retry color
|
|
398
|
-
status: 'retry',
|
|
399
|
-
retry
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async testStepHandler(steps, caseItem) {
|
|
404
|
-
|
|
405
|
-
const list = [];
|
|
406
|
-
|
|
407
|
-
for (const testStep of steps) {
|
|
408
|
-
|
|
409
|
-
// random id for report
|
|
410
|
-
const stepId = Util.uid();
|
|
411
|
-
|
|
412
|
-
const step = {
|
|
413
|
-
id: stepId,
|
|
414
|
-
title: testStep.title,
|
|
415
|
-
type: 'step',
|
|
416
|
-
stepType: testStep.category,
|
|
417
|
-
|
|
418
|
-
duration: testStep.duration,
|
|
419
|
-
location: this.locationHandler(testStep.location)
|
|
420
|
-
};
|
|
421
|
-
this.stepErrorsHandler(step, testStep, caseItem);
|
|
422
|
-
if (Util.isList(testStep.steps)) {
|
|
423
|
-
// console.log(testStep.title);
|
|
424
|
-
caseItem.stepSubs = true;
|
|
425
|
-
step.subs = await this.testStepHandler(testStep.steps, caseItem);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
await this.customVisitorsHandler(step, testStep);
|
|
429
|
-
|
|
430
|
-
caseItem.stepNum += 1;
|
|
431
|
-
list.push(step);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return this.dedupeSteps(list);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
dedupeSteps(stepList) {
|
|
438
|
-
if (stepList.length < 8) {
|
|
439
|
-
return stepList;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const diffProps = (step, lastStep, props) => {
|
|
443
|
-
for (const k of props) {
|
|
444
|
-
if (step[k] !== lastStep[k]) {
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return false;
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
const canMerge = (step, lastStep) => {
|
|
452
|
-
|
|
453
|
-
// stepType: category
|
|
454
|
-
// location is string now
|
|
455
|
-
if (diffProps(step, lastStep, ['title', 'stepType', 'location'])) {
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (lastStep.subs || lastStep.errors) {
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (step.subs || step.errors) {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return true;
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const list = [];
|
|
471
|
-
const endStep = stepList.reduce((lastStep, step) => {
|
|
472
|
-
if (canMerge(step, lastStep)) {
|
|
473
|
-
if (lastStep.count) {
|
|
474
|
-
lastStep.duration += step.duration;
|
|
475
|
-
lastStep.count += 1;
|
|
476
|
-
return lastStep;
|
|
477
|
-
}
|
|
478
|
-
const mergedStep = {
|
|
479
|
-
... lastStep,
|
|
480
|
-
duration: lastStep.duration + step.duration,
|
|
481
|
-
count: 2
|
|
482
|
-
};
|
|
483
|
-
// console.log(mergedStep);
|
|
484
|
-
return mergedStep;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
list.push(lastStep);
|
|
488
|
-
return step;
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
list.push(endStep);
|
|
492
|
-
|
|
493
|
-
return list;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
attachmentsHandler(caseItem, caseId) {
|
|
497
|
-
const attachments = caseItem.attachments;
|
|
498
|
-
delete caseItem.attachments;
|
|
499
|
-
if (!Util.isList(attachments)) {
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const title = caseItem.title;
|
|
504
|
-
|
|
505
|
-
const list = [];
|
|
506
|
-
|
|
507
|
-
attachments.forEach((item, i) => {
|
|
508
|
-
|
|
509
|
-
// metadata with body
|
|
510
|
-
if (this.testMetadataHandler(item, caseItem)) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// fixed expected image path
|
|
515
|
-
this.expectedAttachmentHandler(item, attachments);
|
|
516
|
-
|
|
517
|
-
if (item.body) {
|
|
518
|
-
if (!item.path) {
|
|
519
|
-
this.saveAttachmentBodyHandler(item, i, caseId);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// text attachment may no path
|
|
524
|
-
if (!item.path) {
|
|
525
|
-
list.push(item);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// before path change
|
|
530
|
-
// Attachments with a text/html content type can now be opened in a new tab in the HTML report.
|
|
531
|
-
// This is useful for including third-party reports or other HTML content in the Playwright test report and distributing it to your team.
|
|
532
|
-
this.reportHandler(item, 'audit', title);
|
|
533
|
-
this.reportHandler(item, 'coverage', title);
|
|
534
|
-
this.reportHandler(item, 'network', title);
|
|
535
|
-
|
|
536
|
-
// content: text
|
|
537
|
-
this.testContentHandler(item);
|
|
538
|
-
|
|
539
|
-
const o = this.options;
|
|
540
|
-
// store relative path first
|
|
541
|
-
this.copyAttachmentsHandler(item);
|
|
542
|
-
item.path = Util.relativePath(item.path, o.outputDir);
|
|
543
|
-
|
|
544
|
-
// custom attachment path
|
|
545
|
-
const attachmentPathHandler = o.attachmentPath;
|
|
546
|
-
if (typeof attachmentPathHandler === 'function') {
|
|
547
|
-
const extras = Util.getAttachmentPathExtras(o);
|
|
548
|
-
const newPath = attachmentPathHandler(item.path, extras);
|
|
549
|
-
// if forgot return new path
|
|
550
|
-
if (newPath) {
|
|
551
|
-
item.path = newPath;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
list.push(item);
|
|
556
|
-
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
if (list.length) {
|
|
560
|
-
caseItem.attachments = list;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
copyAttachmentsHandler(item) {
|
|
565
|
-
|
|
566
|
-
const { attachmentsDir } = this.options;
|
|
567
|
-
if (!attachmentsDir) {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (!fs.existsSync(attachmentsDir)) {
|
|
572
|
-
fs.mkdirSync(attachmentsDir, {
|
|
573
|
-
recursive: true
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// custom report
|
|
578
|
-
if (item.report) {
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const oldPath = item.path;
|
|
583
|
-
const filename = Util.calculateSha1(oldPath);
|
|
584
|
-
const ext = path.extname(oldPath);
|
|
585
|
-
const newPath = path.resolve(attachmentsDir, `${filename}${ext}`);
|
|
586
|
-
fs.cpSync(oldPath, newPath, {
|
|
587
|
-
force: true,
|
|
588
|
-
recursive: true
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
item.path = newPath;
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
getImageCategory(item) {
|
|
596
|
-
if (item.contentType && item.contentType.startsWith('image/')) {
|
|
597
|
-
if (item.name) {
|
|
598
|
-
const match = item.name.match(/^(.*)-(expected|actual|diff)(\.[^.]+)?$/);
|
|
599
|
-
if (match) {
|
|
600
|
-
// , name, category, extension
|
|
601
|
-
return match[2];
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
expectedAttachmentHandler(item, attachments) {
|
|
608
|
-
const category = this.getImageCategory(item);
|
|
609
|
-
if (category !== 'expected') {
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const actualItem = attachments.find((it) => {
|
|
614
|
-
if (it.retry !== item.retry) {
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
const c = this.getImageCategory(it);
|
|
618
|
-
if (c === 'actual') {
|
|
619
|
-
return true;
|
|
620
|
-
}
|
|
621
|
-
return false;
|
|
622
|
-
});
|
|
623
|
-
if (!actualItem) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// console.log(item, actualItem);
|
|
628
|
-
|
|
629
|
-
const itemDir = path.dirname(actualItem.path);
|
|
630
|
-
const itemPath = path.resolve(itemDir, item.name);
|
|
631
|
-
|
|
632
|
-
if (fs.existsSync(itemPath)) {
|
|
633
|
-
item.path = itemPath;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// no need copy the expected file, it exists
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
reportHandler(item, itemName, title) {
|
|
641
|
-
|
|
642
|
-
const definition = Util.attachments[itemName];
|
|
643
|
-
if (!definition) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (item.name !== definition.name || item.contentType !== definition.contentType) {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// itemName = item.name = definition.name
|
|
652
|
-
|
|
653
|
-
const jsonPath = path.resolve(path.dirname(item.path), definition.reportFile);
|
|
654
|
-
if (!fs.existsSync(jsonPath)) {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const report = Util.readJSONSync(jsonPath);
|
|
659
|
-
if (!report) {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
this.artifacts.push({
|
|
664
|
-
type: itemName,
|
|
665
|
-
name: report.name || title,
|
|
666
|
-
path: Util.relativePath(item.path)
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
item.report = report;
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
testMetadataHandler(item, caseItem) {
|
|
674
|
-
const definition = Util.attachments.metadata;
|
|
675
|
-
if (item.name !== definition.name || item.contentType !== definition.contentType) {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// console.log(item);
|
|
680
|
-
// name, path(undefined), contentType, body, retry
|
|
681
|
-
|
|
682
|
-
let content = item.body;
|
|
683
|
-
if (Buffer.isBuffer(content)) {
|
|
684
|
-
content = content.toString('utf8');
|
|
685
|
-
}
|
|
686
|
-
if (!content) {
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
let metadata;
|
|
691
|
-
try {
|
|
692
|
-
metadata = JSON.parse(content);
|
|
693
|
-
} catch (e) {
|
|
694
|
-
// invalid json format
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (!metadata) {
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
Object.assign(caseItem, metadata);
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
testContentHandler(item) {
|
|
706
|
-
if (item.content) {
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
if (Util.isTextType(item.contentType)) {
|
|
710
|
-
item.content = Util.readFileSync(item.path);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
contentToString(content) {
|
|
715
|
-
if (typeof content === 'string') {
|
|
716
|
-
return content;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (Buffer.isBuffer(content)) {
|
|
720
|
-
return content.toString('utf8');
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return content.toString();
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
saveAttachmentBodyHandler(item, i, caseId) {
|
|
727
|
-
|
|
728
|
-
const content = item.body;
|
|
729
|
-
delete item.body;
|
|
730
|
-
|
|
731
|
-
// if text content no need save file, just show the content
|
|
732
|
-
const contentType = item.contentType;
|
|
733
|
-
if (Util.isTextType(contentType)) {
|
|
734
|
-
// body is buffer
|
|
735
|
-
item.content = this.contentToString(content);
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// testOutputDir is for test results not reporter
|
|
740
|
-
const { outputDir, testOutputDir } = this.options;
|
|
741
|
-
|
|
742
|
-
const attachmentsPath = path.resolve(testOutputDir || outputDir, caseId);
|
|
743
|
-
if (!fs.existsSync(attachmentsPath)) {
|
|
744
|
-
fs.mkdirSync(attachmentsPath, {
|
|
745
|
-
recursive: true
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
750
|
-
const types = {
|
|
751
|
-
'text/plain': 'txt',
|
|
752
|
-
'application/octet-stream': 'bin'
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
const filename = sanitize(`${item.name}-${i + 1}`);
|
|
756
|
-
|
|
757
|
-
let ext = 'bin';
|
|
758
|
-
if (contentType) {
|
|
759
|
-
ext = types[contentType] || contentType.split('/').pop().slice(0, 4);
|
|
760
|
-
}
|
|
761
|
-
const filePath = path.resolve(attachmentsPath, `${filename}.${ext}`);
|
|
762
|
-
fs.writeFileSync(filePath, content);
|
|
763
|
-
item.path = filePath;
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
locationHandler(location) {
|
|
768
|
-
if (!location) {
|
|
769
|
-
return '';
|
|
770
|
-
}
|
|
771
|
-
const file = Util.relativePath(location.file);
|
|
772
|
-
return `${file}:${location.line}:${location.column}`;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// ==============================================================================================
|
|
776
|
-
|
|
777
|
-
caseErrorsHandler(caseItem) {
|
|
778
|
-
|
|
779
|
-
const errors = caseItem.errors;
|
|
780
|
-
if (Util.isList(errors)) {
|
|
781
|
-
caseItem.errors = this.errorsHandler(errors);
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// missed errors for unexpected
|
|
786
|
-
if (caseItem.outcome === 'unexpected') {
|
|
787
|
-
const error = {
|
|
788
|
-
message: EC.red(`Expected to "${caseItem.expectedStatus}", but "${caseItem.status}"`)
|
|
789
|
-
};
|
|
790
|
-
caseItem.errors = this.errorsHandler([error]);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
delete caseItem.errors;
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
stepErrorsHandler(step, testStep, caseItem) {
|
|
799
|
-
const error = testStep.error;
|
|
800
|
-
if (!error) {
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
caseItem.stepFailed += 1;
|
|
804
|
-
step.errors = this.errorsHandler([error]);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
errorsHandler(errors) {
|
|
808
|
-
return errors.map((err) => {
|
|
809
|
-
// error to string
|
|
810
|
-
err = err.stack || err.message || err.value || err;
|
|
811
|
-
if (typeof err === 'string') {
|
|
812
|
-
return err;
|
|
813
|
-
}
|
|
814
|
-
return `${err}`;
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// ==============================================================================================
|
|
819
|
-
|
|
820
|
-
duplicatedErrorsHandler(rows) {
|
|
821
|
-
|
|
822
|
-
Util.forEach(rows, (item) => {
|
|
823
|
-
if (!item.errors) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// for mark errors and sort by errors
|
|
828
|
-
item.errorNum = item.errors.length;
|
|
829
|
-
|
|
830
|
-
const errors = item.errors.filter((err) => {
|
|
831
|
-
const sub = this.findSubByError(item.subs, err);
|
|
832
|
-
if (sub) {
|
|
833
|
-
// keep first error id with last sub id
|
|
834
|
-
if (!item.errorId) {
|
|
835
|
-
item.errorId = sub.id;
|
|
836
|
-
}
|
|
837
|
-
return false;
|
|
838
|
-
}
|
|
839
|
-
return true;
|
|
840
|
-
});
|
|
841
|
-
if (errors.length) {
|
|
842
|
-
item.errors = this.errorsToSnippets(errors);
|
|
843
|
-
} else {
|
|
844
|
-
delete item.errors;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
findSubByError(subs, err) {
|
|
852
|
-
let sub;
|
|
853
|
-
Util.forEach(subs, (item) => {
|
|
854
|
-
if (item.errors) {
|
|
855
|
-
if (item.errors.find((e) => e === err)) {
|
|
856
|
-
sub = item;
|
|
857
|
-
// return false to break loop
|
|
858
|
-
return false;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
});
|
|
862
|
-
if (sub && sub.subs) {
|
|
863
|
-
const s = this.findSubByError(sub.subs, err);
|
|
864
|
-
if (s) {
|
|
865
|
-
return s;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return sub;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
errorsToSnippets(errors) {
|
|
872
|
-
return errors.map((err, i) => {
|
|
873
|
-
const lines = err.split('\n');
|
|
874
|
-
const firstStackLine = lines.findIndex((line) => line.trim().startsWith('at '));
|
|
875
|
-
if (firstStackLine === -1) {
|
|
876
|
-
return `${err}\n`;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const line = lines[firstStackLine];
|
|
880
|
-
|
|
881
|
-
const stackUtils = new StackUtils();
|
|
882
|
-
const location = stackUtils.parseLine(line);
|
|
883
|
-
if (!location) {
|
|
884
|
-
return err;
|
|
885
|
-
}
|
|
886
|
-
const file = location.file;
|
|
887
|
-
// may in anonymous script by addInitScript
|
|
888
|
-
// file: 'eval at evaluate (:195:30), <anonymous>',
|
|
889
|
-
if (!file || !fs.existsSync(file)) {
|
|
890
|
-
return err;
|
|
891
|
-
}
|
|
892
|
-
const source = fs.readFileSync(file, 'utf8');
|
|
893
|
-
const codeFrame = codeFrameColumns(source, {
|
|
894
|
-
start: location
|
|
895
|
-
}, {
|
|
896
|
-
highlightCode: true,
|
|
897
|
-
// forceColor: true
|
|
898
|
-
// linesAbove: 2,
|
|
899
|
-
linesBelow: 0
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
if (!codeFrame) {
|
|
903
|
-
return err;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
lines.splice(firstStackLine, 0, `\n${codeFrame}\n`);
|
|
907
|
-
|
|
908
|
-
// console.log(codeFrame);
|
|
909
|
-
return lines.join('\n');
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
module.exports = Visitor;
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EC = require('eight-colors');
|
|
4
|
+
const {
|
|
5
|
+
StackUtils, codeFrameColumns, sanitize
|
|
6
|
+
} = require('./packages/monocart-reporter-vendor.js');
|
|
7
|
+
const Util = require('./utils/util.js');
|
|
8
|
+
const commentsPlugin = require('./plugins/comments.js');
|
|
9
|
+
const getDefaultColumns = require('./default/columns.js');
|
|
10
|
+
|
|
11
|
+
class Visitor {
|
|
12
|
+
constructor(root, options) {
|
|
13
|
+
this.root = root;
|
|
14
|
+
this.options = options;
|
|
15
|
+
|
|
16
|
+
// console.log(options);
|
|
17
|
+
|
|
18
|
+
if (typeof options.visitor === 'function') {
|
|
19
|
+
this.customCommonVisitor = options.visitor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async start() {
|
|
25
|
+
|
|
26
|
+
const columns = getDefaultColumns();
|
|
27
|
+
// default columns not detailed in report
|
|
28
|
+
columns.forEach((item) => {
|
|
29
|
+
item.detailed = false;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// custom column formatters with string passed to JSON
|
|
33
|
+
this.formatters = {};
|
|
34
|
+
|
|
35
|
+
// user defined custom columns
|
|
36
|
+
const handler = this.options.columns;
|
|
37
|
+
if (!this.columnsUpdated && typeof handler === 'function') {
|
|
38
|
+
// prevent repeated execution
|
|
39
|
+
this.columnsUpdated = true;
|
|
40
|
+
|
|
41
|
+
// update default columns by user
|
|
42
|
+
handler.call(this, columns);
|
|
43
|
+
|
|
44
|
+
// maybe a tree
|
|
45
|
+
const customVisitors = [];
|
|
46
|
+
this.initCustomHandler(columns, customVisitors, this.formatters);
|
|
47
|
+
if (customVisitors.length) {
|
|
48
|
+
this.customVisitors = customVisitors;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// console.log(customFormatters);
|
|
53
|
+
|
|
54
|
+
this.columns = columns;
|
|
55
|
+
this.rows = [];
|
|
56
|
+
this.jobs = [];
|
|
57
|
+
this.artifacts = [];
|
|
58
|
+
|
|
59
|
+
await this.visit(this.root, this.rows);
|
|
60
|
+
|
|
61
|
+
this.duplicatedErrorsHandler(this.rows);
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ==============================================================================================
|
|
66
|
+
|
|
67
|
+
initCustomHandler(list, visitors, formatters) {
|
|
68
|
+
|
|
69
|
+
list.forEach((column) => {
|
|
70
|
+
if (column.id) {
|
|
71
|
+
|
|
72
|
+
// custom visitor
|
|
73
|
+
if (typeof column.visitor === 'function') {
|
|
74
|
+
|
|
75
|
+
visitors.push({
|
|
76
|
+
id: column.id,
|
|
77
|
+
visitor: column.visitor
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// remove function (can not be in JSON)
|
|
81
|
+
delete column.visitor;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// custom formatter
|
|
86
|
+
if (typeof column.formatter === 'function') {
|
|
87
|
+
|
|
88
|
+
formatters[column.id] = column.formatter.toString();
|
|
89
|
+
|
|
90
|
+
// remove function (can not be in JSON)
|
|
91
|
+
delete column.formatter;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// drill down
|
|
97
|
+
if (Util.isList(column.subs)) {
|
|
98
|
+
this.initCustomHandler(column.subs, visitors, formatters);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// generate the column data from playwright metadata
|
|
104
|
+
// data.type is suite, metadata is Suite, https://playwright.dev/docs/api/class-suite
|
|
105
|
+
// data.type is case, metadata is TestCase, https://playwright.dev/docs/api/class-testcase
|
|
106
|
+
// data.type is step, metadata is TestStep, https://playwright.dev/docs/api/class-teststep
|
|
107
|
+
async customVisitorsHandler(data, metadata) {
|
|
108
|
+
|
|
109
|
+
if (this.options.customFieldsInComments) {
|
|
110
|
+
const customData = commentsPlugin(metadata);
|
|
111
|
+
Object.assign(data, customData);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// for all data
|
|
115
|
+
if (this.customCommonVisitor) {
|
|
116
|
+
await this.customCommonVisitor.call(this, data, metadata);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// for single column data (high priority)
|
|
120
|
+
if (this.customVisitors) {
|
|
121
|
+
for (const item of this.customVisitors) {
|
|
122
|
+
const res = await item.visitor.call(this, data, metadata);
|
|
123
|
+
if (typeof res !== 'undefined') {
|
|
124
|
+
data[item.id] = res;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ==============================================================================================
|
|
131
|
+
|
|
132
|
+
async visit(suite, list) {
|
|
133
|
+
if (!suite._entries) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// suite -> tests/test case -> test result -> test step
|
|
137
|
+
for (const entry of suite._entries) {
|
|
138
|
+
// only case has results
|
|
139
|
+
if (entry.results) {
|
|
140
|
+
await this.testCaseHandler(entry, list);
|
|
141
|
+
} else {
|
|
142
|
+
await this.testSuiteHandler(entry, list);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==============================================================================================
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
Project suite #1. Has a child suite for each test file in the project.
|
|
151
|
+
File suite #1
|
|
152
|
+
TestCase #1
|
|
153
|
+
Suite corresponding to a test.describe(title, callback) group
|
|
154
|
+
TestCase #1 in a group
|
|
155
|
+
TestStep
|
|
156
|
+
*/
|
|
157
|
+
async testSuiteHandler(suite, list) {
|
|
158
|
+
|
|
159
|
+
// sometimes project title is empty
|
|
160
|
+
const suiteType = suite._type;
|
|
161
|
+
let suiteTitle = Util.formatPath(suite.title);
|
|
162
|
+
if (!suiteTitle) {
|
|
163
|
+
suiteTitle = suiteType;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// suite uid for report
|
|
167
|
+
const suiteStr = [suite._fileId].concat(suite.titlePath()).filter((it) => it).join(' ');
|
|
168
|
+
// console.log(suiteStr);
|
|
169
|
+
const suiteId = Util.calculateId(suiteStr);
|
|
170
|
+
|
|
171
|
+
const group = {
|
|
172
|
+
id: suiteId,
|
|
173
|
+
title: suiteTitle,
|
|
174
|
+
type: 'suite',
|
|
175
|
+
// root, project, file, describe
|
|
176
|
+
suiteType: suiteType,
|
|
177
|
+
// all test cases in this suite and its descendants
|
|
178
|
+
caseNum: suite.allTests().length,
|
|
179
|
+
subs: []
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (suiteType === 'project') {
|
|
183
|
+
this.projectMetadataHandler(group, suite);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (suite.location) {
|
|
187
|
+
group.location = this.locationHandler(suite.location);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.customVisitorsHandler(group, suite);
|
|
191
|
+
|
|
192
|
+
list.push(group);
|
|
193
|
+
// drill down
|
|
194
|
+
await this.visit(suite, group.subs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
projectMetadataHandler(project, suite) {
|
|
198
|
+
const sp = suite._fullProject;
|
|
199
|
+
if (!sp) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const projectMetadata = sp.project && sp.project.metadata;
|
|
204
|
+
if (!projectMetadata) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const config = sp.fullConfig && sp.fullConfig.config;
|
|
209
|
+
const configMetadata = config && config.metadata;
|
|
210
|
+
if (configMetadata && configMetadata === projectMetadata) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
project.metadata = projectMetadata;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ==============================================================================================
|
|
218
|
+
|
|
219
|
+
async testCaseHandler(testCase, list) {
|
|
220
|
+
|
|
221
|
+
// duration
|
|
222
|
+
// total of testResult.duration is not exact, it will cost time before/between/after result
|
|
223
|
+
const caseTimestamps = [].concat(testCase.timestamps);
|
|
224
|
+
const duration = caseTimestamps.pop() - caseTimestamps.shift();
|
|
225
|
+
|
|
226
|
+
// Unique test ID that is computed based on the test file name, test title and project name
|
|
227
|
+
|
|
228
|
+
// 6113402d7bc11a0fb7a9-281a9986cca0dfd6fa4b
|
|
229
|
+
// const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
|
|
230
|
+
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
|
|
231
|
+
// const testIdExpression = `[project=${project._internal.id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
|
|
232
|
+
// const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
|
|
233
|
+
|
|
234
|
+
const caseId = Util.calculateId(testCase.id);
|
|
235
|
+
|
|
236
|
+
const caseItem = {
|
|
237
|
+
id: caseId,
|
|
238
|
+
title: testCase.title,
|
|
239
|
+
type: 'case',
|
|
240
|
+
caseType: '',
|
|
241
|
+
|
|
242
|
+
// Whether the test is considered running fine. Non-ok tests fail the test run with non-zero exit code.
|
|
243
|
+
ok: testCase.ok(),
|
|
244
|
+
|
|
245
|
+
// Testing outcome for this test. Note that outcome is not the same as testResult.status:
|
|
246
|
+
// returns: <"skipped"|"expected"|"unexpected"|"flaky">
|
|
247
|
+
outcome: testCase.outcome(),
|
|
248
|
+
|
|
249
|
+
expectedStatus: testCase.expectedStatus,
|
|
250
|
+
location: this.locationHandler(testCase.location),
|
|
251
|
+
|
|
252
|
+
// custom collection
|
|
253
|
+
logs: testCase.logs,
|
|
254
|
+
timestamps: testCase.timestamps,
|
|
255
|
+
|
|
256
|
+
duration,
|
|
257
|
+
|
|
258
|
+
// annotations, string or array
|
|
259
|
+
annotations: this.getCaseAnnotations(testCase.annotations),
|
|
260
|
+
// new syntax in playwright v1.42
|
|
261
|
+
tags: testCase.tags,
|
|
262
|
+
|
|
263
|
+
// repeatEachIndex: testCase.repeatEachIndex,
|
|
264
|
+
|
|
265
|
+
// The maximum number of retries given to this test in the configuration
|
|
266
|
+
// retries: testCase.retries,
|
|
267
|
+
|
|
268
|
+
// The timeout given to the test.
|
|
269
|
+
// Affected by testConfig.timeout, testProject.timeout, test.setTimeout(timeout), test.slow() and testInfo.setTimeout(timeout).
|
|
270
|
+
timeout: testCase.timeout,
|
|
271
|
+
|
|
272
|
+
// ===============================================================
|
|
273
|
+
// merge all results (retry multiple times)
|
|
274
|
+
|
|
275
|
+
attachments: [],
|
|
276
|
+
|
|
277
|
+
// errors thrown during the test execution.
|
|
278
|
+
// error is first errors
|
|
279
|
+
errors: [],
|
|
280
|
+
|
|
281
|
+
retry: 0,
|
|
282
|
+
|
|
283
|
+
// <"passed"|"failed"|"timedOut"|"skipped">
|
|
284
|
+
status: '',
|
|
285
|
+
|
|
286
|
+
// all results steps
|
|
287
|
+
stepNum: 0,
|
|
288
|
+
stepFailed: 0,
|
|
289
|
+
stepSubs: false,
|
|
290
|
+
subs: []
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const resultsTimestamps = [].concat(testCase.timestamps);
|
|
294
|
+
|
|
295
|
+
for (const testResult of testCase.results) {
|
|
296
|
+
|
|
297
|
+
const retry = testResult.retry;
|
|
298
|
+
|
|
299
|
+
caseItem.retry = retry;
|
|
300
|
+
caseItem.status = testResult.status;
|
|
301
|
+
|
|
302
|
+
const attachments = this.initAttachmentsRetry(testResult.attachments, retry);
|
|
303
|
+
caseItem.attachments = caseItem.attachments.concat(attachments);
|
|
304
|
+
caseItem.errors = caseItem.errors.concat(testResult.errors);
|
|
305
|
+
|
|
306
|
+
// The worker index is used to reference a specific browser instance
|
|
307
|
+
// The parallel index coordinates the parallel execution of tests across multiple worker instances.
|
|
308
|
+
// https://playwright.dev/docs/test-parallel#worker-index-and-parallel-index
|
|
309
|
+
|
|
310
|
+
// result duration
|
|
311
|
+
const time_start = resultsTimestamps.shift();
|
|
312
|
+
const time_end = resultsTimestamps.shift();
|
|
313
|
+
const resultDuration = time_end - time_start;
|
|
314
|
+
|
|
315
|
+
// console.log(resultDuration, testResult.duration);
|
|
316
|
+
|
|
317
|
+
this.jobs.push({
|
|
318
|
+
caseId,
|
|
319
|
+
// worker
|
|
320
|
+
parallelIndex: testResult.parallelIndex,
|
|
321
|
+
// job
|
|
322
|
+
workerIndex: testResult.workerIndex,
|
|
323
|
+
timestamp: time_start,
|
|
324
|
+
duration: resultDuration
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// concat all steps
|
|
328
|
+
if (caseItem.subs.length) {
|
|
329
|
+
caseItem.subs.push(this.getRetryStep(retry));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const steps = await this.testStepHandler(testResult.steps, caseItem);
|
|
333
|
+
|
|
334
|
+
caseItem.subs = caseItem.subs.concat(steps);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 'passed', 'flaky', 'skipped', 'failed'
|
|
338
|
+
// after all required status in results
|
|
339
|
+
caseItem.caseType = this.getCaseType(caseItem);
|
|
340
|
+
|
|
341
|
+
// will no steps if someone skipped
|
|
342
|
+
if (!caseItem.subs.length) {
|
|
343
|
+
delete caseItem.subs;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.attachmentsHandler(caseItem, caseId);
|
|
347
|
+
this.caseErrorsHandler(caseItem);
|
|
348
|
+
|
|
349
|
+
await this.customVisitorsHandler(caseItem, testCase);
|
|
350
|
+
|
|
351
|
+
list.push(caseItem);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getCaseType(item) {
|
|
355
|
+
// ok includes outcome === 'expected' || 'flaky' || 'skipped'
|
|
356
|
+
if (item.ok) {
|
|
357
|
+
if (item.outcome === 'skipped' || item.status === 'skipped') {
|
|
358
|
+
return 'skipped';
|
|
359
|
+
}
|
|
360
|
+
if (item.outcome === 'flaky') {
|
|
361
|
+
return 'flaky';
|
|
362
|
+
}
|
|
363
|
+
return 'passed';
|
|
364
|
+
}
|
|
365
|
+
return 'failed';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getCaseAnnotations(annotations) {
|
|
369
|
+
// array
|
|
370
|
+
if (Util.isList(annotations)) {
|
|
371
|
+
return annotations;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// string from comments
|
|
375
|
+
if (typeof annotations === 'string' && annotations) {
|
|
376
|
+
return annotations;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
initAttachmentsRetry(attachments, retry) {
|
|
382
|
+
attachments.forEach((item) => {
|
|
383
|
+
item.retry = retry;
|
|
384
|
+
});
|
|
385
|
+
return attachments;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ==============================================================================================
|
|
389
|
+
|
|
390
|
+
getRetryStep(retry) {
|
|
391
|
+
const stepId = Util.uid();
|
|
392
|
+
return {
|
|
393
|
+
id: stepId,
|
|
394
|
+
title: `Retry #${retry}`,
|
|
395
|
+
type: 'step',
|
|
396
|
+
stepType: 'retry',
|
|
397
|
+
// for retry color
|
|
398
|
+
status: 'retry',
|
|
399
|
+
retry
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async testStepHandler(steps, caseItem) {
|
|
404
|
+
|
|
405
|
+
const list = [];
|
|
406
|
+
|
|
407
|
+
for (const testStep of steps) {
|
|
408
|
+
|
|
409
|
+
// random id for report
|
|
410
|
+
const stepId = Util.uid();
|
|
411
|
+
|
|
412
|
+
const step = {
|
|
413
|
+
id: stepId,
|
|
414
|
+
title: testStep.title,
|
|
415
|
+
type: 'step',
|
|
416
|
+
stepType: testStep.category,
|
|
417
|
+
|
|
418
|
+
duration: testStep.duration,
|
|
419
|
+
location: this.locationHandler(testStep.location)
|
|
420
|
+
};
|
|
421
|
+
this.stepErrorsHandler(step, testStep, caseItem);
|
|
422
|
+
if (Util.isList(testStep.steps)) {
|
|
423
|
+
// console.log(testStep.title);
|
|
424
|
+
caseItem.stepSubs = true;
|
|
425
|
+
step.subs = await this.testStepHandler(testStep.steps, caseItem);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await this.customVisitorsHandler(step, testStep);
|
|
429
|
+
|
|
430
|
+
caseItem.stepNum += 1;
|
|
431
|
+
list.push(step);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return this.dedupeSteps(list);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
dedupeSteps(stepList) {
|
|
438
|
+
if (stepList.length < 8) {
|
|
439
|
+
return stepList;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const diffProps = (step, lastStep, props) => {
|
|
443
|
+
for (const k of props) {
|
|
444
|
+
if (step[k] !== lastStep[k]) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const canMerge = (step, lastStep) => {
|
|
452
|
+
|
|
453
|
+
// stepType: category
|
|
454
|
+
// location is string now
|
|
455
|
+
if (diffProps(step, lastStep, ['title', 'stepType', 'location'])) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (lastStep.subs || lastStep.errors) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (step.subs || step.errors) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return true;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const list = [];
|
|
471
|
+
const endStep = stepList.reduce((lastStep, step) => {
|
|
472
|
+
if (canMerge(step, lastStep)) {
|
|
473
|
+
if (lastStep.count) {
|
|
474
|
+
lastStep.duration += step.duration;
|
|
475
|
+
lastStep.count += 1;
|
|
476
|
+
return lastStep;
|
|
477
|
+
}
|
|
478
|
+
const mergedStep = {
|
|
479
|
+
... lastStep,
|
|
480
|
+
duration: lastStep.duration + step.duration,
|
|
481
|
+
count: 2
|
|
482
|
+
};
|
|
483
|
+
// console.log(mergedStep);
|
|
484
|
+
return mergedStep;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
list.push(lastStep);
|
|
488
|
+
return step;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
list.push(endStep);
|
|
492
|
+
|
|
493
|
+
return list;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
attachmentsHandler(caseItem, caseId) {
|
|
497
|
+
const attachments = caseItem.attachments;
|
|
498
|
+
delete caseItem.attachments;
|
|
499
|
+
if (!Util.isList(attachments)) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const title = caseItem.title;
|
|
504
|
+
|
|
505
|
+
const list = [];
|
|
506
|
+
|
|
507
|
+
attachments.forEach((item, i) => {
|
|
508
|
+
|
|
509
|
+
// metadata with body
|
|
510
|
+
if (this.testMetadataHandler(item, caseItem)) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// fixed expected image path
|
|
515
|
+
this.expectedAttachmentHandler(item, attachments);
|
|
516
|
+
|
|
517
|
+
if (item.body) {
|
|
518
|
+
if (!item.path) {
|
|
519
|
+
this.saveAttachmentBodyHandler(item, i, caseId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// text attachment may no path
|
|
524
|
+
if (!item.path) {
|
|
525
|
+
list.push(item);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// before path change
|
|
530
|
+
// Attachments with a text/html content type can now be opened in a new tab in the HTML report.
|
|
531
|
+
// This is useful for including third-party reports or other HTML content in the Playwright test report and distributing it to your team.
|
|
532
|
+
this.reportHandler(item, 'audit', title);
|
|
533
|
+
this.reportHandler(item, 'coverage', title);
|
|
534
|
+
this.reportHandler(item, 'network', title);
|
|
535
|
+
|
|
536
|
+
// content: text
|
|
537
|
+
this.testContentHandler(item);
|
|
538
|
+
|
|
539
|
+
const o = this.options;
|
|
540
|
+
// store relative path first
|
|
541
|
+
this.copyAttachmentsHandler(item);
|
|
542
|
+
item.path = Util.relativePath(item.path, o.outputDir);
|
|
543
|
+
|
|
544
|
+
// custom attachment path
|
|
545
|
+
const attachmentPathHandler = o.attachmentPath;
|
|
546
|
+
if (typeof attachmentPathHandler === 'function') {
|
|
547
|
+
const extras = Util.getAttachmentPathExtras(o);
|
|
548
|
+
const newPath = attachmentPathHandler(item.path, extras);
|
|
549
|
+
// if forgot return new path
|
|
550
|
+
if (newPath) {
|
|
551
|
+
item.path = newPath;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
list.push(item);
|
|
556
|
+
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (list.length) {
|
|
560
|
+
caseItem.attachments = list;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
copyAttachmentsHandler(item) {
|
|
565
|
+
|
|
566
|
+
const { attachmentsDir } = this.options;
|
|
567
|
+
if (!attachmentsDir) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!fs.existsSync(attachmentsDir)) {
|
|
572
|
+
fs.mkdirSync(attachmentsDir, {
|
|
573
|
+
recursive: true
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// custom report
|
|
578
|
+
if (item.report) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const oldPath = item.path;
|
|
583
|
+
const filename = Util.calculateSha1(oldPath);
|
|
584
|
+
const ext = path.extname(oldPath);
|
|
585
|
+
const newPath = path.resolve(attachmentsDir, `${filename}${ext}`);
|
|
586
|
+
fs.cpSync(oldPath, newPath, {
|
|
587
|
+
force: true,
|
|
588
|
+
recursive: true
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
item.path = newPath;
|
|
592
|
+
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
getImageCategory(item) {
|
|
596
|
+
if (item.contentType && item.contentType.startsWith('image/')) {
|
|
597
|
+
if (item.name) {
|
|
598
|
+
const match = item.name.match(/^(.*)-(expected|actual|diff)(\.[^.]+)?$/);
|
|
599
|
+
if (match) {
|
|
600
|
+
// , name, category, extension
|
|
601
|
+
return match[2];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
expectedAttachmentHandler(item, attachments) {
|
|
608
|
+
const category = this.getImageCategory(item);
|
|
609
|
+
if (category !== 'expected') {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const actualItem = attachments.find((it) => {
|
|
614
|
+
if (it.retry !== item.retry) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
const c = this.getImageCategory(it);
|
|
618
|
+
if (c === 'actual') {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
});
|
|
623
|
+
if (!actualItem) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// console.log(item, actualItem);
|
|
628
|
+
|
|
629
|
+
const itemDir = path.dirname(actualItem.path);
|
|
630
|
+
const itemPath = path.resolve(itemDir, item.name);
|
|
631
|
+
|
|
632
|
+
if (fs.existsSync(itemPath)) {
|
|
633
|
+
item.path = itemPath;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// no need copy the expected file, it exists
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
reportHandler(item, itemName, title) {
|
|
641
|
+
|
|
642
|
+
const definition = Util.attachments[itemName];
|
|
643
|
+
if (!definition) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (item.name !== definition.name || item.contentType !== definition.contentType) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// itemName = item.name = definition.name
|
|
652
|
+
|
|
653
|
+
const jsonPath = path.resolve(path.dirname(item.path), definition.reportFile);
|
|
654
|
+
if (!fs.existsSync(jsonPath)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const report = Util.readJSONSync(jsonPath);
|
|
659
|
+
if (!report) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.artifacts.push({
|
|
664
|
+
type: itemName,
|
|
665
|
+
name: report.name || title,
|
|
666
|
+
path: Util.relativePath(item.path)
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
item.report = report;
|
|
670
|
+
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
testMetadataHandler(item, caseItem) {
|
|
674
|
+
const definition = Util.attachments.metadata;
|
|
675
|
+
if (item.name !== definition.name || item.contentType !== definition.contentType) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// console.log(item);
|
|
680
|
+
// name, path(undefined), contentType, body, retry
|
|
681
|
+
|
|
682
|
+
let content = item.body;
|
|
683
|
+
if (Buffer.isBuffer(content)) {
|
|
684
|
+
content = content.toString('utf8');
|
|
685
|
+
}
|
|
686
|
+
if (!content) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let metadata;
|
|
691
|
+
try {
|
|
692
|
+
metadata = JSON.parse(content);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
// invalid json format
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (!metadata) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
Object.assign(caseItem, metadata);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
testContentHandler(item) {
|
|
706
|
+
if (item.content) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (Util.isTextType(item.contentType)) {
|
|
710
|
+
item.content = Util.readFileSync(item.path);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
contentToString(content) {
|
|
715
|
+
if (typeof content === 'string') {
|
|
716
|
+
return content;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (Buffer.isBuffer(content)) {
|
|
720
|
+
return content.toString('utf8');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return content.toString();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
saveAttachmentBodyHandler(item, i, caseId) {
|
|
727
|
+
|
|
728
|
+
const content = item.body;
|
|
729
|
+
delete item.body;
|
|
730
|
+
|
|
731
|
+
// if text content no need save file, just show the content
|
|
732
|
+
const contentType = item.contentType;
|
|
733
|
+
if (Util.isTextType(contentType)) {
|
|
734
|
+
// body is buffer
|
|
735
|
+
item.content = this.contentToString(content);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// testOutputDir is for test results not reporter
|
|
740
|
+
const { outputDir, testOutputDir } = this.options;
|
|
741
|
+
|
|
742
|
+
const attachmentsPath = path.resolve(testOutputDir || outputDir, caseId);
|
|
743
|
+
if (!fs.existsSync(attachmentsPath)) {
|
|
744
|
+
fs.mkdirSync(attachmentsPath, {
|
|
745
|
+
recursive: true
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
750
|
+
const types = {
|
|
751
|
+
'text/plain': 'txt',
|
|
752
|
+
'application/octet-stream': 'bin'
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const filename = sanitize(`${item.name}-${i + 1}`);
|
|
756
|
+
|
|
757
|
+
let ext = 'bin';
|
|
758
|
+
if (contentType) {
|
|
759
|
+
ext = types[contentType] || contentType.split('/').pop().slice(0, 4);
|
|
760
|
+
}
|
|
761
|
+
const filePath = path.resolve(attachmentsPath, `${filename}.${ext}`);
|
|
762
|
+
fs.writeFileSync(filePath, content);
|
|
763
|
+
item.path = filePath;
|
|
764
|
+
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
locationHandler(location) {
|
|
768
|
+
if (!location) {
|
|
769
|
+
return '';
|
|
770
|
+
}
|
|
771
|
+
const file = Util.relativePath(location.file);
|
|
772
|
+
return `${file}:${location.line}:${location.column}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ==============================================================================================
|
|
776
|
+
|
|
777
|
+
caseErrorsHandler(caseItem) {
|
|
778
|
+
|
|
779
|
+
const errors = caseItem.errors;
|
|
780
|
+
if (Util.isList(errors)) {
|
|
781
|
+
caseItem.errors = this.errorsHandler(errors);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// missed errors for unexpected
|
|
786
|
+
if (caseItem.outcome === 'unexpected') {
|
|
787
|
+
const error = {
|
|
788
|
+
message: EC.red(`Expected to "${caseItem.expectedStatus}", but "${caseItem.status}"`)
|
|
789
|
+
};
|
|
790
|
+
caseItem.errors = this.errorsHandler([error]);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
delete caseItem.errors;
|
|
795
|
+
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
stepErrorsHandler(step, testStep, caseItem) {
|
|
799
|
+
const error = testStep.error;
|
|
800
|
+
if (!error) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
caseItem.stepFailed += 1;
|
|
804
|
+
step.errors = this.errorsHandler([error]);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
errorsHandler(errors) {
|
|
808
|
+
return errors.map((err) => {
|
|
809
|
+
// error to string
|
|
810
|
+
err = err.stack || err.message || err.value || err;
|
|
811
|
+
if (typeof err === 'string') {
|
|
812
|
+
return err;
|
|
813
|
+
}
|
|
814
|
+
return `${err}`;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ==============================================================================================
|
|
819
|
+
|
|
820
|
+
duplicatedErrorsHandler(rows) {
|
|
821
|
+
|
|
822
|
+
Util.forEach(rows, (item) => {
|
|
823
|
+
if (!item.errors) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// for mark errors and sort by errors
|
|
828
|
+
item.errorNum = item.errors.length;
|
|
829
|
+
|
|
830
|
+
const errors = item.errors.filter((err) => {
|
|
831
|
+
const sub = this.findSubByError(item.subs, err);
|
|
832
|
+
if (sub) {
|
|
833
|
+
// keep first error id with last sub id
|
|
834
|
+
if (!item.errorId) {
|
|
835
|
+
item.errorId = sub.id;
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
return true;
|
|
840
|
+
});
|
|
841
|
+
if (errors.length) {
|
|
842
|
+
item.errors = this.errorsToSnippets(errors);
|
|
843
|
+
} else {
|
|
844
|
+
delete item.errors;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
findSubByError(subs, err) {
|
|
852
|
+
let sub;
|
|
853
|
+
Util.forEach(subs, (item) => {
|
|
854
|
+
if (item.errors) {
|
|
855
|
+
if (item.errors.find((e) => e === err)) {
|
|
856
|
+
sub = item;
|
|
857
|
+
// return false to break loop
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
if (sub && sub.subs) {
|
|
863
|
+
const s = this.findSubByError(sub.subs, err);
|
|
864
|
+
if (s) {
|
|
865
|
+
return s;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return sub;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
errorsToSnippets(errors) {
|
|
872
|
+
return errors.map((err, i) => {
|
|
873
|
+
const lines = err.split('\n');
|
|
874
|
+
const firstStackLine = lines.findIndex((line) => line.trim().startsWith('at '));
|
|
875
|
+
if (firstStackLine === -1) {
|
|
876
|
+
return `${err}\n`;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const line = lines[firstStackLine];
|
|
880
|
+
|
|
881
|
+
const stackUtils = new StackUtils();
|
|
882
|
+
const location = stackUtils.parseLine(line);
|
|
883
|
+
if (!location) {
|
|
884
|
+
return err;
|
|
885
|
+
}
|
|
886
|
+
const file = location.file;
|
|
887
|
+
// may in anonymous script by addInitScript
|
|
888
|
+
// file: 'eval at evaluate (:195:30), <anonymous>',
|
|
889
|
+
if (!file || !fs.existsSync(file)) {
|
|
890
|
+
return err;
|
|
891
|
+
}
|
|
892
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
893
|
+
const codeFrame = codeFrameColumns(source, {
|
|
894
|
+
start: location
|
|
895
|
+
}, {
|
|
896
|
+
highlightCode: true,
|
|
897
|
+
// forceColor: true
|
|
898
|
+
// linesAbove: 2,
|
|
899
|
+
linesBelow: 0
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
if (!codeFrame) {
|
|
903
|
+
return err;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
lines.splice(firstStackLine, 0, `\n${codeFrame}\n`);
|
|
907
|
+
|
|
908
|
+
// console.log(codeFrame);
|
|
909
|
+
return lines.join('\n');
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
module.exports = Visitor;
|