@testomatio/reporter 2.0.1-beta.6 → 2.0.1-beta.7

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.
@@ -126,6 +126,7 @@ function CodeceptReporter(config) {
126
126
  const logs = getTestLogs(test);
127
127
  const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(test.fullTitle());
128
128
  const keyValues = index_js_1.services.keyValues.get(test.fullTitle());
129
+ const labels = index_js_1.services.labels.get(test.fullTitle());
129
130
  index_js_1.services.setContext(null);
130
131
  client.addTestRun(constants_js_1.STATUS.PASSED, {
131
132
  ...stripExampleFromTitle(title),
@@ -138,6 +139,7 @@ function CodeceptReporter(config) {
138
139
  logs,
139
140
  manuallyAttachedArtifacts,
140
141
  meta: keyValues,
142
+ labels: labels,
141
143
  });
142
144
  // output.stop();
143
145
  });
@@ -180,6 +182,7 @@ function CodeceptReporter(config) {
180
182
  const logs = getTestLogs(test);
181
183
  const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(test.fullTitle());
182
184
  const keyValues = index_js_1.services.keyValues.get(test.fullTitle());
185
+ const labels = index_js_1.services.labels.get(test.fullTitle());
183
186
  index_js_1.services.setContext(null);
184
187
  client.addTestRun(constants_js_1.STATUS.FAILED, {
185
188
  ...stripExampleFromTitle(title),
@@ -194,6 +197,7 @@ function CodeceptReporter(config) {
194
197
  logs,
195
198
  manuallyAttachedArtifacts,
196
199
  meta: keyValues,
200
+ labels: labels,
197
201
  });
198
202
  debug('artifacts', artifacts);
199
203
  for (const aid in artifacts) {
@@ -123,9 +123,18 @@ class PlaywrightReporter {
123
123
  }
124
124
  if (artifact.body) {
125
125
  let filePath = generateTmpFilepath(artifact.name);
126
- const extension = artifact.contentType?.split('/')[1]?.replace('jpeg', 'jpg');
127
- if (extension)
128
- filePath += `.${extension}`;
126
+ // Check if file already has an extension
127
+ const hasExtension = artifact.name && path_1.default.extname(artifact.name);
128
+ if (!hasExtension && artifact.contentType) {
129
+ const mimeType = artifact.contentType.split('/')[1];
130
+ const extensionMap = {
131
+ jpeg: 'jpg',
132
+ plain: 'txt',
133
+ };
134
+ const extension = extensionMap[mimeType] || mimeType;
135
+ if (extension)
136
+ filePath += `.${extension}`;
137
+ }
129
138
  fs_1.default.writeFileSync(filePath, artifact.body);
130
139
  return filePath;
131
140
  }
package/lib/client.js CHANGED
@@ -179,7 +179,7 @@ class Client {
179
179
  /**
180
180
  * @type {TestData}
181
181
  */
182
- const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, manuallyAttachedArtifacts, } = testData;
182
+ const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, manuallyAttachedArtifacts, labels, } = testData;
183
183
  let { message = '', meta = {} } = testData;
184
184
  // stringify meta values and limit keys and values length to 255
185
185
  meta = Object.entries(meta)
@@ -218,6 +218,7 @@ class Client {
218
218
  acc[key] = value;
219
219
  return acc;
220
220
  }, {});
221
+ // Labels are simple array of strings, no processing needed
221
222
  let errorFormatted = '';
222
223
  if (error) {
223
224
  errorFormatted += this.formatError(error) || '';
@@ -265,6 +266,7 @@ class Client {
265
266
  timestamp,
266
267
  artifacts,
267
268
  meta,
269
+ labels,
268
270
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
269
271
  };
270
272
  // debug('Adding test run...', data);
@@ -12,22 +12,22 @@ declare class DataStorage {
12
12
  /**
13
13
  * Puts any data to storage (file or global variable).
14
14
  * If file: stores data as text, if global variable – stores as array of data.
15
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
15
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
16
16
  * @param {*} data anything you want to store (string, object, array, etc)
17
17
  * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
18
18
  * suite name + test name is used by default
19
19
  * @returns
20
20
  */
21
- putData(dataType: "log" | "artifact" | "keyvalue", data: any, context?: any): void;
21
+ putData(dataType: "log" | "artifact" | "keyvalue" | "labels", data: any, context?: any): void;
22
22
  /**
23
23
  * Returns data, stored for specific test/context (or data which was stored without test id specified).
24
24
  * This method will get data from global variable and/or from from file (previosly saved with put method).
25
25
  *
26
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
26
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
27
27
  * @param {string} context
28
28
  * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
29
29
  */
30
- getData(dataType: "log" | "artifact" | "keyvalue", context: string): any[];
30
+ getData(dataType: "log" | "artifact" | "keyvalue" | "labels", context: string): any[];
31
31
  #private;
32
32
  }
33
33
  export function stringToMD5Hash(str: any): string;
@@ -75,7 +75,7 @@ class DataStorage {
75
75
  /**
76
76
  * Puts any data to storage (file or global variable).
77
77
  * If file: stores data as text, if global variable – stores as array of data.
78
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
78
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
79
79
  * @param {*} data anything you want to store (string, object, array, etc)
80
80
  * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
81
81
  * suite name + test name is used by default
@@ -103,7 +103,7 @@ class DataStorage {
103
103
  * Returns data, stored for specific test/context (or data which was stored without test id specified).
104
104
  * This method will get data from global variable and/or from from file (previosly saved with put method).
105
105
  *
106
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
106
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
107
107
  * @param {string} context
108
108
  * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
109
109
  */
@@ -134,7 +134,7 @@ class DataStorage {
134
134
  return null;
135
135
  }
136
136
  /**
137
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
137
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
138
138
  * @param {string} context
139
139
  * @returns aray of data (any type)
140
140
  */
@@ -154,7 +154,7 @@ class DataStorage {
154
154
  }
155
155
  }
156
156
  /**
157
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
157
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
158
158
  * @param {*} context
159
159
  * @returns array of data (any type)
160
160
  */
@@ -179,7 +179,7 @@ class DataStorage {
179
179
  }
180
180
  /**
181
181
  * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
182
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
182
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
183
183
  * @param {*} data
184
184
  * @param {*} context
185
185
  */
@@ -195,7 +195,7 @@ class DataStorage {
195
195
  }
196
196
  /**
197
197
  * Puts data to file. Unlike the global variable storage, stores data as string
198
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
198
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
199
199
  * @param {*} data
200
200
  * @param {string} context
201
201
  * @returns
@@ -3,6 +3,7 @@ declare namespace _default {
3
3
  export { logMessage as log };
4
4
  export { addStep as step };
5
5
  export { setKeyValue as keyValue };
6
+ export { setLabel as label };
6
7
  }
7
8
  export default _default;
8
9
  /**
@@ -32,3 +33,9 @@ declare function addStep(message: string): void;
32
33
  declare function setKeyValue(keyValue: {
33
34
  [key: string]: string;
34
35
  } | string, value?: string | null): void;
36
+ /**
37
+ * Add a single label to the test report
38
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
39
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
40
+ */
41
+ declare function setLabel(key: string, value?: string): void;
@@ -44,9 +44,45 @@ function setKeyValue(keyValue, value = null) {
44
44
  }
45
45
  index_js_1.services.keyValues.put(keyValue);
46
46
  }
47
+ /**
48
+ * Add a single label to the test report
49
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
50
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
51
+ */
52
+ function setLabel(key, value = null) {
53
+ if (!key || typeof key !== 'string') {
54
+ console.warn('Label key must be a non-empty string');
55
+ return;
56
+ }
57
+ // Limit key length to 255 characters
58
+ if (key.length > 255) {
59
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
60
+ key = key.substring(0, 255);
61
+ }
62
+ let labelString = key;
63
+ if (value !== null && value !== undefined && value !== '') {
64
+ if (typeof value !== 'string') {
65
+ console.warn('Label value must be a string, converting:', value);
66
+ value = String(value);
67
+ }
68
+ // Limit value length to 255 characters
69
+ if (value.length > 255) {
70
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
71
+ value = value.substring(0, 255);
72
+ }
73
+ labelString = `${key}:${value}`;
74
+ }
75
+ // Limit total label length to 255 characters
76
+ if (labelString.length > 255) {
77
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
78
+ labelString = labelString.substring(0, 255);
79
+ }
80
+ index_js_1.services.labels.put([labelString]);
81
+ }
47
82
  module.exports = {
48
83
  artifact: saveArtifact,
49
84
  log: logMessage,
50
85
  step: addStep,
51
86
  keyValue: setKeyValue,
87
+ label: setLabel,
52
88
  };
package/lib/reporter.d.ts CHANGED
@@ -81,6 +81,8 @@ export const meta: (keyValue: {
81
81
  } | string, value?: string | null) => void;
82
82
  export type step = typeof import("./reporter-functions.js");
83
83
  export const step: (message: string) => void;
84
+ export type label = typeof import("./reporter-functions.js");
85
+ export const label: (key: string, value?: string) => void;
84
86
  declare namespace _default {
85
87
  let testomatioLogger: {
86
88
  "__#13@#originalUserLogger": {
@@ -228,5 +230,6 @@ declare namespace _default {
228
230
  [key: string]: string;
229
231
  } | string, value?: string | null) => void;
230
232
  let step: (message: string) => void;
233
+ let label: (key: string, value?: string) => void;
231
234
  }
232
235
  export default _default;
package/lib/reporter.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
6
+ exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
7
7
  // import TestomatClient from './client.js';
8
8
  // import * as TRConstants from './constants.js';
9
9
  const index_js_1 = require("./services/index.js");
@@ -13,12 +13,14 @@ exports.log = reporter_functions_js_1.default.log;
13
13
  exports.logger = index_js_1.services.logger;
14
14
  exports.meta = reporter_functions_js_1.default.keyValue;
15
15
  exports.step = reporter_functions_js_1.default.step;
16
+ exports.label = reporter_functions_js_1.default.label;
16
17
  /**
17
18
  * @typedef {import('./reporter-functions.js')} artifact
18
19
  * @typedef {import('./reporter-functions.js')} log
19
20
  * @typedef {import('./services/index.js')} logger
20
21
  * @typedef {import('./reporter-functions.js')} meta
21
22
  * @typedef {import('./reporter-functions.js')} step
23
+ * @typedef {import('./reporter-functions.js')} label
22
24
  */
23
25
  module.exports = {
24
26
  /**
@@ -30,6 +32,7 @@ module.exports = {
30
32
  logger: index_js_1.services.logger,
31
33
  meta: reporter_functions_js_1.default.keyValue,
32
34
  step: reporter_functions_js_1.default.step,
35
+ label: reporter_functions_js_1.default.label,
33
36
  // TestomatClient,
34
37
  // TRConstants,
35
38
  };
@@ -2,8 +2,10 @@ export namespace services {
2
2
  export { logger };
3
3
  export { artifactStorage as artifacts };
4
4
  export { keyValueStorage as keyValues };
5
+ export { labelStorage as labels };
5
6
  export function setContext(context: any): void;
6
7
  }
7
8
  import { logger } from './logger.js';
8
9
  import { artifactStorage } from './artifacts.js';
9
10
  import { keyValueStorage } from './key-values.js';
11
+ import { labelStorage } from './labels.js';
@@ -4,11 +4,13 @@ exports.services = void 0;
4
4
  const logger_js_1 = require("./logger.js");
5
5
  const artifacts_js_1 = require("./artifacts.js");
6
6
  const key_values_js_1 = require("./key-values.js");
7
+ const labels_js_1 = require("./labels.js");
7
8
  const data_storage_js_1 = require("../data-storage.js");
8
9
  exports.services = {
9
10
  logger: logger_js_1.logger,
10
11
  artifacts: artifacts_js_1.artifactStorage,
11
12
  keyValues: key_values_js_1.keyValueStorage,
13
+ labels: labels_js_1.labelStorage,
12
14
  setContext: context => {
13
15
  data_storage_js_1.dataStorage.setContext(context);
14
16
  },
@@ -0,0 +1,22 @@
1
+ export const labelStorage: LabelStorage;
2
+ declare class LabelStorage {
3
+ static "__#16@#instance": any;
4
+ /**
5
+ *
6
+ * @returns {LabelStorage}
7
+ */
8
+ static getInstance(): LabelStorage;
9
+ /**
10
+ * Stores labels array and passes it to reporter
11
+ * @param {string[]} labels - array of label strings
12
+ * @param {*} context - full test title
13
+ */
14
+ put(labels: string[], context?: any): void;
15
+ /**
16
+ * Returns labels array for the test
17
+ * @param {*} context testId or test context from test runner
18
+ * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
19
+ */
20
+ get(context?: any): string[];
21
+ }
22
+ export {};
@@ -0,0 +1,62 @@
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
+ exports.labelStorage = void 0;
7
+ const debug_1 = __importDefault(require("debug"));
8
+ const data_storage_js_1 = require("../data-storage.js");
9
+ const debug = (0, debug_1.default)('@testomatio/reporter:services-labels');
10
+ class LabelStorage {
11
+ static #instance;
12
+ /**
13
+ *
14
+ * @returns {LabelStorage}
15
+ */
16
+ static getInstance() {
17
+ if (!this.#instance) {
18
+ this.#instance = new LabelStorage();
19
+ }
20
+ return this.#instance;
21
+ }
22
+ /**
23
+ * Stores labels array and passes it to reporter
24
+ * @param {string[]} labels - array of label strings
25
+ * @param {*} context - full test title
26
+ */
27
+ put(labels, context = null) {
28
+ if (!labels || !Array.isArray(labels))
29
+ return;
30
+ data_storage_js_1.dataStorage.putData('labels', labels, context);
31
+ }
32
+ /**
33
+ * Returns labels array for the test
34
+ * @param {*} context testId or test context from test runner
35
+ * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
36
+ */
37
+ get(context = null) {
38
+ const labelsList = data_storage_js_1.dataStorage.getData('labels', context);
39
+ if (!labelsList || !labelsList?.length)
40
+ return [];
41
+ const allLabels = [];
42
+ for (const labels of labelsList) {
43
+ if (Array.isArray(labels)) {
44
+ allLabels.push(...labels);
45
+ }
46
+ else if (typeof labels === 'string') {
47
+ try {
48
+ const parsedLabels = JSON.parse(labels);
49
+ if (Array.isArray(parsedLabels)) {
50
+ allLabels.push(...parsedLabels);
51
+ }
52
+ }
53
+ catch (e) {
54
+ debug(`Error parsing labels for test ${context}`, labels);
55
+ }
56
+ }
57
+ }
58
+ // Remove duplicates
59
+ return [...new Set(allLabels)];
60
+ }
61
+ }
62
+ exports.labelStorage = LabelStorage.getInstance();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.0.1-beta.6",
3
+ "version": "2.0.1-beta.7",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -151,6 +151,7 @@ function CodeceptReporter(config) {
151
151
  const logs = getTestLogs(test);
152
152
  const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
153
153
  const keyValues = services.keyValues.get(test.fullTitle());
154
+ const labels = services.labels.get(test.fullTitle());
154
155
  services.setContext(null);
155
156
 
156
157
  client.addTestRun(STATUS.PASSED, {
@@ -164,6 +165,7 @@ function CodeceptReporter(config) {
164
165
  logs,
165
166
  manuallyAttachedArtifacts,
166
167
  meta: keyValues,
168
+ labels: labels,
167
169
  });
168
170
  // output.stop();
169
171
  });
@@ -208,6 +210,7 @@ function CodeceptReporter(config) {
208
210
  const logs = getTestLogs(test);
209
211
  const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
210
212
  const keyValues = services.keyValues.get(test.fullTitle());
213
+ const labels = services.labels.get(test.fullTitle());
211
214
  services.setContext(null);
212
215
 
213
216
  client.addTestRun(STATUS.FAILED, {
@@ -223,6 +226,7 @@ function CodeceptReporter(config) {
223
226
  logs,
224
227
  manuallyAttachedArtifacts,
225
228
  meta: keyValues,
229
+ labels: labels,
226
230
  });
227
231
 
228
232
  debug('artifacts', artifacts);
@@ -125,20 +125,24 @@ class PlaywrightReporter {
125
125
  #getArtifactPath(artifact) {
126
126
  if (artifact.path) {
127
127
  if (path.isAbsolute(artifact.path)) return artifact.path;
128
-
129
128
  return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
130
129
  }
131
-
132
130
  if (artifact.body) {
133
131
  let filePath = generateTmpFilepath(artifact.name);
134
-
135
- const extension = artifact.contentType?.split('/')[1]?.replace('jpeg', 'jpg');
136
- if (extension) filePath += `.${extension}`;
137
-
132
+ // Check if file already has an extension
133
+ const hasExtension = artifact.name && path.extname(artifact.name);
134
+ if (!hasExtension && artifact.contentType) {
135
+ const mimeType = artifact.contentType.split('/')[1];
136
+ const extensionMap = {
137
+ jpeg: 'jpg',
138
+ plain: 'txt',
139
+ };
140
+ const extension = extensionMap[mimeType] || mimeType;
141
+ if (extension) filePath += `.${extension}`;
142
+ }
138
143
  fs.writeFileSync(filePath, artifact.body);
139
144
  return filePath;
140
145
  }
141
-
142
146
  return null;
143
147
  }
144
148
 
package/src/client.js CHANGED
@@ -179,6 +179,7 @@ class Client {
179
179
  test_id,
180
180
  timestamp,
181
181
  manuallyAttachedArtifacts,
182
+ labels,
182
183
  } = testData;
183
184
  let { message = '', meta = {} } = testData;
184
185
 
@@ -219,6 +220,8 @@ class Client {
219
220
  return acc;
220
221
  }, {});
221
222
 
223
+ // Labels are simple array of strings, no processing needed
224
+
222
225
  let errorFormatted = '';
223
226
  if (error) {
224
227
  errorFormatted += this.formatError(error) || '';
@@ -273,6 +276,7 @@ class Client {
273
276
  timestamp,
274
277
  artifacts,
275
278
  meta,
279
+ labels,
276
280
  ...(rootSuiteId && { root_suite_id: rootSuiteId }),
277
281
  };
278
282
 
@@ -41,7 +41,7 @@ class DataStorage {
41
41
  /**
42
42
  * Puts any data to storage (file or global variable).
43
43
  * If file: stores data as text, if global variable – stores as array of data.
44
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
44
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
45
45
  * @param {*} data anything you want to store (string, object, array, etc)
46
46
  * @param {*} context could be testId or any context (test name, suite name, including their IDs etc)
47
47
  * suite name + test name is used by default
@@ -70,7 +70,7 @@ class DataStorage {
70
70
  * Returns data, stored for specific test/context (or data which was stored without test id specified).
71
71
  * This method will get data from global variable and/or from from file (previosly saved with put method).
72
72
  *
73
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
73
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
74
74
  * @param {string} context
75
75
  * @returns {any []} array of data (any type), null (if no data found for context) or string (if data type is log)
76
76
  */
@@ -108,7 +108,7 @@ class DataStorage {
108
108
  }
109
109
 
110
110
  /**
111
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
111
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
112
112
  * @param {string} context
113
113
  * @returns aray of data (any type)
114
114
  */
@@ -127,7 +127,7 @@ class DataStorage {
127
127
  }
128
128
 
129
129
  /**
130
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
130
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
131
131
  * @param {*} context
132
132
  * @returns array of data (any type)
133
133
  */
@@ -151,7 +151,7 @@ class DataStorage {
151
151
 
152
152
  /**
153
153
  * Puts data to global variable. Unlike the file storage, stores data in array (file storage just append as string).
154
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
154
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
155
155
  * @param {*} data
156
156
  * @param {*} context
157
157
  */
@@ -166,7 +166,7 @@ class DataStorage {
166
166
 
167
167
  /**
168
168
  * Puts data to file. Unlike the global variable storage, stores data as string
169
- * @param {'log' | 'artifact' | 'keyvalue'} dataType
169
+ * @param {'log' | 'artifact' | 'keyvalue' | 'labels'} dataType
170
170
  * @param {*} data
171
171
  * @param {string} context
172
172
  * @returns
@@ -47,9 +47,50 @@ function setKeyValue(keyValue, value = null) {
47
47
  services.keyValues.put(keyValue);
48
48
  }
49
49
 
50
+ /**
51
+ * Add a single label to the test report
52
+ * @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')
54
+ */
55
+ function setLabel(key, value = null) {
56
+ if (!key || typeof key !== 'string') {
57
+ console.warn('Label key must be a non-empty string');
58
+ return;
59
+ }
60
+
61
+ // Limit key length to 255 characters
62
+ if (key.length > 255) {
63
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
64
+ key = key.substring(0, 255);
65
+ }
66
+
67
+ let labelString = key;
68
+ if (value !== null && value !== undefined && value !== '') {
69
+ if (typeof value !== 'string') {
70
+ console.warn('Label value must be a string, converting:', value);
71
+ value = String(value);
72
+ }
73
+ // Limit value length to 255 characters
74
+ if (value.length > 255) {
75
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
76
+ value = value.substring(0, 255);
77
+ }
78
+ labelString = `${key}:${value}`;
79
+ }
80
+
81
+ // Limit total label length to 255 characters
82
+ if (labelString.length > 255) {
83
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
84
+ labelString = labelString.substring(0, 255);
85
+ }
86
+
87
+ services.labels.put([labelString]);
88
+ }
89
+
50
90
  export default {
51
91
  artifact: saveArtifact,
52
92
  log: logMessage,
53
93
  step: addStep,
54
94
  keyValue: setKeyValue,
95
+ label: setLabel,
55
96
  };
package/src/reporter.js CHANGED
@@ -8,6 +8,7 @@ export const log = reporterFunctions.log;
8
8
  export const logger = services.logger;
9
9
  export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
+ export const label = reporterFunctions.label;
11
12
 
12
13
  /**
13
14
  * @typedef {import('./reporter-functions.js')} artifact
@@ -15,6 +16,7 @@ export const step = reporterFunctions.step;
15
16
  * @typedef {import('./services/index.js')} logger
16
17
  * @typedef {import('./reporter-functions.js')} meta
17
18
  * @typedef {import('./reporter-functions.js')} step
19
+ * @typedef {import('./reporter-functions.js')} label
18
20
  */
19
21
  export default {
20
22
  /**
@@ -27,6 +29,7 @@ export default {
27
29
  logger: services.logger,
28
30
  meta: reporterFunctions.keyValue,
29
31
  step: reporterFunctions.step,
32
+ label: reporterFunctions.label,
30
33
 
31
34
  // TestomatClient,
32
35
  // TRConstants,
@@ -1,12 +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
5
  import { dataStorage } from '../data-storage.js';
5
6
 
6
7
  export const services = {
7
8
  logger,
8
9
  artifacts: artifactStorage,
9
10
  keyValues: keyValueStorage,
11
+ labels: labelStorage,
10
12
  setContext: context => {
11
13
  dataStorage.setContext(context);
12
14
  },
@@ -0,0 +1,59 @@
1
+ import createDebugMessages from 'debug';
2
+ import { dataStorage } from '../data-storage.js';
3
+
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();