@testomatio/reporter 2.0.1-beta.5-timestamp → 2.0.1-beta.6

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 (149) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.d.ts +2 -0
  3. package/lib/adapter/codecept.js +293 -335
  4. package/lib/adapter/cucumber/current.d.ts +14 -0
  5. package/lib/adapter/cucumber/current.js +195 -203
  6. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  7. package/lib/adapter/cucumber/legacy.js +130 -155
  8. package/lib/adapter/cucumber.d.ts +2 -0
  9. package/lib/adapter/cucumber.js +5 -16
  10. package/lib/adapter/cypress-plugin/index.d.ts +2 -0
  11. package/lib/adapter/cypress-plugin/index.js +91 -105
  12. package/lib/adapter/jasmine.d.ts +11 -0
  13. package/lib/adapter/jasmine.js +54 -53
  14. package/lib/adapter/jest.d.ts +13 -0
  15. package/lib/adapter/jest.js +97 -99
  16. package/lib/adapter/mocha.d.ts +2 -0
  17. package/lib/adapter/mocha.js +112 -141
  18. package/lib/adapter/nightwatch.d.ts +4 -0
  19. package/lib/adapter/nightwatch.js +80 -0
  20. package/lib/adapter/playwright.d.ts +14 -0
  21. package/lib/adapter/playwright.js +199 -231
  22. package/lib/adapter/vitest.d.ts +35 -0
  23. package/lib/adapter/vitest.js +150 -149
  24. package/lib/adapter/webdriver.d.ts +24 -0
  25. package/lib/adapter/webdriver.js +144 -121
  26. package/lib/bin/cli.d.ts +2 -0
  27. package/lib/bin/cli.js +229 -211
  28. package/lib/bin/reportXml.d.ts +2 -0
  29. package/lib/bin/reportXml.js +51 -52
  30. package/lib/bin/startTest.d.ts +2 -0
  31. package/lib/bin/startTest.js +83 -95
  32. package/lib/bin/uploadArtifacts.d.ts +2 -0
  33. package/lib/bin/uploadArtifacts.js +56 -61
  34. package/lib/client.d.ts +76 -0
  35. package/lib/client.js +429 -465
  36. package/lib/config.d.ts +1 -0
  37. package/lib/config.js +18 -23
  38. package/lib/constants.d.ts +25 -0
  39. package/lib/constants.js +50 -44
  40. package/lib/data-storage.d.ts +34 -0
  41. package/lib/data-storage.js +216 -188
  42. package/lib/junit-adapter/adapter.d.ts +9 -0
  43. package/lib/junit-adapter/adapter.js +17 -20
  44. package/lib/junit-adapter/csharp.d.ts +5 -0
  45. package/lib/junit-adapter/csharp.js +28 -14
  46. package/lib/junit-adapter/index.d.ts +3 -0
  47. package/lib/junit-adapter/index.js +27 -25
  48. package/lib/junit-adapter/java.d.ts +5 -0
  49. package/lib/junit-adapter/java.js +41 -53
  50. package/lib/junit-adapter/javascript.d.ts +4 -0
  51. package/lib/junit-adapter/javascript.js +30 -27
  52. package/lib/junit-adapter/python.d.ts +5 -0
  53. package/lib/junit-adapter/python.js +38 -37
  54. package/lib/junit-adapter/ruby.d.ts +4 -0
  55. package/lib/junit-adapter/ruby.js +11 -8
  56. package/lib/output.d.ts +11 -0
  57. package/lib/output.js +44 -52
  58. package/lib/package.json +3 -0
  59. package/lib/pipe/bitbucket.d.ts +25 -0
  60. package/lib/pipe/bitbucket.js +223 -230
  61. package/lib/pipe/csv.d.ts +47 -0
  62. package/lib/pipe/csv.js +113 -126
  63. package/lib/pipe/debug.d.ts +29 -0
  64. package/lib/pipe/debug.js +125 -99
  65. package/lib/pipe/github.d.ts +30 -0
  66. package/lib/pipe/github.js +218 -213
  67. package/lib/pipe/gitlab.d.ts +25 -0
  68. package/lib/pipe/gitlab.js +183 -206
  69. package/lib/pipe/html.d.ts +35 -0
  70. package/lib/pipe/html.js +258 -321
  71. package/lib/pipe/index.d.ts +1 -0
  72. package/lib/pipe/index.js +94 -66
  73. package/lib/pipe/testomatio.d.ts +71 -0
  74. package/lib/pipe/testomatio.js +429 -474
  75. package/lib/replay.d.ts +31 -0
  76. package/lib/replay.js +255 -0
  77. package/lib/reporter-functions.d.ts +34 -0
  78. package/lib/reporter-functions.js +28 -26
  79. package/lib/reporter.d.ts +232 -0
  80. package/lib/reporter.js +34 -29
  81. package/lib/services/artifacts.d.ts +33 -0
  82. package/lib/services/artifacts.js +55 -51
  83. package/lib/services/index.d.ts +9 -0
  84. package/lib/services/index.js +14 -12
  85. package/lib/services/key-values.d.ts +27 -0
  86. package/lib/services/key-values.js +56 -53
  87. package/lib/services/logger.d.ts +64 -0
  88. package/lib/services/logger.js +226 -245
  89. package/lib/template/testomatio.hbs +1026 -1366
  90. package/lib/uploader.d.ts +60 -0
  91. package/lib/uploader.js +295 -364
  92. package/lib/utils/pipe_utils.d.ts +41 -0
  93. package/lib/utils/pipe_utils.js +89 -85
  94. package/lib/utils/utils.d.ts +54 -0
  95. package/lib/utils/utils.js +398 -307
  96. package/lib/xmlReader.d.ts +92 -0
  97. package/lib/xmlReader.js +525 -532
  98. package/package.json +64 -21
  99. package/src/adapter/codecept.js +373 -0
  100. package/src/adapter/cucumber/current.js +228 -0
  101. package/src/adapter/cucumber/legacy.js +158 -0
  102. package/src/adapter/cucumber.js +4 -0
  103. package/src/adapter/cypress-plugin/index.js +110 -0
  104. package/src/adapter/jasmine.js +60 -0
  105. package/src/adapter/jest.js +107 -0
  106. package/src/adapter/mocha.cjs +2 -0
  107. package/src/adapter/mocha.js +156 -0
  108. package/src/adapter/nightwatch.js +88 -0
  109. package/src/adapter/playwright.js +254 -0
  110. package/src/adapter/vitest.js +183 -0
  111. package/src/adapter/webdriver.js +142 -0
  112. package/src/bin/cli.js +348 -0
  113. package/src/bin/reportXml.js +77 -0
  114. package/src/bin/startTest.js +124 -0
  115. package/src/bin/uploadArtifacts.js +91 -0
  116. package/src/client.js +515 -0
  117. package/src/config.js +30 -0
  118. package/src/constants.js +53 -0
  119. package/src/data-storage.js +204 -0
  120. package/src/junit-adapter/adapter.js +23 -0
  121. package/src/junit-adapter/csharp.js +28 -0
  122. package/src/junit-adapter/index.js +28 -0
  123. package/src/junit-adapter/java.js +58 -0
  124. package/src/junit-adapter/javascript.js +31 -0
  125. package/src/junit-adapter/python.js +42 -0
  126. package/src/junit-adapter/ruby.js +10 -0
  127. package/src/output.js +57 -0
  128. package/src/pipe/bitbucket.js +252 -0
  129. package/src/pipe/csv.js +140 -0
  130. package/src/pipe/debug.js +125 -0
  131. package/src/pipe/github.js +232 -0
  132. package/src/pipe/gitlab.js +247 -0
  133. package/src/pipe/html.js +373 -0
  134. package/src/pipe/index.js +71 -0
  135. package/src/pipe/testomatio.js +504 -0
  136. package/src/replay.js +262 -0
  137. package/src/reporter-functions.js +55 -0
  138. package/src/reporter.cjs_decprecated +21 -0
  139. package/src/reporter.js +33 -0
  140. package/src/services/artifacts.js +59 -0
  141. package/src/services/index.js +13 -0
  142. package/src/services/key-values.js +59 -0
  143. package/src/services/logger.js +315 -0
  144. package/src/template/emptyData.svg +23 -0
  145. package/src/template/testomatio.hbs +1081 -0
  146. package/src/uploader.js +376 -0
  147. package/src/utils/pipe_utils.js +119 -0
  148. package/src/utils/utils.js +416 -0
  149. package/src/xmlReader.js +614 -0
