@testomatio/reporter 2.5.2 → 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.
- package/lib/adapter/playwright.d.ts +12 -0
- package/lib/adapter/playwright.js +85 -4
- package/lib/bin/cli.js +35 -7
- package/lib/pipe/coverage.js +63 -5
- package/lib/pipe/github.js +15 -0
- package/lib/pipe/testomatio.js +71 -35
- package/lib/reporter-functions.js +8 -0
- package/lib/services/links.d.ts +4 -2
- package/lib/services/links.js +1 -1
- package/package.json +1 -1
- package/src/adapter/playwright.js +95 -5
- package/src/bin/cli.js +37 -11
- package/src/pipe/coverage.js +90 -32
- package/src/pipe/github.js +14 -0
- package/src/pipe/testomatio.js +86 -52
- package/src/reporter-functions.js +8 -0
- package/src/services/links.js +1 -1
- package/types/types.d.ts +1 -1
|
@@ -18,4 +18,16 @@ declare class PlaywrightReporter {
|
|
|
18
18
|
* @returns {string[]} - array of normalized tags with @ prefix
|
|
19
19
|
*/
|
|
20
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
|
+
};
|
|
21
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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 {{
|
|
@@ -259,6 +273,73 @@ function extractTags(test) {
|
|
|
259
273
|
function getTestContextName(test) {
|
|
260
274
|
return `${test._requireFile || ''}_${test.title}`;
|
|
261
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
|
+
}
|
|
262
341
|
module.exports = PlaywrightReporter;
|
|
263
342
|
|
|
264
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('
|
|
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
|
-
|
|
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
|
|
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(' ');
|
package/lib/pipe/coverage.js
CHANGED
|
@@ -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.
|
|
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;
|
package/lib/pipe/github.js
CHANGED
|
@@ -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) {
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/lib/services/links.d.ts
CHANGED
|
@@ -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 {
|
|
18
|
+
* @returns {{[key: 'test' | 'jira']: string}[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
|
|
19
19
|
*/
|
|
20
|
-
get(context?: any):
|
|
20
|
+
get(context?: any): {
|
|
21
|
+
[key: "test" | "jira"]: string;
|
|
22
|
+
}[];
|
|
21
23
|
}
|
|
22
24
|
export {};
|
package/lib/services/links.js
CHANGED
|
@@ -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 {
|
|
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);
|