@appliqation/automation-sdk 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,91 +4,81 @@ 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 — --appq CLI flag or APPQ_ENABLED env var
78
+ this.appqEnabled = process.argv.includes('--appq')
79
+ || process.env.APPQ_ENABLED === 'true'
80
+ || process.env.APPQ_ENABLED === '1';
81
+
92
82
  this.config = {
93
83
  autoCreateRun: true,
94
84
  logOrphans: true,
@@ -100,364 +90,305 @@ class AppliqationReporter {
100
90
  ...config
101
91
  };
102
92
 
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;
93
+ // Resolve environment: CLI --appq-env > env var > config
94
+ const cliEnv = getEnvironmentFromCli();
95
+ if (cliEnv) {
96
+ this.config.environment = cliEnv;
97
+ } else if (!this.config.environment) {
98
+ this.config.environment = process.env.APPLIQATION_ENVIRONMENT || null;
113
99
  }
114
- // Note: No default fallback - validation will catch missing value
115
100
 
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
101
+ // Resolve title: CLI --appq-title > env var > config > timestamp
118
102
  if (!this.config.title) {
119
103
  const cliTitle = getRunTitleFromCli();
120
104
  this.config.title = cliTitle || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`;
121
105
  }
122
106
 
123
- // Check if --appq flag is present (opt-in approach)
124
- this.appqEnabled = isAppqEnabled();
107
+ // Check for existing run ID (TDD iteration mode)
108
+ this.existingRunId = this.config.runId || getRunIdFromCli() || process.env.APPLIQATION_RUN_ID || null;
125
109
 
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');
110
+ // If disabled, stop here — true no-op
111
+ if (!this.appqEnabled) {
112
+ this.client = null;
113
+ this._initState();
114
+ return;
134
115
  }
135
116
 
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
117
+ // 2. Try to create client wrapped in try-catch (never crashes)
118
+ try {
119
+ const resolved = AppliqationClient.resolveConfig(this.config);
120
+
121
+ if (!resolved.apiKey) {
122
+ console.warn('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
123
+ console.warn('[Appliqation] Tests will continue without reporting.');
124
+ this.appqEnabled = false;
125
+ this.client = null;
126
+ this._initState();
127
+ return;
145
128
  }
146
- });
147
129
 
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
130
+ this.client = new AppliqationClient({
131
+ baseUrl: resolved.baseUrl,
132
+ apiKey: resolved.apiKey,
133
+ projectKey: resolved.projectKey,
134
+ rejectUnauthorized: this.config.rejectUnauthorized,
135
+ options: {
136
+ logLevel: this.config.logLevel,
137
+ logOrphans: this.config.logOrphans
138
+ }
139
+ });
140
+
141
+ logger.info('Appliqation reporting ENABLED');
142
+ console.log('[Appliqation] Reporting enabled.');
143
+ } catch (err) {
144
+ console.warn(`[Appliqation] Setup failed: ${err.message}. Tests will continue without reporting.`);
145
+ this.appqEnabled = false;
146
+ this.client = null;
147
+ }
148
+
149
+ this._initState();
150
+ }
151
+
152
+ /**
153
+ * Initialize state tracking
154
+ * @private
155
+ */
156
+ _initState() {
157
+ this.runsByProject = new Map();
158
+ this.resultsByRun = new Map();
159
+ this.orphansByRun = new Map();
160
+ this.submittedUuidsByRun = new Map();
161
+ this.submittedUuidsGlobal = new Map();
162
+ this.browserVersionDetected = new Map();
155
163
  this.totalTests = 0;
156
164
  this.passedTests = 0;
157
165
  this.failedTests = 0;
158
166
  this.skippedTests = 0;
159
167
  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
168
+ this.validTests = 0;
169
+ this.duplicateTests = 0;
170
+ this.duplicateDetails = [];
171
+ this.backendRejectedTests = 0;
172
+ this.rejectionsByRun = new Map();
173
+ this.deletedOrphanRuns = [];
174
+ this.uniqueOrphans = new Map();
175
+ this.uniqueDuplicates = new Map();
170
176
  this.executionStartTime = null;
171
177
  this.executionEndTime = null;
172
178
  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
179
  }
183
180
 
184
181
  /**
185
- * Called once before running tests
186
- * @param {Object} config - Playwright config
187
- * @param {Object} suite - Root test suite
182
+ * Called once before running tests — non-blocking
188
183
  */
189
184
  async onBegin(config, suite) {
190
- // Capture execution start time for summary file
191
185
  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
186
  this.playwrightOutputDir = config._internal?.outputDir || config.rootDir || process.cwd();
198
187
 
199
- logger.info('Test run starting...');
188
+ if (!this.appqEnabled) return;
200
189
 
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
190
  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;
191
+ // Auto-discover project from API key
192
+ await this.client.initialize();
193
+
194
+ // Validate environment
195
+ if (!this.config.environment) {
196
+ console.warn('[Appliqation] Environment required. Use --appq-env=Stage');
197
+ console.warn('[Appliqation] Tests will continue without reporting.');
198
+ this.appqEnabled = false;
199
+ return;
245
200
  }
246
201
 
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;
202
+ // Validate against available environments (if known from auto-discovery)
203
+ if (this.client.availableEnvironments?.length) {
204
+ const normalizedAvail = this.client.availableEnvironments.map(e => e.toUpperCase());
205
+ if (!normalizedAvail.includes(this.config.environment.toUpperCase())) {
206
+ console.warn(`[Appliqation] Invalid environment "${this.config.environment}".`);
207
+ console.warn(`[Appliqation] Available: ${this.client.availableEnvironments.join(', ')}`);
208
+ console.warn('[Appliqation] Tests will continue without reporting.');
209
+ this.appqEnabled = false;
210
+ return;
254
211
  }
255
212
  }
256
- return true;
257
- });
258
213
 
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);
214
+ if (!this.config.autoCreateRun) {
215
+ logger.info('Auto-create run disabled. Skipping run matrix creation.');
216
+ return;
217
+ }
263
218
 
264
- // Get unique device/OS/browser matrix configurations from browser projects only
265
- const matrixConfigs = DeviceOsDetector.getMatrixConfigurations(browserProjects);
219
+ // TDD mode: reuse existing run ID
220
+ if (this.existingRunId) {
221
+ logger.info(`TDD mode: Reusing existing run ID: ${this.existingRunId}`);
222
+ console.log(`[Appliqation] TDD mode: Reusing run ${this.existingRunId}`);
266
223
 
267
- logger.info(`Creating ${matrixConfigs.length} run matrices for device/OS combinations...`);
268
- logger.debug(`Matrix configurations:`, matrixConfigs);
224
+ const runInfo = {
225
+ runId: this.existingRunId,
226
+ device: 'Desktop',
227
+ os: 'Unknown',
228
+ browsers: []
229
+ };
269
230
 
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}`;
231
+ for (const project of config.projects) {
232
+ const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
233
+ const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
234
+ if (!this.runsByProject.has(projectKey)) {
235
+ this.runsByProject.set(projectKey, { ...runInfo, device: deviceInfo.device, os: deviceInfo.os });
236
+ this.resultsByRun.set(this.existingRunId, this.resultsByRun.get(this.existingRunId) || []);
237
+ this.orphansByRun.set(this.existingRunId, this.orphansByRun.get(this.existingRunId) || []);
238
+ }
239
+ }
240
+ return;
241
+ }
274
242
 
275
- // Skip if already created (shouldn't happen but safety check)
276
- if (this.runsByProject.has(projectKey)) return null;
243
+ // Get running project names
244
+ const runningProjectNames = this.getRunningProjectNames(suite);
245
+ logger.debug('Projects actually running:', runningProjectNames);
277
246
 
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
- };
247
+ const browserProjects = config.projects.filter(project => {
248
+ if (runningProjectNames.length > 0 && !runningProjectNames.includes(project.name)) {
249
+ return false;
250
+ }
251
+ if (project.testMatch) {
252
+ const testMatchStr = project.testMatch.toString();
253
+ if (testMatchStr.includes('.api')) return false;
254
+ }
255
+ return true;
256
+ });
288
257
 
