@testomatio/reporter 2.5.1 → 2.6.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.
@@ -12,9 +12,22 @@ declare class PlaywrightReporter {
12
12
  #private;
13
13
  }
14
14
  /**
15
- * Extracts and normalizes tags from test title, test options, and suite level
15
+ * Extracts tags from test title, test options, and suite level
16
+ * Identifies duplicate tags (case-insensitive)
16
17
  * @param {*} test - testInfo object from Playwright
17
- * @returns {string[]} - array of normalized tags
18
+ * @returns {string[]} - array of normalized tags with @ prefix
18
19
  */
19
20
  export function extractTags(test: any): string[];
21
+ /**
22
+ * Fetches links from stdout. Returns links and filtered stdout (without data containing markers)
23
+ *
24
+ * @param {(string | Buffer)[]} stdout
25
+ * @returns {{ links: { [key: 'test' | 'jira']: string }[], stdout: (string | Buffer)[] }}
26
+ */
27
+ export function fetchLinksFromLogs(stdout: (string | Buffer)[]): {
28
+ links: {
29
+ [key: "test" | "jira"]: string;
30
+ }[];
31
+ stdout: (string | Buffer)[];
32
+ };
20
33
  import TestomatioClient from '../client.js';
@@ -4,7 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extractTags = extractTags;
7
- const picocolors_1 = __importDefault(require("picocolors"));
7
+ exports.fetchLinksFromLogs = fetchLinksFromLogs;
8
+ const debug_1 = __importDefault(require("debug"));
8
9
  const crypto_1 = __importDefault(require("crypto"));
9
10
  const os_1 = __importDefault(require("os"));
10
11
  const path_1 = __importDefault(require("path"));
@@ -16,7 +17,9 @@ const utils_js_1 = require("../utils/utils.js");
16
17
  const index_js_1 = require("../services/index.js");
17
18
  const data_storage_js_1 = require("../data-storage.js");
18
19
  const constants_js_2 = require("../utils/constants.js");
20
+ const picocolors_1 = __importDefault(require("picocolors"));
19
21
  const reportTestPromises = [];
