testomatio-reporter-cli 2.8.5-beta.2-yarn → 2.8.6-beta-fix-xml-batch

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.8.6-beta-fix-xml-batch",
4
4
  "description": "Yarn Berry compatible standalone CLI for @testomatio/reporter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,
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
  /**