289
- const run = await this.client.createRun(runOptions);
258
+ // Auto-detect browser versions
259
+ await this.injectBrowserVersions(browserProjects);
290
260
 
291
- const runInfo = {
292
- ...run,
293
- device: matrixConfig.device,
294
- os: matrixConfig.os,
295
- browsers: matrixConfig.browsers
296
- };
261
+ // Get matrix configurations
262
+ const matrixConfigs = DeviceOsDetector.getMatrixConfigurations(browserProjects);
297
263
 
298
- this.runsByProject.set(projectKey, runInfo);
299
- this.resultsByRun.set(run.runId, []);
300
- this.orphansByRun.set(run.runId, []);
264
+ logger.info(`Creating ${matrixConfigs.length} run matrices...`);
301
265
 
302
- logger.info(`Run matrix created for ${projectKey}`, {
303
- runId: run.runId,
304
- browsers: matrixConfig.browsers
305
- });
266
+ const creationStartTime = Date.now();
267
+ const creationPromises = matrixConfigs.map(async (matrixConfig) => {
268
+ const projectKey = `${matrixConfig.device}-${matrixConfig.os}`;
269
+ if (this.runsByProject.has(projectKey)) return null;
306
270
 
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
271
+ try {
272
+ const runOptions = {
273
+ scenarioId: this.config.scenarioId,
274
+ testSetId: this.config.testSetId,
275
+ environment: this.config.environment,
276
+ browsers: matrixConfig.browsers,
277
+ device: matrixConfig.device,
278
+ os: matrixConfig.os,
279
+ title: this.config.title
280
+ };
281
+
282
+ const run = await this.client.createRun(runOptions);
283
+
284
+ const runInfo = {
285
+ ...run,
286
+ device: matrixConfig.device,
287
+ os: matrixConfig.os,
288
+ browsers: matrixConfig.browsers
289
+ };
290
+
291
+ this.runsByProject.set(projectKey, runInfo);
292
+ this.resultsByRun.set(run.runId, []);
293
+ this.orphansByRun.set(run.runId, []);
294
+
295
+ logger.info(`Run matrix created for ${projectKey}`, {
296
+ runId: run.runId, browsers: matrixConfig.browsers
319
297
  });
320
298
 
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}`);
299
+ return { projectKey, runId: run.runId };
300
+ } catch (error) {
301
+ // Display validation error details if available
302
+ if (error.details) {
303
+ const details = error.details;
304
+ if (details.error_code === 'INVALID_ENVIRONMENT') {
305
+ console.warn(`[Appliqation] Invalid environment "${details.provided_environment}".`);
306
+ if (details.valid_environments?.length) {
307
+ console.warn(`[Appliqation] Available: ${details.valid_environments.join(', ')}`);
308
+ }
309
+ } else {
310
+ console.warn(`[Appliqation] ${details.message || error.message}`);
345
311
  }
312
+ } else {
313
+ console.warn(`[Appliqation] Run creation failed for ${projectKey}: ${error.message}`);
346
314
  }
315
+ return null; // Don't throw — allow other matrices to succeed
316
+ }
317
+ });
347
318
 
348
- console.error('═'.repeat(80) + '\n');
319
+ // Wait for all — wrapped in try-catch
320
+ try {
321
+ await Promise.all(creationPromises);
322
+ } catch (err) {
323
+ console.warn(`[Appliqation] ${err.message}`);
324
+ }
349
325
 
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}`);
326
+ const creationDuration = Date.now() - creationStartTime;
359
327
 
360
- // Throw error to fail the test run
361
- throw error;
362
- }
328
+ if (this.runsByProject.size > 0) {
329
+ console.log(`[Appliqation] Created ${this.runsByProject.size} run matrix(es) in ${(creationDuration / 1000).toFixed(2)}s`);
330
+ } else {
331
+ console.warn('[Appliqation] No run matrices created. Tests will continue without reporting.');
332
+ this.appqEnabled = false;
363
333
  }
