@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,361 @@
1
+ const AppliqationClient = require('../../AppliqationClient');
2
+ const UuidExtractor = require('./UuidExtractor');
3
+ const logger = require('../../utils/logger');
4
+
5
+ /**
6
+ * Appliqation Reporter for Jest
7
+ *
8
+ * Custom Jest reporter that automatically reports test results
9
+ * to the Appliqation platform.
10
+ *
11
+ * @example
12
+ * // jest.config.js
13
+ * const { JestReporter } = require('@appliqation/automation-sdk/jest');
14
+ *
15
+ * module.exports = {
16
+ * reporters: [
17
+ * 'default',
18
+ * [JestReporter, {
19
+ * baseUrl: process.env.APPLIQATION_BASE_URL,
20
+ * apiKey: process.env.APPLIQATION_API_KEY,
21
+ * projectKey: process.env.APPLIQATION_PROJECT_KEY,
22
+ * scenarioId: parseInt(process.env.APPLIQATION_SCENARIO_ID),
23
+ * environment: process.env.APPLIQATION_ENVIRONMENT || 'Local',
24
+ * title: process.env.APPLIQATION_RUN_TITLE
25
+ * }]
26
+ * ]
27
+ * };
28
+ */
29
+ class JestReporter {
30
+ constructor(globalConfig, options) {
31
+ this.globalConfig = globalConfig;
32
+ this.config = {
33
+ baseUrl: options.baseUrl,
34
+ apiKey: options.apiKey,
35
+ projectKey: options.projectKey,
36
+ scenarioId: options.scenarioId,
37
+ testSetId: options.testSetId,
38
+ environment: options.environment || 'Local',
39
+ title: options.title || process.env.APPLIQATION_RUN_TITLE,
40
+ autoCreateRun: options.autoCreateRun !== false,
41
+ logLevel: options.logLevel || 'info',
42
+ submitOrphans: options.submitOrphans !== false
43
+ };
44
+
45
+ // Validate required config
46
+ this.validateConfig();
47
+
48
+ // Set logger level
49
+ logger.setLevel(this.config.logLevel);
50
+
51
+ // Initialize Appliqation client
52
+ this.client = new AppliqationClient({
53
+ baseUrl: this.config.baseUrl,
54
+ apiKey: this.config.apiKey,
55
+ projectKey: this.config.projectKey,
56
+ title: this.config.title,
57
+ options: {
58
+ logLevel: this.config.logLevel
59
+ }
60
+ });
61
+
62
+ // Test result storage
63
+ this.testResults = [];
64
+ this.orphanTests = [];
65
+ this.runId = null;
66
+ this.runCreated = false;
67
+
68
+ logger.info('Appliqation Jest Reporter initialized', {
69
+ environment: this.config.environment,
70
+ scenarioId: this.config.scenarioId,
71
+ autoCreateRun: this.config.autoCreateRun
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Called once at the start of the test run
77
+ */
78
+ async onRunStart(results, options) {
79
+ try {
80
+ logger.info('Jest test run starting...', {
81
+ numTotalTestSuites: results.numTotalTestSuites
82
+ });
83
+
84
+ if (this.config.autoCreateRun) {
85
+ await this.createRun();
86
+ }
87
+ } catch (error) {
88
+ logger.error('Error in onRunStart', { error: error.message });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Create Appliqation run matrix
94
+ */
95
+ async createRun() {
96
+ try {
97
+ const os = this.detectOS();
98
+
99
+ const runOptions = {
100
+ scenarioId: this.config.scenarioId,
101
+ testSetId: this.config.testSetId,
102
+ environment: this.config.environment,
103
+ browsers: ['Node.js'],
104
+ device: 'Server',
105
+ os: os,
106
+ title: this.config.title || `Automation Run - ${new Date().toISOString()}`
107
+ };
108
+
109
+ logger.info('Creating run matrix...', runOptions);
110
+
111
+ const run = await this.client.createRun(runOptions);
112
+ this.runId = run.runId;
113
+ this.runCreated = true;
114
+
115
+ logger.info('Run matrix created successfully', {
116
+ runId: this.runId
117
+ });
118
+ } catch (error) {
119
+ logger.error('Failed to create run matrix', { error: error.message });
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Called after each test file completes
126
+ */
127
+ async onTestFileResult(test, testResult, results) {
128
+ try {
129
+ logger.debug('Processing test file results', {
130
+ testFilePath: testResult.testFilePath,
131
+ numTests: testResult.testResults.length
132
+ });
133
+
134
+ // Process all tests in this file
135
+ for (const result of testResult.testResults) {
136
+ await this.processTestResult(result, testResult);
137
+ }
138
+ } catch (error) {
139
+ logger.error('Error processing test file results', {
140
+ error: error.message,
141
+ file: testResult.testFilePath
142
+ });
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Process individual test result
148
+ */
149
+ async processTestResult(testResult, fileResult) {
150
+ try {
151
+ // Extract UUID from test
152
+ const uuid = UuidExtractor.extractUuid(testResult);
153
+
154
+ if (!uuid) {
155
+ // Track orphan test
156
+ this.orphanTests.push({
157
+ title: testResult.fullName || testResult.title,
158
+ status: this.mapJestStatus(testResult.status),
159
+ file: fileResult.testFilePath,
160
+ timestamp: new Date().toISOString()
161
+ });
162
+
163
+ logger.warn('No UUID found for test', {
164
+ title: testResult.fullName || testResult.title
165
+ });
166
+ return;
167
+ }
168
+
169
+ // Prepare result
170
+ const result = {
171
+ uuid: uuid,
172
+ runId: this.runId,
173
+ status: this.mapJestStatus(testResult.status),
174
+ browser: 'Jest',
175
+ environment: this.config.environment,
176
+ comment: this.buildComment(testResult),
177
+ parent_uuid: null // Jest doesn't have nested tests in the same way
178
+ };
179
+
180
+ this.testResults.push(result);
181
+
182
+ logger.debug('Test result prepared', {
183
+ uuid: uuid,
184
+ status: result.status,
185
+ title: testResult.fullName
186
+ });
187
+ } catch (error) {
188
+ logger.error('Error processing test result', {
189
+ title: testResult.fullName,
190
+ error: error.message
191
+ });
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Called after all tests complete
197
+ */
198
+ async onRunComplete(contexts, results) {
199
+ try {
200
+ logger.info('Jest test run completed', {
201
+ tests: this.testResults.length,
202
+ orphans: this.orphanTests.length,
203
+ numTotalTests: results.numTotalTests,
204
+ numPassedTests: results.numPassedTests,
205
+ numFailedTests: results.numFailedTests
206
+ });
207
+
208
+ // Submit all test results
209
+ if (this.testResults.length > 0 && this.runId) {
210
+ await this.submitResults();
211
+ }
212
+
213
+ // Submit orphan tests
214
+ if (this.orphanTests.length > 0 && this.config.submitOrphans && this.runId) {
215
+ await this.submitOrphanTests();
216
+ }
217
+
218
+ // Print summary
219
+ this.printSummary(results);
220
+ } catch (error) {
221
+ logger.error('Error in onRunComplete', { error: error.message });
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Submit test results to Appliqation
227
+ */
228
+ async submitResults() {
229
+ try {
230
+ logger.info(`Submitting ${this.testResults.length} test results...`);
231
+
232
+ const summary = await this.client.submitBatch(this.testResults, {
233
+ batchSize: 50,
234
+ retryFailures: true
235
+ });
236
+
237
+ logger.info('Results submitted successfully', {
238
+ success: summary.success,
239
+ failed: summary.failed,
240
+ total: summary.total
241
+ });
242
+
243
+ return summary;
244
+ } catch (error) {
245
+ logger.error('Failed to submit results', { error: error.message });
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Submit orphan tests
252
+ */
253
+ async submitOrphanTests() {
254
+ try {
255
+ logger.info(`Logging ${this.orphanTests.length} orphan tests...`);
256
+
257
+ await this.client.logOrphanTests(this.runId, this.orphanTests);
258
+
259
+ logger.info('Orphan tests logged successfully');
260
+ } catch (error) {
261
+ logger.error('Failed to log orphan tests', { error: error.message });
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Map Jest test status to Appliqation status
267
+ */
268
+ mapJestStatus(jestStatus) {
269
+ const statusMap = {
270
+ 'passed': 'passed',
271
+ 'failed': 'failed',
272
+ 'skipped': 'skipped',
273
+ 'pending': 'skipped',
274
+ 'todo': 'skipped',
275
+ 'disabled': 'skipped'
276
+ };
277
+
278
+ return statusMap[jestStatus] || 'skipped';
279
+ }
280
+
281
+ /**
282
+ * Build comment from test details
283
+ */
284
+ buildComment(testResult) {
285
+ const comments = [];
286
+
287
+ if (testResult.failureMessages && testResult.failureMessages.length > 0) {
288
+ // Get first failure message (Jest can have multiple)
289
+ const errorMsg = testResult.failureMessages[0];
290
+ // Truncate to first 200 chars to avoid too long comments
291
+ const truncated = errorMsg.substring(0, 200);
292
+ comments.push(`Error: ${truncated}${errorMsg.length > 200 ? '...' : ''}`);
293
+ }
294
+
295
+ if (testResult.duration) {
296
+ comments.push(`Duration: ${testResult.duration}ms`);
297
+ }
298
+
299
+ if (testResult.numPassingAsserts) {
300
+ comments.push(`Assertions: ${testResult.numPassingAsserts}`);
301
+ }
302
+
303
+ return comments.length > 0 ? comments.join(' | ') : null;
304
+ }
305
+
306
+ /**
307
+ * Detect OS
308
+ */
309
+ detectOS() {
310
+ const platform = process.platform;
311
+
312
+ if (platform === 'darwin') return 'macOS';
313
+ if (platform === 'win32') return 'Windows';
314
+ if (platform === 'linux') return 'Linux';
315
+
316
+ return platform;
317
+ }
318
+
319
+ /**
320
+ * Print test summary
321
+ */
322
+ printSummary(results) {
323
+ console.log('\n' + '='.repeat(60));
324
+ console.log('📊 Appliqation Jest Reporter Summary');
325
+ console.log('='.repeat(60));
326
+ console.log(`Environment: ${this.config.environment}`);
327
+ console.log(`Run ID: ${this.runId || 'N/A'}`);
328
+ console.log(`Tests Submitted: ${this.testResults.length}`);
329
+ console.log(`Orphan Tests: ${this.orphanTests.length}`);
330
+ console.log(`Total Tests: ${results.numTotalTests}`);
331
+ console.log(`Passed: ${results.numPassedTests}`);
332
+ console.log(`Failed: ${results.numFailedTests}`);
333
+ console.log(`Skipped: ${results.numPendingTests}`);
334
+ console.log(`Duration: ${(results.startTime && ((Date.now() - results.startTime) / 1000).toFixed(2))}s`);
335
+ console.log('='.repeat(60) + '\n');
336
+ }
337
+
338
+ /**
339
+ * Validate configuration
340
+ */
341
+ validateConfig() {
342
+ const required = ['baseUrl', 'apiKey', 'projectKey'];
343
+ const missing = required.filter(key => !this.config[key]);
344
+
345
+ if (missing.length > 0) {
346
+ throw new Error(`Missing required config: ${missing.join(', ')}`);
347
+ }
348
+
349
+ // Note: scenarioId and testSetId are now optional
350
+ // If neither is provided, will default to 0 for generic automation runs
351
+ }
352
+
353
+ /**
354
+ * Get last error (Jest API requirement)
355
+ */
356
+ getLastError() {
357
+ return this.lastError;
358
+ }
359
+ }
360
+
361
+ module.exports = JestReporter;
@@ -0,0 +1,174 @@
1
+ const UuidValidator = require('../../utils/UuidValidator');
2
+ const logger = require('../../utils/logger');
3
+
4
+ /**
5
+ * UUID Extractor for Jest Tests
6
+ *
7
+ * Extracts Appliqation test case UUIDs from Jest tests.
8
+ * Supports multiple UUID assignment methods.
9
+ */
10
+ class UuidExtractor {
11
+ /**
12
+ * Extract UUID from Jest test result
13
+ *
14
+ * Supports multiple UUID formats:
15
+ * 1. Test title with UUID prefix: test('1154-... - should login', () => {})
16
+ * 2. Docblock with @uuid tag: /** @uuid 1154-... *\/
17
+ * 3. Custom test metadata (if available)
18
+ *
19
+ * @param {Object} testResult - Jest test result object
20
+ * @returns {string|null} - Extracted UUID or null
21
+ */
22
+ static extractUuid(testResult) {
23
+ try {
24
+ // Method 1: Extract from test title/fullName
25
+ const fullName = testResult.fullName || testResult.title;
26
+ if (fullName) {
27
+ const uuidFromTitle = this.extractUuidFromString(fullName);
28
+ if (uuidFromTitle) {
29
+ logger.debug('UUID extracted from test title', { uuid: uuidFromTitle });
30
+ return uuidFromTitle;
31
+ }
32
+ }
33
+
34
+ // Method 2: Extract from ancestorTitles + title
35
+ if (testResult.ancestorTitles && testResult.title) {
36
+ const combinedTitle = [...testResult.ancestorTitles, testResult.title].join(' ');
37
+ const uuidFromCombined = this.extractUuidFromString(combinedTitle);
38
+ if (uuidFromCombined) {
39
+ logger.debug('UUID extracted from combined titles', { uuid: uuidFromCombined });
40
+ return uuidFromCombined;
41
+ }
42
+ }
43
+
44
+ // Method 3: Check each ancestor title individually
45
+ if (testResult.ancestorTitles) {
46
+ for (const ancestorTitle of testResult.ancestorTitles) {
47
+ const uuidFromAncestor = this.extractUuidFromString(ancestorTitle);
48
+ if (uuidFromAncestor) {
49
+ logger.debug('UUID extracted from ancestor title', { uuid: uuidFromAncestor });
50
+ return uuidFromAncestor;
51
+ }
52
+ }
53
+ }
54
+
55
+ // Method 4: Check test title alone
56
+ if (testResult.title) {
57
+ const uuidFromTestTitle = this.extractUuidFromString(testResult.title);
58
+ if (uuidFromTestTitle) {
59
+ logger.debug('UUID extracted from test title alone', { uuid: uuidFromTestTitle });
60
+ return uuidFromTestTitle;
61
+ }
62
+ }
63
+
64
+ // No UUID found
65
+ return null;
66
+ } catch (error) {
67
+ logger.error('Error extracting UUID from Jest test', {
68
+ error: error.message,
69
+ test: testResult.title
70
+ });
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Extract UUID from string using regex pattern
77
+ *
78
+ * @param {string} str - String to search
79
+ * @returns {string|null} - Extracted UUID or null
80
+ */
81
+ static extractUuidFromString(str) {
82
+ if (!str || typeof str !== 'string') {
83
+ return null;
84
+ }
85
+
86
+ // Pattern: nid-uuid format (e.g., "1154-7a17b809-0ff9-4ba1-9322-4eb2a49abfc5")
87
+ const uuidPattern = /(\d+)-([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i;
88
+ const match = str.match(uuidPattern);
89
+
90
+ if (match && match[0]) {
91
+ const uuid = match[0];
92
+ if (UuidValidator.validate(uuid)) {
93
+ return uuid;
94
+ }
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Extract UUID from Jest docblock comment
102
+ *
103
+ * Example: /** @uuid 1154-7a17b809-0ff9-4ba1-9322-4eb2a49abfc5 *\/
104
+ *
105
+ * @param {string} docblock - Docblock comment string
106
+ * @returns {string|null} - Extracted UUID or null
107
+ */
108
+ static extractUuidFromDocblock(docblock) {
109
+ if (!docblock || typeof docblock !== 'string') {
110
+ return null;
111
+ }
112
+
113
+ // Pattern: @uuid tag in docblock
114
+ const docblockPattern = /@uuid\s+(\d+-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i;
115
+ const match = docblock.match(docblockPattern);
116
+
117
+ if (match && match[1]) {
118
+ const uuid = match[1];
119
+ if (UuidValidator.validate(uuid)) {
120
+ return uuid;
121
+ }
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Extract all UUIDs from a collection of tests
129
+ *
130
+ * @param {Array} testResults - Array of Jest test result objects
131
+ * @returns {Object} - { uuids: [...], orphans: [...] }
132
+ */
133
+ static extractFromTests(testResults) {
134
+ const results = {
135
+ uuids: [],
136
+ orphans: []
137
+ };
138
+
139
+ if (!Array.isArray(testResults)) {
140
+ return results;
141
+ }
142
+
143
+ for (const testResult of testResults) {
144
+ const uuid = this.extractUuid(testResult);
145
+
146
+ if (uuid) {
147
+ results.uuids.push({
148
+ uuid: uuid,
149
+ test: testResult
150
+ });
151
+ } else {
152
+ results.orphans.push({
153
+ title: testResult.fullName || testResult.title,
154
+ test: testResult
155
+ });
156
+ }
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * Validate if test has a valid UUID
164
+ *
165
+ * @param {Object} testResult - Jest test result object
166
+ * @returns {boolean} - True if test has valid UUID
167
+ */
168
+ static hasValidUuid(testResult) {
169
+ const uuid = this.extractUuid(testResult);
170
+ return uuid !== null;
171
+ }
172
+ }
173
+
174
+ module.exports = UuidExtractor;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Appliqation Jest Reporter
3
+ *
4
+ * @example
5
+ * // jest.config.js
6
+ * const { JestReporter } = require('@appliqation/automation-sdk/jest');
7
+ *
8
+ * module.exports = {
9
+ * reporters: [
10
+ * 'default',
11
+ * [JestReporter, {
12
+ * baseUrl: process.env.APPLIQATION_BASE_URL,
13
+ * apiKey: process.env.APPLIQATION_API_KEY,
14
+ * projectKey: process.env.APPLIQATION_PROJECT_KEY,
15
+ * scenarioId: parseInt(process.env.APPLIQATION_SCENARIO_ID),
16
+ * environment: process.env.APPLIQATION_ENVIRONMENT || 'Local',
17
+ * title: process.env.APPLIQATION_RUN_TITLE
18
+ * }]
19
+ * ]
20
+ * };
21
+ */
22
+
23
+ const JestReporter = require('./JestReporter');
24
+ const UuidExtractor = require('./UuidExtractor');
25
+
26
+ module.exports = JestReporter;
27
+ module.exports.JestReporter = JestReporter;
28
+ module.exports.UuidExtractor = UuidExtractor;