@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,387 @@
1
+ const AppliqationClient = require('../../AppliqationClient');
2
+ const UuidExtractor = require('./UuidExtractor');
3
+ const logger = require('../../utils/logger');
4
+
5
+ /**
6
+ * Appliqation Reporter for Cypress
7
+ *
8
+ * Integrates with Cypress via setupNodeEvents hooks to automatically
9
+ * report test results to Appliqation platform.
10
+ *
11
+ * @example
12
+ * // cypress.config.js
13
+ * const { CypressReporter } = require('@appliqation/automation-sdk/cypress');
14
+ *
15
+ * module.exports = defineConfig({
16
+ * e2e: {
17
+ * setupNodeEvents(on, config) {
18
+ * CypressReporter(on, {
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
+ */
30
+ class CypressReporter {
31
+ constructor(config) {
32
+ this.config = {
33
+ baseUrl: config.baseUrl,
34
+ apiKey: config.apiKey,
35
+ projectKey: config.projectKey,
36
+ scenarioId: config.scenarioId,
37
+ testSetId: config.testSetId,
38
+ environment: config.environment || 'Local',
39
+ title: config.title || process.env.APPLIQATION_RUN_TITLE,
40
+ autoCreateRun: config.autoCreateRun !== false,
41
+ logLevel: config.logLevel || 'info',
42
+ submitOrphans: config.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 Cypress Reporter initialized', {
69
+ environment: this.config.environment,
70
+ scenarioId: this.config.scenarioId,
71
+ autoCreateRun: this.config.autoCreateRun
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Register Cypress event handlers
77
+ * @param {Object} on - Cypress event registration function
78
+ */
79
+ register(on) {
80
+ // Event: Before all specs run
81
+ on('before:run', async (details) => {
82
+ await this.onBeforeRun(details);
83
+ });
84
+
85
+ // Event: After each spec file completes
86
+ on('after:spec', async (spec, results) => {
87
+ await this.onAfterSpec(spec, results);
88
+ });
89
+
90
+ // Event: After all specs complete
91
+ on('after:run', async (results) => {
92
+ await this.onAfterRun(results);
93
+ });
94
+
95
+ logger.debug('Cypress event handlers registered');
96
+ }
97
+
98
+ /**
99
+ * Called before test run starts
100
+ */
101
+ async onBeforeRun(details) {
102
+ try {
103
+ logger.info('Cypress test run starting...', {
104
+ browser: details.browser?.displayName,
105
+ specs: details.specs?.length
106
+ });
107
+
108
+ if (this.config.autoCreateRun) {
109
+ await this.createRun(details);
110
+ }
111
+ } catch (error) {
112
+ logger.error('Error in before:run hook', { error: error.message });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Create Appliqation run matrix
118
+ */
119
+ async createRun(details) {
120
+ try {
121
+ const browser = details.browser?.displayName || details.browser?.name || 'Cypress';
122
+ const platform = details.system?.platform || process.platform;
123
+ const os = this.detectOS(platform);
124
+
125
+ const runOptions = {
126
+ scenarioId: this.config.scenarioId,
127
+ testSetId: this.config.testSetId,
128
+ environment: this.config.environment,
129
+ browsers: [browser],
130
+ device: 'Desktop',
131
+ os: os,
132
+ title: this.config.title || `Automation Run - ${new Date().toISOString()}`
133
+ };
134
+
135
+ logger.info('Creating run matrix...', runOptions);
136
+
137
+ const run = await this.client.createRun(runOptions);
138
+ this.runId = run.runId;
139
+ this.runCreated = true;
140
+
141
+ logger.info('Run matrix created successfully', {
142
+ runId: this.runId,
143
+ browser: browser
144
+ });
145
+ } catch (error) {
146
+ logger.error('Failed to create run matrix', { error: error.message });
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Called after each spec file completes
153
+ */
154
+ async onAfterSpec(spec, results) {
155
+ try {
156
+ logger.debug('Processing spec results', {
157
+ spec: spec.relative,
158
+ tests: results.tests?.length
159
+ });
160
+
161
+ if (!results.tests || results.tests.length === 0) {
162
+ return;
163
+ }
164
+
165
+ // Extract results from all tests in this spec
166
+ for (const test of results.tests) {
167
+ await this.processTest(test, results.stats.startedAt);
168
+ }
169
+ } catch (error) {
170
+ logger.error('Error processing spec results', {
171
+ spec: spec.relative,
172
+ error: error.message
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Process individual test result
179
+ */
180
+ async processTest(test, startedAt) {
181
+ try {
182
+ // Extract UUID from test
183
+ const uuid = UuidExtractor.extractUuid(test);
184
+
185
+ if (!uuid) {
186
+ // Track orphan test
187
+ this.orphanTests.push({
188
+ title: test.title.join(' > '),
189
+ status: this.mapCypressStatus(test.state),
190
+ file: test.file || 'unknown',
191
+ timestamp: new Date().toISOString()
192
+ });
193
+
194
+ logger.warn('No UUID found for test', { title: test.title.join(' > ') });
195
+ return;
196
+ }
197
+
198
+ // Prepare result
199
+ const result = {
200
+ uuid: uuid,
201
+ runId: this.runId,
202
+ status: this.mapCypressStatus(test.state),
203
+ browser: 'Cypress',
204
+ environment: this.config.environment,
205
+ comment: this.buildComment(test),
206
+ parent_uuid: null // Cypress doesn't have nested tests like Playwright
207
+ };
208
+
209
+ this.testResults.push(result);
210
+
211
+ logger.debug('Test result prepared', {
212
+ uuid: uuid,
213
+ status: result.status,
214
+ title: test.title.join(' > ')
215
+ });
216
+ } catch (error) {
217
+ logger.error('Error processing test', {
218
+ title: test.title,
219
+ error: error.message
220
+ });
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Called after all tests complete
226
+ */
227
+ async onAfterRun(results) {
228
+ try {
229
+ logger.info('Cypress test run completed', {
230
+ tests: this.testResults.length,
231
+ orphans: this.orphanTests.length
232
+ });
233
+
234
+ // Submit all test results
235
+ if (this.testResults.length > 0 && this.runId) {
236
+ await this.submitResults();
237
+ }
238
+
239
+ // Submit orphan tests
240
+ if (this.orphanTests.length > 0 && this.config.submitOrphans && this.runId) {
241
+ await this.submitOrphanTests();
242
+ }
243
+
244
+ // Print summary
245
+ this.printSummary(results);
246
+ } catch (error) {
247
+ logger.error('Error in after:run hook', { error: error.message });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Submit test results to Appliqation
253
+ */
254
+ async submitResults() {
255
+ try {
256
+ logger.info(`Submitting ${this.testResults.length} test results...`);
257
+
258
+ const summary = await this.client.submitBatch(this.testResults, {
259
+ batchSize: 50,
260
+ retryFailures: true
261
+ });
262
+
263
+ logger.info('Results submitted successfully', {
264
+ success: summary.success,
265
+ failed: summary.failed,
266
+ total: summary.total
267
+ });
268
+
269
+ return summary;
270
+ } catch (error) {
271
+ logger.error('Failed to submit results', { error: error.message });
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Submit orphan tests
278
+ */
279
+ async submitOrphanTests() {
280
+ try {
281
+ logger.info(`Logging ${this.orphanTests.length} orphan tests...`);
282
+
283
+ await this.client.logOrphanTests(this.runId, this.orphanTests);
284
+
285
+ logger.info('Orphan tests logged successfully');
286
+ } catch (error) {
287
+ logger.error('Failed to log orphan tests', { error: error.message });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Map Cypress test status to Appliqation status
293
+ */
294
+ mapCypressStatus(cypressStatus) {
295
+ const statusMap = {
296
+ 'passed': 'passed',
297
+ 'failed': 'failed',
298
+ 'pending': 'skipped',
299
+ 'skipped': 'skipped'
300
+ };
301
+
302
+ return statusMap[cypressStatus] || 'skipped';
303
+ }
304
+
305
+ /**
306
+ * Build comment from test details
307
+ */
308
+ buildComment(test) {
309
+ const comments = [];
310
+
311
+ if (test.displayError) {
312
+ comments.push(`Error: ${test.displayError}`);
313
+ }
314
+
315
+ if (test.duration) {
316
+ comments.push(`Duration: ${test.duration}ms`);
317
+ }
318
+
319
+ if (test.attempts && test.attempts.length > 1) {
320
+ comments.push(`Attempts: ${test.attempts.length}`);
321
+ }
322
+
323
+ return comments.length > 0 ? comments.join(' | ') : null;
324
+ }
325
+
326
+ /**
327
+ * Detect OS from platform
328
+ */
329
+ detectOS(platform) {
330
+ if (!platform) return 'Unknown';
331
+
332
+ const platformLower = platform.toLowerCase();
333
+
334
+ if (platformLower.includes('darwin')) return 'macOS';
335
+ if (platformLower.includes('win')) return 'Windows';
336
+ if (platformLower.includes('linux')) return 'Linux';
337
+
338
+ return platform;
339
+ }
340
+
341
+ /**
342
+ * Print test summary
343
+ */
344
+ printSummary(results) {
345
+ console.log('\n' + '='.repeat(60));
346
+ console.log('📊 Appliqation Cypress Reporter Summary');
347
+ console.log('='.repeat(60));
348
+ console.log(`Environment: ${this.config.environment}`);
349
+ console.log(`Run ID: ${this.runId || 'N/A'}`);
350
+ console.log(`Tests Submitted: ${this.testResults.length}`);
351
+ console.log(`Orphan Tests: ${this.orphanTests.length}`);
352
+ console.log(`Total Tests: ${results.totalTests || 0}`);
353
+ console.log(`Passed: ${results.totalPassed || 0}`);
354
+ console.log(`Failed: ${results.totalFailed || 0}`);
355
+ console.log(`Skipped: ${results.totalSkipped || 0}`);
356
+ console.log('='.repeat(60) + '\n');
357
+ }
358
+
359
+ /**
360
+ * Validate configuration
361
+ */
362
+ validateConfig() {
363
+ const required = ['baseUrl', 'apiKey', 'projectKey'];
364
+ const missing = required.filter(key => !this.config[key]);
365
+
366
+ if (missing.length > 0) {
367
+ throw new Error(`Missing required config: ${missing.join(', ')}`);
368
+ }
369
+
370
+ // Note: scenarioId and testSetId are now optional
371
+ // If neither is provided, will default to 0 for generic automation runs
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Factory function for Cypress setupNodeEvents
377
+ * @param {Object} on - Cypress event registration function
378
+ * @param {Object} config - Reporter configuration
379
+ */
380
+ function setupCypressReporter(on, config) {
381
+ const reporter = new CypressReporter(config);
382
+ reporter.register(on);
383
+ return reporter;
384
+ }
385
+
386
+ module.exports = setupCypressReporter;
387
+ module.exports.CypressReporter = CypressReporter;
@@ -0,0 +1,139 @@
1
+ const UuidValidator = require('../../utils/UuidValidator');
2
+ const logger = require('../../utils/logger');
3
+
4
+ /**
5
+ * UUID Extractor for Cypress Tests
6
+ *
7
+ * Extracts Appliqation test case UUIDs from Cypress tests.
8
+ * Supports multiple UUID assignment methods.
9
+ */
10
+ class UuidExtractor {
11
+ /**
12
+ * Extract UUID from Cypress test
13
+ *
14
+ * Supports multiple UUID formats:
15
+ * 1. Test config property: it('test', { uuid: '1154-...' }, () => {})
16
+ * 2. Test title with UUID: it('1154-... - should login', () => {})
17
+ * 3. Custom metadata (if available in future Cypress versions)
18
+ *
19
+ * @param {Object} test - Cypress test object
20
+ * @returns {string|null} - Extracted UUID or null
21
+ */
22
+ static extractUuid(test) {
23
+ try {
24
+ // Method 1: Check test config for uuid property
25
+ if (test.config && test.config.uuid) {
26
+ const uuid = test.config.uuid;
27
+ if (UuidValidator.validate(uuid)) {
28
+ logger.debug('UUID extracted from test config', { uuid });
29
+ return uuid;
30
+ }
31
+ }
32
+
33
+ // Method 2: Check test title for UUID
34
+ // Title can be array in Cypress (suite hierarchy)
35
+ const fullTitle = Array.isArray(test.title) ? test.title.join(' ') : test.title;
36
+
37
+ if (fullTitle) {
38
+ const uuidFromTitle = this.extractUuidFromString(fullTitle);
39
+ if (uuidFromTitle) {
40
+ logger.debug('UUID extracted from test title', { uuid: uuidFromTitle });
41
+ return uuidFromTitle;
42
+ }
43
+ }
44
+
45
+ // Method 3: Check individual title parts if it's an array
46
+ if (Array.isArray(test.title)) {
47
+ for (const titlePart of test.title) {
48
+ const uuidFromPart = this.extractUuidFromString(titlePart);
49
+ if (uuidFromPart) {
50
+ logger.debug('UUID extracted from title part', { uuid: uuidFromPart });
51
+ return uuidFromPart;
52
+ }
53
+ }
54
+ }
55
+
56
+ // No UUID found
57
+ return null;
58
+ } catch (error) {
59
+ logger.error('Error extracting UUID from Cypress test', {
60
+ error: error.message,
61
+ test: test.title
62
+ });
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Extract UUID from string using regex pattern
69
+ *
70
+ * @param {string} str - String to search
71
+ * @returns {string|null} - Extracted UUID or null
72
+ */
73
+ static extractUuidFromString(str) {
74
+ if (!str || typeof str !== 'string') {
75
+ return null;
76
+ }
77
+
78
+ // Pattern: nid-uuid format (e.g., "1154-7a17b809-0ff9-4ba1-9322-4eb2a49abfc5")
79
+ const uuidPattern = /(\d+)-([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i;
80
+ const match = str.match(uuidPattern);
81
+
82
+ if (match && match[0]) {
83
+ const uuid = match[0];
84
+ if (UuidValidator.validate(uuid)) {
85
+ return uuid;
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Extract all UUIDs from a collection of tests
94
+ *
95
+ * @param {Array} tests - Array of Cypress test objects
96
+ * @returns {Object} - { uuids: [...], orphans: [...] }
97
+ */
98
+ static extractFromTests(tests) {
99
+ const results = {
100
+ uuids: [],
101
+ orphans: []
102
+ };
103
+
104
+ if (!Array.isArray(tests)) {
105
+ return results;
106
+ }
107
+
108
+ for (const test of tests) {
109
+ const uuid = this.extractUuid(test);
110
+
111
+ if (uuid) {
112
+ results.uuids.push({
113
+ uuid: uuid,
114
+ test: test
115
+ });
116
+ } else {
117
+ results.orphans.push({
118
+ title: Array.isArray(test.title) ? test.title.join(' > ') : test.title,
119
+ test: test
120
+ });
121
+ }
122
+ }
123
+
124
+ return results;
125
+ }
126
+
127
+ /**
128
+ * Validate if test has a valid UUID
129
+ *
130
+ * @param {Object} test - Cypress test object
131
+ * @returns {boolean} - True if test has valid UUID
132
+ */
133
+ static hasValidUuid(test) {
134
+ const uuid = this.extractUuid(test);
135
+ return uuid !== null;
136
+ }
137
+ }
138
+
139
+ module.exports = UuidExtractor;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Appliqation Cypress Reporter
3
+ *
4
+ * @example
5
+ * // cypress.config.js
6
+ * const { defineConfig } = require('cypress');
7
+ * const { CypressReporter } = require('@appliqation/automation-sdk/cypress');
8
+ *
9
+ * module.exports = defineConfig({
10
+ * e2e: {
11
+ * setupNodeEvents(on, config) {
12
+ * CypressReporter(on, {
13
+ * baseUrl: process.env.APPLIQATION_BASE_URL,
14
+ * apiKey: process.env.APPLIQATION_API_KEY,
15
+ * projectKey: process.env.APPLIQATION_PROJECT_KEY,
16
+ * scenarioId: parseInt(process.env.APPLIQATION_SCENARIO_ID),
17
+ * environment: process.env.APPLIQATION_ENVIRONMENT || 'Local',
18
+ * title: process.env.APPLIQATION_RUN_TITLE
19
+ * });
20
+ * }
21
+ * }
22
+ * });
23
+ */
24
+
25
+ const CypressReporter = require('./CypressReporter');
26
+ const UuidExtractor = require('./UuidExtractor');
27
+
28
+ module.exports = CypressReporter;
29
+ module.exports.CypressReporter = CypressReporter;
30
+ module.exports.UuidExtractor = UuidExtractor;