364
- });
365
334
 
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`);
335
+ } catch (err) {
336
+ console.warn(`[Appliqation] ${err.message}`);
337
+ console.warn('[Appliqation] Tests will continue without reporting.');
338
+ this.appqEnabled = false;
339
+ }
372
340
  }
373
341
 
374
342
  /**
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
343
+ * Wait for run matrix to be available
379
344
  */
380
345
  async waitForRunMatrix(projectKey, maxWaitMs = 60000) {
381
346
  const startTime = Date.now();
382
- const pollInterval = 500; // Check every 500ms
347
+ const pollInterval = 500;
383
348
  let warningShown = false;
384
349
 
385
350
  while (Date.now() - startTime < maxWaitMs) {
386
351
  const runInfo = this.runsByProject.get(projectKey);
352
+ if (runInfo) return runInfo;
387
353
 
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
354
  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
355
  logger.warn(`Still waiting for run matrix after 5 seconds`, { projectKey });
400
356
  warningShown = true;
401
357
  }
402
358
 
403
- // Wait before next poll
404
359
  await new Promise(resolve => setTimeout(resolve, pollInterval));
405
360
  }
406
361
 
407
- // Timeout reached
408
362
  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
363
  return null;
413
364
  }
414
365
 
415
366
  /**
416
- * Called after a test completes
417
- * @param {Object} test - Test object
418
- * @param {Object} result - Test result
367
+ * Called after a test completes — non-blocking
419
368
  */
420
369
  async onTestEnd(test, result) {
421
- // Note: Don't increment totalTests here - it's incremented later for non-duplicates/non-orphans only
370
+ if (!this.appqEnabled) return;
422
371
 
423
372
  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
373
  const uuid = UuidExtractor.extractFromAnnotations(result.annotations || []) || UuidExtractor.extractFromTest(test);
431
- logger.debug('UUID extraction result', { title: test.title, uuid });
432
374
 
433
- // Get device info from project
434
- // Note: project() is a method, not a property
435
375
  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
376
  const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
440
377
  const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
441
378
 
442
- // Wait for run matrix to be available (handles race condition with slow backend)
443
379
  const runInfo = await this.waitForRunMatrix(projectKey, 30000);
444
380
 
445
381
  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
382
+ logger.error('Run matrix not available result not submitted', {
383
+ projectKey, testTitle: test.title
450
384
  });
451
- console.error(`\n⚠️ ERROR: Run matrix not created for ${projectKey}. Test result for "${test.title}" will not be submitted.\n`);
452
385
  return;
453
386
  }
454
387
 
455
388
  if (!uuid) {
456
- // Orphan test - no UUID found
389
+ // Orphan test
457
390
  const orphanKey = `${test.location?.file || 'unknown'}:${test.title}`;
458
-
459
391
  if (!this.uniqueOrphans.has(orphanKey)) {
460
- // First occurrence of this unique orphan
461
392
  this.orphanTests++;
462
393
  this.uniqueOrphans.set(orphanKey, {
463
394
  file: path.basename(test.location?.file || 'unknown'),
@@ -465,79 +396,48 @@ class AppliqationReporter {
465
396
  browsers: [deviceInfo.browser]
466
397
  });
467
398
  } else {
468
- // Same orphan on different browser - just add browser to list
469
399
  const orphan = this.uniqueOrphans.get(orphanKey);
470
400
  if (!orphan.browsers.includes(deviceInfo.browser)) {
471
401
  orphan.browsers.push(deviceInfo.browser);
472
402
  }
473
403
  }
474
-
475
404
  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
405
  return;
484
406
  }
485
407
 
486
- // Duplicate detection (per-run AND per-browser - allows same UUID across different browsers/devices)
408
+ // Duplicate detection
487
409
  const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
488
410
  const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
489
411
 
490
- // Check per-run+browser map (allows same UUID for different browsers in same run)
491
412
  if (submittedUuids.has(uuid)) {
492
413
  const firstOccurrence = submittedUuids.get(uuid);
493
414
  const firstFile = path.basename(firstOccurrence.file);
494
415
  const currentFile = path.basename(test.location?.file || 'unknown');
495
416
 
496
- // Track unique duplicates
497
417
  if (!this.uniqueDuplicates.has(uuid)) {
498
- // First time seeing this duplicate UUID
499
418
  this.duplicateTests++;
500
419
  this.uniqueDuplicates.set(uuid, {
501
420
  uuid,
502
421
  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
- }
422
+ { file: firstFile, title: firstOccurrence.title, browser: firstOccurrence.browser },
423
+ { file: currentFile, title: test.title, browser: deviceInfo.browser }
513
424
  ]
514
425
  });
515
426
  } 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
427
+ this.uniqueDuplicates.get(uuid).occurrences.push({
428
+ file: currentFile, title: test.title, browser: deviceInfo.browser
522
429
  });
523
430
  }
524
431
 
525
- // Keep old duplicateDetails for backward compatibility (if needed)
526
432
  this.duplicateDetails.push({
527
- uuid,
528
- firstFile,
529
- firstTitle: firstOccurrence.title,
530
- duplicateFile: currentFile,
531
- duplicateTitle: test.title
433
+ uuid, firstFile, firstTitle: firstOccurrence.title,
434
+ duplicateFile: currentFile, duplicateTitle: test.title
532
435
  });
533
436
 
534
437
  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
438
+ return;
538
439
  }
539
440
 
540
- // Store UUID occurrence (per-run only)
541
441
  submittedUuids.set(uuid, {
542
442
  file: test.location?.file || 'unknown',
543
443
  title: test.title,
@@ -545,13 +445,11 @@ class AppliqationReporter {
545
445
  });
546
446
  this.submittedUuidsByRun.set(trackingKey, submittedUuids);
547
447
 
548
- // Count only tests that will be submitted (non-duplicate, non-orphan)
549
448
  this.totalTests++;
550
- this.validTests++; // Track tests with valid UUIDs
449
+ this.validTests++;
551
450
 
552
- // Create result object
553
451
  const testResult = {
554
- runId: runInfo.runId, // Add runId for batch submission
452
+ runId: runInfo.runId,
555
453
  uuid,
556
454
  status: this.mapStatus(result.status),
557
455
  browser: deviceInfo.browser,
@@ -562,304 +460,135 @@ class AppliqationReporter {
562
460
  timestamp: Math.floor(Date.now() / 1000)
563
461
  };
564
462
 
565
- // Track result for batch submission
566
463
  this.trackResult(runInfo.runId, testResult);
567
464
 
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
- }
465
+ if (testResult.status === 'Pass') this.passedTests++;
466
+ else if (testResult.status === 'Fail') this.failedTests++;
467
+ else if (testResult.status === 'Skipped') this.skippedTests++;
576
468
 
577
- // Submit immediately if batch mode disabled
578
469
  if (!this.config.batchSubmit) {
579
470
  await this.client.submitResult(runInfo.runId, testResult);
580
471
  }
581
472
  } catch (error) {
582
- logger.error('Error processing test result', {
583
- error: error.message,
584
- test: test.title
585
- });
473
+ logger.error('Error processing test result', { error: error.message, test: test.title });
586
474
  }
587
475
  }
588
476
 
589
477
  /**
590
- * Handle runs that have only orphan tests (no valid UUIDs).
591
- * Deletes empty runs and shows clear error messages.
592
- *
478
+ * Handle orphan-only runs delete empty runs
593
479
  * @private
594
- * @returns {Promise<Array>} Array of deleted runs
595
480
  */
596
481
  async handleOrphanOnlyRuns() {
597
- if (!this.config.deleteOrphanOnlyRuns) {
598
- return []; // Feature disabled
599
- }
600
-
482
+ if (!this.config.deleteOrphanOnlyRuns) return [];
601
483
  const deletedRuns = [];
602
484
 
603
485
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
604
486
  const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
605
487
  const validCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
606
488
 
607
- // CRITICAL CHECK: Delete ONLY if all tests are orphans
608
489
  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
490
  this.printOrphanOnlyError(orphanCount, projectKey);
612
-
613
491
  try {
614
- logger.warn('Deleting orphan-only run', {
615
- runId: runInfo.runId,
616
- orphanCount,
617
- projectKey
618
- });
619
-
620
492
  await this.client.deleteRun(runInfo.runId, 'all_tests_orphaned');
621
- deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
622
493
  } 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 });
494
+ logger.error('Failed to delete orphan-only run', { error: error.message, runId: runInfo.runId });
629
495
  }
496
+ deletedRuns.push({ runId: runInfo.runId, orphanCount, projectKey });
630
497
  }
631
498
  }
632
499
 
633
- // Store for summary
634
500
  this.deletedOrphanRuns = deletedRuns;
635
501
  return deletedRuns;
636
502
  }
637
503
 
638
504
  /**
639
- * Print error message for orphan-only runs.
640
- *
641
- * @param {number} orphanCount - Number of orphan tests
642
- * @param {string} projectKey - Project key
643
505
  * @private
644
506
  */
645
507
  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');
508
+ console.error('\n' + '='.repeat(70));
509
+ console.error('[Appliqation] RUN FAILED - All tests missing UUID annotations');
510
+ console.error('='.repeat(70));
511
+ console.error(` Project: ${projectKey}`);
512
+ console.error(` Orphan Tests: ${orphanCount}`);
513
+ console.error('');
514
+ console.error(' No results were submitted. The run was automatically deleted.');
515
+ console.error('');
516
+ console.error(' Add UUID annotations to your tests:');
517
+ console.error(" test('My Test', { tag: '@uuid:123-xxx-xxx' }, async ({ page }) => { ... });");
518
+ console.error('='.repeat(70) + '\n');
691
519
  }
692
520
 
693
521
  /**
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
522
+ * Handle runs where all tests were rejected by backend
697
523
  * @private
698
- * @returns {Array} Array of deleted runs
699
524
  */
700
525
  async handleAllRejectedRuns() {
701
- if (!this.config.deleteOrphanOnlyRuns) {
702
- return []; // Feature disabled
703
- }
704
-
526
+ if (!this.config.deleteOrphanOnlyRuns) return [];
705
527
  const deletedRuns = [];
706
528
 
707
529
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
708
530
  const submittedCount = this.resultsByRun.get(runInfo.runId)?.length || 0;
709
531
  const orphanCount = this.orphansByRun.get(runInfo.runId)?.length || 0;
710
532
  const rejectedCount = this.rejectionsByRun.get(runInfo.runId) || 0;
711
-
712
- // Calculate how many were actually accepted by backend
713
533
  const acceptedCount = submittedCount - rejectedCount;
714
534
 
715
- // CRITICAL CHECK: Delete ONLY if we submitted tests but ALL were rejected
716
- // AND there are no orphans (orphans are handled separately)
717
535
  if (submittedCount > 0 && acceptedCount === 0 && orphanCount === 0) {
718
- // Show user-friendly error message FIRST (before deletion attempt)
719
- this.printAllRejectedError(submittedCount, rejectedCount, projectKey);
536
+ console.error('\n' + '='.repeat(70));
537
+ console.error('[Appliqation] RUN FAILED - All tests rejected by backend');
538
+ console.error('='.repeat(70));
539
+ console.error(` Project: ${projectKey}`);
540
+ console.error(` Submitted: ${submittedCount}, Rejected: ${rejectedCount}`);
541
+ console.error('');
542
+ console.error(' Common causes: invalid UUID format, wrong project, test case not found.');
543
+ console.error(' Check test logs for specific rejection reasons.');
544
+ console.error('='.repeat(70) + '\n');
720
545
 
721
546
  try {
722
- logger.warn('Deleting all-rejected run', {
723
- runId: runInfo.runId,
724
- submittedCount,
725
- rejectedCount,
726
- acceptedCount,
727
- projectKey
728
- });
729
547
  await this.client.deleteRun(runInfo.runId, 'all_tests_rejected');
730
- deletedRuns.push({
731
- runId: runInfo.runId,
732
- submittedCount,
733
- rejectedCount,
734
- projectKey
735
- });
736
548
  } 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
- });
549
+ logger.error('Failed to delete all-rejected run', { error: error.message });
749
550
  }
551
+ deletedRuns.push({ runId: runInfo.runId, submittedCount, rejectedCount, projectKey });
750
552
  }
751
553
  }
752
554
 
753
- // Track deleted rejected runs
754
555
  if (deletedRuns.length > 0) {
755
556
  this.deletedOrphanRuns = [...(this.deletedOrphanRuns || []), ...deletedRuns];
756
557
  }
757
-
758
558
  return deletedRuns;
759
559
  }
760
560
 
761
561
  /**
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
562
+ * Called once after all tests complete — non-blocking
814
563
  */
815
564
  async onEnd(result) {
816
565
  logger.info('Test run complete.');
817
566
 
818
567
  try {
819
- // Handle orphan-only runs BEFORE submitting results
820
568
  const orphanDeletedRuns = await this.handleOrphanOnlyRuns();
821
569
 
822
- // Skip submission if --appq flag not present
823
570
  if (this.appqEnabled) {
824
- logger.info('Submitting results to Appliqation...');
825
-
826
- // Submit batched results
827
571
  if (this.config.batchSubmit) {
828
572
  await this.submitBatchedResults();
829
573
  }
830
-
831
- // Submit orphan tests
832
574
  await this.submitOrphanTests();
833
575
 
834
- // Handle all-rejected runs AFTER submission (when we know backend rejection counts)
835
576
  const rejectedDeletedRuns = await this.handleAllRejectedRuns();
836
-
837
- // Combine all deleted runs
838
577
  const allDeletedRuns = [...(orphanDeletedRuns || []), ...(rejectedDeletedRuns || [])];
839
578
 
840
- // Exit with error code if any runs were deleted
841
579
  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
580
+ process.exitCode = 1;
844
581
  }
845
- } else {
846
- logger.info('Appliqation reporting disabled. Skipping result submission.');
847
582
  }
848
583
 
849
- // Print summary (always show, even if not submitted)
850
584
  this.printSummary();
851
-
852
- // Write summary to file (always write, even if not submitted)
853
585
  await this.writeSummaryToFile();
854
586
  } catch (error) {
855
- logger.error('Error in onEnd', {
856
- error: error.message
857
- });
587
+ logger.error('Error in onEnd', { error: error.message });
858
588
  }
859
589
  }
860
590
 
861
591
  /**
862
- * Submit batched results for all runs
863
592
  * @private
864
593
  */
865
594
  async submitBatchedResults() {
@@ -869,16 +598,11 @@ class AppliqationReporter {
869
598
  try {
870
599
  logger.info(`Submitting ${results.length} results for run ${runId}...`);
871
600
 
872
- // Find run info for this runId to get metadata
873
601
  let runInfo = null;
874
602
  for (const [projectKey, info] of this.runsByProject.entries()) {
875
- if (info.runId === runId) {
876
- runInfo = info;
877
- break;
878
- }
603
+ if (info.runId === runId) { runInfo = info; break; }
879
604
  }
880
605
 
881
- // Build run metadata for legacy endpoint
882
606
  const runMetadata = {
883
607
  nid: runInfo?.metadata?.nid || 0,
884
608
  run_timestamp: runInfo?.timestamp || Math.floor(Date.now() / 1000),
@@ -887,32 +611,24 @@ class AppliqationReporter {
887
611
 
888
612
  const summary = await this.client.submitBatch(results, {
889
613
  batchSize: this.config.batchSize,
890
- runMetadata: runMetadata,
891
- onProgress: (progress) => {
892
- logger.debug('Batch progress', progress);
893
- }
614
+ runMetadata,
615
+ onProgress: (progress) => { logger.debug('Batch progress', progress); }
894
616
  });
895
617
 
896
- // Track backend validation rejections (global and per-run)
897
618
  const rejectedCount = summary.failed || 0;
898
619
  this.backendRejectedTests += rejectedCount;
899
620
  this.rejectionsByRun.set(runId, rejectedCount);
900
621
 
901
622
  logger.info(`Results submitted for run ${runId}`, {
902
- success: summary.success,
903
- failed: summary.failed,
904
- total: summary.total
623
+ success: summary.success, failed: summary.failed, total: summary.total
905
624
  });
906
625
  } catch (error) {
907
- logger.error(`Failed to submit results for run ${runId}`, {
908
- error: error.message
909
- });
626
+ console.warn(`[Appliqation] Failed to submit batch: ${error.message}. ${results.length} results were not reported.`);
910
627
  }
911
628
  }
912
629
  }
913
630
 
914
631
  /**
915
- * Submit orphan tests for all runs
916
632
  * @private
917
633
  */
918
634
  async submitOrphanTests() {
@@ -923,596 +639,310 @@ class AppliqationReporter {
923
639
 
924
640
  try {
925
641
  await this.client.logOrphanTests(runId, orphans);
926
-
927
- logger.info(`Orphan tests logged for run ${runId}`, {
928
- count: orphans.length
929
- });
930
642
  } catch (error) {
931
- logger.error(`Failed to log orphan tests for run ${runId}`, {
932
- error: error.message
933
- });
643
+ logger.error(`Failed to log orphan tests for run ${runId}`, { error: error.message });
934
644
  }
935
645
  }
936
646
  }
937
647
 
938
- /**
939
- * Track result for batch submission
940
- * @private
941
- */
648
+ /** @private */
942
649
  trackResult(runId, result) {
943
- if (!this.resultsByRun.has(runId)) {
944
- this.resultsByRun.set(runId, []);
945
- }
650
+ if (!this.resultsByRun.has(runId)) this.resultsByRun.set(runId, []);
946
651
  this.resultsByRun.get(runId).push(result);
947
652
  }
948
653
 
949
- /**
950
- * Track orphan test
951
- * @private
952
- */
654
+ /** @private */
953
655
  trackOrphan(runId, test, result, browser) {
954
- if (!this.orphansByRun.has(runId)) {
955
- this.orphansByRun.set(runId, []);
956
- }
957
-
656
+ if (!this.orphansByRun.has(runId)) this.orphansByRun.set(runId, []);
958
657
  const orphanEntry = UuidExtractor.createOrphanEntry(test, result, browser);
959
658
  this.orphansByRun.get(runId).push(orphanEntry);
960
659
  }
961
660
 
962
- /**
963
- * Map Playwright status to Appliqation status
964
- * @private
965
- */
661
+ /** @private */
966
662
  mapStatus(status) {
967
663
  const statusMap = {
968
- 'passed': 'Pass', // Capital case to match backend expectations
969
- 'failed': 'Fail', // Capital case to match backend expectations
664
+ 'passed': 'Pass',
665
+ 'failed': 'Fail',
970
666
  'timedOut': 'Fail',
971
- 'skipped': 'Skipped', // Capital case to match backend expectations
667
+ 'skipped': 'Skipped',
972
668
  'interrupted': 'Skipped'
973
669
  };
974
-
975
670
  return statusMap[status] || 'Fail';
976
671
  }
977
672
 
978
- /**
979
- * Build comment from test result
980
- * @private
981
- */
673
+ /** @private */
982
674
  buildComment(test, result) {
983
675
  const parts = [];
984
-
985
- if (result.duration) {
986
- parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
987
- }
988
-
676
+ if (result.duration) parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
989
677
  if (result.error) {
990
678
  const errorMsg = result.error.message || result.error.toString();
991
679
  parts.push(`Error: ${errorMsg.substring(0, 500)}`);
992
680
  }
993
-
994
- if (result.retry > 0) {
995
- parts.push(`Retry: ${result.retry}`);
996
- }
997
-
681
+ if (result.retry > 0) parts.push(`Retry: ${result.retry}`);
998
682
  return parts.join(' | ');
999
683
  }
1000
684
 
1001
- /**
1002
- * Print summary report
1003
- * @private
1004
- */
685
+ /** @private */
1005
686
  printSummary() {
1006
687
  const testsSubmitted = this.totalTests;
1007
688
  const testsAccepted = testsSubmitted - this.backendRejectedTests;
1008
689
 
1009
- console.log('\n╔═══════════════════════════════════════════════════════════╗');
1010
- console.log('Appliqation Test Results Summary');
1011
- console.log('╠═══════════════════════════════════════════════════════════╣');
690
+ console.log('\n' + '='.repeat(60));
691
+ console.log(' Appliqation Test Results Summary');
692
+ console.log('='.repeat(60));
1012
693
 
1013
- // Show reporting status
1014
694
  if (!this.appqEnabled) {
1015
- console.log(' ⚠️ Appliqation Reporting: DISABLED');
1016
- console.log('(Set APPQ_ENABLE=1 or add -- --appq flag) ║');
1017
- console.log('╠═══════════════════════════════════════════════════════════╣');
695
+ console.log(' Reporting: DISABLED (use -- --appq to enable)');
696
+ console.log('-'.repeat(60));
1018
697
  } 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('║ ║');
698
+ console.log(` Submitted: ${testsSubmitted} | Accepted: ${testsAccepted} | Rejected: ${this.backendRejectedTests}`);
1024
699
  }
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
700
 
701
+ console.log(` Passed: ${this.passedTests} | Failed: ${this.failedTests} | Skipped: ${this.skippedTests}`);
702
+ console.log(` Orphan (No UUID): ${this.orphanTests} | Duplicates: ${this.duplicateTests}`);
703
+
704
+ if (this.appqEnabled && this.runsByProject.size > 0) {
705
+ console.log('-'.repeat(60));
706
+ console.log(` Run Matrices: ${this.runsByProject.size}`);
1040
707
  for (const [projectKey, runInfo] of this.runsByProject.entries()) {
1041
- console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
708
+ console.log(` ${projectKey}: ${runInfo.runId}`);
1042
709
  }
1043
-
1044
- // Show deleted orphan-only runs
1045
710
  if (this.deletedOrphanRuns.length > 0) {
1046
- console.log(`║ Deleted (Orphan-only): ${this.deletedOrphanRuns.length} ║`);
711
+ console.log(` Deleted (orphan-only): ${this.deletedOrphanRuns.length}`);
1047
712
  }
1048
713
  }
1049
714
 
1050
- console.log('╚═══════════════════════════════════════════════════════════╝\n');
715
+ console.log('='.repeat(60) + '\n');
1051
716
 
1052
717
  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
718
+ console.log(`\n[Appliqation] Warning: ${this.duplicateTests} duplicate UUID(s) detected.\n`);
1056
719
  let index = 1;
1057
720
  for (const duplicate of this.uniqueDuplicates.values()) {
1058
721
  console.log(`${index}. UUID: ${duplicate.uuid}`);
1059
- console.log(` Occurrences (${duplicate.occurrences.length}):`);
1060
722
  duplicate.occurrences.forEach((occ, i) => {
1061
723
  console.log(` ${i + 1}) ${occ.file} - "${occ.title}" (${occ.browser})`);
1062
724
  });
1063
725
  console.log('');
1064
726
  index++;
1065
727
  }
1066
-
1067
- console.log('Each test should have a unique UUID within a browser.\n');
1068
728
  }
1069
729
 
1070
730
  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
731
+ console.log(`\n[Appliqation] ${this.orphanTests} test(s) missing UUIDs not submitted.\n`);
1074
732
  let index = 1;
1075
733
  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`);
