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