@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +40 -11
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +57 -152
  7. package/lib/data-storage.d.ts +1 -1
  8. package/lib/helpers.d.ts +1 -0
  9. package/lib/helpers.js +4 -0
  10. package/lib/junit-adapter/csharp.d.ts +0 -1
  11. package/lib/junit-adapter/csharp.js +43 -7
  12. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  13. package/lib/junit-adapter/nunit-parser.js +433 -0
  14. package/lib/pipe/bitbucket.js +5 -5
  15. package/lib/pipe/coverage.d.ts +82 -0
  16. package/lib/pipe/coverage.js +373 -0
  17. package/lib/pipe/gitlab.js +4 -4
  18. package/lib/pipe/index.js +2 -0
  19. package/lib/pipe/testomatio.d.ts +3 -2
  20. package/lib/pipe/testomatio.js +44 -18
  21. package/lib/reporter-functions.js +14 -12
  22. package/lib/reporter.d.ts +31 -21
  23. package/lib/reporter.js +40 -5
  24. package/lib/services/artifacts.d.ts +1 -1
  25. package/lib/services/key-values.d.ts +1 -1
  26. package/lib/services/links.d.ts +1 -1
  27. package/lib/services/logger.d.ts +1 -1
  28. package/lib/uploader.js +4 -0
  29. package/lib/utils/log-formatter.d.ts +28 -0
  30. package/lib/utils/log-formatter.js +127 -0
  31. package/lib/utils/pipe_utils.d.ts +15 -0
  32. package/lib/utils/pipe_utils.js +44 -2
  33. package/lib/utils/utils.d.ts +6 -0
  34. package/lib/utils/utils.js +260 -25
  35. package/lib/xmlReader.d.ts +32 -26
  36. package/lib/xmlReader.js +121 -52
  37. package/package.json +12 -7
  38. package/src/adapter/codecept.js +19 -19
  39. package/src/adapter/mocha.js +1 -1
  40. package/src/adapter/playwright.js +2 -2
  41. package/src/bin/cli.js +51 -13
  42. package/src/bin/reportXml.js +5 -2
  43. package/src/client.js +69 -130
  44. package/src/helpers.js +1 -0
  45. package/src/junit-adapter/csharp.js +48 -6
  46. package/src/junit-adapter/nunit-parser.js +474 -0
  47. package/src/pipe/bitbucket.js +5 -5
  48. package/src/pipe/coverage.js +440 -0
  49. package/src/pipe/debug.js +1 -2
  50. package/src/pipe/gitlab.js +4 -4
  51. package/src/pipe/index.js +2 -0
  52. package/src/pipe/testomatio.js +109 -85
  53. package/src/reporter-functions.js +15 -12
  54. package/src/reporter.js +6 -4
  55. package/src/services/links.js +1 -1
  56. package/src/uploader.js +5 -0
  57. package/src/utils/log-formatter.js +113 -0
  58. package/src/utils/pipe_utils.js +52 -3
  59. package/src/utils/utils.js +277 -22
  60. package/src/xmlReader.js +144 -46
  61. package/types/types.d.ts +364 -0
  62. package/types/vitest.types.d.ts +93 -0
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ const child_process_1 = require("child_process");
10
+ const gaxios_1 = require("gaxios");
11
+ const minimatch_1 = require("minimatch");
12
+ const constants_js_1 = require("../constants.js");
13
+ const pipe_utils_js_1 = require("../utils/pipe_utils.js");
14
+ const pipe_utils_js_2 = require("../utils/pipe_utils.js");
15
+ const config_js_1 = require("../config.js");
16
+ const debug_1 = __importDefault(require("debug"));
17
+ const debug = (0, debug_1.default)('@testomatio/reporter:pipe:csv');
18
+ // Example of use 'coverage:file=coverage/coverage.yml,diff=master' cmd:
19
+ // | Option | Git command | Notes |
20
+ // | --- | --- | --- |
21
+ // | --filter "coverage:file=coverage/coverage.yml,diff=new-branch" | ✅ git diff new-branch --name-only | - |
22
+ // | --filter "coverage:diff=master,file=coverage.yml" | ✅ git diff master --name-only | - |
23
+ // | --filter "coverage:file=coverage/coverage.yml" | ✅ git diff master --name-only | default branch = "master" |
24
+ // | --filter "coverage:file=coverage.yml,dif=noexist-branch" | ❌ Git command failed ...| - |
25
+ // | --filter "coverage:file=coverage.yml,diff=noexist-branch" | ❌ Git command failed ...| because no branch found |
26
+ // | --filter "coverage:file=no-exist-coverage.yml" | ❌ Coverage file not found: <>filename>.yml | - |
27
+ // | --filter "coverage:filepath=coverage.yml" | 🚫 Missing required parameter: "file"... | - |
28
+ // | --filter "coverage:diff=my-branch" | 🚫 Missing required parameter: "file"...| - |
29
+ //maybe GitChanges or GitCoverage
30
+ class CoveragePipe {
31
+ #GIT = {
32
+ default_branch: 'master',
33
+ diff_command: 'git diff',
34
+ only_file_opt: '--name-only',
35
+ uncommitted_marker: 'uncommitted',
36
+ // test_defaultGitChangedFile - uses only for unit tests in "coverage_pipe_test.js" file
37
+ test_defaultGitChangedFile: ['todomvc-tests/edit-todos_test.js'],
38
+ };
39
+ constructor(params, store) {
40
+ this.id = 'coverage'; // as future updates -> find by id in client.js
41
+ this.store = store || {};
42
+ this.branch = undefined;
43
+ this.isDefaultGitChanges = false; // COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
44
+ this.isEnabled = false;
45
+ const { pipeOptions } = params;
46
+ const options = (0, pipe_utils_js_2.parsePipeOptions)(pipeOptions || "");
47
+ debug("Pipe options", options);
48
+ // this.isDefaultGitChanges - COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
49
+ this.isDefaultGitChanges = process.env.COVERAGE_BY_DEFAULT_GIT_FILE === '1' ? true : false;
50
+ this.coverageFilePath = options?.file || process.env.COVERAGE_FILEPATH || undefined;
51
+ if (!this.coverageFilePath)
52
+ return;
53
+ this.branch = options?.diff || process.env.COVERAGE_BRANCH || this.#GIT.default_branch;
54
+ this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
55
+ if (this.isBranchDefault) {
56
+ console.log(constants_js_1.APP_PREFIX, `🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
57
+ '👉 You can set it via --filter "coverage:file=coverage.yml,diff=your-branch"');
58
+ }
59
+ // Client config section
60
+ this.formattedDate = new Date().toISOString().replace(/T/, '-').replace(/:/g, '-').split('.')[0];
61
+ this.title = process.env.TESTOMATIO_TITLE || `Testomatio Coverage Test Execution - ${this.formattedDate}`;
62
+ this.apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
63
+ this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
64
+ const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || null;
65
+ const proxy = proxyUrl ? new URL(proxyUrl) : null;
66
+ // Create a new instance of gaxios with a custom config
67
+ this.client = new gaxios_1.Gaxios({
68
+ baseURL: `${this.url.trim()}`,
69
+ timeout: constants_js_1.AXIOS_TIMEOUT,
70
+ proxy: proxy ? proxy.toString() : undefined,
71
+ retry: true,
72
+ retryConfig: {
73
+ retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
74
+ retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
75
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
76
+ shouldRetry: (error) => {
77
+ if (!error.response)
78
+ return false;
79
+ switch (error.response?.status) {
80
+ case 400: // Bad request (probably wrong API key)
81
+ case 404: // Test not matched
82
+ case 429: // Rate limit exceeded
83
+ case 500: // Internal server error
84
+ return false;
85
+ default:
86
+ break;
87
+ }
88
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
89
+ }
90
+ }
91
+ });
92
+ // In case if we have all needed data
93
+ this.isEnabled = true;
94
+ debug('Coverage Pipe initialized', {
95
+ branch: this.branch,
96
+ coverageFilePath: this.coverageFilePath,
97
+ });
98
+ this.parsedCoverage = {};
99
+ this.changedFiles = [];
100
+ this.matchedLines = new Set();
101
+ this.tests = new Set();
102
+ this.suiteIds = new Set();
103
+ this.tagLabels = new Set();
104
+ this.results = [];
105
+ debug(`Coverage Pipe: is Enabled = ${this.isEnabled}`);
106
+ }
107
+ async prepareRun(opts) {
108
+ // Reset internal mutable state for isolation
109
+ this.tests.clear();
110
+ this.suiteIds.clear();
111
+ this.tagLabels.clear();
112
+ this.results = [];
113
+ if (!this.isEnabled)
114
+ return [];
115
+ // Step 1: Validate coverage file path & Git changes & Coverage parsing
116
+ if (!this.getGitChangedFiles()?.validateCoverageFile()?.parseCoverageFile())
117
+ return [];
118
+ // Step 2: Extract all available tests and compare with coverage file
119
+ const lines = await this.extractRelevantTestsFromChanges();
120
+ if (lines.size === 0) {
121
+ console.log(constants_js_1.APP_PREFIX, 'ℹ️ No matching entries in coverage file for provided Git changes.');
122
+ return [];
123
+ }
124
+ // Step 3: Handle tag labels tests from the server
125
+ // if (this.tagLabels && this.tagLabels.size > 0) { //TODO: in case if we add labels in future!!!
126
+ if (this.tagLabels.size > 0) {
127
+ for (const tag of this.tagLabels) {
128
+ const tagType = 'tag';
129
+ const tests = await this.#getTestomatioTestsByParam(tagType, tag);
130
+ if (!tests)
131
+ return [];
132
+ console.log(constants_js_1.APP_PREFIX, `✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
133
+ ' in Testomat.io service side.');
134
+ tests.forEach(testId => this.tests.add(testId));
135
+ }
136
+ }
137
+ if (this.tests.size === 0) {
138
+ console.log(constants_js_1.APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
139
+ return [];
140
+ }
141
+ this.results = [...this.tests];
142
+ return this.results;
143
+ }
144
+ addTest(data) { }
145
+ async createRun() { }
146
+ updateRun() { }
147
+ async finishRun(runParams) { }
148
+ toString() {
149
+ return 'Coverage Reporter';
150
+ }
151
+ /**
152
+ * Fetches a list of tests from the Testomat.io server based on a given filter parameter.
153
+ *
154
+ * The method parses the provided filter (`type=id`), builds the appropriate request parameters,
155
+ * sends a GET request to the Testomat.io `/api/test_grep` endpoint, and returns the matching tests.
156
+ *
157
+ * If the filter is invalid, no query is generated, or the server responds with no matching tests,
158
+ * it logs relevant information and returns `undefined`.
159
+ *
160
+ * @async function
161
+ * @param {string} type - The filter string in the format like `tag-name` for tag by.
162
+ * @param {string} id - The filter string in the format like `smoke`.
163
+ * @returns {Promise<Array<Object>|undefined>} Resolves to an array of test objects if found, otherwise `undefined`.
164
+ */
165
+ async #getTestomatioTestsByParam(type, id) {
166
+ // Get tests from the server
167
+ try {
168
+ const q = (0, pipe_utils_js_1.generateFilterRequestParams)({
169
+ type,
170
+ id,
171
+ apiKey: this?.apiKey?.trim(),
172
+ });
173
+ if (!q) {
174
+ return;
175
+ }
176
+ const resp = await this.client.request({
177
+ method: 'GET',
178
+ url: '/api/test_grep',
179
+ ...q,
180
+ });
181
+ if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
182
+ console.log(constants_js_1.APP_PREFIX, `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
183
+ return undefined;
184
+ }
185
+ return resp.data.tests;
186
+ }
187
+ catch (err) {
188
+ console.error(constants_js_1.APP_PREFIX, `🚩 Error getting available tests from the Testomat.io by "test_grep" option: ${err}`);
189
+ return undefined;
190
+ }
191
+ }
192
+ /**
193
+ * Executes a Git command to retrieve a list of changed files.
194
+ *
195
+ * @param {string} cmd - The Git command to execute.
196
+ * @returns {string[]} An array of changed file paths. Returns an empty array if an error occurs
197
+ * (e.g., not a Git repository or command failure).
198
+ */
199
+ #getChangedFilesFromGit(cmd) {
200
+ try {
201
+ const result = (0, child_process_1.execSync)(cmd, {
202
+ encoding: 'utf-8',
203
+ stdio: ['pipe', 'pipe', 'ignore']
204
+ });
205
+ return result
206
+ .split('\n')
207
+ .map(f => f.trim())
208
+ .filter(Boolean);
209
+ }
210
+ catch (err) {
211
+ const errorMessage = err.message || '';
212
+ // Git edge: Not a git repository or other error
213
+ if (errorMessage.includes('Not a git repository')) {
214
+ console.error(constants_js_1.APP_PREFIX, '❌ Error: This folder is not a Git repository.');
215
+ }
216
+ else {
217
+ throw new Error(`❌ Git command failed ("${cmd}"):\n`, errorMessage);
218
+ }
219
+ return [];
220
+ }
221
+ }
222
+ /**
223
+ * Builds a Git command string to list file changes between the current state
224
+ * and a specified Git branch using `git diff --name-only`.
225
+ *
226
+ * Private pipe function
227
+ * @throws {Error} Throws an error if `this.branch` is not defined.
228
+ * @returns {string} A Git command string, e.g., 'git diff <branch> --name-only'.
229
+ */
230
+ #buildGitCommand() {
231
+ if (!this.branch)
232
+ throw new Error(`❌ Invalid changes option for setted branch!`);
233
+ return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
234
+ }
235
+ /**
236
+ * Retrieves the list of files changed in the current Git working directory
237
+ * compared to a specified branch.
238
+ *
239
+ * This method builds a Git diff command and attempts to retrieve the changed
240
+ * files using that command. It logs helpful information and errors during the process.
241
+ *
242
+ * If no changed files are found, or an error occurs at any stage, the method logs
243
+ * the issue and returns `undefined`.
244
+ *
245
+ * @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
246
+ * otherwise, returns `undefined`.
247
+ */
248
+ getGitChangedFiles() {
249
+ let cmd;
250
+ try {
251
+ cmd = this.#buildGitCommand();
252
+ }
253
+ catch (err) {
254
+ console.error(constants_js_1.APP_PREFIX, err.message);
255
+ return undefined;
256
+ }
257
+ console.error(constants_js_1.APP_PREFIX, `ℹ️ We will use '${cmd}' Git command.`);
258
+ try {
259
+ // For clear unit testing process -> Like test_defaultGitChangedFile = todomvc-tests/edit-todos_test.js
260
+ if (this.isDefaultGitChanges) {
261
+ this.changedFiles = this.#GIT.test_defaultGitChangedFile;
262
+ }
263
+ else {
264
+ this.changedFiles = this.#getChangedFilesFromGit(cmd);
265
+ if (this.changedFiles.length === 0) {
266
+ console.log(constants_js_1.APP_PREFIX, 'ℹ️ No files changed in the latest Git commit. Skipping coverage processing.');
267
+ return undefined;
268
+ }
269
+ }
270
+ }
271
+ catch (err) {
272
+ console.error(constants_js_1.APP_PREFIX, err.message);
273
+ console.error(constants_js_1.APP_PREFIX, "🔍 Pls, check this Git command manually to understand the original problem.");
274
+ return undefined;
275
+ }
276
+ console.log(constants_js_1.APP_PREFIX, `📑 GIT changed files:\n - ${this.changedFiles.join('\n - ')}`);
277
+ return this;
278
+ }
279
+ /**
280
+ * Validates the coverage file path (stored in `this.coverageFilePath`).
281
+ *
282
+ * This method checks:
283
+ * - That the file exists on disk.
284
+ * - That it is a regular file (not a directory or special file).
285
+ * - That it has a `.yml` extension to ensure it's a YAML file.
286
+ *
287
+ * Logs descriptive error messages for any failures.
288
+ *
289
+ * @returns {this | undefined} "true" in case if coverage file is valid and we can keep going;
290
+ * otherwise, `undefined`.
291
+ */
292
+ validateCoverageFile() {
293
+ // Validate the presence of the coverage filepath
294
+ if (!fs_1.default.existsSync(this.coverageFilePath)) {
295
+ console.log(constants_js_1.APP_PREFIX, '❌ Coverage file not found:', this.coverageFilePath);
296
+ return undefined;
297
+ }
298
+ // Ensure the given path is a file (not a directory or other type)
299
+ const stat = fs_1.default.statSync(this.coverageFilePath);
300
+ if (!stat.isFile()) {
301
+ console.log(constants_js_1.APP_PREFIX, '❌ Provided coverage path is not a file:', this.coverageFilePath);
302
+ return undefined;
303
+ }
304
+ // Validate the file extension to be ".yml" to ensure it's a YAML file
305
+ if (path_1.default.extname(this.coverageFilePath) !== ".yml") {
306
+ console.log(constants_js_1.APP_PREFIX, '❌ Coverage file must have a .yml extension:', this.coverageFilePath);
307
+ return undefined;
308
+ }
309
+ debug('Coverage file validation is OK!');
310
+ return this;
311
+ }
312
+ /**
313
+ * Parses the YAML coverage file (located at `this.coverageFilePath`) into a JavaScript object.
314
+ *
315
+ * - Reads the file content using UTF-8 encoding.
316
+ * - Parses it as YAML using the `yaml` library.
317
+ * - Stores the result in `this.parsedCoverage`.
318
+ * - If parsing fails, logs an error and returns `undefined`.
319
+ *
320
+ * @returns {this | undefined} The current parsed coverage yml file change lines, or "undefined" if parsing fails.
321
+ */
322
+ parseCoverageFile() {
323
+ try {
324
+ // Read the contents of the YAML file and attempt to parse the YAML into a JavaScript object
325
+ const rawYml = fs_1.default.readFileSync(this.coverageFilePath, 'utf8');
326
+ this.parsedCoverage = js_yaml_1.default.load(rawYml) || {};
327
+ debug(`Coverage filepath = ${this.coverageFilePath})`);
328
+ console.log(constants_js_1.APP_PREFIX, `✅ Coverage file parsed successfully: ${this.coverageFilePath}`);
329
+ return this;
330
+ }
331
+ catch (err) {
332
+ console.error(constants_js_1.APP_PREFIX, '❌ Failed to parse YAML:', err.message);
333
+ return undefined;
334
+ }
335
+ }
336
+ /**
337
+ * Extracts relevant test identifiers from changed files based on coverage mapping.
338
+ *
339
+ * Iterates over changed files and matches them against patterns in the parsed coverage data.
340
+ * For each match, it extracts test IDs or tags and stores them in corresponding sets:
341
+ * - Test IDs (starting with '@T' or '@S') are stored in `this.tests`
342
+ * - Tag labels (starting with 'tag:') are stored in `this.tagLabels`
343
+ * - Matched file paths are stored in `this.matchedLines`
344
+ *
345
+ * @returns {Promise <Set<string>>} A set of file paths that matched coverage patterns (`this.matchedLines`).
346
+ */
347
+ async extractRelevantTestsFromChanges() {
348
+ for (const changedFile of this.changedFiles) {
349
+ for (const [pattern, ids] of Object.entries(this.parsedCoverage)) {
350
+ if ((0, minimatch_1.minimatch)(changedFile, pattern)) {
351
+ this.matchedLines.add(changedFile);
352
+ ids.forEach(id => {
353
+ // Example: "@Tt74099t1"
354
+ if (id.startsWith('@T')) {
355
+ this.tests.add(id.slice(1));
356
+ }
357
+ // Example: "@Sd74099c1"
358
+ else if (id.startsWith('@S')) {
359
+ this.tests.add(id.slice(1));
360
+ }
361
+ // Example: "tag:@TestSmoke"
362
+ else if (id.startsWith('tag')) {
363
+ this.tagLabels.add(id.split(':')[1].slice(1));
364
+ }
365
+ });
366
+ }
367
+ }
368
+ }
369
+ debug(`Matched lines: ${this.matchedLines}`);
370
+ return this.matchedLines;
371
+ }
372
+ }
373
+ module.exports = CoveragePipe;
@@ -43,7 +43,7 @@ class GitLabPipe {
43
43
  baseURL: 'https://gitlab.com/api/v4',
44
44
  headers: {
45
45
  'Content-Type': 'application/json',
46
- }
46
+ },
47
47
  });
48
48
  debug('GitLab Pipe: Enabled');
49
49
  }
@@ -142,7 +142,7 @@ class GitLabPipe {
142
142
  method: 'POST',
143
143
  url: commentsRequestURL,
144
144
  params: { access_token: this.token },
145
- data: { body }
145
+ data: { body },
146
146
  });
147
147
  const commentID = addCommentResponse.data.id;
148
148
  // eslint-disable-next-line max-len
@@ -169,7 +169,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
169
169
  const response = await client.request({
170
170
  method: 'GET',
171
171
  url: commentsRequestURL,
172
- params: { access_token: token }
172
+ params: { access_token: token },
173
173
  });
174
174
  comments = response.data;
175
175
  }
@@ -187,7 +187,7 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
187
187
  await client.request({
188
188
  method: 'DELETE',
189
189
  url: deleteCommentURL,
190
- params: { access_token: token }
190
+ params: { access_token: token },
191
191
  });
192
192
  }
193
193
  catch (e) {
package/lib/pipe/index.js CHANGED
@@ -46,6 +46,7 @@ const github_js_1 = __importDefault(require("./github.js"));
46
46
  const gitlab_js_1 = __importDefault(require("./gitlab.js"));
47
47
  const csv_js_1 = __importDefault(require("./csv.js"));
48
48
  const html_js_1 = __importDefault(require("./html.js"));
49
+ const coverage_js_1 = __importDefault(require("./coverage.js"));
49
50
  const bitbucket_js_1 = require("./bitbucket.js");
50
51
  const debug_js_1 = require("./debug.js");
51
52
  async function pipesFactory(params, opts) {
@@ -83,6 +84,7 @@ async function pipesFactory(params, opts) {
83
84
  new csv_js_1.default(params, opts),
84
85
  new html_js_1.default(params, opts),
85
86
  new bitbucket_js_1.BitbucketPipe(params, opts),
87
+ new coverage_js_1.default(params, opts),
86
88
  new debug_js_1.DebugPipe(params, opts),
87
89
  ...extraPipes,
88
90
  ];
@@ -26,7 +26,7 @@ declare class TestomatioPipe implements Pipe {
26
26
  store: any;
27
27
  title: any;
28
28
  sharedRun: boolean;
29
- sharedRunTimeout: boolean;
29
+ sharedRunTimeout: number;
30
30
  groupTitle: any;
31
31
  env: string;
32
32
  label: string;
@@ -47,11 +47,12 @@ declare class TestomatioPipe implements Pipe {
47
47
  prepareRun(opts: any): Promise<string[]>;
48
48
  /**
49
49
  * Creates a new run on Testomat.io
50
- * @param {{isBatchEnabled?: boolean}} params
50
+ * @param {{isBatchEnabled?: boolean, kind?: string}} params
51
51
  * @returns Promise<void>
52
52
  */
53
53
  createRun(params?: {
54
54
  isBatchEnabled?: boolean;
55
+ kind?: string;
55
56
  }): Promise<void>;
56
57
  runUrl: string;
57
58
  runPublicUrl: any;
@@ -46,7 +46,24 @@ class TestomatioPipe {
46
46
  this.store = store || {};
47
47
  this.title = params.title || process.env.TESTOMATIO_TITLE;
48
48
  this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
49
- this.sharedRunTimeout = !!process.env.TESTOMATIO_SHARED_RUN_TIMEOUT;
49
+ this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
50
+ ? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
51
+ : undefined;
52
+ if (this.sharedRunTimeout && !this.sharedRun) {
53
+ debug('Auto-enabling sharedRun because sharedRunTimeout is set');
54
+ this.sharedRun = true;
55
+ }
56
+ if (!this.title && (this.sharedRun || this.sharedRunTimeout)) {
57
+ const sha = (0, utils_js_1.getGitCommitSha)();
58
+ if (sha) {
59
+ this.title = `Shared Run - ${sha}`;
60
+ console.log(constants_js_1.APP_PREFIX, `🔄 Auto-generated title for shared run: ${this.title}`);
61
+ }
62
+ else {
63
+ console.log(constants_js_1.APP_PREFIX, picocolors_1.default.red('Failed to resolve git commit SHA for shared run title.'));
64
+ console.log(constants_js_1.APP_PREFIX, 'Please run the tests inside a Git repository or set TESTOMATIO_TITLE explicitly.');
65
+ }
66
+ }
50
67
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
51
68
  this.env = process.env.TESTOMATIO_ENV;
52
69
  this.label = process.env.TESTOMATIO_LABEL;
@@ -60,7 +77,7 @@ class TestomatioPipe {
60
77
  retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
61
78
  retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
62
79
  httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
63
- shouldRetry: (error) => {
80
+ shouldRetry: error => {
64
81
  if (!error.response)
65
82
  return false;
66
83
  switch (error.response?.status) {
@@ -73,8 +90,8 @@ class TestomatioPipe {
73
90
  break;
74
91
  }
75
92
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- }
77
- }
93
+ },
94
+ },
78
95
  });
79
96
  this.isEnabled = true;
80
97
  // do not finish this run (for parallel testing)
@@ -121,15 +138,19 @@ class TestomatioPipe {
121
138
  async prepareRun(opts) {
122
139
  if (!this.isEnabled)
123
140
  return [];
124
- const { type, id } = (0, pipe_utils_js_1.parseFilterParams)(opts);
141
+ const clearOptions = (0, pipe_utils_js_1.parseFilterParams)(opts);
142
+ if (!clearOptions) {
143
+ return [];
144
+ }
145
+ const { type, id } = clearOptions;
125
146
  try {
126
147
  const q = (0, pipe_utils_js_1.generateFilterRequestParams)({
127
148
  type,
128
149
  id,
129
- apiKey: this.apiKey.trim(),
150
+ apiKey: this?.apiKey?.trim(),
130
151
  });
131
152
  if (!q) {
132
- return;
153
+ return [];
133
154
  }
134
155
  const resp = await this.client.request({
135
156
  method: 'GET',
@@ -148,7 +169,7 @@ class TestomatioPipe {
148
169
  }
149
170
  /**
150
171
  * Creates a new run on Testomat.io
151
- * @param {{isBatchEnabled?: boolean}} params
172
+ * @param {{isBatchEnabled?: boolean, kind?: string}} params
152
173
  * @returns Promise<void>
153
174
  */
154
175
  async createRun(params = {}) {
@@ -184,6 +205,7 @@ class TestomatioPipe {
184
205
  label: this.label,
185
206
  shared_run: this.sharedRun,
186
207
  shared_run_timeout: this.sharedRunTimeout,
208
+ kind: params.kind,
187
209
  }).filter(([, value]) => !!value));
188
210
  debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
189
211
  if (this.runId) {
@@ -193,7 +215,7 @@ class TestomatioPipe {
193
215
  method: 'PUT',
194
216
  url: `/api/reporter/${this.runId}`,
195
217
  data: runParams,
196
- responseType: 'json'
218
+ responseType: 'json',
197
219
  });
198
220
  if (resp.data.artifacts)
199
221
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
@@ -206,7 +228,7 @@ class TestomatioPipe {
206
228
  url: '/api/reporter',
207
229
  data: runParams,
208
230
  maxContentLength: Infinity,
209
- responseType: 'json'
231
+ responseType: 'json',
210
232
  });
211
233
  this.runId = resp.data.uid;
212
234
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -259,15 +281,17 @@ class TestomatioPipe {
259
281
  this.#formatData(data);
260
282
  const json = json_cycle_1.default.stringify(data);
261
283
  debug('Adding test', json);
262
- return this.client.request({
284
+ return this.client
285
+ .request({
263
286
  method: 'POST',
264
287
  url: `/api/reporter/${this.runId}/testrun`,
265
288
  data: json,
266
289
  headers: {
267
290
  'Content-Type': 'application/json',
268
291
  },
269
- maxContentLength: Infinity
270
- }).catch(err => {
292
+ maxContentLength: Infinity,
293
+ })
294
+ .catch(err => {
271
295
  this.requestFailures++;
272
296
  this.notReportedTestsCount++;
273
297
  if (err.response) {
@@ -312,19 +336,21 @@ class TestomatioPipe {
312
336
  // get tests from batch and clear batch
313
337
  const testsToSend = this.batch.tests.splice(0);
314
338
  debug('📨 Batch upload', testsToSend.length, 'tests');
315
- return this.client.request({
339
+ return this.client
340
+ .request({
316
341
  method: 'POST',
317
342
  url: `/api/reporter/${this.runId}/testrun`,
318
343
  data: {
319
344
  api_key: this.apiKey,
320
345
  tests: testsToSend,
321
- batch_index: this.batch.batchIndex
346
+ batch_index: this.batch.batchIndex,
322
347
  },
323
348
  headers: {
324
349
  'Content-Type': 'application/json',
325
350
  },
326
- maxContentLength: Infinity
327
- }).catch(err => {
351
+ maxContentLength: Infinity,
352
+ })
353
+ .catch(err => {
328
354
  this.requestFailures++;
329
355
  this.notReportedTestsCount += testsToSend.length;
330
356
  if (err.response) {
@@ -408,7 +434,7 @@ class TestomatioPipe {
408
434
  status_event,
409
435
  detach: params.detach,
410
436
  tests: params.tests,
411
- }
437
+ },
412
438
  });
413
439
  if (this.runUrl) {
414
440
  console.log(constants_js_1.APP_PREFIX, '📊 Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const helpers_js_1 = require("./helpers.js");
3
4
  const index_js_1 = require("./services/index.js");
4
5
  /**
5
6
  * Stores path to file as artifact and uploads it to the S3 storage
@@ -8,9 +9,7 @@ const index_js_1 = require("./services/index.js");
8
9
  * @returns {void}
9
10
  */
10
11
  function saveArtifact(data, context = null) {
11
- if (process.env.IS_PLAYWRIGHT)
12
- throw new Error(`This function is not available in Playwright framework.
13
- /Playwright supports artifacts out of the box`);
12
+ showPlaywrightWarning('artifact', 'Playwright supports artifacts out of the box.');
14
13
  if (!data)
15
14
  return;
16
15
  index_js_1.services.artifacts.put(data, context);
@@ -21,8 +20,6 @@ function saveArtifact(data, context = null) {
21
20
  * @returns {void}
22
21
  */
23
22
  function logMessage(...args) {
24
- if (process.env.IS_PLAYWRIGHT)
25
- throw new Error('This function is not available in Playwright framework');
26
23
  index_js_1.services.logger._templateLiteralLog(...args);
27
24
  }
28
25
  /**
@@ -31,9 +28,11 @@ function logMessage(...args) {
31
28
  * @returns {void}
32
29
  */
33
30
  function addStep(message) {
34
- if (process.env.IS_PLAYWRIGHT)
35
- throw new Error('This function is not available in Playwright framework. Use playwright steps');
36
31
  index_js_1.services.logger.step(message);
32
+ // this is done because Playwright reporter intercepts console logs and then we gather them and show on Testomat
33
+ // if not console.log, the step message will be lost from reporter
34
+ if (helpers_js_1.isPlaywright)
35
+ console.log(`Step: ${message}`);
37
36
  }
38
37
  /**
39
38
  * Add key-value pair(s) to the test report
@@ -42,8 +41,7 @@ function addStep(message) {
42
41
  * @returns {void}
43
42
  */
44
43
  function setKeyValue(keyValue, value = null) {
45
- if (process.env.IS_PLAYWRIGHT)
46
- throw new Error('This function is not available in Playwright framework. Use test tag instead.');
44
+ showPlaywrightWarning('meta', 'Use test annotations instead.');
47
45
  if (typeof keyValue === 'string') {
48
46
  keyValue = { [keyValue]: value };
49
47
  }
@@ -56,12 +54,11 @@ function setKeyValue(keyValue, value = null) {
56
54
  * @returns {void}
57
55
  */
58
56
  function setLabel(key, value = null) {
57
+ showPlaywrightWarning('label', 'Use test tag instead.');
59
58
  if (Array.isArray(value)) {
60
59
  return value.forEach(label => setLabel(key, label));
61
60
  }
62
- const labelObject = value !== null && value !== undefined && value !== ''
63
- ? { label: `${key}:${value}` }
64
- : { label: key };
61
+ const labelObject = value !== null && value !== undefined && value !== '' ? { label: `${key}:${value}` } : { label: key };
65
62
  index_js_1.services.links.put([labelObject]);
66
63
  }
67
64
  /**
@@ -82,6 +79,11 @@ function linkJira(...jiraIds) {
82
79
  const links = jiraIds.map(jiraId => ({ jira: jiraId }));
83
80
  index_js_1.services.links.put(links);
84
81
  }
82
+ function showPlaywrightWarning(functionName, recommendation) {
83
+ if (helpers_js_1.isPlaywright) {
84
+ console.warn(`[TESTOMATIO] '${functionName}' function is not supported for Playwright. ${recommendation}`);
85
+ }
86
+ }
85
87
  module.exports = {
86
88
  artifact: saveArtifact,
87
89
  log: logMessage,