734
+ console.log(`${index}. ${orphan.file} - "${orphan.title}" (${orphan.browsers.join(', ')})`);
1079
735
  index++;
1080
736
  }
1081
-
1082
- console.log('Fix: Add UUID tag → test(\'name\', { tag: \'@uuid:1154-xxx...\' }, async () => {...});\n');
737
+ console.log("\nFix: test('name', { tag: '@uuid:1154-xxx...' }, async () => {...});\n");
1083
738
  }
1084
739
  }
1085
740
 
1086
741
  /**
1087
742
  * Write execution summary to file
1088
- * Creates timestamped txt file in AppQ_Execution_Summary folder
1089
743
  * @private
1090
744
  */
1091
745
  async writeSummaryToFile() {
1092
746
  const fs = require('fs').promises;
1093
- const path = require('path');
747
+ const pathModule = require('path');
1094
748
 
1095
749
  try {
1096
750
  this.executionEndTime = new Date();
1097
751
  const duration = this.executionEndTime - this.executionStartTime;
1098
752
 
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
753
  const baseDir = this.playwrightOutputDir;
1103
754
  const needsTestResults = !baseDir.includes('test-results');
1104
-
1105
755
  const summaryDir = needsTestResults
1106
- ? path.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
1107
- : path.join(baseDir, 'AppQ_Execution_Summary');
756
+ ? pathModule.join(baseDir, 'test-results', 'AppQ_Execution_Summary')
757
+ : pathModule.join(baseDir, 'AppQ_Execution_Summary');
1108
758
 
1109
- // Create directory if it doesn't exist
1110
759
  await fs.mkdir(summaryDir, { recursive: true });
1111
760
 
1112
- // Build filename with run title and timestamp
1113
761
  const runTitle = this.config.title || 'execution_summary';
1114
762
  const timestamp = this.formatDateTimeForFilename(this.executionStartTime);
1115
763
  const filename = `${runTitle}_${timestamp}.txt`;
1116
- const filepath = path.join(summaryDir, filename);
764
+ const filepath = pathModule.join(summaryDir, filename);
1117
765
 
1118
- // Build comprehensive summary content
1119
766
  const summaryContent = this.buildSummaryContent(duration);
1120
-
1121
- // Write to file
1122
767
  await fs.writeFile(filepath, summaryContent, 'utf8');
1123
768
 
1124
769
  logger.info(`Execution summary saved to: ${filepath}`);
1125
- console.log(`\n📄 Execution summary saved: ${filename}`);
1126
-
1127
770
  } catch (error) {
1128
771
  logger.error('Failed to write summary file', { error: error.message });
1129
- // Don't throw - file writing failure should not break test run
1130
772
  }
1131
773
  }
