@appliqation/automation-sdk 2.2.0 → 2.3.1

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.
@@ -95,6 +95,8 @@ class AppliqationReporter {
95
95
  batchSubmit: true,
96
96
  batchSize: 50,
97
97
  logLevel: 'info',
98
+ deleteOrphanOnlyRuns: config.deleteOrphanOnlyRuns !== false,
99
+ failOnOrphanOnlyRuns: config.failOnOrphanOnlyRuns !== false,
98
100
  ...config
99
101
  };
100
102
 
@@ -155,9 +157,14 @@ class AppliqationReporter {
155
157
  this.failedTests = 0;
156
158
  this.skippedTests = 0;
157
159
  this.orphanTests = 0;
160
+ this.validTests = 0; // Track tests with valid UUIDs
158
161
  this.duplicateTests = 0; // Track duplicate UUID submissions
159
162
  this.duplicateDetails = []; // Store details of each duplicate for summary
160
163
  this.backendRejectedTests = 0; // Track tests rejected by backend validation (e.g., project mismatch)
164
+ this.rejectionsByRun = new Map(); // Track backend rejections per run: runId -> rejectedCount
165
+ this.deletedOrphanRuns = []; // Track deleted orphan-only runs
166
+ this.uniqueOrphans = new Map(); // Track unique orphan tests: "file:title" -> {file, title, browsers[]}
167
+ this.uniqueDuplicates = new Map(); // Track unique duplicate UUIDs: "uuid" -> {uuid, occurrences: [{file, title, browser}]}
161
168
 
162
169
  // Execution tracking for summary file generation
163
170
  this.executionStartTime = null;
@@ -427,7 +434,24 @@ class AppliqationReporter {
427
434
 
428
435
  if (!uuid) {
429
436
  // Orphan test - no UUID found
430
- this.orphanTests++;
437
+ const orphanKey = `${test.location?.file || 'unknown'}:${test.title}`;
438
+
439
+ if (!this.uniqueOrphans.has(orphanKey)) {
440
+ // First occurrence of this unique orphan
441
+ this.orphanTests++;
442
+ this.uniqueOrphans.set(orphanKey, {
443
+ file: path.basename(test.location?.file || 'unknown'),
444
+ title: test.title,
445
+ browsers: [deviceInfo.browser]
446
+ });
447
+ } else {
448
+ // Same orphan on different browser - just add browser to list
449
+ const orphan = this.uniqueOrphans.get(orphanKey);
450
+ if (!orphan.browsers.includes(deviceInfo.browser)) {
451
+ orphan.browsers.push(deviceInfo.browser);
452
+ }
453
+ }
454
+
431
455
  this.trackOrphan(runInfo.runId, test, result, deviceInfo.browser);
432
456
 
433
457
  logger.warn('Test without UUID', {
@@ -439,41 +463,46 @@ class AppliqationReporter {
439
463
  return;
440
464
  }
441
465
 
442
- // Duplicate detection (global across all runs + per run)
443
- const trackingKey = runInfo.runId;
466
+ // Duplicate detection (per-run AND per-browser - allows same UUID across different browsers/devices)
467
+ const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
444
468
  const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
445
469
 
446
- const existingGlobal = this.submittedUuidsGlobal.get(uuid);
447
-
448
- // Check global map first to catch duplicates across files/runs/browsers
449
- if (existingGlobal) {
450
- const firstFile = path.basename(existingGlobal.file);
451
- const currentFile = path.basename(test.location?.file || 'unknown');
452
-
453
- this.duplicateTests++;
454
- // Note: Don't increment skippedTests - duplicates have their own category
455
- this.duplicateDetails.push({
456
- uuid,
457
- firstFile,
458
- firstTitle: existingGlobal.title,
459
- duplicateFile: currentFile,
460
- duplicateTitle: test.title
461
- });
462
-
463
- logger.warn(`Duplicate UUID detected (global): ${uuid}`);
464
- logger.warn(` First: ${firstFile} -> "${existingGlobal.title}"`);
465
- logger.warn(` Dup: ${currentFile} -> "${test.title}"`);
466
- return; // Skip duplicate submission
467
- }
468
-
469
- // Check per-run map to allow future per-run reporting if needed
470
+ // Check per-run+browser map (allows same UUID for different browsers in same run)
470
471
  if (submittedUuids.has(uuid)) {
471
472
  const firstOccurrence = submittedUuids.get(uuid);
472
473
  const firstFile = path.basename(firstOccurrence.file);
473
474
  const currentFile = path.basename(test.location?.file || 'unknown');
474
475
 
475
- this.duplicateTests++;
476
- // Note: Don't increment skippedTests - duplicates have their own category
476
+ // Track unique duplicates
477
+ if (!this.uniqueDuplicates.has(uuid)) {
478
+ // First time seeing this duplicate UUID
479
+ this.duplicateTests++;
480
+ this.uniqueDuplicates.set(uuid, {
481
+ uuid,
482
+ occurrences: [
483
+ {
484
+ file: firstFile,
485
+ title: firstOccurrence.title,
486
+ browser: firstOccurrence.browser
487
+ },
488
+ {
489
+ file: currentFile,
490
+ title: test.title,
491
+ browser: deviceInfo.browser
492
+ }
493
+ ]
494
+ });
495
+ } else {
496
+ // Already tracking this duplicate - add this occurrence
497
+ const duplicate = this.uniqueDuplicates.get(uuid);
498
+ duplicate.occurrences.push({
499
+ file: currentFile,
500
+ title: test.title,
501
+ browser: deviceInfo.browser
502
+ });
503
+ }
504
+
505
+ // Keep old duplicateDetails for backward compatibility (if needed)
477
506
  this.duplicateDetails.push({
478
507
  uuid,
479
508
  firstFile,
@@ -488,23 +517,17 @@ class AppliqationReporter {
488
517
  return; // Skip duplicate submission
489
518
  }
490
519
 
491
- // Store UUID occurrence
520
+ // Store UUID occurrence (per-run only)
492
521
  submittedUuids.set(uuid, {
493
522
  file: test.location?.file || 'unknown',
494
523
  title: test.title,
495
524
  browser: deviceInfo.browser
496
525
  });
497
526
  this.submittedUuidsByRun.set(trackingKey, submittedUuids);
498
- // Also track globally to catch duplicates across files/runs/browsers
499
- this.submittedUuidsGlobal.set(uuid, {
500
- file: test.location?.file || 'unknown',
501
- title: test.title,
502
- browser: deviceInfo.browser,
503
- runId: runInfo.runId
504
- });
505
527
 
506
528
  // Count only tests that will be submitted (non-duplicate, non-orphan)
507
529
  this.totalTests++;
530
+ this.validTests++; // Track tests with valid UUIDs
508
531
 
509
532
  // Create result object
510
533
  const testResult = {
@@ -543,6 +566,229 @@ class AppliqationReporter {
543
566
  }
544
567
  }
545
568
 
569
+ /**
570
+ * Handle runs that have only orphan tests (no valid UUIDs).
571
+ * Deletes empty runs and shows clear error messages.
572
+ *
573
+ * @private
574
+ * @returns {Promise<Array>} Array of deleted runs
575
+ */
576
+ async handleOrphanOnlyRuns() {
577
+ if (!this.config.deleteOrphanOnlyRuns) {
578
+ return []; // Feature disabled
579
+ }
580
+
581
+ const deletedRuns = [];
582
+
583
+ for (const [projectKey, runInfo] of this.runsByProject.entries()) {
584
+ const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
585
+ const validCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
586
+
587
+ // CRITICAL CHECK: Delete ONLY if all tests are orphans
588
+ if (orphanCount > 0 && validCount === 0) {
589
+ // Show clear error message FIRST (before attempting deletion)
590
+ // This ensures users always see the message, even if deletion fails
591
+ this.printOrphanOnlyError(orphanCount, projectKey);
592
+
593
+ try {
594
+ logger.warn('Deleting orphan-only run', {
595
+ runId: runInfo.runId,
596
+ orphanCount,
597
+ projectKey
598
+ });
599
+
600
+ await this.client.deleteRun(runInfo.runId, 'all_tests_orphaned');
601
+ deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
602
+ } catch (error) {
603
+ logger.error('Failed to delete orphan-only run (may already be deleted)', {
604
+ error: error.message,
605
+ runId: runInfo.runId
606
+ });
607
+ // Still track as deleted since user saw the error message
608
+ deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
609
+ }
610
+ }
611
+ }
612
+
613
+ // Store for summary
614
+ this.deletedOrphanRuns = deletedRuns;
615
+ return deletedRuns;
616
+ }
617
+
618
+ /**
619
+ * Print error message for orphan-only runs.
620
+ *
621
+ * @param {number} orphanCount - Number of orphan tests
622
+ * @param {string} projectKey - Project key
623
+ * @private
624
+ */
625
+ printOrphanOnlyError(orphanCount, projectKey) {
626
+ console.error('\n╔════════════════════════════════════════════════════════════════════╗');
627
+ console.error('║ ❌ RUN CREATION FAILED - ALL TESTS MISSING UUID ANNOTATIONS ║');
628
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
629
+ console.error(`║ Project: ${projectKey.padEnd(56)} ║`);
630
+ console.error(`║ Orphan Tests: ${orphanCount.toString().padEnd(51)} ║`);
631
+ console.error('║ ║');
632
+ console.error('║ ⚠️ NO RESULTS WERE SUBMITTED TO APPLIQATION ║');
633
+ console.error('║ The test run was automatically deleted to prevent analytics ║');
634
+ console.error('║ corruption. All tests are missing UUID annotations. ║');
635
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
636
+ console.error('║ ✅ ACTION REQUIRED: Add UUID Annotations ║');
637
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
638
+ console.error('║ ║');
639
+ console.error('║ Option 1: Using test tags (Recommended) ║');
640
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
641
+ console.error('║ test(\'My Test\', { tag: \'@uuid:123-xxx-xxx\' }, async ({ page }) => {║');
642
+ console.error('║ // your test code ║');
643
+ console.error('║ }); ║');
644
+ console.error('║ ║');
645
+ console.error('║ Option 2: Using test annotations ║');
646
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
647
+ console.error('║ test(\'My Test\', async ({ page }, testInfo) => { ║');
648
+ console.error('║ testInfo.annotations.push({ ║');
649
+ console.error('║ type: \'uuid\', ║');
650
+ console.error('║ description: \'123-xxx-xxx\' ║');
651
+ console.error('║ }); ║');
652
+ console.error('║ }); ║');
653
+ console.error('║ ║');
654
+ console.error('║ Option 3: Using mapAppqUuid helper ║');
655
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
656
+ console.error('║ const { mapAppqUuid } = require(\'@appliqation/automation-sdk/utils\');║');
657
+ console.error('║ test(\'My Test\', async ({ page }, testInfo) => { ║');
658
+ console.error('║ mapAppqUuid(testInfo, \'123-xxx-xxx\'); ║');
659
+ console.error('║ }); ║');
660
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
661
+ console.error('║ 📚 WHERE TO FIND UUIDs ║');
662
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
663
+ console.error('║ ║');
664
+ console.error('║ 1. Open your test scenario in Appliqation UI ║');
665
+ console.error('║ 2. Find the test case you want to automate ║');
666
+ console.error('║ 3. Copy the UUID from the test case details ║');
667
+ console.error('║ Format: {scenario-id}-{uuid} (e.g., 1154-7a17b809-0ff9...) ║');
668
+ console.error('║ ║');
669
+ console.error('║ 💡 Tip: Export UUIDs in bulk from Appliqation for faster setup ║');
670
+ console.error('╚════════════════════════════════════════════════════════════════════╝\n');
671
+ }
672
+
673
+ /**
674
+ * Handle runs where ALL tests were rejected by backend validation
675
+ * This happens when tests have UUIDs but they're invalid (wrong project, format, duplicates)
676
+ * Called AFTER submission when we know backend rejection counts
677
+ * @private
678
+ * @returns {Array} Array of deleted runs
679
+ */
680
+ async handleAllRejectedRuns() {
681
+ if (!this.config.deleteOrphanOnlyRuns) {
682
+ return []; // Feature disabled
683
+ }
684
+
685
+ const deletedRuns = [];
686
+
687
+ for (const [projectKey, runInfo] of this.runsByProject.entries()) {
688
+ const submittedCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
689
+ const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
690
+ const rejectedCount = this.rejectionsByRun.get(runInfo.runId) || 0;
691
+
692
+ // Calculate how many were actually accepted by backend
693
+ const acceptedCount = submittedCount - rejectedCount;
694
+
695
+ // CRITICAL CHECK: Delete ONLY if we submitted tests but ALL were rejected
696
+ // AND there are no orphans (orphans are handled separately)
697
+ if (submittedCount > 0 && acceptedCount === 0 && orphanCount === 0) {
698
+ // Show user-friendly error message FIRST (before deletion attempt)
699
+ this.printAllRejectedError(submittedCount, rejectedCount, projectKey);
700
+
701
+ try {
702
+ logger.warn('Deleting all-rejected run', {
703
+ runId: runInfo.runId,
704
+ submittedCount,
705
+ rejectedCount,
706
+ acceptedCount,
707
+ projectKey
708
+ });
709
+ await this.client.deleteRun(runInfo.runId, 'all_tests_rejected');
710
+ deletedRuns.push({
711
+ runId: runInfo.runId,
712
+ submittedCount,
713
+ rejectedCount,
714
+ projectKey
715
+ });
716
+ } catch (error) {
717
+ logger.error('Failed to delete all-rejected run (may already be deleted)', {
718
+ error: error.message,
719
+ runId: runInfo.runId
720
+ });
721
+ // Non-fatal: Continue even if deletion fails
722
+ // Still track as deleted since end result is the same
723
+ deletedRuns.push({
724
+ runId: runInfo.runId,
725
+ submittedCount,
726
+ rejectedCount,
727
+ projectKey
728
+ });
729
+ }
730
+ }
731
+ }
732
+
733
+ // Track deleted rejected runs
734
+ if (deletedRuns.length > 0) {
735
+ this.deletedOrphanRuns = [...(this.deletedOrphanRuns || []), ...deletedRuns];
736
+ }
737
+
738
+ return deletedRuns;
739
+ }
740
+
741
+ /**
742
+ * Print user-friendly error message for all-rejected runs
743
+ * @private
744
+ */
745
+ printAllRejectedError(submittedCount, rejectedCount, projectKey) {
746
+ console.error('\n╔════════════════════════════════════════════════════════════════════╗');
747
+ console.error('║ ❌ RUN CREATION FAILED - ALL TESTS REJECTED BY BACKEND ║');
748
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
749
+ console.error(`║ Project: ${projectKey.padEnd(56)} ║`);
750
+ console.error(`║ Tests Submitted: ${submittedCount.toString().padEnd(47)} ║`);
751
+ console.error(`║ Tests Rejected: ${rejectedCount.toString().padEnd(47)} ║`);
752
+ console.error('║ ║');
753
+ console.error('║ ⚠️ NO RESULTS WERE ACCEPTED BY APPLIQATION ║');
754
+ console.error('║ The test run was automatically deleted to prevent analytics ║');
755
+ console.error('║ corruption. All submitted tests were rejected by the backend. ║');
756
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
757
+ console.error('║ ✅ COMMON REJECTION REASONS ║');
758
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
759
+ console.error('║ ║');
760
+ console.error('║ 1. INVALID UUID FORMAT ║');
761
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
762
+ console.error('║ ❌ Wrong: \'1154-uuid-here-99\' (extra suffix) ║');
763
+ console.error('║ ✅ Right: \'1154-7f991165-9f59-47e1-8e90-d57d2e9cbf66\' ║');
764
+ console.error('║ ║');
765
+ console.error('║ 2. WRONG PROJECT ║');
766
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
767
+ console.error('║ Using UUID from project 1162 in project 1154 tests ║');
768
+ console.error('║ ✅ Solution: Use UUIDs from the correct project ║');
769
+ console.error('║ ║');
770
+ console.error('║ 3. DUPLICATE UUIDs ║');
771
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
772
+ console.error('║ Multiple tests using the same UUID in one run ║');
773
+ console.error('║ ✅ Solution: Each test needs a unique UUID ║');
774
+ console.error('║ ║');
775
+ console.error('║ 4. TEST CASE DOESN\'T EXIST ║');
776
+ console.error('║ ───────────────────────────────────────────────────────────── ║');
777
+ console.error('║ UUID doesn\'t match any test case in your project ║');
778
+ console.error('║ ✅ Solution: Verify UUID exists in Appliqation Test Cases ║');
779
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
780
+ console.error('║ 📋 HOW TO FIX ║');
781
+ console.error('╠════════════════════════════════════════════════════════════════════╣');
782
+ console.error('║ ║');
783
+ console.error('║ 1. Check test logs above for specific rejection reasons ║');
784
+ console.error('║ 2. Verify UUIDs in Appliqation portal → Test Cases ║');
785
+ console.error('║ 3. Ensure UUID format: {nid}-{uuid} (no extra suffixes) ║');
786
+ console.error('║ 4. Confirm UUIDs belong to the correct project ║');
787
+ console.error('║ 5. Remove any duplicate UUID mappings ║');
788
+ console.error('║ ║');
789
+ console.error('╚════════════════════════════════════════════════════════════════════╝\n');
790
+ }
791
+
546
792
  /**
547
793
  * Called once after all tests complete
548
794
  */
@@ -550,6 +796,9 @@ class AppliqationReporter {
550
796
  logger.info('Test run complete.');
551
797
 
552
798
  try {
799
+ // Handle orphan-only runs BEFORE submitting results
800
+ const orphanDeletedRuns = await this.handleOrphanOnlyRuns();
801
+
553
802
  // Skip submission if --appq flag not present
554
803
  if (this.appqEnabled) {
555
804
  logger.info('Submitting results to Appliqation...');
@@ -561,6 +810,18 @@ class AppliqationReporter {
561
810
 
562
811
  // Submit orphan tests
563
812
  await this.submitOrphanTests();
813
+
814
+ // Handle all-rejected runs AFTER submission (when we know backend rejection counts)
815
+ const rejectedDeletedRuns = await this.handleAllRejectedRuns();
816
+
817
+ // Combine all deleted runs
818
+ const allDeletedRuns = [...(orphanDeletedRuns || []), ...(rejectedDeletedRuns || [])];
819
+
820
+ // Exit with error code if any runs were deleted
821
+ if (allDeletedRuns.length > 0 && this.config.failOnOrphanOnlyRuns) {
822
+ logger.error('Exiting with error code due to deleted runs (orphan or rejected)');
823
+ process.exitCode = 1; // Set error exit code for CI/CD
824
+ }
564
825
  } else {
565
826
  logger.info('Appliqation reporting disabled. Skipping result submission.');
566
827
  }
@@ -612,8 +873,10 @@ class AppliqationReporter {
612
873
  }
613
874
  });
614
875
 
615
- // Track backend validation rejections
616
- this.backendRejectedTests += summary.failed || 0;
876
+ // Track backend validation rejections (global and per-run)
877
+ const rejectedCount = summary.failed || 0;
878
+ this.backendRejectedTests += rejectedCount;
879
+ this.rejectionsByRun.set(runId, rejectedCount);
617
880
 
618
881
  logger.info(`Results submitted for run ${runId}`, {
619
882
  success: summary.success,
@@ -682,14 +945,14 @@ class AppliqationReporter {
682
945
  */
683
946
  mapStatus(status) {
684
947
  const statusMap = {
685
- 'passed': 'passed',
686
- 'failed': 'failed',
687
- 'timedOut': 'failed',
688
- 'skipped': 'skipped',
689
- 'interrupted': 'skipped'
948
+ 'passed': 'Pass', // Capital case to match backend expectations
949
+ 'failed': 'Fail', // Capital case to match backend expectations
950
+ 'timedOut': 'Fail',
951
+ 'skipped': 'Skipped', // Capital case to match backend expectations
952
+ 'interrupted': 'Skipped'
690
953
  };
691
954
 
692
- return statusMap[status] || 'failed';
955
+ return statusMap[status] || 'Fail';
693
956
  }
694
957
 
695
958
  /**
@@ -757,27 +1020,46 @@ class AppliqationReporter {
757
1020
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
758
1021
  console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
759
1022
  }
1023
+
1024
+ // Show deleted orphan-only runs
1025
+ if (this.deletedOrphanRuns.length > 0) {
1026
+ console.log(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length} ║`);
1027
+ }
760
1028
  }
761
1029
 
762
1030
  console.log('╚═══════════════════════════════════════════════════════════╝\n');
763
1031
 
764
1032
  if (this.duplicateTests > 0) {
765
- console.log('⚠️ Warning: Duplicate UUID(s) detected!');
766
- console.log('');
767
- this.duplicateDetails.forEach((dup, index) => {
768
- console.log(`${index + 1}. UUID: ${dup.uuid}`);
769
- console.log(` First occurrence: ${dup.firstFile} - "${dup.firstTitle}"`);
770
- console.log(` Duplicate found: ${dup.duplicateFile} - "${dup.duplicateTitle}"`);
1033
+ console.log(`\n⚠️ Warning: ${this.duplicateTests} duplicate UUID(s) detected!\n`);
1034
+
1035
+ // Show details of each unique duplicate UUID
1036
+ let index = 1;
1037
+ for (const duplicate of this.uniqueDuplicates.values()) {
1038
+ console.log(`${index}. UUID: ${duplicate.uuid}`);
1039
+ console.log(` Occurrences (${duplicate.occurrences.length}):`);
1040
+ duplicate.occurrences.forEach((occ, i) => {
1041
+ console.log(` ${i + 1}) ${occ.file} - "${occ.title}" (${occ.browser})`);
1042
+ });
771
1043
  console.log('');
772
- });
773
- console.log(`Total: ${this.duplicateTests} duplicate test(s) skipped.`);
1044
+ index++;
1045
+ }
1046
+
774
1047
  console.log('Each test should have a unique UUID within a browser.\n');
775
1048
  }
776
1049
 
777
1050
  if (this.orphanTests > 0) {
778
- console.log('⚠️ Warning: Some tests are missing UUID annotations!');
779
- console.log(' Add UUIDs to map tests to Appliqation test cases:');
780
- console.log(' test(\'My Test\', { tag: \'@uuid:123-xxx-...\' }, async ({ page }) => { ... });\n');
1051
+ console.log(`\n⚠️ ${this.orphanTests} test(s) missing UUIDs - not submitted to Appliqation\n`);
1052
+
1053
+ // Show details of each unique orphan test
1054
+ let index = 1;
1055
+ for (const orphan of this.uniqueOrphans.values()) {
1056
+ console.log(`${index}. File: ${orphan.file}`);
1057
+ console.log(` Test: "${orphan.title}"`);
1058
+ console.log(` Browsers: ${orphan.browsers.join(', ')}\n`);
1059
+ index++;
1060
+ }
1061
+
1062
+ console.log('Fix: Add UUID tag → test(\'name\', { tag: \'@uuid:1154-xxx...\' }, async () => {...});\n');
781
1063
  }
782
1064
  }
783
1065
 
@@ -904,6 +1186,11 @@ class AppliqationReporter {
904
1186
  const paddedKey = displayKey.padEnd(30);
905
1187
  lines.push(`║ ${paddedKey}: ${runInfo.runId.padEnd(20)} ║`);
906
1188
  }
1189
+
1190
+ // Show deleted orphan-only runs
1191
+ if (this.deletedOrphanRuns.length > 0) {
1192
+ lines.push(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length.toString().padStart(5)} ║`);
1193
+ }
907
1194
  }
908
1195
 
909
1196
  lines.push('╚═══════════════════════════════════════════════════════════╝');
@@ -918,30 +1205,49 @@ class AppliqationReporter {
918
1205
  }
919
1206
 
920
1207
  // Duplicate UUIDs Details
921
- if (this.duplicateTests > 0 && this.duplicateDetails.length > 0) {
922
- lines.push('DUPLICATE UUIDs DETECTED:');
1208
+ if (this.duplicateTests > 0 && this.uniqueDuplicates.size > 0) {
1209
+ lines.push('⚠️ DUPLICATE UUIDs DETECTED:');
923
1210
  lines.push('─────────────────────────────────────────────────────────');
924
- this.duplicateDetails.forEach((dup, idx) => {
925
- lines.push(`${idx + 1}. UUID: ${dup.uuid}`);
926
- lines.push(` First occurrence: ${dup.firstFile}`);
927
- lines.push(` "${dup.firstTitle}"`);
928
- lines.push(` Duplicate found: ${dup.duplicateFile}`);
929
- lines.push(` "${dup.duplicateTitle}"`);
1211
+ lines.push(`Count: ${this.duplicateTests} unique UUID(s)`);
1212
+ lines.push('');
1213
+
1214
+ // List each unique duplicate UUID with all occurrences
1215
+ let index = 1;
1216
+ for (const duplicate of this.uniqueDuplicates.values()) {
1217
+ lines.push(`${index}. UUID: ${duplicate.uuid}`);
1218
+ lines.push(` Occurrences (${duplicate.occurrences.length}):`);
1219
+ duplicate.occurrences.forEach((occ, i) => {
1220
+ lines.push(` ${i + 1}) File: ${occ.file}`);
1221
+ lines.push(` Test: "${occ.title}"`);
1222
+ lines.push(` Browser: ${occ.browser}`);
1223
+ });
930
1224
  lines.push('');
931
- });
1225
+ index++;
1226
+ }
1227
+
1228
+ lines.push('Action Required: Each test should have a unique UUID within a browser.');
1229
+ lines.push('');
932
1230
  }
933
1231
 
934
1232
  // Orphan Tests Warning
935
1233
  if (this.orphanTests > 0) {
936
- lines.push('ORPHAN TESTS (Missing UUID Annotations):');
1234
+ lines.push('⚠️ TESTS MISSING UUIDs (Not Submitted):');
937
1235
  lines.push('─────────────────────────────────────────────────────────');
938
- lines.push(`Total orphan tests: ${this.orphanTests}`);
1236
+ lines.push(`Count: ${this.orphanTests} unique test(s)`);
939
1237
  lines.push('');
940
- lines.push('These tests are missing UUID annotations. Add UUIDs to map tests');
941
- lines.push('to Appliqation test cases:');
942
- lines.push(' test(\'My Test\', { tag: \'@uuid:123-xxx-...\' }, async ({ page }) => {');
943
- lines.push(' // test code');
944
- lines.push(' });');
1238
+
1239
+ // List each unique orphan test with details
1240
+ let index = 1;
1241
+ for (const orphan of this.uniqueOrphans.values()) {
1242
+ lines.push(`${index}. File: ${orphan.file}`);
1243
+ lines.push(` Test: "${orphan.title}"`);
1244
+ lines.push(` Browsers: ${orphan.browsers.join(', ')}`);
1245
+ lines.push('');
1246
+ index++;
1247
+ }
1248
+
1249
+ lines.push('Action Required: Add UUID tags to submit results');
1250
+ lines.push('Example: test(\'name\', { tag: \'@uuid:1154-abc-...\' }, async () => {...});');
945
1251
  lines.push('');
946
1252
  }
947
1253
 
@@ -120,16 +120,21 @@ class UuidExtractor {
120
120
  for (const annotation of annotations) {
121
121
  if (!annotation || typeof annotation !== 'object') continue;
122
122
 
123
- // Check annotation type
123
+ // Check annotation type - STRICT validation for explicit UUID annotations
124
124
  if (annotation.type === 'uuid' || annotation.type === 'appliqation' || annotation.type === 'appliqation-uuid') {
125
125
  const uuid = annotation.description || annotation.value;
126
126
  if (uuid && UuidValidator.validate(uuid)) {
127
127
  return uuid;
128
128
  }
129
+ // If UUID type annotation exists but invalid, do NOT try pattern extraction
130
+ // This ensures invalid UUIDs are treated as orphans, not auto-corrected
131
+ if (uuid) {
132
+ return null; // Invalid UUID explicitly provided -> treat as orphan
133
+ }
129
134
  }
130
135
 
131
- // Check annotation description for UUID
132
- if (annotation.description) {
136
+ // Check annotation description for UUID (only for non-UUID-type annotations)
137
+ if (annotation.description && annotation.type !== 'uuid' && annotation.type !== 'appliqation' && annotation.type !== 'appliqation-uuid') {
133
138
  const uuid = this.extractFromTitle(annotation.description);
134
139
  if (uuid) return uuid;
135
140
  }
@@ -2,8 +2,9 @@ const logger = require('../utils/logger');
2
2
  const { normalizeBrowser } = require('../utils/RunDataNormalizer');
3
3
 
4
4
  class ResultService {
5
- constructor(httpClient, config = { options: {} }) {
5
+ constructor(httpClient, taggingService = null, config = { options: {} }) {
6
6
  this.http = httpClient;
7
+ this.tagging = taggingService;
7
8
  this.config = config;
8
9
  }
9
10
 
@@ -51,6 +52,18 @@ class ResultService {
51
52
  throw new Error(response.error || 'Result submission failed');
52
53
  }
53
54
 
55
+ // Auto-tag accepted result (await to ensure completion)
56
+ if (this.tagging && this.tagging.isEnabled()) {
57
+ try {
58
+ await this.tagging.autoTagAcceptedResults([result.uuid]);
59
+ } catch (err) {
60
+ logger.debug('Background tagging failed', {
61
+ error: err.message,
62
+ uuid: result.uuid
63
+ });
64
+ }
65
+ }
66
+
54
67
  return response.data || response;
55
68
  } catch (error) {
56
69
  logger.error('Failed to submit result', {
@@ -242,6 +255,28 @@ class ResultService {
242
255
 
243
256
  logger.info('Batch submission completed', summary);
244
257
 
258
+ // Auto-tag accepted results (await to ensure completion before reporter exits)
259
+ if (this.tagging && this.tagging.isEnabled() && allResults.length > 0) {
260
+ const acceptedUuids = allResults.map(r => r.uuid).filter(Boolean);
261
+
262
+ try {
263
+ const tagResult = await this.tagging.autoTagAcceptedResults(acceptedUuids);
264
+ if (tagResult.tagged > 0) {
265
+ console.log(`✅ Auto-tagged ${tagResult.tagged} test case(s) with "${this.tagging.tagName}"`);
266
+ if (tagResult.skipped > 0) {
267
+ console.log(`ℹ️ Skipped ${tagResult.skipped} test case(s) - already tagged with "${this.tagging.tagName}"`);
268
+ }
269
+ } else if (tagResult.skipped > 0) {
270
+ console.log(`ℹ️ All ${tagResult.skipped} test case(s) already tagged with "${this.tagging.tagName}" - skipping`);
271
+ }
272
+ } catch (err) {
273
+ logger.warn('Auto-tagging failed (non-blocking)', {
274
+ error: err.message,
275
+ count: acceptedUuids.length
276
+ });
277
+ }
278
+ }
279
+
245
280
  return summary;
246
281
  }
247
282