@appliqation/automation-sdk 2.1.10 → 2.2.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.
Files changed (38) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +260 -20
  3. package/package.json +9 -1
  4. package/src/AppliqationClient.js +1 -1
  5. package/src/constants.js +0 -0
  6. package/src/core/AuthManager.js +0 -0
  7. package/src/core/HttpClient.js +0 -0
  8. package/src/index.d.ts +0 -0
  9. package/src/index.js +0 -0
  10. package/src/playwright/JwtBrowserAuth.js +0 -0
  11. package/src/playwright/fixture.js +0 -0
  12. package/src/playwright/global-setup.js +43 -0
  13. package/src/playwright/global-teardown.js +42 -0
  14. package/src/playwright/helpers/jwt-browser-auth.js +0 -0
  15. package/src/playwright/index.js +0 -0
  16. package/src/reporters/cypress/CypressReporter.js +49 -2
  17. package/src/reporters/cypress/UuidExtractor.js +0 -0
  18. package/src/reporters/cypress/index.js +0 -0
  19. package/src/reporters/jest/JestReporter.js +49 -2
  20. package/src/reporters/jest/UuidExtractor.js +0 -0
  21. package/src/reporters/jest/index.js +0 -0
  22. package/src/reporters/playwright/AppliqationReporter.js +424 -22
  23. package/src/reporters/playwright/helpers/DeviceOsDetector.js +0 -0
  24. package/src/reporters/playwright/helpers/UuidExtractor.js +0 -0
  25. package/src/reporters/playwright/index.d.ts +0 -0
  26. package/src/reporters/playwright/index.js +0 -0
  27. package/src/services/OrphanTestService.js +0 -0
  28. package/src/services/ResultService.js +158 -24
  29. package/src/services/RunMatrixService.js +16 -0
  30. package/src/services/TaggingService.js +44 -0
  31. package/src/utils/PayloadBuilder.js +0 -0
  32. package/src/utils/RunDataNormalizer.js +0 -0
  33. package/src/utils/UuidValidator.js +0 -0
  34. package/src/utils/errors.js +0 -0
  35. package/src/utils/index.js +0 -0
  36. package/src/utils/logger.js +0 -0
  37. package/src/utils/mapAppqUuid.js +0 -0
  38. package/src/utils/validator.js +0 -0