1132
774
 
1133
- /**
1134
- * Format date/time for filename: 2025-01-21_14-30-45
1135
- * @private
1136
- */
775
+ /** @private */
1137
776
  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}`;
777
+ const y = date.getFullYear();
778
+ const m = String(date.getMonth() + 1).padStart(2, '0');
779
+ const d = String(date.getDate()).padStart(2, '0');
780
+ const h = String(date.getHours()).padStart(2, '0');
781
+ const min = String(date.getMinutes()).padStart(2, '0');
782
+ const s = String(date.getSeconds()).padStart(2, '0');
783
+ return `${y}-${m}-${d}_${h}-${min}-${s}`;
1146
784
  }
1147
785
 
1148
- /**
1149
- * Build comprehensive summary content for file
1150
- * @private
1151
- */
786
+ /** @private */
1152
787
  buildSummaryContent(duration) {
1153
788
  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
789
  const testsSubmitted = this.totalTests;
1173
790
  const testsAccepted = testsSubmitted - this.backendRejectedTests;
1174
791
 
1175
- lines.push('╔═══════════════════════════════════════════════════════════╗');
1176
- lines.push('║ Appliqation Test Results Summary ║');
1177
- lines.push('╠═══════════════════════════════════════════════════════════╣');
792
+ lines.push('Appliqation Test Execution Summary');
793
+ lines.push('='.repeat(60));
794
+ lines.push(`Start: ${this.executionStartTime.toISOString()}`);
795
+ lines.push(`End: ${this.executionEndTime.toISOString()}`);
796
+ lines.push(`Duration: ${this.formatDuration(duration)}`);
797
+ lines.push(`Title: ${this.config.title || 'N/A'}`);
798
+ lines.push(`Reporting: ${this.appqEnabled ? 'ENABLED' : 'DISABLED'}`);
799
+ lines.push('');
1178
800
 
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('║ ║');
801
+ if (this.appqEnabled) {
802
+ lines.push(`Submitted: ${testsSubmitted} | Accepted: ${testsAccepted} | Rejected: ${this.backendRejectedTests}`);
1190
803
  }
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)
804
+ lines.push(`Passed: ${this.passedTests} | Failed: ${this.failedTests} | Skipped: ${this.skippedTests}`);
805
+ lines.push(`Orphans: ${this.orphanTests} | Duplicates: ${this.duplicateTests}`);
806
+ lines.push('');
807
+
1202
808
  if (this.appqEnabled && this.runsByProject.size > 0) {
1203
- lines.push(`║ Run Matrices Created: ${this.runsByProject.size.toString().padStart(5)} ║`);
809
+ lines.push(`Run Matrices: ${this.runsByProject.size}`);
1204
810
  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)} ║`);
