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