@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.
- package/lib/adapter/codecept.js +4 -0
- package/lib/adapter/playwright.js +12 -3
- package/lib/client.js +3 -1
- package/lib/data-storage.d.ts +4 -4
- package/lib/data-storage.js +6 -6
- package/lib/reporter-functions.d.ts +7 -0
- package/lib/reporter-functions.js +36 -0
- package/lib/reporter.d.ts +3 -0
- package/lib/reporter.js +4 -1
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.js +2 -0
- package/lib/services/labels.d.ts +22 -0
- package/lib/services/labels.js +62 -0
- package/package.json +1 -1
- package/src/adapter/codecept.js +4 -0
- package/src/adapter/playwright.js +11 -7
- package/src/client.js +4 -0
- package/src/data-storage.js +6 -6
- package/src/reporter-functions.js +41 -0
- package/src/reporter.js +3 -0
- package/src/services/index.js +2 -0
- package/src/services/labels.js +59 -0
package/lib/adapter/codecept.js
CHANGED
|
@@ -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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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);
|
package/lib/data-storage.d.ts
CHANGED
|
@@ -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;
|
package/lib/data-storage.js
CHANGED
|
@@ -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
|
};
|
package/lib/services/index.d.ts
CHANGED
|
@@ -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';
|
package/lib/services/index.js
CHANGED
|
@@ -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
package/src/adapter/codecept.js
CHANGED
|
@@ -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
|
|
136
|
-
if (
|
|
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
|
|
package/src/data-storage.js
CHANGED
|
@@ -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,
|
package/src/services/index.js
CHANGED
|
@@ -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();
|