811
+ lines.push(` ${projectKey}: ${runInfo.runId}`);
1208
812
  }
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
813
  lines.push('');
1225
814
  }
1226
815
 
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}`);
816
+ if (this.duplicateTests > 0) {
817
+ lines.push('Duplicate UUIDs:');
818
+ for (const dup of this.uniqueDuplicates.values()) {
819
+ lines.push(` UUID: ${dup.uuid}`);
820
+ dup.occurrences.forEach((occ, i) => {
821
+ lines.push(` ${i + 1}) ${occ.file} - "${occ.title}" (${occ.browser})`);
1243
822
  });
1244
- lines.push('');
1245
- index++;
1246
823
  }
1247
-
1248
- lines.push('Action Required: Each test should have a unique UUID within a browser.');
1249
824
  lines.push('');
1250
825
  }
1251
826
 
1252
- // Orphan Tests Warning
1253
827
  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;
828
+ lines.push('Tests missing UUIDs:');
1261
829
  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++;
830
+ lines.push(` ${orphan.file} - "${orphan.title}" (${orphan.browsers.join(', ')})`);
1267
831
  }
1268
-
1269
- lines.push('Action Required: Add UUID tags to submit results');
1270
- lines.push('Example: test(\'name\', { tag: \'@uuid:1154-abc-...\' }, async () => {...});');
1271
832
  lines.push('');
1272
833
  }
1273
834
 
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('');
835
+ lines.push('='.repeat(60));
836
+ try {
837
+ lines.push(`Generated by Appliqation SDK v${require('../../../package.json').version}`);
838
+ } catch (e) {
839
+ lines.push('Generated by Appliqation SDK');
1284
840
  }
1285
841
 
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
842
  return lines.join('\n');
1293
843
  }
1294
844
 
1295
- /**
1296
- * Format duration in human-readable format
1297
- * @private
1298
- */
845
+ /** @private */
1299
846
  formatDuration(ms) {
1300
847
  const seconds = Math.floor(ms / 1000);
1301
848
  const minutes = Math.floor(seconds / 60);
1302
849
  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
- }
850
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
851
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
852
+ return `${seconds}s`;
1311
853
  }
1312
854
 
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
- */
855
+ /** @private */
1319
856
  getRunningProjectNames(suite) {
1320
857
  const projectNames = new Set();
1321
-
1322
- const extractProjectNames = (node) => {
1323
- // Check if this node has a project
858
+ const extract = (node) => {
1324
859
  if (node.project && typeof node.project === 'function') {
1325
860
  const project = node.project();
1326
- if (project && project.name) {
1327
- projectNames.add(project.name);
1328
- }
861
+ if (project?.name) projectNames.add(project.name);
1329
862
  }
1330
-
1331
- // Recursively check suites
1332
863
  if (node.suites && Array.isArray(node.suites)) {
1333
- for (const childSuite of node.suites) {
1334
- extractProjectNames(childSuite);
1335
- }
864
+ for (const child of node.suites) extract(child);
1336
865
  }
1337
866
  };
1338
-
1339
- extractProjectNames(suite);
867
+ extract(suite);
1340
868
  return Array.from(projectNames);
1341
869
  }
1342
870
 
1343
871
  /**
1344
- * Auto-detect and inject browser versions for projects missing browserVersion metadata
1345
- * Now runs in parallel for better performance
872
+ * Auto-detect browser versions
1346
873
  * @private
1347
- * @param {Array} projects - Array of Playwright project configurations
1348
874
  */
1349
875
  async injectBrowserVersions(projects) {
1350
- // Use playwright package directly (not @playwright/test) to avoid circular dependency
1351
876
  let playwright = null;
1352
877
  try {
1353
878
  playwright = require('playwright');
1354
879
  } catch (e) {
1355
- // If playwright is not installed, try playwright-core
1356
880
  try {
1357
881
  playwright = require('playwright-core');
1358
882
  } catch (e2) {
1359
- logger.debug('Could not load playwright package for browser version detection');
1360
883
  return;
1361
884
  }
1362
885
  }
1363
886
 
1364
887
  const { chromium, firefox, webkit } = playwright;
1365
-
1366
- // Cache for browser versions to avoid launching same browser multiple times
1367
888
  const versionCache = new Map();
1368
889
 
1369
- // Create detection promises for all projects in parallel
1370
890
  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
- }
891
+ if (project.use?.metadata?.browserVersion) return null;
1378
892
 
1379
- // Get browser name from project - check multiple sources
1380
893
  let browserName = project.use?.browserName || project.use?.defaultBrowserType;
1381
-
1382
- // Fallback to project name if it matches browser names
1383
894
  if (!browserName) {
1384
895
  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
- }
896
+ if (projectName.includes('chromium') || projectName.includes('chrome')) browserName = 'chromium';
897
+ else if (projectName.includes('firefox')) browserName = 'firefox';
898
+ else if (projectName.includes('webkit') || projectName.includes('safari')) browserName = 'webkit';
1392
899
  }
1393
-
1394
900
  if (!browserName) return null;
1395
901
 
1396
902
  try {
1397
- let version = null;
903
+ let version = versionCache.get(browserName);
1398
904
 
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
905
+ if (!version) {
1405
906
  let browser = null;
1406
-
1407
907
  switch (browserName.toLowerCase()) {
1408
- case 'chromium':
1409
- case 'chrome':
908
+ case 'chromium': case 'chrome':
1410
909
  browser = await chromium.launch({ headless: true });
1411
910
  version = browser.version();
1412
911
  await browser.close();
1413
912
  break;
1414
-
1415
913
  case 'firefox':
1416
914
  browser = await firefox.launch({ headless: true });
1417
915
  version = browser.version();
1418
916
  await browser.close();
1419
917
  break;
1420
-
1421
- case 'webkit':
1422
- case 'safari':
918
+ case 'webkit': case 'safari':
1423
919
  browser = await webkit.launch({ headless: true });
1424
920
  version = browser.version();
1425
921
  await browser.close();
1426
922
  break;
1427
923
  }
1428
-
1429
- // Cache the version
1430
- if (version) {
1431
- versionCache.set(browserName, version);
1432
- }
924
+ if (version) versionCache.set(browserName, version);
1433
925
  }
1434
926
 
1435
927
  if (version) {
1436
- // Extract major version (e.g., "140.0.6778.44" -> "140")
1437
928
  const majorVersion = version.match(/^(\d+)/)?.[1];
1438
-
1439
929
  if (majorVersion) {
1440
- // Inject browserVersion into project metadata
1441
- if (!project.use.metadata) {
1442
- project.use.metadata = {};
1443
- }
930
+ if (!project.use.metadata) project.use.metadata = {};
1444
931
  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
932
  return { project: project.name, browser: browserName, version: majorVersion };
1453
933
  }
1454
934
  }
1455
935
  } 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
- });
936
+ logger.debug(`Could not auto-detect browser version for "${project.name}"`, { error: error.message });
1460
937
  }
1461
-
1462
938
  return null;
1463
939
  });
1464
940
 
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
941
+ await Promise.all(detectionPromises);
1506
942
  }
1507
943
 
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
- }
944
+ onStdOut(chunk, test, result) {}
945
+ onStdErr(chunk, test, result) {}
1516
946
  }
1517
947
 
1518
948
  module.exports = AppliqationReporter;