@appliqation/automation-sdk 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/package.json +107 -0
- package/src/AppliqationClient.js +562 -0
- package/src/constants.js +245 -0
- package/src/core/AuthManager.js +353 -0
- package/src/core/HttpClient.js +475 -0
- package/src/index.d.ts +333 -0
- package/src/index.js +26 -0
- package/src/playwright/JwtBrowserAuth.js +240 -0
- package/src/playwright/fixture.js +92 -0
- package/src/playwright/global-setup.js +243 -0
- package/src/playwright/helpers/jwt-browser-auth.js +227 -0
- package/src/playwright/index.js +16 -0
- package/src/reporters/cypress/CypressReporter.js +387 -0
- package/src/reporters/cypress/UuidExtractor.js +139 -0
- package/src/reporters/cypress/index.js +30 -0
- package/src/reporters/jest/JestReporter.js +361 -0
- package/src/reporters/jest/UuidExtractor.js +174 -0
- package/src/reporters/jest/index.js +28 -0
- package/src/reporters/playwright/AppliqationReporter.js +654 -0
- package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
- package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
- package/src/reporters/playwright/index.d.ts +96 -0
- package/src/reporters/playwright/index.js +14 -0
- package/src/services/OrphanTestService.js +74 -0
- package/src/services/ResultService.js +252 -0
- package/src/services/RunMatrixService.js +309 -0
- package/src/utils/PayloadBuilder.js +280 -0
- package/src/utils/RunDataNormalizer.js +335 -0
- package/src/utils/UuidValidator.js +102 -0
- package/src/utils/errors.js +217 -0
- package/src/utils/index.js +17 -0
- package/src/utils/logger.js +124 -0
- package/src/utils/mapAppqUuid.js +83 -0
- package/src/utils/validator.js +157 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
const AppliqationClient = require('../../AppliqationClient');
|
|
2
|
+
const UuidExtractor = require('./helpers/UuidExtractor');
|
|
3
|
+
const DeviceOsDetector = require('./helpers/DeviceOsDetector');
|
|
4
|
+
const PayloadBuilder = require('../../utils/PayloadBuilder');
|
|
5
|
+
const logger = require('../../utils/logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Appliqation Reporter for Playwright
|
|
9
|
+
* Automatically reports test results to Appliqation platform
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // playwright.config.js
|
|
13
|
+
* import { AppliqationReporter } from '@appliqation/automation-sdk/playwright';
|
|
14
|
+
*
|
|
15
|
+
* export default {
|
|
16
|
+
* reporter: [
|
|
17
|
+
* ['list'],
|
|
18
|
+
* [AppliqationReporter, {
|
|
19
|
+
* baseUrl: 'https://your-instance.appliqation.com',
|
|
20
|
+
* apiKey: process.env.APPLIQATION_API_KEY,
|
|
21
|
+
* projectKey: process.env.APPLIQATION_PROJECT_KEY,
|
|
22
|
+
* scenarioId: 123,
|
|
23
|
+
* environment: 'Production'
|
|
24
|
+
* }]
|
|
25
|
+
* ]
|
|
26
|
+
* };
|
|
27
|
+
*/
|
|
28
|
+
class AppliqationReporter {
|
|
29
|
+
/**
|
|
30
|
+
* Create Appliqation reporter
|
|
31
|
+
* @param {Object} config - Reporter configuration
|
|
32
|
+
* @param {string} config.baseUrl - Appliqation instance URL
|
|
33
|
+
* @param {string} config.apiKey - API key
|
|
34
|
+
* @param {string} config.projectKey - Project key
|
|
35
|
+
* @param {number} config.scenarioId - Scenario ID
|
|
36
|
+
* @param {number} [config.testSetId] - Test Set ID (alternative to scenarioId)
|
|
37
|
+
* @param {string} [config.environment='Local'] - Environment name
|
|
38
|
+
* @param {boolean} [config.autoCreateRun=true] - Auto-create run matrix
|
|
39
|
+
* @param {boolean} [config.logOrphans=true] - Log orphan tests
|
|
40
|
+
* @param {boolean} [config.batchSubmit=true] - Use batch submission
|
|
41
|
+
* @param {number} [config.batchSize=50] - Batch size for submissions
|
|
42
|
+
* @param {string} [config.logLevel='info'] - Log level
|
|
43
|
+
*/
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.config = {
|
|
46
|
+
autoCreateRun: true,
|
|
47
|
+
logOrphans: true,
|
|
48
|
+
batchSubmit: true,
|
|
49
|
+
batchSize: 50,
|
|
50
|
+
logLevel: 'info',
|
|
51
|
+
...config
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Validate configuration
|
|
55
|
+
this.validateConfig();
|
|
56
|
+
|
|
57
|
+
// Initialize SDK client
|
|
58
|
+
this.client = new AppliqationClient({
|
|
59
|
+
baseUrl: this.config.baseUrl,
|
|
60
|
+
apiKey: this.config.apiKey,
|
|
61
|
+
projectKey: this.config.projectKey,
|
|
62
|
+
rejectUnauthorized: this.config.rejectUnauthorized, // Pass through SSL configuration
|
|
63
|
+
options: {
|
|
64
|
+
logLevel: this.config.logLevel,
|
|
65
|
+
logOrphans: this.config.logOrphans
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// State tracking
|
|
70
|
+
this.runsByProject = new Map(); // Map<projectKey, runInfo>
|
|
71
|
+
this.resultsByRun = new Map(); // Map<runId, results[]>
|
|
72
|
+
this.orphansByRun = new Map(); // Map<runId, orphans[]>
|
|
73
|
+
this.browserVersionDetected = new Map(); // Map<projectKey, boolean> - Track if version detected
|
|
74
|
+
this.totalTests = 0;
|
|
75
|
+
this.passedTests = 0;
|
|
76
|
+
this.failedTests = 0;
|
|
77
|
+
this.skippedTests = 0;
|
|
78
|
+
this.orphanTests = 0;
|
|
79
|
+
|
|
80
|
+
logger.info('Appliqation Playwright Reporter initialized', {
|
|
81
|
+
baseUrl: this.config.baseUrl,
|
|
82
|
+
scenarioId: this.config.scenarioId,
|
|
83
|
+
environment: this.config.environment
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Called once before running tests
|
|
89
|
+
* @param {Object} config - Playwright config
|
|
90
|
+
* @param {Object} suite - Root test suite
|
|
91
|
+
*/
|
|
92
|
+
async onBegin(config, suite) {
|
|
93
|
+
logger.info('Test run starting...');
|
|
94
|
+
|
|
95
|
+
if (!this.config.autoCreateRun) {
|
|
96
|
+
logger.info('Auto-create run disabled. Skipping run matrix creation.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Filter out projects that have custom restrictive testMatch patterns in the original config
|
|
101
|
+
// (e.g., API project with /.*\.api\.spec\.js/)
|
|
102
|
+
// Note: When you run "npx playwright test file.spec.js", Playwright adds testMatch to ALL projects
|
|
103
|
+
// So we need to check if testMatch is a CUSTOM pattern (contains ".api" or similar)
|
|
104
|
+
const browserProjects = config.projects.filter(project => {
|
|
105
|
+
// Check if testMatch is a custom API-specific pattern
|
|
106
|
+
if (project.testMatch) {
|
|
107
|
+
const testMatchStr = project.testMatch.toString();
|
|
108
|
+
// Exclude API testing projects (testMatch contains ".api")
|
|
109
|
+
if (testMatchStr.includes('.api')) {
|
|
110
|
+
logger.debug(`Excluding API project "${project.name}" from matrix creation`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
logger.debug(`Creating matrices for ${browserProjects.length} browser projects (filtered from ${config.projects.length} total)`);
|
|
118
|
+
|
|
119
|
+
// Auto-detect browser versions for projects that don't have metadata.browserVersion configured
|
|
120
|
+
await this.injectBrowserVersions(browserProjects);
|
|
121
|
+
|
|
122
|
+
// Get unique device/OS/browser matrix configurations from browser projects only
|
|
123
|
+
const matrixConfigs = DeviceOsDetector.getMatrixConfigurations(browserProjects);
|
|
124
|
+
|
|
125
|
+
logger.info(`Creating ${matrixConfigs.length} run matrices for device/OS combinations...`);
|
|
126
|
+
logger.debug(`Matrix configurations:`, matrixConfigs);
|
|
127
|
+
|
|
128
|
+
// Create runs for each device/OS combination with ALL browsers
|
|
129
|
+
for (const matrixConfig of matrixConfigs) {
|
|
130
|
+
const projectKey = `${matrixConfig.device}-${matrixConfig.os}`;
|
|
131
|
+
|
|
132
|
+
// Skip if already created (shouldn't happen but safety check)
|
|
133
|
+
if (this.runsByProject.has(projectKey)) continue;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const runOptions = {
|
|
137
|
+
scenarioId: this.config.scenarioId,
|
|
138
|
+
testSetId: this.config.testSetId,
|
|
139
|
+
environment: this.config.environment,
|
|
140
|
+
browsers: matrixConfig.browsers, // ALL browsers for this device/OS
|
|
141
|
+
device: matrixConfig.device,
|
|
142
|
+
os: matrixConfig.os,
|
|
143
|
+
title: this.config.title || process.env.APPLIQATION_RUN_TITLE || `Automation Run - ${new Date().toISOString()}`
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const run = await this.client.createRun(runOptions);
|
|
147
|
+
|
|
148
|
+
const runInfo = {
|
|
149
|
+
...run,
|
|
150
|
+
device: matrixConfig.device,
|
|
151
|
+
os: matrixConfig.os,
|
|
152
|
+
browsers: matrixConfig.browsers
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.runsByProject.set(projectKey, runInfo);
|
|
156
|
+
this.resultsByRun.set(run.runId, []);
|
|
157
|
+
this.orphansByRun.set(run.runId, []);
|
|
158
|
+
|
|
159
|
+
logger.info(`Run matrix created for ${projectKey}`, {
|
|
160
|
+
runId: run.runId,
|
|
161
|
+
browsers: matrixConfig.browsers
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Check if this is an API validation error with details
|
|
165
|
+
if (error.details) {
|
|
166
|
+
const details = error.details;
|
|
167
|
+
|
|
168
|
+
// Log structured validation error
|
|
169
|
+
logger.error(`API validation error for ${projectKey}`, {
|
|
170
|
+
error_code: details.error_code,
|
|
171
|
+
message: details.message,
|
|
172
|
+
status: error.statusCode,
|
|
173
|
+
details: details
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Display formatted validation error to user
|
|
177
|
+
console.error('\n' + '═'.repeat(80));
|
|
178
|
+
console.error('❌ ENVIRONMENT VALIDATION ERROR');
|
|
179
|
+
console.error('═'.repeat(80));
|
|
180
|
+
console.error(`Project ID: ${details.project_id || this.config.projectKey}`);
|
|
181
|
+
console.error(`Device/OS: ${projectKey}`);
|
|
182
|
+
console.error(`Error: ${details.message || error.message}`);
|
|
183
|
+
|
|
184
|
+
if (details.error_code === 'NO_ENVIRONMENTS_CONFIGURED') {
|
|
185
|
+
console.error('\n⚠️ ACTION REQUIRED:');
|
|
186
|
+
console.error(` ${details.action_required || 'Configure testing environments in project settings'}`);
|
|
187
|
+
if (details.help_url) {
|
|
188
|
+
console.error(` URL: ${details.help_url}`);
|
|
189
|
+
}
|
|
190
|
+
} else if (details.error_code === 'INVALID_ENVIRONMENT') {
|
|
191
|
+
console.error(`\nProvided Environment: "${details.provided_environment}"`);
|
|
192
|
+
if (details.valid_environments && details.valid_environments.length > 0) {
|
|
193
|
+
console.error('\n✓ Valid Environments:');
|
|
194
|
+
details.valid_environments.forEach(env => {
|
|
195
|
+
console.error(` - ${env}`);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (details.hint) {
|
|
199
|
+
console.error(`\n💡 Hint: ${details.hint}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.error('═'.repeat(80) + '\n');
|
|
204
|
+
|
|
205
|
+
// Throw error to fail the test run visibly
|
|
206
|
+
throw new Error(`Environment validation failed: ${details.message || error.message}`);
|
|
207
|
+
} else {
|
|
208
|
+
// Generic error without validation details
|
|
209
|
+
logger.error(`Failed to create run matrix for ${projectKey}`, {
|
|
210
|
+
error: error.message,
|
|
211
|
+
stack: error.stack
|
|
212
|
+
});
|
|
213
|
+
console.error(`⚠️ WARNING: Run matrix creation failed for ${projectKey}: ${error.message}`);
|
|
214
|
+
|
|
215
|
+
// Throw error to fail the test run
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Called after a test completes
|
|
224
|
+
* @param {Object} test - Test object
|
|
225
|
+
* @param {Object} result - Test result
|
|
226
|
+
*/
|
|
227
|
+
async onTestEnd(test, result) {
|
|
228
|
+
this.totalTests++;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// Extract UUID from test - check result.annotations first since test.info().annotations.push() adds to result
|
|
232
|
+
logger.debug('Extracting UUID for test', {
|
|
233
|
+
title: test.title,
|
|
234
|
+
resultAnnotations: result.annotations || [],
|
|
235
|
+
testAnnotations: test.annotations || []
|
|
236
|
+
});
|
|
237
|
+
const uuid = UuidExtractor.extractFromAnnotations(result.annotations || []) || UuidExtractor.extractFromTest(test);
|
|
238
|
+
logger.debug('UUID extraction result', { title: test.title, uuid });
|
|
239
|
+
|
|
240
|
+
// Get device info from project
|
|
241
|
+
// Note: project() is a method, not a property
|
|
242
|
+
const project = test.parent?.project?.() || test.parent?.project;
|
|
243
|
+
|
|
244
|
+
// Browser version auto-detection happens during onBegin via getBrowserVersionFromPlaywright()
|
|
245
|
+
// No need to extract browser instance here - version is already in project metadata
|
|
246
|
+
const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
|
|
247
|
+
const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
|
|
248
|
+
|
|
249
|
+
// Get run info for this device/OS combination (pre-created in onBegin)
|
|
250
|
+
const runInfo = this.runsByProject.get(projectKey);
|
|
251
|
+
|
|
252
|
+
if (!runInfo) {
|
|
253
|
+
logger.warn('No run matrix found for project', { projectKey });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!uuid) {
|
|
258
|
+
// Orphan test - no UUID found
|
|
259
|
+
this.orphanTests++;
|
|
260
|
+
this.trackOrphan(runInfo.runId, test, result, deviceInfo.browser);
|
|
261
|
+
|
|
262
|
+
logger.warn('Test without UUID', {
|
|
263
|
+
title: test.title,
|
|
264
|
+
file: test.location?.file,
|
|
265
|
+
browser: deviceInfo.browser
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Create result object
|
|
272
|
+
const testResult = {
|
|
273
|
+
runId: runInfo.runId, // Add runId for batch submission
|
|
274
|
+
uuid,
|
|
275
|
+
status: this.mapStatus(result.status),
|
|
276
|
+
browser: deviceInfo.browser,
|
|
277
|
+
device: deviceInfo.device,
|
|
278
|
+
os: deviceInfo.os,
|
|
279
|
+
comment: this.buildComment(test, result),
|
|
280
|
+
duration: result.duration,
|
|
281
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Track result for batch submission
|
|
285
|
+
this.trackResult(runInfo.runId, testResult);
|
|
286
|
+
|
|
287
|
+
// Update counters
|
|
288
|
+
if (testResult.status === 'passed') {
|
|
289
|
+
this.passedTests++;
|
|
290
|
+
} else if (testResult.status === 'failed') {
|
|
291
|
+
this.failedTests++;
|
|
292
|
+
} else if (testResult.status === 'skipped') {
|
|
293
|
+
this.skippedTests++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Submit immediately if batch mode disabled
|
|
297
|
+
if (!this.config.batchSubmit) {
|
|
298
|
+
await this.client.submitResult(runInfo.runId, testResult);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logger.error('Error processing test result', {
|
|
302
|
+
error: error.message,
|
|
303
|
+
test: test.title
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Called once after all tests complete
|
|
310
|
+
*/
|
|
311
|
+
async onEnd(result) {
|
|
312
|
+
logger.info('Test run complete. Submitting results...');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Submit batched results
|
|
316
|
+
if (this.config.batchSubmit) {
|
|
317
|
+
await this.submitBatchedResults();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Submit orphan tests
|
|
321
|
+
await this.submitOrphanTests();
|
|
322
|
+
|
|
323
|
+
// Print summary
|
|
324
|
+
this.printSummary();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logger.error('Error in onEnd', {
|
|
327
|
+
error: error.message
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Submit batched results for all runs
|
|
334
|
+
* @private
|
|
335
|
+
*/
|
|
336
|
+
async submitBatchedResults() {
|
|
337
|
+
for (const [runId, results] of this.resultsByRun.entries()) {
|
|
338
|
+
if (results.length === 0) continue;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
logger.info(`Submitting ${results.length} results for run ${runId}...`);
|
|
342
|
+
|
|
343
|
+
// Find run info for this runId to get metadata
|
|
344
|
+
let runInfo = null;
|
|
345
|
+
for (const [projectKey, info] of this.runsByProject.entries()) {
|
|
346
|
+
if (info.runId === runId) {
|
|
347
|
+
runInfo = info;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build run metadata for legacy endpoint
|
|
353
|
+
const runMetadata = {
|
|
354
|
+
nid: runInfo?.metadata?.nid || 0,
|
|
355
|
+
run_timestamp: runInfo?.timestamp || Math.floor(Date.now() / 1000),
|
|
356
|
+
environment: runInfo?.metadata?.environment || this.config.environment || 'Local'
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const summary = await this.client.submitBatch(results, {
|
|
360
|
+
batchSize: this.config.batchSize,
|
|
361
|
+
runMetadata: runMetadata,
|
|
362
|
+
onProgress: (progress) => {
|
|
363
|
+
logger.debug('Batch progress', progress);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
logger.info(`Results submitted for run ${runId}`, {
|
|
368
|
+
success: summary.success,
|
|
369
|
+
failed: summary.failed,
|
|
370
|
+
total: summary.total
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger.error(`Failed to submit results for run ${runId}`, {
|
|
374
|
+
error: error.message
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Submit orphan tests for all runs
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
async submitOrphanTests() {
|
|
385
|
+
if (!this.config.logOrphans) return;
|
|
386
|
+
|
|
387
|
+
for (const [runId, orphans] of this.orphansByRun.entries()) {
|
|
388
|
+
if (orphans.length === 0) continue;
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
await this.client.logOrphanTests(runId, orphans);
|
|
392
|
+
|
|
393
|
+
logger.info(`Orphan tests logged for run ${runId}`, {
|
|
394
|
+
count: orphans.length
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
logger.error(`Failed to log orphan tests for run ${runId}`, {
|
|
398
|
+
error: error.message
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Track result for batch submission
|
|
406
|
+
* @private
|
|
407
|
+
*/
|
|
408
|
+
trackResult(runId, result) {
|
|
409
|
+
if (!this.resultsByRun.has(runId)) {
|
|
410
|
+
this.resultsByRun.set(runId, []);
|
|
411
|
+
}
|
|
412
|
+
this.resultsByRun.get(runId).push(result);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Track orphan test
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
trackOrphan(runId, test, result, browser) {
|
|
420
|
+
if (!this.orphansByRun.has(runId)) {
|
|
421
|
+
this.orphansByRun.set(runId, []);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const orphanEntry = UuidExtractor.createOrphanEntry(test, result, browser);
|
|
425
|
+
this.orphansByRun.get(runId).push(orphanEntry);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Map Playwright status to Appliqation status
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
mapStatus(status) {
|
|
433
|
+
const statusMap = {
|
|
434
|
+
'passed': 'passed',
|
|
435
|
+
'failed': 'failed',
|
|
436
|
+
'timedOut': 'failed',
|
|
437
|
+
'skipped': 'skipped',
|
|
438
|
+
'interrupted': 'skipped'
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return statusMap[status] || 'failed';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Build comment from test result
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
buildComment(test, result) {
|
|
449
|
+
const parts = [];
|
|
450
|
+
|
|
451
|
+
if (result.duration) {
|
|
452
|
+
parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (result.error) {
|
|
456
|
+
const errorMsg = result.error.message || result.error.toString();
|
|
457
|
+
parts.push(`Error: ${errorMsg.substring(0, 500)}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (result.retry > 0) {
|
|
461
|
+
parts.push(`Retry: ${result.retry}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return parts.join(' | ');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Print summary report
|
|
469
|
+
* @private
|
|
470
|
+
*/
|
|
471
|
+
printSummary() {
|
|
472
|
+
console.log('\n╔═══════════════════════════════════════════════════════════╗');
|
|
473
|
+
console.log('║ Appliqation Test Results Summary ║');
|
|
474
|
+
console.log('╠═══════════════════════════════════════════════════════════╣');
|
|
475
|
+
console.log(`║ Total Tests: ${this.totalTests.toString().padStart(5)} ║`);
|
|
476
|
+
console.log(`║ Passed: ${this.passedTests.toString().padStart(5)} ║`);
|
|
477
|
+
console.log(`║ Failed: ${this.failedTests.toString().padStart(5)} ║`);
|
|
478
|
+
console.log(`║ Skipped: ${this.skippedTests.toString().padStart(5)} ║`);
|
|
479
|
+
console.log(`║ Orphan (No UUID): ${this.orphanTests.toString().padStart(5)} ║`);
|
|
480
|
+
console.log('╠═══════════════════════════════════════════════════════════╣');
|
|
481
|
+
console.log(`║ Run Matrices Created: ${this.runsByProject.size} ║`);
|
|
482
|
+
|
|
483
|
+
for (const [projectKey, runInfo] of this.runsByProject.entries()) {
|
|
484
|
+
console.log(`║ ${projectKey}: ${runInfo.runId} ║`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('╚═══════════════════════════════════════════════════════════╝\n');
|
|
488
|
+
|
|
489
|
+
if (this.orphanTests > 0) {
|
|
490
|
+
console.log('⚠️ Warning: Some tests are missing UUID annotations!');
|
|
491
|
+
console.log(' Add UUIDs to map tests to Appliqation test cases:');
|
|
492
|
+
console.log(' test(\'My Test\', { tag: \'@uuid:123-xxx-...\' }, async ({ page }) => { ... });\n');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Extract project names that are actually running from the test suite
|
|
498
|
+
* @private
|
|
499
|
+
* @param {Object} suite - Root test suite
|
|
500
|
+
* @returns {Array<string>} Array of project names
|
|
501
|
+
*/
|
|
502
|
+
getRunningProjectNames(suite) {
|
|
503
|
+
const projectNames = new Set();
|
|
504
|
+
|
|
505
|
+
const extractProjectNames = (node) => {
|
|
506
|
+
// Check if this node has a project
|
|
507
|
+
if (node.project && typeof node.project === 'function') {
|
|
508
|
+
const project = node.project();
|
|
509
|
+
if (project && project.name) {
|
|
510
|
+
projectNames.add(project.name);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Recursively check suites
|
|
515
|
+
if (node.suites && Array.isArray(node.suites)) {
|
|
516
|
+
for (const childSuite of node.suites) {
|
|
517
|
+
extractProjectNames(childSuite);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
extractProjectNames(suite);
|
|
523
|
+
return Array.from(projectNames);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Auto-detect and inject browser versions for projects missing browserVersion metadata
|
|
528
|
+
* @private
|
|
529
|
+
* @param {Array} projects - Array of Playwright project configurations
|
|
530
|
+
*/
|
|
531
|
+
async injectBrowserVersions(projects) {
|
|
532
|
+
// Use playwright package directly (not @playwright/test) to avoid circular dependency
|
|
533
|
+
let playwright = null;
|
|
534
|
+
try {
|
|
535
|
+
playwright = require('playwright');
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// If playwright is not installed, try playwright-core
|
|
538
|
+
try {
|
|
539
|
+
playwright = require('playwright-core');
|
|
540
|
+
} catch (e2) {
|
|
541
|
+
logger.debug('Could not load playwright package for browser version detection');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const { chromium, firefox, webkit } = playwright;
|
|
547
|
+
|
|
548
|
+
for (const project of projects) {
|
|
549
|
+
// Skip if browserVersion is already configured
|
|
550
|
+
if (project.use?.metadata?.browserVersion) {
|
|
551
|
+
logger.debug(`Project "${project.name}" already has browserVersion configured`, {
|
|
552
|
+
version: project.use.metadata.browserVersion
|
|
553
|
+
});
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const browserName = project.use?.browserName;
|
|
558
|
+
if (!browserName) continue;
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
let browser = null;
|
|
562
|
+
let version = null;
|
|
563
|
+
|
|
564
|
+
// Launch browser to detect version
|
|
565
|
+
switch (browserName.toLowerCase()) {
|
|
566
|
+
case 'chromium':
|
|
567
|
+
case 'chrome':
|
|
568
|
+
browser = await chromium.launch();
|
|
569
|
+
version = browser.version();
|
|
570
|
+
await browser.close();
|
|
571
|
+
break;
|
|
572
|
+
|
|
573
|
+
case 'firefox':
|
|
574
|
+
browser = await firefox.launch();
|
|
575
|
+
version = browser.version();
|
|
576
|
+
await browser.close();
|
|
577
|
+
break;
|
|
578
|
+
|
|
579
|
+
case 'webkit':
|
|
580
|
+
case 'safari':
|
|
581
|
+
browser = await webkit.launch();
|
|
582
|
+
version = browser.version();
|
|
583
|
+
await browser.close();
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (version) {
|
|
588
|
+
// Extract major version (e.g., "140.0.6778.44" -> "140")
|
|
589
|
+
const majorVersion = version.match(/^(\d+)/)?.[1];
|
|
590
|
+
|
|
591
|
+
if (majorVersion) {
|
|
592
|
+
// Inject browserVersion into project metadata
|
|
593
|
+
if (!project.use.metadata) {
|
|
594
|
+
project.use.metadata = {};
|
|
595
|
+
}
|
|
596
|
+
project.use.metadata.browserVersion = majorVersion;
|
|
597
|
+
|
|
598
|
+
logger.info(`Auto-detected browser version for project "${project.name}"`, {
|
|
599
|
+
browser: browserName,
|
|
600
|
+
version: majorVersion,
|
|
601
|
+
fullVersion: version
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (error) {
|
|
606
|
+
// Silently fail - version detection is optional
|
|
607
|
+
logger.debug(`Could not auto-detect browser version for project "${project.name}"`, {
|
|
608
|
+
error: error.message
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Validate reporter configuration
|
|
616
|
+
* @private
|
|
617
|
+
*/
|
|
618
|
+
validateConfig() {
|
|
619
|
+
if (!this.config.baseUrl) {
|
|
620
|
+
throw new Error('baseUrl is required');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!this.config.apiKey) {
|
|
624
|
+
throw new Error('apiKey is required');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!this.config.projectKey) {
|
|
628
|
+
throw new Error('projectKey is required');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Note: scenarioId and testSetId are now optional
|
|
632
|
+
// If neither is provided, will default to 0 for generic automation runs
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Called when the reporter starts
|
|
637
|
+
* @param {Object} runnerSuite - Root suite
|
|
638
|
+
* @param {Object} config - Playwright config
|
|
639
|
+
*/
|
|
640
|
+
onStdOut(chunk, test, result) {
|
|
641
|
+
// Optional: capture stdout if needed
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Called when the reporter starts
|
|
646
|
+
* @param {Object} runnerSuite - Root suite
|
|
647
|
+
* @param {Object} config - Playwright config
|
|
648
|
+
*/
|
|
649
|
+
onStdErr(chunk, test, result) {
|
|
650
|
+
// Optional: capture stderr if needed
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
module.exports = AppliqationReporter;
|