testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn

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 (67) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.js +6 -26
  3. package/package.json +39 -4
  4. package/src/adapter/codecept.js +626 -0
  5. package/src/adapter/cucumber/current.js +230 -0
  6. package/src/adapter/cucumber/legacy.js +158 -0
  7. package/src/adapter/cucumber.js +4 -0
  8. package/src/adapter/cypress-plugin/index.js +110 -0
  9. package/src/adapter/jasmine.js +60 -0
  10. package/src/adapter/jest.js +108 -0
  11. package/src/adapter/mocha.cjs +2 -0
  12. package/src/adapter/mocha.js +211 -0
  13. package/src/adapter/nightwatch.js +88 -0
  14. package/src/adapter/playwright.js +343 -0
  15. package/src/adapter/utils/playwright.js +121 -0
  16. package/src/adapter/utils/step-formatter.js +232 -0
  17. package/src/adapter/vitest.js +455 -0
  18. package/src/adapter/webdriver.js +201 -0
  19. package/src/bin/cli.js +507 -0
  20. package/src/bin/reportXml.js +79 -0
  21. package/src/bin/startTest.js +54 -0
  22. package/src/bin/uploadArtifacts.js +91 -0
  23. package/src/client.js +524 -0
  24. package/src/config.js +30 -0
  25. package/src/constants.js +72 -0
  26. package/src/data-storage.js +204 -0
  27. package/src/helpers.js +1 -0
  28. package/src/junit-adapter/adapter.js +23 -0
  29. package/src/junit-adapter/csharp.js +70 -0
  30. package/src/junit-adapter/index.js +28 -0
  31. package/src/junit-adapter/java.js +58 -0
  32. package/src/junit-adapter/javascript.js +31 -0
  33. package/src/junit-adapter/nunit-parser.js +474 -0
  34. package/src/junit-adapter/python.js +42 -0
  35. package/src/junit-adapter/ruby.js +10 -0
  36. package/src/output.js +57 -0
  37. package/src/pipe/bitbucket.js +285 -0
  38. package/src/pipe/coverage.js +500 -0
  39. package/src/pipe/csv.js +161 -0
  40. package/src/pipe/debug.js +143 -0
  41. package/src/pipe/github.js +256 -0
  42. package/src/pipe/gitlab.js +258 -0
  43. package/src/pipe/html.js +1153 -0
  44. package/src/pipe/index.js +73 -0
  45. package/src/pipe/markdown.js +753 -0
  46. package/src/pipe/testomatio.js +707 -0
  47. package/src/replay.js +274 -0
  48. package/src/reporter-functions.js +155 -0
  49. package/src/reporter.js +42 -0
  50. package/src/services/artifacts.js +59 -0
  51. package/src/services/index.js +15 -0
  52. package/src/services/key-values.js +59 -0
  53. package/src/services/links.js +69 -0
  54. package/src/services/logger.js +320 -0
  55. package/src/template/emptyData.svg +23 -0
  56. package/src/template/testomatio-old.hbs +1421 -0
  57. package/src/template/testomatio.hbs +3726 -0
  58. package/src/uploader.js +382 -0
  59. package/src/utils/constants.js +12 -0
  60. package/src/utils/debug.js +20 -0
  61. package/src/utils/log-formatter.js +118 -0
  62. package/src/utils/log.js +88 -0
  63. package/src/utils/pipe_utils.js +193 -0
  64. package/src/utils/utils.js +732 -0
  65. package/src/xmlReader.js +834 -0
  66. package/types/types.d.ts +425 -0
  67. package/types/vitest.types.d.ts +93 -0
