testomatio-reporter-cli 2.19.0 → 2.21.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/package.json +1 -1
- package/src/adapter/codecept.js +24 -2
- package/src/adapter/cucumber/current.js +2 -0
- package/src/adapter/cypress-plugin/index.js +2 -0
- package/src/adapter/e2e.js +248 -0
- package/src/adapter/jasmine.js +2 -0
- package/src/adapter/jest.js +2 -0
- package/src/adapter/mocha.js +146 -145
- package/src/adapter/nightwatch.js +2 -0
- package/src/adapter/playwright.js +33 -1
- package/src/adapter/vitest.js +6 -1
- package/src/adapter/webdriver.js +46 -2
- package/src/allureReader.js +2 -1
- package/src/bin/cli.js +246 -18
- package/src/client.js +11 -3
- package/src/data-storage.js +4 -3
- package/src/fetch/report.js +86 -0
- package/src/fetch/testruns.js +283 -0
- package/src/pipe/markdown.js +7 -0
- package/src/pipe/testomatio.js +27 -1
- package/src/replay.js +1 -0
- package/src/reporter-functions.js +41 -0
- package/src/uploader.js +81 -1
- package/src/utils/detectRunner.js +151 -0
- package/src/utils/pipe_utils.js +45 -4
- package/src/xmlReader.js +3 -0
- package/types/types.d.ts +1 -0
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -11,6 +11,8 @@ import codeceptjs from 'codeceptjs';
|
|
|
11
11
|
import { log } from '../utils/log.js';
|
|
12
12
|
|
|
13
13
|
const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
|
|
14
|
+
|
|
15
|
+
global.TESTOMATIO_ADAPTER = 'codeceptjs';
|
|
14
16
|
// @ts-ignore
|
|
15
17
|
if (!global.codeceptjs) {
|
|
16
18
|
// @ts-ignore
|
|
@@ -18,7 +20,7 @@ if (!global.codeceptjs) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
// @ts-ignore
|
|
21
|
-
const { event, recorder, codecept, output } = global.codeceptjs;
|
|
23
|
+
const { event, recorder, codecept, output, container } = global.codeceptjs;
|
|
22
24
|
|
|
23
25
|
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
24
26
|
.version()
|
|
@@ -57,6 +59,7 @@ function CodeceptReporter(config) {
|
|
|
57
59
|
let isRunFinalized = false;
|
|
58
60
|
|
|
59
61
|
const testTimeMap = {};
|
|
62
|
+
const failedUrls = new Map();
|
|
60
63
|
const clientConfig = buildCodeceptClientConfig(config);
|
|
61
64
|
const client = new TestomatClient(clientConfig);
|
|
62
65
|
|
|
@@ -241,6 +244,22 @@ function CodeceptReporter(config) {
|
|
|
241
244
|
}, true);
|
|
242
245
|
});
|
|
243
246
|
|
|
247
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
248
|
+
// no browser in suite hooks
|
|
249
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return;
|
|
250
|
+
const helper = Object.values(container.helpers()).find(h => typeof h.grabCurrentUrl === 'function');
|
|
251
|
+
if (!helper) return;
|
|
252
|
+
|
|
253
|
+
recorder.add('url of failed test', async () => {
|
|
254
|
+
try {
|
|
255
|
+
const url = await helper.grabCurrentUrl();
|
|
256
|
+
if (url) failedUrls.set(test.uid, url);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
debug('Failed to grab url of failed test', err?.message || err);
|
|
259
|
+
}
|
|
260
|
+
}, true);
|
|
261
|
+
});
|
|
262
|
+
|
|
244
263
|
event.dispatcher.on(event.test.skipped, test => {
|
|
245
264
|
const { uid, tags, title } = test.simplify();
|
|
246
265
|
|
|
@@ -272,6 +291,9 @@ function CodeceptReporter(config) {
|
|
|
272
291
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
273
292
|
const links = services.links.get(test.fullTitle());
|
|
274
293
|
const screenshotOnFailPath = artifacts.screenshot || null;
|
|
294
|
+
const meta = { ...keyValues, ...test.meta };
|
|
295
|
+
if (test.state === STATUS.FAILED && failedUrls.has(uid)) meta.failedUrl = failedUrls.get(uid);
|
|
296
|
+
failedUrls.delete(uid);
|
|
275
297
|
|
|
276
298
|
// Build step hierarchy with screenshot from screenshotOnFail
|
|
277
299
|
const stepHierarchy = buildUnifiedStepHierarchy(
|
|
@@ -295,7 +317,7 @@ function CodeceptReporter(config) {
|
|
|
295
317
|
logs,
|
|
296
318
|
links,
|
|
297
319
|
manuallyAttachedArtifacts,
|
|
298
|
-
meta
|
|
320
|
+
meta,
|
|
299
321
|
});
|
|
300
322
|
reportTestPromises.push(reportTestPromise);
|
|
301
323
|
|
|
@@ -8,6 +8,8 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../../utils/utils.js';
|
|
|
8
8
|
import { config } from '../../config.js';
|
|
9
9
|
import { services } from '../../services/index.js';
|
|
10
10
|
|
|
11
|
+
global.TESTOMATIO_ADAPTER = 'cucumber';
|
|
12
|
+
|
|
11
13
|
const { GherkinDocumentParser, PickleParser } = formatterHelpers;
|
|
12
14
|
const { getGherkinScenarioLocationMap, getGherkinStepMap } = GherkinDocumentParser;
|
|
13
15
|
const { getPickleStepMap } = PickleParser;
|
|
@@ -3,6 +3,8 @@ import { getTestomatIdFromTestTitle, parseSuite } from '../../utils/utils.js';
|
|
|
3
3
|
import TestomatClient from '../../client.js';
|
|
4
4
|
import { config } from '../../config.js';
|
|
5
5
|
|
|
6
|
+
global.TESTOMATIO_ADAPTER = 'cypress';
|
|
7
|
+
|
|
6
8
|
const testomatioReporter = on => {
|
|
7
9
|
if (!config.TESTOMATIO) {
|
|
8
10
|
console.log('TESTOMATIO key is empty, ignoring reports');
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import createDebugMessages from 'debug';
|
|
3
|
+
import TestomatioClient from '../client.js';
|
|
4
|
+
import { STATUS, SCREENSHOTS_ON_STEPS } from '../constants.js';
|
|
5
|
+
import { getTestomatIdFromTestTitle, truncate } from '../utils/utils.js';
|
|
6
|
+
import { formatStep, addStatusToStep, addArtifactsToStep } from './utils/step-formatter.js';
|
|
7
|
+
import { log } from '../utils/log.js';
|
|
8
|
+
|
|
9
|
+
const debug = createDebugMessages('@testomatio/reporter:adapter-e2e');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reporter for the e2e framework (https://github.com/tester-army/e2e).
|
|
13
|
+
* Add it to `reporters` in `e2e.config.ts`: `reporters: ['list', testomatio()]`.
|
|
14
|
+
*
|
|
15
|
+
* The config is loaded by every worker, so the client is created only inside
|
|
16
|
+
* handlers, which run in the runner process.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ apiKey?: string }} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function testomatio(options = {}) {
|
|
21
|
+
let client = null;
|
|
22
|
+
let runPromise = null;
|
|
23
|
+
|
|
24
|
+
function getClient() {
|
|
25
|
+
if (!client) client = new TestomatioClient({ apiKey: options.apiKey });
|
|
26
|
+
return client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function startRun() {
|
|
30
|
+
if (!runPromise) {
|
|
31
|
+
runPromise = getClient()
|
|
32
|
+
.createRun()
|
|
33
|
+
.catch(err => log.error('Failed to create Testomat.io run:', err?.message || err));
|
|
34
|
+
}
|
|
35
|
+
return runPromise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'testomatio',
|
|
40
|
+
|
|
41
|
+
onEvent(event) {
|
|
42
|
+
try {
|
|
43
|
+
if (event?.type === 'run-started') startRun();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
debug('onEvent failed', err);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async onRunFinished(run, signal) {
|
|
50
|
+
const reporter = getClient();
|
|
51
|
+
await startRun();
|
|
52
|
+
|
|
53
|
+
const { report, artifactsRoot } = run;
|
|
54
|
+
const groups = new Map((report.run.serialGroups || []).map(group => [group.id, group]));
|
|
55
|
+
|
|
56
|
+
const promises = [];
|
|
57
|
+
for (const result of report.run.results) {
|
|
58
|
+
if (signal?.aborted) break;
|
|
59
|
+
// results left out by `--last-failed` belong to the previous run
|
|
60
|
+
if (result.selected === false) continue;
|
|
61
|
+
const { status, data } = toTestData(result, groups, artifactsRoot);
|
|
62
|
+
promises.push(reporter.addTestRun(status, data));
|
|
63
|
+
}
|
|
64
|
+
await Promise.all(promises);
|
|
65
|
+
|
|
66
|
+
const duration = (Date.parse(report.run.finishedAt) - Date.parse(report.run.startedAt)) / 1000;
|
|
67
|
+
await reporter.updateRunStatus(runStatus(report.run.status), { duration });
|
|
68
|
+
|
|
69
|
+
const runUrl = reporter.pipeStore?.runUrl;
|
|
70
|
+
if (runUrl) return [{ label: 'Testomat.io', text: runUrl }];
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Converts one report-1 result into Testomat.io test data.
|
|
77
|
+
* Only the final attempt is reported; earlier attempts are counted as retries.
|
|
78
|
+
*/
|
|
79
|
+
export function toTestData(result, groups, artifactsRoot) {
|
|
80
|
+
const attempts = attemptsOf(result, groups);
|
|
81
|
+
const final = attempts.at(-1);
|
|
82
|
+
const titlePath = [...result.titlePath];
|
|
83
|
+
const title = titlePath.pop();
|
|
84
|
+
|
|
85
|
+
let suiteTitle = titlePath.join(' › ');
|
|
86
|
+
if (!suiteTitle) suiteTitle = path.basename(result.file);
|
|
87
|
+
|
|
88
|
+
const meta = {
|
|
89
|
+
target: result.targetId,
|
|
90
|
+
platform: result.platform,
|
|
91
|
+
browser: final?.environment?.browser,
|
|
92
|
+
};
|
|
93
|
+
if (result.agent && result.agent !== 'default') meta.agent = result.agent;
|
|
94
|
+
if (result.repeat) meta.repeat = result.repeat;
|
|
95
|
+
if (attempts.length > 1) meta.retryCount = attempts.length - 1;
|
|
96
|
+
if (result.status === 'flaky') meta.isFlaky = true;
|
|
97
|
+
const status = testStatus(result.status);
|
|
98
|
+
if (status === STATUS.FAILED && final?.failure?.url) meta.failedUrl = final.failure.url;
|
|
99
|
+
|
|
100
|
+
const filesById = new Map();
|
|
101
|
+
for (const artifact of final?.artifacts || []) {
|
|
102
|
+
if (artifact.path) {
|
|
103
|
+
filesById.set(artifact.id, {
|
|
104
|
+
path: path.join(artifactsRoot, artifact.path),
|
|
105
|
+
type: artifact.mediaType,
|
|
106
|
+
title: artifact.kind,
|
|
107
|
+
});
|
|
108
|
+
} else if (artifact.url) {
|
|
109
|
+
// hosted engines keep videos remotely
|
|
110
|
+
meta[artifact.kind] = artifact.url;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const files = [...filesById.values()];
|
|
114
|
+
|
|
115
|
+
const steps = (final?.steps || []).map(step => toStep(step, filesById));
|
|
116
|
+
|
|
117
|
+
const data = {
|
|
118
|
+
rid: result.id,
|
|
119
|
+
title,
|
|
120
|
+
suite_title: suiteTitle,
|
|
121
|
+
file: result.file,
|
|
122
|
+
test_id: getTestomatIdFromTestTitle(title),
|
|
123
|
+
tags: result.tags || [],
|
|
124
|
+
time: final?.durationMs || 0,
|
|
125
|
+
timestamp: timestampOf(final),
|
|
126
|
+
error: toError(final?.error, final?.secondaryErrors),
|
|
127
|
+
steps: steps.length ? steps : undefined,
|
|
128
|
+
logs: logsOf(result, final),
|
|
129
|
+
files: files.length ? files : undefined,
|
|
130
|
+
meta,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return { status, data };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Attempts of a result; a serial member keeps its part of the group's attempts. */
|
|
137
|
+
function attemptsOf(result, groups) {
|
|
138
|
+
if (!result.serialGroupId) return result.attempts || [];
|
|
139
|
+
const groupAttempts = groups.get(result.serialGroupId)?.attempts || [];
|
|
140
|
+
return groupAttempts.map(attempt => {
|
|
141
|
+
const member = attempt.members?.find(candidate => candidate.testId === result.testId);
|
|
142
|
+
return {
|
|
143
|
+
status: member?.status || attempt.status,
|
|
144
|
+
error: member?.error || attempt.error,
|
|
145
|
+
failure: member?.failure || attempt.failure,
|
|
146
|
+
steps: member?.steps || [],
|
|
147
|
+
appLog: member?.appLog || [],
|
|
148
|
+
skip: member?.skip,
|
|
149
|
+
durationMs: member?.durationMs,
|
|
150
|
+
startedAt: member?.startedAt || attempt.startedAt,
|
|
151
|
+
artifacts: attempt.artifacts || [],
|
|
152
|
+
secondaryErrors: [...(member?.secondaryErrors || []), ...(attempt.secondaryErrors || [])],
|
|
153
|
+
environment: attempt.environment,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function timestampOf(attempt) {
|
|
159
|
+
if (!attempt?.startedAt) return undefined;
|
|
160
|
+
return Date.parse(attempt.startedAt);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function testStatus(status) {
|
|
164
|
+
if (status === 'passed' || status === 'flaky') return STATUS.PASSED;
|
|
165
|
+
if (status === 'skipped') return STATUS.SKIPPED;
|
|
166
|
+
// failed, timed-out, interrupted: the test reached no passing verdict
|
|
167
|
+
return STATUS.FAILED;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runStatus(status) {
|
|
171
|
+
if (status === 'passed') return STATUS.PASSED;
|
|
172
|
+
return STATUS.FAILED;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toError(error, secondaryErrors = []) {
|
|
176
|
+
if (!error) return undefined;
|
|
177
|
+
/** @type {Error & { expected?: string, actual?: string }} */
|
|
178
|
+
const err = new Error(error.message);
|
|
179
|
+
err.name = error.code;
|
|
180
|
+
err.stack = `${error.code}: ${error.message}\n${error.stack || sourceFrame(error.source)}`;
|
|
181
|
+
if (error.details?.expected && error.details?.observed) {
|
|
182
|
+
err.expected = error.details.expected;
|
|
183
|
+
err.actual = error.details.observed;
|
|
184
|
+
}
|
|
185
|
+
for (const secondary of secondaryErrors) {
|
|
186
|
+
err.stack += `\n\nAlso failed (${secondary.phase || secondary.category}) ${secondary.code}: ${secondary.message}`;
|
|
187
|
+
}
|
|
188
|
+
return err;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sourceFrame(source) {
|
|
192
|
+
if (!source) return '';
|
|
193
|
+
return ` at ${source.file}:${source.line}:${source.column}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toStep(step, filesById) {
|
|
197
|
+
let category = 'user';
|
|
198
|
+
if (step.phase === 'beforeEach' || step.phase === 'afterEach') category = 'hook';
|
|
199
|
+
|
|
200
|
+
let title = step.api;
|
|
201
|
+
if (step.label) title = `${step.api} ${step.label}`;
|
|
202
|
+
|
|
203
|
+
const formatted = formatStep({
|
|
204
|
+
category,
|
|
205
|
+
title,
|
|
206
|
+
duration: step.durationMs,
|
|
207
|
+
log: stepLog(step),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let status = 'failed';
|
|
211
|
+
if (step.status === 'passed') status = 'passed';
|
|
212
|
+
addStatusToStep(formatted, status, step.error);
|
|
213
|
+
|
|
214
|
+
if (step.error) {
|
|
215
|
+
formatted.error = { message: truncate(`${step.error.code}: ${step.error.message}`, 250) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (SCREENSHOTS_ON_STEPS) {
|
|
219
|
+
const screenshot = (step.artifacts || []).map(id => filesById.get(id)).find(file => file?.type === 'image/png');
|
|
220
|
+
if (screenshot) addArtifactsToStep(formatted, { screenshot: screenshot.path });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return formatted;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Agent reasoning and non-binary step statuses, which a passed/failed step would lose. */
|
|
227
|
+
function stepLog(step) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
if (step.status !== 'passed' && step.status !== 'failed') lines.push(`status: ${step.status}`);
|
|
230
|
+
if (step.explanation) lines.push(step.explanation);
|
|
231
|
+
for (const event of step.events || []) {
|
|
232
|
+
if (event.reasoning) lines.push(event.reasoning);
|
|
233
|
+
}
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function logsOf(result, final) {
|
|
238
|
+
const lines = [];
|
|
239
|
+
const skip = final?.skip || result.skip;
|
|
240
|
+
if (skip) lines.push(`Skipped (${skip.cause}): ${skip.reason}`);
|
|
241
|
+
if (final?.appLog?.length) {
|
|
242
|
+
lines.push('App log:');
|
|
243
|
+
for (const line of final.appLog) lines.push(`[${line.source}] ${line.level}: ${line.text}`);
|
|
244
|
+
}
|
|
245
|
+
return lines.join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default testomatio;
|
package/src/adapter/jasmine.js
CHANGED
|
@@ -2,6 +2,8 @@ import TestomatClient from '../client.js';
|
|
|
2
2
|
import { getTestomatIdFromTestTitle, ansiRegExp } from '../utils/utils.js';
|
|
3
3
|
import { STATUS } from '../constants.js';
|
|
4
4
|
|
|
5
|
+
global.TESTOMATIO_ADAPTER = 'jasmine';
|
|
6
|
+
|
|
5
7
|
class JasmineReporter {
|
|
6
8
|
constructor(options) {
|
|
7
9
|
this.testTimeMap = {};
|
package/src/adapter/jest.js
CHANGED
|
@@ -8,6 +8,8 @@ import createDebugMessages from 'debug';
|
|
|
8
8
|
|
|
9
9
|
const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
|
|
10
10
|
|
|
11
|
+
global.TESTOMATIO_ADAPTER = 'jest';
|
|
12
|
+
|
|
11
13
|
export class JestReporter {
|
|
12
14
|
constructor(globalConfig, options) {
|
|
13
15
|
this._globalConfig = globalConfig;
|
package/src/adapter/mocha.js
CHANGED
|
@@ -6,6 +6,8 @@ import { config } from '../config.js';
|
|
|
6
6
|
import { services } from '../services/index.js';
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
|
|
9
|
+
global.TESTOMATIO_ADAPTER = 'mocha';
|
|
10
|
+
|
|
9
11
|
const {
|
|
10
12
|
EVENT_RUN_BEGIN,
|
|
11
13
|
EVENT_RUN_END,
|
|
@@ -18,157 +20,160 @@ const {
|
|
|
18
20
|
EVENT_TEST_END,
|
|
19
21
|
} = Mocha.Runner.constants;
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
23
|
+
// ES class so it works with mocha 12, where Base is a class and utils.inherits is gone
|
|
24
|
+
class MochaReporter extends Mocha.reporters.Base {
|
|
25
|
+
constructor(runner, opts) {
|
|
26
|
+
super(runner, opts);
|
|
27
|
+
let passes = 0;
|
|
28
|
+
let failures = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
// let artifactStore;
|
|
31
|
+
|
|
32
|
+
const apiKey = opts?.reporterOptions?.apiKey || config.TESTOMATIO;
|
|
33
|
+
|
|
34
|
+
const client = new TestomatClient({ apiKey });
|
|
35
|
+
|
|
36
|
+
// Track hook failures
|
|
37
|
+
const hookFailures = new Map();
|
|
38
|
+
|
|
39
|
+
runner.on(EVENT_RUN_BEGIN, () => {
|
|
40
|
+
client.createRun();
|
|
41
|
+
|
|
42
|
+
// clear dir with artifacts/logs
|
|
43
|
+
fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
runner.on(EVENT_SUITE_BEGIN, async suite => {
|
|
47
|
+
services.setContext(suite.fullTitle());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
runner.on(EVENT_SUITE_END, async suite => {
|
|
51
|
+
if (hookFailures.has(suite.fullTitle())) {
|
|
52
|
+
const { error, suiteTitle } = hookFailures.get(suite.fullTitle());
|
|
53
|
+
|
|
54
|
+
for (const test of suite.tests) {
|
|
55
|
+
if (test.state === 'pending' || !test.state) {
|
|
56
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
57
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
58
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
59
|
+
const links = services.links.get(test.fullTitle());
|
|
60
|
+
|
|
61
|
+
client.addTestRun(STATUS.FAILED, {
|
|
62
|
+
error,
|
|
63
|
+
suite_title: suiteTitle || suite.title,
|
|
64
|
+
file: suite.file,
|
|
65
|
+
test_id: testId,
|
|
66
|
+
title: test.title,
|
|
67
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
68
|
+
time: 0,
|
|
69
|
+
manuallyAttachedArtifacts: artifacts,
|
|
70
|
+
meta: keyValues,
|
|
71
|
+
links,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
services.setContext(null);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
runner.on(EVENT_TEST_PASS, async test => {
|
|
85
|
-
passes += 1;
|
|
86
|
-
|
|
87
|
-
console.log(pc.bold(pc.green('✔')), test.fullTitle());
|
|
88
|
-
const testId = getTestomatIdFromTestTitle(test.title);
|
|
89
|
-
|
|
90
|
-
const logs = getTestLogs(test);
|
|
91
|
-
const artifacts = services.artifacts.get(test.fullTitle());
|
|
92
|
-
const keyValues = services.keyValues.get(test.fullTitle());
|
|
93
|
-
const links = services.links.get(test.fullTitle());
|
|
94
|
-
|
|
95
|
-
client.addTestRun(STATUS.PASSED, {
|
|
96
|
-
test_id: testId,
|
|
97
|
-
suite_title: getSuiteTitle(test),
|
|
98
|
-
title: getTestName(test),
|
|
99
|
-
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
100
|
-
file: getFile(test),
|
|
101
|
-
time: test.duration,
|
|
102
|
-
logs,
|
|
103
|
-
manuallyAttachedArtifacts: artifacts,
|
|
104
|
-
meta: keyValues,
|
|
105
|
-
links,
|
|
76
|
+
|
|
77
|
+
services.setContext(null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
runner.on(EVENT_TEST_BEGIN, async test => {
|
|
81
|
+
services.setContext(test.fullTitle());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
runner.on(EVENT_TEST_END, async () => {
|
|
85
|
+
services.setContext(null);
|
|
106
86
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
87
|
+
|
|
88
|
+
runner.on(EVENT_TEST_PASS, async test => {
|
|
89
|
+
passes += 1;
|
|
90
|
+
|
|
91
|
+
console.log(pc.bold(pc.green('✔')), test.fullTitle());
|
|
92
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
93
|
+
|
|
94
|
+
const logs = getTestLogs(test);
|
|
95
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
96
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
97
|
+
const links = services.links.get(test.fullTitle());
|
|
98
|
+
|
|
99
|
+
client.addTestRun(STATUS.PASSED, {
|
|
100
|
+
test_id: testId,
|
|
101
|
+
suite_title: getSuiteTitle(test),
|
|
102
|
+
title: getTestName(test),
|
|
103
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
104
|
+
file: getFile(test),
|
|
105
|
+
time: test.duration,
|
|
106
|
+
logs,
|
|
107
|
+
manuallyAttachedArtifacts: artifacts,
|
|
108
|
+
meta: keyValues,
|
|
109
|
+
links,
|
|
110
|
+
});
|
|
127
111
|
});
|
|
128
|
-
});
|
|
129
112
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
runner.on(EVENT_TEST_PENDING, test => {
|
|
114
|
+
skipped += 1;
|
|
115
|
+
console.log('skip: %s', test.fullTitle());
|
|
116
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
117
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
118
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
119
|
+
const links = services.links.get(test.fullTitle());
|
|
120
|
+
|
|
121
|
+
client.addTestRun(STATUS.SKIPPED, {
|
|
122
|
+
title: getTestName(test),
|
|
123
|
+
suite_title: getSuiteTitle(test),
|
|
124
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
125
|
+
file: getFile(test),
|
|
126
|
+
test_id: testId,
|
|
127
|
+
time: test.duration,
|
|
128
|
+
manuallyAttachedArtifacts: artifacts,
|
|
129
|
+
meta: keyValues,
|
|
130
|
+
links,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
runner.on(EVENT_TEST_FAIL, async (test, err) => {
|
|
135
|
+
failures += 1;
|
|
136
|
+
console.log(pc.bold(pc.red('✖')), test.fullTitle(), pc.gray(err.message));
|
|
137
|
+
|
|
138
|
+
const isHookFailure = test.title.includes('before each') || test.title.includes('after each');
|
|
133
139
|
|
|
134
|
-
|
|
140
|
+
if (isHookFailure && test.parent) {
|
|
141
|
+
hookFailures.set(test.parent.fullTitle(), {
|
|
142
|
+
error: err,
|
|
143
|
+
suiteTitle: getSuiteTitle(test),
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const testId = getTestomatIdFromTestTitle(test.title);
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
const logs = getTestLogs(test);
|
|
151
|
+
const artifacts = services.artifacts.get(test.fullTitle());
|
|
152
|
+
const keyValues = services.keyValues.get(test.fullTitle());
|
|
153
|
+
const links = services.links.get(test.fullTitle());
|
|
154
|
+
|
|
155
|
+
client.addTestRun(STATUS.FAILED, {
|
|
138
156
|
error: err,
|
|
139
|
-
|
|
157
|
+
suite_title: getSuiteTitle(test),
|
|
158
|
+
file: getFile(test),
|
|
159
|
+
test_id: testId,
|
|
160
|
+
title: getTestName(test),
|
|
161
|
+
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
162
|
+
time: test.duration,
|
|
163
|
+
logs,
|
|
164
|
+
manuallyAttachedArtifacts: artifacts,
|
|
165
|
+
meta: keyValues,
|
|
166
|
+
links,
|
|
140
167
|
});
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const testId = getTestomatIdFromTestTitle(test.title);
|
|
145
|
-
|
|
146
|
-
const logs = getTestLogs(test);
|
|
147
|
-
const artifacts = services.artifacts.get(test.fullTitle());
|
|
148
|
-
const keyValues = services.keyValues.get(test.fullTitle());
|
|
149
|
-
const links = services.links.get(test.fullTitle());
|
|
150
|
-
|
|
151
|
-
client.addTestRun(STATUS.FAILED, {
|
|
152
|
-
error: err,
|
|
153
|
-
suite_title: getSuiteTitle(test),
|
|
154
|
-
file: getFile(test),
|
|
155
|
-
test_id: testId,
|
|
156
|
-
title: getTestName(test),
|
|
157
|
-
code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
|
|
158
|
-
time: test.duration,
|
|
159
|
-
logs,
|
|
160
|
-
manuallyAttachedArtifacts: artifacts,
|
|
161
|
-
meta: keyValues,
|
|
162
|
-
links,
|
|
163
168
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
169
|
+
|
|
170
|
+
runner.on(EVENT_RUN_END, () => {
|
|
171
|
+
const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
|
|
172
|
+
console.log(pc.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
client.updateRunStatus(status);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
function getTestLogs(test) {
|
|
@@ -204,8 +209,4 @@ function getTestName(test) {
|
|
|
204
209
|
return test.title;
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
// To have this reporter "extend" a built-in reporter uncomment the following line:
|
|
208
|
-
// @ts-ignore
|
|
209
|
-
Mocha.utils.inherits(MochaReporter, Mocha.reporters.Spec);
|
|
210
|
-
|
|
211
212
|
export default MochaReporter;
|