@appliqation/automation-sdk 2.1.11 → 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/LICENSE +0 -0
- package/README.md +509 -7
- package/package.json +2 -2
- package/src/AppliqationClient.js +47 -2
- package/src/constants.js +0 -0
- package/src/core/AuthManager.js +0 -0
- package/src/core/HttpClient.js +52 -0
- package/src/index.d.ts +0 -0
- package/src/index.js +0 -0
- package/src/playwright/JwtBrowserAuth.js +0 -0
- package/src/playwright/fixture.js +0 -0
- package/src/playwright/global-setup.js +43 -0
- package/src/playwright/global-teardown.js +0 -0
- package/src/playwright/helpers/jwt-browser-auth.js +0 -0
- package/src/playwright/index.js +0 -0
- package/src/reporters/cypress/CypressReporter.js +49 -2
- package/src/reporters/cypress/UuidExtractor.js +0 -0
- package/src/reporters/cypress/index.js +0 -0
- package/src/reporters/jest/JestReporter.js +49 -2
- package/src/reporters/jest/UuidExtractor.js +0 -0
- package/src/reporters/jest/index.js +0 -0
- package/src/reporters/playwright/AppliqationReporter.js +734 -26
- package/src/reporters/playwright/helpers/DeviceOsDetector.js +0 -0
- package/src/reporters/playwright/helpers/UuidExtractor.js +8 -3
- package/src/reporters/playwright/index.d.ts +0 -0
- package/src/reporters/playwright/index.js +0 -0
- package/src/services/OrphanTestService.js +0 -0
- package/src/services/ResultService.js +193 -24
- package/src/services/RunMatrixService.js +44 -0
- package/src/services/TaggingService.js +241 -0
- package/src/utils/PayloadBuilder.js +0 -0
- package/src/utils/RunDataNormalizer.js +0 -0
- package/src/utils/UuidValidator.js +0 -0
- package/src/utils/errors.js +0 -0
- package/src/utils/index.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/utils/mapAppqUuid.js +0 -0
- package/src/utils/validator.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const path = require('path');
|
|
1
2
|
const AppliqationClient = require('../../AppliqationClient');
|
|
2
3
|
const UuidExtractor = require('./helpers/UuidExtractor');
|
|
3
4
|
const DeviceOsDetector = require('./helpers/DeviceOsDetector');
|
|
@@ -5,6 +6,51 @@ const PayloadBuilder = require('../../utils/PayloadBuilder');
|
|
|
5
6
|
const logger = require('../../utils/logger');
|
|
6
7
|
const { DEFAULT_APPLIQATION_BASE_URL } = require('../../constants');
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Check if Appliqation reporting should be enabled
|
|
11
|
+
* Checks both environment variable and CLI flag
|
|
12
|
+
* @returns {boolean} True if reporting should be enabled
|
|
13
|
+
*/
|
|
14
|
+
function isAppqEnabled() {
|
|
15
|
+
// Check environment variable first (easier to use)
|
|
16
|
+
const envEnabled = process.env.APPQ_ENABLE === '1' ||
|
|
17
|
+
process.env.APPQ_ENABLE === 'true' ||
|
|
18
|
+
process.env.APPLIQATION_ENABLE === '1' ||
|
|
19
|
+
process.env.APPLIQATION_ENABLE === 'true';
|
|
20
|
+
|
|
21
|
+
if (envEnabled) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check CLI flag (requires -- separator: npx playwright test -- --appq)
|
|
26
|
+
const argv = process.argv || [];
|
|
27
|
+
return argv.includes('--appq');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract run title from CLI arguments
|
|
32
|
+
* Supports: --appq_run_title="My Title" or --appq_run_title=MyTitle
|
|
33
|
+
* @returns {string|null} Run title from CLI args, or null if not found
|
|
34
|
+
*/
|
|
35
|
+
function getRunTitleFromCli() {
|
|
36
|
+
const argv = process.argv || [];
|
|
37
|
+
|
|
38
|
+
// Look for --appq_run_title=value or --appq_run_title="value"
|
|
39
|
+
for (const arg of argv) {
|
|
40
|
+
if (arg.startsWith('--appq_run_title=')) {
|
|
41
|
+
// Extract value after the = sign
|
|
42
|
+
let value = arg.substring('--appq_run_title='.length);
|
|
43
|
+
|
|
44
|
+
// Remove surrounding quotes if present
|
|
45
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
46
|
+
|
|
47
|
+
return value || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
8
54
|
/**
|
|
9
55
|
* Appliqation Reporter for Playwright
|
|
10
56
|
* Automatically reports test results to Appliqation platform
|
|
@@ -49,6 +95,8 @@ class AppliqationReporter {
|
|
|
49
95
|
batchSubmit: true,
|
|
50
96
|
batchSize: 50,
|
|
51
97
|
logLevel: 'info',
|
|
98
|
+
deleteOrphanOnlyRuns: config.deleteOrphanOnlyRuns !== false,
|
|
99
|
+
failOnOrphanOnlyRuns: config.failOnOrphanOnlyRuns !== false,
|
|
52
100
|
...config
|
|
53
101
|
};
|
|
54
102
|
|
|
@@ -66,15 +114,26 @@ class AppliqationReporter {
|
|
|
66
114
|
// Note: No default fallback - validation will catch missing value
|
|
67
115
|
|
|
68
116
|
// Apply title fallback if not provided
|
|
69
|
-
// Priority: 1) config.title, 2) process.env.APPLIQATION_RUN_TITLE,
|
|
117
|
+
// Priority: 1) config.title, 2) CLI --appq_run_title, 3) process.env.APPLIQATION_RUN_TITLE, 4) timestamp-based default
|
|
70
118
|
if (!this.config.title) {
|
|
71
|
-
|
|
119
|
+
const cliTitle = getRunTitleFromCli();
|
|
120
|
+
this.config.title = cliTitle || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`;
|
|
72
121
|
}
|
|
73
122
|
|
|
74
|
-
//
|
|
75
|
-
this.
|
|
123
|
+
// Check if --appq flag is present (opt-in approach)
|
|
124
|
+
this.appqEnabled = isAppqEnabled();
|
|
125
|
+
|
|
126
|
+
if (this.appqEnabled) {
|
|
127
|
+
// Validate configuration only if appq is enabled
|
|
128
|
+
this.validateConfig();
|
|
129
|
+
logger.info('Appliqation reporting ENABLED');
|
|
130
|
+
console.log('✅ Appliqation reporting enabled: Results will be sent to Appliqation portal');
|
|
131
|
+
} else {
|
|
132
|
+
logger.info('Appliqation reporting DISABLED');
|
|
133
|
+
console.log('ℹ️ Appliqation reporting disabled: Set APPQ_ENABLE=1 or add -- --appq flag to enable');
|
|
134
|
+
}
|
|
76
135
|
|
|
77
|
-
// Initialize SDK client
|
|
136
|
+
// Initialize SDK client (needed for UUID validation even if disabled)
|
|
78
137
|
this.client = new AppliqationClient({
|
|
79
138
|
baseUrl: this.config.baseUrl,
|
|
80
139
|
apiKey: this.config.apiKey,
|
|
@@ -90,12 +149,27 @@ class AppliqationReporter {
|
|
|
90
149
|
this.runsByProject = new Map(); // Map<projectKey, runInfo>
|
|
91
150
|
this.resultsByRun = new Map(); // Map<runId, results[]>
|
|
92
151
|
this.orphansByRun = new Map(); // Map<runId, orphans[]>
|
|
152
|
+
this.submittedUuidsByRun = new Map(); // Map<runId, Map<uuid, {file, title, browser}>>
|
|
153
|
+
this.submittedUuidsGlobal = new Map(); // Map<uuid, {file, title, browser, runId}>
|
|
93
154
|
this.browserVersionDetected = new Map(); // Map<projectKey, boolean> - Track if version detected
|
|
94
155
|
this.totalTests = 0;
|
|
95
156
|
this.passedTests = 0;
|
|
96
157
|
this.failedTests = 0;
|
|
97
158
|
this.skippedTests = 0;
|
|
98
159
|
this.orphanTests = 0;
|
|
160
|
+
this.validTests = 0; // Track tests with valid UUIDs
|
|
161
|
+
this.duplicateTests = 0; // Track duplicate UUID submissions
|
|
162
|
+
this.duplicateDetails = []; // Store details of each duplicate for summary
|
|
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}]}
|
|
168
|
+
|
|
169
|
+
// Execution tracking for summary file generation
|
|
170
|
+
this.executionStartTime = null;
|
|
171
|
+
this.executionEndTime = null;
|
|
172
|
+
this.playwrightOutputDir = null;
|
|
99
173
|
|
|
100
174
|
// Show baseUrl only in DEBUG mode
|
|
101
175
|
logger.info('Appliqation Playwright Reporter initialized', {
|
|
@@ -113,8 +187,23 @@ class AppliqationReporter {
|
|
|
113
187
|
* @param {Object} suite - Root test suite
|
|
114
188
|
*/
|
|
115
189
|
async onBegin(config, suite) {
|
|
190
|
+
// Capture execution start time for summary file
|
|
191
|
+
this.executionStartTime = new Date();
|
|
192
|
+
|
|
193
|
+
// Capture Playwright output directory
|
|
194
|
+
// Priority: 1) config._internal.outputDir (actual test-results path)
|
|
195
|
+
// 2) config.rootDir (project root)
|
|
196
|
+
// 3) process.cwd() (current working directory)
|
|
197
|
+
this.playwrightOutputDir = config._internal?.outputDir || config.rootDir || process.cwd();
|
|
198
|
+
|
|
116
199
|
logger.info('Test run starting...');
|
|
117
200
|
|
|
201
|
+
// Skip run creation if --appq flag not present
|
|
202
|
+
if (!this.appqEnabled) {
|
|
203
|
+
logger.info('Appliqation reporting disabled. Skipping run matrix creation.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
118
207
|
if (!this.config.autoCreateRun) {
|
|
119
208
|
logger.info('Auto-create run disabled. Skipping run matrix creation.');
|
|
120
209
|
return;
|
|
@@ -309,7 +398,7 @@ class AppliqationReporter {
|
|
|
309
398
|
* @param {Object} result - Test result
|
|
310
399
|
*/
|
|
311
400
|
async onTestEnd(test, result) {
|
|
312
|
-
|
|
401
|
+
// Note: Don't increment totalTests here - it's incremented later for non-duplicates/non-orphans only
|
|
313
402
|
|
|
314
403
|
try {
|
|
315
404
|
// Extract UUID from test - check result.annotations first since test.info().annotations.push() adds to result
|
|
@@ -345,7 +434,24 @@ class AppliqationReporter {
|
|
|
345
434
|
|
|
346
435
|
if (!uuid) {
|
|
347
436
|
// Orphan test - no UUID found
|
|
348
|
-
|
|
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
|
+
|
|
349
455
|
this.trackOrphan(runInfo.runId, test, result, deviceInfo.browser);
|
|
350
456
|
|
|
351
457
|
logger.warn('Test without UUID', {
|
|
@@ -357,6 +463,72 @@ class AppliqationReporter {
|
|
|
357
463
|
return;
|
|
358
464
|
}
|
|
359
465
|
|
|
466
|
+
// Duplicate detection (per-run AND per-browser - allows same UUID across different browsers/devices)
|
|
467
|
+
const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
|
|
468
|
+
const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
|
|
469
|
+
|
|
470
|
+
// Check per-run+browser map (allows same UUID for different browsers in same run)
|
|
471
|
+
if (submittedUuids.has(uuid)) {
|
|
472
|
+
const firstOccurrence = submittedUuids.get(uuid);
|
|
473
|
+
const firstFile = path.basename(firstOccurrence.file);
|
|
474
|
+
const currentFile = path.basename(test.location?.file || 'unknown');
|
|
475
|
+
|
|
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)
|
|
506
|
+
this.duplicateDetails.push({
|
|
507
|
+
uuid,
|
|
508
|
+
firstFile,
|
|
509
|
+
firstTitle: firstOccurrence.title,
|
|
510
|
+
duplicateFile: currentFile,
|
|
511
|
+
duplicateTitle: test.title
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
logger.warn(`Duplicate UUID detected: ${uuid}`);
|
|
515
|
+
logger.warn(` First: ${firstFile} -> "${firstOccurrence.title}"`);
|
|
516
|
+
logger.warn(` Dup: ${currentFile} -> "${test.title}"`);
|
|
517
|
+
return; // Skip duplicate submission
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Store UUID occurrence (per-run only)
|
|
521
|
+
submittedUuids.set(uuid, {
|
|
522
|
+
file: test.location?.file || 'unknown',
|
|
523
|
+
title: test.title,
|
|
524
|
+
browser: deviceInfo.browser
|
|
525
|
+
});
|
|
526
|
+
this.submittedUuidsByRun.set(trackingKey, submittedUuids);
|
|
527
|
+
|
|
528
|
+
// Count only tests that will be submitted (non-duplicate, non-orphan)
|
|
529
|
+
this.totalTests++;
|
|
530
|
+
this.validTests++; // Track tests with valid UUIDs
|
|
531
|
+
|
|
360
532
|
// Create result object
|
|
361
533
|
const testResult = {
|
|
362
534
|
runId: runInfo.runId, // Add runId for batch submission
|
|
@@ -394,23 +566,271 @@ class AppliqationReporter {
|
|
|
394
566
|
}
|
|
395
567
|
}
|
|
396
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
|
+
|
|
397
792
|
/**
|
|
398
793
|
* Called once after all tests complete
|
|
399
794
|
*/
|
|
400
795
|
async onEnd(result) {
|
|
401
|
-
logger.info('Test run complete.
|
|
796
|
+
logger.info('Test run complete.');
|
|
402
797
|
|
|
403
798
|
try {
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
799
|
+
// Handle orphan-only runs BEFORE submitting results
|
|
800
|
+
const orphanDeletedRuns = await this.handleOrphanOnlyRuns();
|
|
801
|
+
|
|
802
|
+
// Skip submission if --appq flag not present
|
|
803
|
+
if (this.appqEnabled) {
|
|
804
|
+
logger.info('Submitting results to Appliqation...');
|
|
805
|
+
|
|
806
|
+
// Submit batched results
|
|
807
|
+
if (this.config.batchSubmit) {
|
|
808
|
+
await this.submitBatchedResults();
|
|
809
|
+
}
|
|
408
810
|
|
|
409
|
-
|
|
410
|
-
|
|
811
|
+
// Submit orphan tests
|
|
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
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
logger.info('Appliqation reporting disabled. Skipping result submission.');
|
|
827
|
+
}
|
|
411
828
|
|
|
412
|
-
// Print summary
|
|
829
|
+
// Print summary (always show, even if not submitted)
|
|
413
830
|
this.printSummary();
|
|
831
|
+
|
|
832
|
+
// Write summary to file (always write, even if not submitted)
|
|
833
|
+
await this.writeSummaryToFile();
|
|
414
834
|
} catch (error) {
|
|
415
835
|
logger.error('Error in onEnd', {
|
|
416
836
|
error: error.message
|
|
@@ -453,6 +873,11 @@ class AppliqationReporter {
|
|
|
453
873
|
}
|
|
454
874
|
});
|
|
455
875
|
|
|
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);
|
|
880
|
+
|
|
456
881
|
logger.info(`Results submitted for run ${runId}`, {
|
|
457
882
|
success: summary.success,
|
|
458
883
|
failed: summary.failed,
|
|
@@ -558,27 +983,310 @@ class AppliqationReporter {
|
|
|
558
983
|
* @private
|
|
559
984
|
*/
|
|
560
985
|
printSummary() {
|
|
986
|
+
const testsSubmitted = this.totalTests;
|
|
987
|
+
const testsAccepted = testsSubmitted - this.backendRejectedTests;
|
|
988
|
+
|
|
561
989
|
console.log('\n╔═══════════════════════════════════════════════════════════╗');
|
|
562
990
|
console.log('║ Appliqation Test Results Summary ║');
|
|
563
991
|
console.log('╠═══════════════════════════════════════════════════════════╣');
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
992
|
+
|
|
993
|
+
// Show reporting status
|
|
994
|
+
if (!this.appqEnabled) {
|
|
995
|
+
console.log('║ ⚠️ Appliqation Reporting: DISABLED ║');
|
|
996
|
+
console.log('║ (Set APPQ_ENABLE=1 or add -- --appq flag) ║');
|
|
997
|
+
console.log('╠═══════════════════════════════════════════════════════════╣');
|
|
998
|
+
} else {
|
|
999
|
+
console.log('║ Submitted to Backend: ║');
|
|
1000
|
+
console.log(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
|
|
1001
|
+
console.log(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
|
|
1002
|
+
console.log(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
|
|
1003
|
+
console.log('║ ║');
|
|
1004
|
+
}
|
|
1005
|
+
console.log('║ ║');
|
|
1006
|
+
console.log('║ Test Execution Results (Playwright): ║');
|
|
1007
|
+
console.log(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
|
|
1008
|
+
console.log(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
|
|
1009
|
+
console.log(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
|
|
1010
|
+
console.log('║ ║');
|
|
1011
|
+
console.log('║ Not Submitted: ║');
|
|
1012
|
+
console.log(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
|
|
1013
|
+
console.log(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
|
|
569
1014
|
console.log('╠═══════════════════════════════════════════════════════════╣');
|
|
570
|
-
console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
|
|
571
1015
|
|
|
572
|
-
|
|
573
|
-
|
|
1016
|
+
// Only show run matrices if reporting is enabled
|
|
1017
|
+
if (this.appqEnabled && this.runsByProject.size > 0) {
|
|
1018
|
+
console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
|
|
1019
|
+
|
|
1020
|
+
for (const [projectKey, runInfo] of this.runsByProject.entries()) {
|
|
1021
|
+
console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Show deleted orphan-only runs
|
|
1025
|
+
if (this.deletedOrphanRuns.length > 0) {
|
|
1026
|
+
console.log(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length} ║`);
|
|
1027
|
+
}
|
|
574
1028
|
}
|
|
575
1029
|
|
|
576
1030
|
console.log('╚═══════════════════════════════════════════════════════════╝\n');
|
|
577
1031
|
|
|
1032
|
+
if (this.duplicateTests > 0) {
|
|
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
|
+
});
|
|
1043
|
+
console.log('');
|
|
1044
|
+
index++;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
console.log('Each test should have a unique UUID within a browser.\n');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (this.orphanTests > 0) {
|
|
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');
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Write execution summary to file
|
|
1068
|
+
* Creates timestamped txt file in AppQ_Execution_Summary folder
|
|
1069
|
+
* @private
|
|
1070
|
+
*/
|
|
1071
|
+
async writeSummaryToFile() {
|
|
1072
|
+
const fs = require('fs').promises;
|
|
1073
|
+
const path = require('path');
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
this.executionEndTime = new Date();
|
|
1077
|
+
const duration = this.executionEndTime - this.executionStartTime;
|
|
1078
|
+
|
|
1079
|
+
// Build output directory path
|
|
1080
|
+
// If playwrightOutputDir already contains 'test-results' (from config._internal.outputDir),
|
|
1081
|
+
// don't add it again. Otherwise, add 'test-results' to the path.
|
|
1082
|
+
const baseDir = this.playwrightOutputDir;
|
|
1083
|
+
const needsTestResults = !baseDir.includes('test-results');
|
|
1084
|
+
|
|
1085
|
+
const summaryDir = needsTestResults
|
|
1086
|
+
? path.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
|
|
1087
|
+
: path.join(baseDir, 'AppQ_Execution_Summary');
|
|
1088
|
+
|
|
1089
|
+
// Create directory if it doesn't exist
|
|
1090
|
+
await fs.mkdir(summaryDir, { recursive: true });
|
|
1091
|
+
|
|
1092
|
+
// Build filename with run title and timestamp
|
|
1093
|
+
const runTitle = this.config.title || 'execution_summary';
|
|
1094
|
+
const timestamp = this.formatDateTimeForFilename(this.executionStartTime);
|
|
1095
|
+
const filename = `${runTitle}_${timestamp}.txt`;
|
|
1096
|
+
const filepath = path.join(summaryDir, filename);
|
|
1097
|
+
|
|
1098
|
+
// Build comprehensive summary content
|
|
1099
|
+
const summaryContent = this.buildSummaryContent(duration);
|
|
1100
|
+
|
|
1101
|
+
// Write to file
|
|
1102
|
+
await fs.writeFile(filepath, summaryContent, 'utf8');
|
|
1103
|
+
|
|
1104
|
+
logger.info(`Execution summary saved to: ${filepath}`);
|
|
1105
|
+
console.log(`\n📄 Execution summary saved: ${filename}`);
|
|
1106
|
+
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
logger.error('Failed to write summary file', { error: error.message });
|
|
1109
|
+
// Don't throw - file writing failure should not break test run
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Format date/time for filename: 2025-01-21_14-30-45
|
|
1115
|
+
* @private
|
|
1116
|
+
*/
|
|
1117
|
+
formatDateTimeForFilename(date) {
|
|
1118
|
+
const year = date.getFullYear();
|
|
1119
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1120
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
1121
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
1122
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
1123
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
1124
|
+
|
|
1125
|
+
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Build comprehensive summary content for file
|
|
1130
|
+
* @private
|
|
1131
|
+
*/
|
|
1132
|
+
buildSummaryContent(duration) {
|
|
1133
|
+
const lines = [];
|
|
1134
|
+
|
|
1135
|
+
// Header
|
|
1136
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
1137
|
+
lines.push(' APPLIQATION TEST EXECUTION SUMMARY');
|
|
1138
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
1139
|
+
lines.push('');
|
|
1140
|
+
|
|
1141
|
+
// Execution Metadata
|
|
1142
|
+
lines.push('EXECUTION METADATA:');
|
|
1143
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
1144
|
+
lines.push(`Start Time: ${this.executionStartTime.toISOString()}`);
|
|
1145
|
+
lines.push(`End Time: ${this.executionEndTime.toISOString()}`);
|
|
1146
|
+
lines.push(`Duration: ${this.formatDuration(duration)}`);
|
|
1147
|
+
lines.push(`Run Title: ${this.config.title || 'N/A'}`);
|
|
1148
|
+
lines.push(`Appq Reporting: ${this.appqEnabled ? 'ENABLED' : 'DISABLED (Set APPQ_ENABLE=1 or add -- --appq flag)'}`);
|
|
1149
|
+
lines.push('');
|
|
1150
|
+
|
|
1151
|
+
// Test Results Summary (ASCII Table from printSummary)
|
|
1152
|
+
const testsSubmitted = this.totalTests;
|
|
1153
|
+
const testsAccepted = testsSubmitted - this.backendRejectedTests;
|
|
1154
|
+
|
|
1155
|
+
lines.push('╔═══════════════════════════════════════════════════════════╗');
|
|
1156
|
+
lines.push('║ Appliqation Test Results Summary ║');
|
|
1157
|
+
lines.push('╠═══════════════════════════════════════════════════════════╣');
|
|
1158
|
+
|
|
1159
|
+
// Show submission status
|
|
1160
|
+
if (!this.appqEnabled) {
|
|
1161
|
+
lines.push('║ ⚠️ Appliqation Reporting: DISABLED ║');
|
|
1162
|
+
lines.push('║ (Set APPQ_ENABLE=1 or add -- --appq flag) ║');
|
|
1163
|
+
lines.push('╠═══════════════════════════════════════════════════════════╣');
|
|
1164
|
+
} else {
|
|
1165
|
+
lines.push('║ Submitted to Backend: ║');
|
|
1166
|
+
lines.push(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
|
|
1167
|
+
lines.push(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
|
|
1168
|
+
lines.push(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
|
|
1169
|
+
lines.push('║ ║');
|
|
1170
|
+
}
|
|
1171
|
+
lines.push('║ Test Execution Results (Playwright): ║');
|
|
1172
|
+
lines.push(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
|
|
1173
|
+
lines.push(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
|
|
1174
|
+
lines.push(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
|
|
1175
|
+
lines.push('║ ║');
|
|
1176
|
+
lines.push('║ Not Submitted: ║');
|
|
1177
|
+
lines.push(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
|
|
1178
|
+
lines.push(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
|
|
1179
|
+
lines.push('╠═══════════════════════════════════════════════════════════╣');
|
|
1180
|
+
|
|
1181
|
+
// Run IDs (only show if reporting is enabled)
|
|
1182
|
+
if (this.appqEnabled && this.runsByProject.size > 0) {
|
|
1183
|
+
lines.push(`║ Run Matrices Created: ${this.runsByProject.size.toString().padStart(5)} ║`);
|
|
1184
|
+
for (const [projectKey, runInfo] of this.runsByProject) {
|
|
1185
|
+
const displayKey = projectKey.length > 30 ? projectKey.substring(0, 27) + '...' : projectKey;
|
|
1186
|
+
const paddedKey = displayKey.padEnd(30);
|
|
1187
|
+
lines.push(`║ ${paddedKey}: ${runInfo.runId.padEnd(20)} ║`);
|
|
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
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
lines.push('╚═══════════════════════════════════════════════════════════╝');
|
|
1197
|
+
lines.push('');
|
|
1198
|
+
|
|
1199
|
+
// Detailed Errors Section
|
|
1200
|
+
if (this.duplicateTests > 0 || this.orphanTests > 0 || this.backendRejectedTests > 0) {
|
|
1201
|
+
lines.push('');
|
|
1202
|
+
lines.push('DETAILED ERRORS & WARNINGS:');
|
|
1203
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
1204
|
+
lines.push('');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Duplicate UUIDs Details
|
|
1208
|
+
if (this.duplicateTests > 0 && this.uniqueDuplicates.size > 0) {
|
|
1209
|
+
lines.push('⚠️ DUPLICATE UUIDs DETECTED:');
|
|
1210
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
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
|
+
});
|
|
1224
|
+
lines.push('');
|
|
1225
|
+
index++;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
lines.push('Action Required: Each test should have a unique UUID within a browser.');
|
|
1229
|
+
lines.push('');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Orphan Tests Warning
|
|
578
1233
|
if (this.orphanTests > 0) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1234
|
+
lines.push('⚠️ TESTS MISSING UUIDs (Not Submitted):');
|
|
1235
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
1236
|
+
lines.push(`Count: ${this.orphanTests} unique test(s)`);
|
|
1237
|
+
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 () => {...});');
|
|
1251
|
+
lines.push('');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Backend Rejections
|
|
1255
|
+
if (this.backendRejectedTests > 0) {
|
|
1256
|
+
lines.push('BACKEND VALIDATION REJECTIONS:');
|
|
1257
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
1258
|
+
lines.push(`Total rejected: ${this.backendRejectedTests}`);
|
|
1259
|
+
lines.push('');
|
|
1260
|
+
lines.push('These test cases were rejected due to project ownership validation.');
|
|
1261
|
+
lines.push('The test case UUID does not belong to the project associated with');
|
|
1262
|
+
lines.push('your API key. Verify the correct project and test case mappings.');
|
|
1263
|
+
lines.push('');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Footer
|
|
1267
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
1268
|
+
lines.push(`Generated by Appliqation SDK v${require('../../../package.json').version}`);
|
|
1269
|
+
lines.push(`Timestamp: ${new Date().toISOString()}`);
|
|
1270
|
+
lines.push('═══════════════════════════════════════════════════════════');
|
|
1271
|
+
|
|
1272
|
+
return lines.join('\n');
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Format duration in human-readable format
|
|
1277
|
+
* @private
|
|
1278
|
+
*/
|
|
1279
|
+
formatDuration(ms) {
|
|
1280
|
+
const seconds = Math.floor(ms / 1000);
|
|
1281
|
+
const minutes = Math.floor(seconds / 60);
|
|
1282
|
+
const hours = Math.floor(minutes / 60);
|
|
1283
|
+
|
|
1284
|
+
if (hours > 0) {
|
|
1285
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
1286
|
+
} else if (minutes > 0) {
|
|
1287
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
1288
|
+
} else {
|
|
1289
|
+
return `${seconds}s`;
|
|
582
1290
|
}
|
|
583
1291
|
}
|
|
584
1292
|
|