testomatio-reporter-cli 2.8.5-beta.2-yarn → 2.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-reporter-cli",
3
- "version": "2.8.5-beta.2-yarn",
3
+ "version": "2.9.0",
4
4
  "description": "Yarn Berry compatible standalone CLI for @testomatio/reporter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,6 +48,7 @@ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
48
48
 
49
49
  function CodeceptReporter(config) {
50
50
  const failedTests = [];
51
+ const reportedTestUids = new Set();
51
52
  let videos = [];
52
53
  let traces = [];
53
54
  const reportTestPromises = [];
@@ -161,6 +162,7 @@ function CodeceptReporter(config) {
161
162
  const error = hook?.ctx?.currentTest?.err;
162
163
 
163
164
  for (const test of suite.tests) {
165
+ reportedTestUids.add(test.uid);
164
166
  const reportTestPromise = client.addTestRun('failed', {
165
167
  ...stripExampleFromTitle(test.title),
166
168
  rid: test.uid,
@@ -197,9 +199,29 @@ function CodeceptReporter(config) {
197
199
  });
198
200
  });
199
201
 
202
+ event.dispatcher.on(event.test.skipped, test => {
203
+ const { uid, tags, title } = test.simplify();
204
+
205
+ if (uid && reportedTestUids.has(uid)) return;
206
+
207
+ services.setContext(null);
208
+
209
+ const reportTestPromise = client.addTestRun(STATUS.SKIPPED, {
210
+ ...stripExampleFromTitle(title),
211
+ rid: uid,
212
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
213
+ suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
214
+ time: test.duration,
215
+ meta: test.meta,
216
+ });
217
+ reportTestPromises.push(reportTestPromise);
218
+ reportedTestUids.add(uid);
219
+ });
220
+
200
221
  event.dispatcher.on(event.test.after, test => {
201
222
  const { uid, tags, title, artifacts } = test.simplify();
202
223
  const error = test.err || null;
224
+ reportedTestUids.add(uid);
203
225
  failedTests.push(uid || title);
204
226
  const testObj = getTestAndMessage(title);
205
227
  const files = buildArtifactFiles(artifacts);
@@ -211,8 +233,8 @@ function CodeceptReporter(config) {
211
233
 
212
234
  // Build step hierarchy with screenshot from screenshotOnFail
213
235
  const stepHierarchy = buildUnifiedStepHierarchy(
214
- test.steps,
215
- hookSteps,
236
+ test.steps,
237
+ hookSteps,
216
238
  screenshotOnFailPath
217
239
  );
218
240
 
@@ -14,13 +14,13 @@ import { fetchLinksFromLogs } from './utils/playwright.js';
14
14
  import { formatStep, addStatusToStep, addArtifactsToStep } from './utils/step-formatter.js';
15
15
  import { log } from '../utils/log.js';
16
16
 
17
- const reportTestPromises = [];
18
-
19
17
  class PlaywrightReporter {
20
18
  constructor(config = {}) {
21
19
  this.client = new TestomatioClient({ apiKey: config?.apiKey });
22
20
 
23
21
  this.uploads = [];
22
+ this.reportTestPromises = [];
23
+ this.runPromise = Promise.resolve();
24
24
  }
25
25
 
26
26
  onBegin(config, suite) {
@@ -29,7 +29,9 @@ class PlaywrightReporter {
29
29
  if (!this.client) return;
30
30
  this.suite = suite;
31
31
  this.config = config;
32
- this.client.createRun();
32
+ this.uploads = [];
33
+ this.reportTestPromises = [];
34
+ this.runPromise = this.client.createRun();
33
35
  }
34
36
 
35
37
  onTestBegin(testInfo) {
@@ -41,6 +43,7 @@ class PlaywrightReporter {
41
43
  // test.parent.project().__projectId
42
44
 
43
45
  if (!this.client) return;
46
+ await this.runPromise;
44
47
 
45
48
  const { title } = test;
46
49
  const { error, duration } = result;
@@ -133,7 +136,11 @@ class PlaywrightReporter {
133
136
  ...meta,
134
137
  ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
135
138
  ...test.annotations?.reduce((acc, annotation) => {
136
- acc[annotation.type] = annotation.description;
139
+ if (acc[annotation.type]) {
140
+ acc[annotation.type] = `${acc[annotation.type]}, ${annotation.description}`;
141
+ } else {
142
+ acc[annotation.type] = annotation.description;
143
+ }
137
144
  return acc;
138
145
  }, {}),
139
146
  },
@@ -149,7 +156,7 @@ class PlaywrightReporter {
149
156
  // remove empty uploads
150
157
  this.uploads = this.uploads.filter(anUpload => anUpload.files.length);
151
158
 
152
- reportTestPromises.push(reportTestPromise);
159
+ this.reportTestPromises.push(reportTestPromise);
153
160
  }
154
161
 
155
162
  #getArtifactPath(artifact) {
@@ -173,7 +180,8 @@ class PlaywrightReporter {
173
180
  async onEnd(result) {
174
181
  if (!this.client) return;
175
182
 
176
- await Promise.all(reportTestPromises);
183
+ await this.runPromise;
184
+ await Promise.all(this.reportTestPromises);
177
185
 
178
186
  if (this.uploads.length) {
179
187
  if (this.client.uploader.isEnabled) log.info(`🎞️ Uploading ${this.uploads.length} files...`);
package/src/bin/cli.js CHANGED
@@ -6,7 +6,7 @@ import { glob } from 'glob';
6
6
  import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
- import { APP_PREFIX, STATUS, DEBUG_FILE } from '../constants.js';
9
+ import { APP_PREFIX, STATUS, DEBUG_FILE, BATCH_MODE } from '../constants.js';
10
10
  import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
11
11
  import { config } from '../config.js';
12
12
  import { readLatestRunId } from '../utils/utils.js';
@@ -369,7 +369,7 @@ program
369
369
  const client = new TestomatClient({
370
370
  apiKey,
371
371
  runId,
372
- isBatchEnabled: false,
372
+ batchMode: BATCH_MODE.DISABLED,
373
373
  });
374
374
 
375
375
  let testruns = client.uploader.readUploadedFiles(runId);
@@ -9,6 +9,7 @@ import { config } from '../config.js';
9
9
  import { readLatestRunId } from '../utils/utils.js';
10
10
  import dotenv from 'dotenv';
11
11
  import { log } from '../utils/log.js';
12
+ import { BATCH_MODE } from '../constants.js';
12
13
 
13
14
  const debug = createDebugMessages('@testomatio/reporter:upload-cli');
14
15
  const version = getPackageVersion();
@@ -37,7 +38,7 @@ program
37
38
  const client = new TestomatClient({
38
39
  apiKey,
39
40
  runId,
40
- isBatchEnabled: false,
41
+ batchMode: BATCH_MODE.DISABLED,
41
42
  });
42
43
  let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
43
44
 
package/src/constants.js CHANGED
@@ -28,6 +28,14 @@ const STATUS = {
28
28
  SKIPPED: 'skipped',
29
29
  FINISHED: 'finished',
30
30
  };
31
+
32
+ // batch upload mode
33
+ /** @type {{ AUTO: 'auto', MANUAL: 'manual', DISABLED: 'disabled' }} */
34
+ const BATCH_MODE = {
35
+ AUTO: 'auto',
36
+ MANUAL: 'manual',
37
+ DISABLED: 'disabled',
38
+ };
31
39
  // html pipe var
32
40
  const HTML_REPORT = {
33
41
  FOLDER: 'html-report',
@@ -61,6 +69,7 @@ export {
61
69
  TESTOMAT_TMP_STORAGE_DIR,
62
70
  CSV_HEADERS,
63
71
  STATUS,
72
+ BATCH_MODE,
64
73
  HTML_REPORT,
65
74
  MARKDOWN_REPORT,
66
75
  REQUEST_TIMEOUT,
@@ -261,9 +261,10 @@ class CoveragePipe { // or Changes for the future???
261
261
  */
262
262
  #getChangedFilesFromGit(cmd) {
263
263
  try {
264
+ // Capture stderr (instead of ignoring it) so Git's actual error is available for diagnostics
264
265
  const result = execSync(cmd, {
265
266
  encoding: 'utf-8',
266
- stdio: ['pipe', 'pipe', 'ignore']
267
+ stdio: ['pipe', 'pipe', 'pipe']
267
268
  });
268
269
 
269
270
  return result
@@ -272,16 +273,33 @@ class CoveragePipe { // or Changes for the future???
272
273
  .filter(Boolean);
273
274
  }
274
275
  catch (err) {
275
- const errorMessage = err.message || '';
276
- // Git edge: Not a git repository or other error
276
+ // Prefer Git's own stderr output, fall back to the generic error message
277
+ const gitOutput = (err.stderr || '').toString().trim();
278
+ const errorMessage = gitOutput || err.message || '';
279
+
280
+ // Git edge: Not a git repository
277
281
  if (errorMessage.includes('Not a git repository')) {
278
- log.error( '❌ Error: This folder is not a Git repository.');
282
+ log.error('❌ Error: This folder is not a Git repository.');
283
+ return [];
279
284
  }
280
- else {
281
- throw new Error(`❌ Git command failed ("${cmd}"):\n`, errorMessage);
285
+
286
+ // Git edge: the branch/ref to diff against is not available locally.
287
+ // This is common in CI, where a shallow checkout fetches only the current branch.
288
+ if (
289
+ errorMessage.includes('unknown revision') ||
290
+ errorMessage.includes('ambiguous argument') ||
291
+ errorMessage.includes('bad revision')
292
+ ) {
293
+ log.error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
294
+ log.error(
295
+ `🔍 Branch "${this.branch}" was not found locally. ` +
296
+ `In CI this usually means a shallow checkout — fetch full history first, e.g. ` +
297
+ `actions/checkout with "fetch-depth: 0", or run "git fetch origin ${this.branch}:${this.branch}".`
298
+ );
299
+ return [];
282
300
  }
283
301
 
284
- return [];
302
+ throw new Error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
285
303
  }
286
304
  }
287
305
 
package/src/pipe/debug.js CHANGED
@@ -14,13 +14,7 @@ export class DebugPipe {
14
14
 
15
15
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
16
  if (this.isEnabled) {
17
- this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
19
- intervalFunction: null,
20
- intervalTime: 5000,
21
- tests: [],
22
- batchIndex: 0,
23
- };
17
+ this.tests = [];
24
18
  const suffix = process.env.TESTOMATIO_REPLAY ? 'replay' : '';
25
19
  const paths = getDebugFilePath(suffix);
26
20
  this.logFilePath = paths.tmp;
@@ -60,8 +54,13 @@ export class DebugPipe {
60
54
  this.logToFile({ datetime: new Date().toISOString(), timestamp: Date.now() });
61
55
  this.logToFile({ data: 'variables', testomatioEnvVars: this.testomatioEnvVars });
62
56
  this.logToFile({ data: 'store', store: this.store || {} });
63
- // Bind batchUpload to the instance
64
- this.batchUpload = this.batchUpload.bind(this);
57
+
58
+ // Safety net for hook failures (e.g. a failing AfterSuite) that abort the run
59
+ // before finishRun: buffered tests would otherwise be lost. The handler is
60
+ // attached lazily when the first test is buffered and detached once flushed,
61
+ // so processes that create many pipes don't pile up `exit` listeners.
62
+ this.flushOnExit = () => this.flushBufferedTests();
63
+ this.exitListenerAttached = false;
65
64
  }
66
65
  }
67
66
 
@@ -88,42 +87,22 @@ export class DebugPipe {
88
87
 
89
88
  async createRun(params = {}) {
90
89
  if (!this.isEnabled) return;
91
- if (params.isBatchEnabled === true || params.isBatchEnabled === false) this.batch.isEnabled = params.isBatchEnabled;
92
-
93
- if (!this.isEnabled) return {};
94
- if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.batchUpload, this.batch.intervalTime);
95
90
 
96
91
  this.logToFile({ action: 'createRun', params });
97
92
  }
98
93
 
99
94
  async addTest(data) {
100
95
  if (!this.isEnabled) return;
101
-
102
- if (!this.batch.isEnabled) {
103
- const logData = { action: 'addTest', testId: data };
104
- if (this.store.runId) logData.runId = this.store.runId;
105
- this.logToFile(logData);
106
- } else this.batch.tests.push(data);
107
-
108
- if (!this.batch.intervalFunction) await this.batchUpload();
109
- }
110
-
111
- async batchUpload() {
112
- this.batch.batchIndex++;
113
- if (!this.batch.isEnabled) return;
114
- if (!this.batch.tests.length) return;
115
-
116
- const testsToSend = this.batch.tests.splice(0);
117
-
118
- const logData = { action: 'addTestsBatch', tests: testsToSend };
119
- if (this.store.runId) logData.runId = this.store.runId;
120
- this.logToFile(logData);
96
+ this.tests.push(data);
97
+ if (!this.exitListenerAttached) {
98
+ process.once('exit', this.flushOnExit);
99
+ this.exitListenerAttached = true;
100
+ }
121
101
  }
122
102
 
123
103
  async finishRun(params) {
124
104
  if (!this.isEnabled) return;
125
105
  await this.sync();
126
- if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
127
106
  const logData = { action: 'finishRun', params };
128
107
  if (this.store.runId) logData.runId = this.store.runId;
129
108
  this.logToFile(logData);
@@ -133,8 +112,28 @@ export class DebugPipe {
133
112
  }
134
113
 
135
114
  async sync() {
136
- if (!this.isEnabled) return;
137
- await this.batchUpload();
115
+ this.flushBufferedTests();
116
+ }
117
+
118
+ /**
119
+ * Writes any buffered tests to the debug file as a single batch.
120
+ * Runs synchronously so it can also be invoked from a process `exit` handler,
121
+ * which is the only chance to persist tests when a hook failure (e.g. a failing
122
+ * AfterSuite) prevents `finishRun` from being reached. Idempotent: the buffer is
123
+ * drained on flush, so a later `finishRun`/exit flush is a no-op.
124
+ */
125
+ flushBufferedTests() {
126
+ if (!this.isEnabled || !this.tests.length) return;
127
+
128
+ const tests = this.tests.splice(0);
129
+ const logData = { action: 'addTestsBatch', tests };
130
+ if (this.store.runId) logData.runId = this.store.runId;
131
+ this.logToFile(logData);
132
+
133
+ if (this.exitListenerAttached) {
134
+ process.removeListener('exit', this.flushOnExit);
135
+ this.exitListenerAttached = false;
136
+ }
138
137
  }
139
138
 
140
139
  toString() {
@@ -5,6 +5,7 @@ import JsonCycle from 'json-cycle';
5
5
  import {
6
6
  APP_PREFIX,
7
7
  STATUS,
8
+ BATCH_MODE,
8
9
  REQUEST_TIMEOUT,
9
10
  getCreateRunRequestTimeout,
10
11
  REPORTER_REQUEST_RETRIES,
@@ -46,17 +47,25 @@ function parseCiParams(raw) {
46
47
  /**
47
48
  * @typedef {import('../../types/types.js').Pipe} Pipe
48
49
  * @typedef {import('../../types/types.js').TestData} TestData
50
+ * @typedef {import('../../types/types.js').BatchMode} BatchMode
51
+ * @typedef {import('../../types/types.js').CreateRunParams} CreateRunParams
49
52
  * @class TestomatioPipe
50
53
  * @implements {Pipe}
51
54
  */
52
55
  class TestomatioPipe {
53
56
  constructor(params, store) {
54
57
  this.batch = {
55
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
58
+ /** @type {BatchMode}
59
+ * Batch upload mode:
60
+ * - `auto`: upload tests automatically by time interval (e.g. every 5 seconds).
61
+ * - `manual`: buffer tests and upload only when `sync()` is invoked manually.
62
+ * - `disabled`: send one test per request, no batching.
63
+ */
64
+ mode: params.batchMode || (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? BATCH_MODE.DISABLED : BATCH_MODE.AUTO),
56
65
  intervalFunction: null, // will be created in createRun by setInterval function
57
- intervalTime: 5000, // how often tests are sent
66
+ intervalTime: 6000, // how often tests are sent
58
67
  tests: [], // array of tests in batch
59
- batchIndex: 0, // represents the current batch index (starts from 1 and increments by 1 for each batch)
68
+ batchIndex: 0, // represents the current batch index (starts from 1 and increments by 1 for each batch)
60
69
  numberOfTimesCalledWithoutTests: 0, // how many times batch was called without tests
61
70
  };
62
71
  this.retriesTimestamps = [];
@@ -222,13 +231,13 @@ class TestomatioPipe {
222
231
 
223
232
  /**
224
233
  * Creates a new run on Testomat.io
225
- * @param {{isBatchEnabled?: boolean, kind?: string, configuration?: Record<string, any>}} params
234
+ * @param {CreateRunParams} params
226
235
  * @returns Promise<void>
227
236
  */
228
237
  async createRun(params = {}) {
229
- this.batch.isEnabled = params.isBatchEnabled ?? this.batch.isEnabled;
238
+ if (params.batchMode) this.batch.mode = params.batchMode;
230
239
  if (!this.isEnabled) return;
231
- if (this.batch.isEnabled && this.isEnabled)
240
+ if (this.batch.mode === BATCH_MODE.AUTO && this.isEnabled)
232
241
  this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
233
242
  if (this.store) {
234
243
  this.store.runKind = params.kind;
@@ -433,14 +442,14 @@ class TestomatioPipe {
433
442
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
434
443
  */
435
444
  #batchUpload = async () => {
436
- if (!this.batch.isEnabled) return;
445
+ if (this.batch.mode === BATCH_MODE.DISABLED) return;
437
446
  if (!this.batch.tests.length) return;
438
447
  if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
439
448
  // prevent infinite loop
440
449
  if (this.batch.numberOfTimesCalledWithoutTests > 10) {
441
450
  debug('📨 Batch upload: no tests to send for 10 times, stopping batch');
442
451
  clearInterval(this.batch.intervalFunction);
443
- this.batch.isEnabled = false;
452
+ this.batch.mode = BATCH_MODE.DISABLED;
444
453
  }
445
454
  if (!this.batch.tests.length) {
446
455
  debug('📨 Batch upload: no tests to send');
@@ -496,11 +505,11 @@ class TestomatioPipe {
496
505
  this.#formatData(data);
497
506
 
498
507
  let uploading = null;
499
- if (!this.batch.isEnabled) uploading = this.#uploadSingleTest(data);
508
+ if (this.batch.mode === BATCH_MODE.DISABLED) uploading = this.#uploadSingleTest(data);
500
509
  else this.batch.tests.push(data);
501
510
 
502
- // if test is added after run which is already finished
503
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
511
+ // auto mode but no interval running yet (e.g. createRun hasn't started it): flush immediately
512
+ if (this.batch.mode === BATCH_MODE.AUTO && !this.batch.intervalFunction) uploading = this.#batchUpload();
504
513
 
505
514
  // return promise to be able to wait for it
506
515
  return uploading;
@@ -529,7 +538,7 @@ class TestomatioPipe {
529
538
  // (e.g. if test has artifacts, add test function will be invoked only after artifacts are uploaded)
530
539
  // batch stops working after run is finished; thus, disable it to use single test uploading
531
540
  this.batch.intervalFunction = null;
532
- this.batch.isEnabled = false;
541
+ this.batch.mode = BATCH_MODE.DISABLED;
533
542
  }
534
543
 
535
544
  debug('Finishing run...');
@@ -613,7 +622,7 @@ class TestomatioPipe {
613
622
  if (this.batch.intervalFunction) {
614
623
  clearInterval(this.batch.intervalFunction);
615
624
  this.batch.intervalFunction = null;
616
- this.batch.isEnabled = false;
625
+ this.batch.mode = BATCH_MODE.DISABLED;
617
626
  }
618
627
  this.batch.tests = [];
619
628
  }
package/src/replay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import TestomatClient from './client.js';
4
- import { STATUS, DEBUG_FILE } from './constants.js';
4
+ import { STATUS, DEBUG_FILE, BATCH_MODE } from './constants.js';
5
5
  import { config } from './config.js';
6
6
 
7
7
  export class Replay {
@@ -216,7 +216,7 @@ export class Replay {
216
216
 
217
217
  const client = new TestomatClient({
218
218
  apiKey: this.apiKey,
219
- isBatchEnabled: true,
219
+ batchMode: BATCH_MODE.AUTO,
220
220
  ...runParams,
221
221
  ...(runId && { runId }),
222
222
  });
package/src/xmlReader.js CHANGED
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import pc from 'picocolors';
4
4
  import fs from 'fs';
5
5
  import { XMLParser } from 'fast-xml-parser';
6
- import { APP_PREFIX, STATUS } from './constants.js';
6
+ import { APP_PREFIX, STATUS, BATCH_MODE } from './constants.js';
7
7
  import { randomUUID } from 'crypto';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { NUnitXmlParser } from './junit-adapter/nunit-parser.js';
@@ -73,8 +73,7 @@ class XmlReader {
73
73
  env: TESTOMATIO_ENV,
74
74
  group_title: TESTOMATIO_RUNGROUP_TITLE,
75
75
  detach: TESTOMATIO_MARK_DETACHED,
76
- // batch uploading is implemented for xml already
77
- isBatchEnabled: false,
76
+ batchMode: BATCH_MODE.MANUAL,
78
77
  };
79
78
  this.runId = opts.runId || TESTOMATIO_RUN;
80
79
  this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts);
@@ -543,7 +542,7 @@ class XmlReader {
543
542
  title: this.requestParams.title,
544
543
  env: this.requestParams.env,
545
544
  group_title: this.requestParams.group_title,
546
- isBatchEnabled: this.requestParams.isBatchEnabled,
545
+ batchMode: this.requestParams.batchMode,
547
546
  };
548
547
 
549
548
  debug('Run', runParams);
@@ -611,7 +610,7 @@ class XmlReader {
611
610
  this.pipes = this.pipes || (await this.pipesPromise);
612
611
 
613
612
  // Create run before uploading tests to ensure runId is set
614
- await this.createRun();
613
+ // await this.createRun(); // makes reporting stuck after finish, thus commenting out
615
614
 
616
615
  if (!this.tests || !Array.isArray(this.tests) || this.tests.length === 0) {
617
616
  debug('No tests to upload, finishing run');
package/types/types.d.ts CHANGED
@@ -234,7 +234,7 @@ export interface HtmlTestData extends TestData {
234
234
  /**
235
235
  * Extended test data for Markdown reporter.
236
236
  */
237
- export interface MarkdownTestData extends HtmlTestData {}
237
+ export interface MarkdownTestData extends HtmlTestData { }
238
238
 
239
239
  /**
240
240
  * Object representing a result of a Run.
@@ -288,6 +288,13 @@ export enum RunStatus {
288
288
  Finished = 'finished',
289
289
  }
290
290
 
291
+ /** Batch upload strategy:
292
+ * `auto` (by time interval, e.g. every 5 seconds),
293
+ * `manual` (send tests via manually invoking sync() ),
294
+ * `disabled` (one test per request, no batching).
295
+ */
296
+ export type BatchMode = 'auto' | 'manual' | 'disabled';
297
+
291
298
  export interface Pipe {
292
299
  isEnabled: boolean;
293
300
  store: {};
@@ -334,8 +341,8 @@ export interface CreateRunParams {
334
341
  /** Run configuration merged into the server-side run configuration. */
335
342
  configuration?: Record<string, any>;
336
343
 
337
- /** Override batch upload on/off. */
338
- isBatchEnabled?: boolean;
344
+ /** Override batch upload mode. */
345
+ batchMode?: BatchMode;
339
346
  }
340
347
 
341
348
  /**