@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,280 @@
1
+ const { normalizeBrowser, normalizeOS, normalizeDevice } = require('./RunDataNormalizer');
2
+
3
+ /**
4
+ * Payload Builder for Appliqation API requests
5
+ * Ensures consistent payload structure and field mapping
6
+ */
7
+ class PayloadBuilder {
8
+ /**
9
+ * Build run creation payload
10
+ * @param {Object} config - SDK configuration
11
+ * @param {Object} options - Run options
12
+ * @returns {Object} API payload
13
+ */
14
+ static buildRunPayload(config, options) {
15
+ // Normalize browsers array
16
+ const browsers = options.browsers || ['Chrome'];
17
+ const normalizedBrowsers = browsers.map(browser => normalizeBrowser(browser));
18
+
19
+ const payload = {
20
+ project_key: Buffer.from(config.projectKey).toString('base64'),
21
+ environment: options.environment || 'Local',
22
+ browsers: normalizedBrowsers,
23
+ device: normalizeDevice(options.device || 'Desktop'),
24
+ os: normalizeOS(options.os || this.detectOS()),
25
+ title: options.title || `Automated Run - ${new Date().toISOString()}`
26
+ };
27
+
28
+ // Add either scenario_id or testset_id
29
+ if (options.scenarioId) {
30
+ payload.scenario_id = parseInt(options.scenarioId);
31
+ payload.type = options.type || 'scenario';
32
+ } else if (options.testSetId) {
33
+ payload.testset_id = parseInt(options.testSetId);
34
+ payload.type = 'testset';
35
+ }
36
+
37
+ return payload;
38
+ }
39
+
40
+ /**
41
+ * Build result submission payload
42
+ * @param {string} runId - Run ID
43
+ * @param {Object} result - Test result object
44
+ * @returns {Object} API payload
45
+ */
46
+ static buildResultPayload(runId, result) {
47
+ const browserName = result.browser || result.browserName || 'Unknown Browser';
48
+
49
+ return {
50
+ run_id: runId,
51
+ run_timestamp: result.runTimestamp || this.getCurrentTimestamp(),
52
+ uuid: result.uuid,
53
+ parent_uuid: result.parent_uuid || '',
54
+ nid: result.nid || this.extractNid(result.uuid),
55
+ browserName: normalizeBrowser(browserName),
56
+ status: this.normalizeStatus(result.status),
57
+ environment: result.environment || 'Local',
58
+ comment: result.comment || '',
59
+ timestamp: result.timestamp || this.getCurrentTimestamp()
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Build orphan test entry payload
65
+ * @param {Object} test - Test object
66
+ * @param {string} reason - Reason for being orphaned
67
+ * @returns {Object} Formatted orphan test entry
68
+ */
69
+ static buildOrphanEntry(test, reason = 'No UUID annotation found') {
70
+ const browser = test.browser || 'unknown';
71
+
72
+ return {
73
+ test_title: test.title || test.name || test.test_title || 'Unknown Test',
74
+ test_file: test.file || test.location?.file || test.test_file || 'unknown',
75
+ browser: normalizeBrowser(browser),
76
+ timestamp: test.timestamp || this.getCurrentTimestamp(),
77
+ reason: reason,
78
+ framework: test.framework || 'unknown'
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Build batch orphan tests payload
84
+ * @param {string} runId - Run ID
85
+ * @param {Array} orphanTests - Array of orphan test objects
86
+ * @returns {Object} API payload
87
+ */
88
+ static buildOrphanTestsPayload(runId, orphanTests) {
89
+ return {
90
+ run_id: runId,
91
+ orphan_tests: orphanTests.map(test => this.buildOrphanEntry(test))
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Build browser update payload
97
+ * @param {string} runId - Run ID
98
+ * @param {string} browser - Browser name
99
+ * @returns {Object} API payload
100
+ */
101
+ static buildBrowserUpdatePayload(runId, browser) {
102
+ return {
103
+ run_id: runId,
104
+ operation: 'add_browser',
105
+ browser: normalizeBrowser(browser)
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Normalize test status to Appliqation format
111
+ * @param {string} status - Test status
112
+ * @returns {string} Normalized status
113
+ */
114
+ static normalizeStatus(status) {
115
+ if (!status) return 'unknown';
116
+
117
+ const statusMap = {
118
+ // Standard statuses
119
+ 'passed': 'passed',
120
+ 'failed': 'failed',
121
+ 'skipped': 'skipped',
122
+ 'pending': 'pending',
123
+
124
+ // Playwright variants
125
+ 'expected': 'passed',
126
+ 'unexpected': 'failed',
127
+ 'flaky': 'flaky',
128
+ 'timedOut': 'failed',
129
+ 'interrupted': 'skipped',
130
+
131
+ // Cypress variants
132
+ 'pass': 'passed',
133
+ 'fail': 'failed',
134
+
135
+ // Jest variants
136
+ 'success': 'passed',
137
+ 'error': 'failed',
138
+ 'disabled': 'skipped',
139
+
140
+ // Generic variants
141
+ 'ok': 'passed',
142
+ 'success': 'passed',
143
+ 'failure': 'failed',
144
+ 'error': 'failed',
145
+ 'skip': 'skipped',
146
+ 'todo': 'pending'
147
+ };
148
+
149
+ const normalized = statusMap[status.toLowerCase()];
150
+ return normalized || status.toLowerCase();
151
+ }
152
+
153
+ /**
154
+ * Extract NID from UUID
155
+ * @param {string} uuid - UUID string
156
+ * @returns {number|null} NID or null if invalid
157
+ */
158
+ static extractNid(uuid) {
159
+ if (!uuid || typeof uuid !== 'string') return null;
160
+ const parts = uuid.split('-');
161
+ if (parts.length < 2) return null;
162
+ const nid = parseInt(parts[0], 10);
163
+ return isNaN(nid) ? null : nid;
164
+ }
165
+
166
+ /**
167
+ * Get current Unix timestamp in seconds
168
+ * @returns {number} Unix timestamp
169
+ */
170
+ static getCurrentTimestamp() {
171
+ return Math.floor(Date.now() / 1000);
172
+ }
173
+
174
+ /**
175
+ * Auto-detect operating system
176
+ * @returns {string} OS name (normalized)
177
+ */
178
+ static detectOS() {
179
+ const platform = process.platform;
180
+ const osMap = {
181
+ 'win32': 'Windows',
182
+ 'darwin': 'macOS',
183
+ 'linux': 'Linux'
184
+ };
185
+ // Return normalized value directly
186
+ return normalizeOS(osMap[platform] || 'Unknown');
187
+ }
188
+
189
+ /**
190
+ * Validate required result fields
191
+ * @param {Object} result - Test result object
192
+ * @returns {Object} { valid: boolean, errors: string[] }
193
+ */
194
+ static validateResult(result) {
195
+ const errors = [];
196
+
197
+ if (!result.uuid) {
198
+ errors.push('UUID is required');
199
+ }
200
+
201
+ if (!result.status) {
202
+ errors.push('Status is required');
203
+ }
204
+
205
+ if (!result.browser && !result.browserName) {
206
+ errors.push('Browser name is required');
207
+ }
208
+
209
+ return {
210
+ valid: errors.length === 0,
211
+ errors
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Validate run creation options
217
+ * @param {Object} options - Run options
218
+ * @returns {Object} { valid: boolean, errors: string[] }
219
+ */
220
+ static validateRunOptions(options) {
221
+ const errors = [];
222
+
223
+ if (!options.scenarioId && !options.testSetId) {
224
+ errors.push('Either scenarioId or testSetId is required');
225
+ }
226
+
227
+ if (options.scenarioId && options.testSetId) {
228
+ errors.push('Cannot specify both scenarioId and testSetId');
229
+ }
230
+
231
+ if (options.browsers && !Array.isArray(options.browsers)) {
232
+ errors.push('browsers must be an array');
233
+ }
234
+
235
+ if (options.browsers && options.browsers.length === 0) {
236
+ errors.push('browsers array cannot be empty');
237
+ }
238
+
239
+ return {
240
+ valid: errors.length === 0,
241
+ errors
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Sanitize string for API submission
247
+ * @param {string} str - String to sanitize
248
+ * @param {number} maxLength - Maximum length
249
+ * @returns {string} Sanitized string
250
+ */
251
+ static sanitizeString(str, maxLength = 1000) {
252
+ if (!str || typeof str !== 'string') return '';
253
+
254
+ // Remove control characters and trim
255
+ let sanitized = str.replace(/[\x00-\x1F\x7F]/g, '').trim();
256
+
257
+ // Truncate if needed
258
+ if (sanitized.length > maxLength) {
259
+ sanitized = sanitized.substring(0, maxLength) + '...';
260
+ }
261
+
262
+ return sanitized;
263
+ }
264
+
265
+ /**
266
+ * Format error message for API
267
+ * @param {Error} error - Error object
268
+ * @returns {string} Formatted error message
269
+ */
270
+ static formatErrorMessage(error) {
271
+ if (!error) return 'Unknown error';
272
+
273
+ const message = error.message || error.toString();
274
+ const stack = error.stack ? `\n${error.stack}` : '';
275
+
276
+ return this.sanitizeString(message + stack, 5000);
277
+ }
278
+ }
279
+
280
+ module.exports = PayloadBuilder;
@@ -0,0 +1,335 @@
1
+ /**
2
+ * RunDataNormalizer - Validates and normalizes Browser, OS, and Device data
3
+ *
4
+ * This module provides centralized validation and normalization for test run metadata.
5
+ * It ensures consistent formatting across all reporters and frameworks.
6
+ *
7
+ * @module RunDataNormalizer
8
+ * @example
9
+ * const { normalizeBrowser, normalizeOS, normalizeDevice } = require('./RunDataNormalizer');
10
+ *
11
+ * normalizeBrowser('chrome 141.0.7811.4'); // Returns: "Chrome 141"
12
+ * normalizeBrowser('Mozilla/5.0 ... Chrome/141.0.0.0'); // Returns: "Chrome 141"
13
+ * normalizeOS('Windows 11'); // Returns: "Windows"
14
+ * normalizeDevice('mobile'); // Returns: "Mobile"
15
+ */
16
+
17
+ /**
18
+ * Configuration object for supported browsers
19
+ * Add new browsers here to extend support
20
+ */
21
+ const BROWSER_CONFIG = {
22
+ chrome: { name: 'Chrome', aliases: ['chromium', 'google chrome', 'chrome headless'] },
23
+ firefox: { name: 'Firefox', aliases: ['mozilla firefox', 'ff', 'firefox headless'] },
24
+ safari: { name: 'Safari', aliases: ['webkit', 'safari technology preview'] },
25
+ edge: { name: 'Edge', aliases: ['microsoft edge', 'msedge', 'edge chromium'] },
26
+ opera: { name: 'Opera', aliases: ['opera gx', 'opera mini'] },
27
+ brave: { name: 'Brave', aliases: ['brave browser'] },
28
+ ie: { name: 'Internet Explorer', aliases: ['msie', 'trident', 'internet explorer'] },
29
+ node: { name: 'Node.js', aliases: ['nodejs', 'node'] },
30
+ electron: { name: 'Electron', aliases: [] }
31
+ };
32
+
33
+ /**
34
+ * Configuration object for supported operating systems
35
+ * Maps various OS variants to generic families
36
+ */
37
+ const OS_CONFIG = {
38
+ windows: {
39
+ name: 'Windows',
40
+ aliases: ['win', 'win32', 'win64', 'windows nt', 'windows 10', 'windows 11', 'windows 7', 'windows 8', 'windows server']
41
+ },
42
+ macos: {
43
+ name: 'macOS',
44
+ aliases: ['mac os', 'mac os x', 'osx', 'os x', 'darwin', 'mac', 'macosx', 'macos']
45
+ },
46
+ chromeos: {
47
+ name: 'ChromeOS',
48
+ aliases: ['chrome os', 'cros']
49
+ },
50
+ linux: {
51
+ name: 'Linux',
52
+ aliases: ['gnu/linux', 'linux gnu']
53
+ },
54
+ ubuntu: {
55
+ name: 'Ubuntu',
56
+ aliases: ['ubuntu linux']
57
+ },
58
+ unix: {
59
+ name: 'Unix',
60
+ aliases: []
61
+ },
62
+ android: {
63
+ name: 'Android',
64
+ aliases: []
65
+ },
66
+ ios: {
67
+ name: 'iOS',
68
+ aliases: ['iphone os', 'ipad os', 'ipados']
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Configuration object for supported device types
74
+ */
75
+ const DEVICE_CONFIG = {
76
+ desktop: { name: 'Desktop', aliases: ['pc', 'computer', 'laptop', 'workstation', 'server'] },
77
+ mobile: { name: 'Mobile', aliases: ['phone', 'smartphone', 'mobile device', 'cellphone'] },
78
+ tablet: { name: 'Tablet', aliases: ['ipad', 'tablet device'] }
79
+ };
80
+
81
+ /**
82
+ * Extracts major version from a version string or user agent
83
+ *
84
+ * @param {string} input - Version string or user agent
85
+ * @returns {string|null} - Major version number or null if not found
86
+ *
87
+ * @example
88
+ * extractMajorVersion('141.0.7811.4'); // Returns: "141"
89
+ * extractMajorVersion('Chrome/141.0.0.0'); // Returns: "141"
90
+ */
91
+ function extractMajorVersion(input) {
92
+ if (!input) return null;
93
+
94
+ // Match version patterns: 141.0.7811.4, v141.0, 141
95
+ const versionMatch = input.match(/(\d+)(?:\.\d+)*/);
96
+ return versionMatch ? versionMatch[1] : null;
97
+ }
98
+
99
+ /**
100
+ * Normalizes browser name and extracts major version
101
+ *
102
+ * Supports:
103
+ * - Simple names: "chrome", "Chrome", "CHROME"
104
+ * - With versions: "Chrome 141", "chrome 141.0.7811.4"
105
+ * - User agents: "Mozilla/5.0 ... Chrome/141.0.0.0 ..."
106
+ * - Playwright projects: "chromium", "firefox", "webkit"
107
+ *
108
+ * @param {string} input - Browser name, version, or user agent
109
+ * @returns {string} - Normalized format: "BrowserName MajorVersion" or "BrowserName" or "Unknown Browser"
110
+ *
111
+ * @example
112
+ * normalizeBrowser('chrome'); // Returns: "Chrome"
113
+ * normalizeBrowser('Chrome 141'); // Returns: "Chrome 141"
114
+ * normalizeBrowser('chrome 141.0.7811.4'); // Returns: "Chrome 141"
115
+ * normalizeBrowser('chromium'); // Returns: "Chrome"
116
+ * normalizeBrowser('Mozilla/5.0 ... Chrome/141.0.0.0'); // Returns: "Chrome 141"
117
+ * normalizeBrowser('unknown'); // Returns: "Unknown Browser"
118
+ */
119
+ function normalizeBrowser(input) {
120
+ if (!input || typeof input !== 'string') {
121
+ return 'Unknown Browser';
122
+ }
123
+
124
+ const inputLower = input.toLowerCase().trim();
125
+
126
+ // Try to match against known browsers
127
+ for (const [key, config] of Object.entries(BROWSER_CONFIG)) {
128
+ const allNames = [key, ...config.aliases];
129
+
130
+ for (const name of allNames) {
131
+ // Check if input contains the browser name
132
+ if (inputLower.includes(name)) {
133
+ // Try to extract version from various formats
134
+ let version = null;
135
+
136
+ // Pattern 1: "Chrome/141.0.0.0" (user agent)
137
+ const uaPattern = new RegExp(`${name.replace(/\s/g, '\\s*')}/([\\d.]+)`, 'i');
138
+ const uaMatch = input.match(uaPattern);
139
+ if (uaMatch) {
140
+ version = extractMajorVersion(uaMatch[1]);
141
+ }
142
+
143
+ // Pattern 2: "Chrome 141.0.7811.4" (space-separated)
144
+ if (!version) {
145
+ const spacePattern = new RegExp(`${name.replace(/\s/g, '\\s*')}\\s+([\\d.]+)`, 'i');
146
+ const spaceMatch = input.match(spacePattern);
147
+ if (spaceMatch) {
148
+ version = extractMajorVersion(spaceMatch[1]);
149
+ }
150
+ }
151
+
152
+ // Pattern 3: Just version numbers after browser name
153
+ if (!version) {
154
+ const afterBrowser = input.substring(input.toLowerCase().indexOf(name) + name.length);
155
+ version = extractMajorVersion(afterBrowser);
156
+ }
157
+
158
+ // Return normalized format
159
+ return version ? `${config.name} ${version}` : config.name;
160
+ }
161
+ }
162
+ }
163
+
164
+ // No match found
165
+ return 'Unknown Browser';
166
+ }
167
+
168
+ /**
169
+ * Normalizes operating system name to generic family
170
+ *
171
+ * Supports:
172
+ * - Generic names: "windows", "macos", "linux"
173
+ * - Specific versions: "Windows 11", "macOS Sonoma"
174
+ * - User agent formats: "Windows NT 10.0", "Mac OS X 10_15_7"
175
+ * - Platform values: "win32", "darwin", "linux"
176
+ *
177
+ * @param {string} input - OS name, version, or platform string
178
+ * @returns {string} - Generic OS family or "Unknown OS"
179
+ *
180
+ * @example
181
+ * normalizeOS('Windows 11'); // Returns: "Windows"
182
+ * normalizeOS('win32'); // Returns: "Windows"
183
+ * normalizeOS('macOS Sonoma'); // Returns: "macOS"
184
+ * normalizeOS('darwin'); // Returns: "macOS"
185
+ * normalizeOS('Ubuntu 22.04'); // Returns: "Ubuntu"
186
+ * normalizeOS('unknown'); // Returns: "Unknown OS"
187
+ */
188
+ function normalizeOS(input) {
189
+ if (!input || typeof input !== 'string') {
190
+ return 'Unknown OS';
191
+ }
192
+
193
+ const inputLower = input.toLowerCase().trim();
194
+
195
+ // Try exact match first
196
+ for (const [key, config] of Object.entries(OS_CONFIG)) {
197
+ if (inputLower === key || config.aliases.some(alias => inputLower === alias)) {
198
+ return config.name;
199
+ }
200
+ }
201
+
202
+ // Try substring match with longer aliases first to avoid false matches
203
+ // (e.g., "darwin" shouldn't match "win" in "darwin")
204
+ const allAliases = [];
205
+ for (const [key, config] of Object.entries(OS_CONFIG)) {
206
+ const names = [key, ...config.aliases];
207
+ names.forEach(name => allAliases.push({ name, result: config.name }));
208
+ }
209
+
210
+ // Sort by length (longest first) to match more specific patterns first
211
+ allAliases.sort((a, b) => b.name.length - a.name.length);
212
+
213
+ // Try substring match
214
+ for (const alias of allAliases) {
215
+ if (inputLower.includes(alias.name)) {
216
+ return alias.result;
217
+ }
218
+ }
219
+
220
+ // No match found
221
+ return 'Unknown OS';
222
+ }
223
+
224
+ /**
225
+ * Normalizes device type to standard categories
226
+ *
227
+ * Supports:
228
+ * - Standard types: "desktop", "mobile", "tablet"
229
+ * - Aliases: "pc", "phone", "smartphone", "laptop"
230
+ * - Case-insensitive matching
231
+ *
232
+ * @param {string} input - Device type or description
233
+ * @returns {string} - Normalized device type: "Desktop", "Mobile", "Tablet", or "Desktop" (default)
234
+ *
235
+ * @example
236
+ * normalizeDevice('desktop'); // Returns: "Desktop"
237
+ * normalizeDevice('MOBILE'); // Returns: "Mobile"
238
+ * normalizeDevice('phone'); // Returns: "Mobile"
239
+ * normalizeDevice('pc'); // Returns: "Desktop"
240
+ * normalizeDevice('unknown'); // Returns: "Desktop" (fallback)
241
+ */
242
+ function normalizeDevice(input) {
243
+ if (!input || typeof input !== 'string') {
244
+ return 'Desktop'; // Default fallback
245
+ }
246
+
247
+ const inputLower = input.toLowerCase().trim();
248
+
249
+ // Try to match against known device types
250
+ for (const [key, config] of Object.entries(DEVICE_CONFIG)) {
251
+ const allNames = [key, ...config.aliases];
252
+
253
+ for (const name of allNames) {
254
+ if (inputLower.includes(name)) {
255
+ return config.name;
256
+ }
257
+ }
258
+ }
259
+
260
+ // Default fallback
261
+ return 'Desktop';
262
+ }
263
+
264
+ /**
265
+ * Normalizes all run data fields at once
266
+ *
267
+ * @param {Object} data - Object containing browser, os, and/or device fields
268
+ * @param {string} [data.browser] - Browser name/version/user agent
269
+ * @param {string} [data.os] - OS name/version/platform
270
+ * @param {string} [data.device] - Device type
271
+ * @returns {Object} - Normalized data with same field names
272
+ *
273
+ * @example
274
+ * normalizeRunData({
275
+ * browser: 'chrome 141.0.7811.4',
276
+ * os: 'Windows 11',
277
+ * device: 'desktop'
278
+ * });
279
+ * // Returns: { browser: 'Chrome 141', os: 'Windows', device: 'Desktop' }
280
+ */
281
+ function normalizeRunData(data) {
282
+ const normalized = {};
283
+
284
+ if (data.browser !== undefined) {
285
+ normalized.browser = normalizeBrowser(data.browser);
286
+ }
287
+
288
+ if (data.os !== undefined) {
289
+ normalized.os = normalizeOS(data.os);
290
+ }
291
+
292
+ if (data.device !== undefined) {
293
+ normalized.device = normalizeDevice(data.device);
294
+ }
295
+
296
+ return normalized;
297
+ }
298
+
299
+ /**
300
+ * Gets list of all supported browsers
301
+ * @returns {Array<string>} - Array of browser names
302
+ */
303
+ function getSupportedBrowsers() {
304
+ return Object.values(BROWSER_CONFIG).map(config => config.name);
305
+ }
306
+
307
+ /**
308
+ * Gets list of all supported operating systems
309
+ * @returns {Array<string>} - Array of OS names
310
+ */
311
+ function getSupportedOS() {
312
+ return Object.values(OS_CONFIG).map(config => config.name);
313
+ }
314
+
315
+ /**
316
+ * Gets list of all supported device types
317
+ * @returns {Array<string>} - Array of device types
318
+ */
319
+ function getSupportedDevices() {
320
+ return Object.values(DEVICE_CONFIG).map(config => config.name);
321
+ }
322
+
323
+ module.exports = {
324
+ normalizeBrowser,
325
+ normalizeOS,
326
+ normalizeDevice,
327
+ normalizeRunData,
328
+ getSupportedBrowsers,
329
+ getSupportedOS,
330
+ getSupportedDevices,
331
+ // Export configs for extensibility
332
+ BROWSER_CONFIG,
333
+ OS_CONFIG,
334
+ DEVICE_CONFIG
335
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * UUID Validator for Appliqation test case UUIDs
3
+ * Format: {nid}-{uuid-v4}
4
+ * Example: 124-d1f9559c-b978-43cc-9c76-fd539c717cb4
5
+ */
6
+ class UuidValidator {
7
+ /**
8
+ * Validate UUID format
9
+ * @param {string} uuid - UUID to validate
10
+ * @returns {boolean} True if valid, false otherwise
11
+ */
12
+ static validate(uuid) {
13
+ if (!uuid || typeof uuid !== 'string') {
14
+ return false;
15
+ }
16
+
17
+ // Pattern: nid-uuid-v4
18
+ const pattern = /^\d+-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
19
+ return pattern.test(uuid);
20
+ }
21
+
22
+ /**
23
+ * Extract NID (node ID) from UUID
24
+ * @param {string} uuid - UUID string
25
+ * @returns {number|null} NID or null if invalid
26
+ */
27
+ static extractNid(uuid) {
28
+ if (!this.validate(uuid)) {
29
+ return null;
30
+ }
31
+ const parts = uuid.split('-');
32
+ return parseInt(parts[0], 10);
33
+ }
34
+
35
+ /**
36
+ * Extract UUID portion (without NID)
37
+ * @param {string} uuid - Full UUID string
38
+ * @returns {string|null} UUID portion or null if invalid
39
+ */
40
+ static extractUuidPortion(uuid) {
41
+ if (!this.validate(uuid)) {
42
+ return null;
43
+ }
44
+ const parts = uuid.split('-');
45
+ return parts.slice(1).join('-');
46
+ }
47
+
48
+ /**
49
+ * Validate and extract components
50
+ * @param {string} uuid - UUID string
51
+ * @returns {Object} { valid, nid, uuid, error }
52
+ */
53
+ static validateAndExtract(uuid) {
54
+ if (!uuid) {
55
+ return {
56
+ valid: false,
57
+ nid: null,
58
+ uuid: null,
59
+ error: 'UUID is required'
60
+ };
61
+ }
62
+
63
+ if (typeof uuid !== 'string') {
64
+ return {
65
+ valid: false,
66
+ nid: null,
67
+ uuid: null,
68
+ error: 'UUID must be a string'
69
+ };
70
+ }
71
+
72
+ const isValid = this.validate(uuid);
73
+
74
+ if (!isValid) {
75
+ return {
76
+ valid: false,
77
+ nid: null,
78
+ uuid: null,
79
+ error: 'Invalid UUID format. Expected: {nid}-{uuid-v4}'
80
+ };
81
+ }
82
+
83
+ return {
84
+ valid: true,
85
+ nid: this.extractNid(uuid),
86
+ uuid: this.extractUuidPortion(uuid),
87
+ error: null
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Create UUID from components
93
+ * @param {number} nid - Node ID
94
+ * @param {string} uuidPortion - UUID v4 string
95
+ * @returns {string} Combined UUID
96
+ */
97
+ static create(nid, uuidPortion) {
98
+ return `${nid}-${uuidPortion}`;
99
+ }
100
+ }
101
+
102
+ module.exports = UuidValidator;