22
+ const debug = (0, debug_1.default)('@testomatio/reporter:adapter-playwright');
20
23
  class PlaywrightReporter {
21
24
  constructor(config = {}) {
22
25
  this.client = new client_js_1.default({ apiKey: config?.apiKey });
@@ -53,12 +56,23 @@ class PlaywrightReporter {
53
56
  const tags = extractTags(test);
54
57
  const fullTestTitle = getTestContextName(test);
55
58
  let logs = '';
56
- if (result.stderr.length || result.stdout.length) {
57
- logs = `\n\n${picocolors_1.default.bold('Logs:')}\n${picocolors_1.default.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
59
+ // get links along with filtered logs (liks related logs removed)
60
+ const { stdout: filteredStdout, links } = fetchLinksFromLogs(result.stdout);
61
+ if (filteredStdout?.length || result.stderr?.length) {
62
+ logs = `\n\n${picocolors_1.default.bold('Logs:')}\n${picocolors_1.default.red(result.stderr.join(''))}\n${filteredStdout.join('')}`;
58
63
  }
64
+ /*
65
+ All services fucntions work different for Playwright.
66
+ We don't have access to test title (as result, to test id) when calling this functions inside a test.
67
+ Thus, when user calls services functions inside a test, we just log this data to console.
68
+ Playwright intercepts the console.log on it's end and we just get this data from it.
69
+ Thus, we have a tiny drawback: all data from services functions inside a test will be logged to console.
70
+ And this requires a condition to be added for each service function – if its Playwright, then log to console.
71
+
72
+ "get" method of services will not return data for Playwright, we should parse stdout.
73
+ */
59
74
  const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(fullTestTitle);
60
75
  const testMeta = index_js_1.services.keyValues.get(fullTestTitle);
61
- const links = index_js_1.services.links.get(fullTestTitle);
62
76
  const rid = test.id || test.testId || (0, uuid_1.v4)();
63
77
  /**
64
78
  * @type {{
@@ -92,7 +106,7 @@ class PlaywrightReporter {
92
106
  test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
93
107
  suite_title,
94
108
  title,
95
- tags,
109
+ tags: tags.map(tag => tag.replace('@', '')),
96
110
  steps: steps.length ? steps : undefined,
97
111
  time: duration,
98
112
  logs,
@@ -223,27 +237,33 @@ function generateTmpFilepath(filename = '') {
223
237
  return path_1.default.join(tmpdir, filename);
224
238
  }
225
239
  /**
226
- * Extracts and normalizes tags from test title, test options, and suite level
240
+ * Extracts tags from test title, test options, and suite level
241
+ * Identifies duplicate tags (case-insensitive)
227
242
  * @param {*} test - testInfo object from Playwright
228
- * @returns {string[]} - array of normalized tags
243
+ * @returns {string[]} - array of normalized tags with @ prefix
229
244
  */
230
245
  function extractTags(test) {
231
- const tagsSet = new Set();
232
- // Extract tags from test title (@tag format)
233
- const titleTagsMatch = test.title.match(/@\w+/g);
234
- if (titleTagsMatch) {
235
- titleTagsMatch.forEach(tag => {
236
- tagsSet.add(tag.replace('@', '').toLowerCase());
237
- });
246
+ const tagsMap = new Map(); // key: lowercase tag, value: original case tag
247
+ function addTag(tag) {
248
+ if (typeof tag !== 'string')
249
+ return;
250
+ const trimmed = tag.trim();
251
+ if (!trimmed)
252
+ return;
253
+ const normalizedTag = trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
254
+ const lowercaseTag = normalizedTag.toLowerCase();
255
+ if (!tagsMap.has(lowercaseTag)) {
256
+ tagsMap.set(lowercaseTag, normalizedTag);
257
+ }
238
258
  }
239
- // Extract tags from test.tags (Playwright built-in tags)
240
- if (test.tags && Array.isArray(test.tags)) {
241
- test.tags.forEach(tag => {
242
- const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
243
- tagsSet.add(normalizedTag);
244
- });
259
+ // Extract tags from test title (@tag format); only test title is considered
260
+ const titleTagsMatch = test.title.match(/@[A-Za-z0-9_-]+/g) || [];
261
+ titleTagsMatch.forEach(addTag);
262
+ // Extract tags from test.tags (Playwright built-in tags); ignore parents
263
+ if (Array.isArray(test.tags)) {
264
+ test.tags.forEach(addTag);
245
265
  }
246
- return Array.from(tagsSet);
266
+ return Array.from(tagsMap.values());
247
267
  }
248
268
  /**
249
269
  * Returns filename + test title
@@ -253,6 +273,73 @@ function extractTags(test) {
253
273
  function getTestContextName(test) {
254
274
  return `${test._requireFile || ''}_${test.title}`;
255
275
  }
276
+ /**
277
+ * Fetches links from stdout. Returns links and filtered stdout (without data containing markers)
278
+ *
279
+ * @param {(string | Buffer)[]} stdout
280
+ * @returns {{ links: { [key: 'test' | 'jira']: string }[], stdout: (string | Buffer)[] }}
281
+ */
282
+ function fetchLinksFromLogs(stdout) {
283
+ const links = [];
284
+ const markers = [
285
+ { key: '[TESTOMATIO-LINK-TESTS]', type: 'test' },
286
+ { key: '[TESTOMATIO-LINK-JIRA]', type: 'jira' },
287
+ ];
288
+ const filteredStdout = [];
289
+ stdout.forEach(entry => {
290
+ if (typeof entry !== 'string') {
291
+ filteredStdout.push(entry);
292
+ return;
293
+ }
294
+ // check if entry contains any of markers
295
+ if (!markers.some(m => entry.includes(m.key))) {
296
+ filteredStdout.push(entry);
297
+ return;
298
+ }
299
+ const newEntryLines = [];
300
+ entry.split('\n').forEach(line => {
301
+ line = line.trim();
302
+ let hasMarker = false;
303
+ for (const marker of markers) {
304
+ if (line.includes(marker.key)) {
305
+ hasMarker = true;
306
+ try {
307
+ const rawJson = line.split(marker.key)[1]?.trim();
308
+ if (!rawJson)
309
+ continue;
310
+ // smart JSON extraction: take until the last ']', otherwise take the whole string
311
+ const lastBracketIndex = rawJson.lastIndexOf(']');
312
+ const jsonStr = lastBracketIndex !== -1 ? rawJson.substring(0, lastBracketIndex + 1) : rawJson;
313
+ // test ids or jira ids
314
+ const ids = JSON.parse(jsonStr);
315
+ links.push(...ids
316
+ // filter non-truthy ids
317
+ .filter(id => !!id)
318
+ .map(id => ({
319
+ // marker type is either 'test' or 'jira'
320
+ [marker.type]: id,
321
+ })));
322
+ }
323
+ catch (e) {
324
+ debug('Error parsing links from string:', line, '\n', e);
325
+ }
326
+ }
327
+ }
328
+ if (!hasMarker && line) {
329
+ newEntryLines.push(line);
330
+ }
331
+ });
332
+ if (newEntryLines.length) {
333
+ filteredStdout.push(newEntryLines.join('\n'));
334
+ }
335
+ });
336
+ return {
337
+ stdout: filteredStdout,
338
+ links,
339
+ };
340
+ }
256
341
  module.exports = PlaywrightReporter;
257
342
 
258
343
  module.exports.extractTags = extractTags;
344
+
345
+ module.exports.fetchLinksFromLogs = fetchLinksFromLogs;
package/lib/bin/cli.js CHANGED
@@ -73,25 +73,25 @@ program
73
73
  .command('run')
74
74
  .alias('test')
75
75
  .description('Run tests with the specified command')
76
- .argument('<command>', 'Test runner command')
76
+ .argument('[command]', 'Test runner command')
77
77
  .option('--filter <filter>', 'Additional execution filter')
78
78
  .option('--filter-list <filter>', 'Get a list of all tests by filter before running')
79
79
  .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
80
80
  .action(async (command, opts) => {
81
81
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
82
82
  const title = process.env.TESTOMATIO_TITLE;
83
- if (!command || !command.split) {
84
- console.log(constants_js_1.APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
85
- return process.exit(255);
86
- }
87
83
  const client = new client_js_1.default({ apiKey, title });
88
84
  if (opts.filter || opts.filterList) {
85
+ console.log(constants_js_1.APP_PREFIX, 'Filtering tests...');
89
86
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
90
87
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
91
88
  // Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
92
89
  const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
93
90
  const pipeOptions = optsArray.join(':');
94
91
  const prepareRunParams = { pipe, pipeOptions };
92
+ if (opts.filterList) {
93
+ client.pipeStore.filterList = true;
94
+ }
95
95
  try {
96
96
  const tests = await client.prepareRun(prepareRunParams);
97
97
  if (!tests || tests.length === 0) {
@@ -103,16 +103,44 @@ program
103
103
  debug(`Execution pattern: "${pattern}"`);
104
104
  if (opts.filterList) {
105
105
  console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
106
- console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
106
+ if (command)
107
+ console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
107
108
  return;
108
109
  }
109
- command = filteredCommand;
110
+ if (command && command.split) {
111
+ command = filteredCommand;
112
+ }
110
113
  }
111
114
  catch (err) {
112
115
  console.log(constants_js_1.APP_PREFIX, err.message || err);
113
116
  return;
114
117
  }
115
118
  }
119
+ // just create a run (wich tests which match filters) without executing tests
120
+ if (!command || !command.split) {
121
+ const createRunParams = {};
122
+ if (title) {
123
+ createRunParams.title = title;
124
+ }
125
+ if (opts.kind) {
126
+ createRunParams.kind = opts.kind;
127
+ }
128
+ if (apiKey) {
129
+ await client.createRun(createRunParams);
130
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId;
131
+ if (client.pipeStore.runUrl)
132
+ console.log(constants_js_1.APP_PREFIX, `📊 Report URL: ${picocolors_1.default.magenta(client.pipeStore.runUrl)}`);
133
+ if (opts.kind !== 'manual') {
134
+ console.log(constants_js_1.APP_PREFIX, `No command passed, so you need to run tests yourself:`);
135
+ console.log(constants_js_1.APP_PREFIX, `TESTOMATIO_RUN=${runId} <command>`);
136
+ }
137
+ }
138
+ else {
139
+ console.log(constants_js_1.APP_PREFIX, '⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
140
+ process.exit(1);
141
+ }
142
+ return process.exit(0);
143
+ }
116
144
  console.log(constants_js_1.APP_PREFIX, `🚀 Running`, picocolors_1.default.green(command));
117
145
  const runTests = async () => {
118
146
  const testCmds = command.split(' ');
@@ -89,7 +89,7 @@ class CoveragePipe {
89
89
  }
90
90
  }
91
91
  });
92
- // In case if we have all needed data
92
+ // In case if we have all needed data
93
93
  this.isEnabled = true;
94
94
  debug('Coverage Pipe initialized', {
95
95
  branch: this.branch,
@@ -110,6 +110,9 @@ class CoveragePipe {
110
110
  this.suiteIds.clear();
111
111
  this.tagLabels.clear();
112
112
  this.results = [];
113
+ if (this.store) {
114
+ this.store.coverageConfiguration = undefined;
115
+ }
113
116
  if (!this.isEnabled)
114
117
  return [];
115
118
  // Step 1: Validate coverage file path & Git changes & Coverage parsing
@@ -117,6 +120,9 @@ class CoveragePipe {
117
120
  return [];
118
121
  // Step 2: Extract all available tests and compare with coverage file
119
122
  const lines = await this.extractRelevantTestsFromChanges();
123
+ if (this.store?.filterList && lines.size > 0) {
124
+ console.log(constants_js_1.APP_PREFIX, `Matched files: ${[...lines].join(', ')}`);
125
+ }
120
126
  if (lines.size === 0) {
121
127
  console.log(constants_js_1.APP_PREFIX, 'ℹ️ No matching entries in coverage file for provided Git changes.');
122
128
  return [];
@@ -134,11 +140,22 @@ class CoveragePipe {
134
140
  tests.forEach(testId => this.tests.add(testId));
135
141
  }
136
142
  }
137
- if (this.tests.size === 0) {
143
+ if (this.tests.size === 0 && this.suiteIds.size === 0) {
138
144
  console.log(constants_js_1.APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
139
145
  return [];
140
146
  }
141
- this.results = [...this.tests];
147
+ this.results = [...this.tests, ...this.suiteIds];
148
+ if (this.store) {
149
+ this.store.coverageConfiguration = {
150
+ tests: [...this.tests],
151
+ suites: [...this.suiteIds],
152
+ };
153
+ this.store.coverageDescription = this.#buildRunDescription({
154
+ matchedLines: lines,
155
+ testsCount: this.tests.size,
156
+ suitesCount: this.suiteIds.size,
157
+ });
158
+ }
142
159
  return this.results;
143
160
  }
144
161
  addTest(data) { }
@@ -230,7 +247,7 @@ class CoveragePipe {
230
247
  #buildGitCommand() {
231
248
  if (!this.branch)
232
249
  throw new Error(`❌ Invalid changes option for setted branch!`);
233
- return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
250
+ return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
234
251
  }
235
252
  /**
236
253
  * Retrieves the list of files changed in the current Git working directory
@@ -356,7 +373,7 @@ class CoveragePipe {
356
373
  }
357
374
  // Example: "@Sd74099c1"
358
375
  else if (id.startsWith('@S')) {
359
- this.tests.add(id.slice(1));
376
+ this.suiteIds.add(id.slice(1));
360
377
  }
361
378
  // Example: "tag:@TestSmoke"
362
379
  else if (id.startsWith('tag')) {
@@ -369,5 +386,46 @@ class CoveragePipe {
369
386
  debug(`Matched lines: ${this.matchedLines}`);
370
387
  return this.matchedLines;
371
388
  }
389
+ #buildRunDescription({ matchedLines, testsCount, suitesCount }) {
390
+ const sourceBranch = process.env.GITHUB_HEAD_REF ||
391
+ process.env.GITHUB_REF_NAME ||
392
+ process.env.CI_COMMIT_REF_NAME ||
393
+ this.#getCurrentGitBranch() ||
394
+ 'current branch';
395
+ const targetBranch = this.branch || 'target branch';
396
+ const coverageFile = this.coverageFilePath ? path_1.default.basename(this.coverageFilePath) : 'coverage.yml';
397
+ const updatedFiles = matchedLines && matchedLines.size > 0 ? [...matchedLines] : this.changedFiles;
398
+ let description = `Changes to **${updatedFiles.length}** files in ${sourceBranch} to ${targetBranch}.\n\n`;
399
+ if (suitesCount > 0 || testsCount > 0) {
400
+ const affectedItems = [];
401
+ if (suitesCount > 0)
402
+ affectedItems.push(`**${suitesCount} suites**`);
403
+ if (testsCount > 0)
404
+ affectedItems.push(`**${testsCount} individual tests**`);
405
+ description += `May affect ${affectedItems.join(' and ')} which are recommended to be tested for regression.\n\n`; // eslint-disable-line
406
+ }
407
+ description += 'Updated source files:\n';
408
+ if (updatedFiles.length) {
409
+ description += updatedFiles.map(file => `* \`${file}\``).join('\n');
410
+ description += '\n\n';
411
+ }
412
+ else {
413
+ description += '* No matched files found\n\n';
414
+ }
415
+ description += `Mapping source files to tests set via \`${coverageFile}\` file.`;
416
+ return description;
417
+ }
418
+ #getCurrentGitBranch() {
419
+ try {
420
+ const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
421
+ encoding: 'utf-8',
422
+ stdio: ['pipe', 'pipe', 'ignore'],
423
+ }).trim();
424
+ return branch || undefined;
425
+ }
426
+ catch (err) {
427
+ return undefined;
428
+ }
429
+ }
372
430
  }
