@testomatio/reporter 2.7.3 → 2.7.4
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 +7 -0
- package/lib/adapter/vitest.d.ts +22 -3
- package/lib/adapter/vitest.js +250 -26
- package/lib/client.d.ts +2 -1
- package/lib/client.js +3 -2
- package/lib/pipe/bitbucket.js +16 -1
- package/lib/pipe/html.js +18 -3
- package/lib/template/testomatio.hbs +131 -20
- package/lib/xmlReader.js +7 -5
- package/package.json +1 -1
- package/src/adapter/vitest.js +253 -26
- package/src/client.js +3 -2
- package/src/pipe/bitbucket.js +23 -1
- package/src/pipe/html.js +20 -3
- package/src/template/testomatio.hbs +131 -20
- package/src/xmlReader.js +9 -6
package/README.md
CHANGED
|
@@ -58,6 +58,13 @@ pnpm install @testomatio/reporter --save-dev
|
|
|
58
58
|
yarn add @testomatio/reporter --dev
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
For Yarn 4 (Berry), use CLI wrapper package:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
yarn add testomatio-reporter-cli --dev
|
|
65
|
+
npx testomatio-reporter <command> [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
61
68
|
## Getting Started
|
|
62
69
|
|
|
63
70
|
### 1️⃣ Attach Reporter to the Test Runner
|
package/lib/adapter/vitest.d.ts
CHANGED
|
@@ -18,15 +18,22 @@ export type TestData = import("../../types/types.js").TestData;
|
|
|
18
18
|
export class VitestReporter {
|
|
19
19
|
constructor(config?: {});
|
|
20
20
|
client: TestomatioClient;
|
|
21
|
-
/**
|
|
22
|
-
* @type {(TestData & {status: string})[]} tests
|
|
23
|
-
*/
|
|
21
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
24
22
|
tests: (TestData & {
|
|
25
23
|
status: string;
|
|
24
|
+
_reportKey?: string | null;
|
|
26
25
|
})[];
|
|
27
26
|
_finalized: boolean;
|
|
28
27
|
_finalizing: boolean;
|
|
28
|
+
_runStartedAtMs: number;
|
|
29
|
+
_runStartedAtMicros: number;
|
|
30
|
+
_reportedTestKeys: Set<any>;
|
|
31
|
+
_liveQueue: Promise<void>;
|
|
29
32
|
onInit(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
35
|
+
*/
|
|
36
|
+
onTestRunStart(): void;
|
|
30
37
|
/**
|
|
31
38
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
32
39
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -39,6 +46,18 @@ export class VitestReporter {
|
|
|
39
46
|
* @param {unknown[] | undefined} errors
|
|
40
47
|
*/
|
|
41
48
|
onTestRunEnd(testModules: Array<unknown> | undefined, errors: unknown[] | undefined): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Vitest 4 callback fired when single test case is finished.
|
|
51
|
+
*
|
|
52
|
+
* @param {unknown} testCase
|
|
53
|
+
*/
|
|
54
|
+
onTestCaseResult(testCase: unknown): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Vitest 3 fallback callback with task updates.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown[] | undefined} packs
|
|
59
|
+
*/
|
|
60
|
+
onTaskUpdate(packs: unknown[] | undefined): Promise<void>;
|
|
42
61
|
#private;
|
|
43
62
|
}
|
|
44
63
|
import { Client as TestomatioClient } from '../client.js';
|
package/lib/adapter/vitest.js
CHANGED
|
@@ -22,19 +22,34 @@ const debug = (0, debug_1.default)('@testomatio/reporter:adapter-jest');
|
|
|
22
22
|
class VitestReporter {
|
|
23
23
|
constructor(config = {}) {
|
|
24
24
|
this.client = new client_js_1.Client({ apiKey: config?.apiKey });
|
|
25
|
-
/**
|
|
26
|
-
* @type {(TestData & {status: string})[]} tests
|
|
27
|
-
*/
|
|
25
|
+
/** @type {(TestData & {status: string, _reportKey?: string | null})[]} tests */
|
|
28
26
|
this.tests = [];
|
|
29
27
|
this._finalized = false;
|
|
30
28
|
this._finalizing = false;
|
|
29
|
+
this._runStartedAtMs = null;
|
|
30
|
+
this._runStartedAtMicros = null;
|
|
31
|
+
this._reportedTestKeys = new Set();
|
|
32
|
+
this._liveQueue = Promise.resolve();
|
|
31
33
|
}
|
|
32
34
|
// on run start
|
|
33
35
|
onInit() {
|
|
36
|
+
const now = Date.now();
|
|
34
37
|
this._finalized = false;
|
|
35
38
|
this._finalizing = false;
|
|
39
|
+
this._runStartedAtMs = now;
|
|
40
|
+
this._runStartedAtMicros = now * 1000;
|
|
41
|
+
this._reportedTestKeys = new Set();
|
|
42
|
+
this._liveQueue = Promise.resolve();
|
|
36
43
|
this.client.createRun();
|
|
37
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Vitest 3/4 callback fired when test run starts.
|
|
47
|
+
*/
|
|
48
|
+
onTestRunStart() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
this._runStartedAtMs = now;
|
|
51
|
+
this._runStartedAtMicros = now * 1000;
|
|
52
|
+
}
|
|
38
53
|
/**
|
|
39
54
|
* @param {VitestTestFile[] | undefined} files // array with results;
|
|
40
55
|
* @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
|
|
@@ -68,12 +83,19 @@ class VitestReporter {
|
|
|
68
83
|
debug(this.tests.length, 'tests collected');
|
|
69
84
|
// send tests to Testomat.io
|
|
70
85
|
for (const test of this.tests) {
|
|
86
|
+
if (test._reportKey && this._reportedTestKeys.has(test._reportKey))
|
|
87
|
+
continue;
|
|
88
|
+
if (test._reportKey)
|
|
89
|
+
this._reportedTestKeys.add(test._reportKey);
|
|
71
90
|
await this.client.addTestRun(test.status, test);
|
|
72
91
|
}
|
|
92
|
+
await this._liveQueue;
|
|
73
93
|
console.log('finished');
|
|
74
94
|
if (errors.length)
|
|
75
95
|
console.error('Vitest adapter errors:', errors);
|
|
76
|
-
|
|
96
|
+
const startedAtMs = this._runStartedAtMs || getEarliestTestStartMs(files) || Date.now();
|
|
97
|
+
const duration = Math.max(0, (Date.now() - startedAtMs) / 1000);
|
|
98
|
+
await this.client.updateRunStatus(getRunStatusFromResults(files), { duration });
|
|
77
99
|
this._finalized = true;
|
|
78
100
|
}
|
|
79
101
|
finally {
|
|
@@ -92,6 +114,28 @@ class VitestReporter {
|
|
|
92
114
|
.filter(Boolean);
|
|
93
115
|
await this.onFinished(files, errors);
|
|
94
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Vitest 4 callback fired when single test case is finished.
|
|
119
|
+
*
|
|
120
|
+
* @param {unknown} testCase
|
|
121
|
+
*/
|
|
122
|
+
async onTestCaseResult(testCase) {
|
|
123
|
+
await this.#reportLive(testCase);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Vitest 3 fallback callback with task updates.
|
|
127
|
+
*
|
|
128
|
+
* @param {unknown[] | undefined} packs
|
|
129
|
+
*/
|
|
130
|
+
async onTaskUpdate(packs) {
|
|
131
|
+
if (!Array.isArray(packs) || !packs.length)
|
|
132
|
+
return;
|
|
133
|
+
for (const pack of packs) {
|
|
134
|
+
const test = getTestFromTaskUpdatePack(pack);
|
|
135
|
+
if (test)
|
|
136
|
+
await this.#reportLive(test);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
95
139
|
/* non-used listeners
|
|
96
140
|
onUserConsoleLog(log) {}
|
|
97
141
|
onPathsCollected(paths) {} // paths array to files with tests
|
|
@@ -126,25 +170,50 @@ class VitestReporter {
|
|
|
126
170
|
/**
|
|
127
171
|
* Processes task and returns test data ready to be sent to Testomat.io
|
|
128
172
|
*
|
|
129
|
-
* @param {
|
|
173
|
+
* @param {any} test
|
|
130
174
|
*
|
|
131
|
-
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
|
|
175
|
+
* @returns {TestData & {status: 'passed' | 'failed' | 'skipped', _reportKey?: string | null}}
|
|
132
176
|
*/
|
|
133
177
|
#getDataFromTest(test) {
|
|
178
|
+
const normalized = normalizeVitestTest(test);
|
|
179
|
+
const reportKey = getReportKey(test, normalized);
|
|
180
|
+
const startMicros = typeof normalized.startTime === 'number'
|
|
181
|
+
? Math.floor(normalized.startTime * 1000)
|
|
182
|
+
: this._runStartedAtMicros || undefined;
|
|
134
183
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
184
|
+
_reportKey: reportKey,
|
|
185
|
+
error: normalized.error,
|
|
186
|
+
file: normalized.file,
|
|
187
|
+
logs: normalized.logs,
|
|
188
|
+
meta: normalized.meta,
|
|
139
189
|
// @ts-ignore - STATUS values are string literals but type system sees them as string
|
|
140
|
-
status: getTestStatus(
|
|
141
|
-
suite_title:
|
|
142
|
-
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(
|
|
143
|
-
time:
|
|
144
|
-
|
|
190
|
+
status: getTestStatus(normalized.state, normalized.mode),
|
|
191
|
+
suite_title: normalized.suiteTitle,
|
|
192
|
+
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(normalized.name),
|
|
193
|
+
time: normalized.duration,
|
|
194
|
+
timestamp: startMicros,
|
|
195
|
+
title: normalized.name,
|
|
145
196
|
// testomatio functions (artifacts, logs, steps, meta) are not supported
|
|
146
197
|
};
|
|
147
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* @param {unknown} testCase
|
|
201
|
+
*/
|
|
202
|
+
async #reportLive(testCase) {
|
|
203
|
+
if (this._finalized || this._finalizing)
|
|
204
|
+
return;
|
|
205
|
+
const normalized = normalizeVitestTest(testCase);
|
|
206
|
+
if (!isLiveReportableState(normalized.state, normalized.mode))
|
|
207
|
+
return;
|
|
208
|
+
const data = this.#getDataFromTest(testCase);
|
|
209
|
+
if (!data._reportKey || this._reportedTestKeys.has(data._reportKey))
|
|
210
|
+
return;
|
|
211
|
+
this._reportedTestKeys.add(data._reportKey);
|
|
212
|
+
this._liveQueue = this._liveQueue
|
|
213
|
+
.then(() => this.client.addTestRun(data.status, data))
|
|
214
|
+
.catch(() => undefined);
|
|
215
|
+
await this._liveQueue;
|
|
216
|
+
}
|
|
148
217
|
}
|
|
149
218
|
exports.VitestReporter = VitestReporter;
|
|
150
219
|
/**
|
|
@@ -159,16 +228,15 @@ function getRunStatusFromResults(files) {
|
|
|
159
228
|
*/
|
|
160
229
|
let status = 'finished'; // default status (if no failed or passed tests)
|
|
161
230
|
files.forEach(file => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (taskOrSuite.result?.state === 'fail') {
|
|
231
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
232
|
+
if (isFailedState(taskOrSuite?.result?.state)) {
|
|
165
233
|
status = 'failed'; // set status to failed if any test failed
|
|
166
234
|
}
|
|
167
235
|
});
|
|
168
236
|
// if there are no failed tests > search for passed tests
|
|
169
237
|
if (status !== 'failed') {
|
|
170
|
-
file.
|
|
171
|
-
if (taskOrSuite
|
|
238
|
+
getTasks(file).forEach(taskOrSuite => {
|
|
239
|
+
if (isPassedState(taskOrSuite?.result?.state)) {
|
|
172
240
|
status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
|
|
173
241
|
}
|
|
174
242
|
});
|
|
@@ -179,17 +247,18 @@ function getRunStatusFromResults(files) {
|
|
|
179
247
|
/**
|
|
180
248
|
* Returns test status in Testomat.io format
|
|
181
249
|
*
|
|
182
|
-
* @param {
|
|
250
|
+
* @param {string | undefined} state
|
|
251
|
+
* @param {string | undefined} mode
|
|
183
252
|
* @returns 'passed' | 'failed' | 'skipped'
|
|
184
253
|
*/
|
|
185
|
-
function getTestStatus(
|
|
186
|
-
if (
|
|
254
|
+
function getTestStatus(state, mode) {
|
|
255
|
+
if (isFailedState(state))
|
|
187
256
|
return constants_js_1.STATUS.FAILED;
|
|
188
|
-
if (
|
|
257
|
+
if (isPassedState(state))
|
|
189
258
|
return constants_js_1.STATUS.PASSED;
|
|
190
|
-
if (
|
|
259
|
+
if (isSkippedState(state) || (!state && mode === 'skip'))
|
|
191
260
|
return constants_js_1.STATUS.SKIPPED;
|
|
192
|
-
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team.
|
|
261
|
+
console.error(picocolors_1.default.red('Unprocessed case for defining test status. Contact dev team. State:'), state);
|
|
193
262
|
return constants_js_1.STATUS.SKIPPED;
|
|
194
263
|
}
|
|
195
264
|
/**
|
|
@@ -221,10 +290,165 @@ function getTasks(node) {
|
|
|
221
290
|
return node.tasks;
|
|
222
291
|
if (Array.isArray(node.children))
|
|
223
292
|
return node.children;
|
|
293
|
+
if (node.children && typeof node.children[Symbol.iterator] === 'function')
|
|
294
|
+
return Array.from(node.children);
|
|
224
295
|
if (node.task)
|
|
225
296
|
return [node.task];
|
|
226
297
|
return [];
|
|
227
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* @param {string | undefined} state
|
|
301
|
+
* @returns {boolean}
|
|
302
|
+
*/
|
|
303
|
+
function isFailedState(state) {
|
|
304
|
+
return state === 'fail' || state === 'failed';
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @param {string | undefined} state
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
function isPassedState(state) {
|
|
311
|
+
return state === 'pass' || state === 'passed';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* @param {string | undefined} state
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
function isSkippedState(state) {
|
|
318
|
+
return state === 'skip' || state === 'skipped' || state === 'todo';
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Accept only completed test states for live upload to avoid reporting
|
|
322
|
+
* intermediate task updates as skipped.
|
|
323
|
+
*
|
|
324
|
+
* @param {string | undefined} state
|
|
325
|
+
* @param {string | undefined} mode
|
|
326
|
+
* @returns {boolean}
|
|
327
|
+
*/
|
|
328
|
+
function isLiveReportableState(state, mode) {
|
|
329
|
+
if (isFailedState(state) || isPassedState(state) || isSkippedState(state))
|
|
330
|
+
return true;
|
|
331
|
+
if (!state && mode === 'skip')
|
|
332
|
+
return true;
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* @param {VitestTestFile[] | undefined} files
|
|
337
|
+
* @returns {number | null}
|
|
338
|
+
*/
|
|
339
|
+
function getEarliestTestStartMs(files) {
|
|
340
|
+
let earliest = null;
|
|
341
|
+
const walk = node => {
|
|
342
|
+
if (!node)
|
|
343
|
+
return;
|
|
344
|
+
const startTime = node?.result?.startTime;
|
|
345
|
+
if (typeof startTime === 'number' && !Number.isNaN(startTime)) {
|
|
346
|
+
if (earliest == null || startTime < earliest)
|
|
347
|
+
earliest = startTime;
|
|
348
|
+
}
|
|
349
|
+
getTasks(node).forEach(walk);
|
|
350
|
+
};
|
|
351
|
+
(files || []).forEach(walk);
|
|
352
|
+
return earliest;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* @param {any} test
|
|
356
|
+
* @returns {{
|
|
357
|
+
* name: string,
|
|
358
|
+
* state: string | undefined,
|
|
359
|
+
* mode: string | undefined,
|
|
360
|
+
* duration: number,
|
|
361
|
+
* startTime: number | undefined,
|
|
362
|
+
* error: any,
|
|
363
|
+
* file: string,
|
|
364
|
+
* suiteTitle: string,
|
|
365
|
+
* logs: string,
|
|
366
|
+
* meta: any
|
|
367
|
+
* }}
|
|
368
|
+
*/
|
|
369
|
+
function normalizeVitestTest(test) {
|
|
370
|
+
if (test && typeof test.result === 'function') {
|
|
371
|
+
const result = test.result();
|
|
372
|
+
const diagnostic = typeof test.diagnostic === 'function' ? test.diagnostic() : undefined;
|
|
373
|
+
const state = result?.state;
|
|
374
|
+
const duration = diagnostic?.duration || 0;
|
|
375
|
+
const startTime = diagnostic?.startTime;
|
|
376
|
+
const error = Array.isArray(result?.errors) ? result.errors[0] : undefined;
|
|
377
|
+
const file = test.module?.relativeModuleId ||
|
|
378
|
+
test.module?.moduleId ||
|
|
379
|
+
test.task?.file?.name ||
|
|
380
|
+
test.task?.file?.filepath ||
|
|
381
|
+
'';
|
|
382
|
+
const suiteTitle = (test.parent?.type === 'suite' ? test.parent?.name : null) ||
|
|
383
|
+
test.task?.suite?.name ||
|
|
384
|
+
test.task?.file?.name ||
|
|
385
|
+
file;
|
|
386
|
+
return {
|
|
387
|
+
name: test.name || test.task?.name || '',
|
|
388
|
+
state,
|
|
389
|
+
mode: test.options?.mode || test.task?.mode,
|
|
390
|
+
duration,
|
|
391
|
+
startTime,
|
|
392
|
+
error,
|
|
393
|
+
file,
|
|
394
|
+
suiteTitle,
|
|
395
|
+
logs: '',
|
|
396
|
+
meta: typeof test.meta === 'function' ? test.meta() : {},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
name: test?.name || '',
|
|
401
|
+
state: test?.result?.state,
|
|
402
|
+
mode: test?.mode,
|
|
403
|
+
duration: test?.result?.duration || 0,
|
|
404
|
+
startTime: test?.result?.startTime,
|
|
405
|
+
error: test?.result?.errors ? test.result.errors[0] : undefined,
|
|
406
|
+
file: test?.file?.name || test?.file?.filepath || '',
|
|
407
|
+
suiteTitle: test?.suite?.name || test?.file?.name || test?.file?.filepath || '',
|
|
408
|
+
logs: test?.logs ? transformLogsToString(test.logs) : '',
|
|
409
|
+
meta: test?.meta,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* @param {any} test
|
|
414
|
+
* @param {{file: string, suiteTitle: string, name: string, startTime?: number}} normalized
|
|
415
|
+
* @returns {string | null}
|
|
416
|
+
*/
|
|
417
|
+
function getReportKey(test, normalized) {
|
|
418
|
+
if (test?.id)
|
|
419
|
+
return String(test.id);
|
|
420
|
+
if (test?.task?.id)
|
|
421
|
+
return String(test.task.id);
|
|
422
|
+
if (!normalized?.name)
|
|
423
|
+
return null;
|
|
424
|
+
const loc = test?.location || test?.task?.location;
|
|
425
|
+
const locationKey = loc ? `${loc.line || ''}:${loc.column || ''}` : '';
|
|
426
|
+
const startKey = typeof normalized.startTime === 'number' && !Number.isNaN(normalized.startTime) ? String(normalized.startTime) : '';
|
|
427
|
+
return `${normalized.file}::${normalized.suiteTitle}::${normalized.name}::${locationKey}::${startKey}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Vitest can pass task updates as tuples. Try to extract a test-like object.
|
|
431
|
+
*
|
|
432
|
+
* @param {unknown} pack
|
|
433
|
+
* @returns {any | null}
|
|
434
|
+
*/
|
|
435
|
+
function getTestFromTaskUpdatePack(pack) {
|
|
436
|
+
if (!pack)
|
|
437
|
+
return null;
|
|
438
|
+
if (Array.isArray(pack)) {
|
|
439
|
+
if (pack[2]?.type === 'test')
|
|
440
|
+
return pack[2];
|
|
441
|
+
if (pack[1]?.type === 'test')
|
|
442
|
+
return pack[1];
|
|
443
|
+
if (pack[0]?.type === 'test')
|
|
444
|
+
return pack[0];
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const objectPack = /** @type {any} */ (pack);
|
|
448
|
+
if (typeof objectPack === 'object' && objectPack?.type === 'test')
|
|
449
|
+
return objectPack;
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
228
452
|
module.exports = VitestReporter;
|
|
229
453
|
|
|
230
454
|
module.exports.VitestReporter = VitestReporter;
|
package/lib/client.d.ts
CHANGED
|
@@ -65,9 +65,10 @@ export class Client {
|
|
|
65
65
|
*
|
|
66
66
|
* Updates the status of the current test run and finishes the run.
|
|
67
67
|
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
68
|
+
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
68
69
|
* Must be one of "passed", "failed", or "finished"
|
|
69
70
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
70
71
|
*/
|
|
71
|
-
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
|
|
72
|
+
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
72
73
|
}
|
|
73
74
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -308,17 +308,18 @@ class Client {
|
|
|
308
308
|
*
|
|
309
309
|
* Updates the status of the current test run and finishes the run.
|
|
310
310
|
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
311
|
+
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
311
312
|
* Must be one of "passed", "failed", or "finished"
|
|
312
313
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
313
314
|
*/
|
|
314
|
-
async updateRunStatus(status) {
|
|
315
|
+
async updateRunStatus(status, params = {}) {
|
|
315
316
|
this.pipes ||= await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
316
317
|
this.runId ||= (0, utils_js_1.readLatestRunId)();
|
|
317
318
|
debug('Updating run status...');
|
|
318
319
|
// all pipes disabled, skipping
|
|
319
320
|
if (!this.pipes?.filter(p => p.isEnabled).length)
|
|
320
321
|
return Promise.resolve();
|
|
321
|
-
const runParams = { status };
|
|
322
|
+
const runParams = { ...params, status };
|
|
322
323
|
this.queue = this.queue
|
|
323
324
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
324
325
|
.then(() => {
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -101,6 +101,14 @@ class BitbucketPipe {
|
|
|
101
101
|
async finishRun(runParams) {
|
|
102
102
|
if (!this.isEnabled)
|
|
103
103
|
return;
|
|
104
|
+
if (!this.ENV.BITBUCKET_PR_ID) {
|
|
105
|
+
log_js_1.log.warn(picocolors_1.default.yellow('Bitbucket'), 'Skipping PR comment: BITBUCKET_PR_ID is not set. Run this pipe in a Bitbucket pull-requests pipeline.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!this.ENV.BITBUCKET_WORKSPACE || !this.ENV.BITBUCKET_REPO_SLUG) {
|
|
109
|
+
log_js_1.log.warn(picocolors_1.default.yellow('Bitbucket'), 'Skipping PR comment: BITBUCKET_WORKSPACE or BITBUCKET_REPO_SLUG is missing.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
104
112
|
if (runParams.tests)
|
|
105
113
|
runParams.tests.forEach(t => this.addTest(t));
|
|
106
114
|
// Clean up the logs from ANSI codes
|
|
@@ -194,9 +202,16 @@ class BitbucketPipe {
|
|
|
194
202
|
log_js_1.log.info(picocolors_1.default.yellow('Bitbucket'), `Report created: ${picocolors_1.default.magenta(commentURL)}`);
|
|
195
203
|
}
|
|
196
204
|
catch (err) {
|
|
205
|
+
const isForbiddenError = `${err}`.includes('Forbidden') || `${err}`.includes('403');
|
|
206
|
+
const scopeHint = isForbiddenError
|
|
207
|
+
? '\nHint: use a token that can write PR comments '
|
|
208
|
+
+ '(recommended: Repository Access Token with Pull requests: Write '
|
|
209
|
+
+ 'and Repository: Read) and run inside a pull-requests pipeline '
|
|
210
|
+
+ 'where BITBUCKET_PR_ID is available.'
|
|
211
|
+
: '';
|
|
197
212
|
console.error(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('Bitbucket'), `Couldn't create Bitbucket report\n${err}.
|
|
198
213
|
Request URL: ${commentsRequestURL}
|
|
199
|
-
Request data: ${body}`);
|
|
214
|
+
Request data: ${body}${scopeHint}`);
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
async sync() {
|
package/lib/pipe/html.js
CHANGED
|
@@ -936,6 +936,16 @@ function normalizeArtifacts(test) {
|
|
|
936
936
|
return allArtifacts
|
|
937
937
|
.map(artifact => {
|
|
938
938
|
if (typeof artifact === 'string') {
|
|
939
|
+
if (/^https?:\/\//i.test(artifact)) {
|
|
940
|
+
const base = path_1.default.basename(new URL(artifact).pathname) || artifact;
|
|
941
|
+
return {
|
|
942
|
+
name: base,
|
|
943
|
+
title: base,
|
|
944
|
+
path: artifact,
|
|
945
|
+
fsPath: null,
|
|
946
|
+
relativePath: artifact,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
939
949
|
const abs = path_1.default.isAbsolute(artifact) ? artifact : path_1.default.resolve(process.cwd(), artifact);
|
|
940
950
|
const href = artifact.startsWith('file://') ? artifact : (0, file_url_1.default)(abs, { resolve: true });
|
|
941
951
|
const base = path_1.default.basename(abs);
|
|
@@ -950,9 +960,14 @@ function normalizeArtifacts(test) {
|
|
|
950
960
|
if (artifact?.path) {
|
|
951
961
|
const raw = String(artifact.path);
|
|
952
962
|
const isFileUrl = raw.startsWith('file://');
|
|
953
|
-
const
|
|
954
|
-
const
|
|
955
|
-
const
|
|
963
|
+
const isHttpUrl = /^https?:\/\//i.test(raw);
|
|
964
|
+
const abs = isFileUrl || isHttpUrl ? null : path_1.default.isAbsolute(raw) ? raw : path_1.default.resolve(process.cwd(), raw);
|
|
965
|
+
const href = isFileUrl || isHttpUrl ? raw : (0, file_url_1.default)(abs, { resolve: true });
|
|
966
|
+
const base = abs
|
|
967
|
+
? path_1.default.basename(abs)
|
|
968
|
+
: isHttpUrl
|
|
969
|
+
? path_1.default.basename(new URL(raw).pathname) || artifact.name || artifact.title || 'attachment'
|
|
970
|
+
: artifact.name || artifact.title || 'attachment';
|
|
956
971
|
return {
|
|
957
972
|
...artifact,
|
|
958
973
|
name: artifact.name || artifact.title || base,
|