@testomatio/reporter 2.1.3-beta.3-multi-links → 2.3.0-beta.1-links

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 (40) hide show
  1. package/lib/adapter/codecept.js +5 -3
  2. package/lib/adapter/cucumber/current.js +2 -0
  3. package/lib/adapter/jest.js +2 -0
  4. package/lib/adapter/mocha.js +14 -0
  5. package/lib/adapter/playwright.js +2 -0
  6. package/lib/adapter/webdriver.js +8 -9
  7. package/lib/bin/startTest.js +38 -91
  8. package/lib/client.js +4 -32
  9. package/lib/data-storage.d.ts +4 -4
  10. package/lib/data-storage.js +11 -12
  11. package/lib/pipe/testomatio.js +3 -3
  12. package/lib/reporter-functions.d.ts +26 -6
  13. package/lib/reporter-functions.js +36 -35
  14. package/lib/reporter.d.ts +10 -8
  15. package/lib/reporter.js +9 -7
  16. package/lib/services/index.d.ts +2 -2
  17. package/lib/services/index.js +2 -2
  18. package/lib/services/labels.d.ts +0 -22
  19. package/lib/services/labels.js +0 -62
  20. package/lib/services/links.d.ts +1 -1
  21. package/lib/utils/utils.js +3 -1
  22. package/package.json +1 -1
  23. package/src/adapter/codecept.js +6 -4
  24. package/src/adapter/cucumber/current.js +2 -0
  25. package/src/adapter/jest.js +2 -0
  26. package/src/adapter/mocha.js +15 -0
  27. package/src/adapter/playwright.js +2 -0
  28. package/src/adapter/webdriver.js +8 -9
  29. package/src/bin/startTest.js +43 -114
  30. package/src/client.js +4 -32
  31. package/src/data-storage.js +11 -14
  32. package/src/pipe/testomatio.js +3 -3
  33. package/src/reporter-functions.js +36 -38
  34. package/src/reporter.js +8 -6
  35. package/src/services/index.js +2 -2
  36. package/src/services/labels.js +0 -58
  37. package/src/services/links.js +69 -0
  38. package/src/utils/utils.js +5 -3
  39. package/lib/utils/cli_utils.d.ts +0 -1
  40. package/lib/utils/cli_utils.js +0 -524304
@@ -1,124 +1,53 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'cross-spawn';
3
- import { Command } from 'commander';
4
- import pc from 'picocolors';
5
- import TestomatClient from '../client.js';
6
- import { APP_PREFIX, STATUS } from '../constants.js';
2
+ import { spawn } from 'node:child_process';
3
+ import { join, dirname } from 'node:path';
7
4
  import { getPackageVersion } from '../utils/utils.js';
8
- import { config } from '../config.js';
9
- import dotenv from 'dotenv';
5
+ import pc from 'picocolors';
6
+
7
+ // Define __dirname - this will be replaced by build script with actual __dirname for CommonJS
8
+ const __dirname = typeof globalThis.__dirname !== 'undefined' ? globalThis.__dirname : '.';
9
+ const cliPath = join(__dirname, 'cli.js');
10
10
 
11
11
  const version = getPackageVersion();
