@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
@@ -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
- function MochaReporter(runner, opts) {
22
- Mocha.reporters.Base.call(this, runner);
23
- let passes = 0;
24
- let failures = 0;
25
- let skipped = 0;
26
- // let artifactStore;
27
-
28
- const apiKey = opts?.reporterOptions?.apiKey || config.TESTOMATIO;
29
-
30
- const client = new TestomatClient({ apiKey });
31
-
32
- // Track hook failures
33
- const hookFailures = new Map();
34
-
35
- runner.on(EVENT_RUN_BEGIN, () => {
36
- client.createRun();
37
-
38
- // clear dir with artifacts/logs
39
- fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
40
- });
41
-
42
- runner.on(EVENT_SUITE_BEGIN, async suite => {
43
- services.setContext(suite.fullTitle());
44
- });
45
-
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
- });
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
- services.setContext(null);
74
- });
75
-
76
- runner.on(EVENT_TEST_BEGIN, async test => {
77
- services.setContext(test.fullTitle());
78
- });
79
-
80
- runner.on(EVENT_TEST_END, async () => {
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
- runner.on(EVENT_TEST_PENDING, test => {
110
- skipped += 1;
111
- console.log('skip: %s', test.fullTitle());
112
- const testId = getTestomatIdFromTestTitle(test.title);
113
- const artifacts = services.artifacts.get(test.fullTitle());
114
- const keyValues = services.keyValues.get(test.fullTitle());
115
- const links = services.links.get(test.fullTitle());
116
-
117
- client.addTestRun(STATUS.SKIPPED, {
118
- title: getTestName(test),
119
- suite_title: getSuiteTitle(test),
120
- code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
121
- file: getFile(test),
122
- test_id: testId,
123
- time: test.duration,
124
- manuallyAttachedArtifacts: artifacts,
125
- meta: keyValues,
126
- links,
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
- runner.on(EVENT_TEST_FAIL, async (test, err) => {
131
- failures += 1;
132
- console.log(pc.bold(pc.red('✖')), test.fullTitle(), pc.gray(err.message));
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
- const isHookFailure = test.title.includes('before each') || test.title.includes('after each');
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
- if (isHookFailure && test.parent) {
137
- hookFailures.set(test.parent.fullTitle(), {
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
- suiteTitle: getSuiteTitle(test),
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
- runner.on(EVENT_RUN_END, () => {
167
- const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
168
- console.log(pc.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
169
- // @ts-ignore
170
- client.updateRunStatus(status);
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;
@@ -3,6 +3,8 @@ import { config } from '../config.js';
3
3
  import { STATUS } from '../constants.js';
4
4
  import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
5
 
6
+ global.TESTOMATIO_ADAPTER = 'nightwatch';
7
+
6
8
  const apiKey = config.TESTOMATIO;
7
9
  const client = new TestomatClient({ apiKey });
8
10
 
@@ -14,6 +14,8 @@ import { fetchLinksFromLogs } from './utils/playwright.js';
14
14
  import { formatStep, addStatusToStep, addArtifactsToStep } from './utils/step-formatter.js';
15
15
  import { log } from '../utils/log.js';
16
16
 
17
+ global.TESTOMATIO_ADAPTER = 'playwright';
18
+
17
19
  class PlaywrightReporter {
18
20
  constructor(config = {}) {
19
21
  this.client = new TestomatioClient({ apiKey: config?.apiKey });
@@ -108,7 +110,11 @@ class PlaywrightReporter {
108
110
  if (result.status === 'passed') status = 'failed';
109
111
  }
110
112
 
111
- const reportTestPromise = this.client.addTestRun(checkStatus(status), {
113
+ const testStatus = checkStatus(status);
114
+ let failedUrl;
115
+ if (testStatus === Status.FAILED) failedUrl = lastNavigatedUrl(result.steps, test.parent.project().use.baseURL);
116
+
117
+ const reportTestPromise = this.client.addTestRun(testStatus, {
112
118
  rid: `${rid}-${project.name}`,
113
119
  error,
114
120
  test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
@@ -126,6 +132,7 @@ class PlaywrightReporter {
126
132
  isMobile: project.isMobile,
127
133
  project: project.name,
128
134
  projectDependencies: project.dependencies?.length ? project.dependencies : null,
135
+ failedUrl,
129
136
  ...testMeta,
130
137
  ...meta,
131
138
  ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
@@ -242,6 +249,31 @@ function isScreenshotArtifact(attachment) {
242
249
  return attachment?.contentType === 'image/png' && attachment?.name === 'screenshot';
243
250
  }
244
251
 
252
+ /**
253
+ * Last url the test navigated to; the reporter has no access to the page itself.
254
+ * @returns {string|undefined}
255
+ */
256
+ function lastNavigatedUrl(steps, baseURL) {
257
+ const url = findLastNavigation(steps);
258
+ if (!url) return;
259
+ try {
260
+ return new URL(url, baseURL).href;
261
+ } catch {
262
+ return url;
263
+ }
264
+ }
265
+
266
+ function findLastNavigation(steps = []) {
267
+ let url;
268
+ for (const step of steps) {
269
+ // 'Navigate to "url"' since Playwright 1.53, 'page.goto(url)' before
270
+ const match = step.title.match(/^Navigate to "(.+)"$/) || step.title.match(/^page\.goto\((.+)\)$/);
271
+ if (step.category === 'pw:api' && match) url = match[1];
272
+ url = findLastNavigation(step.steps) || url;
273
+ }
274
+ return url;
275
+ }
276
+
245
277
  function appendStep(step, shift = 0) {
246
278
  // nesting too deep, ignore those steps
247
279
  if (shift >= 10) return;
@@ -6,6 +6,8 @@ import createDebugMessages from 'debug';
6
6
 
7
7
  const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
8
8
 
9
+ global.TESTOMATIO_ADAPTER = 'vitest';
10
+
9
11
  /**
10
12
  * @typedef {import('../../types/types.js').VitestTest} VitestTest
11
13
  * @typedef {import('../../types/types.js').VitestTestFile} VitestTestFile
@@ -396,7 +398,10 @@ function normalizeVitestTest(test) {
396
398
  file,
397
399
  suiteTitle,
398
400
  logs: '',
399
- meta: typeof test.meta === 'function' ? test.meta() : {},
401
+ meta: {
402
+ ...(test.task?.meta || {}),
403
+ ...(typeof test.meta === 'function' ? test.meta() : test.meta || {}),
404
+ },
400
405
  };
401
406
  }
402
407
 
@@ -4,6 +4,11 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
4
4
  import { services } from '../services/index.js';
5
5
  import { TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
6
6
  import { stringToMD5Hash } from '../data-storage.js';
7
+ import createDebugMessages from 'debug';
8
+
9
+ const debug = createDebugMessages('@testomatio/reporter:adapter:webdriver');
10
+
11
+ global.TESTOMATIO_ADAPTER = 'webdriverio';
7
12
 
8
13
  class WebdriverReporter extends WDIOReporter {
9
14
  constructor(options) {
@@ -120,7 +125,8 @@ class WebdriverReporter extends WDIOReporter {
120
125
  async addTest(test) {
121
126
  if (!this.client) return;
122
127
 
123
- const { title, _duration: duration, state, error, output, links, artifacts, meta, logs } = test;
128
+ const { title, _duration: duration, state, error, output, links, artifacts, logs } = test;
129
+ let { meta } = test;
124
130
 
125
131
  const testId = getTestomatIdFromTestTitle(title);
126
132
 
@@ -131,6 +137,11 @@ class WebdriverReporter extends WDIOReporter {
131
137
 
132
138
  const rid = stringToMD5Hash(test.fullTitle);
133
139
 
140
+ if (state === 'failed') {
141
+ const failedUrl = (await grabCurrentUrl()) || lastNavigatedUrl(output);
142
+ if (failedUrl) meta = { ...meta, failedUrl };
143
+ }
144
+
134
145
  await this.client.addTestRun(state, {
135
146
  rid,
136
147
  manuallyAttachedArtifacts: test.artifacts,
@@ -148,7 +159,7 @@ class WebdriverReporter extends WDIOReporter {
148
159
  /**
149
160
  * @param {import('../../types/types.js').WebdriverIOScenario} scenario
150
161
  */
151
- addBddScenario(scenario) {
162
+ async addBddScenario(scenario) {
152
163
  if (!this.client) return;
153
164
 
154
165
  const { title, _duration: duration } = scenario;
@@ -167,6 +178,12 @@ class WebdriverReporter extends WDIOReporter {
167
178
 
168
179
  const tags = scenario.tags.map(tag => tag.name);
169
180
 
181
+ let meta;
182
+ if (scenarioState === 'failed') {
183
+ const failedUrl = await grabCurrentUrl();
184
+ if (failedUrl) meta = { failedUrl };
185
+ }
186
+
170
187
  return this.client.addTestRun(scenarioState, {
171
188
  error: error ? Error(error) : null,
172
189
  title,
@@ -174,11 +191,38 @@ class WebdriverReporter extends WDIOReporter {
174
191
  time: duration,
175
192
  tags,
176
193
  file: scenario.file,
194
+ meta,
177
195
  // filesBuffers: screenshotsBuffers,
178
196
  });
179
197
  }
180
198
  }
181
199
 
200
+ /**
201
+ * Current url of the browser in this worker; reporters run in the worker process.
202
+ * @returns {Promise<string|undefined>}
203
+ */
204
+ async function grabCurrentUrl() {
205
+ // @ts-ignore
206
+ const browser = globalThis.browser;
207
+ if (typeof browser?.getUrl !== 'function') return;
208
+ try {
209
+ const url = await browser.getUrl();
210
+ // multiremote returns a url per browser
211
+ if (typeof url === 'string') return url;
212
+ } catch (err) {
213
+ debug('Failed to grab url of failed test', err?.message || err);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Last url opened by the test, taken from its command log.
219
+ * @returns {string|undefined}
220
+ */
221
+ function lastNavigatedUrl(output = []) {
222
+ const navigations = output.filter(el => el.endpoint === '/session/:sessionId/url' && el.body?.url);
223
+ return navigations.at(-1)?.body.url;
224
+ }
225
+
182
226
  /**
183
227
  *
184
228
  * @param {*} fullTestTitle
@@ -186,7 +186,8 @@ class AllureReader {
186
186
  steps: this.convertSteps(result.steps || [], resultsDir),
187
187
  message: result.statusDetails?.message || '',
188
188
  stack: result.statusDetails?.trace || '',
189
- meta: this.extractMeta(result),
189
+ // meta must always include reporter version and adapter used
190
+ meta: { ...this.extractMeta(result), reporterVersion: this.version, adapter: 'allure' },
190
191
  links: this.extractLinks(result),
191
192
  artifacts: [],
192
193
  create: true,