@appliqation/automation-sdk 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/package.json +107 -0
- package/src/AppliqationClient.js +562 -0
- package/src/constants.js +245 -0
- package/src/core/AuthManager.js +353 -0
- package/src/core/HttpClient.js +475 -0
- package/src/index.d.ts +333 -0
- package/src/index.js +26 -0
- package/src/playwright/JwtBrowserAuth.js +240 -0
- package/src/playwright/fixture.js +92 -0
- package/src/playwright/global-setup.js +243 -0
- package/src/playwright/helpers/jwt-browser-auth.js +227 -0
- package/src/playwright/index.js +16 -0
- package/src/reporters/cypress/CypressReporter.js +387 -0
- package/src/reporters/cypress/UuidExtractor.js +139 -0
- package/src/reporters/cypress/index.js +30 -0
- package/src/reporters/jest/JestReporter.js +361 -0
- package/src/reporters/jest/UuidExtractor.js +174 -0
- package/src/reporters/jest/index.js +28 -0
- package/src/reporters/playwright/AppliqationReporter.js +654 -0
- package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
- package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
- package/src/reporters/playwright/index.d.ts +96 -0
- package/src/reporters/playwright/index.js +14 -0
- package/src/services/OrphanTestService.js +74 -0
- package/src/services/ResultService.js +252 -0
- package/src/services/RunMatrixService.js +309 -0
- package/src/utils/PayloadBuilder.js +280 -0
- package/src/utils/RunDataNormalizer.js +335 -0
- package/src/utils/UuidValidator.js +102 -0
- package/src/utils/errors.js +217 -0
- package/src/utils/index.js +17 -0
- package/src/utils/logger.js +124 -0
- package/src/utils/mapAppqUuid.js +83 -0
- package/src/utils/validator.js +157 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
const { normalizeBrowser } = require('../utils/RunDataNormalizer');
|
|
3
|
+
|
|
4
|
+
class ResultService {
|
|
5
|
+
constructor(httpClient) {
|
|
6
|
+
this.http = httpClient;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Submit a single test result
|
|
11
|
+
* @param {string} runId - Run ID
|
|
12
|
+
* @param {Object} result - Test result
|
|
13
|
+
* @param {Object} runMetadata - Run metadata (nid, run_timestamp, environment)
|
|
14
|
+
* @returns {Promise<Object>} Response from server
|
|
15
|
+
*/
|
|
16
|
+
async submitSingle(runId, result, runMetadata = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const payload = [this.buildLegacyResultPayload(result, runMetadata)];
|
|
19
|
+
|
|
20
|
+
logger.debug('Submitting single result', {
|
|
21
|
+
runId,
|
|
22
|
+
uuid: result.uuid,
|
|
23
|
+
status: result.status
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Use existing /api/insert/scenario/result endpoint (expects array)
|
|
27
|
+
const response = await this.http.post('/api/insert/scenario/result', payload);
|
|
28
|
+
|
|
29
|
+
// Check if request was successful
|
|
30
|
+
if (!response.success) {
|
|
31
|
+
throw new Error(response.error || 'Result submission failed');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return response;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.error('Failed to submit result', {
|
|
37
|
+
error: error.message,
|
|
38
|
+
runId,
|
|
39
|
+
uuid: result.uuid
|
|
40
|
+
});
|
|
41
|
+
throw new Error(`Failed to submit result: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Submit batch of results with smart grouping
|
|
47
|
+
* @param {Array} results - Array of test results
|
|
48
|
+
* @param {Object} options - Batch options
|
|
49
|
+
* @param {number} options.batchSize - Number of results per batch (default: 50)
|
|
50
|
+
* @param {Function} options.onProgress - Progress callback
|
|
51
|
+
* @param {boolean} options.retryFailures - Retry failed submissions (default: true)
|
|
52
|
+
* @param {Object} options.runMetadata - Run metadata (nid, run_timestamp, environment)
|
|
53
|
+
* @returns {Promise<Object>} Submission summary
|
|
54
|
+
*/
|
|
55
|
+
async submitBatch(results, options = {}) {
|
|
56
|
+
const {
|
|
57
|
+
batchSize = 50,
|
|
58
|
+
onProgress = null,
|
|
59
|
+
retryFailures = true,
|
|
60
|
+
runMetadata = {}
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
if (!results || results.length === 0) {
|
|
64
|
+
logger.warn('No results to submit');
|
|
65
|
+
return {
|
|
66
|
+
success: 0,
|
|
67
|
+
failed: 0,
|
|
68
|
+
total: 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.info(`Starting batch submission of ${results.length} results`);
|
|
73
|
+
|
|
74
|
+
// Group results by browser for efficient metadata updates
|
|
75
|
+
const grouped = this.groupByBrowser(results);
|
|
76
|
+
|
|
77
|
+
const allResults = [];
|
|
78
|
+
const failures = [];
|
|
79
|
+
|
|
80
|
+
for (const [browser, browserResults] of Object.entries(grouped)) {
|
|
81
|
+
const batches = this.chunkArray(browserResults, batchSize);
|
|
82
|
+
|
|
83
|
+
logger.info(`Submitting ${browserResults.length} results for ${browser} in ${batches.length} batches`);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < batches.length; i++) {
|
|
86
|
+
const batch = batches[i];
|
|
87
|
+
// Transform results to legacy format for existing endpoint
|
|
88
|
+
const payloads = batch.map(r => this.buildLegacyResultPayload(r, runMetadata));
|
|
89
|
+
|
|
90
|
+
logger.debug('Batch payload', { payload: JSON.stringify(payloads, null, 2) });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Use existing /api/insert/scenario/result endpoint
|
|
94
|
+
const response = await this.http.post('/api/insert/scenario/result', payloads);
|
|
95
|
+
|
|
96
|
+
// Check if request was successful
|
|
97
|
+
if (!response.success) {
|
|
98
|
+
throw new Error(response.error || 'Batch submission failed');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Track successful submissions
|
|
102
|
+
allResults.push(...batch);
|
|
103
|
+
|
|
104
|
+
// Call progress callback
|
|
105
|
+
if (onProgress) {
|
|
106
|
+
onProgress({
|
|
107
|
+
browser,
|
|
108
|
+
batchIndex: i + 1,
|
|
109
|
+
totalBatches: batches.length,
|
|
110
|
+
completed: (i + 1) * batchSize,
|
|
111
|
+
total: browserResults.length
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logger.debug(`Batch ${i + 1}/${batches.length} submitted for ${browser}`, {
|
|
116
|
+
count: batch.length
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error(`Batch ${i + 1}/${batches.length} failed for ${browser}`, {
|
|
120
|
+
error: error.message,
|
|
121
|
+
count: batch.length
|
|
122
|
+
});
|
|
123
|
+
failures.push(...batch);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Retry failures individually
|
|
129
|
+
if (retryFailures && failures.length > 0) {
|
|
130
|
+
logger.info(`Retrying ${failures.length} failed results...`);
|
|
131
|
+
|
|
132
|
+
for (const result of failures) {
|
|
133
|
+
try {
|
|
134
|
+
await this.submitSingle(result.runId, result, runMetadata);
|
|
135
|
+
allResults.push(result);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error(`Failed to submit result on retry`, {
|
|
138
|
+
uuid: result.uuid,
|
|
139
|
+
error: error.message
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const summary = {
|
|
146
|
+
success: allResults.length,
|
|
147
|
+
failed: results.length - allResults.length,
|
|
148
|
+
total: results.length
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
logger.info('Batch submission completed', summary);
|
|
152
|
+
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build result payload for API (single submission)
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
buildResultPayload(runId, result) {
|
|
161
|
+
return {
|
|
162
|
+
run_id: runId,
|
|
163
|
+
test_case_uuid: result.uuid,
|
|
164
|
+
status: result.status,
|
|
165
|
+
duration: result.duration || 0,
|
|
166
|
+
browser: result.browser,
|
|
167
|
+
device: result.device || 'Desktop',
|
|
168
|
+
os: result.os || '',
|
|
169
|
+
error_message: result.error || result.comment || '',
|
|
170
|
+
timestamp: result.timestamp || Math.floor(Date.now() / 1000)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build result payload for batch submission (no run_id)
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
buildBatchResultPayload(result) {
|
|
179
|
+
return {
|
|
180
|
+
test_case_uuid: result.uuid,
|
|
181
|
+
status: result.status,
|
|
182
|
+
duration: result.duration || 0,
|
|
183
|
+
browser: result.browser,
|
|
184
|
+
device: result.device || 'Desktop',
|
|
185
|
+
os: result.os || '',
|
|
186
|
+
error_message: result.error || result.comment || ''
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build result payload for legacy /api/insert/scenario/result endpoint
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
buildLegacyResultPayload(result, runMetadata = {}) {
|
|
195
|
+
// Map status to legacy format (Pass/Fail capitalized)
|
|
196
|
+
const statusMap = {
|
|
197
|
+
'passed': 'Pass',
|
|
198
|
+
'failed': 'Fail',
|
|
199
|
+
'skipped': 'Skipped'
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
run_id: result.runId,
|
|
204
|
+
nid: runMetadata.nid || 0,
|
|
205
|
+
run_timestamp: runMetadata.run_timestamp || Math.floor(Date.now() / 1000),
|
|
206
|
+
uuid: result.uuid,
|
|
207
|
+
parent_uuid: result.parent_uuid || '',
|
|
208
|
+
status: statusMap[result.status] || result.status,
|
|
209
|
+
browserName: normalizeBrowser(result.browser || 'Unknown Browser'),
|
|
210
|
+
environment: runMetadata.environment || 'Local'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Group results by browser
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
groupByBrowser(results) {
|
|
219
|
+
return results.reduce((acc, result) => {
|
|
220
|
+
const browser = result.browser || 'unknown';
|
|
221
|
+
if (!acc[browser]) {
|
|
222
|
+
acc[browser] = [];
|
|
223
|
+
}
|
|
224
|
+
acc[browser].push(result);
|
|
225
|
+
return acc;
|
|
226
|
+
}, {});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Split array into chunks
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
chunkArray(array, size) {
|
|
234
|
+
const chunks = [];
|
|
235
|
+
for (let i = 0; i < array.length; i += size) {
|
|
236
|
+
chunks.push(array.slice(i, i + size));
|
|
237
|
+
}
|
|
238
|
+
return chunks;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extract NID from UUID
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
extractNid(uuid) {
|
|
246
|
+
if (!uuid || typeof uuid !== 'string') return null;
|
|
247
|
+
const parts = uuid.split('-');
|
|
248
|
+
return parseInt(parts[0]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = ResultService;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
const { normalizeBrowser, normalizeOS, normalizeDevice } = require('../utils/RunDataNormalizer');
|
|
3
|
+
|
|
4
|
+
class RunMatrixService {
|
|
5
|
+
constructor(httpClient, config) {
|
|
6
|
+
this.http = httpClient;
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a new test run matrix
|
|
12
|
+
* @param {Object} options - Run configuration
|
|
13
|
+
* @param {number} options.scenarioId - Scenario node ID
|
|
14
|
+
* @param {number} options.testSetId - Test Set node ID (alternative to scenarioId)
|
|
15
|
+
* @param {string} options.type - 'scenario' or 'testset'
|
|
16
|
+
* @param {string} options.environment - Environment name
|
|
17
|
+
* @param {string[]} options.browsers - Array of browser strings
|
|
18
|
+
* @param {string} options.device - Device type (Desktop, Mobile, Tablet)
|
|
19
|
+
* @param {string} options.os - Operating system
|
|
20
|
+
* @param {string} options.title - Custom run title
|
|
21
|
+
* @returns {Promise<Object>} { runId, token, timestamp, metadata }
|
|
22
|
+
*/
|
|
23
|
+
async create(options) {
|
|
24
|
+
try {
|
|
25
|
+
// Validate required fields
|
|
26
|
+
this.validateCreateOptions(options);
|
|
27
|
+
|
|
28
|
+
// Build payload for unified endpoint
|
|
29
|
+
// Support both numeric project IDs and base64-encoded project keys
|
|
30
|
+
// Numeric IDs are sent as-is (temporary workaround for duplicate key issues)
|
|
31
|
+
// Base64 keys are double-encoded for transmission
|
|
32
|
+
const projectKeyValue = /^\d+$/.test(this.config.projectKey)
|
|
33
|
+
? this.config.projectKey // Numeric ID - send as-is
|
|
34
|
+
: Buffer.from(this.config.projectKey).toString('base64'); // Base64 key - encode for transmission
|
|
35
|
+
|
|
36
|
+
// Normalize browsers array
|
|
37
|
+
const browsers = options.browsers || ['Chrome'];
|
|
38
|
+
const normalizedBrowsers = browsers.map(browser => normalizeBrowser(browser));
|
|
39
|
+
|
|
40
|
+
const payload = {
|
|
41
|
+
project_key: projectKeyValue,
|
|
42
|
+
environment: options.environment || 'Local',
|
|
43
|
+
browsers: normalizedBrowsers,
|
|
44
|
+
device: normalizeDevice(options.device || this.detectDevice()),
|
|
45
|
+
os: normalizeOS(options.os || this.detectOS()),
|
|
46
|
+
title: options.title || this.config.runTitle || `Automation Run - ${new Date().toISOString()}`
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Add scenario_id, testset_id, or default to 0 for generic automation runs
|
|
50
|
+
if (options.scenarioId) {
|
|
51
|
+
payload.scenario_id = parseInt(options.scenarioId);
|
|
52
|
+
payload.type = options.type || 'scenario';
|
|
53
|
+
} else if (options.testSetId) {
|
|
54
|
+
payload.testset_id = parseInt(options.testSetId);
|
|
55
|
+
payload.type = 'testset';
|
|
56
|
+
} else {
|
|
57
|
+
// No scenario or test set - use 0 for generic automation run
|
|
58
|
+
payload.scenario_id = 0;
|
|
59
|
+
payload.type = options.type || 'automation';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.info('Creating run matrix...', {
|
|
63
|
+
type: payload.type,
|
|
64
|
+
id: payload.scenario_id || payload.testset_id,
|
|
65
|
+
environment: payload.environment,
|
|
66
|
+
browsers: payload.browsers
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Call unified endpoint with detailed timing logs
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
logger.info('Sending POST request to /api/automation/run/create...', {
|
|
72
|
+
timestamp: new Date().toISOString()
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const response = await this.http.post('/api/automation/run/create', payload);
|
|
76
|
+
|
|
77
|
+
const duration = Date.now() - startTime;
|
|
78
|
+
logger.info('POST request completed', {
|
|
79
|
+
duration: `${duration}ms`,
|
|
80
|
+
timestamp: new Date().toISOString()
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Debug logging to understand response structure
|
|
84
|
+
logger.debug('Raw response from HttpClient:', {
|
|
85
|
+
success: response.success,
|
|
86
|
+
hasData: !!response.data,
|
|
87
|
+
dataKeys: response.data ? Object.keys(response.data) : [],
|
|
88
|
+
fullResponse: JSON.stringify(response)
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// HttpClient wraps response in {success, data} - extract the actual API response
|
|
92
|
+
const apiResponse = response.data || response;
|
|
93
|
+
|
|
94
|
+
logger.debug('Extracted API response:', {
|
|
95
|
+
hasRunId: !!apiResponse.run_id,
|
|
96
|
+
apiResponseKeys: Object.keys(apiResponse),
|
|
97
|
+
runId: apiResponse.run_id
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Validate: HttpClient success + API response exists + API has run_id
|
|
101
|
+
if (!response.success || !apiResponse || !apiResponse.run_id) {
|
|
102
|
+
logger.error('Validation failed:', {
|
|
103
|
+
responseSuccess: response.success,
|
|
104
|
+
hasApiResponse: !!apiResponse,
|
|
105
|
+
hasRunId: !!apiResponse?.run_id,
|
|
106
|
+
response: JSON.stringify(response, null, 2)
|
|
107
|
+
});
|
|
108
|
+
throw new Error('Run creation failed: Invalid response from server');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Store JWT token in auth manager for subsequent requests
|
|
112
|
+
if (apiResponse.jwt_token && this.http.auth) {
|
|
113
|
+
this.http.auth.setJwtToken(apiResponse.jwt_token);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
logger.info('Run matrix created successfully', {
|
|
117
|
+
runId: apiResponse.run_id,
|
|
118
|
+
timestamp: apiResponse.run_timestamp
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
runId: apiResponse.run_id,
|
|
123
|
+
token: apiResponse.jwt_token,
|
|
124
|
+
timestamp: apiResponse.run_timestamp,
|
|
125
|
+
metadata: apiResponse.metadata
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Extract API validation error details if available
|
|
129
|
+
let errorMessage = error.message;
|
|
130
|
+
let errorDetails = null;
|
|
131
|
+
|
|
132
|
+
// Check if this is an HTTP error with response data (API validation errors)
|
|
133
|
+
if (error.response && error.response.data) {
|
|
134
|
+
errorDetails = error.response.data;
|
|
135
|
+
|
|
136
|
+
// Use API error message if available
|
|
137
|
+
if (errorDetails.message) {
|
|
138
|
+
errorMessage = errorDetails.message;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
logger.error('API validation error', {
|
|
142
|
+
status: error.response.status,
|
|
143
|
+
error_code: errorDetails.error_code,
|
|
144
|
+
message: errorDetails.message,
|
|
145
|
+
details: errorDetails
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
logger.error('Failed to create run matrix', {
|
|
149
|
+
error: error.message,
|
|
150
|
+
options
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create enhanced error with details for upstream handlers
|
|
155
|
+
const enhancedError = new Error(errorMessage);
|
|
156
|
+
enhancedError.details = errorDetails;
|
|
157
|
+
enhancedError.statusCode = error.response?.status;
|
|
158
|
+
|
|
159
|
+
throw enhancedError;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create multiple runs for device/OS matrix
|
|
165
|
+
* @param {Array} configs - Array of run configurations
|
|
166
|
+
* @returns {Promise<Array>} Array of created runs
|
|
167
|
+
*/
|
|
168
|
+
async createMultiple(configs) {
|
|
169
|
+
const runs = [];
|
|
170
|
+
const errors = [];
|
|
171
|
+
|
|
172
|
+
logger.info(`Creating ${configs.length} run matrices...`);
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < configs.length; i++) {
|
|
175
|
+
const config = configs[i];
|
|
176
|
+
try {
|
|
177
|
+
const run = await this.create(config);
|
|
178
|
+
runs.push({
|
|
179
|
+
...run,
|
|
180
|
+
device: config.device,
|
|
181
|
+
os: config.os,
|
|
182
|
+
config: config
|
|
183
|
+
});
|
|
184
|
+
logger.info(`Run ${i + 1}/${configs.length} created: ${run.runId}`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const errorInfo = {
|
|
187
|
+
index: i,
|
|
188
|
+
config,
|
|
189
|
+
error: error.message
|
|
190
|
+
};
|
|
191
|
+
errors.push(errorInfo);
|
|
192
|
+
logger.error(`Failed to create run ${i + 1}/${configs.length}`, errorInfo);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (errors.length > 0) {
|
|
197
|
+
logger.warn(`${errors.length} out of ${configs.length} runs failed to create`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
runs,
|
|
202
|
+
errors,
|
|
203
|
+
summary: {
|
|
204
|
+
total: configs.length,
|
|
205
|
+
success: runs.length,
|
|
206
|
+
failed: errors.length
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Add browser to existing run matrix dynamically
|
|
213
|
+
* @param {string} runId - Run ID
|
|
214
|
+
* @param {string} browser - Browser name
|
|
215
|
+
* @returns {Promise<Object>} Response from server
|
|
216
|
+
*/
|
|
217
|
+
async addBrowser(runId, browser, nid = 0) {
|
|
218
|
+
try {
|
|
219
|
+
logger.info('Adding browser to run matrix', { runId, browser, nid });
|
|
220
|
+
|
|
221
|
+
const response = await this.http.post('/api/run-matrix/update', {
|
|
222
|
+
run_id: runId,
|
|
223
|
+
nid: nid,
|
|
224
|
+
operation: 'add_browser',
|
|
225
|
+
data: {
|
|
226
|
+
browser: normalizeBrowser(browser)
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
logger.info('Browser added successfully', { runId, browser });
|
|
231
|
+
return response;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.error('Failed to add browser', {
|
|
234
|
+
error: error.message,
|
|
235
|
+
runId,
|
|
236
|
+
browser
|
|
237
|
+
});
|
|
238
|
+
throw new Error(`Failed to add browser: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get run matrix data
|
|
244
|
+
* @param {string} runId - Run ID
|
|
245
|
+
* @returns {Promise<Object>} Run matrix data
|
|
246
|
+
*/
|
|
247
|
+
async get(runId) {
|
|
248
|
+
try {
|
|
249
|
+
const response = await this.http.get(`/api/result/${runId}`);
|
|
250
|
+
return response;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error('Failed to get run matrix', {
|
|
253
|
+
error: error.message,
|
|
254
|
+
runId
|
|
255
|
+
});
|
|
256
|
+
throw new Error(`Failed to get run matrix: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Validate create options
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
validateCreateOptions(options) {
|
|
265
|
+
if (!options) {
|
|
266
|
+
throw new Error('Options are required');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Note: scenarioId and testSetId are now optional
|
|
270
|
+
// If neither is provided, defaults to 0 for generic automation runs
|
|
271
|
+
if (!options.scenarioId && !options.testSetId) {
|
|
272
|
+
logger.info('No scenarioId or testSetId provided, using default (0) for generic automation run');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!options.environment) {
|
|
276
|
+
logger.warn('No environment specified, using "Local"');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!options.browsers || options.browsers.length === 0) {
|
|
280
|
+
logger.warn('No browsers specified, using ["Chrome"]');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Auto-detect device type
|
|
286
|
+
* @private
|
|
287
|
+
* @returns {string} Normalized device type
|
|
288
|
+
*/
|
|
289
|
+
detectDevice() {
|
|
290
|
+
return normalizeDevice('Desktop');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Auto-detect operating system
|
|
295
|
+
* @private
|
|
296
|
+
* @returns {string} Normalized OS name
|
|
297
|
+
*/
|
|
298
|
+
detectOS() {
|
|
299
|
+
const platform = process.platform;
|
|
300
|
+
const osMap = {
|
|
301
|
+
'win32': 'Windows',
|
|
302
|
+
'darwin': 'macOS',
|
|
303
|
+
'linux': 'Linux'
|
|
304
|
+
};
|
|
305
|
+
return normalizeOS(osMap[platform] || 'Unknown');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = RunMatrixService;
|