@testomatio/reporter 2.0.0-beta-esm → 2.0.0-beta.1-xml

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 (123) hide show
  1. package/lib/adapter/codecept.d.ts +2 -0
  2. package/lib/adapter/codecept.js +31 -26
  3. package/lib/adapter/cucumber/current.d.ts +14 -0
  4. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  5. package/lib/adapter/cucumber.d.ts +2 -0
  6. package/lib/adapter/cypress-plugin/index.d.ts +2 -0
  7. package/lib/adapter/cypress-plugin/index.js +10 -10
  8. package/lib/adapter/jasmine.d.ts +11 -0
  9. package/lib/adapter/jest.d.ts +13 -0
  10. package/lib/adapter/mocha.d.ts +2 -0
  11. package/lib/adapter/mocha.js +4 -4
  12. package/lib/adapter/nightwatch.d.ts +4 -0
  13. package/lib/adapter/nightwatch.js +80 -0
  14. package/lib/adapter/playwright.d.ts +14 -0
  15. package/lib/adapter/playwright.js +58 -33
  16. package/lib/adapter/vitest.d.ts +35 -0
  17. package/lib/adapter/vitest.js +6 -6
  18. package/lib/adapter/webdriver.d.ts +24 -0
  19. package/lib/adapter/webdriver.js +51 -14
  20. package/lib/bin/cli.d.ts +2 -0
  21. package/lib/bin/cli.js +250 -0
  22. package/lib/bin/reportXml.d.ts +2 -0
  23. package/lib/bin/reportXml.js +15 -11
  24. package/lib/bin/startTest.d.ts +2 -0
  25. package/lib/bin/startTest.js +12 -7
  26. package/lib/bin/uploadArtifacts.d.ts +2 -0
  27. package/lib/bin/uploadArtifacts.js +82 -0
  28. package/lib/client.d.ts +76 -0
  29. package/lib/client.js +128 -53
  30. package/lib/config.d.ts +1 -0
  31. package/lib/config.js +2 -2
  32. package/lib/constants.d.ts +25 -0
  33. package/lib/constants.js +5 -1
  34. package/lib/data-storage.d.ts +34 -0
  35. package/lib/data-storage.js +19 -9
  36. package/lib/junit-adapter/adapter.d.ts +9 -0
  37. package/lib/junit-adapter/csharp.d.ts +5 -0
  38. package/lib/junit-adapter/csharp.js +11 -1
  39. package/lib/junit-adapter/index.d.ts +3 -0
  40. package/lib/junit-adapter/java.d.ts +5 -0
  41. package/lib/junit-adapter/javascript.d.ts +4 -0
  42. package/lib/junit-adapter/python.d.ts +5 -0
  43. package/lib/junit-adapter/ruby.d.ts +4 -0
  44. package/lib/output.d.ts +11 -0
  45. package/lib/package.json +3 -1
  46. package/lib/pipe/bitbucket.d.ts +23 -0
  47. package/lib/pipe/bitbucket.js +19 -9
  48. package/lib/pipe/csv.d.ts +47 -0
  49. package/lib/pipe/csv.js +2 -2
  50. package/lib/pipe/debug.d.ts +29 -0
  51. package/lib/pipe/debug.js +108 -0
  52. package/lib/pipe/github.d.ts +30 -0
  53. package/lib/pipe/github.js +37 -5
  54. package/lib/pipe/gitlab.d.ts +23 -0
  55. package/lib/pipe/gitlab.js +2 -3
  56. package/lib/pipe/html.d.ts +35 -0
  57. package/lib/pipe/html.js +9 -4
  58. package/lib/pipe/index.d.ts +1 -0
  59. package/lib/pipe/index.js +20 -10
  60. package/lib/pipe/testomatio.d.ts +70 -0
  61. package/lib/pipe/testomatio.js +54 -39
  62. package/lib/reporter-functions.d.ts +34 -0
  63. package/lib/reporter-functions.js +17 -7
  64. package/lib/reporter.d.ts +232 -0
  65. package/lib/reporter.js +19 -33
  66. package/lib/services/artifacts.d.ts +33 -0
  67. package/lib/services/index.d.ts +9 -0
  68. package/lib/services/key-values.d.ts +27 -0
  69. package/lib/services/key-values.js +1 -1
  70. package/lib/services/logger.d.ts +64 -0
  71. package/lib/services/logger.js +1 -2
  72. package/lib/template/testomatio.hbs +651 -1366
  73. package/lib/uploader.d.ts +60 -0
  74. package/lib/uploader.js +312 -0
  75. package/lib/utils/pipe_utils.d.ts +41 -0
  76. package/lib/utils/pipe_utils.js +3 -5
  77. package/lib/utils/utils.d.ts +47 -0
  78. package/lib/utils/utils.js +99 -12
  79. package/lib/xmlReader.d.ts +92 -0
  80. package/lib/xmlReader.js +64 -25
  81. package/package.json +19 -13
  82. package/src/adapter/codecept.js +30 -26
  83. package/src/adapter/cypress-plugin/index.js +5 -5
  84. package/src/adapter/mocha.cjs +1 -1
  85. package/src/adapter/mocha.js +4 -4
  86. package/src/adapter/nightwatch.js +88 -0
  87. package/src/adapter/playwright.js +59 -31
  88. package/src/adapter/vitest.js +6 -6
  89. package/src/adapter/webdriver.js +42 -12
  90. package/src/bin/cli.js +303 -0
  91. package/src/bin/reportXml.js +19 -9
  92. package/src/bin/startTest.js +9 -4
  93. package/src/bin/uploadArtifacts.js +91 -0
  94. package/src/client.js +137 -57
  95. package/src/config.js +2 -2
  96. package/src/constants.js +5 -1
  97. package/src/data-storage.js +2 -2
  98. package/src/junit-adapter/csharp.js +13 -1
  99. package/src/pipe/bitbucket.js +2 -2
  100. package/src/pipe/csv.js +3 -3
  101. package/src/pipe/debug.js +104 -0
  102. package/src/pipe/github.js +3 -5
  103. package/src/pipe/gitlab.js +6 -7
  104. package/src/pipe/html.js +14 -7
  105. package/src/pipe/index.js +5 -7
  106. package/src/pipe/testomatio.js +75 -76
  107. package/src/reporter-functions.js +18 -7
  108. package/src/reporter.cjs_decprecated +21 -0
  109. package/src/reporter.js +20 -11
  110. package/src/services/key-values.js +1 -1
  111. package/src/services/logger.js +5 -4
  112. package/src/template/testomatio.hbs +651 -1366
  113. package/src/uploader.js +371 -0
  114. package/src/utils/pipe_utils.js +4 -12
  115. package/src/utils/utils.js +64 -15
  116. package/src/xmlReader.js +76 -26
  117. package/lib/adapter/jasmine/jasmine.js +0 -63
  118. package/lib/adapter/mocha/mocha.js +0 -125
  119. package/lib/fileUploader.js +0 -245
  120. package/lib/utils/chalk.js +0 -10
  121. package/src/fileUploader.js +0 -307
  122. package/src/reporter.cjs +0 -22
  123. package/src/utils/chalk.js +0 -13
