@testomatio/reporter 2.7.4-beta.allure-1 → 2.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/lib/adapter/vitest.d.ts +22 -3
- package/lib/adapter/vitest.js +250 -26
- package/lib/bin/cli.js +0 -28
- package/lib/client.d.ts +2 -1
- package/lib/client.js +3 -2
- package/lib/junit-adapter/index.js +0 -4
- package/lib/pipe/bitbucket.js +16 -1
- package/lib/pipe/html.js +24 -5
- package/lib/template/testomatio.hbs +131 -20
- package/lib/utils/utils.js +0 -9
- package/lib/xmlReader.js +7 -5
- package/package.json +1 -1
- package/src/adapter/vitest.js +253 -26
- package/src/bin/cli.js +0 -35
- package/src/client.js +3 -2
- package/src/junit-adapter/index.js +0 -4
- package/src/pipe/bitbucket.js +23 -1
- package/src/pipe/html.js +24 -5
- package/src/template/testomatio.hbs +131 -20
- package/src/utils/utils.js +0 -5
- package/src/xmlReader.js +9 -6
- package/lib/allureReader.d.ts +0 -65
- package/lib/allureReader.js +0 -461
- package/lib/junit-adapter/kotlin.d.ts +0 -5
- package/lib/junit-adapter/kotlin.js +0 -46
- package/src/allureReader.js +0 -540
- package/src/junit-adapter/kotlin.js +0 -48
|
@@ -741,6 +741,18 @@
|
|
|
741
741
|
min-width: 0;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
.step-header {
|
|
745
|
+
display: flex;
|
|
746
|
+
align-items: center;
|
|
747
|
+
gap: 12px;
|
|
748
|
+
width: 100%;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.step-main {
|
|
752
|
+
flex: 1;
|
|
753
|
+
min-width: 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
744
756
|
.step-text {
|
|
745
757
|
font-size: 14px;
|
|
746
758
|
color: var(--gray-700);
|
|
@@ -755,6 +767,35 @@
|
|
|
755
767
|
margin-top: 4px;
|
|
756
768
|
}
|
|
757
769
|
|
|
770
|
+
.step-toggle {
|
|
771
|
+
width: 24px;
|
|
772
|
+
height: 24px;
|
|
773
|
+
border: none;
|
|
774
|
+
border-radius: 6px;
|
|
775
|
+
background: var(--gray-100);
|
|
776
|
+
color: var(--gray-600);
|
|
777
|
+
display: inline-flex;
|
|
778
|
+
align-items: center;
|
|
779
|
+
justify-content: center;
|
|
780
|
+
cursor: pointer;
|
|
781
|
+
flex-shrink: 0;
|
|
782
|
+
transition: var(--transition);
|
|
783
|
+
margin-top: 2px;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.step-toggle:hover {
|
|
787
|
+
background: var(--gray-200);
|
|
788
|
+
color: var(--gray-700);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.step-toggle.collapsed i {
|
|
792
|
+
transform: rotate(-90deg);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.step-toggle i {
|
|
796
|
+
transition: transform 0.2s ease;
|
|
797
|
+
}
|
|
798
|
+
|
|
758
799
|
.step-status {
|
|
759
800
|
width: 8px;
|
|
760
801
|
height: 8px;
|
|
@@ -790,6 +831,10 @@
|
|
|
790
831
|
padding-left: 0;
|
|
791
832
|
}
|
|
792
833
|
|
|
834
|
+
.step-children.collapsed {
|
|
835
|
+
display: none;
|
|
836
|
+
}
|
|
837
|
+
|
|
793
838
|
.step-children::before {
|
|
794
839
|
content: '';
|
|
795
840
|
position: absolute;
|
|
@@ -867,6 +912,30 @@
|
|
|
867
912
|
border-left-color: var(--info-color);
|
|
868
913
|
}
|
|
869
914
|
|
|
915
|
+
.message-block.passed {
|
|
916
|
+
border-left-color: var(--success-color);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.message-block.skipped,
|
|
920
|
+
.message-block.todo {
|
|
921
|
+
border-left-color: var(--warning-color);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.message-block.failed {
|
|
925
|
+
border-left-color: var(--danger-color);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.step-attachments {
|
|
929
|
+
display: grid;
|
|
930
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
931
|
+
gap: 12px;
|
|
932
|
+
margin-top: 12px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.step-attachments .attachment-item {
|
|
936
|
+
min-height: 120px;
|
|
937
|
+
}
|
|
938
|
+
|
|
870
939
|
.metadata-grid {
|
|
871
940
|
display: grid;
|
|
872
941
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
@@ -2377,6 +2446,11 @@
|
|
|
2377
2446
|
const isTodo = statusClass === 'todo';
|
|
2378
2447
|
const isSkippedOrTodo = statusClass === 'skipped' || statusClass === 'todo';
|
|
2379
2448
|
const displayTime = isSkippedOrTodo ? '-' : formatDuration(test.run_time || 0);
|
|
2449
|
+
const hasMessage = hasMeaningfulMessage(test.message);
|
|
2450
|
+
const initialTab = getInitialTestTab({ isTodo, hasMessage, hasSteps: test.stepsArray?.length || test.steps });
|
|
2451
|
+
const initialMessageClass = initialTab === 'message' ? ' active' : '';
|
|
2452
|
+
const initialStepsClass = initialTab === 'steps' ? ' active' : '';
|
|
2453
|
+
const initialInfoClass = initialTab === 'info' ? ' active' : '';
|
|
2380
2454
|
|
|
2381
2455
|
testItem.innerHTML = `
|
|
2382
2456
|
<div class='test-header' onclick='toggleTest(this)'>
|
|
@@ -2411,14 +2485,14 @@
|
|
|
2411
2485
|
<div class='test-content'>
|
|
2412
2486
|
<div class='test-tabs'>
|
|
2413
2487
|
${isTodo ? `
|
|
2414
|
-
<button class='test-tab
|
|
2488
|
+
<button class='test-tab${initialInfoClass}' onclick='showTestTab(this, \"info\")'>
|
|
2415
2489
|
<i class='fas fa-info-circle'></i> Info
|
|
2416
2490
|
</button>
|
|
2417
2491
|
` : `
|
|
2418
|
-
<button class='test-tab
|
|
2492
|
+
<button class='test-tab${initialStepsClass}' onclick='showTestTab(this, \"steps\")'>
|
|
2419
2493
|
<i class='fas fa-list-ol'></i> Steps
|
|
2420
2494
|
</button>
|
|
2421
|
-
<button class='test-tab' onclick='showTestTab(this, \"message\")'>
|
|
2495
|
+
<button class='test-tab${initialMessageClass}' onclick='showTestTab(this, \"message\")'>
|
|
2422
2496
|
<i class='fas fa-comment-dots'></i> Message
|
|
2423
2497
|
</button>
|
|
2424
2498
|
${hasStack ? `<button class='test-tab' onclick='showTestTab(this, \"stack\")'>
|
|
@@ -2439,7 +2513,7 @@
|
|
|
2439
2513
|
`}
|
|
2440
2514
|
</div>
|
|
2441
2515
|
${isTodo ? `
|
|
2442
|
-
<div class='test-tab-content
|
|
2516
|
+
<div class='test-tab-content${initialInfoClass}' data-tab='info'>
|
|
2443
2517
|
<div class='todo-info'>
|
|
2444
2518
|
<div class='todo-title'>
|
|
2445
2519
|
<i class='fas fa-list-ul'></i>
|
|
@@ -2456,7 +2530,7 @@
|
|
|
2456
2530
|
</div>
|
|
2457
2531
|
</div>
|
|
2458
2532
|
` : `
|
|
2459
|
-
<div class='test-tab-content
|
|
2533
|
+
<div class='test-tab-content${initialStepsClass}' data-tab='steps'>
|
|
2460
2534
|
<div class='steps-container' id='steps-${index}'>
|
|
2461
2535
|
${(() => {
|
|
2462
2536
|
const hasStepsArray = test.stepsArray && Array.isArray(test.stepsArray) && test.stepsArray.length > 0;
|
|
@@ -2476,7 +2550,7 @@
|
|
|
2476
2550
|
})()}
|
|
2477
2551
|
</div>
|
|
2478
2552
|
</div>
|
|
2479
|
-
<div class='test-tab-content' data-tab='message'>
|
|
2553
|
+
<div class='test-tab-content${initialMessageClass}' data-tab='message'>
|
|
2480
2554
|
<div class='message-block ${statusClass}'>
|
|
2481
2555
|
<strong>${statusClass.toUpperCase()}:</strong><br>
|
|
2482
2556
|
${test.message || 'No message available'}
|
|
@@ -2542,6 +2616,16 @@
|
|
|
2542
2616
|
}
|
|
2543
2617
|
}
|
|
2544
2618
|
|
|
2619
|
+
function hasMeaningfulMessage(message) {
|
|
2620
|
+
return typeof message === 'string' && message.trim().length > 0 && message !== 'No message available';
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
function getInitialTestTab({ isTodo, hasMessage }) {
|
|
2624
|
+
if (isTodo) return 'info';
|
|
2625
|
+
if (hasMessage) return 'message';
|
|
2626
|
+
return 'steps';
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2545
2629
|
function parseStepsToHtml(stepsText) {
|
|
2546
2630
|
if (!stepsText || stepsText === 'No steps recorded') {
|
|
2547
2631
|
return `<div class="step-item">
|
|
@@ -2629,25 +2713,33 @@
|
|
|
2629
2713
|
const status = step.status || 'passed';
|
|
2630
2714
|
const duration = step.duration || 0;
|
|
2631
2715
|
const category = step.category || '';
|
|
2716
|
+
const hasArtifacts = Array.isArray(step.artifacts) && step.artifacts.length > 0;
|
|
2632
2717
|
|
|
2633
2718
|
html += `
|
|
2634
2719
|
<div class="step-item step-level-${depth}" data-step-id="${stepId}">
|
|
2635
|
-
<div class="step-number">${stepNumber}</div>
|
|
2636
2720
|
<div class="step-content">
|
|
2637
|
-
<
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2721
|
+
<div class="step-header">
|
|
2722
|
+
<div class="step-number">${stepNumber}</div>
|
|
2723
|
+
<div class="step-main">
|
|
2724
|
+
<p class="step-text">${step.title || 'Untitled step'}</p>
|
|
2725
|
+
${category && category !== 'user' ? `<div class="step-category">${category}</div>` : ''}
|
|
2726
|
+
${duration > 0 ? `<div class="step-time"><span class="step-duration"><i class="fas fa-clock"></i> ${formatDuration(duration)}</span></div>` : ''}
|
|
2727
|
+
${hasArtifacts ? `<div class="step-attachments">${createAttachmentItems(step.artifacts)}</div>` : ''}
|
|
2728
|
+
</div>
|
|
2729
|
+
${hasChildren ? `
|
|
2730
|
+
<button class="step-toggle" type="button" aria-expanded="true" onclick="toggleStepChildren(this)">
|
|
2731
|
+
<i class="fas fa-chevron-down"></i>
|
|
2732
|
+
</button>
|
|
2733
|
+
` : ''}
|
|
2734
|
+
<div class="step-status ${status}"></div>
|
|
2642
2735
|
</div>
|
|
2643
2736
|
</div>
|
|
2644
|
-
<div class="step-status ${status}"></div>
|
|
2645
2737
|
</div>
|
|
2646
2738
|
`;
|
|
2647
2739
|
|
|
2648
2740
|
if (hasChildren) {
|
|
2649
2741
|
html += `
|
|
2650
|
-
<div class="step-children">
|
|
2742
|
+
<div class="step-children" data-parent-step-id="${stepId}">
|
|
2651
2743
|
${renderStepsTree(step.steps, depth + 1)}
|
|
2652
2744
|
</div>
|
|
2653
2745
|
`;
|
|
@@ -2673,19 +2765,38 @@
|
|
|
2673
2765
|
function createStepHtml(text, number, status = null, category = null) {
|
|
2674
2766
|
const statusClass = status || 'passed';
|
|
2675
2767
|
const categoryText = category && category !== 'user' ? `<div class="step-category">${category}</div>` : '';
|
|
2676
|
-
const durationIcon = status ? `<div class="step-duration"><i class="fas fa-clock"></i> auto</div>` : '';
|
|
2768
|
+
const durationIcon = status ? `<div class="step-time"><span class="step-duration"><i class="fas fa-clock"></i> auto</span></div>` : '';
|
|
2677
2769
|
|
|
2678
2770
|
return `<div class="step-item">
|
|
2679
|
-
<div class="step-number">${number}</div>
|
|
2680
2771
|
<div class="step-content">
|
|
2681
|
-
<
|
|
2682
|
-
|
|
2683
|
-
|
|
2772
|
+
<div class="step-header">
|
|
2773
|
+
<div class="step-number">${number}</div>
|
|
2774
|
+
<div class="step-main">
|
|
2775
|
+
<p class="step-text">${text}</p>
|
|
2776
|
+
${categoryText}
|
|
2777
|
+
${durationIcon}
|
|
2778
|
+
</div>
|
|
2779
|
+
<div class="step-status ${statusClass}"></div>
|
|
2780
|
+
</div>
|
|
2684
2781
|
</div>
|
|
2685
|
-
<div class="step-status ${statusClass}"></div>
|
|
2686
2782
|
</div>`;
|
|
2687
2783
|
}
|
|
2688
2784
|
|
|
2785
|
+
function toggleStepChildren(button) {
|
|
2786
|
+
const stepItem = button.closest('.step-item');
|
|
2787
|
+
if (!stepItem) return;
|
|
2788
|
+
|
|
2789
|
+
const stepId = stepItem.getAttribute('data-step-id');
|
|
2790
|
+
if (!stepId) return;
|
|
2791
|
+
|
|
2792
|
+
const children = stepItem.parentElement.querySelector(`.step-children[data-parent-step-id="${stepId}"]`);
|
|
2793
|
+
if (!children) return;
|
|
2794
|
+
|
|
2795
|
+
const collapsed = children.classList.toggle('collapsed');
|
|
2796
|
+
button.classList.toggle('collapsed', collapsed);
|
|
2797
|
+
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2689
2800
|
function createRetryAttempts(retries) {
|
|
2690
2801
|
const attempts = Array.isArray(retries?.attempts) ? retries.attempts : [];
|
|
2691
2802
|
if (!attempts.length) return '';
|
package/lib/utils/utils.js
CHANGED
|
@@ -324,15 +324,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
324
324
|
if (lineIndex === -1)
|
|
325
325
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
326
326
|
}
|
|
327
|
-
else if (opts.lang === 'kotlin') {
|
|
328
|
-
lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
|
|
329
|
-
if (lineIndex === -1)
|
|
330
|
-
lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
331
|
-
if (lineIndex === -1)
|
|
332
|
-
lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
|
|
333
|
-
if (lineIndex === -1)
|
|
334
|
-
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
335
|
-
}
|
|
336
327
|
else if (opts.lang === 'csharp') {
|
|
337
328
|
// Find the method declaration line
|
|
338
329
|
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
package/lib/xmlReader.js
CHANGED
|
@@ -661,16 +661,18 @@ function reduceTestCases(prev, item) {
|
|
|
661
661
|
function processTestSuite(testsuite) {
|
|
662
662
|
if (!testsuite)
|
|
663
663
|
return [];
|
|
664
|
-
if (testsuite.testsuite)
|
|
664
|
+
if (testsuite.testsuite && !testsuite.testcase)
|
|
665
665
|
return processTestSuite(testsuite.testsuite);
|
|
666
666
|
if (testsuite['test-suite'] && !testsuite['test-case'])
|
|
667
667
|
return processTestSuite(testsuite['test-suite']);
|
|
668
668
|
let suites = testsuite;
|
|
669
|
-
if (!Array.isArray(testsuite))
|
|
669
|
+
if (!Array.isArray(testsuite))
|
|
670
670
|
suites = [testsuite];
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
671
|
+
const subSuites = suites.filter(s => (s['test-suite'] || s.testsuite) && !(s['test-case'] || s.testcase));
|
|
672
|
+
return [
|
|
673
|
+
...suites.reduce(reduceTestCases, []),
|
|
674
|
+
...subSuites.map(s => processTestSuite(s['test-suite'] || s.testsuite)),
|
|
675
|
+
].flat();
|
|
674
676
|
}
|
|
675
677
|
function fetchProperties(item) {
|
|
676
678
|
const tags = [];
|
package/package.json
CHANGED
package/src/adapter/vitest.js
CHANGED
|
@@ -19,21 +19,37 @@ const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
|
|
|
19
19
|
class VitestReporter {
|
|
20
20
|
constructor(config = {}) {
|
|
21
21
|
this.client = new TestomatioClient({ apiKey: config?.apiKey });
|
|
22
|
-
/**
|
|
23
|
-
* @type {(TestData & {status: string})[]} tests
|
|
24
|
-
*/
|
|
22
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
25
23
|
this.tests = [];
|
|
26
24
|
this._finalized = false;
|
|
27
25
|
this._finalizing = false;
|
|
26
|
+
this._runStartedAtMs = null;
|
|
27
|
+
this._runStartedAtMicros = null;
|
|
28
|
+
this._reportedTestKeys = new Set();
|
|
29
|
+
this._liveQueue = Promise.resolve();
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
// on run start
|
|
31
33
|
onInit() {
|
|
34
|
+
const now = Date.now();
|
|
32
35
|
this._finalized = false;
|
|
33
36
|
this._finalizing = false;
|
|
37
|
+
this._runStartedAtMs = now;
|
|
38
|
+
this._runStartedAtMicros = now * 1000;
|
|
39
|
+
this._reportedTestKeys = new Set();
|
|
40
|
+
this._liveQueue = Promise.resolve();
|
|
34
41
|
this.client.createRun();
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
46
|
+
*/
|
|
47
|
+
onTestRunStart() {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
this._runStartedAtMs = now;
|
|
50
|
+
this._runStartedAtMicros = now * 1000;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
/**
|
|
38
54
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
39
55
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -68,13 +84,18 @@ class VitestReporter {
|
|
|
68
84
|
|
|
69
85
|
// send tests to Testomat.io
|
|
70
86
|
for (const test of this.tests) {
|
|
87
|
+
if (test._reportKey && this._reportedTestKeys.has(test._reportKey)) continue;
|
|
88
|
+
if (test._reportKey) this._reportedTestKeys.add(test._reportKey);
|
|
71
89
|
await this.client.addTestRun(test.status, test);
|
|
72
90
|
}
|
|
91
|
+
await this._liveQueue;
|
|
73
92
|
|
|
74
93
|
console.log('finished');
|
|
75
94
|
if (errors.length) console.error('Vitest adapter errors:', errors);
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
const startedAtMs = this._runStartedAtMs || getEarliestTestStartMs(files) || Date.now();
|
|
97
|
+
const duration = Math.max(0, (Date.now() - startedAtMs) / 1000);
|
|
98
|
+
await this.client.updateRunStatus(getRunStatusFromResults(files), { duration });
|
|
78
99
|
this._finalized = true;
|
|
79
100
|
} finally {
|
|
80
101
|
this._finalizing = false;
|
|
@@ -94,6 +115,28 @@ class VitestReporter {
|
|
|
94
115
|
await this.onFinished(files, errors);
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Vitest 4 callback fired when single test case is finished.
|
|
120
|
+
*
|
|
121
|
+
* @param {unknown} testCase
|
|
122
|
+
*/
|
|
123
|
+
async onTestCaseResult(testCase) {
|
|
124
|
+
await this.#reportLive(testCase);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Vitest 3 fallback callback with task updates.
|
|
129
|
+
*
|
|
130
|
+
* @param {unknown[] | undefined} packs
|
|
131
|
+
*/
|
|
132
|
+
async onTaskUpdate(packs) {
|
|
133
|
+
if (!Array.isArray(packs) || !packs.length) return;
|
|
134
|
+
for (const pack of packs) {
|
|
135
|
+
const test = getTestFromTaskUpdatePack(pack);
|
|
136
|
+
if (test) await this.#reportLive(test);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
97
140
|
/* non-used listeners
|
|
98
141
|
onUserConsoleLog(log) {}
|
|
99
142
|
onPathsCollected(paths) {} // paths array to files with tests
|
|
@@ -128,25 +171,52 @@ class VitestReporter {
|
|
|
128
171
|
/**
|
|
129
172
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
130
173
|
*
|
|
131
|
-
* @param {
|
|
174
|
+
* @param {any} test
|
|
132
175
|
*
|
|
133
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
176
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
|
|
134
177
|
*/
|
|
135
178
|
#getDataFromTest(test) {
|
|
179
|
+
const normalized = normalizeVitestTest(test);
|
|
180
|
+
const reportKey = getReportKey(test, normalized);
|
|
181
|
+
const startMicros =
|
|
182
|
+
typeof normalized.startTime === 'number'
|
|
183
|
+
? Math.floor(normalized.startTime * 1000)
|
|
184
|
+
: this._runStartedAtMicros || undefined;
|
|
185
|
+
|
|
136
186
|
return {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
_reportKey: reportKey,
|
|
188
|
+
error: normalized.error,
|
|
189
|
+
file: normalized.file,
|
|
190
|
+
logs: normalized.logs,
|
|
191
|
+
meta: normalized.meta,
|
|
141
192
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
142
|
-
status: getTestStatus(
|
|
143
|
-
suite_title:
|
|
144
|
-
test_id: getTestomatIdFromTestTitle(
|
|
145
|
-
time:
|
|
146
|
-
|
|
193
|
+
status: getTestStatus(normalized.state, normalized.mode),
|
|
194
|
+
suite_title: normalized.suiteTitle,
|
|
195
|
+
test_id: getTestomatIdFromTestTitle(normalized.name),
|
|
196
|
+
time: normalized.duration,
|
|
197
|
+
timestamp: startMicros,
|
|
198
|
+
title: normalized.name,
|
|
147
199
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
148
200
|
};
|
|
149
201
|
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {unknown} testCase
|
|
205
|
+
*/
|
|
206
|
+
async #reportLive(testCase) {
|
|
207
|
+
if (this._finalized || this._finalizing) return;
|
|
208
|
+
const normalized = normalizeVitestTest(testCase);
|
|
209
|
+
if (!isLiveReportableState(normalized.state, normalized.mode)) return;
|
|
210
|
+
|
|
211
|
+
const data = this.#getDataFromTest(testCase);
|
|
212
|
+
if (!data._reportKey || this._reportedTestKeys.has(data._reportKey)) return;
|
|
213
|
+
this._reportedTestKeys.add(data._reportKey);
|
|
214
|
+
|
|
215
|
+
this._liveQueue = this._liveQueue
|
|
216
|
+
.then(() => this.client.addTestRun(data.status, data))
|
|
217
|
+
.catch(() => undefined);
|
|
218
|
+
await this._liveQueue;
|
|
219
|
+
}
|
|
150
220
|
}
|
|
151
221
|
|
|
152
222
|
/**
|
|
@@ -162,17 +232,16 @@ function getRunStatusFromResults(files) {
|
|
|
162
232
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
163
233
|
|
|
164
234
|
files.forEach(file => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (taskOrSuite.result?.state === 'fail') {
|
|
235
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
236
|
+
if (isFailedState(taskOrSuite?.result?.state)) {
|
|
168
237
|
status = 'failed'; // set status to failed if any test failed
|
|
169
238
|
}
|
|
170
239
|
});
|
|
171
240
|
|
|
172
241
|
// if there are no failed tests > search for passed tests
|
|
173
242
|
if (status !== 'failed') {
|
|
174
|
-
file.
|
|
175
|
-
if (taskOrSuite
|
|
243
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
244
|
+
if (isPassedState(taskOrSuite?.result?.state)) {
|
|
176
245
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
177
246
|
}
|
|
178
247
|
});
|
|
@@ -185,14 +254,15 @@ function getRunStatusFromResults(files) {
|
|
|
185
254
|
/**
|
|
186
255
|
* Returns test status in Testomat.io format
|
|
187
256
|
*
|
|
188
|
-
* @param {
|
|
257
|
+
* @param {string | undefined} state
|
|
258
|
+
* @param {string | undefined} mode
|
|
189
259
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
190
260
|
*/
|
|
191
|
-
function getTestStatus(
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
console.error(pc.red('Unprocessed case for defining test status. Contact dev team.
|
|
261
|
+
function getTestStatus(state, mode) {
|
|
262
|
+
if (isFailedState(state)) return STATUS.FAILED;
|
|
263
|
+
if (isPassedState(state)) return STATUS.PASSED;
|
|
264
|
+
if (isSkippedState(state) || (!state && mode === 'skip')) return STATUS.SKIPPED;
|
|
265
|
+
console.error(pc.red('Unprocessed case for defining test status. Contact dev team. State:'), state);
|
|
196
266
|
return STATUS.SKIPPED;
|
|
197
267
|
}
|
|
198
268
|
|
|
@@ -220,9 +290,166 @@ function getTasks(node) {
|
|
|
220
290
|
if (!node) return [];
|
|
221
291
|
if (Array.isArray(node.tasks)) return node.tasks;
|
|
222
292
|
if (Array.isArray(node.children)) return node.children;
|
|
293
|
+
if (node.children && typeof node.children[Symbol.iterator] === 'function') return Array.from(node.children);
|
|
223
294
|
if (node.task) return [node.task];
|
|
224
295
|
return [];
|
|
225
296
|
}
|
|
226
297
|
|
|
298
|
+
/**
|
|
299
|
+
* @param {string | undefined} state
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
function isFailedState(state) {
|
|
303
|
+
return state === 'fail' || state === 'failed';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {string | undefined} state
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
function isPassedState(state) {
|
|
311
|
+
return state === 'pass' || state === 'passed';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {string | undefined} state
|
|
316
|
+
* @returns {boolean}
|
|
317
|
+
*/
|
|
318
|
+
function isSkippedState(state) {
|
|
319
|
+
return state === 'skip' || state === 'skipped' || state === 'todo';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Accept only completed test states for live upload to avoid reporting
|
|
324
|
+
* intermediate task updates as skipped.
|
|
325
|
+
*
|
|
326
|
+
* @param {string | undefined} state
|
|
327
|
+
* @param {string | undefined} mode
|
|
328
|
+
* @returns {boolean}
|
|
329
|
+
*/
|
|
330
|
+
function isLiveReportableState(state, mode) {
|
|
331
|
+
if (isFailedState(state) || isPassedState(state) || isSkippedState(state)) return true;
|
|
332
|
+
if (!state && mode === 'skip') return true;
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {VitestTestFile[] | undefined} files
|
|
338
|
+
* @returns {number | null}
|
|
339
|
+
*/
|
|
340
|
+
function getEarliestTestStartMs(files) {
|
|
341
|
+
let earliest = null;
|
|
342
|
+
const walk = node => {
|
|
343
|
+
if (!node) return;
|
|
344
|
+
const startTime = node?.result?.startTime;
|
|
345
|
+
if (typeof startTime === 'number' && !Number.isNaN(startTime)) {
|
|
346
|
+
if (earliest == null || startTime < earliest) earliest = startTime;
|
|
347
|
+
}
|
|
348
|
+
getTasks(node).forEach(walk);
|
|
349
|
+
};
|
|
350
|
+
(files || []).forEach(walk);
|
|
351
|
+
return earliest;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {any} test
|
|
356
|
+
* @returns {{
|
|
357
|
+
* name: string,
|
|
358
|
+
* state: string | undefined,
|
|
359
|
+
* mode: string | undefined,
|
|
360
|
+
* duration: number,
|
|
361
|
+
* startTime: number | undefined,
|
|
362
|
+
* error: any,
|
|
363
|
+
* file: string,
|
|
364
|
+
* suiteTitle: string,
|
|
365
|
+
* logs: string,
|
|
366
|
+
* meta: any
|
|
367
|
+
* }}
|
|
368
|
+
*/
|
|
369
|
+
function normalizeVitestTest(test) {
|
|
370
|
+
if (test && typeof test.result === 'function') {
|
|
371
|
+
const result = test.result();
|
|
372
|
+
const diagnostic = typeof test.diagnostic === 'function' ? test.diagnostic() : undefined;
|
|
373
|
+
const state = result?.state;
|
|
374
|
+
const duration = diagnostic?.duration || 0;
|
|
375
|
+
const startTime = diagnostic?.startTime;
|
|
376
|
+
const error = Array.isArray(result?.errors) ? result.errors[0] : undefined;
|
|
377
|
+
const file =
|
|
378
|
+
test.module?.relativeModuleId ||
|
|
379
|
+
test.module?.moduleId ||
|
|
380
|
+
test.task?.file?.name ||
|
|
381
|
+
test.task?.file?.filepath ||
|
|
382
|
+
'';
|
|
383
|
+
const suiteTitle =
|
|
384
|
+
(test.parent?.type === 'suite' ? test.parent?.name : null) ||
|
|
385
|
+
test.task?.suite?.name ||
|
|
386
|
+
test.task?.file?.name ||
|
|
387
|
+
file;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
name: test.name || test.task?.name || '',
|
|
391
|
+
state,
|
|
392
|
+
mode: test.options?.mode || test.task?.mode,
|
|
393
|
+
duration,
|
|
394
|
+
startTime,
|
|
395
|
+
error,
|
|
396
|
+
file,
|
|
397
|
+
suiteTitle,
|
|
398
|
+
logs: '',
|
|
399
|
+
meta: typeof test.meta === 'function' ? test.meta() : {},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
name: test?.name || '',
|
|
405
|
+
state: test?.result?.state,
|
|
406
|
+
mode: test?.mode,
|
|
407
|
+
duration: test?.result?.duration || 0,
|
|
408
|
+
startTime: test?.result?.startTime,
|
|
409
|
+
error: test?.result?.errors ? test.result.errors[0] : undefined,
|
|
410
|
+
file: test?.file?.name || test?.file?.filepath || '',
|
|
411
|
+
suiteTitle: test?.suite?.name || test?.file?.name || test?.file?.filepath || '',
|
|
412
|
+
logs: test?.logs ? transformLogsToString(test.logs) : '',
|
|
413
|
+
meta: test?.meta,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {any} test
|
|
419
|
+
* @param {{file: string, suiteTitle: string, name: string, startTime?: number}} normalized
|
|
420
|
+
* @returns {string | null}
|
|
421
|
+
*/
|
|
422
|
+
function getReportKey(test, normalized) {
|
|
423
|
+
if (test?.id) return String(test.id);
|
|
424
|
+
if (test?.task?.id) return String(test.task.id);
|
|
425
|
+
if (!normalized?.name) return null;
|
|
426
|
+
const loc = test?.location || test?.task?.location;
|
|
427
|
+
const locationKey = loc ? `${loc.line || ''}:${loc.column || ''}` : '';
|
|
428
|
+
const startKey =
|
|
429
|
+
typeof normalized.startTime === 'number' && !Number.isNaN(normalized.startTime) ? String(normalized.startTime) : '';
|
|
430
|
+
return `${normalized.file}::${normalized.suiteTitle}::${normalized.name}::${locationKey}::${startKey}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Vitest can pass task updates as tuples. Try to extract a test-like object.
|
|
435
|
+
*
|
|
436
|
+
* @param {unknown} pack
|
|
437
|
+
* @returns {any | null}
|
|
438
|
+
*/
|
|
439
|
+
function getTestFromTaskUpdatePack(pack) {
|
|
440
|
+
if (!pack) return null;
|
|
441
|
+
|
|
442
|
+
if (Array.isArray(pack)) {
|
|
443
|
+
if (pack[2]?.type === 'test') return pack[2];
|
|
444
|
+
if (pack[1]?.type === 'test') return pack[1];
|
|
445
|
+
if (pack[0]?.type === 'test') return pack[0];
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const objectPack = /** @type {any} */ (pack);
|
|
450
|
+
if (typeof objectPack === 'object' && objectPack?.type === 'test') return objectPack;
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
227
454
|
export default VitestReporter;
|
|
228
455
|
export { VitestReporter };
|