@testomatio/reporter 2.3.9 → 2.5.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.
@@ -1,3 +1,4 @@
1
+ import { isPlaywright } from './helpers.js';
1
2
  import { services } from './services/index.js';
2
3
 
3
4
  /**
@@ -7,9 +8,7 @@ import { services } from './services/index.js';
7
8
  * @returns {void}
8
9
  */
9
10
  function saveArtifact(data, context = null) {
10
- if (process.env.IS_PLAYWRIGHT)
11
- throw new Error(`This function is not available in Playwright framework.
12
- /Playwright supports artifacts out of the box`);
11
+ showPlaywrightWarning('artifact', 'Playwright supports artifacts out of the box.');
13
12
  if (!data) return;
14
13
  services.artifacts.put(data, context);
15
14
  }
@@ -20,20 +19,24 @@ function saveArtifact(data, context = null) {
20
19
  * @returns {void}
21
20
  */
22
21
  function logMessage(...args) {
23
- if (process.env.IS_PLAYWRIGHT) throw new Error('This function is not available in Playwright framework');
24
22
  services.logger._templateLiteralLog(...args);
25
23
  }
26
24
 
27
25
  /**
28
26
  * Similar to "log" function but marks message in report as a step
29
27
  * @param {string} message - step message
28
+ * @param {{[key: string]: any}} [logs] optional key-value object with additional info, e.g. logs
30
29
  * @returns {void}
30
+ *
31
+ * Example:
32
+ * step('Get response', { logs: {status: 'success'} });
31
33
  */
32
- function addStep(message) {
33
- if (process.env.IS_PLAYWRIGHT)
34
- throw new Error('This function is not available in Playwright framework. Use playwright steps');
35
-
36
- services.logger.step(message);
34
+ function addStep(message, logs) {
35
+ // this is done because Playwright reporter intercepts console logs and then we gather them and show on Testomat
36
+ // if not console.log, the step message will be lost from reporter
37
+ if (isPlaywright) services.logger._templateLiteralLog(message, logs);
38
+ // all other frameworks
39
+ else services.logger.step(message, logs);
37
40
  }
38
41
 
39
42
  /**
@@ -43,8 +46,7 @@ function addStep(message) {
43
46
  * @returns {void}
44
47
  */
45
48
  function setKeyValue(keyValue, value = null) {
46
- if (process.env.IS_PLAYWRIGHT)
47
- throw new Error('This function is not available in Playwright framework. Use test tag instead.');
49
+ showPlaywrightWarning('meta', 'Use test annotations instead.');
48
50
 
49
51
  if (typeof keyValue === 'string') {
50
52
  keyValue = { [keyValue]: value };
@@ -59,6 +61,7 @@ function setKeyValue(keyValue, value = null) {
59
61
  * @returns {void}
60
62
  */
61
63
  function setLabel(key, value = null) {
64
+ showPlaywrightWarning('label', 'Use test tag instead.');
62
65
  if (Array.isArray(value)) {
63
66
  return value.forEach(label => setLabel(key, label));
64
67
  }
@@ -87,6 +90,12 @@ function linkJira(...jiraIds) {
87
90
  services.links.put(links);
88
91
  }
89
92
 
93
+ function showPlaywrightWarning(functionName, recommendation) {
94
+ if (isPlaywright) {
95
+ console.warn(`[TESTOMATIO] '${functionName}' function is not supported for Playwright. ${recommendation}`);
96
+ }
97
+ }
98
+
90
99
  export default {
91
100
  artifact: saveArtifact,
92
101
  log: logMessage,
@@ -59,14 +59,10 @@ class Logger {
59
59
  * @param {...any} values
60
60
  */
61
61
  step(strings, ...values) {
62
- let logs = '';
63
- for (let i = 0; i < strings.length; i++) {
64
- logs += strings[i];
65
- if (i < values.length) {
66
- logs += values[i];
67
- }
68
- }
69
- logs = pc.blue(`> ${logs}`);
62
+ // Filter trailing undefined from optional params (e.g., step('message') called without second arg)
63
+ const filteredValues = values.filter(v => v !== undefined);
64
+ const message = this.#formatMessage(strings, ...filteredValues);
65
+ const logs = pc.blue(`> ${message}`);
70
66
  dataStorage.putData('log', logs);
71
67
  }
72
68
 
@@ -87,7 +83,11 @@ class Logger {
87
83
  for (const arg of args) {
88
84
  // ignore empty strings
89
85
  if (arg === '') continue;
90
- if (typeof arg === 'string') {
86
+ if (arg === undefined) {
87
+ logs.push('undefined');
88
+ } else if (arg === null) {
89
+ logs.push('null');
90
+ } else if (typeof arg === 'string') {
91
91
  logs.push(arg);
92
92
  } else if (Array.isArray(arg)) {
93
93
  logs.push(arg.join(' '));
@@ -104,37 +104,42 @@ class Logger {
104
104
  }
105
105
 
106
106
  /**
107
- * Tagget template literal. Allows to use different syntaxes:
108
- * 1. Tagget template: log`text ${someVar}`
109
- * 2. Standard: log(`text ${someVar}`)
110
- * 3. Standard with multiple arguments: log('text', someVar)
107
+ * Formats a message from either tagged template literal or standard function call.
108
+ * @param {*} strings - Template strings array or first argument
109
+ * @param {...any} args - Template values or additional arguments
110
+ * @returns {string} Formatted message
111
111
  */
112
- _templateLiteralLog(strings, ...args) {
113
- if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
112
+ #formatMessage(strings, ...args) {
114
113
  if (Array.isArray(args)) args = args.filter(item => item !== '');
115
114
 
116
- let logs;
117
- // this block means tagged template is used (syntax like $`text ${someVar}`)
115
+ // Tagged template syntax: func`text ${someVar}`
118
116
  if (Array.isArray(strings) && strings.length === args.length + 1) {
119
- logs = strings.reduce(
117
+ return strings.reduce(
120
118
  (result, current, index) =>
121
119
  result +
122
120
  current +
123
- // strings are splitted by args when use tagged template, thus we add arg after each string
124
- // it looks like: `string1 arg1 string2 arg2 string3`
125
- (args[index] !== undefined
126
- ? typeof args[index] === 'string'
127
- ? args[index] // add arg as it is
128
- : this.#stringifyLogs(args[index]) // stringify arg
121
+ (index < args.length
122
+ ? args[index] === undefined
123
+ ? 'undefined'
124
+ : typeof args[index] === 'string'
125
+ ? args[index]
126
+ : this.#stringifyLogs(args[index])
129
127
  : ''),
130
- // initial accumulator value
131
128
  '',
132
129
  );
133
- } else {
134
- // this block means arguments syntax is used (syntax like $('text', someVar))
135
- // in this case strings represents just a first argument
136
- logs = this.#stringifyLogs(strings, ...args);
137
130
  }
131
+ // Standard function call: func('text', someVar)
132
+ return this.#stringifyLogs(strings, ...args);
133
+ }
134
+
135
+ /**
136
+ * Tagged template literal. Allows to use different syntaxes:
137
+ * 1. Tagged template: log`text ${someVar}`
138
+ * 2. Standard: log(`text ${someVar}`)
139
+ * 3. Standard with multiple arguments: log('text', someVar)
140
+ */
141
+ _templateLiteralLog(strings, ...args) {
142
+ const logs = this.#formatMessage(strings, ...args);
138
143
  this.#originalUserLogger.log(logs);
139
144
  dataStorage.putData('log', logs);
140
145
  }
@@ -15,6 +15,7 @@ function setS3Credentials(artifacts) {
15
15
  if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
16
16
  if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
17
17
  if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
18
+ if (artifacts.stack_artifacts) process.env.TESTOMATIO_STACK_ARTIFACTS = '1';
18
19
  // endpoint is not received from the server; and shuld be empty if IAM used (credentails obtained from the testomat)
19
20
  process.env.S3_ENDPOINT = artifacts.ENDPOINT || '';
20
21
  }
@@ -25,6 +26,12 @@ function setS3Credentials(artifacts) {
25
26
  * @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
26
27
  */
27
28
  function generateFilterRequestParams(params) {
29
+ // Defensive check: ensure params is an object
30
+ if (!params || typeof params !== 'object') {
31
+ console.error(APP_PREFIX, `Invalid parameters provided. Expected an object, got: ${typeof params}`);
32
+ return;
33
+ }
34
+
28
35
  const { type, id, apiKey } = params;
29
36
 
30
37
  if (!type) {
@@ -53,9 +60,13 @@ function generateFilterRequestParams(params) {
53
60
  * The object has properties "type" and "id".
54
61
  */
55
62
  function parseFilterParams(opts) {
56
- const [type, id] = opts.split('=');
63
+ const [type, ...idParts] = opts.split('=');
64
+ const id = idParts.join('=');
65
+
57
66
  const validType = updateFilterType(type);
58
67
 
68
+ if (!validType) return undefined;
69
+
59
70
  return {
60
71
  type: validType,
61
72
  id,
@@ -69,6 +80,8 @@ function parseFilterParams(opts) {
69
80
  * Returns undefined if the type is not valid.
70
81
  */
71
82
  function updateFilterType(type) {
83
+ if (!type || typeof type !== 'string') return;
84
+
72
85
  let typeLowerCase = type.toLowerCase();
73
86
 
74
87
  const filterTypes = ['tag-name', 'plan', 'label', 'jira-ticket'];
@@ -86,7 +99,7 @@ function updateFilterType(type) {
86
99
  ];
87
100
 
88
101
  if (!filterTypes.includes(typeLowerCase)) {
89
- console.log(APP_PREFIX, `❗❗❗ Invalid "filter=${type}" start settings! Available option list: ${filterTypes}`);
102
+ console.log(APP_PREFIX, `❗❗❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
90
103
  return;
91
104
  }
92
105
 
@@ -120,4 +133,40 @@ function fullName(t) {
120
133
  return line;
121
134
  }
122
135
 
123
- export { updateFilterType, parseFilterParams, generateFilterRequestParams, setS3Credentials, statusEmoji, fullName };
136
+ /**
137
+ * Parses a comma-separated list of key-value pairs into an options object.
138
+ *
139
+ * The input string should be formatted as `"key1=value1,key2=value2,..."`.
140
+ * Whitespace around keys and values is trimmed. If the input is empty or undefined,
141
+ * an empty object is returned.
142
+ *
143
+ * @param {string} [optionsStr] - A comma-separated string of key=value pairs.
144
+ * @returns {Object} An object mapping option keys to their string values.
145
+ *
146
+ * @example
147
+ * parsePipeOptions('foo=bar,baz=qux');
148
+ * => Returns: { foo: 'bar', baz: 'qux' }
149
+ */
150
+ function parsePipeOptions(optionsStr) {
151
+ const options = {};
152
+ if (!optionsStr) return options;
153
+
154
+ const pairs = optionsStr.split(',');
155
+ for (const pair of pairs) {
156
+ const [key, value] = pair.split('=');
157
+ if (key && value) {
158
+ options[key.trim()] = value.trim();
159
+ }
160
+ }
161
+ return options;
162
+ }
163
+
164
+ export {
165
+ updateFilterType,
166
+ parseFilterParams,
167
+ generateFilterRequestParams,
168
+ setS3Credentials,
169
+ statusEmoji,
170
+ fullName,
171
+ parsePipeOptions
172
+ };
@@ -6,6 +6,7 @@ import isValid from 'is-valid-path';
6
6
  import createDebugMessages from 'debug';
7
7
  import os from 'os';
8
8
  import { fileURLToPath } from 'url';
9
+ import { execSync } from 'child_process';
9
10
 
10
11
  const debug = createDebugMessages('@testomatio/reporter:util');
11
12
 
@@ -58,6 +59,21 @@ const validateSuiteId = suiteId => {
58
59
  return match ? match[0] : null;
59
60
  };
60
61
 
62
+ /**
63
+ * Gets current git commit SHA
64
+ * @returns {String|null} git commit SHA or null if not available
65
+ */
66
+ const getGitCommitSha = () => {
67
+ try {
68
+ const sha = execSync('git rev-parse --short HEAD', {
69
+ stdio: ['ignore', 'pipe', 'ignore']
70
+ }).toString().trim();
71
+ return sha || null;
72
+ } catch (error) {
73
+ return null;
74
+ }
75
+ };
76
+
61
77
  const ansiRegExp = () => {
62
78
  const pattern = [
63
79
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -615,6 +631,63 @@ function truncate(s, size = 255) {
615
631
  return `${str.substring(0, size)}...`;
616
632
  }
617
633
 
634
+ function applyFilter(command, tests) {
635
+ if (!tests || !tests.length) return command;
636
+
637
+ const lower = (command || '').toLowerCase();
638
+ const regexPattern = `(${tests.join('|')})`;
639
+
640
+ if (lower.includes('jest')) {
641
+ return `${command} --testNamePattern ${regexPattern}`;
642
+ }
643
+
644
+ if (lower.includes('cypress')) {
645
+ const grepValue = tests.join(',');
646
+ const baseEnv = {
647
+ grep: grepValue,
648
+ grepFilterSpecs: true,
649
+ grepOmitFiltered: true,
650
+ };
651
+
652
+ if (command.includes('--env')) {
653
+ return command.replace(
654
+ /--env\s+(['"]?)([^\s'"]+)\1/,
655
+ (match, quote, envVal) => {
656
+ const existingEnv = {};
657
+
658
+ if (envVal.startsWith('{') && envVal.endsWith('}')) {
659
+ try {
660
+ Object.assign(existingEnv, JSON.parse(envVal));
661
+ } catch (e) {
662
+ }
663
+ }
664
+
665
+ if (!Object.keys(existingEnv).length) {
666
+ envVal.split(',').forEach((pair) => {
667
+ const [k, v] = pair.split('=');
668
+ if (!k) return;
669
+
670
+ if (v === 'true') existingEnv[k] = true;
671
+ else if (v === 'false') existingEnv[k] = false;
672
+ else existingEnv[k] = v;
673
+ });
674
+ }
675
+
676
+ const merged = { ...existingEnv, ...baseEnv };
677
+ const json = JSON.stringify(merged);
678
+
679
+ return `--env ${json}`;
680
+ },
681
+ );
682
+ }
683
+
684
+ const json = JSON.stringify(baseEnv);
685
+ return `${command} --env ${json}`;
686
+ }
687
+
688
+ return `${command} --grep ${regexPattern}`;
689
+ }
690
+
618
691
  export {
619
692
  ansiRegExp,
620
693
  truncate,
@@ -629,6 +702,7 @@ export {
629
702
  foundedTestLog,
630
703
  formatStep,
631
704
  getCurrentDateTime,
705
+ getGitCommitSha,
632
706
  getTestomatIdFromTestTitle,
633
707
  humanize,
634
708
  isValidUrl,
@@ -640,4 +714,5 @@ export {
640
714
  testRunnerHelper,
641
715
  transformEnvVarToBoolean,
642
716
  validateSuiteId,
717
+ applyFilter
643
718
  };
package/types/types.d.ts CHANGED
@@ -15,8 +15,9 @@ declare module '@testomatio/reporter' {
15
15
  /**
16
16
  * Similar to "log" function but marks message in report as a step
17
17
  * @param message - step message
18
+ * @param logs - optional key-value object with additional info (e.g. logs)
18
19
  */
19
- export function step(message: string): void;
20
+ export function step(message: string, logs?: {[key: string]: any}): void;
20
21
 
21
22
  /**
22
23
  * Add key-value pair(s) to the test report