@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.1-codeceptjs
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 +1 -0
- package/lib/adapter/codecept.js +288 -202
- package/lib/adapter/cypress-plugin/index.js +0 -2
- package/lib/adapter/mocha.js +0 -1
- package/lib/adapter/nightwatch.js +5 -5
- package/lib/adapter/playwright.js +11 -3
- package/lib/adapter/webdriver.d.ts +1 -1
- package/lib/adapter/webdriver.js +18 -8
- package/lib/bin/cli.js +73 -8
- package/lib/bin/reportXml.js +4 -2
- package/lib/bin/startTest.js +3 -2
- package/lib/bin/uploadArtifacts.js +5 -4
- package/lib/client.js +30 -10
- package/lib/data-storage.d.ts +5 -5
- package/lib/data-storage.js +23 -13
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +11 -1
- package/lib/pipe/bitbucket.d.ts +2 -0
- package/lib/pipe/bitbucket.js +38 -26
- package/lib/pipe/debug.js +27 -6
- package/lib/pipe/github.d.ts +2 -2
- package/lib/pipe/github.js +35 -3
- package/lib/pipe/gitlab.d.ts +2 -0
- package/lib/pipe/gitlab.js +27 -9
- package/lib/pipe/html.js +0 -3
- package/lib/pipe/index.js +17 -7
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +85 -75
- package/lib/replay.d.ts +31 -0
- package/lib/replay.js +255 -0
- package/lib/reporter-functions.d.ts +7 -0
- package/lib/reporter-functions.js +36 -0
- package/lib/reporter.d.ts +15 -12
- package/lib/reporter.js +4 -1
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.js +2 -0
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +22 -0
- package/lib/services/labels.js +62 -0
- package/lib/services/logger.d.ts +1 -1
- package/lib/services/logger.js +1 -2
- package/lib/template/testomatio.hbs +443 -68
- package/lib/uploader.js +10 -6
- package/lib/utils/constants.d.ts +12 -0
- package/lib/utils/constants.js +15 -0
- package/lib/utils/utils.d.ts +10 -1
- package/lib/utils/utils.js +70 -22
- package/lib/xmlReader.js +54 -19
- package/package.json +16 -11
- package/src/adapter/codecept.js +320 -214
- package/src/adapter/cypress-plugin/index.js +0 -2
- package/src/adapter/mocha.js +0 -1
- package/src/adapter/nightwatch.js +1 -1
- package/src/adapter/playwright.js +10 -7
- package/src/adapter/webdriver.js +2 -2
- package/src/bin/cli.js +70 -2
- package/src/bin/reportXml.js +4 -1
- package/src/bin/startTest.js +2 -1
- package/src/bin/uploadArtifacts.js +2 -1
- package/src/client.js +18 -3
- package/src/data-storage.js +6 -6
- package/src/junit-adapter/csharp.js +13 -1
- package/src/pipe/bitbucket.js +22 -24
- package/src/pipe/debug.js +26 -5
- package/src/pipe/github.js +1 -2
- package/src/pipe/gitlab.js +27 -9
- package/src/pipe/html.js +1 -4
- package/src/pipe/testomatio.js +106 -105
- package/src/replay.js +262 -0
- package/src/reporter-functions.js +41 -0
- package/src/reporter.js +3 -0
- package/src/services/index.js +2 -0
- package/src/services/labels.js +59 -0
- package/src/services/logger.js +1 -2
- package/src/template/testomatio.hbs +443 -68
- package/src/uploader.js +11 -6
- package/src/utils/constants.js +12 -0
- package/src/utils/utils.js +46 -13
- package/src/xmlReader.js +70 -18
package/lib/pipe/testomatio.js
CHANGED
|
@@ -5,19 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const debug_1 = __importDefault(require("debug"));
|
|
7
7
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
-
|
|
9
|
-
const axios_retry_1 = __importDefault(require("axios-retry"));
|
|
10
|
-
// Default axios instance
|
|
11
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const gaxios_1 = require("gaxios");
|
|
12
9
|
const json_cycle_1 = __importDefault(require("json-cycle"));
|
|
13
10
|
const constants_js_1 = require("../constants.js");
|
|
14
11
|
const utils_js_1 = require("../utils/utils.js");
|
|
15
12
|
const pipe_utils_js_1 = require("../utils/pipe_utils.js");
|
|
16
13
|
const config_js_1 = require("../config.js");
|
|
17
14
|
const debug = (0, debug_1.default)('@testomatio/reporter:pipe:testomatio');
|
|
18
|
-
if (process.env.TESTOMATIO_RUN)
|
|
19
|
-
|
|
20
|
-
}
|
|
15
|
+
if (process.env.TESTOMATIO_RUN)
|
|
16
|
+
process.env.runId = process.env.TESTOMATIO_RUN;
|
|
21
17
|
/**
|
|
22
18
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
23
19
|
* @typedef {import('../../types/types.js').TestData} TestData
|
|
@@ -55,48 +51,37 @@ class TestomatioPipe {
|
|
|
55
51
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
56
52
|
this.env = process.env.TESTOMATIO_ENV;
|
|
57
53
|
this.label = process.env.TESTOMATIO_LABEL;
|
|
58
|
-
// Create a new instance of
|
|
59
|
-
this.
|
|
54
|
+
// Create a new instance of gaxios with a custom config
|
|
55
|
+
this.client = new gaxios_1.Gaxios({
|
|
60
56
|
baseURL: `${this.url.trim()}`,
|
|
61
57
|
timeout: constants_js_1.AXIOS_TIMEOUT,
|
|
62
|
-
proxy: proxy
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
// Pass the axios instance to the retry function
|
|
71
|
-
(0, axios_retry_1.default)(this.axios, {
|
|
72
|
-
// do not use retries for unit tests
|
|
73
|
-
retries: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
|
|
74
|
-
shouldResetTimeout: true,
|
|
75
|
-
retryCondition: error => {
|
|
76
|
-
if (!error.response)
|
|
77
|
-
return false;
|
|
78
|
-
switch (error.response?.status) {
|
|
79
|
-
case 400: // Bad request (probably wrong API key)
|
|
80
|
-
case 404: // Test not matched
|
|
81
|
-
case 429: // Rate limit exceeded
|
|
82
|
-
case 500: // Internal server error
|
|
58
|
+
proxy: proxy ? proxy.toString() : undefined,
|
|
59
|
+
retry: true,
|
|
60
|
+
retryConfig: {
|
|
61
|
+
retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
62
|
+
retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
63
|
+
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
64
|
+
shouldRetry: (error) => {
|
|
65
|
+
if (!error.response)
|
|
83
66
|
return false;
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
switch (error.response?.status) {
|
|
68
|
+
case 400: // Bad request (probably wrong API key)
|
|
69
|
+
case 404: // Test not matched
|
|
70
|
+
case 429: // Rate limit exceeded
|
|
71
|
+
case 500: // Internal server error
|
|
72
|
+
return false;
|
|
73
|
+
default:
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
86
77
|
}
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
retryDelay: () => constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
|
|
90
|
-
onRetry: async (retryCount, error) => {
|
|
91
|
-
this.retriesTimestamps.push(Date.now());
|
|
92
|
-
debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
|
|
93
|
-
},
|
|
78
|
+
}
|
|
94
79
|
});
|
|
95
80
|
this.isEnabled = true;
|
|
96
81
|
// do not finish this run (for parallel testing)
|
|
97
82
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
98
83
|
this.jiraId = process.env.TESTOMATIO_JIRA_ID;
|
|
99
|
-
this.runId = params.runId || process.env.
|
|
84
|
+
this.runId = params.runId || process.env.TESTOMATIO_RUN;
|
|
100
85
|
this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
|
|
101
86
|
this.hasUnmatchedTests = false;
|
|
102
87
|
this.requestFailures = 0;
|
|
@@ -125,11 +110,14 @@ class TestomatioPipe {
|
|
|
125
110
|
if (!q) {
|
|
126
111
|
return;
|
|
127
112
|
}
|
|
128
|
-
const resp = await this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
const resp = await this.client.request({
|
|
114
|
+
method: 'GET',
|
|
115
|
+
url: '/api/test_grep',
|
|
116
|
+
params: q
|
|
117
|
+
});
|
|
118
|
+
if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
|
|
119
|
+
(0, utils_js_1.foundedTestLog)(constants_js_1.APP_PREFIX, resp.data.tests);
|
|
120
|
+
return resp.data.tests;
|
|
133
121
|
}
|
|
134
122
|
console.log(constants_js_1.APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
135
123
|
}
|
|
@@ -181,16 +169,23 @@ class TestomatioPipe {
|
|
|
181
169
|
if (this.runId) {
|
|
182
170
|
this.store.runId = this.runId;
|
|
183
171
|
debug(`Run with id ${this.runId} already created, updating...`);
|
|
184
|
-
const resp = await this.
|
|
172
|
+
const resp = await this.client.request({
|
|
173
|
+
method: 'PUT',
|
|
174
|
+
url: `/api/reporter/${this.runId}`,
|
|
175
|
+
data: runParams
|
|
176
|
+
});
|
|
185
177
|
if (resp.data.artifacts)
|
|
186
178
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
187
179
|
return;
|
|
188
180
|
}
|
|
189
181
|
debug('Creating run...');
|
|
190
182
|
try {
|
|
191
|
-
const resp = await this.
|
|
183
|
+
const resp = await this.client.request({
|
|
184
|
+
method: 'POST',
|
|
185
|
+
url: '/api/reporter',
|
|
186
|
+
data: runParams,
|
|
192
187
|
maxContentLength: Infinity,
|
|
193
|
-
|
|
188
|
+
responseType: 'json'
|
|
194
189
|
});
|
|
195
190
|
this.runId = resp.data.uid;
|
|
196
191
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
@@ -206,6 +201,7 @@ class TestomatioPipe {
|
|
|
206
201
|
}
|
|
207
202
|
catch (err) {
|
|
208
203
|
const errorText = err.response?.data?.message || err.message;
|
|
204
|
+
debug('Error creating run', err);
|
|
209
205
|
console.log(errorText || err);
|
|
210
206
|
if (!this.apiKey)
|
|
211
207
|
console.error('Testomat.io API key is not set');
|
|
@@ -246,7 +242,15 @@ class TestomatioPipe {
|
|
|
246
242
|
}
|
|
247
243
|
const json = json_cycle_1.default.stringify(data);
|
|
248
244
|
debug('Adding test', json);
|
|
249
|
-
return this.
|
|
245
|
+
return this.client.request({
|
|
246
|
+
method: 'POST',
|
|
247
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
248
|
+
data: json,
|
|
249
|
+
headers: {
|
|
250
|
+
'Content-Type': 'application/json',
|
|
251
|
+
},
|
|
252
|
+
maxContentLength: Infinity
|
|
253
|
+
}).catch(err => {
|
|
250
254
|
this.requestFailures++;
|
|
251
255
|
this.notReportedTestsCount++;
|
|
252
256
|
if (err.response) {
|
|
@@ -291,9 +295,19 @@ class TestomatioPipe {
|
|
|
291
295
|
// get tests from batch and clear batch
|
|
292
296
|
const testsToSend = this.batch.tests.splice(0);
|
|
293
297
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
294
|
-
return this.
|
|
295
|
-
|
|
296
|
-
|
|
298
|
+
return this.client.request({
|
|
299
|
+
method: 'POST',
|
|
300
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
301
|
+
data: {
|
|
302
|
+
api_key: this.apiKey,
|
|
303
|
+
tests: testsToSend,
|
|
304
|
+
batch_index: this.batch.batchIndex
|
|
305
|
+
},
|
|
306
|
+
headers: {
|
|
307
|
+
'Content-Type': 'application/json',
|
|
308
|
+
},
|
|
309
|
+
maxContentLength: Infinity
|
|
310
|
+
}).catch(err => {
|
|
297
311
|
this.requestFailures++;
|
|
298
312
|
this.notReportedTestsCount += testsToSend.length;
|
|
299
313
|
if (err.response) {
|
|
@@ -317,6 +331,7 @@ class TestomatioPipe {
|
|
|
317
331
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
318
332
|
*/
|
|
319
333
|
addTest(data) {
|
|
334
|
+
this.isEnabled = this.apiKey ?? this.isEnabled;
|
|
320
335
|
if (!this.isEnabled)
|
|
321
336
|
return;
|
|
322
337
|
if (!this.runId)
|
|
@@ -326,13 +341,16 @@ class TestomatioPipe {
|
|
|
326
341
|
data.rid = `${this.runId}-${data.rid}`;
|
|
327
342
|
data.api_key = this.apiKey;
|
|
328
343
|
data.create = this.createNewTests;
|
|
344
|
+
let uploading = null;
|
|
329
345
|
if (!this.batch.isEnabled)
|
|
330
|
-
this.#uploadSingleTest(data);
|
|
346
|
+
uploading = this.#uploadSingleTest(data);
|
|
331
347
|
else
|
|
332
348
|
this.batch.tests.push(data);
|
|
333
349
|
// if test is added after run which is already finished
|
|
334
350
|
if (!this.batch.intervalFunction)
|
|
335
|
-
this.#batchUpload();
|
|
351
|
+
uploading = this.#batchUpload();
|
|
352
|
+
// return promise to be able to wait for it
|
|
353
|
+
return uploading;
|
|
336
354
|
}
|
|
337
355
|
/**
|
|
338
356
|
* @param {import('../../types/types.js').RunData} params
|
|
@@ -367,12 +385,16 @@ class TestomatioPipe {
|
|
|
367
385
|
status_event += '_parallel';
|
|
368
386
|
try {
|
|
369
387
|
if (this.runId && !this.proceed) {
|
|
370
|
-
await this.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
await this.client.request({
|
|
389
|
+
method: 'PUT',
|
|
390
|
+
url: `/api/reporter/${this.runId}`,
|
|
391
|
+
data: {
|
|
392
|
+
api_key: this.apiKey,
|
|
393
|
+
duration: params.duration,
|
|
394
|
+
status_event,
|
|
395
|
+
detach: params.detach,
|
|
396
|
+
tests: params.tests,
|
|
397
|
+
}
|
|
376
398
|
});
|
|
377
399
|
if (this.runUrl) {
|
|
378
400
|
console.log(constants_js_1.APP_PREFIX, '📊 Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
|
|
@@ -388,17 +410,13 @@ class TestomatioPipe {
|
|
|
388
410
|
}
|
|
389
411
|
if (this.hasUnmatchedTests) {
|
|
390
412
|
console.log('');
|
|
391
|
-
// eslint-disable-next-line max-len
|
|
392
413
|
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(picocolors_1.default.bold('⚠️ Some reported tests were not found in Testomat.io project')));
|
|
393
|
-
// eslint-disable-next-line max-len
|
|
394
414
|
console.log(constants_js_1.APP_PREFIX, `If you use Testomat.io as a reporter only, please re-run tests using ${picocolors_1.default.bold('TESTOMATIO_CREATE=1')}`);
|
|
395
|
-
// eslint-disable-next-line max-len
|
|
396
415
|
console.log(constants_js_1.APP_PREFIX, `But to keep your tests consistent it is recommended to ${picocolors_1.default.bold('import tests first')}`);
|
|
397
416
|
console.log(constants_js_1.APP_PREFIX, 'If tests were imported but still not matched, assign test IDs to your tests.');
|
|
398
417
|
console.log(constants_js_1.APP_PREFIX, 'You can do that automatically via command line tools:');
|
|
399
418
|
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
|
|
400
419
|
console.log(constants_js_1.APP_PREFIX, 'or for Cucumber:');
|
|
401
|
-
// eslint-disable-next-line max-len
|
|
402
420
|
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.bold('npx check-cucumber ... --update-ids'), 'See: https://bit.ly/bdd-update-ids');
|
|
403
421
|
}
|
|
404
422
|
}
|
|
@@ -420,24 +438,16 @@ function printCreateIssue(err) {
|
|
|
420
438
|
process.on('exit', () => {
|
|
421
439
|
console.log();
|
|
422
440
|
console.log(constants_js_1.APP_PREFIX, 'There was an error reporting to Testomat.io:');
|
|
423
|
-
console.log(constants_js_1.APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new');
|
|
441
|
+
console.log(constants_js_1.APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new');
|
|
424
442
|
console.log(constants_js_1.APP_PREFIX, 'Provide this information:');
|
|
425
443
|
console.log('Error:', err.message || err.code);
|
|
426
444
|
if (!err.config)
|
|
427
445
|
return;
|
|
428
446
|
const time = new Date().toUTCString();
|
|
429
|
-
const {
|
|
447
|
+
const { body, url, baseURL, method } = err?.config || {};
|
|
430
448
|
console.log('```js');
|
|
431
|
-
console.log({
|
|
449
|
+
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
432
450
|
console.log('```');
|
|
433
451
|
});
|
|
434
452
|
}
|
|
435
|
-
const axiosAddTestrunRequestConfig = {
|
|
436
|
-
maxContentLength: Infinity,
|
|
437
|
-
maxBodyLength: Infinity,
|
|
438
|
-
headers: {
|
|
439
|
-
// Overwrite Axios's automatically set Content-Type
|
|
440
|
-
'Content-Type': 'application/json',
|
|
441
|
-
},
|
|
442
|
-
};
|
|
443
453
|
module.exports = TestomatioPipe;
|
package/lib/replay.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class Replay {
|
|
2
|
+
constructor(options?: {});
|
|
3
|
+
apiKey: any;
|
|
4
|
+
dryRun: any;
|
|
5
|
+
onProgress: any;
|
|
6
|
+
onLog: any;
|
|
7
|
+
onError: any;
|
|
8
|
+
/**
|
|
9
|
+
* Get the default debug file path
|
|
10
|
+
* @returns {string} Path to the latest debug file
|
|
11
|
+
*/
|
|
12
|
+
getDefaultDebugFile(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a debug file and extract test data
|
|
15
|
+
* @param {string} debugFile - Path to the debug file
|
|
16
|
+
* @returns {Object} Parsed debug data
|
|
17
|
+
*/
|
|
18
|
+
parseDebugFile(debugFile: string): any;
|
|
19
|
+
/**
|
|
20
|
+
* Restore environment variables from debug data
|
|
21
|
+
* @param {Object} envVars - Environment variables to restore
|
|
22
|
+
*/
|
|
23
|
+
restoreEnvironmentVariables(envVars: any): void;
|
|
24
|
+
/**
|
|
25
|
+
* Replay test data to Testomat.io
|
|
26
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
27
|
+
* @returns {Promise<Object>} Replay results
|
|
28
|
+
*/
|
|
29
|
+
replay(debugFile: string): Promise<any>;
|
|
30
|
+
}
|
|
31
|
+
export default Replay;
|
package/lib/replay.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Replay = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const client_js_1 = __importDefault(require("./client.js"));
|
|
11
|
+
const constants_js_1 = require("./constants.js");
|
|
12
|
+
const config_js_1 = require("./config.js");
|
|
13
|
+
class Replay {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.apiKey = options.apiKey || config_js_1.config.TESTOMATIO || undefined;
|
|
16
|
+
this.dryRun = options.dryRun || false;
|
|
17
|
+
this.onProgress = options.onProgress || (() => { });
|
|
18
|
+
this.onLog = options.onLog || console.log;
|
|
19
|
+
this.onError = options.onError || console.error;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the default debug file path
|
|
23
|
+
* @returns {string} Path to the latest debug file
|
|
24
|
+
*/
|
|
25
|
+
getDefaultDebugFile() {
|
|
26
|
+
return path_1.default.join(os_1.default.tmpdir(), 'testomatio.debug.latest.json');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a debug file and extract test data
|
|
30
|
+
* @param {string} debugFile - Path to the debug file
|
|
31
|
+
* @returns {Object} Parsed debug data
|
|
32
|
+
*/
|
|
33
|
+
parseDebugFile(debugFile) {
|
|
34
|
+
if (!fs_1.default.existsSync(debugFile)) {
|
|
35
|
+
throw new Error(`Debug file not found: ${debugFile}`);
|
|
36
|
+
}
|
|
37
|
+
const fileContent = fs_1.default.readFileSync(debugFile, 'utf-8');
|
|
38
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
|
|
39
|
+
if (lines.length === 0) {
|
|
40
|
+
throw new Error('Debug file is empty');
|
|
41
|
+
}
|
|
42
|
+
let runParams = {};
|
|
43
|
+
let finishParams = {};
|
|
44
|
+
let parseErrors = 0;
|
|
45
|
+
const testsMap = new Map(); // Use Map to deduplicate by rid
|
|
46
|
+
const testsWithoutRid = []; // For tests without rid (backward compatibility)
|
|
47
|
+
const envVars = {};
|
|
48
|
+
let runId = null;
|
|
49
|
+
// Parse debug file line by line
|
|
50
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
51
|
+
try {
|
|
52
|
+
const logEntry = JSON.parse(line);
|
|
53
|
+
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
54
|
+
Object.assign(envVars, logEntry.testomatioEnvVars);
|
|
55
|
+
}
|
|
56
|
+
else if (logEntry.action === 'createRun') {
|
|
57
|
+
runParams = logEntry.params || {};
|
|
58
|
+
}
|
|
59
|
+
else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
60
|
+
// Extract runId if available
|
|
61
|
+
if (logEntry.runId && !runId) {
|
|
62
|
+
runId = logEntry.runId;
|
|
63
|
+
}
|
|
64
|
+
// Process each test in the batch
|
|
65
|
+
for (const test of logEntry.tests) {
|
|
66
|
+
if (test.rid) {
|
|
67
|
+
// Handle tests with rid (deduplicate)
|
|
68
|
+
const existingTest = testsMap.get(test.rid);
|
|
69
|
+
if (existingTest) {
|
|
70
|
+
// Merge test data - prioritize non-null/non-empty values
|
|
71
|
+
const mergedTest = { ...existingTest };
|
|
72
|
+
Object.keys(test).forEach(key => {
|
|
73
|
+
if (test[key] !== null && test[key] !== undefined) {
|
|
74
|
+
if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
75
|
+
// Merge files arrays
|
|
76
|
+
mergedTest.files = [...(existingTest.files || []), ...test[key]];
|
|
77
|
+
}
|
|
78
|
+
else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
79
|
+
// Merge artifacts arrays
|
|
80
|
+
mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
|
|
81
|
+
}
|
|
82
|
+
else if (existingTest[key] === null || existingTest[key] === undefined ||
|
|
83
|
+
(Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
|
|
84
|
+
// Use new value if existing is null/undefined/empty array
|
|
85
|
+
mergedTest[key] = test[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
testsMap.set(test.rid, mergedTest);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
testsMap.set(test.rid, { ...test });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Handle tests without rid (no deduplication)
|
|
97
|
+
testsWithoutRid.push({ ...test });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
102
|
+
// Extract runId if available
|
|
103
|
+
if (logEntry.runId && !runId) {
|
|
104
|
+
runId = logEntry.runId;
|
|
105
|
+
}
|
|
106
|
+
const test = logEntry.testId;
|
|
107
|
+
if (test.rid) {
|
|
108
|
+
// Handle tests with rid (deduplicate)
|
|
109
|
+
const existingTest = testsMap.get(test.rid);
|
|
110
|
+
if (existingTest) {
|
|
111
|
+
// Merge with existing test
|
|
112
|
+
const mergedTest = { ...existingTest, ...test };
|
|
113
|
+
testsMap.set(test.rid, mergedTest);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
testsMap.set(test.rid, { ...test });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Handle tests without rid (no deduplication)
|
|
121
|
+
testsWithoutRid.push({ ...test });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (logEntry.actions === 'finishRun') {
|
|
125
|
+
finishParams = logEntry.params || {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
parseErrors++;
|
|
130
|
+
if (parseErrors <= 3) {
|
|
131
|
+
// Only show first 3 parse errors
|
|
132
|
+
this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (parseErrors > 3) {
|
|
137
|
+
this.onError(`${parseErrors - 3} more parse errors occurred`);
|
|
138
|
+
}
|
|
139
|
+
// Combine tests with rid and tests without rid
|
|
140
|
+
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
|
|
141
|
+
return {
|
|
142
|
+
runParams,
|
|
143
|
+
finishParams,
|
|
144
|
+
tests: allTests,
|
|
145
|
+
envVars,
|
|
146
|
+
parseErrors,
|
|
147
|
+
totalLines: lines.length,
|
|
148
|
+
runId
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Restore environment variables from debug data
|
|
153
|
+
* @param {Object} envVars - Environment variables to restore
|
|
154
|
+
*/
|
|
155
|
+
restoreEnvironmentVariables(envVars) {
|
|
156
|
+
// Only restore env vars that aren't already set (don't override current values)
|
|
157
|
+
Object.keys(envVars).forEach(key => {
|
|
158
|
+
if (process.env[key] === undefined || process.env[key] === '') {
|
|
159
|
+
process.env[key] = envVars[key];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Replay test data to Testomat.io
|
|
165
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
166
|
+
* @returns {Promise<Object>} Replay results
|
|
167
|
+
*/
|
|
168
|
+
async replay(debugFile) {
|
|
169
|
+
if (!debugFile) {
|
|
170
|
+
debugFile = this.getDefaultDebugFile();
|
|
171
|
+
}
|
|
172
|
+
if (!this.apiKey) {
|
|
173
|
+
throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
174
|
+
}
|
|
175
|
+
this.onLog(`Replaying data from debug file: ${debugFile}`);
|
|
176
|
+
// Parse the debug file
|
|
177
|
+
const debugData = this.parseDebugFile(debugFile);
|
|
178
|
+
const { runParams, finishParams, tests, envVars, runId } = debugData;
|
|
179
|
+
this.onLog(`Found ${tests.length} tests to replay`);
|
|
180
|
+
if (tests.length === 0) {
|
|
181
|
+
throw new Error('No test data found in debug file');
|
|
182
|
+
}
|
|
183
|
+
// Restore environment variables
|
|
184
|
+
this.restoreEnvironmentVariables(envVars);
|
|
185
|
+
if (this.dryRun) {
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
testsCount: tests.length,
|
|
189
|
+
runParams,
|
|
190
|
+
finishParams,
|
|
191
|
+
envVars,
|
|
192
|
+
runId,
|
|
193
|
+
dryRun: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Create client and restore the run
|
|
197
|
+
const client = new client_js_1.default({
|
|
198
|
+
apiKey: this.apiKey,
|
|
199
|
+
isBatchEnabled: true,
|
|
200
|
+
...runParams,
|
|
201
|
+
});
|
|
202
|
+
// Use the stored runId if available, otherwise create a new run
|
|
203
|
+
if (runId) {
|
|
204
|
+
this.onLog(`Using existing run ID: ${runId}`);
|
|
205
|
+
client.runId = runId;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.onLog('Publishing to run...');
|
|
209
|
+
await client.createRun(runParams);
|
|
210
|
+
}
|
|
211
|
+
// Send each test result
|
|
212
|
+
let successCount = 0;
|
|
213
|
+
let failureCount = 0;
|
|
214
|
+
for (const [index, test] of tests.entries()) {
|
|
215
|
+
try {
|
|
216
|
+
await client.addTestRun(test.status, test);
|
|
217
|
+
successCount++;
|
|
218
|
+
this.onProgress({
|
|
219
|
+
current: index + 1,
|
|
220
|
+
total: tests.length,
|
|
221
|
+
test,
|
|
222
|
+
success: true
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
failureCount++;
|
|
227
|
+
this.onError(`Failed to send test ${index + 1}: ${err.message}`);
|
|
228
|
+
this.onProgress({
|
|
229
|
+
current: index + 1,
|
|
230
|
+
total: tests.length,
|
|
231
|
+
test,
|
|
232
|
+
success: false,
|
|
233
|
+
error: err.message
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED, finishParams.parallel || false);
|
|
238
|
+
const result = {
|
|
239
|
+
success: true,
|
|
240
|
+
testsCount: tests.length,
|
|
241
|
+
successCount,
|
|
242
|
+
failureCount,
|
|
243
|
+
runParams,
|
|
244
|
+
finishParams,
|
|
245
|
+
envVars,
|
|
246
|
+
runId: runId || client.runId
|
|
247
|
+
};
|
|
248
|
+
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.Replay = Replay;
|
|
253
|
+
module.exports = Replay;
|
|
254
|
+
|
|
255
|
+
module.exports.Replay = Replay;
|
|
@@ -3,6 +3,7 @@ declare namespace _default {
|
|
|
3
3
|
export { logMessage as log };
|
|
4
4
|
export { addStep as step };
|
|
5
5
|
export { setKeyValue as keyValue };
|
|
6
|
+
export { setLabel as label };
|
|
6
7
|
}
|
|
7
8
|
export default _default;
|
|
8
9
|
/**
|
|
@@ -32,3 +33,9 @@ declare function addStep(message: string): void;
|
|
|
32
33
|
declare function setKeyValue(keyValue: {
|
|
33
34
|
[key: string]: string;
|
|
34
35
|
} | string, value?: string | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* Add a single label to the test report
|
|
38
|
+
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
39
|
+
* @param {string} [value] - optional label value (e.g. 'high', 'login')
|
|
40
|
+
*/
|
|
41
|
+
declare function setLabel(key: string, value?: string): void;
|
|
@@ -44,9 +44,45 @@ function setKeyValue(keyValue, value = null) {
|
|
|
44
44
|
}
|
|
45
45
|
index_js_1.services.keyValues.put(keyValue);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Add a single label to the test report
|
|
49
|
+
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
50
|
+
* @param {string} [value] - optional label value (e.g. 'high', 'login')
|
|
51
|
+
*/
|
|
52
|
+
function setLabel(key, value = null) {
|
|
53
|
+
if (!key || typeof key !== 'string') {
|
|
54
|
+
console.warn('Label key must be a non-empty string');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Limit key length to 255 characters
|
|
58
|
+
if (key.length > 255) {
|
|
59
|
+
console.warn('Label key is too long, trimmed to 255 characters:', key);
|
|
60
|
+
key = key.substring(0, 255);
|
|
61
|
+
}
|
|
62
|
+
let labelString = key;
|
|
63
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
64
|
+
if (typeof value !== 'string') {
|
|
65
|
+
console.warn('Label value must be a string, converting:', value);
|
|
66
|
+
value = String(value);
|
|
67
|
+
}
|
|
68
|
+
// Limit value length to 255 characters
|
|
69
|
+
if (value.length > 255) {
|
|
70
|
+
console.warn('Label value is too long, trimmed to 255 characters:', value);
|
|
71
|
+
value = value.substring(0, 255);
|
|
72
|
+
}
|
|
73
|
+
labelString = `${key}:${value}`;
|
|
74
|
+
}
|
|
75
|
+
// Limit total label length to 255 characters
|
|
76
|
+
if (labelString.length > 255) {
|
|
77
|
+
console.warn('Label is too long, trimmed to 255 characters:', labelString);
|
|
78
|
+
labelString = labelString.substring(0, 255);
|
|
79
|
+
}
|
|
80
|
+
index_js_1.services.labels.put([labelString]);
|
|
81
|
+
}
|
|
47
82
|
module.exports = {
|
|
48
83
|
artifact: saveArtifact,
|
|
49
84
|
log: logMessage,
|
|
50
85
|
step: addStep,
|
|
51
86
|
keyValue: setKeyValue,
|
|
87
|
+
label: setLabel,
|
|
52
88
|
};
|