@@ -0,0 +1,382 @@
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
+ import { log } from './utils/log.js';
12
+
13
+ const debug = createDebugMessages('@testomatio/reporter:file-uploader');
14
+
15
+ export class S3Uploader {
16
+ constructor() {
17
+ this.isEnabled = undefined;
18
+ this.storeEnabled = true;
19
+ this.config = undefined;
20
+
21
+ /**
22
+ * @type {{path: string, size: number}[]}
23
+ */
24
+ this.skippedUploads = [];
25
+ this.failedUploads = [];
26
+ /**
27
+ * @type {{path: string, size: number, link: string}[]}
28
+ */
29
+ this.successfulUploads = [];
30
+
31
+ this.configKeys = [
32
+ 'S3_ENDPOINT',
33
+ 'S3_REGION',
34
+ 'S3_BUCKET',
35
+ 'S3_ACCESS_KEY_ID',
36
+ 'S3_SECRET_ACCESS_KEY',
37
+ 'S3_SESSION_TOKEN',
38
+ 'S3_FORCE_PATH_STYLE',
39
+ 'TESTOMATIO_DISABLE_ARTIFACTS',
40
+ 'TESTOMATIO_PRIVATE_ARTIFACTS',
41
+ 'TESTOMATIO_ARTIFACT_MAX_SIZE_MB',
42
+ ];
43
+ }
44
+
45
+ resetConfig() {
46
+ this.config = undefined;
47
+ this.isEnabled = undefined;
48
+ }
49
+
50
+ /**
51
+ *
52
+ * @returns {Record<string, string>}
53
+ */
54
+ getConfig() {
55
+ if (this.config) return this.config;
56
+ this.config = this.configKeys.reduce((acc, key) => {
57
+ acc[key] = process.env[key];
58
+ return acc;
59
+ }, {});
60
+ return this.config;
61
+ }
62
+
63
+ getMaskedConfig() {
64
+ return Object.fromEntries(
65
+ Object.entries(this.getConfig()).map(([key, value]) => [
66
+ key,
67
+ key === 'S3_SECRET_ACCESS_KEY' || key === 'S3_ACCESS_KEY_ID' ? '***' : value,
68
+ ]),
69
+ );
70
+ }
71
+
72
+ checkEnabled() {
73
+ if (this.isEnabled !== undefined) return this.isEnabled;
74
+
75
+ const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = this.getConfig();
76
+ if (!S3_BUCKET) debug(`Artifacts uploading is disabled because S3_BUCKET is not set`);
77
+ this.isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
78
+
79
+ if (this.isEnabled) debug('S3 uploader is enabled');
80
+ debug(this.getMaskedConfig());
81
+
82
+ return this.isEnabled;
83
+ }
84
+
85
+ enableLogStorage() {
86
+ this.storeEnabled = true;
87
+ }
88
+
89
+ disableLogStorage() {
90
+ this.storeEnabled = false;
91
+ }
92
+
93
+ /**
94
+ *
95
+ * @param {*} Body
96
+ * @param {*} Key
97
+ * @param {{path: string, size?: number}} file
98
+ * @returns
99
+ */
100
+ async #uploadToS3(Body, Key, file) {
101
+ const { S3_BUCKET, TESTOMATIO_PRIVATE_ARTIFACTS } = this.getConfig();
102
+ const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
103
+
104
+ if (!S3_BUCKET || !Body) {
105
+ console.log(
106
+ APP_PREFIX,
107
+ pc.bold(pc.red(`Failed uploading '${Key}'. Please check S3 credentials`)),
108
+ this.getMaskedConfig(),
109
+ );
110
+ return;
111
+ }
112
+
113
+ debug('Uploading to S3:', Key);
114
+
115
+ const s3Config = this.#getS3Config();
116
+ const s3 = new S3(s3Config);
117
+ const params = {
118
+ Bucket: S3_BUCKET,
119
+ Key,
120
+ Body,
121
+ };
122
+ // disable ACL for I AM roles
123
+ if (!s3Config.credentials.sessionToken) {
124
+ params.ACL = ACL;
125
+ }
126
+
127
+ try {
128
+ const upload = new Upload({ client: s3, params });
129
+
130
+ const link = await this.getS3LocationLink(upload);
131
+ this.successfulUploads.push({ path: file.path, size: file.size, link });
132
+ debug(`📤 Uploaded artifact. File: ${file.path}, size: ${prettyBytes(file.size || 0)}, link: ${link}`);
133
+ return link;
134
+ } catch (e) {
135
+ this.failedUploads.push({ path: file.path, size: file.size });
136
+ debug('S3 uploading error:', e);
137
+ log.info('Upload failed:', e.message, '\nConfig:\n', this.getMaskedConfig());
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Returns an array of uploaded files
143
+ *
144
+ * @returns {{rid: string, file: string, uploaded: boolean}[]}
145
+ */
146
+ readUploadedFiles(runId) {
147
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
148
+
149
+ debug('Reading file', tempFilePath);
150
+
151
+ if (!fs.existsSync(tempFilePath)) {
152
+ debug('File not found:', tempFilePath);
153
+ return [];
154
+ }
155
+
156
+ const stats = fs.statSync(tempFilePath);
157
+ debug('Artifacts file stats:', +stats.mtime);
158
+ debug('Current time:', +new Date());
159
+ const diff = +new Date() - +stats.mtime;
160
+ debug('Diff:', diff);
161
+ const diffHours = diff / 1000 / 60 / 60;
162
+ debug('Diff hours:', diffHours);
163
+ if (diffHours > 3) {
164
+ log.info("Artifacts file is too old, can't process artifacts. Please re-run the tests.");
165
+ return [];
166
+ }
167
+
168
+ const data = fs.readFileSync(tempFilePath, 'utf8');
169
+ debug('Artifacts file contents:', data);
170
+ const lines = data.split('\n').filter(Boolean);
171
+ return lines.map(line => JSON.parse(line));
172
+ }
173
+
174
+ #getFilePathWithUploadsList(runId) {
175
+ const tempFilePath = path.join(os.tmpdir(), `testomatio.run.${runId}.json`);
176
+ if (!fs.existsSync(tempFilePath)) {
177
+ debug('Creating artifacts file:', tempFilePath);
178
+ fs.writeFileSync(tempFilePath, '');
179
+ }
180
+ return tempFilePath;
181
+ }
182
+
183
+ storeUploadedFile(filePath, runId, rid, uploaded = false) {
184
+ if (!this.storeEnabled) return;
185
+
186
+ if (!filePath || !runId || !rid) return;
187
+
188
+ const tempFilePath = this.#getFilePathWithUploadsList(runId);
189
+
190
+ if (typeof filePath === 'object') {
191
+ filePath = filePath.path;
192
+ }
193
+
194
+ if (typeof filePath === 'string' && !path.isAbsolute(filePath)) {
195
+ filePath = path.join(process.cwd(), filePath);
196
+ }
197
+
198
+ // Normalize path separators for cross-platform compatibility
199
+ if (typeof filePath === 'string') {
200
+ filePath = filePath.replace(/\\/g, '/');
201
+ }
202
+
203
+ const data = { rid, file: filePath, uploaded };
204
+ const jsonLine = `${JSON.stringify(data)}\n`;
205
+ fs.appendFileSync(tempFilePath, jsonLine);
206
+ }
207
+
208
+ /**
209
+ * @param {*} filePath
210
+ * @param {*} pathInS3 contains runId, rid and filename
211
+ * @returns
212
+ */
213
+ async uploadFileByPath(filePath, pathInS3) {
214
+ /* WDIO: some artifacts uploading started before createRun function completion
215
+ probably, the reason is that run is NOT created in adapter (but via cli) */
216
+ this.isEnabled = this.isEnabled ?? this.checkEnabled();
217
+
218
+ const [runId, rid] = pathInS3;
219
+
220
+ if (!filePath) return;
221
+
222
+ let fileSize = null;
223
+ let fileSizeInMb = null;
224
+
225
+ try {
226
+ // file may not exist
227
+ fileSize = fs.statSync(filePath).size || 0;
228
+ fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
229
+ } catch (e) {
230
+ debug(`File ${filePath} does not exist`);
231
+ }
232
+
233
+ if (!this.isEnabled) {
234
+ this.storeUploadedFile(filePath, runId, rid, false);
235
+ this.skippedUploads.push({ path: filePath, size: fileSize });
236
+ return;
237
+ }
238
+
239
+ const { S3_BUCKET, TESTOMATIO_ARTIFACT_MAX_SIZE_MB } = this.getConfig();
240
+
241
+ debug('Started upload', filePath, 'to', S3_BUCKET);
242
+
243
+ const isFileExist = await this.checkArtifactExistsInFileSystem(filePath, 20, 500);
244
+
245
+ if (!isFileExist) {
246
+ console.error(pc.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
247
+ return;
248
+ }
249
+
250
+ // skipping artifact only if: 1. storing to file is enabled, 2. max size is set and 3. file size exceeds the limit
251
+ if (
252
+ this.storeEnabled &&
253
+ TESTOMATIO_ARTIFACT_MAX_SIZE_MB &&
254
+ fileSizeInMb > parseFloat(TESTOMATIO_ARTIFACT_MAX_SIZE_MB)
255
+ ) {
256
+ const skippedArtifact = { path: filePath, size: fileSize };
257
+ this.storeUploadedFile(filePath, runId, rid, false);
258
+ this.skippedUploads.push(skippedArtifact);
259
+ debug(pc.yellow(`Artifacts file ${JSON.stringify(skippedArtifact)} exceeds the maximum allowed size. Skipping.`));
260
+ return;
261
+ }
262
+ debug('File:', filePath, 'exists, size:', prettyBytes(fileSize));
263
+
264
+ const fileStream = fs.createReadStream(filePath);
265
+ const Key = pathInS3.filter(p => !!p).join('/');
266
+
267
+ const link = await this.#uploadToS3(fileStream, Key, { path: filePath, size: fileSize });
268
+
269
+ this.storeUploadedFile(filePath, runId, rid, !!link);
270
+
271
+ return link;
272
+ }
273
+
274
+ /**
275
+ * @param {Buffer} buffer
276
+ * @param {string[]} pathInS3
277
+ * @returns
278
+ */
279
+ async uploadFileAsBuffer(buffer, pathInS3) {
280
+ /* WDIO: some artifacts uploading started before createRun function completion
281
+ probably, the reason is that run is NOT created in adapter (but via cli) */
282
+
283
+ this.isEnabled = this.isEnabled ?? this.checkEnabled();
284
+ if (!this.isEnabled) return;
285
+
286
+ let Key = pathInS3.filter(p => !!p).join('/');
287
+ const ext = this.#getFileExtBase64(buffer.toString('base64'));
288
+
289
+ if (ext) {
290
+ Key = `${Key}.${ext}`;
291
+ }
292
+
293
+ return this.#uploadToS3(buffer, Key, { path: Key });
294
+ }
295
+
296
+ async checkArtifactExistsInFileSystem(filePath, attempts = 5, intervalMs = 500) {
297
+ return promiseRetry(
298
+ async (retry, number) => {
299
+ try {
300
+ fs.accessSync(filePath);
301
+ return true;
302
+ } catch (err) {
303
+ if (number === attempts) {
304
+ return false;
305
+ }
306
+ debug(`File not found, retrying (attempt ${number}/${attempts})`);
307
+ await new Promise(resolve => {
308
+ setTimeout(resolve, intervalMs);
309
+ });
310
+ retry(err);
311
+ }
312
+ },
313
+ {
314
+ retries: attempts,
315
+ minTimeout: intervalMs,
316
+ maxTimeout: intervalMs,
317
+ },
318
+ );
319
+ }
320
+
321
+ async getS3LocationLink(out) {
322
+ const response = await out.done();
323
+
324
+ let s3Location = response?.Location?.trim();
325
+
326
+ if (!s3Location) {
327
+ s3Location = out?.singleUploadResult?.Location;
328
+ debug('Uploaded singleUploadResult.Location', s3Location);
329
+
330
+ if (!s3Location) {
331
+ throw new Error("Problems getting the S3 artifact's link. Please check S3 permissions!");
332
+ }
333
+ }
334
+
335
+ // Normalize the URL
336
+ if (!s3Location.startsWith('http')) {
337
+ s3Location = `https://${s3Location}`;
338
+ }
339
+
340
+ return s3Location;
341
+ }
342
+
343
+ #getFileExtBase64(str) {
344
+ const type = str.charAt(0);
345
+
346
+ return (
347
+ {
348
+ '/': 'jpg',
349
+ i: 'png',
350
+ R: 'gif',
351
+ U: 'webp',
352
+ }[type] || ''
353
+ );
354
+ }
355
+
356
+ #getS3Config() {
357
+ const { S3_REGION, S3_SESSION_TOKEN, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } =
358
+ this.getConfig();
359
+
360
+ const cfg = {
361
+ region: S3_REGION,
362
+ credentials: {
363
+ accessKeyId: S3_ACCESS_KEY_ID,
364
+ secretAccessKey: S3_SECRET_ACCESS_KEY,
365
+ },
366
+ };
367
+
368
+ if (S3_FORCE_PATH_STYLE) {
369
+ cfg.forcePathStyle = !['false', '0'].includes(String(S3_FORCE_PATH_STYLE || '').toLowerCase());
370
+ }
371
+
372
+ if (S3_SESSION_TOKEN) {
373
+ cfg.credentials.sessionToken = S3_SESSION_TOKEN;
374
+ }
375
+
376
+ if (S3_ENDPOINT) {
377
+ cfg.endpoint = S3_ENDPOINT;
378
+ }
379
+
380
+ return cfg;
381
+ }
382
+ }
@@ -0,0 +1,12 @@
1
+ export const extensionMap = {
2
+ 'application/json': 'json',
3
+ 'text/plain': 'txt',
4
+ 'image/jpeg': 'jpg',
5
+ 'image/png': 'png',
6
+ 'text/html': 'html',
7
+ 'text/css': 'css',
8
+ 'text/javascript': 'js',
9
+ 'application/pdf': 'pdf',
10
+ 'application/xml': 'xml',
11
+ 'text/xml': 'xml',
12
+ };
@@ -0,0 +1,20 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { DEBUG_FILE } from '../constants.js';
4
+
5
+ /**
6
+ * Get the debug file path(s).
7
+ *
8
+ * Always creates a timestamped file in tmp dir and a symlink in project root.
9
+ *
10
+ * @param {string} [suffix] - Optional suffix appended to the base name (e.g. 'replay').
11
+ * @returns {{root: string, tmp: string}} root path (symlink), tmp path (actual file)
12
+ */
13
+ export function getDebugFilePath(suffix = '') {
14
+ const dateTime = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
15
+ const baseName = suffix ? `${DEBUG_FILE}-${suffix}` : DEBUG_FILE;
16
+ return {
17
+ root: path.join(process.cwd(), `${baseName}.json`),
18
+ tmp: path.join(os.tmpdir(), `${baseName}.${dateTime}.json`),
19
+ };
20
+ }
@@ -0,0 +1,118 @@
1
+ import createCallsiteRecord from 'callsite-record';
2
+ import { minimatch } from 'minimatch';
3
+ import pc from 'picocolors';
4
+ import { stripVTControlCharacters } from 'util';
5
+ import { sep } from 'path';
6
+ import { formatStep, truncate } from './utils.js';
7
+
8
+ const stripColors = stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
9
+
10
+ /**
11
+ * Returns the formatted stack including the stack trace, steps, and logs.
12
+ * @param {Object} params - Parameters for formatting logs
13
+ * @param {string} params.error - Error message
14
+ * @param {Array|any} [params.steps] - Test steps (array or other types)
15
+ * @param {string} params.logs - Test logs
16
+ * @returns {string}
17
+ */
18
+ export function formatLogs({ error, steps, logs }) {
19
+ error = error?.trim();
20
+ logs = logs
21
+ ?.trim()
22
+ .split('\n')
23
+ .map(l => truncate(l))
24
+ .join('\n');
25
+
26
+ if (Array.isArray(steps)) {
27
+ steps = steps
28
+ .map(step => formatStep(step))
29
+ .flat()
30
+ .join('\n');
31
+ } else {
32
+ steps = null;
33
+ }
34
+
35
+ let testLogs = '';
36
+ if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
37
+ if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
38
+ if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
39
+ return testLogs;
40
+ }
41
+
42
+ /**
43
+ * Formats an error with stack trace and diff information
44
+ * @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
45
+ * The error object to format
46
+ * @param {string} [message] - Optional error message override
47
+ * @returns {string}
48
+ */
49
+ export function formatError(error, message) {
50
+ if (!message) message = error.message;
51
+ // @ts-ignore - inspect is a custom property added by some testing frameworks
52
+ if (error.inspect) message = error.inspect() || '';
53
+
54
+ let stack = '';
55
+ if (error.name) stack += `${pc.red(error.name)}`;
56
+ // @ts-ignore - operator is a custom property added by assertion libraries
57
+ if (error.operator) stack += ` (${pc.red(error.operator)})`;
58
+ // add new line if something was added to stack
59
+ if (stack) stack += ': ';
60
+
61
+ stack += `${message}\n`;
62
+
63
+ // @ts-ignore - diff is a custom property added by vitest
64
+ if (error.diff) {
65
+ // diff for vitest
66
+ stack += error.diff;
67
+ stack += '\n\n';
68
+ } else if (error.actual && error.expected && error.actual !== error.expected) {
69
+ // diffs for mocha, cypress, codeceptjs style
70
+ stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
71
+ stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
72
+ stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
73
+ stack += '\n\n';
74
+ }
75
+
76
+ const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
77
+
78
+ try {
79
+ let hasFrame = false;
80
+ const record = createCallsiteRecord({
81
+ forError: error,
82
+ isCallsiteFrame: frame => {
83
+ if (customFilter && minimatch(frame.fileName, customFilter)) return false;
84
+ if (hasFrame) return false;
85
+ if (isNotInternalFrame(frame)) hasFrame = true;
86
+ return hasFrame;
87
+ },
88
+ });
89
+ // @ts-ignore
90
+ if (record && !record.filename.startsWith('http')) {
91
+ stack += record.renderSync({ stackFilter: isNotInternalFrame });
92
+ }
93
+ return stack;
94
+ } catch (e) {
95
+ console.log(e);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Checks if a stack frame is not an internal frame (node_modules or internal)
101
+ * @param {Object} frame - Stack frame object
102
+ * @returns {boolean}
103
+ */
104
+ function isNotInternalFrame(frame) {
105
+ const fileName = frame.getFileName();
106
+ if (!fileName) return false;
107
+
108
+ const isFileUrl = fileName.startsWith('file://');
109
+ const hasPathSeparator = fileName.includes(sep) || fileName.includes('/') || isFileUrl;
110
+
111
+ return (
112
+ hasPathSeparator &&
113
+ !fileName.includes('node_modules') &&
114
+ !fileName.includes('internal')
115
+ );
116
+ }
117
+
118
+ export { stripColors };
@@ -0,0 +1,88 @@
1
+ import { APP_PREFIX } from '../constants.js';
2
+
3
+ /**
4
+ * Log levels for the Testomat.io reporter.
5
+ * A message is logged if its level is <= the current log level.
6
+ * @example
7
+ * TESTOMATIO_LOG_LEVEL=ERROR npx codeceptjs run // Only errors
8
+ * TESTOMATIO_LOG_LEVEL=WARN npx codeceptjs run // Warnings and errors
9
+ * TESTOMATIO_LOG_LEVEL=INFO npx codeceptjs run // Info, warnings, errors (default)
10
+ */
11
+ export const LOG_LEVELS = {
12
+ ERROR: 0,
13
+ WARN: 1,
14
+ INFO: 2,
15
+ };
16
+
17
+ /**
18
+ * Get the current log level from TESTOMATIO_LOG_LEVEL environment variable.
19
+ * Defaults to INFO (info, warn, and error messages).
20
+ * @returns {number} Numeric log level (0-2)
21
+ */
22
+ export function getLogLevel() {
23
+ const envLevel = process.env.TESTOMATIO_LOG_LEVEL?.toUpperCase();
24
+ return LOG_LEVELS[envLevel] ?? LOG_LEVELS.INFO;
25
+ }
26
+
27
+ /**
28
+ * Check if a message should be logged based on its level.
29
+ * A message is logged if its level is <= the current log level,
30
+ * or if TESTOMATIO_DEBUG is set (for debugging with the debug package).
31
+ * @param {number} messageLevel - Message level (LOG_LEVELS value)
32
+ * @returns {boolean} True if the message should be logged
33
+ */
34
+ export function shouldLog(messageLevel) {
35
+ return messageLevel <= getLogLevel() || !!process.env.TESTOMATIO_DEBUG;
36
+ }
37
+
38
+ /**
39
+ * Log an info message with [TESTOMATIO] prefix.
40
+ * Only logs when TESTOMATIO_LOG_LEVEL is INFO.
41
+ * @param {...any} args - Arguments to log
42
+ */
43
+ export function info(...args) {
44
+ if (shouldLog(LOG_LEVELS.INFO)) {
45
+ const fn = process.env.TESTOMATIO_LOG_STDERR === '1' ? console.error : console.log;
46
+ fn(APP_PREFIX, ...args);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Log a warning message with [TESTOMATIO] prefix.
52
+ * Only logs when TESTOMATIO_LOG_LEVEL is WARN or INFO.
53
+ * @param {...any} args - Arguments to log
54
+ */
55
+ export function warn(...args) {
56
+ if (shouldLog(LOG_LEVELS.WARN)) {
57
+ console.warn(APP_PREFIX, ...args);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Log an error message with [TESTOMATIO] prefix.
63
+ * Logs for all TESTOMATIO_LOG_LEVEL values.
64
+ * @param {...any} args - Arguments to log
65
+ */
66
+ export function error(...args) {
67
+ if (shouldLog(LOG_LEVELS.ERROR)) {
68
+ console.error(APP_PREFIX, ...args);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Logging utility for Testomat.io reporter.
74
+ * All messages are prefixed with [TESTOMATIO] and respect TESTOMATIO_LOG_LEVEL.
75
+ * @example
76
+ * import { log } from './utils/log.js';
77
+ * log.info('Test started');
78
+ * log.warn('This is a warning');
79
+ * log.error('Something went wrong');
80
+ */
81
+ export const log = {
82
+ info,
83
+ warn,
84
+ error,
85
+ getLogLevel,
86
+ shouldLog,
87
+ LOG_LEVELS,
88
+ };