@testomatio/reporter 2.8.0 → 2.8.2

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.
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CodeceptReporter = CodeceptReporter;
7
7
  const debug_1 = __importDefault(require("debug"));
8
+ const path_1 = __importDefault(require("path"));
8
9
  const picocolors_1 = __importDefault(require("picocolors"));
9
10
  const client_js_1 = __importDefault(require("../client.js"));
10
11
  const constants_js_1 = require("../constants.js");
@@ -47,8 +48,8 @@ function CodeceptReporter(config) {
47
48
  const reportTestPromises = [];
48
49
  let isRunFinalized = false;
49
50
  const testTimeMap = {};
50
- const { apiKey } = config;
51
- const client = new client_js_1.default({ apiKey });
51
+ const clientConfig = buildCodeceptClientConfig(config);
52
+ const client = new client_js_1.default(clientConfig);
52
53
  // Store original output methods for fallback
53
54
  const originalOutput = {
54
55
  debug: output.debug,
@@ -492,5 +493,40 @@ function formatHookStep(step) {
492
493
  return formattedStep;
493
494
  }
494
495
  module.exports = CodeceptReporter;
496
+ function buildCodeceptClientConfig(config = {}) {
497
+ const outputDir = resolveCodeceptOutputDir(config);
498
+ const reportDir = resolveCodeceptReportDir(config, outputDir);
499
+ return {
500
+ ...config,
501
+ apiKey: config.apiKey,
502
+ framework: 'codeceptjs',
503
+ outputDir,
504
+ reportDir,
505
+ html: config.html,
506
+ markdown: config.markdown,
507
+ csv: config.csv,
508
+ };
509
+ }
510
+ function resolveCodeceptOutputDir(config = {}) {
511
+ const codeceptStore = /** @type {{ outputDir?: string }} */ (codeceptjs_1.default.store || {});
512
+ const candidates = [
513
+ config.outputDir,
514
+ config.output,
515
+ codeceptStore.outputDir,
516
+ codecept?.config?.get?.()?.output,
517
+ codecept?.config?.output,
518
+ ];
519
+ const outputDir = candidates.find(value => typeof value === 'string' && value.trim());
520
+ return outputDir || 'output';
521
+ }
522
+ function resolveCodeceptReportDir(config = {}, outputDir = 'output') {
523
+ if (typeof config.reportDir === 'string' && config.reportDir.trim()) {
524
+ return config.reportDir;
525
+ }
526
+ if (path_1.default.isAbsolute(outputDir)) {
527
+ return path_1.default.join(outputDir, 'report');
528
+ }
529
+ return path_1.default.join(outputDir, 'report');
530
+ }
495
531
 
496
532
  module.exports.CodeceptReporter = CodeceptReporter;
package/lib/bin/cli.js CHANGED
@@ -103,9 +103,11 @@ program
103
103
  const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
104
104
  debug(`Execution pattern: "${pattern}"`);
105
105
  if (opts.filterList) {
106
- log_js_1.log.info(picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
107
106
  if (command)
108
107
  log_js_1.log.info(picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
108
+ console.log();
109
+ console.log(`Grep string:`);
110
+ console.log(`${tests.join(', ')}`);
109
111
  return;
110
112
  }
111
113
  if (command && command.split) {
package/lib/client.js CHANGED
@@ -105,8 +105,9 @@ class Client {
105
105
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
106
106
  */
107
107
  async createRun(params = {}) {
108
+ const pipeParams = { ...(this.paramsForPipesFactory || {}), ...(params || {}) };
108
109
  if (!this.pipes || !this.pipes.length)
109
- this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
110
+ this.pipes = await (0, index_js_1.pipesFactory)(pipeParams, this.pipeStore);
110
111
  debug('Creating run...');
111
112
  // all pipes disabled, skipping
112
113
  if (!this.pipes?.filter(p => p.isEnabled).length)
@@ -10,6 +10,7 @@ export class BitbucketPipe {
10
10
  store: {};
11
11
  tests: any[];
12
12
  token: any;
13
+ description: any;
13
14
  hiddenCommentData: string;
14
15
  client: Gaxios;
15
16
  cleanLog(log: any): Promise<string>;
@@ -63,6 +63,7 @@ class BitbucketPipe {
63
63
  this.tests = [];
64
64
  // Bitbucket PAT looks like bbpat-*****
65
65
  this.token = params.BITBUCKET_ACCESS_TOKEN || process.env.BITBUCKET_ACCESS_TOKEN || this.ENV.BITBUCKET_ACCESS_TOKEN;
66
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
66
67
  this.hiddenCommentData = `Testomat.io report: ${process.env.BITBUCKET_BRANCH || ''}`;
67
68
  debug(picocolors_1.default.yellow('Bitbucket Pipe:'), this.token ? 'TOKEN passed' : '*no token*', `Project key: ${this.ENV.BITBUCKET_PROJECT_KEY}, Pull request ID: ${this.ENV.BITBUCKET_PR_ID}`);
68
69
  if (!this.token) {
@@ -168,6 +169,9 @@ class BitbucketPipe {
168
169
  return text;
169
170
  });
170
171
  let body = summary;
172
+ if (this.description) {
173
+ body += `\n\n> ${(0, utils_js_1.truncate)(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
174
+ }
171
175
  if (failures.length) {
172
176
  body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
173
177
  if (failures.length > 10) {
package/lib/pipe/csv.d.ts CHANGED
@@ -12,9 +12,9 @@ declare class CsvPipe implements Pipe {
12
12
  store: any;
13
13
  title: any;
14
14
  results: any[];
15
- outputDir: string;
15
+ outputDir: any;
16
16
  defaultReportName: string;
17
- csvFilename: string;
17
+ csvFilename: any;
18
18
  isEnabled: boolean;
19
19
  outputFile: string;
20
20
  prepareRun(): Promise<void>;
@@ -23,7 +23,7 @@ declare class CsvPipe implements Pipe {
23
23
  /**
24
24
  * Create a folder that will contain the exported files
25
25
  */
26
- checkExportDir(): void;
26
+ checkExportDir(): string;
27
27
  /**
28
28
  * Save data to the csv file.
29
29
  * @param {Object} data - data that will be added to the CSV file.
package/lib/pipe/csv.js CHANGED
@@ -23,9 +23,12 @@ class CsvPipe {
23
23
  this.store = store || {};
24
24
  this.title = params.title || process.env.TESTOMATIO_TITLE;
25
25
  this.results = [];
26
- this.outputDir = 'export';
26
+ this.outputDir = params.reportDir || 'export';
27
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
28
+ this.outputDir = path_1.default.join(this.outputDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
29
+ }
27
30
  this.defaultReportName = 'report.csv';
28
- this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
31
+ this.csvFilename = resolveCsvFilename(params);
29
32
  this.isEnabled = false;
30
33
  if (this.csvFilename) {
31
34
  const filenameParts = this.csvFilename.split('.');
@@ -51,7 +54,7 @@ class CsvPipe {
51
54
  */
52
55
  checkExportDir() {
53
56
  if (!fs_1.default.existsSync(this.outputDir)) {
54
- return fs_1.default.mkdirSync(this.outputDir);
57
+ return fs_1.default.mkdirSync(this.outputDir, { recursive: true });
55
58
  }
56
59
  }
57
60
  /**
@@ -127,3 +130,14 @@ class CsvPipe {
127
130
  }
128
131
  }
129
132
  module.exports = CsvPipe;
133
+ function resolveCsvFilename(params = {}) {
134
+ if (typeof params.csvFilename === 'string' && params.csvFilename.trim()) {
135
+ return params.csvFilename;
136
+ }
137
+ if (typeof process.env.TESTOMATIO_CSV_FILENAME === 'string' && process.env.TESTOMATIO_CSV_FILENAME.trim()) {
138
+ return process.env.TESTOMATIO_CSV_FILENAME;
139
+ }
140
+ if (params.csv)
141
+ return 'report.csv';
142
+ return null;
143
+ }
@@ -13,6 +13,7 @@ declare class GitHubPipe implements Pipe {
13
13
  store: {};
14
14
  tests: any[];
15
15
  token: any;
16
+ description: any;
16
17
  ref: string;
17
18
  repo: string;
18
19
  jobKey: string;
@@ -58,6 +58,7 @@ class GitHubPipe {
58
58
  this.store = store;
59
59
  this.tests = [];
60
60
  this.token = params.GH_PAT || process.env.GH_PAT;
61
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
61
62
  this.ref = process.env.GITHUB_REF;
62
63
  this.repo = process.env.GITHUB_REPOSITORY;
63
64
  this.jobKey = `${process.env.GITHUB_WORKFLOW || ''} / ${process.env.GITHUB_JOB || ''}`;
@@ -154,6 +155,9 @@ class GitHubPipe {
154
155
  return text;
155
156
  });
156
157
  let body = summary;
158
+ if (this.description) {
159
+ body += `\n\n> ${(0, utils_js_1.truncate)(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
160
+ }
157
161
  const coverageConfiguration = this.store?.coverageConfiguration;
158
162
  const isManualRun = this.store?.runKind === 'manual';
159
163
  if (isManualRun && coverageConfiguration) {
@@ -13,6 +13,7 @@ declare class GitLabPipe {
13
13
  store: {};
14
14
  tests: any[];
15
15
  token: any;
16
+ description: any;
16
17
  hiddenCommentData: string;
17
18
  client: Gaxios;
18
19
  prepareRun(): Promise<void>;
@@ -29,6 +29,7 @@ class GitLabPipe {
29
29
  this.tests = [];
30
30
  // GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
31
31
  this.token = params.GITLAB_PAT || process.env.GITLAB_PAT || this.ENV.GITLAB_PAT;
32
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
32
33
  this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
33
34
  debug(picocolors_1.default.yellow('GitLab Pipe:'), this.token ? 'TOKEN passed' : '*no token*', `Project id: ${this.ENV.CI_PROJECT_ID}, MR id: ${this.ENV.CI_MERGE_REQUEST_IID}`);
34
35
  if (!this.ENV.CI_PROJECT_ID || !this.ENV.CI_MERGE_REQUEST_IID) {
@@ -116,6 +117,9 @@ class GitLabPipe {
116
117
  return text;
117
118
  });
118
119
  let body = summary;
120
+ if (this.description) {
121
+ body += `\n\n> ${(0, utils_js_1.truncate)(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
122
+ }
119
123
  if (failures.length) {
120
124
  body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
121
125
  if (failures.length > 20) {
@@ -3,14 +3,15 @@ declare class HtmlPipe {
3
3
  constructor(params: any, store?: {});
4
4
  store: {};
5
5
  title: any;
6
+ description: any;
6
7
  apiKey: any;
7
- isHtml: string;
8
+ isHtml: any;
8
9
  isEnabled: boolean;
9
10
  htmlOutputPath: string;
10
11
  filenameMsg: string;
11
12
  tests: any[];
12
13
  configuration: any;
13
- htmlReportDir: string;
14
+ htmlReportDir: any;
14
15
  htmlReportName: string;
15
16
  templateFolderPath: string;
16
17
  templateHtmlPath: string;
package/lib/pipe/html.js CHANGED
@@ -20,8 +20,9 @@ class HtmlPipe {
20
20
  constructor(params, store = {}) {
21
21
  this.store = store || {};
22
22
  this.title = params.title || process.env.TESTOMATIO_TITLE;
23
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
23
24
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
24
- this.isHtml = process.env.TESTOMATIO_HTML_REPORT_SAVE;
25
+ this.isHtml = params.html ?? process.env.TESTOMATIO_HTML_REPORT_SAVE;
25
26
  debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
26
27
  this.isEnabled = false;
27
28
  this.htmlOutputPath = '';
@@ -30,7 +31,10 @@ class HtmlPipe {
30
31
  this.configuration = null;
31
32
  if (this.isHtml) {
32
33
  this.isEnabled = true;
33
- this.htmlReportDir = process.env.TESTOMATIO_HTML_REPORT_FOLDER || constants_js_1.HTML_REPORT.FOLDER;
34
+ this.htmlReportDir = params.reportDir || process.env.TESTOMATIO_HTML_REPORT_FOLDER || constants_js_1.HTML_REPORT.FOLDER;
35
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
36
+ this.htmlReportDir = path_1.default.join(this.htmlReportDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
37
+ }
34
38
  if (process.env.TESTOMATIO_HTML_FILENAME && process.env.TESTOMATIO_HTML_FILENAME.endsWith('.html')) {
35
39
  this.htmlReportName = process.env.TESTOMATIO_HTML_FILENAME;
36
40
  }
@@ -204,7 +208,9 @@ class HtmlPipe {
204
208
  runUrl: this.store.runUrl || '',
205
209
  executionTime: testExecutionSumTime(aggregatedTests),
206
210
  executionDate: getCurrentDateTimeFormatted(),
207
- description: runParams.description || this.store.coverageDescription || this.store.description || '',
211
+ description: [runParams.description || this.store.coverageDescription || this.store.description, this.description]
212
+ .filter(Boolean)
213
+ .join('\n\n') || '',
208
214
  configuration: buildDisplayConfiguration(this.configuration || this.store.configuration || runParams.configuration || null),
209
215
  tests: aggregatedTests,
210
216
  envVars: collectEnvironmentVariables(),
@@ -3,14 +3,15 @@ declare class MarkdownPipe {
3
3
  constructor(params: any, store?: {});
4
4
  store: {};
5
5
  title: any;
6
+ description: any;
6
7
  apiKey: any;
7
- isMarkdown: string;
8
+ isMarkdown: any;
8
9
  isEnabled: boolean;
9
10
  markdownOutputPath: string;
10
11
  filenameMsg: string;
11
12
  tests: any[];
12
13
  configuration: any;
13
- markdownReportDir: string;
14
+ markdownReportDir: any;
14
15
  markdownReportName: string;
15
16
  createRun(params?: {}): Promise<void>;
16
17
  prepareRun(): Promise<void>;
@@ -17,8 +17,9 @@ class MarkdownPipe {
17
17
  constructor(params, store = {}) {
18
18
  this.store = store || {};
19
19
  this.title = params.title || process.env.TESTOMATIO_TITLE;
20
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
20
21
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
21
- this.isMarkdown = process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE;
22
+ this.isMarkdown = params.markdown ?? process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE;
22
23
  debug('Markdown Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
23
24
  this.isEnabled = false;
24
25
  this.markdownOutputPath = '';
@@ -28,7 +29,11 @@ class MarkdownPipe {
28
29
  if (!this.isMarkdown)
29
30
  return;
30
31
  this.isEnabled = true;
31
- this.markdownReportDir = process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER || constants_js_1.MARKDOWN_REPORT.FOLDER;
32
+ this.markdownReportDir =
33
+ params.reportDir || process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER || constants_js_1.MARKDOWN_REPORT.FOLDER;
34
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
35
+ this.markdownReportDir = path_1.default.join(this.markdownReportDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
36
+ }
32
37
  const envName = process.env.TESTOMATIO_MARKDOWN_FILENAME;
33
38
  if (envName && envName.endsWith('.md')) {
34
39
  this.markdownReportName = envName;
@@ -112,7 +117,9 @@ class MarkdownPipe {
112
117
  isParallel: runParams?.isParallel,
113
118
  executionTime: testExecutionSumTime(aggregated),
114
119
  executionDate: getCurrentDateTimeFormatted(),
115
- description: runParams?.description || this.store.coverageDescription || this.store.description || '',
120
+ description: [runParams?.description || this.store.coverageDescription || this.store.description, this.description]
121
+ .filter(Boolean)
122
+ .join('\n\n') || '',
116
123
  configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
117
124
  tests: aggregated,
118
125
  stats,
@@ -30,6 +30,7 @@ declare class TestomatioPipe implements Pipe {
30
30
  groupTitle: any;
31
31
  env: string;
32
32
  label: string;
33
+ description: any;
33
34
  client: Gaxios;
34
35
  proceed: boolean;
35
36
  jiraId: string;
@@ -67,6 +67,7 @@ class TestomatioPipe {
67
67
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
68
68
  this.env = process.env.TESTOMATIO_ENV;
69
69
  this.label = process.env.TESTOMATIO_LABEL;
70
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
70
71
  // Create a new instance of gaxios with a custom config
71
72
  this.client = new gaxios_1.Gaxios({
72
73
  baseURL: `${this.url.trim()}`,
@@ -193,15 +194,17 @@ class TestomatioPipe {
193
194
  buildUrl = undefined;
194
195
  const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
195
196
  const coverageConfiguration = this.store?.coverageConfiguration;
196
- let description = null;
197
+ let coverageDescription = null;
197
198
  let configuration = null;
198
199
  if (coverageConfiguration && (coverageConfiguration.tests?.length || coverageConfiguration.suites?.length)) {
199
- description = this.store?.coverageDescription || null;
200
+ coverageDescription = this.store?.coverageDescription || null;
200
201
  configuration = {
201
202
  tests: coverageConfiguration.tests?.map(id => id.replace(/^T/, '')) || [],
202
203
  suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
203
204
  };
204
205
  }
206
+ // Run description: coverage-derived block (if any) with the user-provided TESTOMATIO_DESCRIPTION appended after it.
207
+ const description = [coverageDescription, this.description].filter(Boolean).join('\n\n') || null;
205
208
  // Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
206
209
  // Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
207
210
  if (params.configuration && typeof params.configuration === 'object') {
@@ -509,11 +512,19 @@ class TestomatioPipe {
509
512
  const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
510
513
  const method = error.response?.config?.method || '<unknown method>';
511
514
  const url = String(error.response?.config?.url || '<unknown url>');
515
+ const statusText = error.response?.statusText || '';
512
516
  let message = picocolors_1.default.yellow('⚠️ Request to Testomat.io failed:\n');
513
517
  message += picocolors_1.default.bold(`${picocolors_1.default.red(statusCode)} ${method} ${picocolors_1.default.gray(url)}\n`);
518
+ const apiMessage = error.response?.data?.message;
514
519
  if (statusCode === 403) {
515
520
  message += `\t${picocolors_1.default.red('Please check your API token. It might be invalid or expired.')}\n`;
516
521
  }
522
+ else if (apiMessage) {
523
+ message += `\t${picocolors_1.default.red(apiMessage)}\n`;
524
+ }
525
+ else if (statusText) {
526
+ message += `\t${picocolors_1.default.red(statusText)}\n`;
527
+ }
517
528
  message += `\t${picocolors_1.default.bold('response: ')}${picocolors_1.default.gray(responseBody)}\n`;
518
529
  const requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
519
530
  if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -1,4 +1,5 @@
1
1
  import createDebugMessages from 'debug';
2
+ import path from 'path';
2
3
  import pc from 'picocolors';
3
4
  import TestomatClient from '../client.js';
4
5
  import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR, SCREENSHOTS_ON_STEPS } from '../constants.js';
@@ -53,9 +54,8 @@ function CodeceptReporter(config) {
53
54
  let isRunFinalized = false;
54
55
 
55
56
  const testTimeMap = {};
56
- const { apiKey } = config;
57
-
58
- const client = new TestomatClient({ apiKey });
57
+ const clientConfig = buildCodeceptClientConfig(config);
58
+ const client = new TestomatClient(clientConfig);
59
59
 
60
60
  // Store original output methods for fallback
61
61
  const originalOutput = {
@@ -577,3 +577,45 @@ function formatHookStep(step) {
577
577
 
578
578
  export { CodeceptReporter };
579
579
  export default CodeceptReporter;
580
+
581
+ function buildCodeceptClientConfig(config = {}) {
582
+ const outputDir = resolveCodeceptOutputDir(config);
583
+ const reportDir = resolveCodeceptReportDir(config, outputDir);
584
+
585
+ return {
586
+ ...config,
587
+ apiKey: config.apiKey,
588
+ framework: 'codeceptjs',
589
+ outputDir,
590
+ reportDir,
591
+ html: config.html,
592
+ markdown: config.markdown,
593
+ csv: config.csv,
594
+ };
595
+ }
596
+
597
+ function resolveCodeceptOutputDir(config = {}) {
598
+ const codeceptStore = /** @type {{ outputDir?: string }} */ (codeceptjs.store || {});
599
+ const candidates = [
600
+ config.outputDir,
601
+ config.output,
602
+ codeceptStore.outputDir,
603
+ codecept?.config?.get?.()?.output,
604
+ codecept?.config?.output,
605
+ ];
606
+
607
+ const outputDir = candidates.find(value => typeof value === 'string' && value.trim());
608
+ return outputDir || 'output';
609
+ }
610
+
611
+ function resolveCodeceptReportDir(config = {}, outputDir = 'output') {
612
+ if (typeof config.reportDir === 'string' && config.reportDir.trim()) {
613
+ return config.reportDir;
614
+ }
615
+
616
+ if (path.isAbsolute(outputDir)) {
617
+ return path.join(outputDir, 'report');
618
+ }
619
+
620
+ return path.join(outputDir, 'report');
621
+ }
package/src/bin/cli.js CHANGED
@@ -116,8 +116,10 @@ program
116
116
  debug(`Execution pattern: "${pattern}"`);
117
117
 
118
118
  if(opts.filterList) {
119
- log.info( pc.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
120
- if (command) log.info( pc.green(`Full Running Command: ${filteredCommand}`));
119
+ if (command) log.info(pc.green(`Full Running Command: ${filteredCommand}`));
120
+ console.log();
121
+ console.log(`Grep string:`);
122
+ console.log(`${tests.join(', ')}`);
121
123
  return;
122
124
  }
123
125
 
package/src/client.js CHANGED
@@ -116,8 +116,9 @@ class Client {
116
116
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
117
117
  */
118
118
  async createRun(params = {}) {
119
+ const pipeParams = { ...(this.paramsForPipesFactory || {}), ...(params || {}) };
119
120
  if (!this.pipes || !this.pipes.length)
120
- this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
121
+ this.pipes = await pipesFactory(pipeParams, this.pipeStore);
121
122
  debug('Creating run...');
122
123
  // all pipes disabled, skipping
123
124
  if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
@@ -1,5 +1,5 @@
1
1
  import { APP_PREFIX, testomatLogoURL } from '../constants.js';
2
- import { ansiRegExp, isSameTest } from '../utils/utils.js';
2
+ import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
3
3
  import { statusEmoji, fullName } from '../utils/pipe_utils.js';
4
4
  import { Gaxios } from 'gaxios';
5
5
  import pc from 'picocolors';
@@ -27,6 +27,7 @@ export class BitbucketPipe {
27
27
  this.tests = [];
28
28
  // Bitbucket PAT looks like bbpat-*****
29
29
  this.token = params.BITBUCKET_ACCESS_TOKEN || process.env.BITBUCKET_ACCESS_TOKEN || this.ENV.BITBUCKET_ACCESS_TOKEN;
30
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
30
31
  this.hiddenCommentData = `Testomat.io report: ${process.env.BITBUCKET_BRANCH || ''}`;
31
32
 
32
33
  debug(
@@ -170,6 +171,10 @@ export class BitbucketPipe {
170
171
 
171
172
  let body = summary;
172
173
 
174
+ if (this.description) {
175
+ body += `\n\n> ${truncate(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
176
+ }
177
+
173
178
  if (failures.length) {
174
179
  body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
175
180
  if (failures.length > 10) {
package/src/pipe/csv.js CHANGED
@@ -20,9 +20,12 @@ class CsvPipe {
20
20
  this.title = params.title || process.env.TESTOMATIO_TITLE;
21
21
  this.results = [];
22
22
 
23
- this.outputDir = 'export';
23
+ this.outputDir = params.reportDir || 'export';
24
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
25
+ this.outputDir = path.join(this.outputDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
26
+ }
24
27
  this.defaultReportName = 'report.csv';
25
- this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
28
+ this.csvFilename = resolveCsvFilename(params);
26
29
  this.isEnabled = false;
27
30
 
28
31
  if (this.csvFilename) {
@@ -57,7 +60,7 @@ class CsvPipe {
57
60
  */
58
61
  checkExportDir() {
59
62
  if (!fs.existsSync(this.outputDir)) {
60
- return fs.mkdirSync(this.outputDir);
63
+ return fs.mkdirSync(this.outputDir, { recursive: true });
61
64
  }
62
65
  }
63
66
 
@@ -142,3 +145,17 @@ class CsvPipe {
142
145
  }
143
146
 
144
147
  export default CsvPipe;
148
+
149
+ function resolveCsvFilename(params = {}) {
150
+ if (typeof params.csvFilename === 'string' && params.csvFilename.trim()) {
151
+ return params.csvFilename;
152
+ }
153
+
154
+ if (typeof process.env.TESTOMATIO_CSV_FILENAME === 'string' && process.env.TESTOMATIO_CSV_FILENAME.trim()) {
155
+ return process.env.TESTOMATIO_CSV_FILENAME;
156
+ }
157
+
158
+ if (params.csv) return 'report.csv';
159
+
160
+ return null;
161
+ }
@@ -4,7 +4,7 @@ import pc from 'picocolors';
4
4
  import humanizeDuration from 'humanize-duration';
5
5
  import merge from 'lodash.merge';
6
6
  import { testomatLogoURL } from '../constants.js';
7
- import { ansiRegExp, isSameTest } from '../utils/utils.js';
7
+ import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
8
8
  import { statusEmoji, fullName } from '../utils/pipe_utils.js';
9
9
  import { log } from '../utils/log.js';
10
10
 
@@ -22,6 +22,7 @@ class GitHubPipe {
22
22
  this.store = store;
23
23
  this.tests = [];
24
24
  this.token = params.GH_PAT || process.env.GH_PAT;
25
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
25
26
  this.ref = process.env.GITHUB_REF;
26
27
  this.repo = process.env.GITHUB_REPOSITORY;
27
28
  this.jobKey = `${process.env.GITHUB_WORKFLOW || ''} / ${process.env.GITHUB_JOB || ''}`;
@@ -143,6 +144,9 @@ class GitHubPipe {
143
144
  });
144
145
 
145
146
  let body = summary;
147
+ if (this.description) {
148
+ body += `\n\n> ${truncate(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
149
+ }
146
150
  const coverageConfiguration = this.store?.coverageConfiguration;
147
151
  const isManualRun = this.store?.runKind === 'manual';
148
152
  if (isManualRun && coverageConfiguration) {
@@ -5,7 +5,7 @@ import humanizeDuration from 'humanize-duration';
5
5
  import merge from 'lodash.merge';
6
6
  import path from 'path';
7
7
  import { APP_PREFIX, testomatLogoURL } from '../constants.js';
8
- import { ansiRegExp, isSameTest } from '../utils/utils.js';
8
+ import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
9
9
  import { statusEmoji, fullName } from '../utils/pipe_utils.js';
10
10
  import { log } from '../utils/log.js';
11
11
 
@@ -27,6 +27,7 @@ class GitLabPipe {
27
27
  this.tests = [];
28
28
  // GitLab PAT looks like glpat-nKGdja3jsG4850sGksh7
29
29
  this.token = params.GITLAB_PAT || process.env.GITLAB_PAT || this.ENV.GITLAB_PAT;
30
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
30
31
  this.hiddenCommentData = `<!--- testomat.io report ${process.env.CI_JOB_NAME || ''} -->`;
31
32
 
32
33
  debug(
@@ -145,6 +146,10 @@ class GitLabPipe {
145
146
 
146
147
  let body = summary;
147
148
 
149
+ if (this.description) {
150
+ body += `\n\n> ${truncate(this.description, 1024).replace(/\r?\n/g, '\n> ')}`;
151
+ }
152
+
148
153
  if (failures.length) {
149
154
  body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
150
155
  if (failures.length > 20) {
package/src/pipe/html.js CHANGED
@@ -19,8 +19,9 @@ class HtmlPipe {
19
19
  constructor(params, store = {}) {
20
20
  this.store = store || {};
21
21
  this.title = params.title || process.env.TESTOMATIO_TITLE;
22
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
22
23
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
23
- this.isHtml = process.env.TESTOMATIO_HTML_REPORT_SAVE;
24
+ this.isHtml = params.html ?? process.env.TESTOMATIO_HTML_REPORT_SAVE;
24
25
 
25
26
  debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
26
27
 
@@ -32,7 +33,10 @@ class HtmlPipe {
32
33
 
33
34
  if (this.isHtml) {
34
35
  this.isEnabled = true;
35
- this.htmlReportDir = process.env.TESTOMATIO_HTML_REPORT_FOLDER || HTML_REPORT.FOLDER;
36
+ this.htmlReportDir = params.reportDir || process.env.TESTOMATIO_HTML_REPORT_FOLDER || HTML_REPORT.FOLDER;
37
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
38
+ this.htmlReportDir = path.join(this.htmlReportDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
39
+ }
36
40
 
37
41
  if (process.env.TESTOMATIO_HTML_FILENAME && process.env.TESTOMATIO_HTML_FILENAME.endsWith('.html')) {
38
42
  this.htmlReportName = process.env.TESTOMATIO_HTML_FILENAME;
@@ -254,7 +258,10 @@ class HtmlPipe {
254
258
  runUrl: this.store.runUrl || '',
255
259
  executionTime: testExecutionSumTime(aggregatedTests),
256
260
  executionDate: getCurrentDateTimeFormatted(),
257
- description: runParams.description || this.store.coverageDescription || this.store.description || '',
261
+ description:
262
+ [runParams.description || this.store.coverageDescription || this.store.description, this.description]
263
+ .filter(Boolean)
264
+ .join('\n\n') || '',
258
265
  configuration: buildDisplayConfiguration(
259
266
  this.configuration || this.store.configuration || runParams.configuration || null,
260
267
  ),
@@ -15,8 +15,9 @@ class MarkdownPipe {
15
15
  constructor(params, store = {}) {
16
16
  this.store = store || {};
17
17
  this.title = params.title || process.env.TESTOMATIO_TITLE;
18
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
18
19
  this.apiKey = params.apiKey || process.env.TESTOMATIO;
19
- this.isMarkdown = process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE;
20
+ this.isMarkdown = params.markdown ?? process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE;
20
21
 
21
22
  debug('Markdown Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
22
23
 
@@ -29,7 +30,11 @@ class MarkdownPipe {
29
30
  if (!this.isMarkdown) return;
30
31
 
31
32
  this.isEnabled = true;
32
- this.markdownReportDir = process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER || MARKDOWN_REPORT.FOLDER;
33
+ this.markdownReportDir =
34
+ params.reportDir || process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER || MARKDOWN_REPORT.FOLDER;
35
+ if (process.env.TESTOMATIO_RUNGROUP_TITLE) {
36
+ this.markdownReportDir = path.join(this.markdownReportDir, process.env.TESTOMATIO_RUNGROUP_TITLE);
37
+ }
33
38
 
34
39
  const envName = process.env.TESTOMATIO_MARKDOWN_FILENAME;
35
40
  if (envName && envName.endsWith('.md')) {
@@ -134,7 +139,10 @@ class MarkdownPipe {
134
139
  isParallel: runParams?.isParallel,
135
140
  executionTime: testExecutionSumTime(aggregated),
136
141
  executionDate: getCurrentDateTimeFormatted(),
137
- description: runParams?.description || this.store.coverageDescription || this.store.description || '',
142
+ description:
143
+ [runParams?.description || this.store.coverageDescription || this.store.description, this.description]
144
+ .filter(Boolean)
145
+ .join('\n\n') || '',
138
146
  configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
139
147
  tests: aggregated,
140
148
  stats,
@@ -143,7 +151,6 @@ class MarkdownPipe {
143
151
  const md = renderDocument(data);
144
152
 
145
153
  fs.writeFileSync(outputPath, md, 'utf-8');
146
-
147
154
  if (fs.existsSync(outputPath)) {
148
155
  const absolutePath = path.resolve(outputPath);
149
156
  const fileUrlPath = fileUrl(absolutePath, { resolve: true });
@@ -83,6 +83,7 @@ class TestomatioPipe {
83
83
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
84
84
  this.env = process.env.TESTOMATIO_ENV;
85
85
  this.label = process.env.TESTOMATIO_LABEL;
86
+ this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
86
87
 
87
88
  // Create a new instance of gaxios with a custom config
88
89
  this.client = new Gaxios({
@@ -227,15 +228,17 @@ class TestomatioPipe {
227
228
  const accessEvent = process.env.TESTOMATIO_PUBLISH ? 'publish' : null;
228
229
 
229
230
  const coverageConfiguration = this.store?.coverageConfiguration;
230
- let description = null;
231
+ let coverageDescription = null;
231
232
  let configuration = null;
232
233
  if (coverageConfiguration && (coverageConfiguration.tests?.length || coverageConfiguration.suites?.length)) {
233
- description = this.store?.coverageDescription || null;
234
+ coverageDescription = this.store?.coverageDescription || null;
234
235
  configuration = {
235
236
  tests: coverageConfiguration.tests?.map(id => id.replace(/^T/, '')) || [],
236
237
  suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
237
238
  };
238
239
  }
240
+ // Run description: coverage-derived block (if any) with the user-provided TESTOMATIO_DESCRIPTION appended after it.
241
+ const description = [coverageDescription, this.description].filter(Boolean).join('\n\n') || null;
239
242
 
240
243
  // Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
241
244
  // Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
@@ -576,12 +579,18 @@ class TestomatioPipe {
576
579
  const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
577
580
  const method = error.response?.config?.method || '<unknown method>';
578
581
  const url = String(error.response?.config?.url || '<unknown url>');
582
+ const statusText = error.response?.statusText || '';
579
583
 
580
584
  let message = pc.yellow('⚠️ Request to Testomat.io failed:\n');
581
585
  message += pc.bold(`${pc.red(statusCode)} ${method} ${pc.gray(url)}\n`);
582
586
 
587
+ const apiMessage = error.response?.data?.message;
583
588
  if (statusCode === 403) {
584
589
  message += `\t${pc.red('Please check your API token. It might be invalid or expired.')}\n`;
590
+ } else if (apiMessage) {
591
+ message += `\t${pc.red(apiMessage)}\n`;
592
+ } else if (statusText) {
593
+ message += `\t${pc.red(statusText)}\n`;
585
594
  }
586
595
 
587
596
  message += `\t${pc.bold('response: ')}${pc.gray(responseBody)}\n`;
package/types/types.d.ts CHANGED
@@ -267,6 +267,9 @@ export interface RunData {
267
267
  /** If duration is pre-set value as in XML tests set it */
268
268
  duration?: number;
269
269
 
270
+ /** Free-form run description (from `TESTOMATIO_DESCRIPTION`); appended to any coverage-derived description. */
271
+ description?: string;
272
+
270
273
  /**
271
274
  * An array of `TestData` objects representing the individual test cases in the test run.
272
275
  * Used for JUNit report when we don't send the tests in realtime but in a batch as a part of final result */