@testomatio/reporter 2.3.0-beta.5-links → 2.3.0-beta.6-links

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.
@@ -26,6 +26,8 @@ const HOOK_EXECUTION_ORDER = {
26
26
  PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
27
27
  POST_TEST: ['AfterHook', 'AfterSuiteHook']
28
28
  };
29
+ // codeceptjs workers are self-contained
30
+ data_storage_js_1.dataStorage.isFileStorage = false;
29
31
  const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
30
32
  if (MAJOR_VERSION < 3) {
31
33
  console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
@@ -399,10 +401,10 @@ function formatHookStep(step) {
399
401
  // For hook steps, construct title from available properties
400
402
  let title = step.name;
401
403
  if (step.actor && step.name) {
402
- title = `${step.actor} ${step.name}`;
404
+ title = `${step.actor}.${step.name}`;
403
405
  if (step.args && step.args.length > 0) {
404
406
  const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
405
- title += ` ${argsStr}`;
407
+ title += `(${argsStr})`;
406
408
  }
407
409
  }
408
410
  return {
@@ -105,6 +105,7 @@ class CucumberReporter extends cucumber_1.Formatter {
105
105
  const logs = index_js_1.services.logger.getLogs(testTitle).join('\n');
106
106
  const artifacts = index_js_1.services.artifacts.get(testTitle);
107
107
  const keyValues = index_js_1.services.keyValues.get(testTitle);
108
+ const links = index_js_1.services.links.get(testTitle);
108
109
  this.client.addTestRun(status, {
109
110
  // error: testCaseAttempt.worstTestStepResult.message,
110
111
  message,
@@ -114,6 +115,7 @@ class CucumberReporter extends cucumber_1.Formatter {
114
115
  .trim(),
115
116
  example: { ...example },
116
117
  logs,
118
+ links,
117
119
  manuallyAttachedArtifacts: artifacts,
118
120
  meta: keyValues,
119
121
  title: scenario,
@@ -57,6 +57,7 @@ class JestReporter {
57
57
  const logs = getTestLogs(result);
58
58
  const artifacts = index_js_1.services.artifacts.get(result.fullName);
59
59
  const keyValues = index_js_1.services.keyValues.get(result.fullName);
60
+ const links = index_js_1.services.links.get(result.fullName);
60
61
  const deducedStatus = status === 'pending' ? 'skipped' : status;
61
62
  // In jest if test is not matched with test name pattern it is considered as skipped.
62
63
  // So adding a check if it is skipped for real or because of test pattern
@@ -69,6 +70,7 @@ class JestReporter {
69
70
  title,
70
71
  time: duration,
71
72
  logs,
73
+ links,
72
74
  manuallyAttachedArtifacts: artifacts,
73
75
  meta: keyValues,
74
76
  });
@@ -48,8 +48,6 @@ class PlaywrightReporter {
48
48
  steps.push(appendedStep);
49
49
  }
50
50
  }
51
- // Extract and normalize tags
52
- const tags = extractTags(test);
53
51
  const fullTestTitle = getTestContextName(test);
54
52
  let logs = '';
55
53
  if (result.stderr.length || result.stdout.length) {
@@ -57,6 +55,7 @@ class PlaywrightReporter {
57
55
  }
58
56
  const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(fullTestTitle);
59
57
  const testMeta = index_js_1.services.keyValues.get(fullTestTitle);
58
+ const links = index_js_1.services.links.get(fullTestTitle);
60
59
  const rid = test.id || test.testId || (0, uuid_1.v4)();
61
60
  /**
62
61
  * @type {{
@@ -87,13 +86,13 @@ class PlaywrightReporter {
87
86
  const reportTestPromise = this.client.addTestRun(checkStatus(status), {
88
87
  rid: `${rid}-${project.name}`,
89
88
  error,
90
- test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
89
+ test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${test.tags?.join(' ')}`),
91
90
  suite_title,
92
91
  title,
93
- tags,
94
92
  steps: steps.length ? steps : undefined,
95
93
  time: duration,
96
94
  logs,
95
+ links,
97
96
  manuallyAttachedArtifacts,
98
97
  meta: {
99
98
  browser: project.browser,
@@ -219,29 +218,6 @@ function generateTmpFilepath(filename = '') {
219
218
  const tmpdir = os_1.default.tmpdir();
220
219
  return path_1.default.join(tmpdir, filename);
221
220
  }
222
- /**
223
- * Extracts and normalizes tags from test title, test options, and suite level
224
- * @param {*} test - testInfo object from Playwright
225
- * @returns {string[]} - array of normalized tags
226
- */
227
- function extractTags(test) {
228
- const tagsSet = new Set();
229
- // Extract tags from test title (@tag format)
230
- const titleTagsMatch = test.title.match(/@\w+/g);
231
- if (titleTagsMatch) {
232
- titleTagsMatch.forEach(tag => {
233
- tagsSet.add(tag.replace('@', '').toLowerCase());
234
- });
235
- }
236
- // Extract tags from test.tags (Playwright built-in tags)
237
- if (test.tags && Array.isArray(test.tags)) {
238
- test.tags.forEach(tag => {
239
- const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
240
- tagsSet.add(normalizedTag);
241
- });
242
- }
243
- return Array.from(tagsSet);
244
- }
245
221
  /**
246
222
  * Returns filename + test title
247
223
  * @param {*} test - testInfo object from Playwright
@@ -41,7 +41,6 @@ const client_js_1 = __importDefault(require("../client.js"));
41
41
  const utils_js_1 = require("../utils/utils.js");
42
42
  const index_js_1 = require("../services/index.js");
43
43
  const constants_js_1 = require("../constants.js");
44
- const data_storage_js_1 = require("../data-storage.js");
45
44
  class WebdriverReporter extends reporter_1.default {
46
45
  constructor(options) {
47
46
  super(options);
@@ -78,12 +77,10 @@ class WebdriverReporter extends reporter_1.default {
78
77
  onTestEnd(test) {
79
78
  test.suite = test.parent;
80
79
  const logs = getTestLogs(test.fullTitle);
81
- // still be under investigation
82
- const artifacts = index_js_1.services.artifacts.get(test.fullTitle);
83
- const keyValues = index_js_1.services.keyValues.get(test.fullTitle);
80
+ test.artifacts = index_js_1.services.artifacts.get(test.fullTitle);
81
+ test.meta = index_js_1.services.keyValues.get(test.fullTitle);
82
+ test.links = index_js_1.services.links.get(test.fullTitle);
84
83
  test.logs = logs;
85
- test.artifacts = artifacts;
86
- test.meta = keyValues;
87
84
  this._addTestPromises.push(this.addTest(test));
88
85
  }
89
86
  // wdio-cucumber does not trigger onTestEnd hook, thus, using this one
@@ -95,19 +92,19 @@ class WebdriverReporter extends reporter_1.default {
95
92
  async addTest(test) {
96
93
  if (!this.client)
97
94
  return;
98
- const { title, _duration: duration, state, error, output } = test;
95
+ const { title, _duration: duration, state, error, output, links, artifacts, meta, logs } = test;
99
96
  const testId = (0, utils_js_1.getTestomatIdFromTestTitle)(title);
100
97
  const screenshotEndpoint = '/session/:sessionId/screenshot';
101
98
  const screenshotsBuffers = output
102
99
  .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
103
100
  .map(el => Buffer.from(el.result.value, 'base64'));
104
- const rid = (0, data_storage_js_1.stringToMD5Hash)(test.fullTitle);
105
101
  await this.client.addTestRun(state, {
106
- rid,
107
- manuallyAttachedArtifacts: test.artifacts,
102
+ rid: test.uid || '',
103
+ manuallyAttachedArtifacts: artifacts,
108
104
  error,
109
- logs: test.logs,
110
- meta: test.meta,
105
+ logs,
106
+ meta,
107
+ links,
111
108
  title,
112
109
  test_id: testId,
113
110
  time: duration,
package/lib/bin/cli.js CHANGED
@@ -78,7 +78,7 @@ program
78
78
  console.log(constants_js_1.APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
79
79
  return process.exit(255);
80
80
  }
81
- const client = new client_js_1.default({ apiKey, title });
81
+ const client = new client_js_1.default({ apiKey, title, parallel: true });
82
82
  if (opts.filter) {
83
83
  const [pipe, ...optsArray] = opts.filter.split(':');
84
84
  const pipeOptions = optsArray.join(':');
@@ -95,16 +95,13 @@ program
95
95
  console.log(constants_js_1.APP_PREFIX, `🚀 Running`, picocolors_1.default.green(command));
96
96
  const runTests = async () => {
97
97
  const testCmds = command.split(' ');
98
- const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), {
99
- stdio: 'inherit',
100
- env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
101
- });
98
+ const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
102
99
  cmd.on('close', async (code) => {
103
100
  const emoji = code === 0 ? '🟢' : '🔴';
104
101
  console.log(constants_js_1.APP_PREFIX, emoji, `Runner exited with ${picocolors_1.default.bold(code)}`);
105
102
  if (apiKey) {
106
103
  const status = code === 0 ? 'passed' : 'failed';
107
- await client.updateRunStatus(status);
104
+ await client.updateRunStatus(status, true);
108
105
  }
109
106
  process.exit(code);
110
107
  });
@@ -260,13 +257,13 @@ program
260
257
  const replayService = new replay_js_1.default({
261
258
  apiKey: config_js_1.config.TESTOMATIO,
262
259
  dryRun: opts.dryRun,
263
- onLog: message => console.log(constants_js_1.APP_PREFIX, message),
264
- onError: message => console.error(constants_js_1.APP_PREFIX, '⚠️ ', message),
260
+ onLog: (message) => console.log(constants_js_1.APP_PREFIX, message),
261
+ onError: (message) => console.error(constants_js_1.APP_PREFIX, '⚠️ ', message),
265
262
  onProgress: ({ current, total }) => {
266
263
  if (current % 10 === 0 || current === total) {
267
264
  console.log(constants_js_1.APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
268
265
  }
269
- },
266
+ }
270
267
  });
271
268
  const result = await replayService.replay(debugFile);
272
269
  if (result.dryRun) {
package/lib/client.d.ts CHANGED
@@ -58,9 +58,10 @@ export class Client {
58
58
  * Updates the status of the current test run and finishes the run.
59
59
  * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
60
60
  * Must be one of "passed", "failed", or "finished"
61
+ * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
61
62
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
62
63
  */
63
- updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
64
+ updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", isParallel?: boolean): Promise<any>;
64
65
  /**
65
66
  * Returns the formatted stack including the stack trace, steps, and logs.
66
67
  * @returns {string}
package/lib/client.js CHANGED
@@ -50,7 +50,6 @@ const path_1 = __importStar(require("path"));
50
50
  const node_url_1 = require("node:url");
51
51
  const uploader_js_1 = require("./uploader.js");
52
52
  const utils_js_1 = require("./utils/utils.js");
53
- const links_js_1 = require("./services/links.js");
54
53
  const filesize_1 = require("filesize");
55
54
  const debug = (0, debug_1.default)('@testomatio/reporter:client');
56
55
  // removed __dirname usage, because:
@@ -182,40 +181,11 @@ class Client {
182
181
  /**
183
182
  * @type {TestData}
184
183
  */
185
- const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, manuallyAttachedArtifacts, overwrite, tags, } = testData;
184
+ const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, } = testData;
186
185
  let { message = '', meta = {} } = testData;
187
186
  // stringify meta values and limit keys and values length to 255
188
187
  meta = Object.entries(meta)
189
188
  .filter(([, value]) => value !== null && value !== undefined)
190
- .map(([key, value]) => {
191
- try {
192
- if (typeof value === 'object') {
193
- value = JSON.stringify(value);
194
- }
195
- else if (typeof value !== 'string') {
196
- try {
197
- value = value.toString();
198
- }
199
- catch (err) {
200
- console.warn(constants_js_1.APP_PREFIX, `Can't convert meta value to string`, err);
201
- }
202
- }
203
- if (value?.length > 255) {
204
- value = value.substring(0, 255);
205
- debug(constants_js_1.APP_PREFIX, `Meta info value "${value}" is too long, trimmed to 255 characters`);
206
- }
207
- if (key?.length > 255) {
208
- const newKey = key.substring(0, 255);
209
- debug(constants_js_1.APP_PREFIX, `Meta info key "${key}" is too long, trimmed to 255 characters`);
210
- return [newKey, value];
211
- }
212
- return [key, value];
213
- }
214
- catch (err) {
215
- debug(constants_js_1.APP_PREFIX, `Error while processing meta info key ${key}`, err);
216
- return [null, null];
217
- }
218
- })
219
189
  .reduce((acc, [key, value]) => {
220
190
  if (key)
221
191
  acc[key] = value;
@@ -223,7 +193,6 @@ class Client {
223
193
  }, {});
224
194
  // Get links from storage using the test context
225
195
  const testContext = suite_title ? `${suite_title} ${title}` : title;
226
- const links = links_js_1.linkStorage.get(testContext) || [];
227
196
  let errorFormatted = '';
228
197
  if (error) {
229
198
  errorFormatted += this.formatError(error) || '';
@@ -273,7 +242,6 @@ class Client {
273
242
  meta,
274
243
  links,
275
244
  overwrite,
276
- tags,
277
245
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
278
246
  };
279
247
  // debug('Adding test run...', data);
@@ -295,16 +263,17 @@ class Client {
295
263
  * Updates the status of the current test run and finishes the run.
296
264
  * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
297
265
  * Must be one of "passed", "failed", or "finished"
266
+ * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
298
267
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
299
268
  */
300
- async updateRunStatus(status) {
269
+ async updateRunStatus(status, isParallel = false) {
301
270
  this.pipes ||= await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
302
271
  this.runId ||= (0, utils_js_1.readLatestRunId)();
303
272
  debug('Updating run status...');
304
273
  // all pipes disabled, skipping
305
274
  if (!this.pipes?.filter(p => p.isEnabled).length)
306
275
  return Promise.resolve();
307
- const runParams = { status };
276
+ const runParams = { status, parallel: isParallel };
308
277
  this.queue = this.queue
309
278
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
310
279
  .then(() => {
@@ -143,7 +143,7 @@ class DataStorage {
143
143
  if (global?.testomatioDataStore[dataType]) {
144
144
  const testData = global.testomatioDataStore[dataType][context];
145
145
  if (testData)
146
- debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
146
+ debug('<=', dataType, 'global', context, testData);
147
147
  return testData || [];
148
148
  }
149
149
  // debug(`No ${this.dataType} data for context ${context} in <global> storage`);
@@ -165,7 +165,7 @@ class DataStorage {
165
165
  if (fs_1.default.existsSync(filepath)) {
166
166
  const testDataAsText = fs_1.default.readFileSync(filepath, 'utf-8');
167
167
  if (testDataAsText)
168
- debug(`"${dataType}" data for context "${context}":`, testDataAsText);
168
+ debug('<=', dataType, 'file', context, testDataAsText);
169
169
  const testDataArr = testDataAsText?.split(os_1.default.EOL) || [];
170
170
  return testDataArr;
171
171
  }
@@ -184,7 +184,7 @@ class DataStorage {
184
184
  * @param {*} context
185
185
  */
186
186
  #putDataToGlobalVar(dataType, data, context) {
187
- debug('Saving data to global variable for ', context, ':', data);
187
+ debug('=>', dataType, 'global', context, data);
188
188
  if (!global.testomatioDataStore)
189
189
  global.testomatioDataStore = {};
190
190
  if (!global.testomatioDataStore?.[dataType])
@@ -208,7 +208,7 @@ class DataStorage {
208
208
  const filepath = (0, path_1.join)(dataDirPath, filename);
209
209
  if (!fs_1.default.existsSync(dataDirPath))
210
210
  utils_js_1.fileSystem.createDir(dataDirPath);
211
- debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
211
+ debug('=>', dataType, 'file', context, data);
212
212
  // append new line if file already exists (in this case its definitely includes some data)
213
213
  if (fs_1.default.existsSync(filepath)) {
214
214
  fs_1.default.appendFileSync(filepath, os_1.default.EOL + data, 'utf-8');
@@ -222,7 +222,7 @@ function stringToMD5Hash(str) {
222
222
  const md5 = crypto_1.default.createHash('md5');
223
223
  md5.update(str);
224
224
  const hash = md5.digest('hex');
225
- return hash;
225
+ return `${process.env.runId || 'run'}_${hash}`;
226
226
  }
227
227
  exports.dataStorage = DataStorage.getInstance();
228
228
  // TODO: consider using fs promises instead of writeSync/appendFileSync to
@@ -23,6 +23,7 @@ declare class TestomatioPipe implements Pipe {
23
23
  isEnabled: boolean;
24
24
  url: any;
25
25
  apiKey: any;
26
+ parallel: any;
26
27
  store: any;
27
28
  title: any;
28
29
  sharedRun: boolean;
@@ -43,6 +43,7 @@ class TestomatioPipe {
43
43
  debug('Testomatio Pipe: Enabled');
44
44
  const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
45
45
  const proxy = proxyUrl ? new URL(proxyUrl) : null;
46
+ this.parallel = params.parallel;
46
47
  this.store = store || {};
47
48
  this.title = params.title || process.env.TESTOMATIO_TITLE;
48
49
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
@@ -153,6 +154,7 @@ class TestomatioPipe {
153
154
  const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
154
155
  const runParams = Object.fromEntries(Object.entries({
155
156
  ci_build_url: buildUrl,
157
+ parallel: this.parallel,
156
158
  api_key: this.apiKey.trim(),
157
159
  group_title: this.groupTitle,
158
160
  access_event: accessEvent,
@@ -375,7 +377,7 @@ class TestomatioPipe {
375
377
  const errorMessage = picocolors_1.default.red(`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`);
376
378
  console.warn(`${constants_js_1.APP_PREFIX} ${errorMessage}`);
377
379
  }
378
- const { status } = params;
380
+ const { status, parallel } = params;
379
381
  let status_event;
380
382
  if (status === constants_js_1.STATUS.FINISHED)
381
383
  status_event = 'finish';
@@ -383,6 +385,8 @@ class TestomatioPipe {
383
385
  status_event = 'pass';
384
386
  if (status === constants_js_1.STATUS.FAILED)
385
387
  status_event = 'fail';
388
+ if (parallel)
389
+ status_event += '_parallel';
386
390
  try {
387
391
  if (this.runId && !this.proceed) {
388
392
  await this.client.request({
package/lib/replay.js CHANGED
@@ -238,7 +238,7 @@ class Replay {
238
238
  });
239
239
  }
240
240
  }
241
- await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED);
241
+ await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED, finishParams.parallel || false);
242
242
  const result = {
243
243
  success: true,
244
244
  testsCount: tests.length,
@@ -5,6 +5,7 @@ declare namespace _default {
5
5
  export { setKeyValue as keyValue };
6
6
  export { setLabel as label };
7
7
  export { linkTest };
8
+ export { linkJira };
8
9
  }
9
10
  export default _default;
10
11
  /**
@@ -40,15 +41,21 @@ declare function setKeyValue(keyValue: {
40
41
  [key: string]: string;
41
42
  } | string, value?: string | null): void;
42
43
  /**
43
- * Add label(s) to the test report
44
+ * Add a single label to the test report
44
45
  * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
45
- * @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
46
+ * @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
46
47
  * @returns {void}
47
48
  */
48
- declare function setLabel(key: string, value?: string | string[] | null): void;
49
+ declare function setLabel(key: string, value?: string | null): void;
49
50
  /**
50
51
  * Add link(s) to the test report
51
52
  * @param {...string} testIds - test IDs to link
52
53
  * @returns {void}
53
54
  */
54
55
  declare function linkTest(...testIds: string[]): void;
56
+ /**
57
+ * Add JIRA issue link(s) to the test report
58
+ * @param {...string} jiraIds - JIRA issue IDs to link
59
+ * @returns {void}
60
+ */
61
+ declare function linkJira(...jiraIds: string[]): void;
@@ -50,15 +50,14 @@ function setKeyValue(keyValue, value = null) {
50
50
  index_js_1.services.keyValues.put(keyValue);
51
51
  }
52
52
  /**
53
- * Add label(s) to the test report
53
+ * Add a single label to the test report
54
54
  * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
55
- * @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
55
+ * @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
56
56
  * @returns {void}
57
57
  */
58
58
  function setLabel(key, value = null) {
59
59
  if (Array.isArray(value)) {
60
- value.forEach(val => setLabel(key, val));
61
- return;
60
+ return value.forEach(label => setLabel(key, label));
62
61
  }
63
62
  const labelObject = value !== null && value !== undefined && value !== ''
64
63
  ? { label: `${key}:${value}` }
@@ -74,6 +73,15 @@ function linkTest(...testIds) {
74
73
  const links = testIds.map(testId => ({ test: testId }));
75
74
  index_js_1.services.links.put(links);
76
75
  }
76
+ /**
77
+ * Add JIRA issue link(s) to the test report
78
+ * @param {...string} jiraIds - JIRA issue IDs to link
79
+ * @returns {void}
80
+ */
81
+ function linkJira(...jiraIds) {
82
+ const links = jiraIds.map(jiraId => ({ jira: jiraId }));
83
+ index_js_1.services.links.put(links);
84
+ }
77
85
  module.exports = {
78
86
  artifact: saveArtifact,
79
87
  log: logMessage,
@@ -81,4 +89,5 @@ module.exports = {
81
89
  keyValue: setKeyValue,
82
90
  label: setLabel,
83
91
  linkTest,
92
+ linkJira,
84
93
  };
package/lib/reporter.d.ts CHANGED
@@ -5,7 +5,7 @@ export const artifact: (data: string | {
5
5
  }, context?: any) => void;
6
6
  export const log: (...args: any[]) => void;
7
7
  export const logger: {
8
- "__#14@#originalUserLogger": {
8
+ "__#13@#originalUserLogger": {
9
9
  assert(condition?: boolean, ...data: any[]): void;
10
10
  assert(value: any, message?: string, ...optionalParams: any[]): void;
11
11
  clear(): void;
@@ -50,13 +50,13 @@ export const logger: {
50
50
  profile(label?: string): void;
51
51
  profileEnd(label?: string): void;
52
52
  };
53
- "__#14@#userLoggerWithOverridenMethods": any;
53
+ "__#13@#userLoggerWithOverridenMethods": any;
54
54
  logLevel: string;
55
55
  step(strings: any, ...values: any[]): void;
56
56
  getLogs(context: string): string[];
57
- "__#14@#stringifyLogs"(...args: any[]): string;
57
+ "__#13@#stringifyLogs"(...args: any[]): string;
58
58
  _templateLiteralLog(strings: any, ...args: any[]): void;
59
- "__#14@#logWrapper"(argsArray: any, level: any): void;
59
+ "__#13@#logWrapper"(argsArray: any, level: any): void;
60
60
  assert(...args: any[]): void;
61
61
  debug(...args: any[]): void;
62
62
  error(...args: any[]): void;
@@ -76,11 +76,12 @@ export const meta: (keyValue: {
76
76
  [key: string]: string;
77
77
  } | string, value?: string | null) => void;
78
78
  export const step: (message: string) => void;
79
- export const label: (key: string, value?: string | string[] | null) => void;
79
+ export const label: (key: string, value?: string | null) => void;
80
80
  export const linkTest: (...testIds: string[]) => void;
81
+ export const linkJira: (...jiraIds: string[]) => void;
81
82
  declare namespace _default {
82
83
  let testomatioLogger: {
83
- "__#14@#originalUserLogger": {
84
+ "__#13@#originalUserLogger": {
84
85
  assert(condition?: boolean, ...data: any[]): void;
85
86
  assert(value: any, message?: string, ...optionalParams: any[]): void;
86
87
  clear(): void;
@@ -125,13 +126,13 @@ declare namespace _default {
125
126
  profile(label?: string): void;
126
127
  profileEnd(label?: string): void;
127
128
  };
128
- "__#14@#userLoggerWithOverridenMethods": any;
129
+ "__#13@#userLoggerWithOverridenMethods": any;
129
130
  logLevel: string;
130
131
  step(strings: any, ...values: any[]): void;
131
132
  getLogs(context: string): string[];
132
- "__#14@#stringifyLogs"(...args: any[]): string;
133
+ "__#13@#stringifyLogs"(...args: any[]): string;
133
134
  _templateLiteralLog(strings: any, ...args: any[]): void;
134
- "__#14@#logWrapper"(argsArray: any, level: any): void;
135
+ "__#13@#logWrapper"(argsArray: any, level: any): void;
135
136
  assert(...args: any[]): void;
136
137
  debug(...args: any[]): void;
137
138
  error(...args: any[]): void;
@@ -154,7 +155,7 @@ declare namespace _default {
154
155
  }, context?: any) => void;
155
156
  let log: (...args: any[]) => void;
156
157
  let logger: {
157
- "__#14@#originalUserLogger": {
158
+ "__#13@#originalUserLogger": {
158
159
  assert(condition?: boolean, ...data: any[]): void;
159
160
  assert(value: any, message?: string, ...optionalParams: any[]): void;
160
161
  clear(): void;
@@ -199,13 +200,13 @@ declare namespace _default {
199
200
  profile(label?: string): void;
200
201
  profileEnd(label?: string): void;
201
202
  };
202
- "__#14@#userLoggerWithOverridenMethods": any;
203
+ "__#13@#userLoggerWithOverridenMethods": any;
203
204
  logLevel: string;
204
205
  step(strings: any, ...values: any[]): void;
205
206
  getLogs(context: string): string[];
206
- "__#14@#stringifyLogs"(...args: any[]): string;
207
+ "__#13@#stringifyLogs"(...args: any[]): string;
207
208
  _templateLiteralLog(strings: any, ...args: any[]): void;
208
- "__#14@#logWrapper"(argsArray: any, level: any): void;
209
+ "__#13@#logWrapper"(argsArray: any, level: any): void;
209
210
  assert(...args: any[]): void;
210
211
  debug(...args: any[]): void;
211
212
  error(...args: any[]): void;
@@ -225,8 +226,9 @@ declare namespace _default {
225
226
  [key: string]: string;
226
227
  } | string, value?: string | null) => void;
227
228
  let step: (message: string) => void;
228
- let label: (key: string, value?: string | string[] | null) => void;
229
+ let label: (key: string, value?: string | null) => void;
229
230
  let linkTest: (...testIds: string[]) => void;
231
+ let linkJira: (...jiraIds: string[]) => void;
230
232
  }
231
233
  export default _default;
232
234
  export type ArtifactFunction = typeof import("./reporter-functions.js").default.artifact;
package/lib/reporter.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
6
+ exports.linkJira = exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
7
7
  // import TestomatClient from './client.js';
8
8
  // import * as TRConstants from './constants.js';
9
9
  const index_js_1 = require("./services/index.js");
@@ -15,6 +15,7 @@ exports.meta = reporter_functions_js_1.default.keyValue;
15
15
  exports.step = reporter_functions_js_1.default.step;
16
16
  exports.label = reporter_functions_js_1.default.label;
17
17
  exports.linkTest = reporter_functions_js_1.default.linkTest;
18
+ exports.linkJira = reporter_functions_js_1.default.linkJira;
18
19
  /**
19
20
  * @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
20
21
  * @typedef {typeof import('./reporter-functions.js').default.log} LogFunction
@@ -35,6 +36,7 @@ module.exports = {
35
36
  step: reporter_functions_js_1.default.step,
36
37
  label: reporter_functions_js_1.default.label,
37
38
  linkTest: reporter_functions_js_1.default.linkTest,
39
+ linkJira: reporter_functions_js_1.default.linkJira,
38
40
  // TestomatClient,
39
41
  // TRConstants,
40
42
  };
@@ -3,7 +3,7 @@ export const artifactStorage: ArtifactStorage;
3
3
  * Artifact storage is supposed to store file paths
4
4
  */
5
5
  declare class ArtifactStorage {
6
- static "__#15@#instance": any;
6
+ static "__#14@#instance": any;
7
7
  /**
8
8
  * Singleton
9
9
  * @returns {ArtifactStorage}
@@ -1,6 +1,6 @@
1
1
  export const keyValueStorage: KeyValueStorage;
2
2
  declare class KeyValueStorage {
3
- static "__#16@#instance": any;
3
+ static "__#15@#instance": any;
4
4
  /**
5
5
  *
6
6
  * @returns {KeyValueStorage}
@@ -1,22 +0,0 @@
1
- export const labelStorage: LabelStorage;
2
- declare class LabelStorage {
3
- static "__#19@#instance": any;
4
- /**
5
- *
6
- * @returns {LabelStorage}
7
- */
8
- static getInstance(): LabelStorage;
9
- /**
10
- * Stores labels array and passes it to reporter
11
- * @param {string[]} labels - array of label strings
12
- * @param {*} context - full test title
13
- */
14
- put(labels: string[], context?: any): void;
15
- /**
16
- * Returns labels array for the test
17
- * @param {*} context testId or test context from test runner
18
- * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
19
- */
20
- get(context?: any): string[];
21
- }
22
- export {};
@@ -1,62 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.labelStorage = void 0;
7
- const debug_1 = __importDefault(require("debug"));
8
- const data_storage_js_1 = require("../data-storage.js");
9
- const debug = (0, debug_1.default)('@testomatio/reporter:services-labels');
10
- class LabelStorage {
11
- static #instance;
12
- /**
13
- *
14
- * @returns {LabelStorage}
15
- */
16
- static getInstance() {
17
- if (!this.#instance) {
18
- this.#instance = new LabelStorage();
19
- }
20
- return this.#instance;
21
- }
22
- /**
23
- * Stores labels array and passes it to reporter
24
- * @param {string[]} labels - array of label strings
25
- * @param {*} context - full test title
26
- */
27
- put(labels, context = null) {
28
- if (!labels || !Array.isArray(labels))
29
- return;
30
- data_storage_js_1.dataStorage.putData('links', labels, context);
31
- }
32
- /**
33
- * Returns labels array for the test
34
- * @param {*} context testId or test context from test runner
35
- * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
36
- */
37
- get(context = null) {
38
- const labelsList = data_storage_js_1.dataStorage.getData('links', context);
39
- if (!labelsList || !labelsList?.length)
40
- return [];
41
- const allLabels = [];
42
- for (const labels of labelsList) {
43
- if (Array.isArray(labels)) {
44
- allLabels.push(...labels);
45
- }
46
- else if (typeof labels === 'string') {
47
- try {
48
- const parsedLabels = JSON.parse(labels);
49
- if (Array.isArray(parsedLabels)) {
50
- allLabels.push(...parsedLabels);
51
- }
52
- }
53
- catch (e) {
54
- debug(`Error parsing labels for test ${context}`, labels);
55
- }
56
- }
57
- }
58
- // Remove duplicates
59
- return [...new Set(allLabels)];
60
- }
61
- }
62
- exports.labelStorage = LabelStorage.getInstance();
@@ -1,6 +1,6 @@
1
1
  export const linkStorage: LinkStorage;
2
2
  declare class LinkStorage {
3
- static "__#13@#instance": any;
3
+ static "__#16@#instance": any;
4
4
  /**
5
5
  *
6
6
  * @returns {LinkStorage}
@@ -5,7 +5,7 @@ export const logger: Logger;
5
5
  * Supports different syntaxes to satisfy any user preferences.
6
6
  */
7
7
  declare class Logger {
8
- static "__#14@#instance": any;
8
+ static "__#13@#instance": any;
9
9
  /**
10
10
  *
11
11
  * @returns {Logger}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.0-beta.5-links",
3
+ "version": "2.3.0-beta.6-links",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -25,6 +25,9 @@ const HOOK_EXECUTION_ORDER = {
25
25
  POST_TEST: ['AfterHook', 'AfterSuiteHook']
26
26
  };
27
27
 
28
+ // codeceptjs workers are self-contained
29
+ dataStorage.isFileStorage = false;
30
+
28
31
  const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
29
32
 
30
33
  if (MAJOR_VERSION < 3) {
@@ -464,10 +467,10 @@ function formatHookStep(step) {
464
467
  // For hook steps, construct title from available properties
465
468
  let title = step.name;
466
469
  if (step.actor && step.name) {
467
- title = `${step.actor} ${step.name}`;
470
+ title = `${step.actor}.${step.name}`;
468
471
  if (step.args && step.args.length > 0) {
469
472
  const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
470
- title += ` ${argsStr}`;
473
+ title += `(${argsStr})`;
471
474
  }
472
475
  }
473
476
 
@@ -113,6 +113,7 @@ class CucumberReporter extends Formatter {
113
113
  const logs = services.logger.getLogs(testTitle).join('\n');
114
114
  const artifacts = services.artifacts.get(testTitle);
115
115
  const keyValues = services.keyValues.get(testTitle);
116
+ const links = services.links.get(testTitle);
116
117
 
117
118
  this.client.addTestRun(status, {
118
119
  // error: testCaseAttempt.worstTestStepResult.message,
@@ -123,6 +124,7 @@ class CucumberReporter extends Formatter {
123
124
  .trim(),
124
125
  example: { ...example },
125
126
  logs,
127
+ links,
126
128
  manuallyAttachedArtifacts: artifacts,
127
129
  meta: keyValues,
128
130
  title: scenario,
@@ -59,6 +59,7 @@ export class JestReporter {
59
59
  const logs = getTestLogs(result);
60
60
  const artifacts = services.artifacts.get(result.fullName);
61
61
  const keyValues = services.keyValues.get(result.fullName);
62
+ const links = services.links.get(result.fullName);
62
63
 
63
64
  const deducedStatus = status === 'pending' ? 'skipped' : status;
64
65
  // In jest if test is not matched with test name pattern it is considered as skipped.
@@ -72,6 +73,7 @@ export class JestReporter {
72
73
  title,
73
74
  time: duration,
74
75
  logs,
76
+ links,
75
77
  manuallyAttachedArtifacts: artifacts,
76
78
  meta: keyValues,
77
79
  });
@@ -51,9 +51,6 @@ class PlaywrightReporter {
51
51
  }
52
52
  }
53
53
 
54
- // Extract and normalize tags
55
- const tags = extractTags(test);
56
-
57
54
  const fullTestTitle = getTestContextName(test);
58
55
  let logs = '';
59
56
  if (result.stderr.length || result.stdout.length) {
@@ -61,6 +58,7 @@ class PlaywrightReporter {
61
58
  }
62
59
  const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
63
60
  const testMeta = services.keyValues.get(fullTestTitle);
61
+ const links = services.links.get(fullTestTitle);
64
62
  const rid = test.id || test.testId || uuidv4();
65
63
 
66
64
  /**
@@ -92,13 +90,13 @@ class PlaywrightReporter {
92
90
  const reportTestPromise = this.client.addTestRun(checkStatus(status), {
93
91
  rid: `${rid}-${project.name}`,
94
92
  error,
95
- test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
93
+ test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
96
94
  suite_title,
97
95
  title,
98
- tags,
99
96
  steps: steps.length ? steps : undefined,
100
97
  time: duration,
101
98
  logs,
99
+ links,
102
100
  manuallyAttachedArtifacts,
103
101
  meta: {
104
102
  browser: project.browser,
@@ -245,32 +243,6 @@ function generateTmpFilepath(filename = '') {
245
243
  return path.join(tmpdir, filename);
246
244
  }
247
245
 
248
- /**
249
- * Extracts and normalizes tags from test title, test options, and suite level
250
- * @param {*} test - testInfo object from Playwright
251
- * @returns {string[]} - array of normalized tags
252
- */
253
- function extractTags(test) {
254
- const tagsSet = new Set();
255
-
256
- // Extract tags from test title (@tag format)
257
- const titleTagsMatch = test.title.match(/@\w+/g);
258
- if (titleTagsMatch) {
259
- titleTagsMatch.forEach(tag => {
260
- tagsSet.add(tag.replace('@', '').toLowerCase());
261
- });
262
- }
263
-
264
- // Extract tags from test.tags (Playwright built-in tags)
265
- if (test.tags && Array.isArray(test.tags)) {
266
- test.tags.forEach(tag => {
267
- const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
268
- tagsSet.add(normalizedTag);
269
- });
270
- }
271
- return Array.from(tagsSet);
272
- }
273
-
274
246
  /**
275
247
  * Returns filename + test title
276
248
  * @param {*} test - testInfo object from Playwright
@@ -3,7 +3,6 @@ import TestomatClient from '../client.js';
3
3
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
4
4
  import { services } from '../services/index.js';
5
5
  import { TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
6
- import { stringToMD5Hash } from '../data-storage.js';
7
6
 
8
7
  class WebdriverReporter extends WDIOReporter {
9
8
  constructor(options) {
@@ -53,12 +52,10 @@ class WebdriverReporter extends WDIOReporter {
53
52
  onTestEnd(test) {
54
53
  test.suite = test.parent;
55
54
  const logs = getTestLogs(test.fullTitle);
56
- // still be under investigation
57
- const artifacts = services.artifacts.get(test.fullTitle);
58
- const keyValues = services.keyValues.get(test.fullTitle);
55
+ test.artifacts = services.artifacts.get(test.fullTitle);
56
+ test.meta = services.keyValues.get(test.fullTitle);
57
+ test.links = services.links.get(test.fullTitle);
59
58
  test.logs = logs;
60
- test.artifacts = artifacts;
61
- test.meta = keyValues;
62
59
 
63
60
  this._addTestPromises.push(this.addTest(test));
64
61
  }
@@ -73,7 +70,7 @@ class WebdriverReporter extends WDIOReporter {
73
70
  async addTest(test) {
74
71
  if (!this.client) return;
75
72
 
76
- const { title, _duration: duration, state, error, output } = test;
73
+ const { title, _duration: duration, state, error, output, links, artifacts, meta, logs } = test;
77
74
 
78
75
  const testId = getTestomatIdFromTestTitle(title);
79
76
 
@@ -82,14 +79,13 @@ class WebdriverReporter extends WDIOReporter {
82
79
  .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
83
80
  .map(el => Buffer.from(el.result.value, 'base64'));
84
81
 
85
- const rid = stringToMD5Hash(test.fullTitle);
86
-
87
82
  await this.client.addTestRun(state, {
88
- rid,
89
- manuallyAttachedArtifacts: test.artifacts,
83
+ rid: test.uid || '',
84
+ manuallyAttachedArtifacts: artifacts,
90
85
  error,
91
- logs: test.logs,
92
- meta: test.meta,
86
+ logs,
87
+ meta,
88
+ links,
93
89
  title,
94
90
  test_id: testId,
95
91
  time: duration,
package/src/bin/cli.js CHANGED
@@ -85,7 +85,7 @@ program
85
85
  return process.exit(255);
86
86
  }
87
87
 
88
- const client = new TestomatClient({ apiKey, title });
88
+ const client = new TestomatClient({ apiKey, title, parallel: true });
89
89
 
90
90
  if (opts.filter) {
91
91
  const [pipe, ...optsArray] = opts.filter.split(':');
@@ -105,17 +105,14 @@ program
105
105
 
106
106
  const runTests = async () => {
107
107
  const testCmds = command.split(' ');
108
- const cmd = spawn(testCmds[0], testCmds.slice(1), {
109
- stdio: 'inherit',
110
- env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
111
- });
108
+ const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
112
109
 
113
110
  cmd.on('close', async code => {
114
111
  const emoji = code === 0 ? '🟢' : '🔴';
115
112
  console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
116
113
  if (apiKey) {
117
114
  const status = code === 0 ? 'passed' : 'failed';
118
- await client.updateRunStatus(status);
115
+ await client.updateRunStatus(status, true);
119
116
  }
120
117
  process.exit(code);
121
118
  });
@@ -313,13 +310,13 @@ program
313
310
  const replayService = new Replay({
314
311
  apiKey: config.TESTOMATIO,
315
312
  dryRun: opts.dryRun,
316
- onLog: message => console.log(APP_PREFIX, message),
317
- onError: message => console.error(APP_PREFIX, '⚠️ ', message),
313
+ onLog: (message) => console.log(APP_PREFIX, message),
314
+ onError: (message) => console.error(APP_PREFIX, '⚠️ ', message),
318
315
  onProgress: ({ current, total }) => {
319
316
  if (current % 10 === 0 || current === total) {
320
317
  console.log(APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
321
318
  }
322
- },
319
+ }
323
320
  });
324
321
 
325
322
  const result = await replayService.replay(debugFile);
package/src/client.js CHANGED
@@ -11,7 +11,6 @@ import path, { sep } from 'path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { S3Uploader } from './uploader.js';
13
13
  import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
14
- import { linkStorage } from './services/links.js';
15
14
  import { filesize as prettyBytes } from 'filesize';
16
15
 
17
16
  const debug = createDebugMessages('@testomatio/reporter:client');
@@ -182,44 +181,15 @@ class Client {
182
181
  suite_id,
183
182
  test_id,
184
183
  timestamp,
184
+ links,
185
185
  manuallyAttachedArtifacts,
186
186
  overwrite,
187
- tags,
188
187
  } = testData;
189
188
  let { message = '', meta = {} } = testData;
190
189
 
191
190
  // stringify meta values and limit keys and values length to 255
192
191
  meta = Object.entries(meta)
193
192
  .filter(([, value]) => value !== null && value !== undefined)
194
- .map(([key, value]) => {
195
- try {
196
- if (typeof value === 'object') {
197
- value = JSON.stringify(value);
198
- } else if (typeof value !== 'string') {
199
- try {
200
- value = value.toString();
201
- } catch (err) {
202
- console.warn(APP_PREFIX, `Can't convert meta value to string`, err);
203
- }
204
- }
205
-
206
- if (value?.length > 255) {
207
- value = value.substring(0, 255);
208
- debug(APP_PREFIX, `Meta info value "${value}" is too long, trimmed to 255 characters`);
209
- }
210
-
211
- if (key?.length > 255) {
212
- const newKey = key.substring(0, 255);
213
- debug(APP_PREFIX, `Meta info key "${key}" is too long, trimmed to 255 characters`);
214
- return [newKey, value];
215
- }
216
-
217
- return [key, value];
218
- } catch (err) {
219
- debug(APP_PREFIX, `Error while processing meta info key ${key}`, err);
220
- return [null, null];
221
- }
222
- })
223
193
  .reduce((acc, [key, value]) => {
224
194
  if (key) acc[key] = value;
225
195
  return acc;
@@ -227,7 +197,6 @@ class Client {
227
197
 
228
198
  // Get links from storage using the test context
229
199
  const testContext = suite_title ? `${suite_title} ${title}` : title;
230
- const links = linkStorage.get(testContext) || [];
231
200
 
232
201
  let errorFormatted = '';
233
202
  if (error) {
@@ -285,7 +254,6 @@ class Client {
285
254
  meta,
286
255
  links,
287
256
  overwrite,
288
- tags,
289
257
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
290
258
  };
291
259
 
@@ -314,9 +282,10 @@ class Client {
314
282
  * Updates the status of the current test run and finishes the run.
315
283
  * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
316
284
  * Must be one of "passed", "failed", or "finished"
285
+ * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
317
286
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
318
287
  */
319
- async updateRunStatus(status) {
288
+ async updateRunStatus(status, isParallel = false) {
320
289
  this.pipes ||= await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
321
290
  this.runId ||= readLatestRunId();
322
291
 
@@ -324,7 +293,7 @@ class Client {
324
293
  // all pipes disabled, skipping
325
294
  if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
326
295
 
327
- const runParams = { status };
296
+ const runParams = { status, parallel: isParallel };
328
297
 
329
298
  this.queue = this.queue
330
299
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
@@ -116,7 +116,7 @@ class DataStorage {
116
116
  try {
117
117
  if (global?.testomatioDataStore[dataType]) {
118
118
  const testData = global.testomatioDataStore[dataType][context];
119
- if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
119
+ if (testData) debug('<=', dataType, 'global', context, testData);
120
120
  return testData || [];
121
121
  }
122
122
  // debug(`No ${this.dataType} data for context ${context} in <global> storage`);
@@ -137,7 +137,7 @@ class DataStorage {
137
137
  const filepath = join(dataDirPath, `${dataType}_${context}`);
138
138
  if (fs.existsSync(filepath)) {
139
139
  const testDataAsText = fs.readFileSync(filepath, 'utf-8');
140
- if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
140
+ if (testDataAsText) debug('<=', dataType, 'file', context, testDataAsText);
141
141
  const testDataArr = testDataAsText?.split(os.EOL) || [];
142
142
  return testDataArr;
143
143
  }
@@ -156,7 +156,7 @@ class DataStorage {
156
156
  * @param {*} context
157
157
  */
158
158
  #putDataToGlobalVar(dataType, data, context) {
159
- debug('Saving data to global variable for ', context, ':', data);
159
+ debug('=>', dataType, 'global', context, data);
160
160
  if (!global.testomatioDataStore) global.testomatioDataStore = {};
161
161
  if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
162
162
 
@@ -177,7 +177,7 @@ class DataStorage {
177
177
  const filename = `${dataType}_${context}`;
178
178
  const filepath = join(dataDirPath, filename);
179
179
  if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
180
- debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
180
+ debug('=>', dataType, 'file', context, data);
181
181
 
182
182
  // append new line if file already exists (in this case its definitely includes some data)
183
183
  if (fs.existsSync(filepath)) {
@@ -192,8 +192,7 @@ function stringToMD5Hash(str) {
192
192
  const md5 = crypto.createHash('md5');
193
193
  md5.update(str);
194
194
  const hash = md5.digest('hex');
195
-
196
- return hash;
195
+ return `${process.env.runId || 'run'}_${hash}`;
197
196
  }
198
197
 
199
198
  export const dataStorage = DataStorage.getInstance();
@@ -43,6 +43,7 @@ class TestomatioPipe {
43
43
  const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
44
44
  const proxy = proxyUrl ? new URL(proxyUrl) : null;
45
45
 
46
+ this.parallel = params.parallel;
46
47
  this.store = store || {};
47
48
  this.title = params.title || process.env.TESTOMATIO_TITLE;
48
49
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
@@ -166,6 +167,7 @@ class TestomatioPipe {
166
167
  const runParams = Object.fromEntries(
167
168
  Object.entries({
168
169
  ci_build_url: buildUrl,
170
+ parallel: this.parallel,
169
171
  api_key: this.apiKey.trim(),
170
172
  group_title: this.groupTitle,
171
173
  access_event: accessEvent,
@@ -417,13 +419,14 @@ class TestomatioPipe {
417
419
  console.warn(`${APP_PREFIX} ${errorMessage}`);
418
420
  }
419
421
 
420
- const { status } = params;
422
+ const { status, parallel } = params;
421
423
 
422
424
  let status_event;
423
425
 
424
426
  if (status === STATUS.FINISHED) status_event = 'finish';
425
427
  if (status === STATUS.PASSED) status_event = 'pass';
426
428
  if (status === STATUS.FAILED) status_event = 'fail';
429
+ if (parallel) status_event += '_parallel';
427
430
 
428
431
  try {
429
432
  if (this.runId && !this.proceed) {
package/src/replay.js CHANGED
@@ -246,7 +246,7 @@ export class Replay {
246
246
  }
247
247
  }
248
248
 
249
- await client.updateRunStatus(finishParams.status || STATUS.FINISHED);
249
+ await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
250
250
 
251
251
  const result = {
252
252
  success: true,
@@ -53,24 +53,21 @@ function setKeyValue(keyValue, value = null) {
53
53
  }
54
54
 
55
55
  /**
56
- * Add label(s) to the test report
56
+ * Add a single label to the test report
57
57
  * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
58
- * @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
58
+ * @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
59
59
  * @returns {void}
60
60
  */
61
61
  function setLabel(key, value = null) {
62
62
  if (Array.isArray(value)) {
63
- value.forEach(val => setLabel(key, val));
64
- return;
63
+ return value.forEach(label => setLabel(key, label));
65
64
  }
66
-
67
- const labelObject = value !== null && value !== undefined && value !== ''
68
- ? { label: `${key}:${value}` }
65
+ const labelObject = value !== null && value !== undefined && value !== ''
66
+ ? { label: `${key}:${value}` }
69
67
  : { label: key };
70
68
  services.links.put([labelObject]);
71
69
  }
72
70
 
73
-
74
71
  /**
75
72
  * Add link(s) to the test report
76
73
  * @param {...string} testIds - test IDs to link
@@ -81,6 +78,16 @@ function linkTest(...testIds) {
81
78
  services.links.put(links);
82
79
  }
83
80
 
81
+ /**
82
+ * Add JIRA issue link(s) to the test report
83
+ * @param {...string} jiraIds - JIRA issue IDs to link
84
+ * @returns {void}
85
+ */
86
+ function linkJira(...jiraIds) {
87
+ const links = jiraIds.map(jiraId => ({ jira: jiraId }));
88
+ services.links.put(links);
89
+ }
90
+
84
91
  export default {
85
92
  artifact: saveArtifact,
86
93
  log: logMessage,
@@ -88,4 +95,5 @@ export default {
88
95
  keyValue: setKeyValue,
89
96
  label: setLabel,
90
97
  linkTest,
98
+ linkJira,
91
99
  };
package/src/reporter.js CHANGED
@@ -10,6 +10,7 @@ export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
11
  export const label = reporterFunctions.label;
12
12
  export const linkTest = reporterFunctions.linkTest;
13
+ export const linkJira = reporterFunctions.linkJira;
13
14
 
14
15
  /**
15
16
  * @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
@@ -32,6 +33,7 @@ export default {
32
33
  step: reporterFunctions.step,
33
34
  label: reporterFunctions.label,
34
35
  linkTest: reporterFunctions.linkTest,
36
+ linkJira: reporterFunctions.linkJira,
35
37
 
36
38
  // TestomatClient,
37
39
  // TRConstants,
@@ -1,59 +1 @@
1
- import createDebugMessages from 'debug';
2
- import { dataStorage } from '../data-storage.js';
3
1
 
4
- const debug = createDebugMessages('@testomatio/reporter:services-labels');
5
- class LabelStorage {
6
- static #instance;
7
-
8
- /**
9
- *
10
- * @returns {LabelStorage}
11
- */
12
- static getInstance() {
13
- if (!this.#instance) {
14
- this.#instance = new LabelStorage();
15
- }
16
- return this.#instance;
17
- }
18
-
19
- /**
20
- * Stores labels array and passes it to reporter
21
- * @param {string[]} labels - array of label strings
22
- * @param {*} context - full test title
23
- */
24
- put(labels, context = null) {
25
- if (!labels || !Array.isArray(labels)) return;
26
- dataStorage.putData('links', labels, context);
27
- }
28
-
29
- /**
30
- * Returns labels array for the test
31
- * @param {*} context testId or test context from test runner
32
- * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
33
- */
34
- get(context = null) {
35
- const labelsList = dataStorage.getData('links', context);
36
- if (!labelsList || !labelsList?.length) return [];
37
-
38
- const allLabels = [];
39
- for (const labels of labelsList) {
40
- if (Array.isArray(labels)) {
41
- allLabels.push(...labels);
42
- } else if (typeof labels === 'string') {
43
- try {
44
- const parsedLabels = JSON.parse(labels);
45
- if (Array.isArray(parsedLabels)) {
46
- allLabels.push(...parsedLabels);
47
- }
48
- } catch (e) {
49
- debug(`Error parsing labels for test ${context}`, labels);
50
- }
51
- }
52
- }
53
-
54
- // Remove duplicates
55
- return [...new Set(allLabels)];
56
- }
57
- }
58
-
59
- export const labelStorage = LabelStorage.getInstance();