@@ -0,0 +1,371 @@
1
+ import createDebugMessages from 'debug';
2
+ import { S3 } from '@aws-sdk/client-s3';
3
+ import { Upload } from '@aws-sdk/lib-storage';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import path from 'path';
7
+ import promiseRetry from 'promise-retry';
8
+ import pc from 'picocolors';
9
+ import { APP_PREFIX } from './constants.js';
10
+ import { filesize as prettyBytes } from 'filesize';
11
+
12
+ const debug = createDebugMessages('@testomatio/reporter:file-uploader');
13
+
14
+ export class S3Uploader {
15
+ constructor() {
16
+ this.isEnabled = undefined;
17
+ this.storeEnabled = true;
18
+ this.config = undefined;
19
+
20
+ /**
21
+ * @type {{path: string, size: number}[]}
22
+ */
23
+ this.skippedUploads = [];
24
+ this.failedUploads = [];
25
+ /**
26
+ * @type {{path: string, size: number, link: string}[]}
27
+ */
28
+ this.successfulUploads = [];
29
+
30
+ this.configKeys = [
31
+ 'S3_ENDPOINT',
32
+ 'S3_REGION',
33
+ 'S3_BUCKET',
34
+ 'S3_ACCESS_KEY_ID',
35
+ 'S3_SECRET_ACCESS_KEY',
36
+ 'S3_SESSION_TOKEN',
37
+ 'S3_FORCE_PATH_STYLE',
38
+ 'TESTOMATIO_DISABLE_ARTIFACTS',
39
+ 'TESTOMATIO_PRIVATE_ARTIFACTS',
40
+ 'TESTOMATIO_ARTIFACT_MAX_SIZE_MB',
41
+ ];
42
+ }
43
+
44
+ resetConfig() {
45
+ this.config = undefined;
46
+ this.isEnabled = undefined;
47
+ }
48
+
49
+ /**
50
+ *
51
+ * @returns {Record<string, string>}
52
+ */
53
+ getConfig() {
54
+ if (this.config) return this.config;
55
+ this.config = this.configKeys.reduce((acc, key) => {
56
+ acc[key] = process.env[key];
57
+ return acc;
58
+ }, {});
59
+ return this.config;
60
+ }
61
+
62
+ getMaskedConfig() {
63
+ return Object.fromEntries(
64
+ Object.entries(this.getConfig()).map(([key, value]) => [
65
+ key,
66
+ key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value,
67
+ ]),
68
+ );
69
+ }
70
+
71
+ checkEnabled() {
72
+ if (this.isEnabled !== undefined) return this.isEnabled;
73
+
74
+ const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = this.getConfig();
75
+ if (!S3_BUCKET) debug(`Artifacts uploading is disabled because S3_BUCKET is not set`);
76
+ this.isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
77
+
78
+ if (this.isEnabled) debug('S3 uploader is enabled');
79
+ debug(this.getMaskedConfig());
80
+
81
+ return this.isEnabled;
82
+ }
83
+
84
+ enableLogStorage() {
85
+ this.storeEnabled = true;
86
+ }
87
+
88
+ disableLogStorage() {
89
+ this.storeEnabled = false;
90
+ }
91
+
92
+ /**
93
+ *
94
+ * @param {*} Body
95
+ * @param {*} Key
96
+ * @param {{path: string, size?: number}} file
97
+ * @returns
98
+ */
99
+ async #uploadToS3(Body, Key, file) {
100
+ const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
101
+ const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
102
+
103
+ if (!S3_BUCKET || !Body) {
104
+ console.log(
105
+ APP_PREFIX,
106
+ pc.bold(pc.red(`Failed uploading '${Key}'. Please check S3 credentials`)),
107
+ this.getMaskedConfig(),
108
+ );
109
+ return;
110
+ }
111
+
112
+ debug('Uploading to S3:', Key);
113
+
114
+ const s3Config = this.#getS3Config();
115
+ const s3 = new S3(s3Config);
116
+ const params = {
117
+ Bucket: S3_BUCKET,
118
+ Key,
119
+ Body,
120
+ };
121
+ // disable ACL for I AM roles
122
+ if (!s3Config.credentials.sessionToken) {
123
+ params.ACL = ACL;
124
+ }
125
+
126
+ try {
127
+ const upload = new Upload({ client: s3, params });
128
+
129
+ const link = await this.getS3LocationLink(upload);
130
+ this.successfulUploads.push({ path: file.path, size: file.size, link });
131
+ debug(`📤 Uploaded artifact. File: ${file.path}, size: ${prettyBytes(file.size)}, link: ${link}`);
132
+ return link;
133
+ } catch (e) {
134
+ this.failedUploads.push({ path: file.path, size: file.size });
135
+ debug('S3 uploading error:', e);
136
+ console.log(APP_PREFIX, 'Upload failed:', e.message, '\nConfig:\n', this.getMaskedConfig());
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Returns an array of uploaded files
142
+ *
143
+ * @returns {{rid: string, file: string, uploaded: boolean}[]}
144
+ */
145
+ readUploadedFiles(runId) {
146
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
147
+
148
+ debug('Reading file', tempFilePath);
149
+
150
+ if (!fs.existsSync(tempFilePath)) {
151
+ debug('File not found:', tempFilePath);
152
+ return [];
153
+ }
154
+
155
+ const stats = fs.statSync(tempFilePath);
156
+ debug('Artifacts file stats:', +stats.mtime);
157
+ debug('Current time:', +new Date());
158
+ const diff = +new Date() - +stats.mtime;
159
+ debug('Diff:', diff);
160
+ const diffHours = diff / 1000 / 60 / 60;
161
+ debug('Diff hours:', diffHours);
162
+ if (diffHours > 3) {
163
+ console.log(APP_PREFIX, "Artifacts file is too old, can't process artifacts. Please re-run the tests.");
164
+ return [];
165
+ }
166
+
167
+ const data = fs.readFileSync(tempFilePath, 'utf8');
168
+ debug('Artifacts file contents:', data);
169
+ const lines = data.split('\n').filter(Boolean);
170
+ return lines.map(line => JSON.parse(line));
171
+ }
172
+
173
+ #getFilePathWithUploadsList(runId) {
174
+ const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.json`);
175
+ if (!fs.existsSync(tempFilePath)) {
176
+ debug('Creating artifacts file:', tempFilePath);
177
+ fs.writeFileSync(tempFilePath, '');
178
+ }
179
+ return tempFilePath;
180
+ }
181
+
182
+ storeUploadedFile(filePath, runId, rid, uploaded = false) {
183
+ if (!this.storeEnabled) return;
184
+
185
+ if (!filePath || !runId || !rid) return;
186
+
187
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
188
+
189
+ if (typeof filePath === 'object') {
190
+ filePath = filePath.path;
191
+ }
192
+
193
+ if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
194
+ filePath = path.join(process.cwd(), filePath);
195
+ }
196
+
197
+ const data = { rid, file: filePath, uploaded };
198
+ const jsonLine = `${JSON.stringify(data)}\n`;
199
+ fs.appendFileSync(tempFilePath, jsonLine);
200
+ }
201
+
202
+ /**
203
+ * @param {*} filePath
204
+ * @param {*} pathInS3 contains runId, rid and filename
205
+ * @returns
206
+ */
207
+ async uploadFileByPath(filePath, pathInS3) {
208
+ // sometimes artifacts uploading started before createRun function completion
209
+ this.isEnabled = this.isEnabled ?? this.checkEnabled();
210
+
211
+ const [runId, rid] = pathInS3;
212
+
213
+ if (!filePath) return;
214
+
215
+ let fileSize = null;
216
+ let fileSizeInMb = null;
217
+
218
+ try {
219
+ // file may not exist
220
+ fileSize = fs.statSync(filePath).size;
221
+ fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
222
+ } catch (e) {
223
+ debug(`File ${filePath} does not exist`);
224
+ }
225
+
226
+ if (!this.isEnabled) {
227
+ this.storeUploadedFile(filePath, runId, rid, false);
228
+ this.skippedUploads.push({ path: filePath, size: fileSize });
229
+ return;
230
+ }
231
+
232
+ const { S3_BUCKET, TESTOMATIO_ARTIFACT_MAX_SIZE_MB } = this.getConfig();
233
+
234
+ debug('Started upload', filePath, 'to', S3_BUCKET);
235
+
236
+ const isFileExist = await this.checkArtifactExistsInFileSystem(filePath, 20, 500);
237
+
238
+ if (!isFileExist) {
239
+ console.error(pc.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
240
+ return;
241
+ }
242
+
243
+ // skipping artifact only if: 1. storing to file is enabled, 2. max size is set and 3. file size exceeds the limit
244
+ if (
245
+ this.storeEnabled &&
246
+ TESTOMATIO_ARTIFACT_MAX_SIZE_MB &&
247
+ fileSizeInMb > parseFloat(TESTOMATIO_ARTIFACT_MAX_SIZE_MB)
248
+ ) {
249
+ const skippedArtifact = { path: filePath, size: fileSize };
250
+ this.storeUploadedFile(filePath, runId, rid, false);
251
+ this.skippedUploads.push(skippedArtifact);
252
+ debug(pc.yellow(`Artifacts file ${JSON.stringify(skippedArtifact)} exceeds the maximum allowed size. Skipping.`));
253
+ return;
254
+ }
255
+ debug('File:', filePath, 'exists, size:', prettyBytes(fileSize));
256
+
257
+ const fileStream = fs.createReadStream(filePath);
258
+ const Key = pathInS3.filter(p => !!p).join('/');
259
+
260
+ const link = await this.#uploadToS3(fileStream, Key, { path: filePath, size: fileSize });
261
+
262
+ this.storeUploadedFile(filePath, runId, rid, !!link);
263
+
264
+ return link;
265
+ }
266
+
267
+ /**
268
+ * @param {Buffer} buffer
269
+ * @param {string[]} pathInS3
270
+ * @returns
271
+ */
272
+ async uploadFileAsBuffer(buffer, pathInS3) {
273
+ if (!this.isEnabled) return;
274
+
275
+ let Key = pathInS3.filter(p => !!p).join('/');
276
+ const ext = this.#getFileExtBase64(buffer);
277
+
278
+ if (ext) {
279
+ Key = `${Key}.${ext}`;
280
+ }
281
+
282
+ return this.#uploadToS3(buffer, Key, { path: Key });
283
+ }
284
+
285
+ async checkArtifactExistsInFileSystem(filePath, attempts = 5, intervalMs = 500) {
286
+ return promiseRetry(
287
+ async (retry, number) => {
288
+ try {
289
+ fs.accessSync(filePath);
290
+ return true;
291
+ } catch (err) {
292
+ if (number === attempts) {
293
+ return false;
294
+ }
295
+ debug(`File not found, retrying (attempt ${number}/${attempts})`);
296
+ await new Promise(resolve => {
297
+ setTimeout(resolve, intervalMs);
298
+ });
299
+ retry(err);
300
+ }
301
+ },
302
+ {
303
+ retries: attempts,
304
+ minTimeout: intervalMs,
305
+ maxTimeout: intervalMs,
306
+ },
307
+ );
308
+ }
309
+
310
+ async getS3LocationLink(out) {
311
+ const response = await out.done();
312
+
313
+ let s3Location = response?.Location?.trim();
314
+
315
+ if (!s3Location) {
316
+ s3Location = out?.singleUploadResult?.Location;
317
+ debug('Uploaded singleUploadResult.Location', s3Location);
318
+
319
+ if (!s3Location) {
320
+ throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
321
+ }
322
+ }
323
+
324
+ // Normalize the URL
325
+ if (!s3Location.startsWith('http')) {
326
+ s3Location = `https://${s3Location}`;
327
+ }
328
+
329
+ return s3Location;
330
+ }
331
+
332
+ #getFileExtBase64(str) {
333
+ const type = str.charAt(0);
334
+
335
+ return (
336
+ {
337
+ '/': 'jpg',
338
+ i: 'png',
339
+ R: 'gif',
340
+ U: 'webp',
341
+ }[type] || ''
342
+ );
343
+ }
344
+
345
+ #getS3Config() {
346
+ const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
347
+ this.getConfig();
348
+
349
+ const cfg = {
350
+ region: S3_REGION,
351
+ credentials: {
352
+ accessKeyId: S3_ACCESS_KEY_ID,
353
+ secretAccessKey: S3_SECRET_ACCESS_KEY,
354
+ },
355
+ };
356
+
357
+ if (S3_FORCE_PATH_STYLE) {
358
+ cfg.forcePathStyle = !['false', '0'].includes(String(S3_FORCE_PATH_STYLE || '').toLowerCase());
359
+ }
360
+
361
+ if (S3_SESSION_TOKEN) {
362
+ cfg.credentials.sessionToken = S3_SESSION_TOKEN;
363
+ }
364
+
365
+ if (S3_ENDPOINT) {
366
+ cfg.endpoint = S3_ENDPOINT;
367
+ }
368
+
369
+ return cfg;
370
+ }
371
+ }
@@ -1,4 +1,3 @@
1
- import { upload } from '../fileUploader.js';
2
1
  import { APP_PREFIX } from '../constants.js';
