@testomatio/reporter 2.7.3 → 2.7.4

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.
@@ -78,6 +78,20 @@ export class BitbucketPipe {
78
78
 
79
79
  async finishRun(runParams) {
80
80
  if (!this.isEnabled) return;
81
+ if (!this.ENV.BITBUCKET_PR_ID) {
82
+ log.warn(
83
+ pc.yellow('Bitbucket'),
84
+ 'Skipping PR comment: BITBUCKET_PR_ID is not set. Run this pipe in a Bitbucket pull-requests pipeline.',
85
+ );
86
+ return;
87
+ }
88
+ if (!this.ENV.BITBUCKET_WORKSPACE || !this.ENV.BITBUCKET_REPO_SLUG) {
89
+ log.warn(
90
+ pc.yellow('Bitbucket'),
91
+ 'Skipping PR comment: BITBUCKET_WORKSPACE or BITBUCKET_REPO_SLUG is missing.',
92
+ );
93
+ return;
94
+ }
81
95
 
82
96
  if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
83
97
 
@@ -196,12 +210,20 @@ export class BitbucketPipe {
196
210
 
197
211
  log.info(pc.yellow('Bitbucket'), `Report created: ${pc.magenta(commentURL)}`);
198
212
  } catch (err) {
213
+ const isForbiddenError = `${err}`.includes('Forbidden') || `${err}`.includes('403');
214
+ const scopeHint =
215
+ isForbiddenError
216
+ ? '\nHint: use a token that can write PR comments '
217
+ + '(recommended: Repository Access Token with Pull requests: Write '
218
+ + 'and Repository: Read) and run inside a pull-requests pipeline '
219
+ + 'where BITBUCKET_PR_ID is available.'
220
+ : '';
199
221
  console.error(
200
222
  APP_PREFIX,
201
223
  pc.yellow('Bitbucket'),
202
224
  `Couldn't create Bitbucket report\n${err}.
203
225
  Request URL: ${commentsRequestURL}
204
- Request data: ${body}`,
226
+ Request data: ${body}${scopeHint}`,
205
227
  );
206
228
  }
207
229
  }
package/src/pipe/html.js CHANGED
@@ -1085,6 +1085,18 @@ function normalizeArtifacts(test) {
1085
1085
  return allArtifacts
1086
1086
  .map(artifact => {
1087
1087
  if (typeof artifact === 'string') {
1088
+ if (/^https?:\/\//i.test(artifact)) {
1089
+ const base = path.basename(new URL(artifact).pathname) || artifact;
1090
+
1091
+ return {
1092
+ name: base,
1093
+ title: base,
1094
+ path: artifact,
1095
+ fsPath: null,
1096
+ relativePath: artifact,
1097
+ };
1098
+ }
1099
+
1088
1100
  const abs = path.isAbsolute(artifact) ? artifact : path.resolve(process.cwd(), artifact);
1089
1101
  const href = artifact.startsWith('file://') ? artifact : fileUrl(abs, { resolve: true });
1090
1102
  const base = path.basename(abs);
@@ -1101,9 +1113,14 @@ function normalizeArtifacts(test) {
1101
1113
  if (artifact?.path) {
1102
1114
  const raw = String(artifact.path);
1103
1115
  const isFileUrl = raw.startsWith('file://');
1104
- const abs = isFileUrl ? null : path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
1105
- const href = isFileUrl ? raw : fileUrl(abs, { resolve: true });
1106
- const base = abs ? path.basename(abs) : artifact.name || artifact.title || 'attachment';
1116
+ const isHttpUrl = /^https?:\/\//i.test(raw);
1117
+ const abs = isFileUrl || isHttpUrl ? null : path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
1118
+ const href = isFileUrl || isHttpUrl ? raw : fileUrl(abs, { resolve: true });
1119
+ const base = abs
1120
+ ? path.basename(abs)
1121
+ : isHttpUrl
1122
+ ? path.basename(new URL(raw).pathname) || artifact.name || artifact.title || 'attachment'
1123
+ : artifact.name || artifact.title || 'attachment';
1107
1124
 
1108
1125
  return {
1109
1126
  ...artifact,
@@ -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 active' onclick='showTestTab(this, \"info\")'>
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 active' onclick='showTestTab(this, \"steps\")'>
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 active' data-tab='info'>
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 active' data-tab='steps'>
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
- <p class="step-text">${step.title || 'Untitled step'}</p>
2638
- ${category && category !== 'user' ? `<div class="step-category">${category}</div>` : ''}
2639
- <div class="step-time">
2640
- Step ${stepNumber}
2641
- ${duration > 0 ? `<span class="step-duration"><i class="fas fa-clock"></i> ${formatDuration(duration)}</span>` : ''}
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
- <p class="step-text">${text}</p>
2682
- ${categoryText}
2683
- <div class="step-time">Step ${number}${durationIcon}</div>
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/src/xmlReader.js CHANGED
@@ -769,17 +769,20 @@ function reduceTestCases(prev, item) {
769
769
 
770
770
  function processTestSuite(testsuite) {
771
771
  if (!testsuite) return [];
772
- if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
772
+ if (testsuite.testsuite && !testsuite.testcase) return processTestSuite(testsuite.testsuite);
773
773
  if (testsuite['test-suite'] && !testsuite['test-case']) return processTestSuite(testsuite['test-suite']);
774
774
 
775
775
  let suites = testsuite;
776
- if (!Array.isArray(testsuite)) {
777
- suites = [testsuite];
778
- }
776
+ if (!Array.isArray(testsuite)) suites = [testsuite];
779
777
 
780
- const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
778
+ const subSuites = suites.filter(
779
+ s => (s['test-suite'] || s.testsuite) && !(s['test-case'] || s.testcase),
780
+ );
781
781
 
782
- return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
782
+ return [
783
+ ...suites.reduce(reduceTestCases, []),
784
+ ...subSuites.map(s => processTestSuite(s['test-suite'] || s.testsuite)),
785
+ ].flat();
783
786
  }
784
787
 
785
788
  function fetchProperties(item) {