@testomatio/reporter 2.6.4-beta.1 → 2.7.1
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/lib/adapter/codecept.js +1 -1
- package/lib/adapter/mocha.js +34 -1
- package/lib/adapter/webdriver.d.ts +8 -1
- package/lib/adapter/webdriver.js +42 -6
- package/lib/bin/startTest.js +2 -1
- package/lib/pipe/bitbucket.d.ts +1 -0
- package/lib/pipe/bitbucket.js +4 -0
- package/lib/pipe/coverage.d.ts +1 -0
- package/lib/pipe/coverage.js +4 -0
- package/lib/pipe/csv.d.ts +1 -0
- package/lib/pipe/csv.js +3 -0
- package/lib/pipe/debug.d.ts +1 -0
- package/lib/pipe/debug.js +6 -1
- package/lib/pipe/github.d.ts +1 -0
- package/lib/pipe/github.js +4 -0
- package/lib/pipe/gitlab.d.ts +1 -0
- package/lib/pipe/gitlab.js +4 -0
- package/lib/pipe/html.d.ts +1 -0
- package/lib/pipe/html.js +4 -0
- package/lib/pipe/testomatio.d.ts +5 -0
- package/lib/pipe/testomatio.js +26 -0
- package/lib/xmlReader.d.ts +1 -0
- package/lib/xmlReader.js +78 -6
- package/package.json +2 -2
- package/src/adapter/codecept.js +1 -1
- package/src/adapter/mocha.js +41 -1
- package/src/adapter/webdriver.js +55 -7
- package/src/bin/startTest.js +3 -2
- package/src/pipe/bitbucket.js +5 -0
- package/src/pipe/coverage.js +5 -0
- package/src/pipe/csv.js +4 -0
- package/src/pipe/debug.js +6 -1
- package/src/pipe/github.js +5 -0
- package/src/pipe/gitlab.js +5 -0
- package/src/pipe/html.js +5 -0
- package/src/pipe/testomatio.js +25 -0
- package/src/xmlReader.js +98 -7
- package/types/types.d.ts +3 -0
package/lib/adapter/codecept.js
CHANGED
|
@@ -115,7 +115,7 @@ function CodeceptReporter(config) {
|
|
|
115
115
|
});
|
|
116
116
|
// mark as failed all tests inside the failed hook
|
|
117
117
|
event.dispatcher.on(event.hook.failed, hook => {
|
|
118
|
-
if (hook.name !== 'BeforeSuiteHook')
|
|
118
|
+
if (hook.name !== 'BeforeSuiteHook' && hook.name !== 'BeforeHook')
|
|
119
119
|
return;
|
|
120
120
|
const suite = hook.runnable.parent;
|
|
121
121
|
if (!suite)
|
package/lib/adapter/mocha.js
CHANGED
|
@@ -19,6 +19,8 @@ function MochaReporter(runner, opts) {
|
|
|
19
19
|
// let artifactStore;
|
|
20
20
|
const apiKey = opts?.reporterOptions?.apiKey || config_js_1.config.TESTOMATIO;
|
|
21
21
|
const client = new client_js_1.default({ apiKey });
|
|
22
|
+
// Track hook failures
|
|
23
|
+
const hookFailures = new Map();
|
|
22
24
|
runner.on(EVENT_RUN_BEGIN, () => {
|
|
23
25
|
client.createRun();
|
|
24
26
|
// clear dir with artifacts/logs
|
|
@@ -27,7 +29,30 @@ function MochaReporter(runner, opts) {
|
|
|
27
29
|
runner.on(EVENT_SUITE_BEGIN, async (suite) => {
|
|
28
30
|
index_js_1.services.setContext(suite.fullTitle());
|
|
29
31
|
});
|
|
30
|
-
runner.on(EVENT_SUITE_END, async () => {
|
|
32
|
+
runner.on(EVENT_SUITE_END, async (suite) => {
|
|
33
|
+
if (hookFailures.has(suite.fullTitle())) {
|
|
34
|
+
const { error, suiteTitle } = hookFailures.get(suite.fullTitle());
|
|
35
|
+
for (const test of suite.tests) {
|
|
36
|
+
if (test.state === 'pending' || !test.state) {
|
|
37
|
+
const testId = (0, utils_js_1.getTestomatIdFromTestTitle)(test.title);
|
|
38
|
+
const artifacts = index_js_1.services.artifacts.get(test.fullTitle());
|
|
39
|
+
const keyValues = index_js_1.services.keyValues.get(test.fullTitle());
|
|
40
|
+
const links = index_js_1.services.links.get(test.fullTitle());
|
|
41
|
+
client.addTestRun(constants_js_1.STATUS.FAILED, {
|
|
42
|
+
error,
|
|
43
|
+
suite_title: suiteTitle || suite.title,
|
|
44
|
+
file: suite.file,
|
|
45
|
+
test_id: testId,
|
|
46
|
+
title: test.title,
|
|
47
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
48
|
+
time: 0,
|
|
49
|
+
manuallyAttachedArtifacts: artifacts,
|
|
50
|
+
meta: keyValues,
|
|
51
|
+
links,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
index_js_1.services.setContext(null);
|
|
32
57
|
});
|
|
33
58
|
runner.on(EVENT_TEST_BEGIN, async (test) => {
|
|
@@ -79,6 +104,14 @@ function MochaReporter(runner, opts) {
|
|
|
79
104
|
runner.on(EVENT_TEST_FAIL, async (test, err) => {
|
|
80
105
|
failures += 1;
|
|
81
106
|
console.log(picocolors_1.default.bold(picocolors_1.default.red('✖')), test.fullTitle(), picocolors_1.default.gray(err.message));
|
|
107
|
+
const isHookFailure = test.title.includes('before each') || test.title.includes('after each');
|
|
108
|
+
if (isHookFailure && test.parent) {
|
|
109
|
+
hookFailures.set(test.parent.fullTitle(), {
|
|
110
|
+
error: err,
|
|
111
|
+
suiteTitle: getSuiteTitle(test),
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
82
115
|
const testId = (0, utils_js_1.getTestomatIdFromTestTitle)(test.title);
|
|
83
116
|
const logs = getTestLogs(test);
|
|
84
117
|
const artifacts = index_js_1.services.artifacts.get(test.fullTitle());
|
|
@@ -4,15 +4,22 @@ declare class WebdriverReporter extends WDIOReporter {
|
|
|
4
4
|
client: TestomatClient;
|
|
5
5
|
_addTestPromises: any[];
|
|
6
6
|
_isSynchronising: boolean;
|
|
7
|
+
hooksEnhancer: any;
|
|
8
|
+
/**
|
|
9
|
+
* Initialize the hooks enhancer
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
private _initializeHooksEnhancer;
|
|
7
13
|
/**
|
|
8
14
|
*
|
|
9
15
|
* @param {RunnerStats} runData
|
|
10
16
|
*/
|
|
11
17
|
onRunnerEnd(runData: RunnerStats): Promise<void>;
|
|
12
18
|
onRunnerStart(): void;
|
|
19
|
+
onHookEnd(hook: any): void;
|
|
20
|
+
onSuiteEnd(suiteOrScenario: any): Promise<void>;
|
|
13
21
|
onTestStart(test: any): void;
|
|
14
22
|
onTestEnd(test: any): void;
|
|
15
|
-
onSuiteEnd(scerario: any): void;
|
|
16
23
|
addTest(test: any): Promise<void>;
|
|
17
24
|
/**
|
|
18
25
|
* @param {import('../../types/types.js').WebdriverIOScenario} scenario
|
package/lib/adapter/webdriver.js
CHANGED
|
@@ -49,6 +49,11 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
49
49
|
options = Object.assign(options, { stdout: true });
|
|
50
50
|
this._addTestPromises = [];
|
|
51
51
|
this._isSynchronising = false;
|
|
52
|
+
// Optional hooks enhancer for beforeEach failure handling
|
|
53
|
+
this.hooksEnhancer = null;
|
|
54
|
+
if (options?.enableHooksEnhancer) {
|
|
55
|
+
this._initializeHooksEnhancer();
|
|
56
|
+
}
|
|
52
57
|
// run is created by cli, if enabling the row below, it mat lead to multiple runs being created
|
|
53
58
|
// thus, need to check if process.env.runId is set and/or add more checks to avoid creating multiple runs
|
|
54
59
|
// this.client.createRun();
|
|
@@ -56,6 +61,27 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
56
61
|
get isSynchronised() {
|
|
57
62
|
return this._isSynchronising === false;
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Initialize the hooks enhancer
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
async _initializeHooksEnhancer() {
|
|
69
|
+
try {
|
|
70
|
+
// Dynamic import to avoid hard dependency
|
|
71
|
+
// Resolve package path from the project's node_modules
|
|
72
|
+
const { createRequire } = await Promise.resolve().then(() => __importStar(require('module')));
|
|
73
|
+
const projectRequire = createRequire(process.cwd() + '/package.json');
|
|
74
|
+
// Import the hooks enhancer package
|
|
75
|
+
const packagePath = projectRequire.resolve('@testomatio/webdriver-hooks-enhancer');
|
|
76
|
+
const hooksEnhancerModule = await Promise.resolve(`${packagePath}`).then(s => __importStar(require(s)));
|
|
77
|
+
const { createHooksEnhancer } = hooksEnhancerModule;
|
|
78
|
+
this.hooksEnhancer = createHooksEnhancer(this);
|
|
79
|
+
console.log('[TESTOMATIO] WebdriverIO Hooks Enhancer enabled');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.warn('[TESTOMATIO] Could not enable WebdriverIO Hooks Enhancer.', 'Install @testomatio/webdriver-hooks-enhancer to use this feature:', error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
59
85
|
/**
|
|
60
86
|
*
|
|
61
87
|
* @param {RunnerStats} runData
|
|
@@ -72,6 +98,22 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
72
98
|
// clear dir with artifacts/logs
|
|
73
99
|
utils_js_1.fileSystem.clearDir(constants_js_1.TESTOMAT_TMP_STORAGE_DIR);
|
|
74
100
|
}
|
|
101
|
+
onHookEnd(hook) {
|
|
102
|
+
// Hooks enhancer will handle this if enabled
|
|
103
|
+
if (this.hooksEnhancer) {
|
|
104
|
+
this.hooksEnhancer.trackHookFailure(hook);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async onSuiteEnd(suiteOrScenario) {
|
|
108
|
+
// Handle hook failures for regular suites using enhancer
|
|
109
|
+
if (this.hooksEnhancer && suiteOrScenario.type !== 'scenario') {
|
|
110
|
+
await this.hooksEnhancer.handleSuiteEnd(suiteOrScenario, this.client, utils_js_1.getTestomatIdFromTestTitle);
|
|
111
|
+
}
|
|
112
|
+
// Handle BDD scenarios (cucumber)
|
|
113
|
+
if (suiteOrScenario.type === 'scenario') {
|
|
114
|
+
this._addTestPromises.push(this.addBddScenario(suiteOrScenario));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
75
117
|
onTestStart(test) {
|
|
76
118
|
index_js_1.services.setContext(test.fullTitle);
|
|
77
119
|
}
|
|
@@ -84,12 +126,6 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
84
126
|
test.logs = logs;
|
|
85
127
|
this._addTestPromises.push(this.addTest(test));
|
|
86
128
|
}
|
|
87
|
-
// wdio-cucumber does not trigger onTestEnd hook, thus, using this one
|
|
88
|
-
onSuiteEnd(scerario) {
|
|
89
|
-
if (scerario.type === 'scenario') {
|
|
90
|
-
this._addTestPromises.push(this.addBddScenario(scerario));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
129
|
async addTest(test) {
|
|
94
130
|
if (!this.client)
|
|
95
131
|
return;
|
package/lib/bin/startTest.js
CHANGED
|
@@ -6,9 +6,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const node_child_process_1 = require("node:child_process");
|
|
8
8
|
const node_path_1 = require("node:path");
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
9
10
|
const utils_js_1 = require("../utils/utils.js");
|
|
10
11
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
11
|
-
//
|
|
12
|
+
// @ts-ignore
|
|
12
13
|
const cliPath = (0, node_path_1.join)(__dirname, 'cli.js');
|
|
13
14
|
const version = (0, utils_js_1.getPackageVersion)();
|
|
14
15
|
console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
package/lib/pipe/bitbucket.d.ts
CHANGED
package/lib/pipe/bitbucket.js
CHANGED
package/lib/pipe/coverage.d.ts
CHANGED
package/lib/pipe/coverage.js
CHANGED
|
@@ -162,6 +162,10 @@ class CoveragePipe {
|
|
|
162
162
|
async createRun() { }
|
|
163
163
|
updateRun() { }
|
|
164
164
|
async finishRun(runParams) { }
|
|
165
|
+
async sync() {
|
|
166
|
+
// CoveragePipe doesn't buffer tests, so sync is a no-op
|
|
167
|
+
// Reserved for future use if needed
|
|
168
|
+
}
|
|
165
169
|
toString() {
|
|
166
170
|
return 'Coverage Reporter';
|
|
167
171
|
}
|
package/lib/pipe/csv.d.ts
CHANGED
package/lib/pipe/csv.js
CHANGED
|
@@ -113,6 +113,9 @@ class CsvPipe {
|
|
|
113
113
|
return;
|
|
114
114
|
if (runParams.tests)
|
|
115
115
|
runParams.tests.forEach(t => this.addTest(t));
|
|
116
|
+
await this.sync();
|
|
117
|
+
}
|
|
118
|
+
async sync() {
|
|
116
119
|
// Save results based on the default headers
|
|
117
120
|
if (this.isEnabled) {
|
|
118
121
|
await this.saveToCsv(this.results, constants_js_1.CSV_HEADERS);
|
package/lib/pipe/debug.d.ts
CHANGED
package/lib/pipe/debug.js
CHANGED
|
@@ -114,12 +114,17 @@ class DebugPipe {
|
|
|
114
114
|
async finishRun(params) {
|
|
115
115
|
if (!this.isEnabled)
|
|
116
116
|
return;
|
|
117
|
-
await this.
|
|
117
|
+
await this.sync();
|
|
118
118
|
if (this.batch.intervalFunction)
|
|
119
119
|
clearInterval(this.batch.intervalFunction);
|
|
120
120
|
this.logToFile({ action: 'finishRun', params });
|
|
121
121
|
console.log(constants_js_1.APP_PREFIX, '🪲 Debug Saved to', this.logFilePath);
|
|
122
122
|
}
|
|
123
|
+
async sync() {
|
|
124
|
+
if (!this.isEnabled)
|
|
125
|
+
return;
|
|
126
|
+
await this.batchUpload();
|
|
127
|
+
}
|
|
123
128
|
toString() {
|
|
124
129
|
return 'Debug Reporter';
|
|
125
130
|
}
|
package/lib/pipe/github.d.ts
CHANGED
|
@@ -26,5 +26,6 @@ declare class GitHubPipe implements Pipe {
|
|
|
26
26
|
octokit: import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types.js").RestEndpointMethods & import("@octokit/plugin-rest-endpoint-methods").Api & {
|
|
27
27
|
paginate: import("@octokit/plugin-paginate-rest").PaginateInterface;
|
|
28
28
|
};
|
|
29
|
+
sync(): Promise<void>;
|
|
29
30
|
toString(): string;
|
|
30
31
|
}
|
package/lib/pipe/github.js
CHANGED
|
@@ -203,6 +203,10 @@ class GitHubPipe {
|
|
|
203
203
|
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('GitHub'), `Couldn't create GitHub report ${err}`);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
async sync() {
|
|
207
|
+
// GitHubPipe doesn't buffer tests, so sync is a no-op
|
|
208
|
+
// Reserved for future use if needed
|
|
209
|
+
}
|
|
206
210
|
toString() {
|
|
207
211
|
return 'GitHub Reporter';
|
|
208
212
|
}
|
package/lib/pipe/gitlab.d.ts
CHANGED
package/lib/pipe/gitlab.js
CHANGED
package/lib/pipe/html.d.ts
CHANGED
package/lib/pipe/html.js
CHANGED
package/lib/pipe/testomatio.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ declare class TestomatioPipe implements Pipe {
|
|
|
60
60
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
61
61
|
*/
|
|
62
62
|
addTest(data: any): Promise<void | import("gaxios").GaxiosResponse<any>>;
|
|
63
|
+
/**
|
|
64
|
+
* Syncs / flushes buffered tests by uploading them as a batch
|
|
65
|
+
* This is used to manually trigger batch upload (e.g., after all tests are added)
|
|
66
|
+
*/
|
|
67
|
+
sync(): Promise<void>;
|
|
63
68
|
/**
|
|
64
69
|
* @param {import('../../types/types.js').RunData} params
|
|
65
70
|
* @returns
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -264,6 +264,8 @@ class TestomatioPipe {
|
|
|
264
264
|
const errorText = err.response?.data?.message || err.message;
|
|
265
265
|
debug('Error creating run', err);
|
|
266
266
|
console.log(errorText || err);
|
|
267
|
+
if (err.response?.status === 403)
|
|
268
|
+
this.#disablePipe();
|
|
267
269
|
if (!this.apiKey)
|
|
268
270
|
console.error('Testomat.io API key is not set');
|
|
269
271
|
if (!this.apiKey?.startsWith('tstmt'))
|
|
@@ -312,6 +314,8 @@ class TestomatioPipe {
|
|
|
312
314
|
maxContentLength: Infinity,
|
|
313
315
|
})
|
|
314
316
|
.catch(err => {
|
|
317
|
+
if (err.response?.status === 403)
|
|
318
|
+
this.#disablePipe();
|
|
315
319
|
this.requestFailures++;
|
|
316
320
|
this.notReportedTestsCount++;
|
|
317
321
|
if (err.response) {
|
|
@@ -363,6 +367,8 @@ class TestomatioPipe {
|
|
|
363
367
|
maxContentLength: Infinity,
|
|
364
368
|
})
|
|
365
369
|
.catch(err => {
|
|
370
|
+
if (err.response?.status === 403)
|
|
371
|
+
this.#disablePipe();
|
|
366
372
|
this.requestFailures++;
|
|
367
373
|
this.notReportedTestsCount += testsToSend.length;
|
|
368
374
|
if (err.response) {
|
|
@@ -398,6 +404,15 @@ class TestomatioPipe {
|
|
|
398
404
|
// return promise to be able to wait for it
|
|
399
405
|
return uploading;
|
|
400
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Syncs / flushes buffered tests by uploading them as a batch
|
|
409
|
+
* This is used to manually trigger batch upload (e.g., after all tests are added)
|
|
410
|
+
*/
|
|
411
|
+
async sync() {
|
|
412
|
+
if (!this.isEnabled)
|
|
413
|
+
return;
|
|
414
|
+
await this.#batchUpload();
|
|
415
|
+
}
|
|
401
416
|
/**
|
|
402
417
|
* @param {import('../../types/types.js').RunData} params
|
|
403
418
|
* @returns
|
|
@@ -472,6 +487,17 @@ class TestomatioPipe {
|
|
|
472
487
|
}
|
|
473
488
|
debug('Run finished');
|
|
474
489
|
}
|
|
490
|
+
#disablePipe() {
|
|
491
|
+
this.isEnabled = false;
|
|
492
|
+
this.apiKey = null;
|
|
493
|
+
// clear interval function, otherwise the proccess will continue indefinitely
|
|
494
|
+
if (this.batch.intervalFunction) {
|
|
495
|
+
clearInterval(this.batch.intervalFunction);
|
|
496
|
+
this.batch.intervalFunction = null;
|
|
497
|
+
this.batch.isEnabled = false;
|
|
498
|
+
}
|
|
499
|
+
this.batch.tests = [];
|
|
500
|
+
}
|
|
475
501
|
#logFailedResponse(error) {
|
|
476
502
|
let responseBody = stringify(error.response?.data ?? error.response ?? error, { pretty: true });
|
|
477
503
|
if (!responseBody)
|
package/lib/xmlReader.d.ts
CHANGED
package/lib/xmlReader.js
CHANGED
|
@@ -452,6 +452,43 @@ class XmlReader {
|
|
|
452
452
|
this.uploader.checkEnabled();
|
|
453
453
|
return run;
|
|
454
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Calculate the approximate size of data in bytes (JSON stringified length)
|
|
457
|
+
* @param {Object} data - Data to measure
|
|
458
|
+
* @returns {number} Size in bytes
|
|
459
|
+
*/
|
|
460
|
+
#getObjectSize(data) {
|
|
461
|
+
const body = JSON.stringify(data);
|
|
462
|
+
return new TextEncoder().encode(body).length;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Split tests array into chunks based on data size
|
|
466
|
+
* @param {Array} tests - Array of tests to split
|
|
467
|
+
* @returns {Array<Array>} Array of test chunks
|
|
468
|
+
*/
|
|
469
|
+
#splitTestsIntoChunks(tests) {
|
|
470
|
+
const maxSizeBytes = 1 * 1024 * 1024;
|
|
471
|
+
const chunks = [];
|
|
472
|
+
let currentChunk = [];
|
|
473
|
+
let currentChunkSize = 0;
|
|
474
|
+
for (const test of tests) {
|
|
475
|
+
const testSize = this.#getObjectSize(test);
|
|
476
|
+
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
477
|
+
if (wouldExceedSize) {
|
|
478
|
+
if (currentChunk.length > 0) {
|
|
479
|
+
chunks.push(currentChunk);
|
|
480
|
+
}
|
|
481
|
+
currentChunk = [];
|
|
482
|
+
currentChunkSize = 0;
|
|
483
|
+
}
|
|
484
|
+
currentChunk.push(test);
|
|
485
|
+
currentChunkSize += testSize;
|
|
486
|
+
}
|
|
487
|
+
if (currentChunk.length > 0) {
|
|
488
|
+
chunks.push(currentChunk);
|
|
489
|
+
}
|
|
490
|
+
return chunks;
|
|
491
|
+
}
|
|
455
492
|
async uploadData() {
|
|
456
493
|
await this.uploadArtifacts();
|
|
457
494
|
this.calculateStats();
|
|
@@ -459,16 +496,51 @@ class XmlReader {
|
|
|
459
496
|
this.fetchSourceCode();
|
|
460
497
|
this.formatErrors();
|
|
461
498
|
this.formatTests();
|
|
462
|
-
|
|
463
|
-
|
|
499
|
+
this.pipes = this.pipes || (await this.pipesPromise);
|
|
500
|
+
// Create run before uploading tests to ensure runId is set
|
|
501
|
+
await this.createRun();
|
|
502
|
+
if (!this.tests || !Array.isArray(this.tests) || this.tests.length === 0) {
|
|
503
|
+
debug('No tests to upload, finishing run');
|
|
504
|
+
const finishData = {
|
|
505
|
+
api_key: this.requestParams.apiKey,
|
|
506
|
+
status: 'finished',
|
|
507
|
+
duration: this.stats.duration,
|
|
508
|
+
detach: this.requestParams.detach,
|
|
509
|
+
};
|
|
510
|
+
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
511
|
+
}
|
|
512
|
+
const testChunks = this.#splitTestsIntoChunks(this.tests);
|
|
513
|
+
const totalChunks = testChunks.length;
|
|
514
|
+
const totalTests = this.tests.length;
|
|
515
|
+
debug(`Split ${totalTests} tests into ${totalChunks} chunks (max 1MB per chunk)`);
|
|
516
|
+
let uploadedTests = 0;
|
|
517
|
+
for (let i = 0; i < testChunks.length; i++) {
|
|
518
|
+
const chunk = testChunks[i];
|
|
519
|
+
const chunkNum = i + 1;
|
|
520
|
+
if (totalChunks > 1) {
|
|
521
|
+
debug(`Uploading chunk ${chunkNum}/${totalChunks} (${chunk.length} tests)`);
|
|
522
|
+
}
|
|
523
|
+
for (const test of chunk) {
|
|
524
|
+
await Promise.all(this.pipes.map(p => p.addTest(test)));
|
|
525
|
+
}
|
|
526
|
+
await Promise.all(this.pipes.map(p => p.sync()));
|
|
527
|
+
uploadedTests += chunk.length;
|
|
528
|
+
debug(`Uploaded ${uploadedTests}/${totalTests} tests`);
|
|
529
|
+
}
|
|
530
|
+
if (totalChunks > 1) {
|
|
531
|
+
console.log(constants_js_1.APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests in ${totalChunks} chunks`);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(constants_js_1.APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests`);
|
|
535
|
+
}
|
|
536
|
+
const finishData = {
|
|
464
537
|
api_key: this.requestParams.apiKey,
|
|
465
538
|
status: 'finished',
|
|
466
539
|
duration: this.stats.duration,
|
|
467
|
-
|
|
540
|
+
detach: this.requestParams.detach,
|
|
468
541
|
};
|
|
469
|
-
debug('
|
|
470
|
-
this.pipes
|
|
471
|
-
return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
|
|
542
|
+
debug('Finishing run with status:', finishData.status);
|
|
543
|
+
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
472
544
|
}
|
|
473
545
|
async _finishRun() {
|
|
474
546
|
this.pipes = this.pipes || (await this.pipesPromise);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"json-cycle": "^1.3.0",
|
|
38
38
|
"lodash.memoize": "^4.1.2",
|
|
39
39
|
"lodash.merge": "^4.6.2",
|
|
40
|
-
"minimatch": "^
|
|
40
|
+
"minimatch": "^10.2.4",
|
|
41
41
|
"picocolors": "^1.0.1",
|
|
42
42
|
"pretty-ms": "^7.0.1",
|
|
43
43
|
"promise-retry": "^2.0.1",
|
package/src/adapter/codecept.js
CHANGED
|
@@ -133,7 +133,7 @@ function CodeceptReporter(config) {
|
|
|
133
133
|
|
|
134
134
|
// mark as failed all tests inside the failed hook
|
|
135
135
|
event.dispatcher.on(event.hook.failed, hook => {
|
|
136
|
-
if (hook.name !== 'BeforeSuiteHook') return;
|
|
136
|
+
if (hook.name !== 'BeforeSuiteHook' && hook.name !== 'BeforeHook') return;
|
|
137
137
|
const suite = hook.runnable.parent;
|
|
138
138
|
|
|
139
139
|
if (!suite) return;
|
package/src/adapter/mocha.js
CHANGED
|
@@ -29,6 +29,9 @@ function MochaReporter(runner, opts) {
|
|
|
29
29
|
|
|
30
30
|
const client = new TestomatClient({ apiKey });
|
|
31
31
|
|
|
32
|
+
// Track hook failures
|
|
33
|
+
const hookFailures = new Map();
|
|
34
|
+
|
|
32
35
|
runner.on(EVENT_RUN_BEGIN, () => {
|
|
33
36
|
client.createRun();
|
|
34
37
|
|
|
@@ -40,7 +43,33 @@ function MochaReporter(runner, opts) {
|
|
|
40
43
|
services.setContext(suite.fullTitle());
|
|
41
44
|
});
|
|
42
45
|
|
|
43
|
-
runner.on(EVENT_SUITE_END, async
|
|
46
|
+
runner.on(EVENT_SUITE_END, async suite => {
|
|
47
|
+
if (hookFailures.has(suite.fullTitle())) {
|
|
48
|
+
const { error, suiteTitle } = hookFailures.get(suite.fullTitle());
|
|
49
|
+
|
|
50
|
+
for (const test of suite.tests) {
|
|
51
|
+
if (test.state === 'pending' || !test.state) {
|
|
52
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
53
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
54
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
55
|
+
const links = services.links.get(test.fullTitle());
|
|
56
|
+
|
|
57
|
+
client.addTestRun(STATUS.FAILED, {
|
|
58
|
+
error,
|
|
59
|
+
suite_title: suiteTitle || suite.title,
|
|
60
|
+
file: suite.file,
|
|
61
|
+
test_id: testId,
|
|
62
|
+
title: test.title,
|
|
63
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
64
|
+
time: 0,
|
|
65
|
+
manuallyAttachedArtifacts: artifacts,
|
|
66
|
+
meta: keyValues,
|
|
67
|
+
links,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
services.setContext(null);
|
|
45
74
|
});
|
|
46
75
|
|
|
@@ -101,6 +130,17 @@ function MochaReporter(runner, opts) {
|
|
|
101
130
|
runner.on(EVENT_TEST_FAIL, async (test, err) => {
|
|
102
131
|
failures += 1;
|
|
103
132
|
console.log(pc.bold(pc.red('✖')), test.fullTitle(), pc.gray(err.message));
|
|
133
|
+
|
|
134
|
+
const isHookFailure = test.title.includes('before each') || test.title.includes('after each');
|
|
135
|
+
|
|
136
|
+
if (isHookFailure && test.parent) {
|
|
137
|
+
hookFailures.set(test.parent.fullTitle(), {
|
|
138
|
+
error: err,
|
|
139
|
+
suiteTitle: getSuiteTitle(test),
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
const testId = getTestomatIdFromTestTitle(test.title);
|
|
105
145
|
|
|
106
146
|
const logs = getTestLogs(test);
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -16,6 +16,12 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
16
16
|
|
|
17
17
|
this._isSynchronising = false;
|
|
18
18
|
|
|
19
|
+
// Optional hooks enhancer for beforeEach failure handling
|
|
20
|
+
this.hooksEnhancer = null;
|
|
21
|
+
if (options?.enableHooksEnhancer) {
|
|
22
|
+
this._initializeHooksEnhancer();
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
// run is created by cli, if enabling the row below, it mat lead to multiple runs being created
|
|
20
26
|
// thus, need to check if process.env.runId is set and/or add more checks to avoid creating multiple runs
|
|
21
27
|
// this.client.createRun();
|
|
@@ -25,6 +31,32 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
25
31
|
return this._isSynchronising === false;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the hooks enhancer
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
async _initializeHooksEnhancer() {
|
|
39
|
+
try {
|
|
40
|
+
// Dynamic import to avoid hard dependency
|
|
41
|
+
// Resolve package path from the project's node_modules
|
|
42
|
+
const { createRequire } = await import('module');
|
|
43
|
+
const projectRequire = createRequire(process.cwd() + '/package.json');
|
|
44
|
+
// Import the hooks enhancer package
|
|
45
|
+
const packagePath = projectRequire.resolve('@testomatio/webdriver-hooks-enhancer');
|
|
46
|
+
const hooksEnhancerModule = await import(packagePath);
|
|
47
|
+
const { createHooksEnhancer } = hooksEnhancerModule;
|
|
48
|
+
|
|
49
|
+
this.hooksEnhancer = createHooksEnhancer(this);
|
|
50
|
+
console.log('[TESTOMATIO] WebdriverIO Hooks Enhancer enabled');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(
|
|
53
|
+
'[TESTOMATIO] Could not enable WebdriverIO Hooks Enhancer.',
|
|
54
|
+
'Install @testomatio/webdriver-hooks-enhancer to use this feature:',
|
|
55
|
+
error.message
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
/**
|
|
29
61
|
*
|
|
30
62
|
* @param {RunnerStats} runData
|
|
@@ -46,6 +78,29 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
46
78
|
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
47
79
|
}
|
|
48
80
|
|
|
81
|
+
onHookEnd(hook) {
|
|
82
|
+
// Hooks enhancer will handle this if enabled
|
|
83
|
+
if (this.hooksEnhancer) {
|
|
84
|
+
this.hooksEnhancer.trackHookFailure(hook);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async onSuiteEnd(suiteOrScenario) {
|
|
89
|
+
// Handle hook failures for regular suites using enhancer
|
|
90
|
+
if (this.hooksEnhancer && suiteOrScenario.type !== 'scenario') {
|
|
91
|
+
await this.hooksEnhancer.handleSuiteEnd(
|
|
92
|
+
suiteOrScenario,
|
|
93
|
+
this.client,
|
|
94
|
+
getTestomatIdFromTestTitle
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle BDD scenarios (cucumber)
|
|
99
|
+
if (suiteOrScenario.type === 'scenario') {
|
|
100
|
+
this._addTestPromises.push(this.addBddScenario(suiteOrScenario));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
onTestStart(test) {
|
|
50
105
|
services.setContext(test.fullTitle);
|
|
51
106
|
}
|
|
@@ -62,13 +117,6 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
62
117
|
this._addTestPromises.push(this.addTest(test));
|
|
63
118
|
}
|
|
64
119
|
|
|
65
|
-
// wdio-cucumber does not trigger onTestEnd hook, thus, using this one
|
|
66
|
-
onSuiteEnd(scerario) {
|
|
67
|
-
if (scerario.type === 'scenario') {
|
|
68
|
-
this._addTestPromises.push(this.addBddScenario(scerario));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
120
|
async addTest(test) {
|
|
73
121
|
if (!this.client) return;
|
|
74
122
|
|
package/src/bin/startTest.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { getPackageVersion } from '../utils/utils.js';
|
|
5
6
|
import pc from 'picocolors';
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
-
const __dirname = typeof
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const __dirname = typeof global.__dirname !== 'undefined' ? global.__dirname : dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const cliPath = join(__dirname, 'cli.js');
|
|
10
11
|
|
|
11
12
|
const version = getPackageVersion();
|
package/src/pipe/bitbucket.js
CHANGED
package/src/pipe/coverage.js
CHANGED
|
@@ -192,6 +192,11 @@ class CoveragePipe { // or Changes for the future???
|
|
|
192
192
|
|
|
193
193
|
async finishRun(runParams) {}
|
|
194
194
|
|
|
195
|
+
async sync() {
|
|
196
|
+
// CoveragePipe doesn't buffer tests, so sync is a no-op
|
|
197
|
+
// Reserved for future use if needed
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
toString() {
|
|
196
201
|
return 'Coverage Reporter';
|
|
197
202
|
}
|
package/src/pipe/csv.js
CHANGED
|
@@ -125,6 +125,10 @@ class CsvPipe {
|
|
|
125
125
|
if (!this.isEnabled) return;
|
|
126
126
|
|
|
127
127
|
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
128
|
+
await this.sync();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async sync() {
|
|
128
132
|
// Save results based on the default headers
|
|
129
133
|
if (this.isEnabled) {
|
|
130
134
|
await this.saveToCsv(this.results, CSV_HEADERS);
|
package/src/pipe/debug.js
CHANGED
|
@@ -112,12 +112,17 @@ export class DebugPipe {
|
|
|
112
112
|
|
|
113
113
|
async finishRun(params) {
|
|
114
114
|
if (!this.isEnabled) return;
|
|
115
|
-
await this.
|
|
115
|
+
await this.sync();
|
|
116
116
|
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
117
117
|
this.logToFile({ action: 'finishRun', params });
|
|
118
118
|
console.log(APP_PREFIX, '🪲 Debug Saved to', this.logFilePath);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
async sync() {
|
|
122
|
+
if (!this.isEnabled) return;
|
|
123
|
+
await this.batchUpload();
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
toString() {
|
|
122
127
|
return 'Debug Reporter';
|
|
123
128
|
}
|
package/src/pipe/github.js
CHANGED
package/src/pipe/gitlab.js
CHANGED
package/src/pipe/html.js
CHANGED
package/src/pipe/testomatio.js
CHANGED
|
@@ -294,6 +294,7 @@ class TestomatioPipe {
|
|
|
294
294
|
const errorText = err.response?.data?.message || err.message;
|
|
295
295
|
debug('Error creating run', err);
|
|
296
296
|
console.log(errorText || err);
|
|
297
|
+
if (err.response?.status === 403) this.#disablePipe();
|
|
297
298
|
if (!this.apiKey) console.error('Testomat.io API key is not set');
|
|
298
299
|
if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
|
|
299
300
|
|
|
@@ -347,6 +348,7 @@ class TestomatioPipe {
|
|
|
347
348
|
maxContentLength: Infinity,
|
|
348
349
|
})
|
|
349
350
|
.catch(err => {
|
|
351
|
+
if (err.response?.status === 403) this.#disablePipe();
|
|
350
352
|
this.requestFailures++;
|
|
351
353
|
this.notReportedTestsCount++;
|
|
352
354
|
if (err.response) {
|
|
@@ -397,6 +399,7 @@ class TestomatioPipe {
|
|
|
397
399
|
maxContentLength: Infinity,
|
|
398
400
|
})
|
|
399
401
|
.catch(err => {
|
|
402
|
+
if (err.response?.status === 403) this.#disablePipe();
|
|
400
403
|
this.requestFailures++;
|
|
401
404
|
this.notReportedTestsCount += testsToSend.length;
|
|
402
405
|
if (err.response) {
|
|
@@ -434,6 +437,15 @@ class TestomatioPipe {
|
|
|
434
437
|
return uploading;
|
|
435
438
|
}
|
|
436
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Syncs / flushes buffered tests by uploading them as a batch
|
|
442
|
+
* This is used to manually trigger batch upload (e.g., after all tests are added)
|
|
443
|
+
*/
|
|
444
|
+
async sync() {
|
|
445
|
+
if (!this.isEnabled) return;
|
|
446
|
+
await this.#batchUpload();
|
|
447
|
+
}
|
|
448
|
+
|
|
437
449
|
/**
|
|
438
450
|
* @param {import('../../types/types.js').RunData} params
|
|
439
451
|
* @returns
|
|
@@ -520,6 +532,19 @@ class TestomatioPipe {
|
|
|
520
532
|
debug('Run finished');
|
|
521
533
|
}
|
|
522
534
|
|
|
535
|
+
#disablePipe() {
|
|
536
|
+
this.isEnabled = false;
|
|
537
|
+
this.apiKey = null;
|
|
538
|
+
|
|
539
|
+
// clear interval function, otherwise the proccess will continue indefinitely
|
|
540
|
+
if (this.batch.intervalFunction) {
|
|
541
|
+
clearInterval(this.batch.intervalFunction);
|
|
542
|
+
this.batch.intervalFunction = null;
|
|
543
|
+
this.batch.isEnabled = false;
|
|
544
|
+
}
|
|
545
|
+
this.batch.tests = [];
|
|
546
|
+
}
|
|
547
|
+
|
|
523
548
|
#logFailedResponse(error) {
|
|
524
549
|
let responseBody = stringify(error.response?.data ?? error.response ?? error, { pretty: true });
|
|
525
550
|
if (!responseBody) responseBody = '<empty>';
|
package/src/xmlReader.js
CHANGED
|
@@ -529,6 +529,52 @@ class XmlReader {
|
|
|
529
529
|
return run;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Calculate the approximate size of data in bytes (JSON stringified length)
|
|
534
|
+
* @param {Object} data - Data to measure
|
|
535
|
+
* @returns {number} Size in bytes
|
|
536
|
+
*/
|
|
537
|
+
#getObjectSize(data) {
|
|
538
|
+
const body = JSON.stringify(data);
|
|
539
|
+
return new TextEncoder().encode(body).length;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Split tests array into chunks based on data size
|
|
544
|
+
* @param {Array} tests - Array of tests to split
|
|
545
|
+
* @returns {Array<Array>} Array of test chunks
|
|
546
|
+
*/
|
|
547
|
+
#splitTestsIntoChunks(tests) {
|
|
548
|
+
const maxSizeBytes = 1 * 1024 * 1024;
|
|
549
|
+
|
|
550
|
+
const chunks = [];
|
|
551
|
+
let currentChunk = [];
|
|
552
|
+
let currentChunkSize = 0;
|
|
553
|
+
|
|
554
|
+
for (const test of tests) {
|
|
555
|
+
const testSize = this.#getObjectSize(test);
|
|
556
|
+
|
|
557
|
+
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
558
|
+
|
|
559
|
+
if (wouldExceedSize) {
|
|
560
|
+
if (currentChunk.length > 0) {
|
|
561
|
+
chunks.push(currentChunk);
|
|
562
|
+
}
|
|
563
|
+
currentChunk = [];
|
|
564
|
+
currentChunkSize = 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
currentChunk.push(test);
|
|
568
|
+
currentChunkSize += testSize;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (currentChunk.length > 0) {
|
|
572
|
+
chunks.push(currentChunk);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return chunks;
|
|
576
|
+
}
|
|
577
|
+
|
|
532
578
|
async uploadData() {
|
|
533
579
|
await this.uploadArtifacts();
|
|
534
580
|
this.calculateStats();
|
|
@@ -537,18 +583,63 @@ class XmlReader {
|
|
|
537
583
|
this.formatErrors();
|
|
538
584
|
this.formatTests();
|
|
539
585
|
|
|
540
|
-
|
|
541
|
-
|
|
586
|
+
this.pipes = this.pipes || (await this.pipesPromise);
|
|
587
|
+
|
|
588
|
+
// Create run before uploading tests to ensure runId is set
|
|
589
|
+
await this.createRun();
|
|
590
|
+
|
|
591
|
+
if (!this.tests || !Array.isArray(this.tests) || this.tests.length === 0) {
|
|
592
|
+
debug('No tests to upload, finishing run');
|
|
593
|
+
const finishData = {
|
|
594
|
+
api_key: this.requestParams.apiKey,
|
|
595
|
+
status: 'finished',
|
|
596
|
+
duration: this.stats.duration,
|
|
597
|
+
detach: this.requestParams.detach,
|
|
598
|
+
};
|
|
599
|
+
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const testChunks = this.#splitTestsIntoChunks(this.tests);
|
|
603
|
+
|
|
604
|
+
const totalChunks = testChunks.length;
|
|
605
|
+
const totalTests = this.tests.length;
|
|
606
|
+
|
|
607
|
+
debug(`Split ${totalTests} tests into ${totalChunks} chunks (max 1MB per chunk)`);
|
|
608
|
+
|
|
609
|
+
let uploadedTests = 0;
|
|
610
|
+
for (let i = 0; i < testChunks.length; i++) {
|
|
611
|
+
const chunk = testChunks[i];
|
|
612
|
+
const chunkNum = i + 1;
|
|
613
|
+
|
|
614
|
+
if (totalChunks > 1) {
|
|
615
|
+
debug(`Uploading chunk ${chunkNum}/${totalChunks} (${chunk.length} tests)`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const test of chunk) {
|
|
619
|
+
await Promise.all(this.pipes.map(p => p.addTest(test)));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
await Promise.all(this.pipes.map(p => p.sync()));
|
|
623
|
+
|
|
624
|
+
uploadedTests += chunk.length;
|
|
625
|
+
debug(`Uploaded ${uploadedTests}/${totalTests} tests`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (totalChunks > 1) {
|
|
629
|
+
console.log(APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests in ${totalChunks} chunks`);
|
|
630
|
+
} else {
|
|
631
|
+
console.log(APP_PREFIX, `✅ Successfully uploaded ${uploadedTests} tests`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const finishData = {
|
|
542
635
|
api_key: this.requestParams.apiKey,
|
|
543
636
|
status: 'finished',
|
|
544
637
|
duration: this.stats.duration,
|
|
545
|
-
|
|
638
|
+
detach: this.requestParams.detach,
|
|
546
639
|
};
|
|
547
640
|
|
|
548
|
-
debug('
|
|
549
|
-
|
|
550
|
-
this.pipes = this.pipes || (await this.pipesPromise);
|
|
551
|
-
return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
|
|
641
|
+
debug('Finishing run with status:', finishData.status);
|
|
642
|
+
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
552
643
|
}
|
|
553
644
|
|
|
554
645
|
async _finishRun() {
|
package/types/types.d.ts
CHANGED
|
@@ -290,6 +290,9 @@ export interface Pipe {
|
|
|
290
290
|
/** adds a test to the current run */
|
|
291
291
|
addTest(test: TestData): any;
|
|
292
292
|
|
|
293
|
+
/** syncs / flushes buffered data (e.g., uploads batched tests) */
|
|
294
|
+
sync(): Promise<void>;
|
|
295
|
+
|
|
293
296
|
/** ends the run */
|
|
294
297
|
finishRun(runParams: RunData): Promise<void>;
|
|
295
298
|
|