12
12
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
13
- const program = new Command();
14
-
15
- program
16
- .option('-c, --command <cmd>', 'Test runner command')
17
- .option('--launch', 'Start a new run and return its ID')
18
- .option('--finish', 'Finish Run by its ID')
19
- .option('--env-file <envfile>', 'Load environment variables from env file')
20
- .option('--filter <filter>', 'Additional execution filter')
21
- .action(async opts => {
22
- const { launch, finish, filter } = opts;
23
- let { command } = opts;
24
-
25
- if (opts.envFile) dotenv.config({ path: opts.envFile });
26
-
27
- const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
28
- const title = process.env.TESTOMATIO_TITLE;
29
-
30
- if (launch) {
31
- console.log('Starting a new Run on Testomat.io...');
32
- const client = new TestomatClient({ apiKey });
33
-
34
- client.createRun().then(() => {
35
- console.log(process.env.runId);
36
- process.exit(0);
37
- });
38
- return;
39
- }
40
-
41
- if (finish) {
42
- // TODO: add error in case of TESTOMATIO environment variable is not set
43
- // because command is fine in console, but actually (on testomat.io) run is not finished
44
- if (!process.env.TESTOMATIO_RUN) {
45
- console.log('TESTOMATIO_RUN environment variable must be set.');
46
- return process.exit(1);
47
- }
48
-
49
- console.log('Finishing Run on Testomat.io...');
50
-
51
- const client = new TestomatClient({ apiKey });
52
13
 
53
- // @ts-ignore
54
- client.updateRunStatus(STATUS.FINISHED).then(() => {
55
- console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
56
- process.exit(0);
57
- });
58
- return;
14
+ // Parse command line arguments to map start-test-run options to @testomatio/reporter run format
15
+ const args = process.argv.slice(2);
16
+ const newArgs = ['run'];
17
+
18
+ let i = 0;
19
+ while (i < args.length) {
20
+ const arg = args[i];
21
+
22
+ if (arg === '-c' || arg === '--command') {
23
+ // Map -c/--command to positional argument for run command
24
+ i++;
25
+ if (i < args.length) {
26
+ newArgs.push(args[i]);
59
27
  }
28
+ } else if (arg.startsWith('--command=')) {
29
+ // Handle --command=value format
30
+ const command = arg.split('=', 2)[1];
31
+ newArgs.push(command);
32
+ } else if (arg === '--launch') {
33
+ // Map --launch to start command
34
+ newArgs[0] = 'start';
35
+ } else if (arg === '--finish') {
36
+ // Map --finish to finish command
37
+ newArgs[0] = 'finish';
38
+ } else {
39
+ // Pass through other arguments
40
+ newArgs.push(arg);
41
+ }
42
+ i++;
43
+ }
60
44
 
61
- let exitCode = 0;
62
-
63
- if (!command.split) {
64
- process.exitCode = 255;
65
- console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
66
- return;
67
- }
68
-
69
- const client = new TestomatClient({ apiKey, title, parallel: true });
70
-
71
- if (filter) {
72
- const [pipe, ...optsArray] = filter.split(':');
73
- const pipeOptions = optsArray.join(':');
74
-
75
- try {
76
- const tests = await client.prepareRun({ pipe, pipeOptions });
77
-
78
- if (!tests || tests.length === 0) {
79
- return;
80
- }
81
-
82
- const grep = ` --grep (${tests.join('|')})`;
83
- command += grep;
84
- } catch (err) {
85
- console.log(APP_PREFIX, err);
86
- }
87
- }
88
-
89
- const testCmds = command.split(' ');
90
- console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
91
-
92
- if (!apiKey) {
93
- const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
94
-
95
- cmd.on('close', code => {
96
- console.log(APP_PREFIX, '⚠️ ', `Runner exited with ${pc.bold(code)}, report is ignored`);
97
-
98
- if (code > exitCode) exitCode = code;
99
- process.exitCode = exitCode;
100
- });
101
-
102
- return;
103
- }
104
-
105
- client.createRun().then(() => {
106
- const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
107
-
108
- cmd.on('close', code => {
109
- const emoji = code === 0 ? '🟢' : '🔴';
110
- console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
111
- const status = code === 0 ? 'passed' : 'failed';
112
- client.updateRunStatus(status, true);
113
-
114
- if (code > exitCode) exitCode = code;
115
- process.exitCode = exitCode;
116
- });
117
- });
118
- });
45
+ // Execute the main CLI with mapped arguments
119
46
 
120
- if (process.argv.length <= 2) {
121
- program.outputHelp();
122
- }
47
+ const child = spawn(process.execPath, [cliPath, ...newArgs], {
48
+ stdio: 'inherit'
49
+ });
123
50
 
124
- program.parse(process.argv);
51
+ child.on('exit', (code) => {
52
+ process.exit(code);
53
+ });
package/src/client.js CHANGED
@@ -181,8 +181,8 @@ class Client {
181
181
  suite_id,
182
182
  test_id,
183
183
  timestamp,
184
+ links,
184
185
  manuallyAttachedArtifacts,
185
- labels,
186
186
  overwrite,
187
187
  } = testData;
188
188
  let { message = '', meta = {} } = testData;
@@ -190,41 +190,13 @@ class Client {
190
190
  // stringify meta values and limit keys and values length to 255
191
191
  meta = Object.entries(meta)
192
192
  .filter(([, value]) => value !== null && value !== undefined)
193
- .map(([key, value]) => {
194
- try {
195
- if (typeof value === 'object') {
196
- value = JSON.stringify(value);
197
- } else if (typeof value !== 'string') {
198
- try {
199
- value = value.toString();
200
- } catch (err) {
201
- console.warn(APP_PREFIX, `Can't convert meta value to string`, err);
202
- }
203
- }
204
-
205
- if (value?.length > 255) {
206
- value = value.substring(0, 255);
207
- debug(APP_PREFIX, `Meta info value "${value}" is too long, trimmed to 255 characters`);
208
- }
209
-
210
- if (key?.length > 255) {
211
- const newKey = key.substring(0, 255);
212
- debug(APP_PREFIX, `Meta info key "${key}" is too long, trimmed to 255 characters`);
213
- return [newKey, value];
214
- }
215
-
216
- return [key, value];
217
- } catch (err) {
218
- debug(APP_PREFIX, `Error while processing meta info key ${key}`, err);
219
- return [null, null];
220
- }
221
- })
222
193
  .reduce((acc, [key, value]) => {
223
194
  if (key) acc[key] = value;
224
195
  return acc;
225
196
  }, {});
226
197
 
227
- // Labels are simple array of strings, no processing needed
198
+ // Get links from storage using the test context
199
+ const testContext = suite_title ? `${suite_title} ${title}` : title;
228
200
 
229
201
  let errorFormatted = '';
230
202
  if (error) {
@@ -280,7 +252,7 @@ class Client {
280
252
  timestamp,
281
253
  artifacts,
282
254
  meta,
283
- labels,
255
+ links,
284
256
  overwrite,
285
257
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
286
258
  };
@@ -6,8 +6,6 @@ import { TESTOMAT_TMP_STORAGE_DIR } from './constants.js';
6
6
  import { fileSystem, testRunnerHelper } from './utils/utils.js';
7
7
  import crypto from 'crypto';
8
8
 
9
- const startTime = Date.now();
10
-
11
9
  const debug = createDebugMessages('@testomatio/reporter:storage');
12
10
  class DataStorage {
13
11
  static #instance;
@@ -43,7 +41,7 @@ class DataStorage {
43
41
  /**
44
42
  * Puts any data to storage (file or global variable).
45
43
  * If file: stores data as text, if global variable – stores as array of data.
46
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
44
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
47
45
  * @param {*} data anything you want to store (string, object, array, etc)
48
46
  * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
49
47
  * suite name + test name is used by default
@@ -72,7 +70,7 @@ class DataStorage {
72
70
  * Returns data, stored for specific test/context (or data which was stored without test id specified).
73
71
  * This method will get data from global variable and/or from from file (previosly saved with put method).
74
72
  *
75
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
73
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
76
74
  * @param {string} context
77
75
  * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
78
76
  */
@@ -110,7 +108,7 @@ class DataStorage {
110
108
  }
111
109
 
112
110
  /**
113
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
111
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
114
112
  * @param {string} context
115
113
  * @returns aray of data (any type)
116
114
  */
@@ -118,7 +116,7 @@ class DataStorage {
118
116
  try {
119
117
  if (global?.testomatioDataStore[dataType]) {
120
118
  const testData = global.testomatioDataStore[dataType][context];
121
- if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
119
+ if (testData) debug('<=', dataType, 'global', context, testData);
122
120
  return testData || [];
123
121
  }
124
122
  // debug(`No ${this.dataType} data for context ${context} in <global> storage`);
@@ -129,7 +127,7 @@ class DataStorage {
129
127
  }
130
128
 
131
129
  /**
132
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
130
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
133
131
  * @param {*} context
134
132
  * @returns array of data (any type)
135
133
  */
@@ -139,7 +137,7 @@ class DataStorage {
139
137
  const filepath = join(dataDirPath, `${dataType}_${context}`);
140
138
  if (fs.existsSync(filepath)) {
141
139
  const testDataAsText = fs.readFileSync(filepath, 'utf-8');
142
- if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
140
+ if (testDataAsText) debug('<=', dataType, 'file', context, testDataAsText);
143
141
  const testDataArr = testDataAsText?.split(os.EOL) || [];
144
142
  return testDataArr;
145
143
  }
@@ -153,12 +151,12 @@ class DataStorage {
153
151
 
154
152
  /**
155
153
  * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
156
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
154
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
157
155
  * @param {*} data
158
156
  * @param {*} context
159
157
  */
160
158
  #putDataToGlobalVar(dataType, data, context) {
161
- debug('Saving data to global variable for ', context, ':', data);
159
+ debug('=>', dataType, 'global', context, data);
162
160
  if (!global.testomatioDataStore) global.testomatioDataStore = {};
163
161
  if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
164
162
 
@@ -168,7 +166,7 @@ class DataStorage {
168
166
 
169
167
  /**
170
168
  * Puts data to file. Unlike the global variable storage, stores data as string
171
- * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
169
+ * @param {'log' | 'artifact' | 'keyvalue' | 'links'} dataType
172
170
  * @param {*} data
173
171
  * @param {string} context
174
172
  * @returns
@@ -179,7 +177,7 @@ class DataStorage {
179
177
  const filename = `${dataType}_${context}`;
180
178
  const filepath = join(dataDirPath, filename);
181
179
  if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
182
- debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
180
+ debug('=>', dataType, 'file', context, data);
183
181
 
184
182
  // append new line if file already exists (in this case its definitely includes some data)
185
183
  if (fs.existsSync(filepath)) {
@@ -194,8 +192,7 @@ function stringToMD5Hash(str) {
194
192
  const md5 = crypto.createHash('md5');
195
193
  md5.update(str);
196
194
  const hash = md5.digest('hex');
197
-
198
- return `${startTime}_${hash}`;
195
+ return `${process.env.runId || 'run'}_${hash}`;
199
196
  }
200
197
 
201
198
  export const dataStorage = DataStorage.getInstance();
@@ -119,8 +119,7 @@ class TestomatioPipe {
119
119
  const resp = await this.client.request({
120
120
  method: 'GET',
121
121
  url: '/api/test_grep',
122
- params: q.params,
123
- responseType: q.responseType
122
+ ...q,
124
123
  });
125
124
 
126
125
  if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
@@ -188,7 +187,8 @@ class TestomatioPipe {
188
187
  const resp = await this.client.request({
189
188
  method: 'PUT',
190
189
  url: `/api/reporter/${this.runId}`,
191
- data: runParams
190
+ data: runParams,
191
+ responseType: 'json'
192
192
  });
193
193
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
194
194
  return;
@@ -3,6 +3,8 @@ import { services } from './services/index.js';
3
3
  /**
4
4
  * Stores path to file as artifact and uploads it to the S3 storage
5
5
  * @param {string | {path: string, type: string, name: string}} data - path to file or object with path, type and name
6
+ * @param {any} [context=null] - optional context parameter
7
+ * @returns {void}
6
8
  */
7
9
  function saveArtifact(data, context = null) {
8
10
  if (process.env.IS_PLAYWRIGHT)
@@ -14,7 +16,8 @@ function saveArtifact(data, context = null) {
14
16
 
15
17
  /**
16
18
  * Attach log message(s) to the test report
17
- * @param string
19
+ * @param {...any} args - log messages to attach
20
+ * @returns {void}
18
21
  */
19
22
  function logMessage(...args) {
20
23
  if (process.env.IS_PLAYWRIGHT) throw new Error('This function is not available in Playwright framework');
@@ -23,7 +26,8 @@ function logMessage(...args) {
23
26
 
24
27
  /**
25
28
  * Similar to "log" function but marks message in report as a step
26
- * @param {string} message
29
+ * @param {string} message - step message
30
+ * @returns {void}
27
31
  */
28
32
  function addStep(message) {
29
33
  if (process.env.IS_PLAYWRIGHT)
@@ -34,8 +38,9 @@ function addStep(message) {
34
38
 
35
39
  /**
36
40
  * Add key-value pair(s) to the test report
37
- * @param {{[key: string]: string} | string} keyValue object { key: value } (multiple props allowed) or key (string)
38
- * @param {string?} value
41
+ * @param {{[key: string]: string} | string} keyValue - object { key: value } (multiple props allowed) or key (string)
42
+ * @param {string|null} [value=null] - optional value when keyValue is a string
43
+ * @returns {void}
39
44
  */
40
45
  function setKeyValue(keyValue, value = null) {
41
46
  if (process.env.IS_PLAYWRIGHT)
@@ -50,46 +55,37 @@ function setKeyValue(keyValue, value = null) {
50
55
  /**
51
56
  * Add a single label to the test report
52
57
  * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
53
- * @param {string} [value] - optional label value (e.g. 'high', 'login')
58
+ * @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
59
+ * @returns {void}
54
60
  */
55
61
  function setLabel(key, value = null) {
56
62
  if (Array.isArray(value)) {
57
- value.forEach(v => setLabel(key, v));
58
- return;
59
- }
60
-
61
- if (!key || typeof key !== 'string') {
62
- console.warn('Label key must be a non-empty string');
63
- return;
64
- }
65
-
66
- // Limit key length to 255 characters
67
- if (key.length > 255) {
68
- console.warn('Label key is too long, trimmed to 255 characters:', key);
69
- key = key.substring(0, 255);
70
- }
71
-
72
- let labelString = key;
73
- if (value !== null && value !== undefined && value !== '') {
74
- if (typeof value !== 'string') {
75
- console.warn('Label value must be a string, converting:', value);
76
- value = String(value);
77
- }
78
- // Limit value length to 255 characters
79
- if (value.length > 255) {
80
- console.warn('Label value is too long, trimmed to 255 characters:', value);
81
- value = value.substring(0, 255);
82
- }
83
- labelString = `${key}:${value}`;
63
+ return value.forEach(label => setLabel(key, label));
84
64
  }
65
+ const labelObject = value !== null && value !== undefined && value !== ''
66
+ ? { label: `${key}:${value}` }
67
+ : { label: key };
68
+ services.links.put([labelObject]);
69
+ }
85
70
 
86
- // Limit total label length to 255 characters
87
- if (labelString.length > 255) {
88
- console.warn('Label is too long, trimmed to 255 characters:', labelString);
89
- labelString = labelString.substring(0, 255);
90
- }
71
+ /**
72
+ * Add link(s) to the test report
73
+ * @param {...string} testIds - test IDs to link
74
+ * @returns {void}
75
+ */
76
+ function linkTest(...testIds) {
77
+ const links = testIds.map(testId => ({ test: testId }));
78
+ services.links.put(links);
79
+ }
91
80
 
92
- services.labels.put([labelString]);
81
+ /**
82
+ * Add JIRA issue link(s) to the test report
83
+ * @param {...string} jiraIds - JIRA issue IDs to link
84
+ * @returns {void}
85
+ */
86
+ function linkJira(...jiraIds) {
87
+ const links = jiraIds.map(jiraId => ({ jira: jiraId }));
88
+ services.links.put(links);
93
89
  }
94
90
 
95
91
  export default {
@@ -98,4 +94,6 @@ export default {
98
94
  step: addStep,
99
95
  keyValue: setKeyValue,
100
96
  label: setLabel,
97
+ linkTest,
98
+ linkJira,
101
99
  };
package/src/reporter.js CHANGED
@@ -9,14 +9,15 @@ export const logger = services.logger;
9
9
  export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
11
  export const label = reporterFunctions.label;
12
+ export const linkTest = reporterFunctions.linkTest;
12
13
 
13
14
  /**
14
- * @typedef {import('./reporter-functions.js')} artifact
15
- * @typedef {import('./reporter-functions.js')} log
16
- * @typedef {import('./services/index.js')} logger
17
- * @typedef {import('./reporter-functions.js')} meta
18
- * @typedef {import('./reporter-functions.js')} step
19
- * @typedef {import('./reporter-functions.js')} label
15
+ * @typedef {typeof import('./reporter-functions.js').default.artifact} ArtifactFunction
16
+ * @typedef {typeof import('./reporter-functions.js').default.log} LogFunction
17
+ * @typedef {typeof import('./services/index.js').services.logger} LoggerService
18
+ * @typedef {typeof import('./reporter-functions.js').default.keyValue} MetaFunction
19
+ * @typedef {typeof import('./reporter-functions.js').default.step} StepFunction
20
+ * @typedef {typeof import('./reporter-functions.js').default.label} LabelFunction
20
21
  */
21
22
  export default {
22
23
  /**
@@ -30,6 +31,7 @@ export default {
30
31
  meta: reporterFunctions.keyValue,
31
32
  step: reporterFunctions.step,
32
33
  label: reporterFunctions.label,
34
+ linkTest: reporterFunctions.linkTest,
33
35
 
34
36
  // TestomatClient,
35
37
  // TRConstants,
@@ -1,14 +1,14 @@
1
1
  import { logger } from './logger.js';
2
2
  import { artifactStorage } from './artifacts.js';
3
3
  import { keyValueStorage } from './key-values.js';
4
- import { labelStorage } from './labels.js';
4
+ import { linkStorage } from './links.js';
5
5
  import { dataStorage } from '../data-storage.js';
6
6
 
7
7
  export const services = {
8
8
  logger,
9
9
  artifacts: artifactStorage,
10
10
  keyValues: keyValueStorage,
11
- labels: labelStorage,
11
+ links: linkStorage,
12
12
  setContext: context => {
13
13
  dataStorage.setContext(context);
14
14
  },
@@ -1,59 +1 @@
1
- import createDebugMessages from 'debug';
2
- import { dataStorage } from '../data-storage.js';
3
1
 
4
- const debug = createDebugMessages('@testomatio/reporter:services-labels');
5
- class LabelStorage {
6
- static #instance;
7
-
8
- /**
9
- *
10
- * @returns {LabelStorage}
11
- */
12
- static getInstance() {
13
- if (!this.#instance) {
14
- this.#instance = new LabelStorage();
15
- }
16
- return this.#instance;
17
- }
18
-
19
- /**
20
- * Stores labels array and passes it to reporter
21
- * @param {string[]} labels - array of label strings
22
- * @param {*} context - full test title
23
- */
24
- put(labels, context = null) {
25
- if (!labels || !Array.isArray(labels)) return;
26
- dataStorage.putData('labels', labels, context);
27
- }
28
-
29
- /**
30
- * Returns labels array for the test
31
- * @param {*} context testId or test context from test runner
32
- * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
33
- */
34
- get(context = null) {
35
- const labelsList = dataStorage.getData('labels', context);
36
- if (!labelsList || !labelsList?.length) return [];
37
-
38
- const allLabels = [];
39
- for (const labels of labelsList) {
40
- if (Array.isArray(labels)) {
41
- allLabels.push(...labels);
42
- } else if (typeof labels === 'string') {
43
- try {
44
- const parsedLabels = JSON.parse(labels);
45
- if (Array.isArray(parsedLabels)) {
46
- allLabels.push(...parsedLabels);
47
- }
48
- } catch (e) {
49
- debug(`Error parsing labels for test ${context}`, labels);
50
- }
51
- }
52
- }
53
-
54
- // Remove duplicates
55
- return [...new Set(allLabels)];
56
- }
57
- }
58
-
59
- export const labelStorage = LabelStorage.getInstance();
@@ -0,0 +1,69 @@
1
+ import createDebugMessages from 'debug';
2
+ import { dataStorage } from '../data-storage.js';
3
+
4
+ const debug = createDebugMessages('@testomatio/reporter:services-links');
5
+
6
+ class LinkStorage {
7
+ static #instance;
8
+
9
+ /**
10
+ *
11
+ * @returns {LinkStorage}
12
+ */
13
+ static getInstance() {
14
+ if (!this.#instance) {
15
+ this.#instance = new LinkStorage();
16
+ }
17
+ return this.#instance;
18
+ }
19
+
20
+ /**
21
+ * Stores links array and passes it to reporter
22
+ * @param {object[]} links - array of link objects
23
+ * @param {*} context - full test title
24
+ */
25
+ put(links, context = null) {
26
+ if (!links || !Array.isArray(links)) return;
27
+ dataStorage.putData('links', links, context);
28
+ }
29
+
30
+ /**
31
+ * Returns links array for the test
32
+ * @param {*} context testId or test context from test runner
33
+ * @returns {object[]} links array, e.g. [{test: 'TEST-123'}, {jira: 'JIRA-456'}]
34
+ */
35
+ get(context = null) {
36
+ const linksList = dataStorage.getData('links', context);
37
+ if (!linksList || !linksList?.length) return [];
38
+
39
+ const allLinks = [];
40
+ for (const links of linksList) {
41
+ if (Array.isArray(links)) {
42
+ allLinks.push(...links);
43
+ } else if (typeof links === 'string') {
44
+ try {
45
+ const parsedLinks = JSON.parse(links);
46
+ if (Array.isArray(parsedLinks)) {
47
+ allLinks.push(...parsedLinks);
48
+ }
49
+ } catch (e) {
50
+ debug(`Error parsing links for test ${context}`, links);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Remove duplicates based on JSON string comparison
56
+ const uniqueLinks = [];
57
+ const seen = new Set();
58
+ for (const link of allLinks) {
59
+ const key = JSON.stringify(link);
60
+ if (!seen.has(key)) {
61
+ seen.add(key);
62
+ uniqueLinks.push(link);
63
+ }
64
+ }
65
+ return uniqueLinks;
66
+ }
67
+ }
68
+
69
+ export const linkStorage = LinkStorage.getInstance();
@@ -53,7 +53,7 @@ const parseSuite = suiteTitle => {
53
53
  */
54
54
  const validateSuiteId = suiteId => {
55
55
  if (!suiteId) return null;
56
-
56
+
57
57
  const match = suiteId.match(SUITE_ID_REGEX);
58
58
  return match ? match[0] : null;
59
59
  };
@@ -273,7 +273,7 @@ const fileSystem = {
273
273
  const foundedTestLog = (app, tests) => {
274
274
  const n = tests.length;
275
275
 
276
- return n === 1 ? console.log(app, `✅ We found one test!`) : console.log(app, `✅ We found ${n} tests!`);
276
+ return console.log(app, `✅ We found ${n === 1 ? 'one test' : `${n} tests`} in Testomat.io!`);
277
277
  };
278
278
 
279
279
  const humanize = text => {
@@ -354,12 +354,14 @@ function storeRunId(runId) {
354
354
  }
355
355
 
356
356
  /**
357
- *
357
+ *
358
358
  * @returns {String|null} latest run ID
359
359
  */
360
360
  function readLatestRunId() {
361
361
  try {
362
362
  const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
363
+ if (!fs.existsSync(filePath)) return null;
364
+
363
365
  const stats = fs.statSync(filePath);
364
366
  const diff = +new Date() - +stats.mtime;
365
367
  const diffHours = diff / 1000 / 60 / 60;