@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/package.json +107 -0
  4. package/src/AppliqationClient.js +562 -0
  5. package/src/constants.js +245 -0
  6. package/src/core/AuthManager.js +353 -0
  7. package/src/core/HttpClient.js +475 -0
  8. package/src/index.d.ts +333 -0
  9. package/src/index.js +26 -0
  10. package/src/playwright/JwtBrowserAuth.js +240 -0
  11. package/src/playwright/fixture.js +92 -0
  12. package/src/playwright/global-setup.js +243 -0
  13. package/src/playwright/helpers/jwt-browser-auth.js +227 -0
  14. package/src/playwright/index.js +16 -0
  15. package/src/reporters/cypress/CypressReporter.js +387 -0
  16. package/src/reporters/cypress/UuidExtractor.js +139 -0
  17. package/src/reporters/cypress/index.js +30 -0
  18. package/src/reporters/jest/JestReporter.js +361 -0
  19. package/src/reporters/jest/UuidExtractor.js +174 -0
  20. package/src/reporters/jest/index.js +28 -0
  21. package/src/reporters/playwright/AppliqationReporter.js +654 -0
  22. package/src/reporters/playwright/helpers/DeviceOsDetector.js +435 -0
  23. package/src/reporters/playwright/helpers/UuidExtractor.js +290 -0
  24. package/src/reporters/playwright/index.d.ts +96 -0
  25. package/src/reporters/playwright/index.js +14 -0
  26. package/src/services/OrphanTestService.js +74 -0
  27. package/src/services/ResultService.js +252 -0
  28. package/src/services/RunMatrixService.js +309 -0
  29. package/src/utils/PayloadBuilder.js +280 -0
  30. package/src/utils/RunDataNormalizer.js +335 -0
  31. package/src/utils/UuidValidator.js +102 -0
  32. package/src/utils/errors.js +217 -0
  33. package/src/utils/index.js +17 -0
  34. package/src/utils/logger.js +124 -0
  35. package/src/utils/mapAppqUuid.js +83 -0
  36. 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;