3
2
 
4
3
  /**
@@ -8,16 +7,16 @@ import { APP_PREFIX } from '../constants.js';
8
7
  function setS3Credentials(artifacts) {
9
8
  if (!Object.keys(artifacts).length) return;
10
9
 
11
- console.log(APP_PREFIX, 'S3 were credentials obtained from Testomat.io...');
10
+ console.log(APP_PREFIX, 'S3 credentials obtained from Testomat.io...');
12
11
 
13
12
  if (artifacts.ACCESS_KEY_ID) process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
14
13
  if (artifacts.SECRET_ACCESS_KEY) process.env.S3_SECRET_ACCESS_KEY = artifacts.SECRET_ACCESS_KEY;
15
14
  if (artifacts.REGION) process.env.S3_REGION = artifacts.REGION;
16
15
  if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
17
- if (artifacts.ENDPOINT) process.env.S3_ENDPOINT = artifacts.ENDPOINT;
18
16
  if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
19
17
  if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
20
- upload.resetConfig();
18
+ // endpoint is not received from the server; and shuld be empty if IAM used (credentails obtained from the testomat)
19
+ process.env.S3_ENDPOINT = artifacts.ENDPOINT || '';
21
20
  }
22
21
 
23
22
  /**
@@ -117,11 +116,4 @@ function fullName(t) {
117
116
  return line;
118
117
  }
119
118
 
120
- export {
121
- updateFilterType,
122
- parseFilterParams,
123
- generateFilterRequestParams,
124
- setS3Credentials,
125
- statusEmoji,
126
- fullName,
127
- };
119
+ export { updateFilterType, parseFilterParams, generateFilterRequestParams, setS3Credentials, statusEmoji, fullName };
@@ -1,12 +1,17 @@
1
1
  import { URL } from 'url';
2
- import { sep, basename } from 'path';
2
+ import path, { sep, basename } from 'path';
3
3
  import pc from 'picocolors';
4
4
  import fs from 'fs';
5
5
  import isValid from 'is-valid-path';
6
6
  import createDebugMessages from 'debug';
7
+ import os from 'os';
8
+ import { fileURLToPath } from 'url';
7
9
 
8
10
  const debug = createDebugMessages('@testomatio/reporter:util');
9
11
 
12
+ // Use __dirname directly since we're compiling to CommonJS
13
+ const __dirname = path.resolve();
14
+
10
15
  /**
11
16
  * @param {String} testTitle - Test title
12
17
  *
@@ -49,7 +54,6 @@ const ansiRegExp = () => {
49
54
 
50
55
  const isValidUrl = s => {
51
56
  try {
52
- // eslint-disable-next-line no-new
53
57
  new URL(s);
54
58
  return true;
55
59
  } catch (err) {
@@ -107,7 +111,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
107
111
  .join('\n');
108
112
  };
109
113
 
110
- const TEST_ID_REGEX = /@T([\w\d]{8})/;
114
+ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
111
115
 
112
116
  const fetchIdFromCode = (code, opts = {}) => {
113
117
  const comments = code
@@ -127,12 +131,9 @@ const fetchIdFromCode = (code, opts = {}) => {
127
131
  };
128
132
 
129
133
  const fetchIdFromOutput = output => {
130
- const lines = output
131
- .split('\n')
132
- .map(l => l.trim())
133
- .filter(l => l.startsWith('tid://'));
134
+ const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${TEST_ID_REGEX.source})`);
134
135
 
135
- return lines.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
136
+ return output.match(TID_FULL_PATTERN)?.[2];
136
137
  };
137
138
 
138
139
  const fetchSourceCode = (contents, opts = {}) => {
@@ -153,6 +154,9 @@ const fetchSourceCode = (contents, opts = {}) => {
153
154
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
154
155
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
155
156
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
157
+ } else if (opts.lang === 'csharp') {
158
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
159
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
156
160
  } else {
157
161
  lineIndex = lines.findIndex(l => l.includes(title));
158
162
  }
@@ -299,7 +303,6 @@ const decamelize = text => {
299
303
  * @returns
300
304
  */