File without changes
File without changes
@@ -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
@@ -66,15 +112,26 @@ class AppliqationReporter {
66
112
  // Note: No default fallback - validation will catch missing value
67
113
 
68
114
  // Apply title fallback if not provided
69
- // Priority: 1) config.title, 2) process.env.APPLIQATION_RUN_TITLE, 3) timestamp-based default
115
+ // Priority: 1) config.title, 2) CLI --appq_run_title, 3) process.env.APPLIQATION_RUN_TITLE, 4) timestamp-based default
70
116
  if (!this.config.title) {
71
- this.config.title = process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`;
117
+ const cliTitle = getRunTitleFromCli();
118
+ this.config.title = cliTitle || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`;
72
119
  }
73
120
 
74
- // Validate configuration
75
- this.validateConfig();
121
+ // Check if --appq flag is present (opt-in approach)
122
+ this.appqEnabled = isAppqEnabled();
123
+
124
+ if (this.appqEnabled) {
125
+ // Validate configuration only if appq is enabled
126
+ this.validateConfig();
127
+ logger.info('Appliqation reporting ENABLED');
128
+ console.log('✅ Appliqation reporting enabled: Results will be sent to Appliqation portal');
129
+ } else {
130
+ logger.info('Appliqation reporting DISABLED');
131
+ console.log('ℹ️ Appliqation reporting disabled: Set APPQ_ENABLE=1 or add -- --appq flag to enable');
132
+ }
76
133
 
77
- // Initialize SDK client
134
+ // Initialize SDK client (needed for UUID validation even if disabled)
78
135
  this.client = new AppliqationClient({
79
136
  baseUrl: this.config.baseUrl,
80
137
  apiKey: this.config.apiKey,
@@ -90,12 +147,22 @@ class AppliqationReporter {
90
147
  this.runsByProject = new Map(); // Map<projectKey, runInfo>
91
148
  this.resultsByRun = new Map(); // Map<runId, results[]>
92
149
  this.orphansByRun = new Map(); // Map<runId, orphans[]>
150
+ this.submittedUuidsByRun = new Map(); // Map<runId, Map<uuid, {file, title, browser}>>
151
+ this.submittedUuidsGlobal = new Map(); // Map<uuid, {file, title, browser, runId}>
93
152
  this.browserVersionDetected = new Map(); // Map<projectKey, boolean> - Track if version detected
94
153
  this.totalTests = 0;
95
154
  this.passedTests = 0;
96
155
  this.failedTests = 0;
97
156
  this.skippedTests = 0;
98
157
  this.orphanTests = 0;
158
+ this.duplicateTests = 0; // Track duplicate UUID submissions
159
+ this.duplicateDetails = []; // Store details of each duplicate for summary
160
+ this.backendRejectedTests = 0; // Track tests rejected by backend validation (e.g., project mismatch)
161
+
162
+ // Execution tracking for summary file generation
163
+ this.executionStartTime = null;
164
+ this.executionEndTime = null;
165
+ this.playwrightOutputDir = null;
99
166
 
100
167
  // Show baseUrl only in DEBUG mode
101
168
  logger.info('Appliqation Playwright Reporter initialized', {
@@ -113,8 +180,23 @@ class AppliqationReporter {
113
180
  * @param {Object} suite - Root test suite
114
181
  */
115
182
  async onBegin(config, suite) {
183
+ // Capture execution start time for summary file
184
+ this.executionStartTime = new Date();
185
+
186
+ // Capture Playwright output directory
187
+ // Priority: 1) config._internal.outputDir (actual test-results path)
188
+ // 2) config.rootDir (project root)
189
+ // 3) process.cwd() (current working directory)
190
+ this.playwrightOutputDir = config._internal?.outputDir || config.rootDir || process.cwd();
191
+
116
192
  logger.info('Test run starting...');
117
193
 
194
+ // Skip run creation if --appq flag not present
195
+ if (!this.appqEnabled) {
196
+ logger.info('Appliqation reporting disabled. Skipping run matrix creation.');
197
+ return;
198
+ }
199
+
118
200
  if (!this.config.autoCreateRun) {
119
201
  logger.info('Auto-create run disabled. Skipping run matrix creation.');
120
202
  return;
@@ -309,7 +391,7 @@ class AppliqationReporter {
309
391
  * @param {Object} result - Test result
310
392
  */
311
393
  async onTestEnd(test, result) {
312
- this.totalTests++;
394
+ // Note: Don't increment totalTests here - it's incremented later for non-duplicates/non-orphans only
313
395
 
314
396
  try {
315
397
  // Extract UUID from test - check result.annotations first since test.info().annotations.push() adds to result
@@ -357,6 +439,73 @@ class AppliqationReporter {
357
439
  return;
358
440
  }
359
441
 
442
+ // Duplicate detection (global across all runs + per run)
443
+ const trackingKey = runInfo.runId;
444
+ const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
445
+
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
+ if (submittedUuids.has(uuid)) {
471
+ const firstOccurrence = submittedUuids.get(uuid);
472
+ const firstFile = path.basename(firstOccurrence.file);
473
+ const currentFile = path.basename(test.location?.file || 'unknown');
474
+
475
+ this.duplicateTests++;
476
+ // Note: Don't increment skippedTests - duplicates have their own category
477
+ this.duplicateDetails.push({
478
+ uuid,
479
+ firstFile,
480
+ firstTitle: firstOccurrence.title,
481
+ duplicateFile: currentFile,
482
+ duplicateTitle: test.title
483
+ });
484
+
485
+ logger.warn(`Duplicate UUID detected: ${uuid}`);
486
+ logger.warn(` First: ${firstFile} -> "${firstOccurrence.title}"`);
487
+ logger.warn(` Dup: ${currentFile} -> "${test.title}"`);
488
+ return; // Skip duplicate submission
489
+ }
490
+
491
+ // Store UUID occurrence
492
+ submittedUuids.set(uuid, {
493
+ file: test.location?.file || 'unknown',
494
+ title: test.title,
495
+ browser: deviceInfo.browser
496
+ });
497
+ 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
+
506
+ // Count only tests that will be submitted (non-duplicate, non-orphan)
507
+ this.totalTests++;
508
+
360
509
  // Create result object
361
510
  const testResult = {
362
511
  runId: runInfo.runId, // Add runId for batch submission
@@ -398,19 +547,29 @@ class AppliqationReporter {
398
547
  * Called once after all tests complete
399
548
  */
400
549
  async onEnd(result) {
401
- logger.info('Test run complete. Submitting results...');
550
+ logger.info('Test run complete.');
402
551
 
403
552
  try {
404
- // Submit batched results
405
- if (this.config.batchSubmit) {
406
- await this.submitBatchedResults();
407
- }
553
+ // Skip submission if --appq flag not present
554
+ if (this.appqEnabled) {
555
+ logger.info('Submitting results to Appliqation...');
408
556
 
409
- // Submit orphan tests
410
- await this.submitOrphanTests();
557
+ // Submit batched results
558
+ if (this.config.batchSubmit) {
559
+ await this.submitBatchedResults();
560
+ }
561
+
562
+ // Submit orphan tests
563
+ await this.submitOrphanTests();
564
+ } else {
565
+ logger.info('Appliqation reporting disabled. Skipping result submission.');
566
+ }
411
567
 
412
- // Print summary
568
+ // Print summary (always show, even if not submitted)
413
569
  this.printSummary();
570
+
571
+ // Write summary to file (always write, even if not submitted)
572
+ await this.writeSummaryToFile();
414
573
  } catch (error) {
415
574
  logger.error('Error in onEnd', {
416
575
  error: error.message
@@ -453,6 +612,9 @@ class AppliqationReporter {
453
612
  }
454
613
  });
455
614
 
615
+ // Track backend validation rejections
616
+ this.backendRejectedTests += summary.failed || 0;
617
+
456
618
  logger.info(`Results submitted for run ${runId}`, {
457
619
  success: summary.success,
458
620
  failed: summary.failed,
@@ -558,23 +720,60 @@ class AppliqationReporter {
558
720
  * @private
559
721
  */
560
722
  printSummary() {
723
+ const testsSubmitted = this.totalTests;
724
+ const testsAccepted = testsSubmitted - this.backendRejectedTests;
725
+
561
726
  console.log('\n╔═══════════════════════════════════════════════════════════╗');
562
727
  console.log('║ Appliqation Test Results Summary ║');
563
728
  console.log('╠═══════════════════════════════════════════════════════════╣');
564
- console.log(`║ Total Tests: ${this.totalTests.toString().padStart(5)} ║`);
565
- console.log(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
566
- console.log(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
567
- console.log(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
568
- console.log(`║ Orphan (No UUID): ${this.orphanTests.toString().padStart(5)} ║`);
729
+
730
+ // Show reporting status
731
+ if (!this.appqEnabled) {
732
+ console.log('║ ⚠️ Appliqation Reporting: DISABLED ║');
733
+ console.log('║ (Set APPQ_ENABLE=1 or add -- --appq flag) ║');
734
+ console.log('╠═══════════════════════════════════════════════════════════╣');
735
+ } else {
736
+ console.log('║ Submitted to Backend: ║');
737
+ console.log(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
738
+ console.log(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
739
+ console.log(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
740
+ console.log('║ ║');
741
+ }
742
+ console.log('║ ║');
743
+ console.log('║ Test Execution Results (Playwright): ║');
744
+ console.log(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
745
+ console.log(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
746
+ console.log(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
747
+ console.log('║ ║');
748
+ console.log('║ Not Submitted: ║');
749
+ console.log(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
750
+ console.log(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
569
751
  console.log('╠═══════════════════════════════════════════════════════════╣');
570
- console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
571
752
 
572
- for (const [projectKey, runInfo] of this.runsByProject.entries()) {
573
- console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
753
+ // Only show run matrices if reporting is enabled
754
+ if (this.appqEnabled && this.runsByProject.size > 0) {
755
+ console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
756
+
757
+ for (const [projectKey, runInfo] of this.runsByProject.entries()) {
758
+ console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
759
+ }
574
760
  }
575
761
 
576
762
  console.log('╚═══════════════════════════════════════════════════════════╝\n');
577
763
 
764
+ 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}"`);
771
+ console.log('');
772
+ });
773
+ console.log(`Total: ${this.duplicateTests} duplicate test(s) skipped.`);
774
+ console.log('Each test should have a unique UUID within a browser.\n');
775
+ }
776
+
578
777
  if (this.orphanTests > 0) {
579
778
  console.log('⚠️ Warning: Some tests are missing UUID annotations!');
580
779
  console.log(' Add UUIDs to map tests to Appliqation test cases:');
@@ -582,6 +781,209 @@ class AppliqationReporter {
582
781
  }
583
782
  }
584
783
 
784
+ /**
785
+ * Write execution summary to file
786
+ * Creates timestamped txt file in AppQ_Execution_Summary folder
787
+ * @private
788
+ */
789
+ async writeSummaryToFile() {
790
+ const fs = require('fs').promises;
791
+ const path = require('path');
792
+
793
+ try {
794
+ this.executionEndTime = new Date();
795
+ const duration = this.executionEndTime - this.executionStartTime;
796
+
797
+ // Build output directory path
798
+ // If playwrightOutputDir already contains 'test-results' (from config._internal.outputDir),
799
+ // don't add it again. Otherwise, add 'test-results' to the path.
800
+ const baseDir = this.playwrightOutputDir;
801
+ const needsTestResults = !baseDir.includes('test-results');
802
+
803
+ const summaryDir = needsTestResults
804
+ ? path.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
805
+ : path.join(baseDir, 'AppQ_Execution_Summary');
806
+
807
+ // Create directory if it doesn't exist
808
+ await fs.mkdir(summaryDir, { recursive: true });
809
+
810
+ // Build filename with run title and timestamp
811
+ const runTitle = this.config.title || 'execution_summary';
812
+ const timestamp = this.formatDateTimeForFilename(this.executionStartTime);
813
+ const filename = `${runTitle}_${timestamp}.txt`;
814
+ const filepath = path.join(summaryDir, filename);
815
+
816
+ // Build comprehensive summary content
817
+ const summaryContent = this.buildSummaryContent(duration);
818
+
819
+ // Write to file
820
+ await fs.writeFile(filepath, summaryContent, 'utf8');
821
+
822
+ logger.info(`Execution summary saved to: ${filepath}`);
823
+ console.log(`\n📄 Execution summary saved: ${filename}`);
824
+
825
+ } catch (error) {
826
+ logger.error('Failed to write summary file', { error: error.message });
827
+ // Don't throw - file writing failure should not break test run
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Format date/time for filename: 2025-01-21_14-30-45
833
+ * @private
834
+ */
835
+ formatDateTimeForFilename(date) {
836
+ const year = date.getFullYear();
837
+ const month = String(date.getMonth() + 1).padStart(2, '0');
838
+ const day = String(date.getDate()).padStart(2, '0');
839
+ const hours = String(date.getHours()).padStart(2, '0');
840
+ const minutes = String(date.getMinutes()).padStart(2, '0');
841
+ const seconds = String(date.getSeconds()).padStart(2, '0');
842
+
843
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
844
+ }
845
+
846
+ /**
847
+ * Build comprehensive summary content for file
848
+ * @private
849
+ */
850
+ buildSummaryContent(duration) {
851
+ const lines = [];
852
+
853
+ // Header
854
+ lines.push('═══════════════════════════════════════════════════════════');
855
+ lines.push(' APPLIQATION TEST EXECUTION SUMMARY');
856
+ lines.push('═══════════════════════════════════════════════════════════');
857
+ lines.push('');
858
+
859
+ // Execution Metadata
860
+ lines.push('EXECUTION METADATA:');
861
+ lines.push('─────────────────────────────────────────────────────────');
862
+ lines.push(`Start Time: ${this.executionStartTime.toISOString()}`);
863
+ lines.push(`End Time: ${this.executionEndTime.toISOString()}`);
864
+ lines.push(`Duration: ${this.formatDuration(duration)}`);
865
+ lines.push(`Run Title: ${this.config.title || 'N/A'}`);
866
+ lines.push(`Appq Reporting: ${this.appqEnabled ? 'ENABLED' : 'DISABLED (Set APPQ_ENABLE=1 or add -- --appq flag)'}`);
867
+ lines.push('');
868
+
869
+ // Test Results Summary (ASCII Table from printSummary)
870
+ const testsSubmitted = this.totalTests;
871
+ const testsAccepted = testsSubmitted - this.backendRejectedTests;
872
+
873
+ lines.push('╔═══════════════════════════════════════════════════════════╗');
874
+ lines.push('║ Appliqation Test Results Summary ║');
875
+ lines.push('╠═══════════════════════════════════════════════════════════╣');
876
+
877
+ // Show submission status
878
+ if (!this.appqEnabled) {
879
+ lines.push('║ ⚠️ Appliqation Reporting: DISABLED ║');
880
+ lines.push('║ (Set APPQ_ENABLE=1 or add -- --appq flag) ║');
881
+ lines.push('╠═══════════════════════════════════════════════════════════╣');
882
+ } else {
883
+ lines.push('║ Submitted to Backend: ║');
884
+ lines.push(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
885
+ lines.push(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
886
+ lines.push(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
887
+ lines.push('║ ║');
888
+ }
889
+ lines.push('║ Test Execution Results (Playwright): ║');
890
+ lines.push(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
891
+ lines.push(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
892
+ lines.push(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
893
+ lines.push('║ ║');
894
+ lines.push('║ Not Submitted: ║');
895
+ lines.push(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
896
+ lines.push(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
897
+ lines.push('╠═══════════════════════════════════════════════════════════╣');
898
+
899
+ // Run IDs (only show if reporting is enabled)
900
+ if (this.appqEnabled && this.runsByProject.size > 0) {
901
+ lines.push(`║ Run Matrices Created: ${this.runsByProject.size.toString().padStart(5)} ║`);
902
+ for (const [projectKey, runInfo] of this.runsByProject) {
903
+ const displayKey = projectKey.length > 30 ? projectKey.substring(0, 27) + '...' : projectKey;
904
+ const paddedKey = displayKey.padEnd(30);
905
+ lines.push(`║ ${paddedKey}: ${runInfo.runId.padEnd(20)} ║`);
906
+ }
907
+ }
908
+
909
+ lines.push('╚═══════════════════════════════════════════════════════════╝');
910
+ lines.push('');
911
+
912
+ // Detailed Errors Section
913
+ if (this.duplicateTests > 0 || this.orphanTests > 0 || this.backendRejectedTests > 0) {
914
+ lines.push('');
915
+ lines.push('DETAILED ERRORS & WARNINGS:');
916
+ lines.push('═══════════════════════════════════════════════════════════');
917
+ lines.push('');
918
+ }
919
+
920
+ // Duplicate UUIDs Details
921
+ if (this.duplicateTests > 0 && this.duplicateDetails.length > 0) {
922
+ lines.push('DUPLICATE UUIDs DETECTED:');
923
+ 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}"`);
930
+ lines.push('');
931
+ });
932
+ }
933
+
934
+ // Orphan Tests Warning
935
+ if (this.orphanTests > 0) {
936
+ lines.push('ORPHAN TESTS (Missing UUID Annotations):');
937
+ lines.push('─────────────────────────────────────────────────────────');
938
+ lines.push(`Total orphan tests: ${this.orphanTests}`);
939
+ 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(' });');
945
+ lines.push('');
946
+ }
947
+
948
+ // Backend Rejections
949
+ if (this.backendRejectedTests > 0) {
950
+ lines.push('BACKEND VALIDATION REJECTIONS:');
951
+ lines.push('─────────────────────────────────────────────────────────');
952
+ lines.push(`Total rejected: ${this.backendRejectedTests}`);
953
+ lines.push('');
954
+ lines.push('These test cases were rejected due to project ownership validation.');
955
+ lines.push('The test case UUID does not belong to the project associated with');
956
+ lines.push('your API key. Verify the correct project and test case mappings.');
957
+ lines.push('');
958
+ }
959
+
960
+ // Footer
961
+ lines.push('═══════════════════════════════════════════════════════════');
962
+ lines.push(`Generated by Appliqation SDK v${require('../../../package.json').version}`);
963
+ lines.push(`Timestamp: ${new Date().toISOString()}`);
964
+ lines.push('═══════════════════════════════════════════════════════════');
965
+
966
+ return lines.join('\n');
967
+ }
968
+
969
+ /**
970
+ * Format duration in human-readable format
971
+ * @private
972
+ */
973
+ formatDuration(ms) {
974
+ const seconds = Math.floor(ms / 1000);
975
+ const minutes = Math.floor(seconds / 60);
976
+ const hours = Math.floor(minutes / 60);
977
+
978
+ if (hours > 0) {
979
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
980
+ } else if (minutes > 0) {
981
+ return `${minutes}m ${seconds % 60}s`;
982
+ } else {
983
+ return `${seconds}s`;
984
+ }
985
+ }
986
+
585
987
  /**
586
988
  * Extract project names that are actually running from the test suite
587
989
  * @private
File without changes
File without changes
File without changes
File without changes