@appliqation/automation-sdk 2.4.0 → 2.5.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.
@@ -4,91 +4,79 @@ const UuidExtractor = require('./helpers/UuidExtractor');
4
4
  const DeviceOsDetector = require('./helpers/DeviceOsDetector');
5
5
  const PayloadBuilder = require('../../utils/PayloadBuilder');
6
6
  const logger = require('../../utils/logger');
7
- const { DEFAULT_APPLIQATION_BASE_URL } = require('../../constants');
8
7
 
9
8
  /**
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
9
+ * Extract run title from CLI arguments
10
+ * Supports: --appq-title="My Title" or --appq-title=MyTitle
11
+ * @returns {string|null}
13
12
  */
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;
13
+ function getRunTitleFromCli() {
14
+ const argv = process.argv || [];
15
+ for (const arg of argv) {
16
+ if (arg.startsWith('--appq-title=') || arg.startsWith('--appq_run_title=')) {
17
+ let value = arg.substring(arg.indexOf('=') + 1);
18
+ value = value.replace(/^["']|["']$/g, '');
19
+ return value || null;
20
+ }
23
21
  }
22
+ return null;
23
+ }
24
24
 
25
- // Check CLI flag (requires -- separator: npx playwright test -- --appq)
25
+ /**
26
+ * Extract environment from CLI arguments
27
+ * Supports: --appq-env=Stage or --appq-env Stage
28
+ * @returns {string|null}
29
+ */
30
+ function getEnvironmentFromCli() {
26
31
  const argv = process.argv || [];
27
- return argv.includes('--appq');
32
+ for (let i = 0; i < argv.length; i++) {
33
+ if (argv[i].startsWith('--appq-env=')) {
34
+ return argv[i].split('=')[1] || null;
35
+ }
36
+ if (argv[i] === '--appq-env' && argv[i + 1] && !argv[i + 1].startsWith('--')) {
37
+ return argv[i + 1];
38
+ }
39
+ }
40
+ return null;
28
41
  }
29
42
 
30
43
  /**
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
44
+ * Extract existing run ID from CLI arguments.
45
+ * Supports: --appq-run-id=run_abc123 or --appq_run_id=run_abc123
46
+ * @returns {string|null}
34
47
  */
35
- function getRunTitleFromCli() {
48
+ function getRunIdFromCli() {
36
49
  const argv = process.argv || [];
37
-
38
- // Look for --appq_run_title=value or --appq_run_title="value"
39
50
  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
51
+ if (arg.startsWith('--appq-run-id=') || arg.startsWith('--appq_run_id=')) {
52
+ let value = arg.substring(arg.indexOf('=') + 1);
45
53
  value = value.replace(/^["']|["']$/g, '');
46
-
47
54
  return value || null;
48
55
  }
49
56
  }
50
-
51
57
  return null;
52
58
  }
53
59
 
54
60
  /**
55
61
  * Appliqation Reporter for Playwright
56
- * Automatically reports test results to Appliqation platform
62
+ *
63
+ * Enabled only when --appq CLI flag is present.
64
+ * Never blocks Playwright test execution — all errors degrade to warnings.
57
65
  *
58
66
  * @example
59
- * // playwright.config.js
60
- * import { AppliqationReporter } from '@appliqation/automation-sdk/playwright';
67
+ * // playwright.config.js — zero-config
68
+ * reporter: [
69
+ * ['list'],
70
+ * ['@appliqation/automation-sdk/playwright/reporter'],
71
+ * ]
61
72
  *
62
- * export default {
63
- * reporter: [
64
- * ['list'],
65
- * [AppliqationReporter, {
66
- * baseUrl: 'https://your-instance.appliqation.com',
67
- * apiKey: process.env.APPLIQATION_API_KEY,
68
- * projectKey: process.env.APPLIQATION_PROJECT_KEY,
69
- * scenarioId: 123,
70
- * environment: 'Production'
71
- * }]
72
- * ]
73
- * };
73
+ * // Run: npx playwright test -- --appq --appq-env=Stage
74
74
  */
75
75
  class AppliqationReporter {
76
- /**
77
- * Create Appliqation reporter
78
- * @param {Object} config - Reporter configuration
79
- * @param {string} config.baseUrl - Appliqation instance URL
80
- * @param {string} config.apiKey - API key
81
- * @param {string} config.projectKey - Project key
82
- * @param {number} config.scenarioId - Scenario ID
83
- * @param {number} [config.testSetId] - Test Set ID (alternative to scenarioId)
84
- * @param {string} [config.environment='Local'] - Environment name
85
- * @param {boolean} [config.autoCreateRun=true] - Auto-create run matrix
86
- * @param {boolean} [config.logOrphans=true] - Log orphan tests
87
- * @param {boolean} [config.batchSubmit=true] - Use batch submission
88
- * @param {number} [config.batchSize=50] - Batch size for submissions
89
- * @param {string} [config.logLevel='info'] - Log level
90
- */
91
- constructor(config) {
76
+ constructor(config = {}) {
77
+ // 1. Check enablement FIRST — --appq flag is the only trigger
78
+ this.appqEnabled = process.argv.includes('--appq');
79
+
92
80
  this.config = {
93
81
  autoCreateRun: true,
94
82
  logOrphans: true,
@@ -100,364 +88,305 @@ class AppliqationReporter {
100
88
  ...config
101
89
  };
102
90
 
103
- // Apply baseUrl fallback if not provided
104
- // Priority: 1) config.baseUrl, 2) process.env.APPLIQATION_BASE_URL, 3) DEFAULT_APPLIQATION_BASE_URL
105
- if (!this.config.baseUrl) {
106
- this.config.baseUrl = process.env.APPLIQATION_BASE_URL || DEFAULT_APPLIQATION_BASE_URL;
107
- }
108
-
109
- // Apply environment from process.env if not in config
110
- // Priority: 1) config.environment, 2) process.env.APPLIQATION_ENVIRONMENT
111
- if (!this.config.environment) {
112
- this.config.environment = process.env.APPLIQATION_ENVIRONMENT;
91
+ // Resolve environment: CLI --appq-env > env var > config
92
+ const cliEnv = getEnvironmentFromCli();
93
+ if (cliEnv) {
94
+ this.config.environment = cliEnv;
95
+ } else if (!this.config.environment) {
96
+ this.config.environment = process.env.APPLIQATION_ENVIRONMENT || null;
113
97
  }
114
- // Note: No default fallback - validation will catch missing value
115
98
 
116
- // Apply title fallback if not provided
117
- // Priority: 1) config.title, 2) CLI --appq_run_title, 3) process.env.APPLIQATION_RUN_TITLE, 4) timestamp-based default
99
+ // Resolve title: CLI --appq-title > env var > config > timestamp
118
100
  if (!this.config.title) {
119
101
  const cliTitle = getRunTitleFromCli();
120
102
  this.config.title = cliTitle || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`;
121
103
  }
122
104
 
123
- // Check if --appq flag is present (opt-in approach)
124
- this.appqEnabled = isAppqEnabled();
105
+ // Check for existing run ID (TDD iteration mode)
106
+ this.existingRunId = this.config.runId || getRunIdFromCli() || process.env.APPLIQATION_RUN_ID || null;
125
107
 
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');
108
+ // If disabled, stop here — true no-op
109
+ if (!this.appqEnabled) {
110
+ this.client = null;
111
+ this._initState();
112
+ return;
134
113
  }
135
114
 
136
- // Initialize SDK client (needed for UUID validation even if disabled)
137
- this.client = new AppliqationClient({
138
- baseUrl: this.config.baseUrl,
139
- apiKey: this.config.apiKey,
140
- projectKey: this.config.projectKey,
141
- rejectUnauthorized: this.config.rejectUnauthorized, // Pass through SSL configuration
142
- options: {
143
- logLevel: this.config.logLevel,
144
- logOrphans: this.config.logOrphans
115
+ // 2. Try to create client wrapped in try-catch (never crashes)
116
+ try {
117
+ const resolved = AppliqationClient.resolveConfig(this.config);
118
+
119
+ if (!resolved.apiKey) {
120
+ console.warn('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
121
+ console.warn('[Appliqation] Tests will continue without reporting.');
122
+ this.appqEnabled = false;
123
+ this.client = null;
124
+ this._initState();
125
+ return;
145
126
  }
146
- });
147
127
 
148
- // State tracking
149
- this.runsByProject = new Map(); // Map<projectKey, runInfo>
150
- this.resultsByRun = new Map(); // Map<runId, results[]>
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}>
154
- this.browserVersionDetected = new Map(); // Map<projectKey, boolean> - Track if version detected
128
+ this.client = new AppliqationClient({
129
+ baseUrl: resolved.baseUrl,
130
+ apiKey: resolved.apiKey,
131
+ projectKey: resolved.projectKey,
132
+ rejectUnauthorized: this.config.rejectUnauthorized,
133
+ options: {
134
+ logLevel: this.config.logLevel,
135
+ logOrphans: this.config.logOrphans
136
+ }
137
+ });
138
+
139
+ logger.info('Appliqation reporting ENABLED');
140
+ console.log('[Appliqation] Reporting enabled.');
141
+ } catch (err) {
142
+ console.warn(`[Appliqation] Setup failed: ${err.message}. Tests will continue without reporting.`);
143
+ this.appqEnabled = false;
144
+ this.client = null;
145
+ }
146
+
147
+ this._initState();
148
+ }
149
+
150
+ /**
151
+ * Initialize state tracking
152
+ * @private
153
+ */
154
+ _initState() {
155
+ this.runsByProject = new Map();
156
+ this.resultsByRun = new Map();
157
+ this.orphansByRun = new Map();
158
+ this.submittedUuidsByRun = new Map();
159
+ this.submittedUuidsGlobal = new Map();
160
+ this.browserVersionDetected = new Map();
155
161
  this.totalTests = 0;
156
162
  this.passedTests = 0;
157
163
  this.failedTests = 0;
158
164
  this.skippedTests = 0;
159
165
  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
166
+ this.validTests = 0;
167
+ this.duplicateTests = 0;
168
+ this.duplicateDetails = [];
169
+ this.backendRejectedTests = 0;
170
+ this.rejectionsByRun = new Map();
171
+ this.deletedOrphanRuns = [];
172
+ this.uniqueOrphans = new Map();
173
+ this.uniqueDuplicates = new Map();
170
174
  this.executionStartTime = null;
171
175
  this.executionEndTime = null;
172
176
  this.playwrightOutputDir = null;
173
-
174
- // Show baseUrl only in DEBUG mode
175
- logger.info('Appliqation Playwright Reporter initialized', {
176
- environment: this.config.environment
177
- });
178
- logger.debug('Reporter configuration', {
179
- baseUrl: this.config.baseUrl,
180
- scenarioId: this.config.scenarioId
181
- });
182
177
  }
183
178
 
184
179
  /**
185
- * Called once before running tests
186
- * @param {Object} config - Playwright config
187
- * @param {Object} suite - Root test suite
180
+ * Called once before running tests — non-blocking
188
181
  */
189
182
  async onBegin(config, suite) {
190
- // Capture execution start time for summary file
191
183
  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
184
  this.playwrightOutputDir = config._internal?.outputDir || config.rootDir || process.cwd();
198
185
 
199
- logger.info('Test run starting...');
186
+ if (!this.appqEnabled) return;
200
187
 
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
-
207
- if (!this.config.autoCreateRun) {
208
- logger.info('Auto-create run disabled. Skipping run matrix creation.');
209
- return;
210
- }
211
-
212
- // Auto-configure project from API key if needed
213
188
  try {
214
- await this.client.ensureProjectConfig();
215
-
216
- // Use auto-detected environment if not explicitly provided
217
- if (!this.config.environment && this.client._projectInfo) {
218
- this.config.environment = this.client._projectInfo.default_environment;
219
- logger.info('Using auto-detected default environment', {
220
- environment: this.config.environment,
221
- source: 'api_key_project_config'
222
- });
223
- }
224
- } catch (error) {
225
- logger.error('Failed to auto-configure from API key', {
226
- error: error.message
227
- });
228
- console.error('\n❌ Auto-configuration failed:', error.message);
229
- throw error;
230
- }
231
-
232
- // Get project names that are actually running (honors --project filter)
233
- const runningProjectNames = this.getRunningProjectNames(suite);
234
- logger.debug('Projects actually running:', runningProjectNames);
235
-
236
- // Filter projects to only those that are:
237
- // 1. Actually running (honors --project filter)
238
- // 2. NOT API-specific projects
239
- const browserProjects = config.projects.filter(project => {
240
- // CRITICAL: Only include projects that are actually being executed
241
- // This honors the --project CLI filter
242
- if (runningProjectNames.length > 0 && !runningProjectNames.includes(project.name)) {
243
- logger.debug(`Excluding project "${project.name}" - not in running projects list`);
244
- return false;
189
+ // Auto-discover project from API key
190
+ await this.client.initialize();
191
+
192
+ // Validate environment
193
+ if (!this.config.environment) {
194
+ console.warn('[Appliqation] Environment required. Use --appq-env=Stage');
195
+ console.warn('[Appliqation] Tests will continue without reporting.');
196
+ this.appqEnabled = false;
197
+ return;
245
198
  }
246
199
 
247
- // Check if testMatch is a custom API-specific pattern
248
- if (project.testMatch) {
249
- const testMatchStr = project.testMatch.toString();
250
- // Exclude API testing projects (testMatch contains ".api")
251
- if (testMatchStr.includes('.api')) {
252
- logger.debug(`Excluding API project "${project.name}" from matrix creation`);
253
- return false;
200
+ // Validate against available environments (if known from auto-discovery)
201
+ if (this.client.availableEnvironments?.length) {
202
+ const normalizedAvail = this.client.availableEnvironments.map(e => e.toUpperCase());
203
+ if (!normalizedAvail.includes(this.config.environment.toUpperCase())) {
204
+ console.warn(`[Appliqation] Invalid environment "${this.config.environment}".`);
205
+ console.warn(`[Appliqation] Available: ${this.client.availableEnvironments.join(', ')}`);
206
+ console.warn('[Appliqation] Tests will continue without reporting.');
207
+ this.appqEnabled = false;
208
+ return;
254
209
  }
255
210
  }
256
- return true;
257
- });
258
211
 
259
- logger.debug(`Creating matrices for ${browserProjects.length} browser projects (filtered from ${config.projects.length} total)`);
260
-
261
- // Auto-detect browser versions for projects that don't have metadata.browserVersion configured
262
- await this.injectBrowserVersions(browserProjects);
212
+ if (!this.config.autoCreateRun) {
213
+ logger.info('Auto-create run disabled. Skipping run matrix creation.');
214
+ return;
215
+ }
263
216
 
264
- // Get unique device/OS/browser matrix configurations from browser projects only
265
- const matrixConfigs = DeviceOsDetector.getMatrixConfigurations(browserProjects);
217
+ // TDD mode: reuse existing run ID
218
+ if (this.existingRunId) {
219
+ logger.info(`TDD mode: Reusing existing run ID: ${this.existingRunId}`);
220
+ console.log(`[Appliqation] TDD mode: Reusing run ${this.existingRunId}`);
266
221
 
267
- logger.info(`Creating ${matrixConfigs.length} run matrices for device/OS combinations...`);
268
- logger.debug(`Matrix configurations:`, matrixConfigs);
222
+ const runInfo = {
223
+ runId: this.existingRunId,
224
+ device: 'Desktop',
225
+ os: 'Unknown',
226
+ browsers: []
227
+ };
269
228
 
270
- // Create runs for each device/OS combination in parallel for better performance
271
- const creationStartTime = Date.now();
272
- const creationPromises = matrixConfigs.map(async (matrixConfig) => {
273
- const projectKey = `${matrixConfig.device}-${matrixConfig.os}`;
229
+ for (const project of config.projects) {
230
+ const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
231
+ const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
232
+ if (!this.runsByProject.has(projectKey)) {
233
+ this.runsByProject.set(projectKey, { ...runInfo, device: deviceInfo.device, os: deviceInfo.os });
234
+ this.resultsByRun.set(this.existingRunId, this.resultsByRun.get(this.existingRunId) || []);
235
+ this.orphansByRun.set(this.existingRunId, this.orphansByRun.get(this.existingRunId) || []);
236
+ }
237
+ }
238
+ return;
239
+ }
274
240
 
275
- // Skip if already created (shouldn't happen but safety check)
276
- if (this.runsByProject.has(projectKey)) return null;
241
+ // Get running project names
242
+ const runningProjectNames = this.getRunningProjectNames(suite);
243
+ logger.debug('Projects actually running:', runningProjectNames);
277
244
 
278
- try {
279
- const runOptions = {
280
- scenarioId: this.config.scenarioId,
281
- testSetId: this.config.testSetId,
282
- environment: this.config.environment,
283
- browsers: matrixConfig.browsers, // ALL browsers for this device/OS
284
- device: matrixConfig.device,
285
- os: matrixConfig.os,
286
- title: this.config.title || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`
287
- };
245
+ const browserProjects = config.projects.filter(project => {
246
+ if (runningProjectNames.length > 0 && !runningProjectNames.includes(project.name)) {
247
+ return false;
248
+ }
249
+ if (project.testMatch) {
250
+ const testMatchStr = project.testMatch.toString();
251
+ if (testMatchStr.includes('.api')) return false;
252
+ }
253
+ return true;
254
+ });
288
255
 
289
- const run = await this.client.createRun(runOptions);
256
+ // Auto-detect browser versions
257
+ await this.injectBrowserVersions(browserProjects);
290
258
 
291
- const runInfo = {
292
- ...run,
293
- device: matrixConfig.device,
294
- os: matrixConfig.os,
295
- browsers: matrixConfig.browsers
296
- };
259
+ // Get matrix configurations
260
+ const matrixConfigs = DeviceOsDetector.getMatrixConfigurations(browserProjects);
297
261
 
298
- this.runsByProject.set(projectKey, runInfo);
299
- this.resultsByRun.set(run.runId, []);
300
- this.orphansByRun.set(run.runId, []);
262
+ logger.info(`Creating ${matrixConfigs.length} run matrices...`);
301
263
 
302
- logger.info(`Run matrix created for ${projectKey}`, {
303
- runId: run.runId,
304
- browsers: matrixConfig.browsers
305
- });
264
+ const creationStartTime = Date.now();
265
+ const creationPromises = matrixConfigs.map(async (matrixConfig) => {
266
+ const projectKey = `${matrixConfig.device}-${matrixConfig.os}`;
267
+ if (this.runsByProject.has(projectKey)) return null;
306
268
 
307
- return { projectKey, runId: run.runId };
308
- } catch (error) {
309
- // Check if this is an API validation error with details
310
- if (error.details) {
311
- const details = error.details;
312
-
313
- // Log structured validation error
314
- logger.error(`API validation error for ${projectKey}`, {
315
- error_code: details.error_code,
316
- message: details.message,
317
- status: error.statusCode,
318
- details: details
269
+ try {
270
+ const runOptions = {
271
+ scenarioId: this.config.scenarioId,
272
+ testSetId: this.config.testSetId,
273
+ environment: this.config.environment,
274
+ browsers: matrixConfig.browsers,
275
+ device: matrixConfig.device,
276
+ os: matrixConfig.os,
277
+ title: this.config.title
278
+ };
279
+
280
+ const run = await this.client.createRun(runOptions);
281
+
282
+ const runInfo = {
283
+ ...run,
284
+ device: matrixConfig.device,
285
+ os: matrixConfig.os,
286
+ browsers: matrixConfig.browsers
287
+ };
288
+
289
+ this.runsByProject.set(projectKey, runInfo);
290
+ this.resultsByRun.set(run.runId, []);
291
+ this.orphansByRun.set(run.runId, []);
292
+
293
+ logger.info(`Run matrix created for ${projectKey}`, {
294
+ runId: run.runId, browsers: matrixConfig.browsers
319
295
  });
320
296
 
321
- // Display formatted validation error to user
322
- console.error('\n' + '═'.repeat(80));
323
- console.error('❌ ENVIRONMENT VALIDATION ERROR');
324
- console.error('═'.repeat(80));
325
- console.error(`Project ID: ${details.project_id || this.config.projectKey}`);
326
- console.error(`Device/OS: ${projectKey}`);
327
- console.error(`Error: ${details.message || error.message}`);
328
-
329
- if (details.error_code === 'NO_ENVIRONMENTS_CONFIGURED') {
330
- console.error('\n⚠️ ACTION REQUIRED:');
331
- console.error(` ${details.action_required || 'Configure testing environments in project settings'}`);
332
- if (details.help_url) {
333
- console.error(` URL: ${details.help_url}`);
334
- }
335
- } else if (details.error_code === 'INVALID_ENVIRONMENT') {
336
- console.error(`\nProvided Environment: "${details.provided_environment}"`);
337
- if (details.valid_environments && details.valid_environments.length > 0) {
338
- console.error('\n✓ Valid Environments:');
339
- details.valid_environments.forEach(env => {
340
- console.error(` - ${env}`);
341
- });
342
- }
343
- if (details.hint) {
344
- console.error(`\n💡 Hint: ${details.hint}`);
297
+ return { projectKey, runId: run.runId };
298
+ } catch (error) {
299
+ // Display validation error details if available
300
+ if (error.details) {
301
+ const details = error.details;
302
+ if (details.error_code === 'INVALID_ENVIRONMENT') {
303
+ console.warn(`[Appliqation] Invalid environment "${details.provided_environment}".`);
304
+ if (details.valid_environments?.length) {
305
+ console.warn(`[Appliqation] Available: ${details.valid_environments.join(', ')}`);
306
+ }
307
+ } else {
308
+ console.warn(`[Appliqation] ${details.message || error.message}`);
345
309
  }
310
+ } else {
311
+ console.warn(`[Appliqation] Run creation failed for ${projectKey}: ${error.message}`);
346
312
  }
313
+ return null; // Don't throw — allow other matrices to succeed
314
+ }
315
+ });
347
316
 
348
- console.error('═'.repeat(80) + '\n');
317
+ // Wait for all — wrapped in try-catch
318
+ try {
319
+ await Promise.all(creationPromises);
320
+ } catch (err) {
321
+ console.warn(`[Appliqation] ${err.message}`);
322
+ }
349
323
 
350
- // Throw error to fail the test run visibly
351
- throw new Error(`Environment validation failed: ${details.message || error.message}`);
352
- } else {
353
- // Generic error without validation details
354
- logger.error(`Failed to create run matrix for ${projectKey}`, {
355
- error: error.message,
356
- stack: error.stack
357
- });
358
- console.error(`⚠️ WARNING: Run matrix creation failed for ${projectKey}: ${error.message}`);
324
+ const creationDuration = Date.now() - creationStartTime;
359
325
 
360
- // Throw error to fail the test run
361
- throw error;
362
- }
326
+ if (this.runsByProject.size > 0) {
327
+ console.log(`[Appliqation] Created ${this.runsByProject.size} run matrix(es) in ${(creationDuration / 1000).toFixed(2)}s`);
328
+ } else {
329
+ console.warn('[Appliqation] No run matrices created. Tests will continue without reporting.');
330
+ this.appqEnabled = false;
363
331
  }
364
- });
365
332
 
366
- // Wait for all run creations to complete in parallel
367
- await Promise.all(creationPromises);
368
-
369
- const creationDuration = Date.now() - creationStartTime;
370
- logger.info(`All run matrices created in ${creationDuration}ms`);
371
- console.log(`✅ Created ${this.runsByProject.size} run matrix(es) in ${(creationDuration / 1000).toFixed(2)}s`);
333
+ } catch (err) {
334
+ console.warn(`[Appliqation] ${err.message}`);
335
+ console.warn('[Appliqation] Tests will continue without reporting.');
336
+ this.appqEnabled = false;
337
+ }
372
338
  }
373
339
 
374
340
  /**
375
- * Wait for run matrix to be available (handles race condition with slow backend)
376
- * @param {string} projectKey - The project key to wait for
377
- * @param {number} maxWaitMs - Maximum time to wait in milliseconds (default 60s for slow backends)
378
- * @returns {Promise<Object|null>} Run info object or null if timeout
341
+ * Wait for run matrix to be available
379
342
  */
380
343
  async waitForRunMatrix(projectKey, maxWaitMs = 60000) {
381
344
  const startTime = Date.now();
382
- const pollInterval = 500; // Check every 500ms
345
+ const pollInterval = 500;
383
346
  let warningShown = false;
384
347
 
385
348
  while (Date.now() - startTime < maxWaitMs) {
386
349
  const runInfo = this.runsByProject.get(projectKey);
350
+ if (runInfo) return runInfo;
387
351
 
388
- if (runInfo) {
389
- const waitedMs = Date.now() - startTime;
390
- if (waitedMs > 1000) {
391
- logger.info(`Run matrix became available after ${waitedMs}ms`, { projectKey });
392
- }
393
- return runInfo;
394
- }
395
-
396
- // Show warning if waiting too long (5 seconds)
397
352
  if (!warningShown && Date.now() - startTime > 5000) {
398
- console.warn(`⏳ Waiting for run matrix creation for ${projectKey}... (this may take a moment with slow backends)`);
399
353
  logger.warn(`Still waiting for run matrix after 5 seconds`, { projectKey });
400
354
  warningShown = true;
401
355
  }
402
356
 
403
- // Wait before next poll
404
357
  await new Promise(resolve => setTimeout(resolve, pollInterval));
405
358
  }
406
359
 
407
- // Timeout reached
408
360
  logger.error(`Timeout waiting for run matrix after ${maxWaitMs}ms`, { projectKey });
409
- console.error(`\n❌ TIMEOUT: Run matrix creation took longer than ${maxWaitMs / 1000}s for ${projectKey}`);
410
- console.error(` This usually indicates slow backend response or browser version detection delays.`);
411
- console.error(` Try adding browserVersion to your project metadata to speed up run creation.\n`);
412
361
  return null;
413
362
  }
414
363
 
415
364
  /**
416
- * Called after a test completes
417
- * @param {Object} test - Test object
418
- * @param {Object} result - Test result
365
+ * Called after a test completes — non-blocking
419
366
  */
420
367
  async onTestEnd(test, result) {
421
- // Note: Don't increment totalTests here - it's incremented later for non-duplicates/non-orphans only
368
+ if (!this.appqEnabled) return;
422
369
 
423
370
  try {
424
- // Extract UUID from test - check result.annotations first since test.info().annotations.push() adds to result
425
- logger.debug('Extracting UUID for test', {
426
- title: test.title,
427
- resultAnnotations: result.annotations || [],
428
- testAnnotations: test.annotations || []
429
- });
430
371
  const uuid = UuidExtractor.extractFromAnnotations(result.annotations || []) || UuidExtractor.extractFromTest(test);
431
- logger.debug('UUID extraction result', { title: test.title, uuid });
432
372
 
433
- // Get device info from project
434
- // Note: project() is a method, not a property
435
373
  const project = test.parent?.project?.() || test.parent?.project;
436
-
437
- // Browser version auto-detection happens during onBegin via getBrowserVersionFromPlaywright()
438
- // No need to extract browser instance here - version is already in project metadata
439
374
  const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
440
375
  const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
441
376
 
442
- // Wait for run matrix to be available (handles race condition with slow backend)
443
377
  const runInfo = await this.waitForRunMatrix(projectKey, 30000);
444
378
 
445
379
  if (!runInfo) {
446
- logger.error('Run matrix not available after timeout - result will not be submitted', {
447
- projectKey,
448
- testTitle: test.title,
449
- testFile: test.location?.file
380
+ logger.error('Run matrix not available result not submitted', {
381
+ projectKey, testTitle: test.title
450
382
  });
451
- console.error(`\n⚠️ ERROR: Run matrix not created for ${projectKey}. Test result for "${test.title}" will not be submitted.\n`);
452
383
  return;
453
384
  }
454
385
 
455
386
  if (!uuid) {
456
- // Orphan test - no UUID found
387
+ // Orphan test
457
388
  const orphanKey = `${test.location?.file || 'unknown'}:${test.title}`;
458
-
459
389
  if (!this.uniqueOrphans.has(orphanKey)) {
460
- // First occurrence of this unique orphan
461
390
  this.orphanTests++;
462
391
  this.uniqueOrphans.set(orphanKey, {
463
392
  file: path.basename(test.location?.file || 'unknown'),
@@ -465,79 +394,48 @@ class AppliqationReporter {
465
394
  browsers: [deviceInfo.browser]
466
395
  });
467
396
  } else {
468
- // Same orphan on different browser - just add browser to list
469
397
  const orphan = this.uniqueOrphans.get(orphanKey);
470
398
  if (!orphan.browsers.includes(deviceInfo.browser)) {
471
399
  orphan.browsers.push(deviceInfo.browser);
472
400
  }
473
401
  }
474
-
475
402
  this.trackOrphan(runInfo.runId, test, result, deviceInfo.browser);
476
-
477
- logger.warn('Test without UUID', {
478
- title: test.title,
479
- file: test.location?.file,
480
- browser: deviceInfo.browser
481
- });
482
-
483
403
  return;
484
404
  }
485
405
 
486
- // Duplicate detection (per-run AND per-browser - allows same UUID across different browsers/devices)
406
+ // Duplicate detection
487
407
  const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
488
408
  const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
489
409
 
490
- // Check per-run+browser map (allows same UUID for different browsers in same run)
491
410
  if (submittedUuids.has(uuid)) {
492
411
  const firstOccurrence = submittedUuids.get(uuid);
493
412
  const firstFile = path.basename(firstOccurrence.file);
494
413
  const currentFile = path.basename(test.location?.file || 'unknown');
495
414
 
496
- // Track unique duplicates
497
415
  if (!this.uniqueDuplicates.has(uuid)) {
498
- // First time seeing this duplicate UUID
499
416
  this.duplicateTests++;
500
417
  this.uniqueDuplicates.set(uuid, {
501
418
  uuid,
502
419
  occurrences: [
503
- {
504
- file: firstFile,
505
- title: firstOccurrence.title,
506
- browser: firstOccurrence.browser
507
- },
508
- {
509
- file: currentFile,
510
- title: test.title,
511
- browser: deviceInfo.browser
512
- }
420
+ { file: firstFile, title: firstOccurrence.title, browser: firstOccurrence.browser },
421
+ { file: currentFile, title: test.title, browser: deviceInfo.browser }
513
422
  ]
514
423
  });
515
424
  } else {
516
- // Already tracking this duplicate - add this occurrence
517
- const duplicate = this.uniqueDuplicates.get(uuid);
518
- duplicate.occurrences.push({
519
- file: currentFile,
520
- title: test.title,
521
- browser: deviceInfo.browser
425
+ this.uniqueDuplicates.get(uuid).occurrences.push({
426
+ file: currentFile, title: test.title, browser: deviceInfo.browser
522
427
  });
523
428
  }
524
429
 
525
- // Keep old duplicateDetails for backward compatibility (if needed)
526
430
  this.duplicateDetails.push({
527
- uuid,
528
- firstFile,
529
- firstTitle: firstOccurrence.title,
530
- duplicateFile: currentFile,
531
- duplicateTitle: test.title
431
+ uuid, firstFile, firstTitle: firstOccurrence.title,
432
+ duplicateFile: currentFile, duplicateTitle: test.title
532
433
  });
533
434
 
534
435
  logger.warn(`Duplicate UUID detected: ${uuid}`);
535
- logger.warn(` First: ${firstFile} -> "${firstOccurrence.title}"`);
536
- logger.warn(` Dup: ${currentFile} -> "${test.title}"`);
537
- return; // Skip duplicate submission
436
+ return;
538
437
  }
539
438
 
540
- // Store UUID occurrence (per-run only)
541
439
  submittedUuids.set(uuid, {
542
440
  file: test.location?.file || 'unknown',
543
441
  title: test.title,
@@ -545,13 +443,11 @@ class AppliqationReporter {
545
443
  });
546
444
  this.submittedUuidsByRun.set(trackingKey, submittedUuids);
547
445
 
548
- // Count only tests that will be submitted (non-duplicate, non-orphan)
549
446
  this.totalTests++;
550
- this.validTests++; // Track tests with valid UUIDs
447
+ this.validTests++;
551
448
 
552
- // Create result object
553
449
  const testResult = {
554
- runId: runInfo.runId, // Add runId for batch submission
450
+ runId: runInfo.runId,
555
451
  uuid,
556
452
  status: this.mapStatus(result.status),
557
453
  browser: deviceInfo.browser,
@@ -562,304 +458,135 @@ class AppliqationReporter {
562
458
  timestamp: Math.floor(Date.now() / 1000)
563
459
  };
564
460
 
565
- // Track result for batch submission
566
461
  this.trackResult(runInfo.runId, testResult);
567
462
 
568
- // Update counters
569
- if (testResult.status === 'Pass') {
570
- this.passedTests++;
571
- } else if (testResult.status === 'Fail') {
572
- this.failedTests++;
573
- } else if (testResult.status === 'Skipped') {
574
- this.skippedTests++;
575
- }
463
+ if (testResult.status === 'Pass') this.passedTests++;
464
+ else if (testResult.status === 'Fail') this.failedTests++;
465
+ else if (testResult.status === 'Skipped') this.skippedTests++;
576
466
 
577
- // Submit immediately if batch mode disabled
578
467
  if (!this.config.batchSubmit) {
579
468
  await this.client.submitResult(runInfo.runId, testResult);
580
469
  }
581
470
  } catch (error) {
582
- logger.error('Error processing test result', {
583
- error: error.message,
584
- test: test.title
585
- });
471
+ logger.error('Error processing test result', { error: error.message, test: test.title });
586
472
  }
587
473
  }
588
474
 
589
475
  /**
590
- * Handle runs that have only orphan tests (no valid UUIDs).
591
- * Deletes empty runs and shows clear error messages.
592
- *
476
+ * Handle orphan-only runs delete empty runs
593
477
  * @private
594
- * @returns {Promise<Array>} Array of deleted runs
595
478
  */
596
479
  async handleOrphanOnlyRuns() {
597
- if (!this.config.deleteOrphanOnlyRuns) {
598
- return []; // Feature disabled
599
- }
600
-
480
+ if (!this.config.deleteOrphanOnlyRuns) return [];
601
481
  const deletedRuns = [];
602
482
 
603
483
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
604
484
  const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
605
485
  const validCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
606
486
 
607
- // CRITICAL CHECK: Delete ONLY if all tests are orphans
608
487
  if (orphanCount > 0 && validCount === 0) {
609
- // Show clear error message FIRST (before attempting deletion)
610
- // This ensures users always see the message, even if deletion fails
611
488
  this.printOrphanOnlyError(orphanCount, projectKey);
612
-
613
489
  try {
614
- logger.warn('Deleting orphan-only run', {
615
- runId: runInfo.runId,
616
- orphanCount,
617
- projectKey
618
- });
619
-
620
490
  await this.client.deleteRun(runInfo.runId, 'all_tests_orphaned');
621
- deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
622
491
  } catch (error) {
623
- logger.error('Failed to delete orphan-only run (may already be deleted)', {
624
- error: error.message,
625
- runId: runInfo.runId
626
- });
627
- // Still track as deleted since user saw the error message
628
- deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
492
+ logger.error('Failed to delete orphan-only run', { error: error.message, runId: runInfo.runId });
629
493
  }
494
+ deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
630
495
  }
631
496
  }
632
497
 
633
- // Store for summary
634
498
  this.deletedOrphanRuns = deletedRuns;
635
499
  return deletedRuns;
636
500
  }
637
501
 
638
502
  /**
639
- * Print error message for orphan-only runs.
640
- *
641
- * @param {number} orphanCount - Number of orphan tests
642
- * @param {string} projectKey - Project key
643
503
  * @private
644
504
  */
645
505
  printOrphanOnlyError(orphanCount, projectKey) {
646
- console.error('\n╔════════════════════════════════════════════════════════════════════╗');
647
- console.error('║ ❌ RUN CREATION FAILED - ALL TESTS MISSING UUID ANNOTATIONS ║');
648
- console.error('╠════════════════════════════════════════════════════════════════════╣');
649
- console.error(`║ Project: ${projectKey.padEnd(56)} ║`);
650
- console.error(`║ Orphan Tests: ${orphanCount.toString().padEnd(51)} ║`);
651
- console.error('║ ║');
652
- console.error(' ⚠️ NO RESULTS WERE SUBMITTED TO APPLIQATION ║');
653
- console.error('║ The test run was automatically deleted to prevent analytics ║');
654
- console.error(' corruption. All tests are missing UUID annotations. ║');
655
- console.error('╠════════════════════════════════════════════════════════════════════╣');
656
- console.error('║ ✅ ACTION REQUIRED: Add UUID Annotations ║');
657
- console.error('╠════════════════════════════════════════════════════════════════════╣');
658
- console.error('║ ║');
659
- console.error('║ Option 1: Using test tags (Recommended) ║');
660
- console.error('║ ───────────────────────────────────────────────────────────── ║');
661
- console.error('║ test(\'My Test\', { tag: \'@uuid:123-xxx-xxx\' }, async ({ page }) => {║');
662
- console.error('║ // your test code ║');
663
- console.error('║ }); ║');
664
- console.error('║ ║');
665
- console.error('║ Option 2: Using test annotations ║');
666
- console.error('║ ───────────────────────────────────────────────────────────── ║');
667
- console.error('║ test(\'My Test\', async ({ page }, testInfo) => { ║');
668
- console.error('║ testInfo.annotations.push({ ║');
669
- console.error('║ type: \'uuid\', ║');
670
- console.error('║ description: \'123-xxx-xxx\' ║');
671
- console.error('║ }); ║');
672
- console.error('║ }); ║');
673
- console.error('║ ║');
674
- console.error('║ Option 3: Using mapAppqUuid helper ║');
675
- console.error('║ ───────────────────────────────────────────────────────────── ║');
676
- console.error('║ const { mapAppqUuid } = require(\'@appliqation/automation-sdk/utils\');║');
677
- console.error('║ test(\'My Test\', async ({ page }, testInfo) => { ║');
678
- console.error('║ mapAppqUuid(testInfo, \'123-xxx-xxx\'); ║');
679
- console.error('║ }); ║');
680
- console.error('╠════════════════════════════════════════════════════════════════════╣');
681
- console.error('║ 📚 WHERE TO FIND UUIDs ║');
682
- console.error('╠════════════════════════════════════════════════════════════════════╣');
683
- console.error('║ ║');
684
- console.error('║ 1. Open your test scenario in Appliqation UI ║');
685
- console.error('║ 2. Find the test case you want to automate ║');
686
- console.error('║ 3. Copy the UUID from the test case details ║');
687
- console.error('║ Format: {scenario-id}-{uuid} (e.g., 1154-7a17b809-0ff9...) ║');
688
- console.error('║ ║');
689
- console.error('║ 💡 Tip: Export UUIDs in bulk from Appliqation for faster setup ║');
690
- console.error('╚════════════════════════════════════════════════════════════════════╝\n');
506
+ console.error('\n' + '='.repeat(70));
507
+ console.error('[Appliqation] RUN FAILED - All tests missing UUID annotations');
508
+ console.error('='.repeat(70));
509
+ console.error(` Project: ${projectKey}`);
510
+ console.error(` Orphan Tests: ${orphanCount}`);
511
+ console.error('');
512
+ console.error(' No results were submitted. The run was automatically deleted.');
513
+ console.error('');
514
+ console.error(' Add UUID annotations to your tests:');
515
+ console.error(" test('My Test', { tag: '@uuid:123-xxx-xxx' }, async ({ page }) => { ... });");
516
+ console.error('='.repeat(70) + '\n');
691
517
  }
692
518
 
693
519
  /**
694
- * Handle runs where ALL tests were rejected by backend validation
695
- * This happens when tests have UUIDs but they're invalid (wrong project, format, duplicates)
696
- * Called AFTER submission when we know backend rejection counts
520
+ * Handle runs where all tests were rejected by backend
697
521
  * @private
698
- * @returns {Array} Array of deleted runs
699
522
  */
700
523
  async handleAllRejectedRuns() {
701
- if (!this.config.deleteOrphanOnlyRuns) {
702
- return []; // Feature disabled
703
- }
704
-
524
+ if (!this.config.deleteOrphanOnlyRuns) return [];
705
525
  const deletedRuns = [];
706
526
 
707
527
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
708
528
  const submittedCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
709
529
  const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
710
530
  const rejectedCount = this.rejectionsByRun.get(runInfo.runId) || 0;
711
-
712
- // Calculate how many were actually accepted by backend
713
531
  const acceptedCount = submittedCount - rejectedCount;
714
532
 
715
- // CRITICAL CHECK: Delete ONLY if we submitted tests but ALL were rejected
716
- // AND there are no orphans (orphans are handled separately)
717
533
  if (submittedCount > 0 && acceptedCount === 0 && orphanCount === 0) {
718
- // Show user-friendly error message FIRST (before deletion attempt)
719
- this.printAllRejectedError(submittedCount, rejectedCount, projectKey);
534
+ console.error('\n' + '='.repeat(70));
535
+ console.error('[Appliqation] RUN FAILED - All tests rejected by backend');
536
+ console.error('='.repeat(70));
537
+ console.error(` Project: ${projectKey}`);
538
+ console.error(` Submitted: ${submittedCount}, Rejected: ${rejectedCount}`);
539
+ console.error('');
540
+ console.error(' Common causes: invalid UUID format, wrong project, test case not found.');
541
+ console.error(' Check test logs for specific rejection reasons.');
542
+ console.error('='.repeat(70) + '\n');
720
543
 
721
544
  try {
722
- logger.warn('Deleting all-rejected run', {
723
- runId: runInfo.runId,
724
- submittedCount,
725
- rejectedCount,
726
- acceptedCount,
727
- projectKey
728
- });
729
545
  await this.client.deleteRun(runInfo.runId, 'all_tests_rejected');
730
- deletedRuns.push({
731
- runId: runInfo.runId,
732
- submittedCount,
733
- rejectedCount,
734
- projectKey
735
- });
736
546
  } catch (error) {
737
- logger.error('Failed to delete all-rejected run (may already be deleted)', {
738
- error: error.message,
739
- runId: runInfo.runId
740
- });
741
- // Non-fatal: Continue even if deletion fails
742
- // Still track as deleted since end result is the same
743
- deletedRuns.push({
744
- runId: runInfo.runId,
745
- submittedCount,
746
- rejectedCount,
747
- projectKey
748
- });
547
+ logger.error('Failed to delete all-rejected run', { error: error.message });
749
548
  }
549
+ deletedRuns.push({ runId: runInfo.runId, submittedCount, rejectedCount, projectKey });
750
550
  }
751
551
  }
752
552
 
753
- // Track deleted rejected runs
754
553
  if (deletedRuns.length > 0) {
755
554
  this.deletedOrphanRuns = [...(this.deletedOrphanRuns || []), ...deletedRuns];
756
555
  }
757
-
758
556
  return deletedRuns;
759
557
  }
760
558
 
761
559
  /**
762
- * Print user-friendly error message for all-rejected runs
763
- * @private
764
- */
765
- printAllRejectedError(submittedCount, rejectedCount, projectKey) {
766
- console.error('\n╔════════════════════════════════════════════════════════════════════╗');
767
- console.error('║ ❌ RUN CREATION FAILED - ALL TESTS REJECTED BY BACKEND ║');
768
- console.error('╠════════════════════════════════════════════════════════════════════╣');
769
- console.error(`║ Project: ${projectKey.padEnd(56)} ║`);
770
- console.error(`║ Tests Submitted: ${submittedCount.toString().padEnd(47)} ║`);
771
- console.error(`║ Tests Rejected: ${rejectedCount.toString().padEnd(47)} ║`);
772
- console.error('║ ║');
773
- console.error('║ ⚠️ NO RESULTS WERE ACCEPTED BY APPLIQATION ║');
774
- console.error('║ The test run was automatically deleted to prevent analytics ║');
775
- console.error('║ corruption. All submitted tests were rejected by the backend. ║');
776
- console.error('╠════════════════════════════════════════════════════════════════════╣');
777
- console.error('║ ✅ COMMON REJECTION REASONS ║');
778
- console.error('╠════════════════════════════════════════════════════════════════════╣');
779
- console.error('║ ║');
780
- console.error('║ 1. INVALID UUID FORMAT ║');
781
- console.error('║ ───────────────────────────────────────────────────────────── ║');
782
- console.error('║ ❌ Wrong: \'1154-uuid-here-99\' (extra suffix) ║');
783
- console.error('║ ✅ Right: \'1154-7f991165-9f59-47e1-8e90-d57d2e9cbf66\' ║');
784
- console.error('║ ║');
785
- console.error('║ 2. WRONG PROJECT ║');
786
- console.error('║ ───────────────────────────────────────────────────────────── ║');
787
- console.error('║ Using UUID from project 1162 in project 1154 tests ║');
788
- console.error('║ ✅ Solution: Use UUIDs from the correct project ║');
789
- console.error('║ ║');
790
- console.error('║ 3. DUPLICATE UUIDs ║');
791
- console.error('║ ───────────────────────────────────────────────────────────── ║');
792
- console.error('║ Multiple tests using the same UUID in one run ║');
793
- console.error('║ ✅ Solution: Each test needs a unique UUID ║');
794
- console.error('║ ║');
795
- console.error('║ 4. TEST CASE DOESN\'T EXIST ║');
796
- console.error('║ ───────────────────────────────────────────────────────────── ║');
797
- console.error('║ UUID doesn\'t match any test case in your project ║');
798
- console.error('║ ✅ Solution: Verify UUID exists in Appliqation Test Cases ║');
799
- console.error('╠════════════════════════════════════════════════════════════════════╣');
800
- console.error('║ 📋 HOW TO FIX ║');
801
- console.error('╠════════════════════════════════════════════════════════════════════╣');
802
- console.error('║ ║');
803
- console.error('║ 1. Check test logs above for specific rejection reasons ║');
804
- console.error('║ 2. Verify UUIDs in Appliqation portal → Test Cases ║');
805
- console.error('║ 3. Ensure UUID format: {nid}-{uuid} (no extra suffixes) ║');
806
- console.error('║ 4. Confirm UUIDs belong to the correct project ║');
807
- console.error('║ 5. Remove any duplicate UUID mappings ║');
808
- console.error('║ ║');
809
- console.error('╚════════════════════════════════════════════════════════════════════╝\n');
810
- }
811
-
812
- /**
813
- * Called once after all tests complete
560
+ * Called once after all tests complete — non-blocking
814
561
  */
815
562
  async onEnd(result) {
816
563
  logger.info('Test run complete.');
817
564
 
818
565
  try {
819
- // Handle orphan-only runs BEFORE submitting results
820
566
  const orphanDeletedRuns = await this.handleOrphanOnlyRuns();
821
567
 
822
- // Skip submission if --appq flag not present
823
568
  if (this.appqEnabled) {
824
- logger.info('Submitting results to Appliqation...');
825
-
826
- // Submit batched results
827
569
  if (this.config.batchSubmit) {
828
570
  await this.submitBatchedResults();
829
571
  }
830
-
831
- // Submit orphan tests
832
572
  await this.submitOrphanTests();
833
573
 
834
- // Handle all-rejected runs AFTER submission (when we know backend rejection counts)
835
574
  const rejectedDeletedRuns = await this.handleAllRejectedRuns();
836
-
837
- // Combine all deleted runs
838
575
  const allDeletedRuns = [...(orphanDeletedRuns || []), ...(rejectedDeletedRuns || [])];
839
576
 
840
- // Exit with error code if any runs were deleted
841
577
  if (allDeletedRuns.length > 0 && this.config.failOnOrphanOnlyRuns) {
842
- logger.error('Exiting with error code due to deleted runs (orphan or rejected)');
843
- process.exitCode = 1; // Set error exit code for CI/CD
578
+ process.exitCode = 1;
844
579
  }
845
- } else {
846
- logger.info('Appliqation reporting disabled. Skipping result submission.');
847
580
  }
848
581
 
849
- // Print summary (always show, even if not submitted)
850
582
  this.printSummary();
851
-
852
- // Write summary to file (always write, even if not submitted)
853
583
  await this.writeSummaryToFile();
854
584
  } catch (error) {
855
- logger.error('Error in onEnd', {
856
- error: error.message
857
- });
585
+ logger.error('Error in onEnd', { error: error.message });
858
586
  }
859
587
  }
860
588
 
861
589
  /**
862
- * Submit batched results for all runs
863
590
  * @private
864
591
  */
865
592
  async submitBatchedResults() {
@@ -869,16 +596,11 @@ class AppliqationReporter {
869
596
  try {
870
597
  logger.info(`Submitting ${results.length} results for run ${runId}...`);
871
598
 
872
- // Find run info for this runId to get metadata
873
599
  let runInfo = null;
874
600
  for (const [projectKey, info] of this.runsByProject.entries()) {
875
- if (info.runId === runId) {
876
- runInfo = info;
877
- break;
878
- }
601
+ if (info.runId === runId) { runInfo = info; break; }
879
602
  }
880
603
 
881
- // Build run metadata for legacy endpoint
882
604
  const runMetadata = {
883
605
  nid: runInfo?.metadata?.nid || 0,
884
606
  run_timestamp: runInfo?.timestamp || Math.floor(Date.now() / 1000),
@@ -887,32 +609,24 @@ class AppliqationReporter {
887
609
 
888
610
  const summary = await this.client.submitBatch(results, {
889
611
  batchSize: this.config.batchSize,
890
- runMetadata: runMetadata,
891
- onProgress: (progress) => {
892
- logger.debug('Batch progress', progress);
893
- }
612
+ runMetadata,
613
+ onProgress: (progress) => { logger.debug('Batch progress', progress); }
894
614
  });
895
615
 
896
- // Track backend validation rejections (global and per-run)
897
616
  const rejectedCount = summary.failed || 0;
898
617
  this.backendRejectedTests += rejectedCount;
899
618
  this.rejectionsByRun.set(runId, rejectedCount);
900
619
 
901
620
  logger.info(`Results submitted for run ${runId}`, {
902
- success: summary.success,
903
- failed: summary.failed,
904
- total: summary.total
621
+ success: summary.success, failed: summary.failed, total: summary.total
905
622
  });
906
623
  } catch (error) {
907
- logger.error(`Failed to submit results for run ${runId}`, {
908
- error: error.message
909
- });
624
+ console.warn(`[Appliqation] Failed to submit batch: ${error.message}. ${results.length} results were not reported.`);
910
625
  }
911
626
  }
912
627
  }
913
628
 
914
629
  /**
915
- * Submit orphan tests for all runs
916
630
  * @private
917
631
  */
918
632
  async submitOrphanTests() {
@@ -923,596 +637,310 @@ class AppliqationReporter {
923
637
 
924
638
  try {
925
639
  await this.client.logOrphanTests(runId, orphans);
926
-
927
- logger.info(`Orphan tests logged for run ${runId}`, {
928
- count: orphans.length
929
- });
930
640
  } catch (error) {
931
- logger.error(`Failed to log orphan tests for run ${runId}`, {
932
- error: error.message
933
- });
641
+ logger.error(`Failed to log orphan tests for run ${runId}`, { error: error.message });
934
642
  }
935
643
  }
936
644
  }
937
645
 
938
- /**
939
- * Track result for batch submission
940
- * @private
941
- */
646
+ /** @private */
942
647
  trackResult(runId, result) {
943
- if (!this.resultsByRun.has(runId)) {
944
- this.resultsByRun.set(runId, []);
945
- }
648
+ if (!this.resultsByRun.has(runId)) this.resultsByRun.set(runId, []);
946
649
  this.resultsByRun.get(runId).push(result);
947
650
  }
948
651
 
949
- /**
950
- * Track orphan test
951
- * @private
952
- */
652
+ /** @private */
953
653
  trackOrphan(runId, test, result, browser) {
954
- if (!this.orphansByRun.has(runId)) {
955
- this.orphansByRun.set(runId, []);
956
- }
957
-
654
+ if (!this.orphansByRun.has(runId)) this.orphansByRun.set(runId, []);
958
655
  const orphanEntry = UuidExtractor.createOrphanEntry(test, result, browser);
959
656
  this.orphansByRun.get(runId).push(orphanEntry);
960
657
  }
961
658
 
962
- /**
963
- * Map Playwright status to Appliqation status
964
- * @private
965
- */
659
+ /** @private */
966
660
  mapStatus(status) {
967
661
  const statusMap = {
968
- 'passed': 'Pass', // Capital case to match backend expectations
969
- 'failed': 'Fail', // Capital case to match backend expectations
662
+ 'passed': 'Pass',
663
+ 'failed': 'Fail',
970
664
  'timedOut': 'Fail',
971
- 'skipped': 'Skipped', // Capital case to match backend expectations
665
+ 'skipped': 'Skipped',
972
666
  'interrupted': 'Skipped'
973
667
  };
974
-
975
668
  return statusMap[status] || 'Fail';
976
669
  }
977
670
 
978
- /**
979
- * Build comment from test result
980
- * @private
981
- */
671
+ /** @private */
982
672
  buildComment(test, result) {
983
673
  const parts = [];
984
-
985
- if (result.duration) {
986
- parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
987
- }
988
-
674
+ if (result.duration) parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
989
675
  if (result.error) {
990
676
  const errorMsg = result.error.message || result.error.toString();
991
677
  parts.push(`Error: ${errorMsg.substring(0, 500)}`);
992
678
  }
993
-
994
- if (result.retry > 0) {
995
- parts.push(`Retry: ${result.retry}`);
996
- }
997
-
679
+ if (result.retry > 0) parts.push(`Retry: ${result.retry}`);
998
680
  return parts.join(' | ');
999
681
  }
1000
682
 
1001
- /**
1002
- * Print summary report
1003
- * @private
1004
- */
683
+ /** @private */
1005
684
  printSummary() {
1006
685
  const testsSubmitted = this.totalTests;
1007
686
  const testsAccepted = testsSubmitted - this.backendRejectedTests;
1008
687
 
1009
- console.log('\n╔═══════════════════════════════════════════════════════════╗');
1010
- console.log('Appliqation Test Results Summary');
1011
- console.log('╠═══════════════════════════════════════════════════════════╣');
688
+ console.log('\n' + '='.repeat(60));
689
+ console.log(' Appliqation Test Results Summary');
690
+ console.log('='.repeat(60));
1012
691
 
1013
- // Show reporting status
1014
692
  if (!this.appqEnabled) {
1015
- console.log(' ⚠️ Appliqation Reporting: DISABLED');
1016
- console.log('(Set APPQ_ENABLE=1 or add -- --appq flag) ║');
1017
- console.log('╠═══════════════════════════════════════════════════════════╣');
693
+ console.log(' Reporting: DISABLED (use -- --appq to enable)');
694
+ console.log('-'.repeat(60));
1018
695
  } else {
1019
- console.log('║ Submitted to Backend: ║');
1020
- console.log(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
1021
- console.log(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
1022
- console.log(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
1023
- console.log('║ ║');
696
+ console.log(` Submitted: ${testsSubmitted} | Accepted: ${testsAccepted} | Rejected: ${this.backendRejectedTests}`);
1024
697
  }
1025
- console.log('║ ║');
1026
- console.log('║ Test Execution Results (Playwright): ║');
1027
- console.log(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
1028
- console.log(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
1029
- console.log(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
1030
- console.log('║ ║');
1031
- console.log('║ Not Submitted: ║');
1032
- console.log(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
1033
- console.log(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
1034
- console.log('╠═══════════════════════════════════════════════════════════╣');
1035
-
1036
- // Only show run matrices if reporting is enabled
1037
- if (this.appqEnabled && this.runsByProject.size > 0) {
1038
- console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
1039
698
 
699
+ console.log(` Passed: ${this.passedTests} | Failed: ${this.failedTests} | Skipped: ${this.skippedTests}`);
700
+ console.log(` Orphan (No UUID): ${this.orphanTests} | Duplicates: ${this.duplicateTests}`);
701
+
702
+ if (this.appqEnabled && this.runsByProject.size > 0) {
703
+ console.log('-'.repeat(60));
704
+ console.log(` Run Matrices: ${this.runsByProject.size}`);
1040
705
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
1041
- console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
706
+ console.log(` ${projectKey}: ${runInfo.runId}`);
1042
707
  }
1043
-
1044
- // Show deleted orphan-only runs
1045
708
  if (this.deletedOrphanRuns.length > 0) {
1046
- console.log(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length} ║`);
709
+ console.log(` Deleted (orphan-only): ${this.deletedOrphanRuns.length}`);
1047
710
  }
1048
711
  }
1049
712
 
1050
- console.log('╚═══════════════════════════════════════════════════════════╝\n');
713
+ console.log('='.repeat(60) + '\n');
1051
714
 
1052
715
  if (this.duplicateTests > 0) {
1053
- console.log(`\n⚠️ Warning: ${this.duplicateTests} duplicate UUID(s) detected!\n`);
1054
-
1055
- // Show details of each unique duplicate UUID
716
+ console.log(`\n[Appliqation] Warning: ${this.duplicateTests} duplicate UUID(s) detected.\n`);
1056
717
  let index = 1;
1057
718
  for (const duplicate of this.uniqueDuplicates.values()) {
1058
719
  console.log(`${index}. UUID: ${duplicate.uuid}`);
1059
- console.log(` Occurrences (${duplicate.occurrences.length}):`);
1060
720
  duplicate.occurrences.forEach((occ, i) => {
1061
721
  console.log(` ${i + 1}) ${occ.file} - "${occ.title}" (${occ.browser})`);
1062
722
  });
1063
723
  console.log('');
1064
724
  index++;
1065
725
  }
1066
-
1067
- console.log('Each test should have a unique UUID within a browser.\n');
1068
726
  }
1069
727
 
1070
728
  if (this.orphanTests > 0) {
1071
- console.log(`\n⚠️ ${this.orphanTests} test(s) missing UUIDs - not submitted to Appliqation\n`);
1072
-
1073
- // Show details of each unique orphan test
729
+ console.log(`\n[Appliqation] ${this.orphanTests} test(s) missing UUIDs not submitted.\n`);
1074
730
  let index = 1;
1075
731
  for (const orphan of this.uniqueOrphans.values()) {
1076
- console.log(`${index}. File: ${orphan.file}`);
1077
- console.log(` Test: "${orphan.title}"`);
1078
- console.log(` Browsers: ${orphan.browsers.join(', ')}\n`);
732
+ console.log(`${index}. ${orphan.file} - "${orphan.title}" (${orphan.browsers.join(', ')})`);
1079
733
  index++;
1080
734
  }
1081
-
1082
- console.log('Fix: Add UUID tag → test(\'name\', { tag: \'@uuid:1154-xxx...\' }, async () => {...});\n');
735
+ console.log("\nFix: test('name', { tag: '@uuid:1154-xxx...' }, async () => {...});\n");
1083
736
  }
1084
737
  }
1085
738
 
1086
739
  /**
1087
740
  * Write execution summary to file
1088
- * Creates timestamped txt file in AppQ_Execution_Summary folder
1089
741
  * @private
1090
742
  */
1091
743
  async writeSummaryToFile() {
1092
744
  const fs = require('fs').promises;
1093
- const path = require('path');
745
+ const pathModule = require('path');
1094
746
 
1095
747
  try {
1096
748
  this.executionEndTime = new Date();
1097
749
  const duration = this.executionEndTime - this.executionStartTime;
1098
750
 
1099
- // Build output directory path
1100
- // If playwrightOutputDir already contains 'test-results' (from config._internal.outputDir),
1101
- // don't add it again. Otherwise, add 'test-results' to the path.
1102
751
  const baseDir = this.playwrightOutputDir;
1103
752
  const needsTestResults = !baseDir.includes('test-results');
1104
-
1105
753
  const summaryDir = needsTestResults
1106
- ? path.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
1107
- : path.join(baseDir, 'AppQ_Execution_Summary');
754
+ ? pathModule.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
755
+ : pathModule.join(baseDir, 'AppQ_Execution_Summary');
1108
756
 
1109
- // Create directory if it doesn't exist
1110
757
  await fs.mkdir(summaryDir, { recursive: true });
1111
758
 
1112
- // Build filename with run title and timestamp
1113
759
  const runTitle = this.config.title || 'execution_summary';
1114
760
  const timestamp = this.formatDateTimeForFilename(this.executionStartTime);
1115
761
  const filename = `${runTitle}_${timestamp}.txt`;
1116
- const filepath = path.join(summaryDir, filename);
762
+ const filepath = pathModule.join(summaryDir, filename);
1117
763
 
1118
- // Build comprehensive summary content
1119
764
  const summaryContent = this.buildSummaryContent(duration);
1120
-
1121
- // Write to file
1122
765
  await fs.writeFile(filepath, summaryContent, 'utf8');
1123
766
 
1124
767
  logger.info(`Execution summary saved to: ${filepath}`);
1125
- console.log(`\n📄 Execution summary saved: ${filename}`);
1126
-
1127
768
  } catch (error) {
1128
769
  logger.error('Failed to write summary file', { error: error.message });
1129
- // Don't throw - file writing failure should not break test run
1130
770
  }
1131
771
  }
1132
772
 
1133
- /**
1134
- * Format date/time for filename: 2025-01-21_14-30-45
1135
- * @private
1136
- */
773
+ /** @private */
1137
774
  formatDateTimeForFilename(date) {
1138
- const year = date.getFullYear();
1139
- const month = String(date.getMonth() + 1).padStart(2, '0');
1140
- const day = String(date.getDate()).padStart(2, '0');
1141
- const hours = String(date.getHours()).padStart(2, '0');
1142
- const minutes = String(date.getMinutes()).padStart(2, '0');
1143
- const seconds = String(date.getSeconds()).padStart(2, '0');
1144
-
1145
- return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
775
+ const y = date.getFullYear();
776
+ const m = String(date.getMonth() + 1).padStart(2, '0');
777
+ const d = String(date.getDate()).padStart(2, '0');
778
+ const h = String(date.getHours()).padStart(2, '0');
779
+ const min = String(date.getMinutes()).padStart(2, '0');
780
+ const s = String(date.getSeconds()).padStart(2, '0');
781
+ return `${y}-${m}-${d}_${h}-${min}-${s}`;
1146
782
  }
1147
783
 
1148
- /**
1149
- * Build comprehensive summary content for file
1150
- * @private
1151
- */
784
+ /** @private */
1152
785
  buildSummaryContent(duration) {
1153
786
  const lines = [];
1154
-
1155
- // Header
1156
- lines.push('═══════════════════════════════════════════════════════════');
1157
- lines.push(' APPLIQATION TEST EXECUTION SUMMARY');
1158
- lines.push('═══════════════════════════════════════════════════════════');
1159
- lines.push('');
1160
-
1161
- // Execution Metadata
1162
- lines.push('EXECUTION METADATA:');
1163
- lines.push('─────────────────────────────────────────────────────────');
1164
- lines.push(`Start Time: ${this.executionStartTime.toISOString()}`);
1165
- lines.push(`End Time: ${this.executionEndTime.toISOString()}`);
1166
- lines.push(`Duration: ${this.formatDuration(duration)}`);
1167
- lines.push(`Run Title: ${this.config.title || 'N/A'}`);
1168
- lines.push(`Appq Reporting: ${this.appqEnabled ? 'ENABLED' : 'DISABLED (Set APPQ_ENABLE=1 or add -- --appq flag)'}`);
1169
- lines.push('');
1170
-
1171
- // Test Results Summary (ASCII Table from printSummary)
1172
787
  const testsSubmitted = this.totalTests;
1173
788
  const testsAccepted = testsSubmitted - this.backendRejectedTests;
1174
789
 
1175
- lines.push('╔═══════════════════════════════════════════════════════════╗');
1176
- lines.push('║ Appliqation Test Results Summary ║');
1177
- lines.push('╠═══════════════════════════════════════════════════════════╣');
790
+ lines.push('Appliqation Test Execution Summary');
791
+ lines.push('='.repeat(60));
792
+ lines.push(`Start: ${this.executionStartTime.toISOString()}`);
793
+ lines.push(`End: ${this.executionEndTime.toISOString()}`);
794
+ lines.push(`Duration: ${this.formatDuration(duration)}`);
795
+ lines.push(`Title: ${this.config.title || 'N/A'}`);
796
+ lines.push(`Reporting: ${this.appqEnabled ? 'ENABLED' : 'DISABLED'}`);
797
+ lines.push('');
1178
798
 
1179
- // Show submission status
1180
- if (!this.appqEnabled) {
1181
- lines.push('║ ⚠️ Appliqation Reporting: DISABLED ║');
1182
- lines.push('║ (Set APPQ_ENABLE=1 or add -- --appq flag) ║');
1183
- lines.push('╠═══════════════════════════════════════════════════════════╣');
1184
- } else {
1185
- lines.push('║ Submitted to Backend: ║');
1186
- lines.push(`║ Total Submitted: ${testsSubmitted.toString().padStart(5)} ║`);
1187
- lines.push(`║ ✅ Accepted: ${testsAccepted.toString().padStart(5)} ║`);
1188
- lines.push(`║ ❌ Rejected: ${this.backendRejectedTests.toString().padStart(5)} ║`);
1189
- lines.push('║ ║');
799
+ if (this.appqEnabled) {
800
+ lines.push(`Submitted: ${testsSubmitted} | Accepted: ${testsAccepted} | Rejected: ${this.backendRejectedTests}`);
1190
801
  }
1191
- lines.push('║ Test Execution Results (Playwright): ║');
1192
- lines.push(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
1193
- lines.push(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
1194
- lines.push(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
1195
- lines.push('║ ║');
1196
- lines.push('║ Not Submitted: ║');
1197
- lines.push(`║ Orphan (No UUID):${this.orphanTests.toString().padStart(5)} ║`);
1198
- lines.push(`║ Duplicates: ${this.duplicateTests.toString().padStart(5)} ║`);
1199
- lines.push('╠═══════════════════════════════════════════════════════════╣');
1200
-
1201
- // Run IDs (only show if reporting is enabled)
802
+ lines.push(`Passed: ${this.passedTests} | Failed: ${this.failedTests} | Skipped: ${this.skippedTests}`);
803
+ lines.push(`Orphans: ${this.orphanTests} | Duplicates: ${this.duplicateTests}`);
804
+ lines.push('');
805
+
1202
806
  if (this.appqEnabled && this.runsByProject.size > 0) {
1203
- lines.push(`║ Run Matrices Created: ${this.runsByProject.size.toString().padStart(5)} ║`);
807
+ lines.push(`Run Matrices: ${this.runsByProject.size}`);
1204
808
  for (const [projectKey, runInfo] of this.runsByProject) {
1205
- const displayKey = projectKey.length > 30 ? projectKey.substring(0, 27) + '...' : projectKey;
1206
- const paddedKey = displayKey.padEnd(30);
1207
- lines.push(`║ ${paddedKey}: ${runInfo.runId.padEnd(20)} ║`);
809
+ lines.push(` ${projectKey}: ${runInfo.runId}`);
1208
810
  }
1209
-
1210
- // Show deleted orphan-only runs
1211
- if (this.deletedOrphanRuns.length > 0) {
1212
- lines.push(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length.toString().padStart(5)} ║`);
1213
- }
1214
- }
1215
-
1216
- lines.push('╚═══════════════════════════════════════════════════════════╝');
1217
- lines.push('');
1218
-
1219
- // Detailed Errors Section
1220
- if (this.duplicateTests > 0 || this.orphanTests > 0 || this.backendRejectedTests > 0) {
1221
- lines.push('');
1222
- lines.push('DETAILED ERRORS & WARNINGS:');
1223
- lines.push('═══════════════════════════════════════════════════════════');
1224
811
  lines.push('');
1225
812
  }
1226
813
 
1227
- // Duplicate UUIDs Details
1228
- if (this.duplicateTests > 0 && this.uniqueDuplicates.size > 0) {
1229
- lines.push('⚠️ DUPLICATE UUIDs DETECTED:');
1230
- lines.push('─────────────────────────────────────────────────────────');
1231
- lines.push(`Count: ${this.duplicateTests} unique UUID(s)`);
1232
- lines.push('');
1233
-
1234
- // List each unique duplicate UUID with all occurrences
1235
- let index = 1;
1236
- for (const duplicate of this.uniqueDuplicates.values()) {
1237
- lines.push(`${index}. UUID: ${duplicate.uuid}`);
1238
- lines.push(` Occurrences (${duplicate.occurrences.length}):`);
1239
- duplicate.occurrences.forEach((occ, i) => {
1240
- lines.push(` ${i + 1}) File: ${occ.file}`);
1241
- lines.push(` Test: "${occ.title}"`);
1242
- lines.push(` Browser: ${occ.browser}`);
814
+ if (this.duplicateTests > 0) {
815
+ lines.push('Duplicate UUIDs:');
816
+ for (const dup of this.uniqueDuplicates.values()) {
817
+ lines.push(` UUID: ${dup.uuid}`);
818
+ dup.occurrences.forEach((occ, i) => {
819
+ lines.push(` ${i + 1}) ${occ.file} - "${occ.title}" (${occ.browser})`);
1243
820
  });
1244
- lines.push('');
1245
- index++;
1246
821
  }
1247
-
1248
- lines.push('Action Required: Each test should have a unique UUID within a browser.');
1249
822
  lines.push('');
1250
823
  }
1251
824
 
1252
- // Orphan Tests Warning
1253
825
  if (this.orphanTests > 0) {
1254
- lines.push('⚠️ TESTS MISSING UUIDs (Not Submitted):');
1255
- lines.push('─────────────────────────────────────────────────────────');
1256
- lines.push(`Count: ${this.orphanTests} unique test(s)`);
1257
- lines.push('');
1258
-
1259
- // List each unique orphan test with details
1260
- let index = 1;
826
+ lines.push('Tests missing UUIDs:');
1261
827
  for (const orphan of this.uniqueOrphans.values()) {
1262
- lines.push(`${index}. File: ${orphan.file}`);
1263
- lines.push(` Test: "${orphan.title}"`);
1264
- lines.push(` Browsers: ${orphan.browsers.join(', ')}`);
1265
- lines.push('');
1266
- index++;
828
+ lines.push(` ${orphan.file} - "${orphan.title}" (${orphan.browsers.join(', ')})`);
1267
829
  }
1268
-
1269
- lines.push('Action Required: Add UUID tags to submit results');
1270
- lines.push('Example: test(\'name\', { tag: \'@uuid:1154-abc-...\' }, async () => {...});');
1271
830
  lines.push('');
1272
831
  }
1273
832
 
1274
- // Backend Rejections
1275
- if (this.backendRejectedTests > 0) {
1276
- lines.push('BACKEND VALIDATION REJECTIONS:');
1277
- lines.push('─────────────────────────────────────────────────────────');
1278
- lines.push(`Total rejected: ${this.backendRejectedTests}`);
1279
- lines.push('');
1280
- lines.push('These test cases were rejected due to project ownership validation.');
1281
- lines.push('The test case UUID does not belong to the project associated with');
1282
- lines.push('your API key. Verify the correct project and test case mappings.');
1283
- lines.push('');
833
+ lines.push('='.repeat(60));
834
+ try {
835
+ lines.push(`Generated by Appliqation SDK v${require('../../../package.json').version}`);
836
+ } catch (e) {
837
+ lines.push('Generated by Appliqation SDK');
1284
838
  }
1285
839
 
1286
- // Footer
1287
- lines.push('═══════════════════════════════════════════════════════════');
1288
- lines.push(`Generated by Appliqation SDK v${require('../../../package.json').version}`);
1289
- lines.push(`Timestamp: ${new Date().toISOString()}`);
1290
- lines.push('═══════════════════════════════════════════════════════════');
1291
-
1292
840
  return lines.join('\n');
1293
841
  }
1294
842
 
1295
- /**
1296
- * Format duration in human-readable format
1297
- * @private
1298
- */
843
+ /** @private */
1299
844
  formatDuration(ms) {
1300
845
  const seconds = Math.floor(ms / 1000);
1301
846
  const minutes = Math.floor(seconds / 60);
1302
847
  const hours = Math.floor(minutes / 60);
1303
-
1304
- if (hours > 0) {
1305
- return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
1306
- } else if (minutes > 0) {
1307
- return `${minutes}m ${seconds % 60}s`;
1308
- } else {
1309
- return `${seconds}s`;
1310
- }
848
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
849
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
850
+ return `${seconds}s`;
1311
851
  }
1312
852
 
1313
- /**
1314
- * Extract project names that are actually running from the test suite
1315
- * @private
1316
- * @param {Object} suite - Root test suite
1317
- * @returns {Array<string>} Array of project names
1318
- */
853
+ /** @private */
1319
854
  getRunningProjectNames(suite) {
1320
855
  const projectNames = new Set();
1321
-
1322
- const extractProjectNames = (node) => {
1323
- // Check if this node has a project
856
+ const extract = (node) => {
1324
857
  if (node.project && typeof node.project === 'function') {
1325
858
  const project = node.project();
1326
- if (project && project.name) {
1327
- projectNames.add(project.name);
1328
- }
859
+ if (project?.name) projectNames.add(project.name);
1329
860
  }
1330
-
1331
- // Recursively check suites
1332
861
  if (node.suites && Array.isArray(node.suites)) {
1333
- for (const childSuite of node.suites) {
1334
- extractProjectNames(childSuite);
1335
- }
862
+ for (const child of node.suites) extract(child);
1336
863
  }
1337
864
  };
1338
-
1339
- extractProjectNames(suite);
865
+ extract(suite);
1340
866
  return Array.from(projectNames);
1341
867
  }
1342
868
 
1343
869
  /**
1344
- * Auto-detect and inject browser versions for projects missing browserVersion metadata
1345
- * Now runs in parallel for better performance
870
+ * Auto-detect browser versions
1346
871
  * @private
1347
- * @param {Array} projects - Array of Playwright project configurations
1348
872
  */
1349
873
  async injectBrowserVersions(projects) {
1350
- // Use playwright package directly (not @playwright/test) to avoid circular dependency
1351
874
  let playwright = null;
1352
875
  try {
1353
876
  playwright = require('playwright');
1354
877
  } catch (e) {
1355
- // If playwright is not installed, try playwright-core
1356
878
  try {
1357
879
  playwright = require('playwright-core');
1358
880
  } catch (e2) {
1359
- logger.debug('Could not load playwright package for browser version detection');
1360
881
  return;
1361
882
  }
1362
883
  }
1363
884
 
1364
885
  const { chromium, firefox, webkit } = playwright;
1365
-
1366
- // Cache for browser versions to avoid launching same browser multiple times
1367
886
  const versionCache = new Map();
1368
887
 
1369
- // Create detection promises for all projects in parallel
1370
888
  const detectionPromises = projects.map(async (project) => {
1371
- // Skip if browserVersion is already configured
1372
- if (project.use?.metadata?.browserVersion) {
1373
- logger.debug(`Project "${project.name}" already has browserVersion configured`, {
1374
- version: project.use.metadata.browserVersion
1375
- });
1376
- return null;
1377
- }
889
+ if (project.use?.metadata?.browserVersion) return null;
1378
890
 
1379
- // Get browser name from project - check multiple sources
1380
891
  let browserName = project.use?.browserName || project.use?.defaultBrowserType;
1381
-
1382
- // Fallback to project name if it matches browser names
1383
892
  if (!browserName) {
1384
893
  const projectName = (project.name || '').toLowerCase();
1385
- if (projectName.includes('chromium') || projectName.includes('chrome')) {
1386
- browserName = 'chromium';
1387
- } else if (projectName.includes('firefox')) {
1388
- browserName = 'firefox';
1389
- } else if (projectName.includes('webkit') || projectName.includes('safari')) {
1390
- browserName = 'webkit';
1391
- }
894
+ if (projectName.includes('chromium') || projectName.includes('chrome')) browserName = 'chromium';
895
+ else if (projectName.includes('firefox')) browserName = 'firefox';
896
+ else if (projectName.includes('webkit') || projectName.includes('safari')) browserName = 'webkit';
1392
897
  }
1393
-
1394
898
  if (!browserName) return null;
1395
899
 
1396
900
  try {
1397
- let version = null;
901
+ let version = versionCache.get(browserName);
1398
902
 
1399
- // Check cache first
1400
- if (versionCache.has(browserName)) {
1401
- version = versionCache.get(browserName);
1402
- logger.debug(`Using cached version for ${browserName}`, { version });
1403
- } else {
1404
- // Launch browser to detect version
903
+ if (!version) {
1405
904
  let browser = null;
1406
-
1407
905
  switch (browserName.toLowerCase()) {
1408
- case 'chromium':
1409
- case 'chrome':
906
+ case 'chromium': case 'chrome':
1410
907
  browser = await chromium.launch({ headless: true });
1411
908
  version = browser.version();
1412
909
  await browser.close();
1413
910
  break;
1414
-
1415
911
  case 'firefox':
1416
912
  browser = await firefox.launch({ headless: true });
1417
913
  version = browser.version();
1418
914
  await browser.close();
1419
915
  break;
1420
-
1421
- case 'webkit':
1422
- case 'safari':
916
+ case 'webkit': case 'safari':
1423
917
  browser = await webkit.launch({ headless: true });
1424
918
  version = browser.version();
1425
919
  await browser.close();
1426
920
  break;
1427
921
  }
1428
-
1429
- // Cache the version
1430
- if (version) {
1431
- versionCache.set(browserName, version);
1432
- }
922
+ if (version) versionCache.set(browserName, version);
1433
923
  }
1434
924
 
1435
925
  if (version) {
1436
- // Extract major version (e.g., "140.0.6778.44" -> "140")
1437
926
  const majorVersion = version.match(/^(\d+)/)?.[1];
1438
-
1439
927
  if (majorVersion) {
1440
- // Inject browserVersion into project metadata
1441
- if (!project.use.metadata) {
1442
- project.use.metadata = {};
1443
- }
928
+ if (!project.use.metadata) project.use.metadata = {};
1444
929
  project.use.metadata.browserVersion = majorVersion;
1445
-
1446
- logger.info(`Auto-detected browser version for project "${project.name}"`, {
1447
- browser: browserName,
1448
- version: majorVersion,
1449
- fullVersion: version
1450
- });
1451
-
1452
930
  return { project: project.name, browser: browserName, version: majorVersion };
1453
931
  }
1454
932
  }
1455
933
  } catch (error) {
1456
- // Silently fail - version detection is optional
1457
- logger.debug(`Could not auto-detect browser version for project "${project.name}"`, {
1458
- error: error.message
1459
- });
934
+ logger.debug(`Could not auto-detect browser version for "${project.name}"`, { error: error.message });
1460
935
  }
1461
-
1462
936
  return null;
1463
937
  });
1464
938
 
1465
- // Wait for all browser version detections to complete in parallel
1466
- const results = await Promise.all(detectionPromises);
1467
-
1468
- // Log summary
1469
- const detected = results.filter(r => r !== null);
1470
- if (detected.length > 0) {
1471
- logger.info(`Browser version detection completed for ${detected.length} project(s)`);
1472
- }
1473
- }
1474
-
1475
- /**
1476
- * Validate reporter configuration
1477
- * @private
1478
- */
1479
- validateConfig() {
1480
- if (!this.config.baseUrl) {
1481
- throw new Error('baseUrl is required');
1482
- }
1483
-
1484
- if (!this.config.apiKey) {
1485
- throw new Error('apiKey is required');
1486
- }
1487
-
1488
- // projectKey is now optional with API key authentication
1489
- // It will be auto-fetched from the API key's associated project
1490
- // If not provided, auto-configuration will happen in onBegin()
1491
-
1492
- // environment is now optional - will be auto-detected from project configuration
1493
- // If not provided, the default environment from the project will be used
1494
- // Manual configuration still supported for backward compatibility
1495
-
1496
- // Note: scenarioId, testSetId, and title are optional
1497
- }
1498
-
1499
- /**
1500
- * Called when the reporter starts
1501
- * @param {Object} runnerSuite - Root suite
1502
- * @param {Object} config - Playwright config
1503
- */
1504
- onStdOut(chunk, test, result) {
1505
- // Optional: capture stdout if needed
939
+ await Promise.all(detectionPromises);
1506
940
  }
1507
941
 
1508
- /**
1509
- * Called when the reporter starts
1510
- * @param {Object} runnerSuite - Root suite
1511
- * @param {Object} config - Playwright config
1512
- */
1513
- onStdErr(chunk, test, result) {
1514
- // Optional: capture stderr if needed
1515
- }
942
+ onStdOut(chunk, test, result) {}
943
+ onStdErr(chunk, test, result) {}
1516
944
  }
1517
945
 
1518
946
  module.exports = AppliqationReporter;