monocart-reporter 2.9.6 → 2.9.7

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/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;