301
305
  function removeColorCodes(input) {
302
- // eslint-disable-next-line no-control-regex
303
306
  return input.replace(/\x1b\[[0-9;]*m/g, '');
304
307
  }
305
308
 
@@ -312,7 +315,6 @@ const testRunnerHelper = {
312
315
  try {
313
316
  // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
314
317
  // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
315
- // eslint-disable-next-line no-undef
316
318
  return expect?.getState()?.currentTestName;
317
319
  } catch (e) {
318
320
  return null;
@@ -320,7 +322,52 @@ const testRunnerHelper = {
320
322
  },
321
323
  };
322
324
 
325
+ function storeRunId(runId) {
326
+ if (!runId || runId === 'undefined') return;
327
+ const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
328
+ fs.writeFileSync(filePath, runId);
329
+ }
330
+
331
+ function readLatestRunId() {
332
+ try {
333
+ const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
334
+ const stats = fs.statSync(filePath);
335
+ const diff = +new Date() - +stats.mtime;
336
+ const diffHours = diff / 1000 / 60 / 60;
337
+ if (diffHours > 1) return;
338
+
339
+ return fs.readFileSync(filePath)?.toString()?.trim();
340
+ } catch (e) {
341
+ return null;
342
+ }
343
+ }
344
+
345
+ function formatStep(step, shift = 0) {
346
+ const prefix = ' '.repeat(shift);
347
+
348
+ const lines = [];
349
+
350
+ if (step.error) {
351
+ lines.push(`${prefix}${pc.red(step.title)} ${pc.gray(`${step.duration}ms`)}`);
352
+ } else {
353
+ lines.push(`${prefix}${step.title} ${pc.gray(`${step.duration}ms`)}`);
354
+ }
355
+
356
+ for (const child of step.steps || []) {
357
+ lines.push(...formatStep(child, shift + 2));
358
+ }
359
+
360
+ return lines;
361
+ }
362
+
363
+ export function getPackageVersion() {
364
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
365
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
366
+ return packageJson.version;
367
+ }
368
+
323
369
  export {
370
+ ansiRegExp,
324
371
  isSameTest,
325
372
  fetchSourceCode,
326
373
  fetchSourceCodeFromStackTrace,
@@ -328,14 +375,16 @@ export {
328
375
  fetchIdFromOutput,
329
376
  fetchFilesFromStackTrace,
330
377
  fileSystem,
378
+ foundedTestLog,
379
+ formatStep,
331
380
  getCurrentDateTime,
332
- specificTestInfo,
333
- isValidUrl,
334
- ansiRegExp,
335
381
  getTestomatIdFromTestTitle,
336
- parseSuite,
337
382
  humanize,
383
+ isValidUrl,
384
+ parseSuite,
385
+ readLatestRunId,
338
386
  removeColorCodes,
339
- foundedTestLog,
387
+ specificTestInfo,
388
+ storeRunId,
340
389
  testRunnerHelper,
341
390
  };