@@ -0,0 +1,156 @@
1
+ import Mocha from 'mocha';
2
+ import TestomatClient from '../client.js';
3
+ import { STATUS, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
5
+ import { config } from '../config.js';
6
+ import { services } from '../services/index.js';
7
+ import pc from 'picocolors';
8
+
9
+ const {
10
+ EVENT_RUN_BEGIN,
11
+ EVENT_RUN_END,
12
+ EVENT_TEST_FAIL,
13
+ EVENT_TEST_PASS,
14
+ EVENT_TEST_PENDING,
15
+ EVENT_SUITE_BEGIN,
16
+ EVENT_SUITE_END,
17
+ EVENT_TEST_BEGIN,
18
+ EVENT_TEST_END,
19
+ } = Mocha.Runner.constants;
20
+
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
+ runner.on(EVENT_RUN_BEGIN, () => {
33
+ client.createRun();
34
+
35
+ // clear dir with artifacts/logs
36
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
37
+ });
38
+
39
+ runner.on(EVENT_SUITE_BEGIN, async suite => {
40
+ services.setContext(suite.fullTitle());
41
+ });
42
+
43
+ runner.on(EVENT_SUITE_END, async () => {
44
+ services.setContext(null);
45
+ });
46
+
47
+ runner.on(EVENT_TEST_BEGIN, async test => {
48
+ services.setContext(test.fullTitle());
49
+ });
50
+
51
+ runner.on(EVENT_TEST_END, async () => {
52
+ services.setContext(null);
53
+ });
54
+
55
+ runner.on(EVENT_TEST_PASS, async test => {
56
+ passes += 1;
57
+
58
+ console.log(pc.bold(pc.green('✔')), test.fullTitle());
59
+ const testId = getTestomatIdFromTestTitle(test.title);
60
+
61
+ const logs = getTestLogs(test);
62
+ const artifacts = services.artifacts.get(test.fullTitle());
63
+ const keyValues = services.keyValues.get(test.fullTitle());
64
+
65
+ client.addTestRun(STATUS.PASSED, {
66
+ test_id: testId,
67
+ suite_title: getSuiteTitle(test),
68
+ title: getTestName(test),
69
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
70
+ file: getFile(test),
71
+ time: test.duration,
72
+ logs,
73
+ manuallyAttachedArtifacts: artifacts,
74
+ meta: keyValues,
75
+ });
76
+ });
77
+
78
+ runner.on(EVENT_TEST_PENDING, test => {
79
+ skipped += 1;
80
+ console.log('skip: %s', test.fullTitle());
81
+ const testId = getTestomatIdFromTestTitle(test.title);
82
+ client.addTestRun(STATUS.SKIPPED, {
83
+ title: getTestName(test),
84
+ suite_title: getSuiteTitle(test),
85
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
86
+ file: getFile(test),
87
+ test_id: testId,
88
+ time: test.duration,
89
+ });
90
+ });
91
+
92
+ runner.on(EVENT_TEST_FAIL, async (test, err) => {
93
+ failures += 1;
94
+ console.log(pc.bold(pc.red('✖')), test.fullTitle(), pc.gray(err.message));
95
+ const testId = getTestomatIdFromTestTitle(test.title);
96
+
97
+ const logs = getTestLogs(test);
98
+
99
+ client.addTestRun(STATUS.FAILED, {
100
+ error: err,
101
+ suite_title: getSuiteTitle(test),
102
+ file: getFile(test),
103
+ test_id: testId,
104
+ title: getTestName(test),
105
+ code: process.env.TESTOMATIO_UPDATE_CODE ? test.body.toString() : '',
106
+ time: test.duration,
107
+ logs,
108
+ });
109
+ });
110
+
111
+ runner.on(EVENT_RUN_END, () => {
112
+ const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
113
+ console.log(pc.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
114
+ // @ts-ignore
115
+ client.updateRunStatus(status);
116
+ });
117
+ }
118
+
119
+ function getTestLogs(test) {
120
+ const suiteLogsArr = services.logger.getLogs(test.parent.fullTitle());
121
+ const suiteLogs = suiteLogsArr ? suiteLogsArr.join('\n').trim() : '';
122
+ const testLogsArr = services.logger.getLogs(test.fullTitle());
123
+ const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
124
+
125
+ let logs = '';
126
+ if (suiteLogs) {
127
+ logs += `${pc.bold('\t--- BeforeSuite ---')}\n${suiteLogs}`;
128
+ }
129
+ if (testLogs) {
130
+ logs += `\n${pc.bold('\t--- Test ---')}\n${testLogs}`;
131
+ }
132
+ return logs;
133
+ }
134
+
135
+ function getSuiteTitle(test, pathArr = []) {
136
+ if (test.parent.parent) getSuiteTitle(test.parent, pathArr);
137
+
138
+ pathArr.push(test.parent.title);
139
+
140
+ return pathArr.filter(t => !!t)[0];
141
+ }
142
+
143
+ function getFile(test) {
144
+ return test.parent.file?.replace(process.cwd(), '');
145
+ }
146
+
147
+ function getTestName(test) {
148
+ if (process.env.TESTOMATIO_CREATE === 'fulltitle') return test.fullTitle();
149
+ return test.title;
150
+ }
151
+
152
+ // To have this reporter "extend" a built-in reporter uncomment the following line:
153
+ // @ts-ignore
154
+ Mocha.utils.inherits(MochaReporter, Mocha.reporters.Spec);
155
+
156
+ export default MochaReporter;
@@ -0,0 +1,88 @@
1
+ import TestomatClient from '../client.js';
2
+ import { config } from '../config.js';
3
+ import { STATUS } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
+
6
+ const apiKey = config.TESTOMATIO;
7
+ const client = new TestomatClient({ apiKey });
8
+
9
+ export default {
10
+ write: async (results, options, done) => {
11
+ await client.createRun();
12
+
13
+ const testFiles = results.modules;
14
+
15
+ for (const fileName in testFiles) {
16
+ // in nightwatch: object containing tests from a single file
17
+ const testModule = testFiles[fileName];
18
+
19
+ // passed and failed tests (tests with assertions)
20
+ const completedTests = testModule.completed;
21
+
22
+ // skipped tests (skipped by user or tests without assertions)
23
+ const skippedTests = testModule.skipped;
24
+
25
+ const tags = testModule.tags || [];
26
+
27
+ // if test file contains multiple suites, the last suite name is used as a name 🤷‍♂️
28
+ // no other places which contain suite name (even inside test object)
29
+ const suiteTitle = testModule.name;
30
+
31
+ for (const testTitle in completedTests) {
32
+ const test = completedTests[testTitle];
33
+ let status;
34
+ switch (test.status) {
35
+ case 'pass':
36
+ status = STATUS.PASSED;
37
+ break;
38
+ case 'fail':
39
+ status = STATUS.FAILED;
40
+ break;
41
+ // probably not required (because skipped tests are in separate array), but just in case
42
+ case 'skip':
43
+ status = STATUS.SKIPPED;
44
+ console.info('Skipped test is in completed tests array:', test, 'Not expected behavior.');
45
+ break;
46
+ default:
47
+ console.error('Test status processing error:', test.status);
48
+ }
49
+
50
+ const testId = getTestomatIdFromTestTitle(testTitle);
51
+
52
+ client.addTestRun(status, {
53
+ error: { name: test.assertions?.[0]?.name, message: test.assertions?.[0]?.message, stack: test.stackTrace },
54
+ file: testModule.modulePath?.replace(process.cwd(), ''),
55
+ message: test.assertions?.[0]?.message,
56
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
57
+ stack: test.stackTrace,
58
+ suite_title: suiteTitle,
59
+ tags,
60
+ test_id: testId,
61
+ time: test.timeMs,
62
+ title: testTitle,
63
+ });
64
+ }
65
+
66
+ // just array with skipped tests titles, no any other info
67
+ for (const testTitle of skippedTests) {
68
+ client.addTestRun(STATUS.SKIPPED, {
69
+ suite_title: suiteTitle,
70
+ tags,
71
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
72
+ title: testTitle,
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @type {'passed' | 'failed' | 'finished'}
79
+ */
80
+ let runStatus = 'finished';
81
+ if (results.failed) runStatus = 'failed';
82
+ else if (results.passed) runStatus = 'passed';
83
+
84
+ await client.updateRunStatus(runStatus);
85
+
86
+ done();
87
+ },
88
+ };
@@ -0,0 +1,254 @@
1
+ import pc from 'picocolors';
2
+ import crypto from 'crypto';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import fs from 'fs';
7
+ import { APP_PREFIX, STATUS as Status, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
8
+ import TestomatioClient from '../client.js';
9
+ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
10
+ import { services } from '../services/index.js';
11
+ import { dataStorage } from '../data-storage.js';
12
+
13
+ const reportTestPromises = [];
14
+
15
+ class PlaywrightReporter {
16
+ constructor(config = {}) {
17
+ this.client = new TestomatioClient({ apiKey: config?.apiKey });
18
+
19
+ this.uploads = [];
20
+ }
21
+
22
+ onBegin(config, suite) {
23
+ // clean data storage
24
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
25
+ if (!this.client) return;
26
+ this.suite = suite;
27
+ this.config = config;
28
+ this.client.createRun();
29
+ }
30
+
31
+ onTestBegin(testInfo) {
32
+ const fullTestTitle = getTestContextName(testInfo);
33
+ dataStorage.setContext(fullTestTitle);
34
+ }
35
+
36
+ onTestEnd(test, result) {
37
+ // test.parent.project().__projectId
38
+
39
+ if (!this.client) return;
40
+
41
+ const { title } = test;
42
+ const { error, duration } = result;
43
+ const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
44
+
45
+ const steps = [];
46
+ for (const step of result.steps) {
47
+ const appendedStep = appendStep(step);
48
+ if (appendedStep) {
49
+ steps.push(appendedStep);
50
+ }
51
+ }
52
+
53
+ const fullTestTitle = getTestContextName(test);
54
+ let logs = '';
55
+ if (result.stderr.length || result.stdout.length) {
56
+ logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
57
+ }
58
+ const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
59
+ const testMeta = services.keyValues.get(fullTestTitle);
60
+ const rid = test.id || test.testId || uuidv4();
61
+
62
+ /**
63
+ * @type {{
64
+ * browser?: string,
65
+ * dependencies: string[],
66
+ * isMobile?: boolean
67
+ * metadata: Record<string, any>,
68
+ * name: string,
69
+ * }}
70
+ */
71
+ const project = {
72
+ browser: test.parent.project().use.defaultBrowserType,
73
+ dependencies: test.parent.project().dependencies,
74
+ isMobile: test.parent.project().use.isMobile,
75
+ metadata: test.parent.project().metadata,
76
+ name: test.parent.project().name,
77
+ };
78
+
79
+ let status = result.status;
80
+ // process test.fail() annotation
81
+ if (test.expectedStatus === 'failed') {
82
+ // actual status = expected
83
+ if (result.status === 'failed') status = 'passed';
84
+ // actual status != expected
85
+ if (result.status === 'passed') status = 'failed';
86
+ }
87
+
88
+ const reportTestPromise = this.client.addTestRun(checkStatus(status), {
89
+ rid: `${rid}-${project.name}`,
90
+ error,
91
+ test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
92
+ suite_title,
93
+ title,
94
+ steps: steps.length ? steps : undefined,
95
+ time: duration,
96
+ logs,
97
+ manuallyAttachedArtifacts,
98
+ meta: {
99
+ browser: project.browser,
100
+ isMobile: project.isMobile,
101
+ project: project.name,
102
+ projectDependencies: project.dependencies?.length ? project.dependencies : null,
103
+ ...testMeta,
104
+ ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
105
+ ...test.annotations?.reduce((acc, annotation) => {
106
+ acc[annotation.type] = annotation.description;
107
+ return acc;
108
+ }, {}),
109
+ },
110
+ file: test.location?.file,
111
+ });
112
+
113
+ this.uploads.push({
114
+ rid: `${rid}-${project.name}`,
115
+ title: test.title,
116
+ files: result.attachments.filter(a => a.body || a.path),
117
+ file: test.location?.file,
118
+ });
119
+ // remove empty uploads
120
+ this.uploads = this.uploads.filter(anUpload => anUpload.files.length);
121
+
122
+ reportTestPromises.push(reportTestPromise);
123
+ }
124
+
125
+ #getArtifactPath(artifact) {
126
+ if (artifact.path) {
127
+ if (path.isAbsolute(artifact.path)) return artifact.path;
128
+
129
+ return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
130
+ }
131
+
132
+ if (artifact.body) {
133
+ let filePath = generateTmpFilepath(artifact.name);
134
+
135
+ const extension = artifact.contentType?.split('/')[1]?.replace('jpeg', 'jpg');
136
+ if (extension) filePath += `.${extension}`;
137
+
138
+ fs.writeFileSync(filePath, artifact.body);
139
+ return filePath;
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ async onEnd(result) {
146
+ if (!this.client) return;
147
+
148
+ await Promise.all(reportTestPromises);
149
+
150
+ if (this.uploads.length) {
151
+ if (this.client.uploader.isEnabled) console.log(APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
152
+
153
+ const promises = [];
154
+
155
+ // ? possible move to addTestRun (needs investigation if files are ready)
156
+ for (const upload of this.uploads) {
157
+ const { rid, file, title } = upload;
158
+
159
+ const files = upload.files.map(attachment => ({
160
+ path: this.#getArtifactPath(attachment),
161
+ title,
162
+ type: attachment.contentType,
163
+ }));
164
+
165
+ if (!this.client.uploader.isEnabled) {
166
+ files.forEach(f => this.client.uploader.storeUploadedFile(f, this.client.runId, rid, false));
167
+ continue;
168
+ }
169
+
170
+ promises.push(
171
+ this.client.addTestRun(undefined, {
172
+ rid,
173
+ title,
174
+ files,
175
+ file,
176
+ }),
177
+ );
178
+ }
179
+ await Promise.all(promises);
180
+ }
181
+
182
+ await this.client.updateRunStatus(checkStatus(result.status));
183
+ }
184
+ }
185
+
186
+ function checkStatus(status) {
187
+ return (
188
+ {
189
+ skipped: Status.SKIPPED,
190
+ timedOut: Status.FAILED,
191
+ passed: Status.PASSED,
192
+ }[status] || Status.FAILED
193
+ );
194
+ }
195
+
196
+ function appendStep(step, shift = 0) {
197
+ // nesting too deep, ignore those steps
198
+ if (shift >= 10) return;
199
+
200
+ let newCategory = step.category;
201
+ switch (newCategory) {
202
+ case 'test.step':
203
+ newCategory = 'user';
204
+ break;
205
+ case 'hook':
206
+ newCategory = 'hook';
207
+ break;
208
+ case 'attach':
209
+ return null; // Skip steps with category 'attach'
210
+ default:
211
+ newCategory = 'framework';
212
+ }
213
+
214
+ const formattedSteps = [];
215
+ for (const child of step.steps || []) {
216
+ const appendedChild = appendStep(child, shift + 2);
217
+ if (appendedChild) {
218
+ formattedSteps.push(appendedChild);
219
+ }
220
+ }
221
+
222
+ const resultStep = {
223
+ category: newCategory,
224
+ title: step.title,
225
+ duration: step.duration,
226
+ };
227
+
228
+ if (formattedSteps.length) {
229
+ resultStep.steps = formattedSteps.filter(s => !!s);
230
+ }
231
+
232
+ if (step.error !== undefined) {
233
+ resultStep.error = step.error;
234
+ }
235
+
236
+ return resultStep;
237
+ }
238
+
239
+ function generateTmpFilepath(filename = '') {
240
+ filename = filename || `tmp.${crypto.randomBytes(16).toString('hex')}`;
241
+ const tmpdir = os.tmpdir();
242
+ return path.join(tmpdir, filename);
243
+ }
244
+
245
+ /**
246
+ * Returns filename + test title
247
+ * @param {*} test - testInfo object from Playwright
248
+ * @returns
249
+ */
250
+ function getTestContextName(test) {
251
+ return `${test._requireFile || ''}_${test.title}`;
252
+ }
253
+
254
+ export default PlaywrightReporter;
@@ -0,0 +1,183 @@
1
+ import pc from 'picocolors';
2
+ import { Client as TestomatioClient } from '../client.js';
3
+ import { STATUS } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
+ import createDebugMessages from 'debug';
6
+
7
+ const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
8
+
9
+ /**
10
+ * @typedef {import('../../types/types.js').VitestTest} VitestTest
11
+ * @typedef {import('../../types/types.js').VitestTestFile} VitestTestFile
12
+ * @typedef {import('../../types/types.js').VitestSuite} VitestSuite
13
+ * @typedef {import('../../types/types.js').VitestTestLogs} VitestTestLogs
14
+ * @typedef {import('../../types/vitest.types.js').ErrorWithDiff} ErrorWithDiff
15
+ * @typedef {typeof import('../constants.js').STATUS} STATUS
16
+ * @typedef {import('../../types/types.js').TestData} TestData
17
+ */
18
+
19
+ class VitestReporter {
20
+ constructor(config = {}) {
21
+ this.client = new TestomatioClient({ apiKey: config?.apiKey });
22
+ /**
23
+ * @type {(TestData & {status: string})[]} tests
24
+ */
25
+ this.tests = [];
26
+ }
27
+
28
+ // on run start
29
+ onInit() {
30
+ this.client.createRun();
31
+ }
32
+
33
+ /**
34
+ * @param {VitestTestFile[] | undefined} files // array with results;
35
+ * @param {unknown[] | undefined} errors // errors does not contain errors from tests; probably its testrunner errors
36
+ */
37
+ async onFinished(files, errors) {
38
+ if (!files || !files.length) console.info('No tests executed');
39
+
40
+ files.forEach(file => {
41
+ // task could be test or suite
42
+ file.tasks.forEach(taskOrSuite => {
43
+ if (taskOrSuite.type === 'test') {
44
+ const test = taskOrSuite;
45
+ this.tests.push(this.#getDataFromTest(test));
46
+ } else if (taskOrSuite.type === 'suite') {
47
+ const suite = taskOrSuite;
48
+ this.#processTasksOfSuite(suite);
49
+ } else {
50
+ throw new Error('Unprocessed case. Unknown task type');
51
+ }
52
+ });
53
+ });
54
+
55
+ debug(this.tests.length, 'tests collected');
56
+
57
+ // send tests to Testomat.io
58
+ for (const test of this.tests) {
59
+ await this.client.addTestRun(test.status, test);
60
+ }
61
+
62
+ console.log('finished');
63
+ if (errors.length) console.error('Vitest adapter errors:', errors);
64
+
65
+ await this.client.updateRunStatus(getRunStatusFromResults(files));
66
+ }
67
+
68
+ /* non-used listeners
69
+ onUserConsoleLog(log) {}
70
+ onPathsCollected(paths) {} // paths array to files with tests
71
+ onCollected(files) {} // files array with tests (but without results)
72
+ onTaskUpdate(packs) {} // some updates come here on afterAll block execution
73
+ onTestRemoved(trigger) {}
74
+ onWatcherStart(files, errors) {}
75
+ onWatcherRerun(files, trigger) {}
76
+ onServerRestart(reason) {}
77
+ onProcessTimeout() {}
78
+ */
79
+
80
+ /**
81
+ * Recursively gets all tasks from suite and pushes them to "tests" array
82
+ *
83
+ * @param {VitestSuite} suite
84
+ */
85
+ #processTasksOfSuite(suite) {
86
+ suite.tasks.forEach(taskOrSuite => {
87
+ if (taskOrSuite.type === 'test') {
88
+ const test = taskOrSuite;
89
+ this.tests.push(this.#getDataFromTest(test));
90
+ } else if (taskOrSuite.type === 'suite') {
91
+ const theSuite = taskOrSuite;
92
+ this.#processTasksOfSuite(theSuite);
93
+ } else {
94
+ throw new Error('Unprocessed case. Unknown task type');
95
+ }
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Processes task and returns test data ready to be sent to Testomat.io
101
+ *
102
+ * @param {VitestTest} test
103
+ *
104
+ * @returns {TestData & {status: string}}
105
+ */
106
+ #getDataFromTest(test) {
107
+ return {
108
+ error: test.result?.errors ? test.result.errors[0] : undefined,
109
+ file: test.file.name,
110
+ logs: test.logs ? transformLogsToString(test.logs) : '',
111
+ meta: test.meta,
112
+ status: getTestStatus(test),
113
+ suite_title: test.suite.name || test.file?.name,
114
+ test_id: getTestomatIdFromTestTitle(test.name),
115
+ time: test.result?.duration || 0,
116
+ title: test.name,
117
+ // testomatio functions (artifacts, logs, steps, meta) are not supported
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Returns run status based on test results
124
+ *
125
+ * @param {VitestTestFile[]} files
126
+ * @returns {'passed' | 'failed' | 'finished'}
127
+ */
128
+ function getRunStatusFromResults(files) {
129
+ /**
130
+ * @type {'passed' | 'failed' | 'finished'}
131
+ */
132
+ let status = 'finished'; // default status (if no failed or passed tests)
133
+
134
+ files.forEach(file => {
135
+ // search for failed tests
136
+ file.tasks.forEach(taskOrSuite => {
137
+ if (taskOrSuite.result?.state === 'fail') {
138
+ status = 'failed'; // set status to failed if any test failed
139
+ }
140
+ });
141
+
142
+ // if there are no failed tests > search for passed tests
143
+ if (status !== 'failed') {
144
+ file.tasks.forEach(taskOrSuite => {
145
+ if (taskOrSuite.result?.state === 'pass') {
146
+ status = 'passed'; // set status to passed if any test passed (and there are no failed tests)
147
+ }
148
+ });
149
+ }
150
+ });
151
+
152
+ return status;
153
+ }
154
+
155
+ /**
156
+ * Returns test status in Testomat.io format
157
+ *
158
+ * @param {VitestTest} test
159
+ * @returns 'passed' | 'failed' | 'skipped'
160
+ */
161
+ function getTestStatus(test) {
162
+ if (test.result?.state === 'fail') return STATUS.FAILED;
163
+ if (test.result?.state === 'pass') return STATUS.PASSED;
164
+ if (!test.result && test.mode === 'skip') return STATUS.SKIPPED;
165
+ console.error(pc.red('Unprocessed case for defining test status. Contact dev team. Test:'), test);
166
+ }
167
+
168
+ /**
169
+ * @param {VitestTestLogs[]} logs
170
+ * @returns string
171
+ */
172
+ function transformLogsToString(logs) {
173
+ if (!logs) return '';
174
+ let logsStr = '';
175
+ logs.forEach(log => {
176
+ if (log.type === 'stdout') logsStr += `${log.content}\n`;
177
+ if (log.type === 'stderr') logsStr += `${pc.red(log.content)}\n`;
178
+ });
179
+ return logsStr;
180
+ }
181
+
182
+ export default VitestReporter;
183
+ export { VitestReporter };