@testomatio/reporter 2.3.9 → 2.4.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.
@@ -0,0 +1,440 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { execSync } from 'child_process';
5
+ import { Gaxios } from 'gaxios';
6
+ import { minimatch } from 'minimatch';
7
+ import { APP_PREFIX, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
8
+ import { generateFilterRequestParams } from '../utils/pipe_utils.js';
9
+ import { parsePipeOptions } from '../utils/pipe_utils.js';
10
+ import { config } from '../config.js';
11
+ import createDebugMessages from 'debug';
12
+
13
+ const debug = createDebugMessages('@testomatio/reporter:pipe:csv');
14
+
15
+ // Example of use 'coverage:file=coverage/coverage.yml,diff=master' cmd:
16
+ // | Option | Git command | Notes |
17
+ // | --- | --- | --- |
18
+ // | --filter "coverage:file=coverage/coverage.yml,diff=new-branch" | ✅ git diff new-branch --name-only | - |
19
+ // | --filter "coverage:diff=master,file=coverage.yml" | ✅ git diff master --name-only | - |
20
+ // | --filter "coverage:file=coverage/coverage.yml" | ✅ git diff master --name-only | default branch = "master" |
21
+ // | --filter "coverage:file=coverage.yml,dif=noexist-branch" | ❌ Git command failed ...| - |
22
+ // | --filter "coverage:file=coverage.yml,diff=noexist-branch" | ❌ Git command failed ...| because no branch found |
23
+ // | --filter "coverage:file=no-exist-coverage.yml" | ❌ Coverage file not found: <>filename>.yml | - |
24
+ // | --filter "coverage:filepath=coverage.yml" | 🚫 Missing required parameter: "file"... | - |
25
+ // | --filter "coverage:diff=my-branch" | 🚫 Missing required parameter: "file"...| - |
26
+
27
+ //maybe GitChanges or GitCoverage
28
+ class CoveragePipe { // or Changes for the future???
29
+ #GIT = {
30
+ default_branch: 'master',
31
+ diff_command: 'git diff',
32
+ only_file_opt: '--name-only',
33
+ uncommitted_marker: 'uncommitted',
34
+ // test_defaultGitChangedFile - uses only for unit tests in "coverage_pipe_test.js" file
35
+ test_defaultGitChangedFile: ['todomvc-tests/edit-todos_test.js'],
36
+ };
37
+
38
+ constructor(params, store) {
39
+ this.id = 'coverage'; // as future updates -> find by id in client.js
40
+ this.store = store || {};
41
+ this.branch = undefined;
42
+ this.isDefaultGitChanges = false; // COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
43
+ this.isEnabled = false;
44
+
45
+ const { pipeOptions } = params;
46
+ const options = parsePipeOptions(pipeOptions || "");
47
+ debug("Pipe options", options);
48
+
49
+ // this.isDefaultGitChanges - COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
50
+ this.isDefaultGitChanges = process.env.COVERAGE_BY_DEFAULT_GIT_FILE === '1'? true : false;
51
+ this.coverageFilePath = options?.file || process.env.COVERAGE_FILEPATH || undefined ;
52
+
53
+ if (!this.coverageFilePath) return;
54
+
55
+ this.branch = options?.diff || process.env.COVERAGE_BRANCH || this.#GIT.default_branch;
56
+ this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
57
+
58
+ if (this.isBranchDefault) {
59
+ console.log(
60
+ APP_PREFIX,
61
+ `🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
62
+ '👉 You can set it via --filter "coverage:file=coverage.yml,diff=your-branch"'
63
+ );
64
+ }
65
+
66
+ // Client config section
67
+ this.formattedDate = new Date().toISOString().replace(/T/, '-').replace(/:/g, '-').split('.')[0];
68
+ this.title = process.env.TESTOMATIO_TITLE || `Testomatio Coverage Test Execution - ${this.formattedDate}`;
69
+ this.apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
70
+
71
+ this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
72
+ const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || null;
73
+ const proxy = proxyUrl ? new URL(proxyUrl) : null;
74
+
75
+ // Create a new instance of gaxios with a custom config
76
+ this.client = new Gaxios({
77
+ baseURL: `${this.url.trim()}`,
78
+ timeout: AXIOS_TIMEOUT,
79
+ proxy: proxy ? proxy.toString() : undefined,
80
+ retry: true,
81
+ retryConfig: {
82
+ retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
83
+ retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
84
+ httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
85
+ shouldRetry: (error) => {
86
+ if (!error.response) return false;
87
+ switch (error.response?.status) {
88
+ case 400: // Bad request (probably wrong API key)
89
+ case 404: // Test not matched
90
+ case 429: // Rate limit exceeded
91
+ case 500: // Internal server error
92
+ return false;
93
+ default:
94
+ break;
95
+ }
96
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
97
+ }
98
+ }
99
+ });
100
+
101
+ // In case if we have all needed data
102
+ this.isEnabled = true;
103
+
104
+ debug('Coverage Pipe initialized', {
105
+ branch: this.branch,
106
+ coverageFilePath: this.coverageFilePath,
107
+ });
108
+
109
+ this.parsedCoverage = {};
110
+ this.changedFiles = [];
111
+ this.matchedLines = new Set();
112
+ this.tests = new Set();
113
+ this.suiteIds = new Set();
114
+ this.tagLabels = new Set();
115
+
116
+ this.results = [];
117
+
118
+ debug(`Coverage Pipe: is Enabled = ${this.isEnabled}`);
119
+ }
120
+
121
+ async prepareRun(opts) {
122
+ // Reset internal mutable state for isolation
123
+ this.tests.clear();
124
+ this.suiteIds.clear();
125
+ this.tagLabels.clear();
126
+ this.results = [];
127
+
128
+ if (!this.isEnabled) return [];
129
+
130
+ // Step 1: Validate coverage file path & Git changes & Coverage parsing
131
+ if (!this.getGitChangedFiles()?.validateCoverageFile()?.parseCoverageFile()) return [];
132
+
133
+ // Step 2: Extract all available tests and compare with coverage file
134
+ const lines = await this.extractRelevantTestsFromChanges();
135
+
136
+ if (lines.size === 0) {
137
+ console.log(APP_PREFIX, 'ℹ️ No matching entries in coverage file for provided Git changes.');
138
+ return [];
139
+ }
140
+
141
+ // Step 3: Handle tag labels tests from the server
142
+ // if (this.tagLabels && this.tagLabels.size > 0) { //TODO: in case if we add labels in future!!!
143
+ if (this.tagLabels.size > 0) {
144
+ for (const tag of this.tagLabels) {
145
+ const tagType = 'tag';
146
+ const tests = await this.#getTestomatioTestsByParam(tagType, tag);
147
+
148
+ if (!tests) return [];
149
+
150
+ console.log(
151
+ APP_PREFIX,
152
+ `✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
153
+ ' in Testomat.io service side.'
154
+ );
155
+
156
+ tests.forEach(testId => this.tests.add(testId));
157
+ }
158
+ }
159
+
160
+ if (this.tests.size === 0) {
161
+ console.log(APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
162
+ return [];
163
+ }
164
+
165
+ this.results = [...this.tests];
166
+
167
+ return this.results;
168
+ }
169
+
170
+ addTest(data) {}
171
+
172
+ async createRun() {}
173
+
174
+ updateRun() {}
175
+
176
+ async finishRun(runParams) {}
177
+
178
+ toString() {
179
+ return 'Coverage Reporter';
180
+ }
181
+
182
+ /**
183
+ * Fetches a list of tests from the Testomat.io server based on a given filter parameter.
184
+ *
185
+ * The method parses the provided filter (`type=id`), builds the appropriate request parameters,
186
+ * sends a GET request to the Testomat.io `/api/test_grep` endpoint, and returns the matching tests.
187
+ *
188
+ * If the filter is invalid, no query is generated, or the server responds with no matching tests,
189
+ * it logs relevant information and returns `undefined`.
190
+ *
191
+ * @async function
192
+ * @param {string} type - The filter string in the format like `tag-name` for tag by.
193
+ * @param {string} id - The filter string in the format like `smoke`.
194
+ * @returns {Promise<Array<Object>|undefined>} Resolves to an array of test objects if found, otherwise `undefined`.
195
+ */
196
+ async #getTestomatioTestsByParam(type, id) {
197
+ // Get tests from the server
198
+ try {
199
+ const q = generateFilterRequestParams({
200
+ type,
201
+ id,
202
+ apiKey: this?.apiKey?.trim(),
203
+ });
204
+
205
+ if (!q) {
206
+ return;
207
+ }
208
+
209
+ const resp = await this.client.request({
210
+ method: 'GET',
211
+ url: '/api/test_grep',
212
+ ...q,
213
+ });
214
+
215
+ if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
216
+ console.log(APP_PREFIX, `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
217
+
218
+ return undefined;
219
+ }
220
+
221
+ return resp.data.tests;
222
+ }
223
+ catch (err) {
224
+ console.error(
225
+ APP_PREFIX,
226
+ `🚩 Error getting available tests from the Testomat.io by "test_grep" option: ${err}`
227
+ );
228
+
229
+ return undefined;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Executes a Git command to retrieve a list of changed files.
235
+ *
236
+ * @param {string} cmd - The Git command to execute.
237
+ * @returns {string[]} An array of changed file paths. Returns an empty array if an error occurs
238
+ * (e.g., not a Git repository or command failure).
239
+ */
240
+ #getChangedFilesFromGit(cmd) {
241
+ try {
242
+ const result = execSync(cmd, {
243
+ encoding: 'utf-8',
244
+ stdio: ['pipe', 'pipe', 'ignore']
245
+ });
246
+
247
+ return result
248
+ .split('\n')
249
+ .map(f => f.trim())
250
+ .filter(Boolean);
251
+ }
252
+ catch (err) {
253
+ const errorMessage = err.message || '';
254
+ // Git edge: Not a git repository or other error
255
+ if (errorMessage.includes('Not a git repository')) {
256
+ console.error(APP_PREFIX, '❌ Error: This folder is not a Git repository.');
257
+ }
258
+ else {
259
+ throw new Error(`❌ Git command failed ("${cmd}"):\n`, errorMessage);
260
+ }
261
+
262
+ return [];
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Builds a Git command string to list file changes between the current state
268
+ * and a specified Git branch using `git diff --name-only`.
269
+ *
270
+ * Private pipe function
271
+ * @throws {Error} Throws an error if `this.branch` is not defined.
272
+ * @returns {string} A Git command string, e.g., 'git diff <branch> --name-only'.
273
+ */
274
+ #buildGitCommand() {
275
+ if (!this.branch) throw new Error(`❌ Invalid changes option for setted branch!`);
276
+
277
+ return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
278
+ }
279
+
280
+ /**
281
+ * Retrieves the list of files changed in the current Git working directory
282
+ * compared to a specified branch.
283
+ *
284
+ * This method builds a Git diff command and attempts to retrieve the changed
285
+ * files using that command. It logs helpful information and errors during the process.
286
+ *
287
+ * If no changed files are found, or an error occurs at any stage, the method logs
288
+ * the issue and returns `undefined`.
289
+ *
290
+ * @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
291
+ * otherwise, returns `undefined`.
292
+ */
293
+ getGitChangedFiles() {
294
+ let cmd;
295
+
296
+ try {
297
+ cmd = this.#buildGitCommand();
298
+ }
299
+ catch (err) {
300
+ console.error(APP_PREFIX, err.message);
301
+ return undefined;
302
+ }
303
+
304
+ console.error(APP_PREFIX, `ℹ️ We will use '${cmd}' Git command.`);
305
+
306
+ try {
307
+ // For clear unit testing process -> Like test_defaultGitChangedFile = todomvc-tests/edit-todos_test.js
308
+ if (this.isDefaultGitChanges) {
309
+ this.changedFiles = this.#GIT.test_defaultGitChangedFile;
310
+ }
311
+ else {
312
+ this.changedFiles = this.#getChangedFilesFromGit(cmd);
313
+
314
+ if (this.changedFiles.length === 0) {
315
+ console.log(
316
+ APP_PREFIX,
317
+ 'ℹ️ No files changed in the latest Git commit. Skipping coverage processing.'
318
+ );
319
+
320
+ return undefined;
321
+ }
322
+ }
323
+ }
324
+ catch (err) {
325
+ console.error(APP_PREFIX, err.message);
326
+ console.error(APP_PREFIX, "🔍 Pls, check this Git command manually to understand the original problem.");
327
+ return undefined;
328
+ }
329
+
330
+ console.log(APP_PREFIX, `📑 GIT changed files:\n - ${this.changedFiles.join('\n - ')}`);
331
+ return this;
332
+ }
333
+
334
+ /**
335
+ * Validates the coverage file path (stored in `this.coverageFilePath`).
336
+ *
337
+ * This method checks:
338
+ * - That the file exists on disk.
339
+ * - That it is a regular file (not a directory or special file).
340
+ * - That it has a `.yml` extension to ensure it's a YAML file.
341
+ *
342
+ * Logs descriptive error messages for any failures.
343
+ *
344
+ * @returns {this | undefined} "true" in case if coverage file is valid and we can keep going;
345
+ * otherwise, `undefined`.
346
+ */
347
+ validateCoverageFile() {
348
+ // Validate the presence of the coverage filepath
349
+ if (!fs.existsSync(this.coverageFilePath)) {
350
+ console.log(APP_PREFIX, '❌ Coverage file not found:', this.coverageFilePath);
351
+ return undefined;
352
+ }
353
+
354
+ // Ensure the given path is a file (not a directory or other type)
355
+ const stat = fs.statSync(this.coverageFilePath);
356
+ if (!stat.isFile()) {
357
+ console.log(APP_PREFIX, '❌ Provided coverage path is not a file:', this.coverageFilePath);
358
+ return undefined;
359
+ }
360
+
361
+ // Validate the file extension to be ".yml" to ensure it's a YAML file
362
+ if (path.extname(this.coverageFilePath) !== ".yml") {
363
+ console.log(APP_PREFIX, '❌ Coverage file must have a .yml extension:', this.coverageFilePath);
364
+ return undefined;
365
+ }
366
+
367
+ debug('Coverage file validation is OK!');
368
+
369
+ return this;
370
+ }
371
+
372
+ /**
373
+ * Parses the YAML coverage file (located at `this.coverageFilePath`) into a JavaScript object.
374
+ *
375
+ * - Reads the file content using UTF-8 encoding.
376
+ * - Parses it as YAML using the `yaml` library.
377
+ * - Stores the result in `this.parsedCoverage`.
378
+ * - If parsing fails, logs an error and returns `undefined`.
379
+ *
380
+ * @returns {this | undefined} The current parsed coverage yml file change lines, or "undefined" if parsing fails.
381
+ */
382
+ parseCoverageFile() {
383
+ try {
384
+ // Read the contents of the YAML file and attempt to parse the YAML into a JavaScript object
385
+ const rawYml = fs.readFileSync(this.coverageFilePath, 'utf8');
386
+ this.parsedCoverage = yaml.load(rawYml) || {};
387
+
388
+ debug(`Coverage filepath = ${this.coverageFilePath})`);
389
+ console.log(APP_PREFIX, `✅ Coverage file parsed successfully: ${this.coverageFilePath}`);
390
+
391
+ return this;
392
+ }
393
+ catch (err) {
394
+ console.error(APP_PREFIX, '❌ Failed to parse YAML:', err.message);
395
+ return undefined;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Extracts relevant test identifiers from changed files based on coverage mapping.
401
+ *
402
+ * Iterates over changed files and matches them against patterns in the parsed coverage data.
403
+ * For each match, it extracts test IDs or tags and stores them in corresponding sets:
404
+ * - Test IDs (starting with '@T' or '@S') are stored in `this.tests`
405
+ * - Tag labels (starting with 'tag:') are stored in `this.tagLabels`
406
+ * - Matched file paths are stored in `this.matchedLines`
407
+ *
408
+ * @returns {Promise <Set<string>>} A set of file paths that matched coverage patterns (`this.matchedLines`).
409
+ */
410
+ async extractRelevantTestsFromChanges() {
411
+ for (const changedFile of this.changedFiles) {
412
+ for (const [pattern, ids] of Object.entries(this.parsedCoverage)) {
413
+ if (minimatch(changedFile, pattern)) {
414
+ this.matchedLines.add(changedFile);
415
+
416
+ ids.forEach(id => {
417
+ // Example: "@Tt74099t1"
418
+ if (id.startsWith('@T')) {
419
+ this.tests.add(id.slice(1));
420
+ }
421
+ // Example: "@Sd74099c1"
422
+ else if (id.startsWith('@S')) {
423
+ this.tests.add(id.slice(1));
424
+ }
425
+ // Example: "tag:@TestSmoke"
426
+ else if (id.startsWith('tag')) {
427
+ this.tagLabels.add(id.split(':')[1].slice(1));
428
+ }
429
+ });
430
+ }
431
+ }
432
+ }
433
+
434
+ debug(`Matched lines: ${this.matchedLines}`);
435
+
436
+ return this.matchedLines;
437
+ }
438
+ }
439
+
440
+ export default CoveragePipe;
package/src/pipe/index.js CHANGED
@@ -7,6 +7,7 @@ import GitHubPipe from './github.js';
7
7
  import GitLabPipe from './gitlab.js';
8
8
  import CsvPipe from './csv.js';
9
9
  import HtmlPipe from './html.js';
10
+ import CoveragePipe from './coverage.js';
10
11
  import { BitbucketPipe } from './bitbucket.js';
11
12
  import { DebugPipe } from './debug.js';
12
13
 
@@ -48,6 +49,7 @@ export async function pipesFactory(params, opts) {
48
49
  new CsvPipe(params, opts),
49
50
  new HtmlPipe(params, opts),
50
51
  new BitbucketPipe(params, opts),
52
+ new CoveragePipe(params, opts),
51
53
  new DebugPipe(params, opts),
52
54
  ...extraPipes,
53
55
  ];
@@ -3,7 +3,12 @@ import pc from 'picocolors';
3
3
  import { Gaxios } from 'gaxios';
4
4
  import JsonCycle from 'json-cycle';
5
5
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
6
- import { isValidUrl, foundedTestLog, readLatestRunId, transformEnvVarToBoolean } from '../utils/utils.js';
6
+ import { isValidUrl,
7
+ foundedTestLog,
8
+ readLatestRunId,
9
+ transformEnvVarToBoolean,
10
+ getGitCommitSha
11
+ } from '../utils/utils.js';
7
12
  import { parseFilterParams, generateFilterRequestParams, setS3Credentials } from '../utils/pipe_utils.js';
8
13
  import { config } from '../config.js';
9
14
 
@@ -46,7 +51,25 @@ class TestomatioPipe {
46
51
  this.store = store || {};
47
52
  this.title = params.title || process.env.TESTOMATIO_TITLE;
48
53
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
49
- this.sharedRunTimeout = !!process.env.TESTOMATIO_SHARED_RUN_TIMEOUT;
54
+ this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
55
+ ? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
56
+ : undefined;
57
+
58
+ if (this.sharedRunTimeout && !this.sharedRun) {
59
+ debug('Auto-enabling sharedRun because sharedRunTimeout is set');
60
+ this.sharedRun = true;
61
+ }
62
+
63
+ if (!this.title && (this.sharedRun || this.sharedRunTimeout)) {
64
+ const sha = getGitCommitSha();
65
+ if (sha) {
66
+ this.title = `Shared Run - ${sha}`;
67
+ console.log(APP_PREFIX, `🔄 Auto-generated title for shared run: ${this.title}`);
68
+ } else {
69
+ console.log(APP_PREFIX, pc.red('Failed to resolve git commit SHA for shared run title.'));
70
+ console.log(APP_PREFIX, 'Please run the tests inside a Git repository or set TESTOMATIO_TITLE explicitly.');
71
+ }
72
+ }
50
73
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
51
74
  this.env = process.env.TESTOMATIO_ENV;
52
75
  this.label = process.env.TESTOMATIO_LABEL;
@@ -129,17 +152,23 @@ class TestomatioPipe {
129
152
  async prepareRun(opts) {
130
153
  if (!this.isEnabled) return [];
131
154
 
132
- const { type, id } = parseFilterParams(opts);
155
+ const clearOptions = parseFilterParams(opts);
156
+
157
+ if (!clearOptions) {
158
+ return [];
159
+ }
160
+
161
+ const { type, id } = clearOptions;
133
162
 
134
163
  try {
135
164
  const q = generateFilterRequestParams({
136
165
  type,
137
166
  id,
138
- apiKey: this.apiKey.trim(),
167
+ apiKey: this?.apiKey?.trim(),
139
168
  });
140
169
 
141
170
  if (!q) {
142
- return;
171
+ return [];
143
172
  }
144
173
 
145
174
  const resp = await this.client.request({
@@ -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,7 +19,6 @@ 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
 
@@ -30,10 +28,10 @@ function logMessage(...args) {
30
28
  * @returns {void}
31
29
  */
32
30
  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
31
  services.logger.step(message);
32
+ // this is done because Playwright reporter intercepts console logs and then we gather them and show on Testomat
33
+ // if not console.log, the step message will be lost from reporter
34
+ if (isPlaywright) console.log(`Step: ${message}`);
37
35
  }
38
36
 
39
37
  /**
@@ -43,8 +41,7 @@ function addStep(message) {
43
41
  * @returns {void}
44
42
  */
45
43
  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.');
44
+ showPlaywrightWarning('meta', 'Use test annotations instead.');
48
45
 
49
46
  if (typeof keyValue === 'string') {
50
47
  keyValue = { [keyValue]: value };
@@ -59,6 +56,7 @@ function setKeyValue(keyValue, value = null) {
59
56
  * @returns {void}
60
57
  */
61
58
  function setLabel(key, value = null) {
59
+ showPlaywrightWarning('label', 'Use test tag instead.');
62
60
  if (Array.isArray(value)) {
63
61
  return value.forEach(label => setLabel(key, label));
64
62
  }
@@ -87,6 +85,12 @@ function linkJira(...jiraIds) {
87
85
  services.links.put(links);
88
86
  }
89
87
 
88
+ function showPlaywrightWarning(functionName, recommendation) {
89
+ if (isPlaywright) {
90
+ console.warn(`[TESTOMATIO] '${functionName}' function is not supported for Playwright. ${recommendation}`);
91
+ }
92
+ }
93
+
90
94
  export default {
91
95
  artifact: saveArtifact,
92
96
  log: logMessage,
@@ -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
+ };