@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,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;
|