373
431
  module.exports = CoveragePipe;
@@ -153,6 +153,21 @@ class GitHubPipe {
153
153
  return text;
154
154
  });
155
155
  let body = summary;
156
+ const coverageConfiguration = this.store?.coverageConfiguration;
157
+ const isManualRun = this.store?.runKind === 'manual';
158
+ if (isManualRun && coverageConfiguration) {
159
+ const testsCount = coverageConfiguration.tests?.length || 0;
160
+ const suitesCount = coverageConfiguration.suites?.length || 0;
161
+ body += '\n\n<details>\n<summary><h3>🧭 Coverage Scope</h3></summary>\n\n';
162
+ if (!testsCount && !suitesCount) {
163
+ body += '- No tests were affected, run disabled\n';
164
+ }
165
+ else {
166
+ body += `- Suites: ${suitesCount}\n`;
167
+ body += `- Tests: ${testsCount}\n`;
168
+ }
169
+ body += '\n</details>';
170
+ }
156
171
  if (failures.length) {
157
172
  body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
158
173
  if (failures.length > 20) {
@@ -178,6 +178,9 @@ class TestomatioPipe {
178
178
  return;
179
179
  if (this.batch.isEnabled && this.isEnabled)
180
180
  this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
181
+ if (this.store) {
182
+ this.store.runKind = params.kind;
183
+ }
181
184
  let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
182
185
  // GitHub Actions Url
183
186
  if (!buildUrl && process.env.GITHUB_RUN_ID) {
@@ -194,6 +197,16 @@ class TestomatioPipe {
194
197
  if (buildUrl && !buildUrl.startsWith('http'))
195
198
  buildUrl = undefined;
196
199
  const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
200
+ const coverageConfiguration = this.store?.coverageConfiguration;
201
+ let description = null;
202
+ let configuration = null;
203
+ if (coverageConfiguration && (coverageConfiguration.tests?.length || coverageConfiguration.suites?.length)) {
204
+ description = this.store?.coverageDescription || null;
205
+ configuration = {
206
+ tests: coverageConfiguration.tests?.map(id => id.replace(/^T/, '')) || [],
207
+ suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
208
+ };
209
+ }
197
210
  const runParams = Object.fromEntries(Object.entries({
198
211
  ci_build_url: buildUrl,
199
212
  api_key: this.apiKey.trim(),
@@ -206,6 +219,8 @@ class TestomatioPipe {
206
219
  shared_run: this.sharedRun,
207
220
  shared_run_timeout: this.sharedRunTimeout,
208
221
  kind: params.kind,
222
+ configuration,
223
+ description,
209
224
  }).filter(([, value]) => !!value));
210
225
  debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
211
226
  if (this.runId) {
@@ -250,8 +265,10 @@ class TestomatioPipe {
250
265
  console.error('Testomat.io API key is not set');
251
266
  if (!this.apiKey?.startsWith('tstmt'))
252
267
  console.error('Testomat.io API key is invalid');
268
+ if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG)
269
+ this.#logFailedResponse(err);
253
270
  console.error(constants_js_1.APP_PREFIX, 'Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report');
254
- printCreateIssue(err);
271
+ printCreateIssue();
255
272
  }
256
273
  debug('"createRun" function finished');
257
274
  }
@@ -295,16 +312,8 @@ class TestomatioPipe {
295
312
  this.requestFailures++;
296
313
  this.notReportedTestsCount++;
297
314
  if (err.response) {
298
- if (err.response.status >= 400) {
299
- const responseData = err.response.data || { message: '' };
300
- console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(`Warning: ${responseData.message} (${err.response.status})`), picocolors_1.default.gray(data?.title || ''));
301
- if (err.response?.data?.message?.includes('could not be matched')) {
302
- this.hasUnmatchedTests = true;
303
- }
304
- return;
305
- }
306
- console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`), `Report couldn't be processed: ${err?.response?.data?.message}`);
307
- printCreateIssue(err);
315
+ this.#logFailedResponse(err);
316
+ printCreateIssue();
308
317
  }
309
318
  else {
310
319
  console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(data?.title || ''), "Report couldn't be processed", err);
@@ -354,16 +363,8 @@ class TestomatioPipe {
354
363
  this.requestFailures++;
355
364
  this.notReportedTestsCount += testsToSend.length;
356
365
  if (err.response) {
357
- if (err.response.status >= 400) {
358
- const responseData = err.response.data || { message: '' };
359
- console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(`Warning: ${responseData.message} (${err.response.status})`));
360
- if (err.response?.data?.message?.includes('could not be matched')) {
361
- this.hasUnmatchedTests = true;
362
- }
363
- return;
364
- }
365
- console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow(`Warning: (${err.response?.status})`), `Report couldn't be processed: ${err?.response?.data?.message}`);
366
- printCreateIssue(err);
366
+ this.#logFailedResponse(err);
367
+ printCreateIssue();
367
368
  }
368
369
  else {
369
370
  console.log(constants_js_1.APP_PREFIX, "Report couldn't be processed", err);
@@ -462,32 +463,67 @@ class TestomatioPipe {
462
463
  }
463
464
  catch (err) {
464
465
  console.log(constants_js_1.APP_PREFIX, 'Error updating status, skipping...', err);
465
- printCreateIssue(err);
466
+ if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG)
467
+ this.#logFailedResponse(err);
468
+ printCreateIssue();
466
469
  }
467
470
  debug('Run finished');
468
471
  }
472
+ #logFailedResponse(error) {
473
+ let responseBody = stringify(error.response?.data ?? error.response ?? error, { pretty: true });
474
+ if (!responseBody)
475
+ responseBody = '<empty>';
476
+ responseBody = hideTestomatioToken(responseBody);
477
+ const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
478
+ const method = error.response?.config.method || '<unknown method>';
479
+ const url = error.response?.config.url || '<unknown url>';
480
+ let message = picocolors_1.default.yellow('\n⚠️ Request to Testomat.io failed:\n');
481
+ message += picocolors_1.default.bold(`${picocolors_1.default.red(statusCode)} ${method} ${url}\n`);
482
+ message += `\t${picocolors_1.default.bold('response: ')}${picocolors_1.default.gray(responseBody)}\n`;
483
+ const requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
484
+ if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG) {
485
+ message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(requestBody)}\n`;
486
+ }
487
+ else {
488
+ const requestBodyCut = requestBody.slice(0, 1000);
489
+ message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(`${requestBodyCut}.....`)}\n`;
490
+ message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
491
+ }
492
+ console.log(message);
493
+ if (error.response?.data?.message?.includes('could not be matched')) {
494
+ this.hasUnmatchedTests = true;
495
+ }
496
+ }
469
497
  toString() {
470
498
  return 'Testomatio Reporter';
471
499
  }
