@testomatio/reporter 2.7.6 → 2.7.9-beta.1-markdown

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.
@@ -2,7 +2,13 @@ import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
3
  import { Gaxios } from 'gaxios';
4
4
  import JsonCycle from 'json-cycle';
5
- import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
5
+ import {
6
+ APP_PREFIX,
7
+ STATUS,
8
+ REQUEST_TIMEOUT,
9
+ getCreateRunRequestTimeout,
10
+ REPORTER_REQUEST_RETRIES,
11
+ } from '../constants.js';
6
12
  import {
7
13
  isValidUrl,
8
14
  foundedTestLog,
@@ -81,7 +87,7 @@ class TestomatioPipe {
81
87
  // Create a new instance of gaxios with a custom config
82
88
  this.client = new Gaxios({
83
89
  baseURL: `${this.url.trim()}`,
84
- timeout: AXIOS_TIMEOUT,
90
+ timeout: REQUEST_TIMEOUT,
85
91
  proxy: proxy ? proxy.toString() : undefined,
86
92
  retry: true,
87
93
  retryConfig: {
@@ -256,6 +262,7 @@ class TestomatioPipe {
256
262
  method: 'PUT',
257
263
  url: `/api/reporter/${this.runId}`,
258
264
  data: runParams,
265
+ timeout: getCreateRunRequestTimeout(),
259
266
  responseType: 'json',
260
267
  });
261
268
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
@@ -277,6 +284,7 @@ class TestomatioPipe {
277
284
  method: 'POST',
278
285
  url: '/api/reporter',
279
286
  data: runParams,
287
+ timeout: getCreateRunRequestTimeout(),
280
288
  maxContentLength: Infinity,
281
289
  responseType: 'json',
282
290
  });
@@ -294,14 +302,13 @@ class TestomatioPipe {
294
302
  process.env.runId = this.runId;
295
303
  debug('Run created', this.runId);
296
304
  } catch (err) {
305
+ if (!this.apiKey) console.error('Testomat.io API key is not set');
297
306
  const errorText = err.response?.data?.message || err.message;
298
307
  debug('Error creating run', err);
299
- console.log(errorText || err);
308
+ console.log(APP_PREFIX, errorText || err);
300
309
  if (err.response?.status === 403) this.#disablePipe();
301
- if (!this.apiKey) console.error('Testomat.io API key is not set');
302
- if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
303
310
 
304
- if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG) this.#logFailedResponse(err);
311
+ this.#logFailedResponse(err);
305
312
 
306
313
  console.error(
307
314
  APP_PREFIX,
@@ -532,8 +539,8 @@ class TestomatioPipe {
532
539
  );
533
540
  }
534
541
  } catch (err) {
535
- log.info('Error updating status, skipping...', err);
536
- if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG) this.#logFailedResponse(err);
542
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
543
+ this.#logFailedResponse(err);
537
544
  printCreateIssue();
538
545
  }
539
546
  debug('Run finished');
@@ -558,19 +565,26 @@ class TestomatioPipe {
558
565
  responseBody = hideTestomatioToken(responseBody);
559
566
 
560
567
  const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
561
- const method = error.response?.config.method || '<unknown method>';
562
- const url = error.response?.config.url || '<unknown url>';
568
+ const method = error.response?.config?.method || '<unknown method>';
569
+ const url = String(error.response?.config?.url || '<unknown url>');
570
+
571
+ let message = pc.yellow('⚠️ Request to Testomat.io failed:\n');
572
+ message += pc.bold(`${pc.red(statusCode)} ${method} ${pc.gray(url)}\n`);
573
+
574
+ if (statusCode === 403) {
575
+ message += `\t${pc.red('Please check your API token. It might be invalid or expired.')}\n`;
576
+ }
563
577
 
564
- let message = pc.yellow('\n⚠️ Request to Testomat.io failed:\n');
565
- message += pc.bold(`${pc.red(statusCode)} ${method} ${url}\n`);
566
578
  message += `\t${pc.bold('response: ')}${pc.gray(responseBody)}\n`;
567
579
 
568
580
  const requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
569
- if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG) {
581
+ if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
582
+ // full body
570
583
  message += `\t${pc.bold('request: ')}${pc.gray(requestBody)}\n`;
571
584
  } else {
585
+ // cut body
572
586
  const requestBodyCut = requestBody.slice(0, 1000);
573
- message += `\t${pc.bold('request: ')}${pc.gray(`${requestBodyCut}.....`)}\n`;
587
+ message += `\t${pc.bold('request: ')}${pc.gray(`${requestBodyCut}...`)}\n`;
574
588
  message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
575
589
  }
576
590
 
package/src/replay.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import TestomatClient from './client.js';
5
- import { STATUS } from './constants.js';
4
+ import { STATUS, DEBUG_FILE } from './constants.js';
6
5
  import { config } from './config.js';
7
6
 
8
7
  export class Replay {
@@ -15,11 +14,12 @@ export class Replay {
15
14
  }
16
15
 
17
16
  /**
18
- * Get the default debug file path
17
+ * Get the default debug file path.
18
+ * Returns ./testomatio.debug.json (actual file in CI, symlink to tmp file in local).
19
19
  * @returns {string} Path to the latest debug file
20
20
  */
21
21
  getDefaultDebugFile() {
22
- return path.join(os.tmpdir(), 'testomatio.debug.latest.json');
22
+ return path.join(process.cwd(), `${DEBUG_FILE}.json`);
23
23
  }
24
24
 
25
25
  /**
@@ -119,8 +119,11 @@ export class Replay {
119
119
  // Handle tests without rid (no deduplication)
120
120
  testsWithoutRid.push({ ...test });
121
121
  }
122
- } else if (logEntry.actions === 'finishRun') {
122
+ } else if (logEntry.action === 'finishRun') {
123
123
  finishParams = logEntry.params || {};
124
+ if (logEntry.runId && !runId) {
125
+ runId = logEntry.runId;
126
+ }
124
127
  }
125
128
  } catch (err) {
126
129
  parseErrors++;
@@ -203,23 +206,28 @@ export class Replay {
203
206
  };
204
207
  }
205
208
 
206
- // Create client and restore the run
209
+ if (runId) {
210
+ this.onLog(`Using existing run ID: ${runId}`);
211
+ process.env.TESTOMATIO_RUN = runId;
212
+ } else {
213
+ this.onLog('Publishing to run...');
214
+ }
215
+ process.env.TESTOMATIO_REPLAY = '1';
216
+
207
217
  const client = new TestomatClient({
208
218
  apiKey: this.apiKey,
209
219
  isBatchEnabled: true,
210
220
  ...runParams,
221
+ ...(runId && { runId }),
211
222
  });
212
223
 
213
- // Use the stored runId if available, otherwise create a new run
214
224
  if (runId) {
215
- this.onLog(`Using existing run ID: ${runId}`);
216
225
  client.runId = runId;
217
- } else {
218
- this.onLog('Publishing to run...');
219
- await client.createRun(runParams);
226
+ client.pipeStore.runId = runId;
220
227
  }
221
228
 
222
- // Send each test result
229
+ await client.createRun(runParams);
230
+
223
231
  let successCount = 0;
224
232
  let failureCount = 0;
225
233
 
@@ -248,7 +256,9 @@ export class Replay {
248
256
 
249
257
  await client.updateRunStatus(finishParams.status || STATUS.FINISHED);
250
258
 
251
- const result = {
259
+ this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
260
+
261
+ return {
252
262
  success: true,
253
263
  testsCount: tests.length,
254
264
  successCount,
@@ -258,10 +268,6 @@ export class Replay {
258
268
  envVars,
259
269
  runId: runId || client.runId,
260
270
  };
261
-
262
- this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
263
-
264
- return result;
265
271
  }
266
272
  }
267
273
 
@@ -2242,7 +2242,6 @@
2242
2242
  <div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
2243
2243
  <div class='env-var-info'>
2244
2244
  <div class='env-var-key'>{{@key}}</div>
2245
- <div class='env-var-description'>{{this.description}}</div>
2246
2245
  </div>
2247
2246
  <div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
2248
2247
  {{#if this.isSensitive}}
@@ -2267,7 +2266,6 @@
2267
2266
  <div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
2268
2267
  <div class='env-var-info'>
2269
2268
  <div class='env-var-key'>{{@key}}</div>
2270
- <div class='env-var-description'>{{this.description}}</div>
2271
2269
  </div>
2272
2270
  <div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
2273
2271
  {{#if this.isSensitive}}
@@ -0,0 +1,20 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { DEBUG_FILE } from '../constants.js';
4
+
5
+ /**
6
+ * Get the debug file path(s).
7
+ *
8
+ * Always creates a timestamped file in tmp dir and a symlink in project root.
9
+ *
10
+ * @param {string} [suffix] - Optional suffix appended to the base name (e.g. 'replay').
11
+ * @returns {{root: string, tmp: string}} root path (symlink), tmp path (actual file)
12
+ */
13
+ export function getDebugFilePath(suffix = '') {
14
+ const dateTime = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
15
+ const baseName = suffix ? `${DEBUG_FILE}-${suffix}` : DEBUG_FILE;
16
+ return {
17
+ root: path.join(process.cwd(), `${baseName}.json`),
18
+ tmp: path.join(os.tmpdir(), `${baseName}.${dateTime}.json`),
19
+ };
20
+ }
package/src/xmlReader.js CHANGED
@@ -39,18 +39,29 @@ const {
39
39
  TESTOMATIO_RUN,
40
40
  TESTOMATIO_MARK_DETACHED,
41
41
  TESTOMATIO_LEGACY_NUNIT,
42
+ TESTOMATIO_MAX_ENTITY_EXPANSIONS,
42
43
  } = process.env;
43
44
 
45
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
46
+ const MAX_ENTITY_EXPANSIONS = parseInt(TESTOMATIO_MAX_ENTITY_EXPANSIONS, 10) || 10000;
47
+ const ENTITY_EXPANSION_LIMIT_REGEXP = /Entity expansion limit exceeded/i;
48
+
44
49
  const options = {
45
50
  ignoreDeclaration: true,
46
51
  ignoreAttributes: false,
47
52
  alwaysCreateTextNode: false,
48
53
  attributeNamePrefix: '',
49
54
  parseTagValue: true,
55
+ processEntities: {
56
+ enabled: true,
57
+ maxEntitySize: 10000,
58
+ maxExpansionDepth: 10,
59
+ maxTotalExpansions: MAX_ENTITY_EXPANSIONS,
60
+ maxExpandedLength: 100000,
61
+ maxEntityCount: 10000,
62
+ },
50
63
  };
51
64
 
52
- const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
53
-
54
65
  const reduceOptions = {};
55
66
 
56
67
  class XmlReader {
@@ -113,7 +124,20 @@ class XmlReader {
113
124
  xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
114
125
  }
115
126
 
116
- const jsonResult = this.parser.parse(xmlData);
127
+ let jsonResult;
128
+ try {
129
+ jsonResult = this.parser.parse(xmlData);
130
+ } catch (error) {
131
+ if (ENTITY_EXPANSION_LIMIT_REGEXP.test(error.message)) {
132
+ throw new Error(
133
+ `${error.message}\n\n` +
134
+ `XML report contains more entity references than the current limit (${MAX_ENTITY_EXPANSIONS}). ` +
135
+ 'If this XML report is trusted, increase the limit with TESTOMATIO_MAX_ENTITY_EXPANSIONS, for example:\n' +
136
+ `TESTOMATIO_MAX_ENTITY_EXPANSIONS=${MAX_ENTITY_EXPANSIONS * 2} npx report-xml "{pattern}" --lang={lang}`,
137
+ );
138
+ }
139
+ throw error;
140
+ }
117
141
  let jsonSuite;
118
142
 
119
143
  if (jsonResult.testsuites) {
package/types/types.d.ts CHANGED
@@ -231,6 +231,11 @@ export interface HtmlTestData extends TestData {
231
231
  };
232
232
  }
233
233
 
234
+ /**
235
+ * Extended test data for Markdown reporter.
236
+ */
237
+ export interface MarkdownTestData extends HtmlTestData {}
238
+
234
239
  /**
235
240
  * Object representing a result of a Run.
236
241
  */