@testomatio/reporter 2.3.0-beta.6-links → 2.3.1-beta.1-dependency

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(() => {
@@ -1,6 +1,6 @@
1
1
  export const dataStorage: DataStorage;
2
2
  declare class DataStorage {
3
- static "__#12@#instance": any;
3
+ static #instance: any;
4
4
  /**
5
5
  *
6
6
  * @returns {DataStorage}
package/lib/pipe/debug.js CHANGED
@@ -18,7 +18,7 @@ class DebugPipe {
18
18
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
19
19
  if (this.isEnabled) {
20
20
  this.batch = {
21
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
21
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
22
22
  intervalFunction: null,
23
23
  intervalTime: 5000,
24
24
  tests: [],
@@ -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;
@@ -23,7 +23,7 @@ if (process.env.TESTOMATIO_RUN)
23
23
  class TestomatioPipe {
24
24
  constructor(params, store) {
25
25
  this.batch = {
26
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
26
+ isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
27
27
  intervalFunction: null, // will be created in createRun by setInterval function
28
28
  intervalTime: 5000, // how often tests are sent
29
29
  tests: [], // array of tests in batch
@@ -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;
@@ -61,7 +60,7 @@ class TestomatioPipe {
61
60
  retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
61
  retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
63
62
  httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
- shouldRetry: (error) => {
63
+ shouldRetry: error => {
65
64
  if (!error.response)
66
65
  return false;
67
66
  switch (error.response?.status) {
@@ -74,8 +73,8 @@ class TestomatioPipe {
74
73
  break;
75
74
  }
76
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
77
- }
78
- }
76
+ },
77
+ },
79
78
  });
80
79
  this.isEnabled = true;
81
80
  // do not finish this run (for parallel testing)
@@ -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,
@@ -173,7 +171,7 @@ class TestomatioPipe {
173
171
  method: 'PUT',
174
172
  url: `/api/reporter/${this.runId}`,
175
173
  data: runParams,
176
- responseType: 'json'
174
+ responseType: 'json',
177
175
  });
178
176
  if (resp.data.artifacts)
179
177
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
@@ -186,7 +184,7 @@ class TestomatioPipe {
186
184
  url: '/api/reporter',
187
185
  data: runParams,
188
186
  maxContentLength: Infinity,
189
- responseType: 'json'
187
+ responseType: 'json',
190
188
  });
191
189
  this.runId = resp.data.uid;
192
190
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -243,15 +241,17 @@ class TestomatioPipe {
243
241
  }
244
242
  const json = json_cycle_1.default.stringify(data);
245
243
  debug('Adding test', json);
246
- return this.client.request({
244
+ return this.client
245
+ .request({
247
246
  method: 'POST',
248
247
  url: `/api/reporter/${this.runId}/testrun`,
249
248
  data: json,
250
249
  headers: {
251
250
  'Content-Type': 'application/json',
252
251
  },
253
- maxContentLength: Infinity
254
- }).catch(err => {
252
+ maxContentLength: Infinity,
253
+ })
254
+ .catch(err => {
255
255
  this.requestFailures++;
256
256
  this.notReportedTestsCount++;
257
257
  if (err.response) {
@@ -296,19 +296,21 @@ class TestomatioPipe {
296
296
  // get tests from batch and clear batch
297
297
  const testsToSend = this.batch.tests.splice(0);
298
298
  debug('📨 Batch upload', testsToSend.length, 'tests');
299
- return this.client.request({
299
+ return this.client
300
+ .request({
300
301
  method: 'POST',
301
302
  url: `/api/reporter/${this.runId}/testrun`,
302
303
  data: {
303
304
  api_key: this.apiKey,
304
305
  tests: testsToSend,
305
- batch_index: this.batch.batchIndex
306
+ batch_index: this.batch.batchIndex,
306
307
  },
307
308
  headers: {
308
309
  'Content-Type': 'application/json',
309
310
  },
310
- maxContentLength: Infinity
311
- }).catch(err => {
311
+ maxContentLength: Infinity,
312
+ })
313
+ .catch(err => {
312
314
  this.requestFailures++;
313
315
  this.notReportedTestsCount += testsToSend.length;
314
316
  if (err.response) {
@@ -377,7 +379,7 @@ class TestomatioPipe {
377
379
  const errorMessage = picocolors_1.default.red(`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`);
378
380
  console.warn(`${constants_js_1.APP_PREFIX} ${errorMessage}`);
379
381
  }
380
- const { status, parallel } = params;
382
+ const { status } = params;
381
383
  let status_event;
382
384
  if (status === constants_js_1.STATUS.FINISHED)
383
385
  status_event = 'finish';
@@ -385,8 +387,6 @@ class TestomatioPipe {
385
387
  status_event = 'pass';
386
388
  if (status === constants_js_1.STATUS.FAILED)
387
389
  status_event = 'fail';
388
- if (parallel)
389
- status_event += '_parallel';
390
390
  try {
391
391
  if (this.runId && !this.proceed) {
392
392
  await this.client.request({
@@ -398,7 +398,7 @@ class TestomatioPipe {
398
398
  status_event,
399
399
  detach: params.detach,
400
400
  tests: params.tests,
401
- }
401
+ },
402
402
  });
403
403
  if (this.runUrl) {
404
404
  console.log(constants_js_1.APP_PREFIX, '📊 Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
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/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
- "__#13@#originalUserLogger": {
8
+ #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
- "__#13@#userLoggerWithOverridenMethods": any;
53
+ #userLoggerWithOverridenMethods: any;
54
54
  logLevel: string;
55
55
  step(strings: any, ...values: any[]): void;
56
56
  getLogs(context: string): string[];
57
- "__#13@#stringifyLogs"(...args: any[]): string;
57
+ #stringifyLogs(...args: any[]): string;
58
58
  _templateLiteralLog(strings: any, ...args: any[]): void;
59
- "__#13@#logWrapper"(argsArray: any, level: any): void;
59
+ #logWrapper(argsArray: any, level: any): void;
60
60
  assert(...args: any[]): void;
61
61
  debug(...args: any[]): void;
62
62
  error(...args: any[]): void;
@@ -81,7 +81,7 @@ export const linkTest: (...testIds: string[]) => void;
81
81
  export const linkJira: (...jiraIds: string[]) => void;
82
82
  declare namespace _default {
83
83
  let testomatioLogger: {
84
- "__#13@#originalUserLogger": {
84
+ #originalUserLogger: {
85
85
  assert(condition?: boolean, ...data: any[]): void;
86
86
  assert(value: any, message?: string, ...optionalParams: any[]): void;
87
87
  clear(): void;
@@ -126,13 +126,13 @@ declare namespace _default {
126
126
  profile(label?: string): void;
127
127
  profileEnd(label?: string): void;
128
128
  };
129
- "__#13@#userLoggerWithOverridenMethods": any;
129
+ #userLoggerWithOverridenMethods: any;
130
130
  logLevel: string;
131
131
  step(strings: any, ...values: any[]): void;
132
132
  getLogs(context: string): string[];
133
- "__#13@#stringifyLogs"(...args: any[]): string;
133
+ #stringifyLogs(...args: any[]): string;
134
134
  _templateLiteralLog(strings: any, ...args: any[]): void;
135
- "__#13@#logWrapper"(argsArray: any, level: any): void;
135
+ #logWrapper(argsArray: any, level: any): void;
136
136
  assert(...args: any[]): void;
137
137
  debug(...args: any[]): void;
138
138
  error(...args: any[]): void;
@@ -155,7 +155,7 @@ declare namespace _default {
155
155
  }, context?: any) => void;
156
156
  let log: (...args: any[]) => void;
157
157
  let logger: {
158
- "__#13@#originalUserLogger": {
158
+ #originalUserLogger: {
159
159
  assert(condition?: boolean, ...data: any[]): void;
160
160
  assert(value: any, message?: string, ...optionalParams: any[]): void;
161
161
  clear(): void;
@@ -200,13 +200,13 @@ declare namespace _default {
200
200
  profile(label?: string): void;
201
201
  profileEnd(label?: string): void;
202
202
  };
203
- "__#13@#userLoggerWithOverridenMethods": any;
203
+ #userLoggerWithOverridenMethods: any;
204
204
  logLevel: string;
205
205
  step(strings: any, ...values: any[]): void;
206
206
  getLogs(context: string): string[];
207
- "__#13@#stringifyLogs"(...args: any[]): string;
207
+ #stringifyLogs(...args: any[]): string;
208
208
  _templateLiteralLog(strings: any, ...args: any[]): void;
209
- "__#13@#logWrapper"(argsArray: any, level: any): void;
209
+ #logWrapper(argsArray: any, level: any): void;
210
210
  assert(...args: any[]): void;
211
211
  debug(...args: any[]): void;
212
212
  error(...args: any[]): void;
@@ -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 "__#14@#instance": any;
6
+ static #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 "__#15@#instance": any;
3
+ static #instance: any;
4
4
  /**
5
5
  *
6
6
  * @returns {KeyValueStorage}
@@ -1,6 +1,6 @@
1
1
  export const linkStorage: LinkStorage;
2
2
  declare class LinkStorage {
3
- static "__#16@#instance": any;
3
+ static #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 "__#13@#instance": any;
8
+ static #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.6-links",
3
+ "version": "2.3.1-beta.1-dependency",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -10,13 +10,20 @@
10
10
  "author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
11
11
  "license": "MIT",
12
12
  "type": "module",
13
+ "overrides": {
14
+ "tmp": "0.2.4",
15
+ "external-editor": {
16
+ "tmp": "0.2.4"
17
+ },
18
+ "follow-redirects": "1.15.6",
19
+ "axios": "^1.7.0"
20
+ },
13
21
  "dependencies": {
14
22
  "@aws-sdk/client-s3": "^3.279.0",
15
23
  "@aws-sdk/lib-storage": "^3.279.0",
16
- "@cucumber/cucumber": "^10.9.0",
24
+ "@cucumber/cucumber": "^12.2.0",
17
25
  "@octokit/rest": "^21.1.1",
18
26
  "aws-sdk": "^2.1072.0",
19
- "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
20
27
  "callsite-record": "^4.1.4",
21
28
  "commander": "^12",
22
29
  "cross-spawn": "^7.0.3",
@@ -26,6 +33,7 @@
26
33
  "fast-xml-parser": "^4.4.1",
27
34
  "file-url": "3.0.0",
28
35
  "filesize": "^10.1.6",
36
+ "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
29
37
  "glob": "^10.3",
30
38
  "handlebars": "^4.7.8",
31
39
  "has-flag": "^5.0.1",
@@ -85,7 +93,7 @@
85
93
  "@types/mocha": "^10.0.7",
86
94
  "@wdio/reporter": "^7.16.13",
87
95
  "chai": "^4.3.6",
88
- "codeceptjs": "^3.6.5",
96
+ "codeceptjs": "^3.7.4",
89
97
  "cucumber": "^6.0.7",
90
98
  "eslint": "^9.24.0",
91
99
  "eslint-config-prettier": "^8.3.0",
@@ -93,13 +101,13 @@
93
101
  "jasmine": "^5.2.0",
94
102
  "jest": "^27.4.7",
95
103
  "jsdom": "^22.1.0",
96
- "mocha": "^9.2.0",
104
+ "mocha": "^11.7.2",
97
105
  "mock-http-server": "^1.4.5",
98
106
  "pino": "^8.15.0",
99
107
  "prettier": "^3.2.5",
100
108
  "puppeteer": "^22.15.0",
101
109
  "typescript": "^5.5.4",
102
- "vitest": "^1.6.0"
110
+ "vitest": "^3.2.4"
103
111
  },
104
112
  "bin": {
105
113
  "@testomatio/reporter": "./lib/bin/cli.js",
@@ -113,27 +121,28 @@
113
121
  "require": "./lib/reporter.js",
114
122
  "types": "./types/types.d.ts"
115
123
  },
116
- "./lib/adapter/codecept/codecept.js": "./lib/adapter/codecept.js",
117
- "./lib/adapter/codecept": "./lib/adapter/codecept.js",
118
- "./codecept": "./lib/adapter/codecept.js",
119
- "./lib/adapter/cucumber/cucumber.js": "./lib/adapter/cucumber/current.js",
120
- "./cucumber": "./lib/adapter/cucumber/current.js",
121
- "./lib/adapter/cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
122
- "./cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
123
- "./cypress": "./lib/adapter/cypress-plugin/index.js",
124
- "./lib/adapter/jasmine.js": "./lib/adapter/jasmine.js",
125
- "./jasmine": "./lib/adapter/jasmine.js",
126
- "./lib/adapter/jest.js": "./lib/adapter/jest.js",
127
- "./jest": "./lib/adapter/jest.js",
128
- "./lib/adapter/mocha/mocha.js": "./lib/adapter/mocha.js",
129
- "./mocha": "./lib/adapter/mocha.js",
130
- "./lib/adapter/playwright.js": "./lib/adapter/playwright.js",
131
- "./nightwatch": "./lib/adapter/nightwatch.js",
132
- "./playwright": "./lib/adapter/playwright.js",
133
- "./vitest": "./lib/adapter/vitest.js",
134
- "./lib/adapter/webdriver.js": "./lib/adapter/webdriver.js",
135
- "./lib/adapter/webdriver": "./lib/adapter/webdriver.js",
136
- "./webdriver": "./lib/adapter/webdriver.js",
137
- "./wdio": "./lib/adapter/webdriver.js"
124
+ "exports": {
125
+ ".": {
126
+ "import": "./src/reporter.js",
127
+ "require": "./lib/reporter.js",
128
+ "types": "./types/types.d.ts"
129
+ },
130
+ "./cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
131
+ "./cypress": "./lib/adapter/cypress-plugin/index.js",
132
+ "./lib/adapter/jasmine.js": "./lib/adapter/jasmine.js",
133
+ "./jasmine": "./lib/adapter/jasmine.js",
134
+ "./lib/adapter/jest.js": "./lib/adapter/jest.js",
135
+ "./jest": "./lib/adapter/jest.js",
136
+ "./lib/adapter/mocha/mocha.js": "./lib/adapter/mocha.js",
137
+ "./mocha": "./lib/adapter/mocha.js",
138
+ "./lib/adapter/playwright.js": "./lib/adapter/playwright.js",
139
+ "./nightwatch": "./lib/adapter/nightwatch.js",
140
+ "./playwright": "./lib/adapter/playwright.js",
141
+ "./vitest": "./lib/adapter/vitest.js",
142
+ "./lib/adapter/webdriver.js": "./lib/adapter/webdriver.js",
143
+ "./lib/adapter/webdriver": "./lib/adapter/webdriver.js",
144
+ "./webdriver": "./lib/adapter/webdriver.js",
145
+ "./wdio": "./lib/adapter/webdriver.js"
146
+ }
138
147
  }
139
148
  }
@@ -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))))
package/src/pipe/debug.js CHANGED
@@ -15,7 +15,7 @@ export class DebugPipe {
15
15
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
16
  if (this.isEnabled) {
17
17
  this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
18
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,8 +93,7 @@ export class DebugPipe {
93
93
  const logData = { action: 'addTest', testId: data };
94
94
  if (this.store.runId) logData.runId = this.store.runId;
95
95
  this.logToFile(logData);
96
- }
97
- else this.batch.tests.push(data);
96
+ } else this.batch.tests.push(data);
98
97
 
99
98
  if (!this.batch.intervalFunction) await this.batchUpload();
100
99
  }
@@ -20,7 +20,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
20
20
  class TestomatioPipe {
21
21
  constructor(params, store) {
22
22
  this.batch = {
23
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
23
+ isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
24
24
  intervalFunction: null, // will be created in createRun by setInterval function
25
25
  intervalTime: 5000, // how often tests are sent
26
26
  tests: [], // array of tests in batch
@@ -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;
@@ -61,8 +60,8 @@ class TestomatioPipe {
61
60
  retryConfig: {
62
61
  retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
63
62
  retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
64
- httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
65
- shouldRetry: (error) => {
63
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
+ shouldRetry: error => {
66
65
  if (!error.response) return false;
67
66
  switch (error.response?.status) {
68
67
  case 400: // Bad request (probably wrong API key)
@@ -74,8 +73,8 @@ class TestomatioPipe {
74
73
  break;
75
74
  }
76
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
77
- }
78
- }
76
+ },
77
+ },
79
78
  });
80
79
 
81
80
  this.isEnabled = true;
@@ -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,
@@ -188,7 +186,7 @@ class TestomatioPipe {
188
186
  method: 'PUT',
189
187
  url: `/api/reporter/${this.runId}`,
190
188
  data: runParams,
191
- responseType: 'json'
189
+ responseType: 'json',
192
190
  });
193
191
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
194
192
  return;
@@ -201,7 +199,7 @@ class TestomatioPipe {
201
199
  url: '/api/reporter',
202
200
  data: runParams,
203
201
  maxContentLength: Infinity,
204
- responseType: 'json'
202
+ responseType: 'json',
205
203
  });
206
204
 
207
205
  this.runId = resp.data.uid;
@@ -265,40 +263,42 @@ class TestomatioPipe {
265
263
 
266
264
  debug('Adding test', json);
267
265
 
268
- return this.client.request({
269
- method: 'POST',
270
- url: `/api/reporter/${this.runId}/testrun`,
271
- data: json,
272
- headers: {
273
- 'Content-Type': 'application/json',
274
- },
275
- maxContentLength: Infinity
276
- }).catch(err => {
277
- this.requestFailures++;
278
- this.notReportedTestsCount++;
279
- if (err.response) {
280
- if (err.response.status >= 400) {
281
- const responseData = err.response.data || { message: '' };
266
+ return this.client
267
+ .request({
268
+ method: 'POST',
269
+ url: `/api/reporter/${this.runId}/testrun`,
270
+ data: json,
271
+ headers: {
272
+ 'Content-Type': 'application/json',
273
+ },
274
+ maxContentLength: Infinity,
275
+ })
276
+ .catch(err => {
277
+ this.requestFailures++;
278
+ this.notReportedTestsCount++;
279
+ if (err.response) {
280
+ if (err.response.status >= 400) {
281
+ const responseData = err.response.data || { message: '' };
282
+ console.log(
283
+ APP_PREFIX,
284
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
285
+ pc.gray(data?.title || ''),
286
+ );
287
+ if (err.response?.data?.message?.includes('could not be matched')) {
288
+ this.hasUnmatchedTests = true;
289
+ }
290
+ return;
291
+ }
282
292
  console.log(
283
293
  APP_PREFIX,
284
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
285
- pc.gray(data?.title || ''),
294
+ pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
295
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
286
296
  );
287
- if (err.response?.data?.message?.includes('could not be matched')) {
288
- this.hasUnmatchedTests = true;
289
- }
290
- return;
297
+ printCreateIssue(err);
298
+ } else {
299
+ console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
291
300
  }
292
- console.log(
293
- APP_PREFIX,
294
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
295
- `Report couldn't be processed: ${err?.response?.data?.message}`,
296
- );
297
- printCreateIssue(err);
298
- } else {
299
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
300
- }
301
- });
301
+ });
302
302
  };
303
303
 
304
304
  /**
@@ -325,43 +325,42 @@ class TestomatioPipe {
325
325
  const testsToSend = this.batch.tests.splice(0);
326
326
  debug('📨 Batch upload', testsToSend.length, 'tests');
327
327
 
328
- return this.client.request({
329
- method: 'POST',
330
- url: `/api/reporter/${this.runId}/testrun`,
331
- data: {
332
- api_key: this.apiKey,
333
- tests: testsToSend,
334
- batch_index: this.batch.batchIndex
335
- },
336
- headers: {
337
- 'Content-Type': 'application/json',
338
- },
339
- maxContentLength: Infinity
340
- }).catch(err => {
341
- this.requestFailures++;
342
- this.notReportedTestsCount += testsToSend.length;
343
- if (err.response) {
344
- if (err.response.status >= 400) {
345
- const responseData = err.response.data || { message: '' };
328
+ return this.client
329
+ .request({
330
+ method: 'POST',
331
+ url: `/api/reporter/${this.runId}/testrun`,
332
+ data: {
333
+ api_key: this.apiKey,
334
+ tests: testsToSend,
335
+ batch_index: this.batch.batchIndex,
336
+ },
337
+ headers: {
338
+ 'Content-Type': 'application/json',
339
+ },
340
+ maxContentLength: Infinity,
341
+ })
342
+ .catch(err => {
343
+ this.requestFailures++;
344
+ this.notReportedTestsCount += testsToSend.length;
345
+ if (err.response) {
346
+ if (err.response.status >= 400) {
347
+ const responseData = err.response.data || { message: '' };
348
+ console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
349
+ if (err.response?.data?.message?.includes('could not be matched')) {
350
+ this.hasUnmatchedTests = true;
351
+ }
352
+ return;
353
+ }
346
354
  console.log(
347
355
  APP_PREFIX,
348
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
356
+ pc.yellow(`Warning: (${err.response?.status})`),
357
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
349
358
  );
350
- if (err.response?.data?.message?.includes('could not be matched')) {
351
- this.hasUnmatchedTests = true;
352
- }
353
- return;
359
+ printCreateIssue(err);
360
+ } else {
361
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
354
362
  }
355
- console.log(
356
- APP_PREFIX,
357
- pc.yellow(`Warning: (${err.response?.status})`),
358
- `Report couldn't be processed: ${err?.response?.data?.message}`,
359
- );
360
- printCreateIssue(err);
361
- } else {
362
- console.log(APP_PREFIX, "Report couldn't be processed", err);
363
- }
364
- });
363
+ });
365
364
  };
366
365
 
367
366
  /**
@@ -387,9 +386,9 @@ class TestomatioPipe {
387
386
  else this.batch.tests.push(data);
388
387
 
389
388
  // if test is added after run which is already finished
390
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
389
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
391
390
 
392
- // return promise to be able to wait for it
391
+ // return promise to be able to wait for it
393
392
  return uploading;
394
393
  }
395
394
 
@@ -419,14 +418,13 @@ class TestomatioPipe {
419
418
  console.warn(`${APP_PREFIX} ${errorMessage}`);
420
419
  }
421
420
 
422
- const { status, parallel } = params;
421
+ const { status } = params;
423
422
 
424
423
  let status_event;
425
424
 
426
425
  if (status === STATUS.FINISHED) status_event = 'finish';
427
426
  if (status === STATUS.PASSED) status_event = 'pass';
428
427
  if (status === STATUS.FAILED) status_event = 'fail';
429
- if (parallel) status_event += '_parallel';
430
428
 
431
429
  try {
432
430
  if (this.runId && !this.proceed) {
@@ -439,7 +437,7 @@ class TestomatioPipe {
439
437
  status_event,
440
438
  detach: params.detach,
441
439
  tests: params.tests,
442
- }
440
+ },
443
441
  });
444
442
  if (this.runUrl) {
445
443
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
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,