472
500
  }
473
501
  let registeredErrorHints = false;
474
- function printCreateIssue(err) {
502
+ function printCreateIssue() {
475
503
  if (registeredErrorHints)
476
504
  return;
477
505
  registeredErrorHints = true;
478
506
  process.on('exit', () => {
479
- console.log();
480
- console.log(constants_js_1.APP_PREFIX, 'There was an error reporting to Testomat.io:');
481
- console.log(constants_js_1.APP_PREFIX, 'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new');
482
- console.log(constants_js_1.APP_PREFIX, 'Provide this information:');
483
- console.log('Error:', err.message || err.code);
484
- if (!err.config)
485
- return;
486
- const time = new Date().toUTCString();
487
- const { body, url, baseURL, method } = err?.config || {};
488
- console.log('```js');
489
- console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
490
- console.log('```');
507
+ console.log(constants_js_1.APP_PREFIX, 'There was an error reporting to Testomat.io.\n', picocolors_1.default.yellow('If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new.'), picocolors_1.default.yellow('Provide the logs from above'));
491
508
  });
492
509
  }
510
+ /**
511
+ * Removes Testomatio token from string data
512
+ *
513
+ * @param {string} data
514
+ * @returns {string}
515
+ */
516
+ function hideTestomatioToken(data) {
517
+ return data.replace(/"api_key": "[^"]+"/g, '"api_key": "<hidden>"').replace(/"(tstmt_[^"]+)"/g, 'tstmt_***');
518
+ }
519
+ /**
520
+ * Stringifies provided data
521
+ *
522
+ * @param {any} anything
523
+ * @param {{ pretty: boolean }} opts
524
+ * @returns {string}
525
+ */
526
+ function stringify(anything, opts = { pretty: false }) {
527
+ return typeof anything === 'string' ? anything : JSON.stringify(anything, null, opts.pretty ? 2 : undefined);
528
+ }
493
529
  module.exports = TestomatioPipe;
@@ -73,6 +73,10 @@ function setLabel(key, value = null) {
73
73
  * @returns {void}
74
74
  */
75
75
  function linkTest(...testIds) {
76
+ if (helpers_js_1.isPlaywright) {
77
+ console.log(`[TESTOMATIO-LINK-TESTS] ${JSON.stringify(testIds)}`);
78
+ return;
79
+ }
76
80
  const links = testIds.map(testId => ({ test: testId }));
77
81
  index_js_1.services.links.put(links);
78
82
  }
@@ -82,6 +86,10 @@ function linkTest(...testIds) {
82
86
  * @returns {void}
83
87
  */
84
88
  function linkJira(...jiraIds) {
89
+ if (helpers_js_1.isPlaywright) {
90
+ console.log(`[TESTOMATIO-LINK-JIRA] ${JSON.stringify(jiraIds)}`);
91
+ return;
92
+ }
85
93
  const links = jiraIds.map(jiraId => ({ jira: jiraId }));
86
94
  index_js_1.services.links.put(links);
87
95
  }
@@ -15,8 +15,10 @@ declare class LinkStorage {
15
15
  /**
16
16
  * Returns links array for the test
17
17
  * @param {*} context testId or test context from test runner
18
- * @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
18
+ * @returns {{[key: 'test' | 'jira']: string}[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
19
19
  */
20
- get(context?: any): object[];
20
+ get(context?: any): {
21
+ [key: "test" | "jira"]: string;
22
+ }[];
21
23
  }
22
24
  export {};
@@ -32,7 +32,7 @@ class LinkStorage {
32
32
  /**
33
33
  * Returns links array for the test
34
34
  * @param {*} context testId or test context from test runner
35
- * @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
35
+ * @returns {{[key: 'test' | 'jira']: string}[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
36
36
  */
37
37
  get(context = null) {
38
38
  const linksList = data_storage_js_1.dataStorage.getData('links', context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"