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