@testomatio/reporter 2.3.0-beta.6-links → 2.3.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.
@@ -401,10 +401,10 @@ function formatHookStep(step) {
401
401
  // For hook steps, construct title from available properties
402
402
  let title = step.name;
403
403
  if (step.actor && step.name) {
404
- title = `${step.actor}.${step.name}`;
404
+ title = `${step.actor} ${step.name}`;
405
405
  if (step.args && step.args.length > 0) {
406
406
  const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
407
- title += `(${argsStr})`;
407
+ title += ` ${argsStr}`;
408
408
  }
409
409
  }
410
410
  return {
@@ -48,6 +48,8 @@ class PlaywrightReporter {
48
48
  steps.push(appendedStep);
49
49
  }
50
50
  }
51
+ // Extract and normalize tags
52
+ const tags = extractTags(test);
51
53
  const fullTestTitle = getTestContextName(test);
52
54
  let logs = '';
53
55
  if (result.stderr.length || result.stdout.length) {
@@ -86,9 +88,10 @@ class PlaywrightReporter {
86
88
  const reportTestPromise = this.client.addTestRun(checkStatus(status), {
87
89
  rid: `${rid}-${project.name}`,
88
90
  error,
89
- test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${test.tags?.join(' ')}`),
91
+ test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
90
92
  suite_title,
91
93
  title,
94
+ tags,
92
95
  steps: steps.length ? steps : undefined,
93
96
  time: duration,
94
97
  logs,
@@ -218,6 +221,29 @@ function generateTmpFilepath(filename = '') {
218
221
  const tmpdir = os_1.default.tmpdir();
219
222
  return path_1.default.join(tmpdir, filename);
220
223
  }
224
+ /**
225
+ * Extracts and normalizes tags from test title, test options, and suite level
226
+ * @param {*} test - testInfo object from Playwright
227
+ * @returns {string[]} - array of normalized tags
228
+ */
229
+ function extractTags(test) {
230
+ const tagsSet = new Set();
231
+ // Extract tags from test title (@tag format)
232
+ const titleTagsMatch = test.title.match(/@\w+/g);
233
+ if (titleTagsMatch) {
234
+ titleTagsMatch.forEach(tag => {
235
+ tagsSet.add(tag.replace('@', '').toLowerCase());
236
+ });
237
+ }
238
+ // Extract tags from test.tags (Playwright built-in tags)
239
+ if (test.tags && Array.isArray(test.tags)) {
240
+ test.tags.forEach(tag => {
241
+ const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
242
+ tagsSet.add(normalizedTag);
243
+ });
244
+ }
245
+ return Array.from(tagsSet);
246
+ }
221
247
  /**
222
248
  * Returns filename + test title
223
249
  * @param {*} test - testInfo object from Playwright
@@ -41,6 +41,7 @@ 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");
44
45
  class WebdriverReporter extends reporter_1.default {
45
46
  constructor(options) {
46
47
  super(options);
@@ -98,9 +99,10 @@ class WebdriverReporter extends reporter_1.default {
98
99
  const screenshotsBuffers = output
99
100
  .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
100
101
  .map(el => Buffer.from(el.result.value, 'base64'));
102
+ const rid = (0, data_storage_js_1.stringToMD5Hash)(test.fullTitle);
101
103
  await this.client.addTestRun(state, {
102
- rid: test.uid || '',
103
- manuallyAttachedArtifacts: artifacts,
104
+ rid,
105
+ manuallyAttachedArtifacts: test.artifacts,
104
106
  error,
105
107
  logs,
106
108
  meta,
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, parallel: true });
81
+ const client = new client_js_1.default({ apiKey, title });
82
82
  if (opts.filter) {
83
83
  const [pipe, ...optsArray] = opts.filter.split(':');
84
84
  const pipeOptions = optsArray.join(':');
@@ -95,13 +95,16 @@ 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), { stdio: 'inherit' });
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
+ });
99
102
  cmd.on('close', async (code) => {
100
103
  const emoji = code === 0 ? '🟢' : '🔴';
101
104
  console.log(constants_js_1.APP_PREFIX, emoji, `Runner exited with ${picocolors_1.default.bold(code)}`);
102
105
  if (apiKey) {
103
106
  const status = code === 0 ? 'passed' : 'failed';
104
- await client.updateRunStatus(status, true);
107
+ await client.updateRunStatus(status);
105
108
  }
106
109
  process.exit(code);
107
110
  });
@@ -257,13 +260,13 @@ program
257
260
  const replayService = new replay_js_1.default({
258
261
  apiKey: config_js_1.config.TESTOMATIO,
259
262
  dryRun: opts.dryRun,
260
- onLog: (message) => console.log(constants_js_1.APP_PREFIX, message),
261
- onError: (message) => console.error(constants_js_1.APP_PREFIX, '⚠️ ', message),
263
+ onLog: message => console.log(constants_js_1.APP_PREFIX, message),
264
+ onError: message => console.error(constants_js_1.APP_PREFIX, '⚠️ ', message),
262
265
  onProgress: ({ current, total }) => {
263
266
  if (current % 10 === 0 || current === total) {
264
267
  console.log(constants_js_1.APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
265
268
  }
266
- }
269
+ },
267
270
  });
268
271
  const result = await replayService.replay(debugFile);
269
272
  if (result.dryRun) {
package/lib/client.d.ts CHANGED
@@ -58,10 +58,9 @@ 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.
62
61
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
63
62
  */
64
- updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", isParallel?: boolean): Promise<any>;
63
+ updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
65
64
  /**
66
65
  * Returns the formatted stack including the stack trace, steps, and logs.
67
66
  * @returns {string}
package/lib/client.js CHANGED
@@ -181,7 +181,7 @@ class Client {
181
181
  /**
182
182
  * @type {TestData}
183
183
  */
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;
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, tags, } = testData;
185
185
  let { message = '', meta = {} } = testData;
186
186
  // stringify meta values and limit keys and values length to 255
187
187
  meta = Object.entries(meta)
@@ -242,6 +242,7 @@ class Client {
242
242
  meta,
243
243
  links,
244
244
  overwrite,
245
+ tags,
245
246
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
246
247
  };
247
248
  // debug('Adding test run...', data);
@@ -263,17 +264,16 @@ class Client {
263
264
  * Updates the status of the current test run and finishes the run.
264
265
  * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
265
266
  * Must be one of "passed", "failed", or "finished"
266
- * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
267
267
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
268
268
  */
269
- async updateRunStatus(status, isParallel = false) {
269
+ async updateRunStatus(status) {
270
270
  this.pipes ||= await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
271
271
  this.runId ||= (0, utils_js_1.readLatestRunId)();
272
272
  debug('Updating run status...');
273
273
  // all pipes disabled, skipping
274
274
  if (!this.pipes?.filter(p => p.isEnabled).length)
275
275
  return Promise.resolve();
276
- const runParams = { status, parallel: isParallel };
276
+ const runParams = { status };
277
277
  this.queue = this.queue
278
278
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
279
279
  .then(() => {
@@ -23,7 +23,6 @@ declare class TestomatioPipe implements Pipe {
23
23
  isEnabled: boolean;
24
24
  url: any;
25
25
  apiKey: any;
26
- parallel: any;
27
26
  store: any;
28
27
  title: any;
29
28
  sharedRun: boolean;
@@ -43,7 +43,6 @@ 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;
47
46
  this.store = store || {};
48
47
  this.title = params.title || process.env.TESTOMATIO_TITLE;
49
48
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
@@ -154,7 +153,6 @@ class TestomatioPipe {
154
153
  const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
155
154
  const runParams = Object.fromEntries(Object.entries({
156
155
  ci_build_url: buildUrl,
157
- parallel: this.parallel,
158
156
  api_key: this.apiKey.trim(),
159
157
  group_title: this.groupTitle,
160
158
  access_event: accessEvent,
@@ -377,7 +375,7 @@ class TestomatioPipe {
377
375
  const errorMessage = picocolors_1.default.red(`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`);
378
376
  console.warn(`${constants_js_1.APP_PREFIX} ${errorMessage}`);
379
377
  }
380
- const { status, parallel } = params;
378
+ const { status } = params;
381
379
  let status_event;
382
380
  if (status === constants_js_1.STATUS.FINISHED)
383
381
  status_event = 'finish';
@@ -385,8 +383,6 @@ class TestomatioPipe {
385
383
  status_event = 'pass';
386
384
  if (status === constants_js_1.STATUS.FAILED)
387
385
  status_event = 'fail';
388
- if (parallel)
389
- status_event += '_parallel';
390
386
  try {
391
387
  if (this.runId && !this.proceed) {
392
388
  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, finishParams.parallel || false);
241
+ await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED);
242
242
  const result = {
243
243
  success: true,
244
244
  testsCount: tests.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.0-beta.6-links",
3
+ "version": "2.3.0",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -467,10 +467,10 @@ function formatHookStep(step) {
467
467
  // For hook steps, construct title from available properties
468
468
  let title = step.name;
469
469
  if (step.actor && step.name) {
470
- title = `${step.actor}.${step.name}`;
470
+ title = `${step.actor} ${step.name}`;
471
471
  if (step.args && step.args.length > 0) {
472
472
  const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
473
- title += `(${argsStr})`;
473
+ title += ` ${argsStr}`;
474
474
  }
475
475
  }
476
476
 
@@ -51,6 +51,9 @@ class PlaywrightReporter {
51
51
  }
52
52
  }
53
53
 
54
+ // Extract and normalize tags
55
+ const tags = extractTags(test);
56
+
54
57
  const fullTestTitle = getTestContextName(test);
55
58
  let logs = '';
56
59
  if (result.stderr.length || result.stdout.length) {
@@ -90,9 +93,10 @@ class PlaywrightReporter {
90
93
  const reportTestPromise = this.client.addTestRun(checkStatus(status), {
91
94
  rid: `${rid}-${project.name}`,
92
95
  error,
93
- test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
96
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
94
97
  suite_title,
95
98
  title,
99
+ tags,
96
100
  steps: steps.length ? steps : undefined,
97
101
  time: duration,
98
102
  logs,
@@ -243,6 +247,32 @@ function generateTmpFilepath(filename = '') {
243
247
  return path.join(tmpdir, filename);
244
248
  }
245
249
 
250
+ /**
251
+ * Extracts and normalizes tags from test title, test options, and suite level
252
+ * @param {*} test - testInfo object from Playwright
253
+ * @returns {string[]} - array of normalized tags
254
+ */
255
+ function extractTags(test) {
256
+ const tagsSet = new Set();
257
+
258
+ // Extract tags from test title (@tag format)
259
+ const titleTagsMatch = test.title.match(/@\w+/g);
260
+ if (titleTagsMatch) {
261
+ titleTagsMatch.forEach(tag => {
262
+ tagsSet.add(tag.replace('@', '').toLowerCase());
263
+ });
264
+ }
265
+
266
+ // Extract tags from test.tags (Playwright built-in tags)
267
+ if (test.tags && Array.isArray(test.tags)) {
268
+ test.tags.forEach(tag => {
269
+ const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
270
+ tagsSet.add(normalizedTag);
271
+ });
272
+ }
273
+ return Array.from(tagsSet);
274
+ }
275
+
246
276
  /**
247
277
  * Returns filename + test title
248
278
  * @param {*} test - testInfo object from Playwright
@@ -3,6 +3,7 @@ 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';
6
7
 
7
8
  class WebdriverReporter extends WDIOReporter {
8
9
  constructor(options) {
@@ -52,6 +53,7 @@ class WebdriverReporter extends WDIOReporter {
52
53
  onTestEnd(test) {
53
54
  test.suite = test.parent;
54
55
  const logs = getTestLogs(test.fullTitle);
56
+
55
57
  test.artifacts = services.artifacts.get(test.fullTitle);
56
58
  test.meta = services.keyValues.get(test.fullTitle);
57
59
  test.links = services.links.get(test.fullTitle);
@@ -79,9 +81,11 @@ class WebdriverReporter extends WDIOReporter {
79
81
  .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
80
82
  .map(el => Buffer.from(el.result.value, 'base64'));
81
83
 
84
+ const rid = stringToMD5Hash(test.fullTitle);
85
+
82
86
  await this.client.addTestRun(state, {
83
- rid: test.uid || '',
84
- manuallyAttachedArtifacts: artifacts,
87
+ rid,
88
+ manuallyAttachedArtifacts: test.artifacts,
85
89
  error,
86
90
  logs,
87
91
  meta,
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, parallel: true });
88
+ const client = new TestomatClient({ apiKey, title });
89
89
 
90
90
  if (opts.filter) {
91
91
  const [pipe, ...optsArray] = opts.filter.split(':');
@@ -105,14 +105,17 @@ program
105
105
 
106
106
  const runTests = async () => {
107
107
  const testCmds = command.split(' ');
108
- const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
108
+ const cmd = spawn(testCmds[0], testCmds.slice(1), {
109
+ stdio: 'inherit',
110
+ env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
111
+ });
109
112
 
110
113
  cmd.on('close', async code => {
111
114
  const emoji = code === 0 ? '🟢' : '🔴';
112
115
  console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
113
116
  if (apiKey) {
114
117
  const status = code === 0 ? 'passed' : 'failed';
115
- await client.updateRunStatus(status, true);
118
+ await client.updateRunStatus(status);
116
119
  }
117
120
  process.exit(code);
118
121
  });
@@ -310,13 +313,13 @@ program
310
313
  const replayService = new Replay({
311
314
  apiKey: config.TESTOMATIO,
312
315
  dryRun: opts.dryRun,
313
- onLog: (message) => console.log(APP_PREFIX, message),
314
- onError: (message) => console.error(APP_PREFIX, '⚠️ ', message),
316
+ onLog: message => console.log(APP_PREFIX, message),
317
+ onError: message => console.error(APP_PREFIX, '⚠️ ', message),
315
318
  onProgress: ({ current, total }) => {
316
319
  if (current % 10 === 0 || current === total) {
317
320
  console.log(APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
318
321
  }
319
- }
322
+ },
320
323
  });
321
324
 
322
325
  const result = await replayService.replay(debugFile);
package/src/client.js CHANGED
@@ -184,6 +184,7 @@ class Client {
184
184
  links,
185
185
  manuallyAttachedArtifacts,
186
186
  overwrite,
187
+ tags,
187
188
  } = testData;
188
189
  let { message = '', meta = {} } = testData;
189
190
 
@@ -254,6 +255,7 @@ class Client {
254
255
  meta,
255
256
  links,
256
257
  overwrite,
258
+ tags,
257
259
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
258
260
  };
259
261
 
@@ -282,10 +284,9 @@ class Client {
282
284
  * Updates the status of the current test run and finishes the run.
283
285
  * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
284
286
  * Must be one of "passed", "failed", or "finished"
285
- * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
286
287
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
287
288
  */
288
- async updateRunStatus(status, isParallel = false) {
289
+ async updateRunStatus(status) {
289
290
  this.pipes ||= await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
290
291
  this.runId ||= readLatestRunId();
291
292
 
@@ -293,7 +294,7 @@ class Client {
293
294
  // all pipes disabled, skipping
294
295
  if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
295
296
 
296
- const runParams = { status, parallel: isParallel };
297
+ const runParams = { status };
297
298
 
298
299
  this.queue = this.queue
299
300
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
@@ -43,7 +43,6 @@ 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;
47
46
  this.store = store || {};
48
47
  this.title = params.title || process.env.TESTOMATIO_TITLE;
49
48
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
@@ -167,7 +166,6 @@ class TestomatioPipe {
167
166
  const runParams = Object.fromEntries(
168
167
  Object.entries({
169
168
  ci_build_url: buildUrl,
170
- parallel: this.parallel,
171
169
  api_key: this.apiKey.trim(),
172
170
  group_title: this.groupTitle,
173
171
  access_event: accessEvent,
@@ -419,14 +417,13 @@ class TestomatioPipe {
419
417
  console.warn(`${APP_PREFIX} ${errorMessage}`);
420
418
  }
421
419
 
422
- const { status, parallel } = params;
420
+ const { status } = params;
423
421
 
424
422
  let status_event;
425
423
 
426
424
  if (status === STATUS.FINISHED) status_event = 'finish';
427
425
  if (status === STATUS.PASSED) status_event = 'pass';
428
426
  if (status === STATUS.FAILED) status_event = 'fail';
429
- if (parallel) status_event += '_parallel';
430
427
 
431
428
  try {
432
429
  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, finishParams.parallel || false);
249
+ await client.updateRunStatus(finishParams.status || STATUS.FINISHED);
250
250
 
251
251
  const result = {
252
252
  success: true,