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