@appliqation/automation-sdk 2.4.0 → 2.5.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/README.md +11 -60
- package/package.json +1 -1
- package/src/AppliqationClient.js +125 -467
- package/src/core/HttpClient.js +0 -62
- package/src/playwright/global-setup.js +44 -153
- package/src/reporters/cypress/CypressReporter.js +0 -12
- package/src/reporters/jest/JestReporter.js +0 -12
- package/src/reporters/playwright/AppliqationReporter.js +396 -968
- package/src/services/ProjectInfoService.js +44 -0
package/src/AppliqationClient.js
CHANGED
|
@@ -4,6 +4,7 @@ const RunMatrixService = require('./services/RunMatrixService');
|
|
|
4
4
|
const ResultService = require('./services/ResultService');
|
|
5
5
|
const TaggingService = require('./services/TaggingService');
|
|
6
6
|
const OrphanTestService = require('./services/OrphanTestService');
|
|
7
|
+
const ProjectInfoService = require('./services/ProjectInfoService');
|
|
7
8
|
const UuidValidator = require('./utils/UuidValidator');
|
|
8
9
|
const PayloadBuilder = require('./utils/PayloadBuilder');
|
|
9
10
|
const logger = require('./utils/logger');
|
|
@@ -13,102 +14,82 @@ const { DEFAULT_APPLIQATION_BASE_URL } = require('./constants');
|
|
|
13
14
|
* Main Appliqation Automation SDK Client
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
|
-
* //
|
|
17
|
+
* // Minimal config — project auto-discovered from API key
|
|
17
18
|
* const client = new AppliqationClient({
|
|
18
|
-
*
|
|
19
|
-
* apiKey: 'appq_live_xxxxxxxxxxxxx',
|
|
20
|
-
* projectKey: 'your-project-key'
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* // Create run matrix
|
|
24
|
-
* const run = await client.createRun({
|
|
25
|
-
* scenarioId: 123,
|
|
26
|
-
* environment: 'Production',
|
|
27
|
-
* browsers: ['Chrome', 'Firefox'],
|
|
28
|
-
* device: 'Desktop',
|
|
29
|
-
* os: 'Windows 11'
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* // Submit test results
|
|
33
|
-
* await client.submitResult(run.runId, {
|
|
34
|
-
* uuid: '124-d1f9559c-b978-43cc-9c76-fd539c717cb4',
|
|
35
|
-
* status: 'passed',
|
|
36
|
-
* browser: 'Chrome'
|
|
19
|
+
* apiKey: process.env.APPLIQATION_API_KEY
|
|
37
20
|
* });
|
|
21
|
+
* await client.initialize(); // auto-discovers project
|
|
38
22
|
*/
|
|
39
23
|
class AppliqationClient {
|
|
24
|
+
|
|
40
25
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param {
|
|
45
|
-
* @
|
|
46
|
-
* @param {string} [config.username] - Username for CSRF auth (legacy)
|
|
47
|
-
* @param {string} [config.password] - Password for CSRF auth (legacy)
|
|
48
|
-
* @param {string} [config.title] - Custom run title (or use APPLIQATION_RUN_TITLE env var)
|
|
49
|
-
* @param {Object} [config.options] - Additional options
|
|
50
|
-
* @param {number} [config.options.timeout=30000] - Request timeout in ms
|
|
51
|
-
* @param {number} [config.options.retries=3] - Number of retry attempts
|
|
52
|
-
* @param {boolean} [config.options.logOrphans=true] - Log orphan tests to backend
|
|
53
|
-
* @param {string} [config.options.logLevel='info'] - Logging level
|
|
26
|
+
* Resolve configuration from multiple sources.
|
|
27
|
+
* Priority: explicit config > env vars > defaults
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} config - User-provided configuration
|
|
30
|
+
* @returns {Object} Resolved configuration
|
|
54
31
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
32
|
+
static resolveConfig(config = {}) {
|
|
33
|
+
const apiKey = config.apiKey || process.env.APPLIQATION_API_KEY || null;
|
|
34
|
+
const baseUrl = (config.baseUrl || DEFAULT_APPLIQATION_BASE_URL || '').replace(/\/$/, '');
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...config,
|
|
38
|
+
apiKey,
|
|
39
|
+
baseUrl,
|
|
40
|
+
projectKey: config.projectKey || process.env.APPLIQATION_PROJECT_KEY || null,
|
|
41
|
+
runTitle: config.runTitle || config.title || process.env.APPLIQATION_RUN_TITLE || null,
|
|
61
42
|
};
|
|
43
|
+
}
|
|
62
44
|
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Create Appliqation client
|
|
47
|
+
* @param {Object} config - Client configuration (can be empty — uses resolveConfig)
|
|
48
|
+
*/
|
|
49
|
+
constructor(config = {}) {
|
|
50
|
+
const resolved = AppliqationClient.resolveConfig(config);
|
|
51
|
+
|
|
52
|
+
// Validate — warn instead of throw
|
|
53
|
+
const validation = this.validateConfig(resolved);
|
|
54
|
+
if (!validation.valid) {
|
|
55
|
+
// Log warnings but don't throw
|
|
56
|
+
for (const warning of validation.warnings) {
|
|
57
|
+
logger.warn(warning);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
65
60
|
|
|
66
61
|
// Determine SSL enforcement based on environment
|
|
67
62
|
const isProduction = this._isProductionEnvironment();
|
|
68
|
-
let rejectUnauthorized = true;
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// WARNING: User is trying to disable SSL in production
|
|
74
|
-
logger.warn('SSL certificate verification is disabled in production environment!', {
|
|
75
|
-
environment: process.env.NODE_ENV,
|
|
76
|
-
ci: process.env.CI,
|
|
77
|
-
warning: 'This is a security risk. SSL verification should be enabled in production.'
|
|
78
|
-
});
|
|
63
|
+
let rejectUnauthorized = true;
|
|
64
|
+
|
|
65
|
+
if (resolved.rejectUnauthorized !== undefined) {
|
|
66
|
+
if (isProduction && resolved.rejectUnauthorized === false) {
|
|
67
|
+
logger.warn('SSL certificate verification is disabled in production environment!');
|
|
79
68
|
}
|
|
80
|
-
rejectUnauthorized =
|
|
81
|
-
} else if (isProduction) {
|
|
82
|
-
// Production environment - force SSL verification
|
|
83
|
-
rejectUnauthorized = true;
|
|
84
|
-
logger.info('SSL certificate verification enforced for production environment');
|
|
85
|
-
} else {
|
|
86
|
-
// Development environment - default to secure but can be overridden
|
|
87
|
-
rejectUnauthorized = true;
|
|
69
|
+
rejectUnauthorized = resolved.rejectUnauthorized;
|
|
88
70
|
}
|
|
89
71
|
|
|
90
72
|
// Store configuration
|
|
91
73
|
this.config = {
|
|
92
|
-
baseUrl:
|
|
93
|
-
apiKey:
|
|
94
|
-
projectKey:
|
|
95
|
-
username:
|
|
96
|
-
password:
|
|
97
|
-
runTitle:
|
|
98
|
-
rejectUnauthorized
|
|
74
|
+
baseUrl: resolved.baseUrl,
|
|
75
|
+
apiKey: resolved.apiKey,
|
|
76
|
+
projectKey: resolved.projectKey,
|
|
77
|
+
username: resolved.username,
|
|
78
|
+
password: resolved.password,
|
|
79
|
+
runTitle: resolved.runTitle,
|
|
80
|
+
rejectUnauthorized,
|
|
99
81
|
options: {
|
|
100
|
-
timeout:
|
|
101
|
-
retries:
|
|
102
|
-
logOrphans:
|
|
103
|
-
logLevel:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
normalizedConfig.autoTagName ||
|
|
82
|
+
timeout: resolved.options?.timeout || 30000,
|
|
83
|
+
retries: resolved.options?.retries || 3,
|
|
84
|
+
logOrphans: resolved.options?.logOrphans !== false,
|
|
85
|
+
logLevel: resolved.options?.logLevel || 'info',
|
|
86
|
+
autoTag: resolved.options?.autoTag !== false,
|
|
87
|
+
autoTagName: resolved.options?.autoTagName ||
|
|
88
|
+
resolved.autoTagName ||
|
|
108
89
|
process.env.APPLIQATION_AUTO_TAG_NAME ||
|
|
109
90
|
'Appq_automated',
|
|
110
|
-
autoTagBatchSize:
|
|
111
|
-
autoTagRetries:
|
|
91
|
+
autoTagBatchSize: resolved.options?.autoTagBatchSize || 50,
|
|
92
|
+
autoTagRetries: resolved.options?.autoTagRetries || 2
|
|
112
93
|
}
|
|
113
94
|
};
|
|
114
95
|
|
|
@@ -124,203 +105,64 @@ class AppliqationClient {
|
|
|
124
105
|
this.tagging = new TaggingService(this.http, this.config);
|
|
125
106
|
this.results = new ResultService(this.http, this.tagging, this.config);
|
|
126
107
|
this.orphans = new OrphanTestService(this.http);
|
|
108
|
+
this.projectInfo = new ProjectInfoService();
|
|
127
109
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
tagName: this.tagging.tagName
|
|
132
|
-
});
|
|
110
|
+
// Auto-discovery state
|
|
111
|
+
this.availableEnvironments = null;
|
|
112
|
+
this._initialized = false;
|
|
133
113
|
|
|
134
114
|
// Track current run context
|
|
135
115
|
this.currentRun = null;
|
|
136
116
|
|
|
137
|
-
// Auto-configuration state
|
|
138
|
-
this._autoConfigured = false;
|
|
139
|
-
this._projectInfo = null;
|
|
140
|
-
this._autoConfigPromise = null; // Prevents concurrent auto-config calls
|
|
141
|
-
|
|
142
|
-
// Show baseUrl only in DEBUG mode
|
|
143
117
|
const meta = {
|
|
144
|
-
authMode: this.config.apiKey ? 'api_key' : 'csrf'
|
|
145
|
-
autoConfigEnabled: !this.config.projectKey // Will auto-fetch if projectKey not provided
|
|
118
|
+
authMode: this.config.apiKey ? 'api_key' : 'csrf'
|
|
146
119
|
};
|
|
147
120
|
logger.info('Appliqation client initialized', meta);
|
|
148
121
|
logger.debug('Client configuration', { baseUrl: this.config.baseUrl });
|
|
149
122
|
}
|
|
150
123
|
|
|
151
124
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
125
|
+
* Auto-discover project info from API key.
|
|
126
|
+
* Fills in missing projectKey and availableEnvironments.
|
|
127
|
+
* Never throws — logs warnings on failure.
|
|
155
128
|
*
|
|
156
|
-
* @private
|
|
157
129
|
* @returns {Promise<void>}
|
|
158
|
-
* @throws {Error} If auto-configuration fails
|
|
159
130
|
*/
|
|
160
|
-
async
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// If already auto-configured, skip
|
|
167
|
-
if (this._autoConfigured) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
131
|
+
async initialize() {
|
|
132
|
+
if (this._initialized) return;
|
|
133
|
+
this._initialized = true;
|
|
170
134
|
|
|
171
|
-
|
|
172
|
-
if (this._autoConfigPromise) {
|
|
173
|
-
return this._autoConfigPromise;
|
|
174
|
-
}
|
|
135
|
+
if (!this.config.apiKey) return;
|
|
175
136
|
|
|
176
|
-
|
|
177
|
-
this._autoConfigPromise = this._performAutoConfig();
|
|
137
|
+
const info = await this.projectInfo.getProjectInfo(this.http);
|
|
178
138
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Perform auto-configuration by fetching project info
|
|
188
|
-
* @private
|
|
189
|
-
* @returns {Promise<void>}
|
|
190
|
-
*/
|
|
191
|
-
async _performAutoConfig() {
|
|
192
|
-
logger.info('Auto-fetching project configuration from API key...');
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
this._projectInfo = await this.http.getProjectInfo();
|
|
196
|
-
|
|
197
|
-
// Validate response structure
|
|
198
|
-
if (!this._projectInfo || typeof this._projectInfo !== 'object') {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Invalid response format: expected object, got ${typeof this._projectInfo}`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!this._projectInfo.project_id && this._projectInfo.project_id !== 0) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`Invalid response: missing project_id field in API response`
|
|
207
|
-
);
|
|
139
|
+
if (info) {
|
|
140
|
+
// Auto-fill projectKey if not explicitly set
|
|
141
|
+
if (!this.config.projectKey && info.key) {
|
|
142
|
+
this.config.projectKey = info.key;
|
|
143
|
+
logger.info('Project auto-discovered', { projectKey: info.key, name: info.name });
|
|
208
144
|
}
|
|
209
145
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
logger.info('Auto-configuration successful', {
|
|
216
|
-
project_id: this._projectInfo.project_id,
|
|
217
|
-
project_title: this._projectInfo.project_title,
|
|
218
|
-
environments: this._projectInfo.environments,
|
|
219
|
-
default_environment: this._projectInfo.default_environment
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Log warning if no environments configured
|
|
223
|
-
if (!this._projectInfo.has_environments_configured) {
|
|
224
|
-
logger.warn('Project has no environments configured. Using default environment "Local".', {
|
|
225
|
-
project_id: this._projectInfo.project_id,
|
|
226
|
-
action_required: 'Configure environments in project settings for better environment validation'
|
|
227
|
-
});
|
|
146
|
+
// Store available environments for validation
|
|
147
|
+
if (info.environments && info.environments.length > 0) {
|
|
148
|
+
this.availableEnvironments = info.environments;
|
|
149
|
+
logger.debug('Available environments', { environments: info.environments });
|
|
228
150
|
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
error: error.message
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Failed to auto-configure project from API key: ${error.message}\n\n` +
|
|
236
|
-
'Possible solutions:\n' +
|
|
237
|
-
'1. Verify your API key is valid and not expired\n' +
|
|
238
|
-
'2. Ensure your API key is associated with a valid project\n' +
|
|
239
|
-
'3. Check network connectivity to Appliqation instance\n' +
|
|
240
|
-
'4. Provide projectKey manually in configuration as fallback:\n' +
|
|
241
|
-
' { projectKey: "your-project-id" }'
|
|
242
|
-
);
|
|
151
|
+
} else if (!this.config.projectKey) {
|
|
152
|
+
console.warn('[Appliqation] Project auto-discovery failed. Tests will continue without reporting.');
|
|
243
153
|
}
|
|
244
154
|
}
|
|
245
155
|
|
|
246
|
-
/**
|
|
247
|
-
* Get available environments for the project
|
|
248
|
-
* Automatically triggers project info fetch if not already loaded.
|
|
249
|
-
*
|
|
250
|
-
* @returns {Promise<Array<string>>} List of environment names
|
|
251
|
-
* @example
|
|
252
|
-
* const environments = await client.getAvailableEnvironments();
|
|
253
|
-
* console.log(environments); // ['Development', 'Staging', 'Production']
|
|
254
|
-
*/
|
|
255
|
-
async getAvailableEnvironments() {
|
|
256
|
-
await this.ensureProjectConfig();
|
|
257
|
-
return this._projectInfo?.environments || [];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Get default environment for the project
|
|
262
|
-
* Automatically triggers project info fetch if not already loaded.
|
|
263
|
-
*
|
|
264
|
-
* @returns {Promise<string>} Default environment name
|
|
265
|
-
* @example
|
|
266
|
-
* const defaultEnv = await client.getDefaultEnvironment();
|
|
267
|
-
* console.log(defaultEnv); // 'Development'
|
|
268
|
-
*/
|
|
269
|
-
async getDefaultEnvironment() {
|
|
270
|
-
await this.ensureProjectConfig();
|
|
271
|
-
return this._projectInfo?.default_environment || 'Local';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Get cached project information
|
|
276
|
-
* Returns null if auto-configuration hasn't been triggered yet.
|
|
277
|
-
*
|
|
278
|
-
* @returns {Object|null} Project metadata or null
|
|
279
|
-
* @property {number} project_id - Project ID
|
|
280
|
-
* @property {string} project_title - Project name
|
|
281
|
-
* @property {Array<string>} environments - Available environments
|
|
282
|
-
* @property {string} default_environment - Default environment
|
|
283
|
-
* @property {boolean} has_environments_configured - Whether environments exist
|
|
284
|
-
*/
|
|
285
|
-
getProjectInfo() {
|
|
286
|
-
return this._projectInfo;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
156
|
/**
|
|
290
157
|
* Create a new test run matrix
|
|
291
|
-
* @param {Object} options - Run configuration
|
|
292
|
-
* @param {number} options.scenarioId - Scenario node ID
|
|
293
|
-
* @param {number} options.testSetId - Test Set node ID (alternative to scenarioId)
|
|
294
|
-
* @param {string} options.type - 'scenario' or 'testset'
|
|
295
|
-
* @param {string} [options.environment='Local'] - Environment name (auto-detected if not provided)
|
|
296
|
-
* @param {string[]} [options.browsers=['Chrome']] - Array of browser names
|
|
297
|
-
* @param {string} [options.device] - Device type (Desktop, Mobile, Tablet)
|
|
298
|
-
* @param {string} [options.os] - Operating system
|
|
299
|
-
* @param {string} [options.title] - Custom run title
|
|
300
|
-
* @returns {Promise<Object>} { runId, token, timestamp, metadata }
|
|
301
158
|
*/
|
|
302
159
|
async createRun(options) {
|
|
303
160
|
try {
|
|
304
|
-
// Ensure project config is loaded (auto-fetch if needed)
|
|
305
|
-
await this.ensureProjectConfig();
|
|
306
|
-
|
|
307
|
-
// If environment not provided, use auto-detected default
|
|
308
|
-
if (!options.environment && this._projectInfo) {
|
|
309
|
-
options.environment = this._projectInfo.default_environment;
|
|
310
|
-
logger.info('Using auto-detected default environment', {
|
|
311
|
-
environment: options.environment,
|
|
312
|
-
source: 'project_config'
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
161
|
logger.info('Creating run matrix...', options);
|
|
317
|
-
|
|
318
162
|
const run = await this.runMatrix.create(options);
|
|
319
163
|
|
|
320
|
-
// Store current run context
|
|
321
164
|
this.currentRun = run;
|
|
322
165
|
|
|
323
|
-
// Store run configuration in AuthManager for JWT refresh capability
|
|
324
166
|
if (this.auth && this.config.apiKey) {
|
|
325
167
|
this.auth.setRunConfig({
|
|
326
168
|
projectKey: this.config.projectKey,
|
|
@@ -332,7 +174,6 @@ class AppliqationClient {
|
|
|
332
174
|
os: options.os,
|
|
333
175
|
title: options.title || this.config.runTitle
|
|
334
176
|
});
|
|
335
|
-
logger.debug('Run configuration stored in AuthManager for JWT refresh');
|
|
336
177
|
}
|
|
337
178
|
|
|
338
179
|
logger.info('Run matrix created successfully', {
|
|
@@ -352,90 +193,56 @@ class AppliqationClient {
|
|
|
352
193
|
|
|
353
194
|
/**
|
|
354
195
|
* Create multiple runs for device/OS matrix testing
|
|
355
|
-
* @param {Array} configs - Array of run configurations
|
|
356
|
-
* @returns {Promise<Object>} { runs, errors, summary }
|
|
357
196
|
*/
|
|
358
197
|
async createMultipleRuns(configs) {
|
|
359
198
|
try {
|
|
360
199
|
logger.info(`Creating ${configs.length} run matrices...`);
|
|
361
|
-
|
|
362
200
|
const result = await this.runMatrix.createMultiple(configs);
|
|
363
|
-
|
|
364
201
|
logger.info('Multiple runs created', {
|
|
365
202
|
total: result.summary.total,
|
|
366
203
|
success: result.summary.success,
|
|
367
204
|
failed: result.summary.failed
|
|
368
205
|
});
|
|
369
|
-
|
|
370
206
|
return result;
|
|
371
207
|
} catch (error) {
|
|
372
|
-
logger.error('Failed to create multiple runs', {
|
|
373
|
-
error: error.message
|
|
374
|
-
});
|
|
208
|
+
logger.error('Failed to create multiple runs', { error: error.message });
|
|
375
209
|
throw error;
|
|
376
210
|
}
|
|
377
211
|
}
|
|
378
212
|
|
|
379
213
|
/**
|
|
380
214
|
* Submit a single test result
|
|
381
|
-
* @param {string} runId - Run ID
|
|
382
|
-
* @param {Object} result - Test result
|
|
383
|
-
* @param {string} result.uuid - Test case UUID
|
|
384
|
-
* @param {string} result.status - Test status (passed, failed, skipped)
|
|
385
|
-
* @param {string} result.browser - Browser name
|
|
386
|
-
* @param {string} [result.parent_uuid] - Parent test case UUID
|
|
387
|
-
* @param {string} [result.comment] - Additional comments
|
|
388
|
-
* @param {string} [result.environment] - Environment name
|
|
389
|
-
* @returns {Promise<Object>} Response from server
|
|
390
215
|
*/
|
|
391
216
|
async submitResult(runId, result) {
|
|
392
217
|
try {
|
|
393
|
-
// Validate UUID
|
|
394
218
|
const validation = UuidValidator.validateAndExtract(result.uuid);
|
|
395
219
|
if (!validation.valid) {
|
|
396
220
|
throw new Error(`Invalid UUID: ${validation.error}`);
|
|
397
221
|
}
|
|
398
|
-
|
|
399
|
-
// Validate result fields
|
|
400
222
|
const resultValidation = PayloadBuilder.validateResult(result);
|
|
401
223
|
if (!resultValidation.valid) {
|
|
402
224
|
throw new Error(`Invalid result: ${resultValidation.errors.join(', ')}`);
|
|
403
225
|
}
|
|
404
226
|
|
|
405
227
|
logger.debug('Submitting test result', {
|
|
406
|
-
runId,
|
|
407
|
-
uuid: result.uuid,
|
|
408
|
-
status: result.status,
|
|
409
|
-
browser: result.browser
|
|
228
|
+
runId, uuid: result.uuid, status: result.status, browser: result.browser
|
|
410
229
|
});
|
|
411
230
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return response;
|
|
231
|
+
return await this.results.submitSingle(runId, result);
|
|
415
232
|
} catch (error) {
|
|
416
233
|
logger.error('Failed to submit result', {
|
|
417
|
-
error: error.message,
|
|
418
|
-
runId,
|
|
419
|
-
uuid: result.uuid
|
|
234
|
+
error: error.message, runId, uuid: result.uuid
|
|
420
235
|
});
|
|
421
236
|
throw error;
|
|
422
237
|
}
|
|
423
238
|
}
|
|
424
239
|
|
|
425
240
|
/**
|
|
426
|
-
* Submit batch of test results
|
|
427
|
-
* @param {Array} results - Array of test results
|
|
428
|
-
* @param {Object} [options] - Batch options
|
|
429
|
-
* @param {number} [options.batchSize=50] - Results per batch
|
|
430
|
-
* @param {Function} [options.onProgress] - Progress callback
|
|
431
|
-
* @param {boolean} [options.retryFailures=true] - Retry failed submissions
|
|
432
|
-
* @returns {Promise<Object>} { success, failed, total }
|
|
241
|
+
* Submit batch of test results
|
|
433
242
|
*/
|
|
434
243
|
async submitBatch(results, options = {}) {
|
|
435
244
|
try {
|
|
436
245
|
logger.info(`Submitting batch of ${results.length} results...`);
|
|
437
|
-
|
|
438
|
-
// Validate all results first
|
|
439
246
|
const validResults = [];
|
|
440
247
|
const invalidResults = [];
|
|
441
248
|
|
|
@@ -445,10 +252,7 @@ class AppliqationClient {
|
|
|
445
252
|
validResults.push(result);
|
|
446
253
|
} else {
|
|
447
254
|
invalidResults.push({ result, error: validation.error });
|
|
448
|
-
logger.warn('Invalid UUID in batch', {
|
|
449
|
-
uuid: result.uuid,
|
|
450
|
-
error: validation.error
|
|
451
|
-
});
|
|
255
|
+
logger.warn('Invalid UUID in batch', { uuid: result.uuid, error: validation.error });
|
|
452
256
|
}
|
|
453
257
|
}
|
|
454
258
|
|
|
@@ -459,317 +263,171 @@ class AppliqationClient {
|
|
|
459
263
|
const summary = await this.results.submitBatch(validResults, options);
|
|
460
264
|
|
|
461
265
|
logger.info('Batch submission completed', {
|
|
462
|
-
success: summary.success,
|
|
463
|
-
|
|
464
|
-
invalid: invalidResults.length,
|
|
465
|
-
total: results.length
|
|
266
|
+
success: summary.success, failed: summary.failed,
|
|
267
|
+
invalid: invalidResults.length, total: results.length
|
|
466
268
|
});
|
|
467
269
|
|
|
468
|
-
return {
|
|
469
|
-
...summary,
|
|
470
|
-
invalid: invalidResults.length,
|
|
471
|
-
invalidResults
|
|
472
|
-
};
|
|
270
|
+
return { ...summary, invalid: invalidResults.length, invalidResults };
|
|
473
271
|
} catch (error) {
|
|
474
|
-
logger.error('Failed to submit batch', {
|
|
475
|
-
error: error.message,
|
|
476
|
-
count: results.length
|
|
477
|
-
});
|
|
272
|
+
logger.error('Failed to submit batch', { error: error.message, count: results.length });
|
|
478
273
|
throw error;
|
|
479
274
|
}
|
|
480
275
|
}
|
|
481
276
|
|
|
482
277
|
/**
|
|
483
|
-
* Log orphan tests
|
|
484
|
-
* @param {string} runId - Run ID
|
|
485
|
-
* @param {Array} orphanTests - Array of orphan test objects
|
|
486
|
-
* @returns {Promise<Object>} Response from server
|
|
278
|
+
* Log orphan tests
|
|
487
279
|
*/
|
|
488
280
|
async logOrphanTests(runId, orphanTests) {
|
|
489
281
|
try {
|
|
490
282
|
if (!this.config.options.logOrphans) {
|
|
491
|
-
logger.debug('Orphan test logging is disabled');
|
|
492
283
|
return { success: true, logged: 0 };
|
|
493
284
|
}
|
|
494
|
-
|
|
495
285
|
if (!orphanTests || orphanTests.length === 0) {
|
|
496
286
|
return { success: true, logged: 0 };
|
|
497
287
|
}
|
|
498
288
|
|
|
499
289
|
logger.info(`Logging ${orphanTests.length} orphan tests...`);
|
|
500
|
-
|
|
501
290
|
const response = await this.orphans.log(runId, orphanTests);
|
|
502
|
-
|
|
503
|
-
logger.info(`Orphan tests logged successfully`, {
|
|
504
|
-
runId,
|
|
505
|
-
count: orphanTests.length
|
|
506
|
-
});
|
|
507
|
-
|
|
291
|
+
logger.info(`Orphan tests logged successfully`, { runId, count: orphanTests.length });
|
|
508
292
|
return response;
|
|
509
293
|
} catch (error) {
|
|
510
294
|
logger.error('Failed to log orphan tests', {
|
|
511
|
-
error: error.message,
|
|
512
|
-
runId,
|
|
513
|
-
count: orphanTests.length
|
|
295
|
+
error: error.message, runId, count: orphanTests.length
|
|
514
296
|
});
|
|
515
|
-
// Don't throw - orphan logging should not break the test run
|
|
516
297
|
return { success: false, error: error.message };
|
|
517
298
|
}
|
|
518
299
|
}
|
|
519
300
|
|
|
520
301
|
/**
|
|
521
|
-
* Add browser to existing run matrix
|
|
522
|
-
* @param {string} runId - Run ID
|
|
523
|
-
* @param {string} browser - Browser name
|
|
524
|
-
* @param {number} nid - Optional scenario/testset node ID (defaults to 0)
|
|
525
|
-
* @returns {Promise<Object>} Response from server
|
|
302
|
+
* Add browser to existing run matrix
|
|
526
303
|
*/
|
|
527
304
|
async addBrowser(runId, browser, nid = 0) {
|
|
528
305
|
try {
|
|
529
306
|
logger.info('Adding browser to run matrix', { runId, browser, nid });
|
|
530
|
-
|
|
531
307
|
const response = await this.runMatrix.addBrowser(runId, browser, nid);
|
|
532
|
-
|
|
533
308
|
logger.info('Browser added successfully', { runId, browser });
|
|
534
|
-
|
|
535
309
|
return response;
|
|
536
310
|
} catch (error) {
|
|
537
|
-
logger.error('Failed to add browser', {
|
|
538
|
-
error: error.message,
|
|
539
|
-
runId,
|
|
540
|
-
browser
|
|
541
|
-
});
|
|
311
|
+
logger.error('Failed to add browser', { error: error.message, runId, browser });
|
|
542
312
|
throw error;
|
|
543
313
|
}
|
|
544
314
|
}
|
|
545
315
|
|
|
546
316
|
/**
|
|
547
317
|
* Get run matrix data
|
|
548
|
-
* @param {string} runId - Run ID
|
|
549
|
-
* @returns {Promise<Object>} Run matrix data
|
|
550
318
|
*/
|
|
551
319
|
async getRun(runId) {
|
|
552
320
|
try {
|
|
553
|
-
|
|
554
|
-
return response;
|
|
321
|
+
return await this.runMatrix.get(runId);
|
|
555
322
|
} catch (error) {
|
|
556
|
-
logger.error('Failed to get run matrix', {
|
|
557
|
-
error: error.message,
|
|
558
|
-
runId
|
|
559
|
-
});
|
|
323
|
+
logger.error('Failed to get run matrix', { error: error.message, runId });
|
|
560
324
|
throw error;
|
|
561
325
|
}
|
|
562
326
|
}
|
|
563
327
|
|
|
564
|
-
/**
|
|
565
|
-
* Get current run context (if set)
|
|
566
|
-
* @returns {Object|null} Current run data or null
|
|
567
|
-
*/
|
|
568
328
|
getCurrentRun() {
|
|
569
329
|
return this.currentRun;
|
|
570
330
|
}
|
|
571
331
|
|
|
572
|
-
/**
|
|
573
|
-
* Set current run context manually
|
|
574
|
-
* @param {Object} run - Run data with runId
|
|
575
|
-
*/
|
|
576
332
|
setCurrentRun(run) {
|
|
577
333
|
this.currentRun = run;
|
|
578
334
|
}
|
|
579
335
|
|
|
580
336
|
/**
|
|
581
337
|
* Delete a test run
|
|
582
|
-
* @param {string} runId - Run ID to delete
|
|
583
|
-
* @param {string} reason - Deletion reason (for audit logging)
|
|
584
|
-
* @returns {Promise<Object>} Deletion result
|
|
585
338
|
*/
|
|
586
339
|
async deleteRun(runId, reason = 'orphan_cleanup') {
|
|
587
340
|
try {
|
|
588
341
|
logger.info('Deleting run...', { runId, reason });
|
|
589
|
-
|
|
590
342
|
const result = await this.runMatrix.delete(runId, reason);
|
|
591
|
-
|
|
592
|
-
// Clear from current run if it matches
|
|
593
343
|
if (this.currentRun && this.currentRun.runId === runId) {
|
|
594
344
|
this.currentRun = null;
|
|
595
345
|
}
|
|
596
|
-
|
|
597
346
|
logger.info('Run deleted successfully', { runId });
|
|
598
|
-
|
|
599
347
|
return result;
|
|
600
348
|
} catch (error) {
|
|
601
|
-
logger.error('Failed to delete run', {
|
|
602
|
-
error: error.message,
|
|
603
|
-
runId
|
|
604
|
-
});
|
|
349
|
+
logger.error('Failed to delete run', { error: error.message, runId });
|
|
605
350
|
throw error;
|
|
606
351
|
}
|
|
607
352
|
}
|
|
608
353
|
|
|
609
|
-
/**
|
|
610
|
-
* Validate UUID format
|
|
611
|
-
* @param {string} uuid - UUID to validate
|
|
612
|
-
* @returns {boolean} True if valid
|
|
613
|
-
*/
|
|
614
354
|
validateUuid(uuid) {
|
|
615
355
|
return UuidValidator.validate(uuid);
|
|
616
356
|
}
|
|
617
357
|
|
|
618
|
-
/**
|
|
619
|
-
* Extract NID from UUID
|
|
620
|
-
* @param {string} uuid - UUID string
|
|
621
|
-
* @returns {number|null} NID or null if invalid
|
|
622
|
-
*/
|
|
623
358
|
extractNid(uuid) {
|
|
624
359
|
return UuidValidator.extractNid(uuid);
|
|
625
360
|
}
|
|
626
361
|
|
|
627
|
-
/**
|
|
628
|
-
* Validate and extract UUID components
|
|
629
|
-
* @param {string} uuid - UUID string
|
|
630
|
-
* @returns {Object} { valid, nid, uuid, error }
|
|
631
|
-
*/
|
|
632
362
|
parseUuid(uuid) {
|
|
633
363
|
return UuidValidator.validateAndExtract(uuid);
|
|
634
364
|
}
|
|
635
365
|
|
|
636
366
|
/**
|
|
637
|
-
* Validate client configuration
|
|
367
|
+
* Validate client configuration — returns warnings instead of throwing.
|
|
638
368
|
* @private
|
|
369
|
+
* @returns {{ valid: boolean, warnings: string[] }}
|
|
639
370
|
*/
|
|
640
371
|
validateConfig(config) {
|
|
372
|
+
const warnings = [];
|
|
373
|
+
|
|
641
374
|
if (!config) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
'Please provide configuration object:\n' +
|
|
645
|
-
' const client = new AppliqationClient({\n' +
|
|
646
|
-
' baseUrl: process.env.APPLIQATION_BASE_URL,\n' +
|
|
647
|
-
' apiKey: process.env.APPLIQATION_API_KEY,\n' +
|
|
648
|
-
' projectKey: process.env.APPLIQATION_PROJECT_KEY\n' +
|
|
649
|
-
' });'
|
|
650
|
-
);
|
|
375
|
+
warnings.push('[Appliqation] No configuration provided');
|
|
376
|
+
return { valid: false, warnings };
|
|
651
377
|
}
|
|
652
378
|
|
|
653
|
-
if (!config.
|
|
654
|
-
|
|
655
|
-
'baseUrl is required.\n' +
|
|
656
|
-
'Please set the APPLIQATION_BASE_URL environment variable or provide baseUrl in configuration:\n' +
|
|
657
|
-
' APPLIQATION_BASE_URL=https://your-instance.appliqation.com\n' +
|
|
658
|
-
'Or in code:\n' +
|
|
659
|
-
' { baseUrl: "https://your-instance.appliqation.com" }'
|
|
660
|
-
);
|
|
379
|
+
if (!config.apiKey) {
|
|
380
|
+
warnings.push('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
|
|
661
381
|
}
|
|
662
382
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
'Authentication credentials are required.\n' +
|
|
667
|
-
'Please set the APPLIQATION_API_KEY environment variable (recommended):\n' +
|
|
668
|
-
' APPLIQATION_API_KEY=appq_live_xxxxxxxxxxxx\n' +
|
|
669
|
-
'Or in code:\n' +
|
|
670
|
-
' { apiKey: "appq_live_xxxxxxxxxxxx" }\n' +
|
|
671
|
-
'\n' +
|
|
672
|
-
'You can generate an API key in the Appliqation dashboard under Settings > API Keys.\n' +
|
|
673
|
-
'\n' +
|
|
674
|
-
'Legacy CSRF authentication (username/password) is also supported but not recommended.'
|
|
675
|
-
);
|
|
383
|
+
if (!config.projectKey) {
|
|
384
|
+
// Not an error — will be auto-discovered
|
|
385
|
+
logger.debug('projectKey not set — will auto-discover from API key');
|
|
676
386
|
}
|
|
677
387
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
' APPLIQATION_PROJECT_KEY=your-project-key\n' +
|
|
685
|
-
'Or in code:\n' +
|
|
686
|
-
' { projectKey: "your-project-key" }\n' +
|
|
687
|
-
'\n' +
|
|
688
|
-
'Alternatively, use API key authentication (recommended) and projectKey will be auto-fetched:\n' +
|
|
689
|
-
' APPLIQATION_API_KEY=appq_live_xxxxxxxxxxxx\n' +
|
|
690
|
-
'\n' +
|
|
691
|
-
'You can find your project key in the Appliqation dashboard under Project Settings.'
|
|
692
|
-
);
|
|
388
|
+
if (config.baseUrl) {
|
|
389
|
+
try {
|
|
390
|
+
new URL(config.baseUrl);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
warnings.push(`[Appliqation] Invalid baseUrl format: "${config.baseUrl}"`);
|
|
393
|
+
}
|
|
693
394
|
}
|
|
694
395
|
|
|
695
|
-
|
|
696
|
-
// Numeric IDs are used when auto-fetched from API key
|
|
697
|
-
// The backend validates project access permissions
|
|
698
|
-
|
|
699
|
-
// Validate URL format
|
|
700
|
-
try {
|
|
701
|
-
new URL(config.baseUrl);
|
|
702
|
-
} catch (error) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
`Invalid baseUrl format: "${config.baseUrl}"\n` +
|
|
705
|
-
'baseUrl must be a valid URL starting with http:// or https://\n' +
|
|
706
|
-
'Example: https://your-instance.appliqation.com'
|
|
707
|
-
);
|
|
708
|
-
}
|
|
396
|
+
return { valid: warnings.length === 0, warnings };
|
|
709
397
|
}
|
|
710
398
|
|
|
711
399
|
/**
|
|
712
|
-
* Test connectivity
|
|
713
|
-
* @returns {Promise<boolean>} True if connection successful
|
|
400
|
+
* Test connectivity
|
|
714
401
|
*/
|
|
715
402
|
async testConnection() {
|
|
716
403
|
try {
|
|
717
404
|
logger.info('Testing connection to Appliqation...');
|
|
718
|
-
|
|
719
|
-
// Try to authenticate
|
|
720
405
|
await this.auth.authenticate();
|
|
721
|
-
|
|
722
406
|
logger.info('Connection test successful');
|
|
723
407
|
return true;
|
|
724
408
|
} catch (error) {
|
|
725
|
-
logger.error('Connection test failed', {
|
|
726
|
-
error: error.message
|
|
727
|
-
});
|
|
409
|
+
logger.error('Connection test failed', { error: error.message });
|
|
728
410
|
throw new Error(`Connection test failed: ${error.message}`);
|
|
729
411
|
}
|
|
730
412
|
}
|
|
731
413
|
|
|
732
414
|
/**
|
|
733
|
-
* Determine if the current environment is production
|
|
734
415
|
* @private
|
|
735
|
-
* @returns {boolean} True if production environment
|
|
736
416
|
*/
|
|
737
417
|
_isProductionEnvironment() {
|
|
738
|
-
|
|
739
|
-
if (process.env.CI === 'true' || process.env.CI === '1') {
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Check NODE_ENV
|
|
418
|
+
if (process.env.CI === 'true' || process.env.CI === '1') return true;
|
|
744
419
|
const nodeEnv = (process.env.NODE_ENV || '').toLowerCase();
|
|
745
|
-
if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'staging')
|
|
746
|
-
return true;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Check for common CI environment variables
|
|
420
|
+
if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'staging') return true;
|
|
750
421
|
const ciEnvVars = [
|
|
751
|
-
'GITHUB_ACTIONS',
|
|
752
|
-
'
|
|
753
|
-
'CIRCLECI',
|
|
754
|
-
'TRAVIS',
|
|
755
|
-
'JENKINS_URL',
|
|
756
|
-
'TEAMCITY_VERSION',
|
|
757
|
-
'BUILDKITE'
|
|
422
|
+
'GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'TRAVIS',
|
|
423
|
+
'JENKINS_URL', 'TEAMCITY_VERSION', 'BUILDKITE'
|
|
758
424
|
];
|
|
759
|
-
|
|
760
425
|
for (const envVar of ciEnvVars) {
|
|
761
|
-
if (process.env[envVar])
|
|
762
|
-
return true;
|
|
763
|
-
}
|
|
426
|
+
if (process.env[envVar]) return true;
|
|
764
427
|
}
|
|
765
|
-
|
|
766
428
|
return false;
|
|
767
429
|
}
|
|
768
430
|
|
|
769
|
-
/**
|
|
770
|
-
* Get SDK version
|
|
771
|
-
* @returns {string} Version string
|
|
772
|
-
*/
|
|
773
431
|
getVersion() {
|
|
774
432
|
return require('../package.json').version;
|
|
775
433
|
}
|