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