@appliqation/automation-sdk 2.2.0 → 2.3.0
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 +269 -7
- package/package.json +2 -2
- package/src/AppliqationClient.js +47 -2
- package/src/core/HttpClient.js +52 -0
- package/src/reporters/playwright/AppliqationReporter.js +372 -66
- package/src/reporters/playwright/helpers/UuidExtractor.js +8 -3
- package/src/services/ResultService.js +36 -1
- package/src/services/RunMatrixService.js +28 -0
- package/src/services/TaggingService.js +211 -14
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
779
|
-
|
|
780
|
-
|
|
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.
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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('
|
|
1234
|
+
lines.push('⚠️ TESTS MISSING UUIDs (Not Submitted):');
|
|
937
1235
|
lines.push('─────────────────────────────────────────────────────────');
|
|
938
|
-
lines.push(`
|
|
1236
|
+
lines.push(`Count: ${this.orphanTests} unique test(s)`);
|
|
939
1237
|
lines.push('');
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
|