@testomatio/reporter 2.3.0-beta.1-links → 2.3.0-beta.3-playwright-tags
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 +0 -2
- package/lib/adapter/cucumber/current.js +0 -2
- package/lib/adapter/jest.js +0 -2
- package/lib/adapter/playwright.js +38 -3
- package/lib/adapter/webdriver.js +11 -8
- package/lib/client.js +32 -1
- package/lib/data-storage.js +5 -5
- package/lib/reporter-functions.d.ts +3 -10
- package/lib/reporter-functions.js +4 -13
- package/lib/reporter.d.ts +14 -14
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +22 -0
- package/lib/services/labels.js +62 -0
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- package/package.json +1 -1
- package/src/adapter/codecept.js +0 -3
- package/src/adapter/cucumber/current.js +0 -2
- package/src/adapter/jest.js +0 -2
- package/src/adapter/playwright.js +44 -3
- package/src/adapter/webdriver.js +11 -8
- package/src/client.js +31 -1
- package/src/data-storage.js +6 -5
- package/src/reporter-functions.js +8 -16
- package/src/services/labels.js +58 -0
package/lib/adapter/codecept.js
CHANGED
|
@@ -26,8 +26,6 @@ const HOOK_EXECUTION_ORDER = {
|
|
|
26
26
|
PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
|
|
27
27
|
POST_TEST: ['AfterHook', 'AfterSuiteHook']
|
|
28
28
|
};
|
|
29
|
-
// codeceptjs workers are self-contained
|
|
30
|
-
data_storage_js_1.dataStorage.isFileStorage = false;
|
|
31
29
|
const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
|
|
32
30
|
if (MAJOR_VERSION < 3) {
|
|
33
31
|
console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
|
|
@@ -105,7 +105,6 @@ class CucumberReporter extends cucumber_1.Formatter {
|
|
|
105
105
|
const logs = index_js_1.services.logger.getLogs(testTitle).join('\n');
|
|
106
106
|
const artifacts = index_js_1.services.artifacts.get(testTitle);
|
|
107
107
|
const keyValues = index_js_1.services.keyValues.get(testTitle);
|
|
108
|
-
const links = index_js_1.services.links.get(testTitle);
|
|
109
108
|
this.client.addTestRun(status, {
|
|
110
109
|
// error: testCaseAttempt.worstTestStepResult.message,
|
|
111
110
|
message,
|
|
@@ -115,7 +114,6 @@ class CucumberReporter extends cucumber_1.Formatter {
|
|
|
115
114
|
.trim(),
|
|
116
115
|
example: { ...example },
|
|
117
116
|
logs,
|
|
118
|
-
links,
|
|
119
117
|
manuallyAttachedArtifacts: artifacts,
|
|
120
118
|
meta: keyValues,
|
|
121
119
|
title: scenario,
|
package/lib/adapter/jest.js
CHANGED
|
@@ -57,7 +57,6 @@ class JestReporter {
|
|
|
57
57
|
const logs = getTestLogs(result);
|
|
58
58
|
const artifacts = index_js_1.services.artifacts.get(result.fullName);
|
|
59
59
|
const keyValues = index_js_1.services.keyValues.get(result.fullName);
|
|
60
|
-
const links = index_js_1.services.links.get(result.fullName);
|
|
61
60
|
const deducedStatus = status === 'pending' ? 'skipped' : status;
|
|
62
61
|
// In jest if test is not matched with test name pattern it is considered as skipped.
|
|
63
62
|
// So adding a check if it is skipped for real or because of test pattern
|
|
@@ -70,7 +69,6 @@ class JestReporter {
|
|
|
70
69
|
title,
|
|
71
70
|
time: duration,
|
|
72
71
|
logs,
|
|
73
|
-
links,
|
|
74
72
|
manuallyAttachedArtifacts: artifacts,
|
|
75
73
|
meta: keyValues,
|
|
76
74
|
});
|
|
@@ -48,6 +48,8 @@ class PlaywrightReporter {
|
|
|
48
48
|
steps.push(appendedStep);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
// Extract and normalize tags
|
|
52
|
+
const tags = extractTags(test);
|
|
51
53
|
const fullTestTitle = getTestContextName(test);
|
|
52
54
|
let logs = '';
|
|
53
55
|
if (result.stderr.length || result.stdout.length) {
|
|
@@ -55,7 +57,6 @@ class PlaywrightReporter {
|
|
|
55
57
|
}
|
|
56
58
|
const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(fullTestTitle);
|
|
57
59
|
const testMeta = index_js_1.services.keyValues.get(fullTestTitle);
|
|
58
|
-
const links = index_js_1.services.links.get(fullTestTitle);
|
|
59
60
|
const rid = test.id || test.testId || (0, uuid_1.v4)();
|
|
60
61
|
/**
|
|
61
62
|
* @type {{
|
|
@@ -86,13 +87,13 @@ class PlaywrightReporter {
|
|
|
86
87
|
const reportTestPromise = this.client.addTestRun(checkStatus(status), {
|
|
87
88
|
rid: `${rid}-${project.name}`,
|
|
88
89
|
error,
|
|
89
|
-
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${
|
|
90
|
+
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
|
|
90
91
|
suite_title,
|
|
91
92
|
title,
|
|
93
|
+
tags,
|
|
92
94
|
steps: steps.length ? steps : undefined,
|
|
93
95
|
time: duration,
|
|
94
96
|
logs,
|
|
95
|
-
links,
|
|
96
97
|
manuallyAttachedArtifacts,
|
|
97
98
|
meta: {
|
|
98
99
|
browser: project.browser,
|
|
@@ -218,6 +219,40 @@ function generateTmpFilepath(filename = '') {
|
|
|
218
219
|
const tmpdir = os_1.default.tmpdir();
|
|
219
220
|
return path_1.default.join(tmpdir, filename);
|
|
220
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Extracts and normalizes tags from test title, test options, and suite level
|
|
224
|
+
* @param {*} test - testInfo object from Playwright
|
|
225
|
+
* @returns {string[]} - array of normalized tags
|
|
226
|
+
*/
|
|
227
|
+
function extractTags(test) {
|
|
228
|
+
const tagsSet = new Set();
|
|
229
|
+
// Extract tags from test title (@tag format)
|
|
230
|
+
const titleTagsMatch = test.title.match(/@\w+/g);
|
|
231
|
+
if (titleTagsMatch) {
|
|
232
|
+
titleTagsMatch.forEach(tag => {
|
|
233
|
+
tagsSet.add(tag.replace('@', '').toLowerCase());
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Extract tags from test.tags (Playwright built-in tags)
|
|
237
|
+
if (test.tags && Array.isArray(test.tags)) {
|
|
238
|
+
test.tags.forEach(tag => {
|
|
239
|
+
const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
|
|
240
|
+
tagsSet.add(normalizedTag);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Extract tags from suite/describe level (inherited tags)
|
|
244
|
+
let parent = test.parent;
|
|
245
|
+
while (parent) {
|
|
246
|
+
if (parent.tags && Array.isArray(parent.tags)) {
|
|
247
|
+
parent.tags.forEach(tag => {
|
|
248
|
+
const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
|
|
249
|
+
tagsSet.add(normalizedTag);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
parent = parent.parent;
|
|
253
|
+
}
|
|
254
|
+
return Array.from(tagsSet);
|
|
255
|
+
}
|
|
221
256
|
/**
|
|
222
257
|
* Returns filename + test title
|
|
223
258
|
* @param {*} test - testInfo object from Playwright
|
package/lib/adapter/webdriver.js
CHANGED
|
@@ -77,10 +77,14 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
77
77
|
onTestEnd(test) {
|
|
78
78
|
test.suite = test.parent;
|
|
79
79
|
const logs = getTestLogs(test.fullTitle);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// TODO: FIX: artifacts for some reason leads to empty report on Testomat.io
|
|
81
|
+
// ^ not reproduced anymore (Jul 2025)
|
|
82
|
+
// but still be under investigation
|
|
83
|
+
const artifacts = index_js_1.services.artifacts.get(test.fullTitle);
|
|
84
|
+
const keyValues = index_js_1.services.keyValues.get(test.fullTitle);
|
|
83
85
|
test.logs = logs;
|
|
86
|
+
test.artifacts = artifacts;
|
|
87
|
+
test.meta = keyValues;
|
|
84
88
|
this._addTestPromises.push(this.addTest(test));
|
|
85
89
|
}
|
|
86
90
|
// wdio-cucumber does not trigger onTestEnd hook, thus, using this one
|
|
@@ -92,7 +96,7 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
92
96
|
async addTest(test) {
|
|
93
97
|
if (!this.client)
|
|
94
98
|
return;
|
|
95
|
-
const { title, _duration: duration, state, error, output
|
|
99
|
+
const { title, _duration: duration, state, error, output } = test;
|
|
96
100
|
const testId = (0, utils_js_1.getTestomatIdFromTestTitle)(title);
|
|
97
101
|
const screenshotEndpoint = '/session/:sessionId/screenshot';
|
|
98
102
|
const screenshotsBuffers = output
|
|
@@ -100,11 +104,10 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
100
104
|
.map(el => Buffer.from(el.result.value, 'base64'));
|
|
101
105
|
await this.client.addTestRun(state, {
|
|
102
106
|
rid: test.uid || '',
|
|
103
|
-
manuallyAttachedArtifacts: artifacts,
|
|
107
|
+
manuallyAttachedArtifacts: test.artifacts,
|
|
104
108
|
error,
|
|
105
|
-
logs,
|
|
106
|
-
meta,
|
|
107
|
-
links,
|
|
109
|
+
logs: test.logs,
|
|
110
|
+
meta: test.meta,
|
|
108
111
|
title,
|
|
109
112
|
test_id: testId,
|
|
110
113
|
time: duration,
|
package/lib/client.js
CHANGED
|
@@ -50,6 +50,7 @@ const path_1 = __importStar(require("path"));
|
|
|
50
50
|
const node_url_1 = require("node:url");
|
|
51
51
|
const uploader_js_1 = require("./uploader.js");
|
|
52
52
|
const utils_js_1 = require("./utils/utils.js");
|
|
53
|
+
const links_js_1 = require("./services/links.js");
|
|
53
54
|
const filesize_1 = require("filesize");
|
|
54
55
|
const debug = (0, debug_1.default)('@testomatio/reporter:client');
|
|
55
56
|
// removed __dirname usage, because:
|
|
@@ -181,11 +182,40 @@ class Client {
|
|
|
181
182
|
/**
|
|
182
183
|
* @type {TestData}
|
|
183
184
|
*/
|
|
184
|
-
const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp,
|
|
185
|
+
const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, manuallyAttachedArtifacts, overwrite, } = testData;
|
|
185
186
|
let { message = '', meta = {} } = testData;
|
|
186
187
|
// stringify meta values and limit keys and values length to 255
|
|
187
188
|
meta = Object.entries(meta)
|
|
188
189
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
190
|
+
.map(([key, value]) => {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof value === 'object') {
|
|
193
|
+
value = JSON.stringify(value);
|
|
194
|
+
}
|
|
195
|
+
else if (typeof value !== 'string') {
|
|
196
|
+
try {
|
|
197
|
+
value = value.toString();
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.warn(constants_js_1.APP_PREFIX, `Can't convert meta value to string`, err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (value?.length > 255) {
|
|
204
|
+
value = value.substring(0, 255);
|
|
205
|
+
debug(constants_js_1.APP_PREFIX, `Meta info value "${value}" is too long, trimmed to 255 characters`);
|
|
206
|
+
}
|
|
207
|
+
if (key?.length > 255) {
|
|
208
|
+
const newKey = key.substring(0, 255);
|
|
209
|
+
debug(constants_js_1.APP_PREFIX, `Meta info key "${key}" is too long, trimmed to 255 characters`);
|
|
210
|
+
return [newKey, value];
|
|
211
|
+
}
|
|
212
|
+
return [key, value];
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
debug(constants_js_1.APP_PREFIX, `Error while processing meta info key ${key}`, err);
|
|
216
|
+
return [null, null];
|
|
217
|
+
}
|
|
218
|
+
})
|
|
189
219
|
.reduce((acc, [key, value]) => {
|
|
190
220
|
if (key)
|
|
191
221
|
acc[key] = value;
|
|
@@ -193,6 +223,7 @@ class Client {
|
|
|
193
223
|
}, {});
|
|
194
224
|
// Get links from storage using the test context
|
|
195
225
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
226
|
+
const links = links_js_1.linkStorage.get(testContext) || [];
|
|
196
227
|
let errorFormatted = '';
|
|
197
228
|
if (error) {
|
|
198
229
|
errorFormatted += this.formatError(error) || '';
|
package/lib/data-storage.js
CHANGED
|
@@ -143,7 +143,7 @@ class DataStorage {
|
|
|
143
143
|
if (global?.testomatioDataStore[dataType]) {
|
|
144
144
|
const testData = global.testomatioDataStore[dataType][context];
|
|
145
145
|
if (testData)
|
|
146
|
-
debug(
|
|
146
|
+
debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
|
|
147
147
|
return testData || [];
|
|
148
148
|
}
|
|
149
149
|
// debug(`No ${this.dataType} data for context ${context} in <global> storage`);
|
|
@@ -165,7 +165,7 @@ class DataStorage {
|
|
|
165
165
|
if (fs_1.default.existsSync(filepath)) {
|
|
166
166
|
const testDataAsText = fs_1.default.readFileSync(filepath, 'utf-8');
|
|
167
167
|
if (testDataAsText)
|
|
168
|
-
debug(
|
|
168
|
+
debug(`"${dataType}" data for context "${context}":`, testDataAsText);
|
|
169
169
|
const testDataArr = testDataAsText?.split(os_1.default.EOL) || [];
|
|
170
170
|
return testDataArr;
|
|
171
171
|
}
|
|
@@ -184,7 +184,7 @@ class DataStorage {
|
|
|
184
184
|
* @param {*} context
|
|
185
185
|
*/
|
|
186
186
|
#putDataToGlobalVar(dataType, data, context) {
|
|
187
|
-
debug('
|
|
187
|
+
debug('Saving data to global variable for ', context, ':', data);
|
|
188
188
|
if (!global.testomatioDataStore)
|
|
189
189
|
global.testomatioDataStore = {};
|
|
190
190
|
if (!global.testomatioDataStore?.[dataType])
|
|
@@ -208,7 +208,7 @@ class DataStorage {
|
|
|
208
208
|
const filepath = (0, path_1.join)(dataDirPath, filename);
|
|
209
209
|
if (!fs_1.default.existsSync(dataDirPath))
|
|
210
210
|
utils_js_1.fileSystem.createDir(dataDirPath);
|
|
211
|
-
debug(
|
|
211
|
+
debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
|
|
212
212
|
// append new line if file already exists (in this case its definitely includes some data)
|
|
213
213
|
if (fs_1.default.existsSync(filepath)) {
|
|
214
214
|
fs_1.default.appendFileSync(filepath, os_1.default.EOL + data, 'utf-8');
|
|
@@ -222,7 +222,7 @@ function stringToMD5Hash(str) {
|
|
|
222
222
|
const md5 = crypto_1.default.createHash('md5');
|
|
223
223
|
md5.update(str);
|
|
224
224
|
const hash = md5.digest('hex');
|
|
225
|
-
return
|
|
225
|
+
return hash;
|
|
226
226
|
}
|
|
227
227
|
exports.dataStorage = DataStorage.getInstance();
|
|
228
228
|
// TODO: consider using fs promises instead of writeSync/appendFileSync to
|
|
@@ -5,7 +5,6 @@ declare namespace _default {
|
|
|
5
5
|
export { setKeyValue as keyValue };
|
|
6
6
|
export { setLabel as label };
|
|
7
7
|
export { linkTest };
|
|
8
|
-
export { linkJira };
|
|
9
8
|
}
|
|
10
9
|
export default _default;
|
|
11
10
|
/**
|
|
@@ -41,21 +40,15 @@ declare function setKeyValue(keyValue: {
|
|
|
41
40
|
[key: string]: string;
|
|
42
41
|
} | string, value?: string | null): void;
|
|
43
42
|
/**
|
|
44
|
-
* Add
|
|
43
|
+
* Add label(s) to the test report
|
|
45
44
|
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
46
|
-
* @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
|
|
45
|
+
* @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
|
|
47
46
|
* @returns {void}
|
|
48
47
|
*/
|
|
49
|
-
declare function setLabel(key: string, value?: string | null): void;
|
|
48
|
+
declare function setLabel(key: string, value?: string | string[] | null): void;
|
|
50
49
|
/**
|
|
51
50
|
* Add link(s) to the test report
|
|
52
51
|
* @param {...string} testIds - test IDs to link
|
|
53
52
|
* @returns {void}
|
|
54
53
|
*/
|
|
55
54
|
declare function linkTest(...testIds: string[]): void;
|
|
56
|
-
/**
|
|
57
|
-
* Add JIRA issue link(s) to the test report
|
|
58
|
-
* @param {...string} jiraIds - JIRA issue IDs to link
|
|
59
|
-
* @returns {void}
|
|
60
|
-
*/
|
|
61
|
-
declare function linkJira(...jiraIds: string[]): void;
|
|
@@ -50,14 +50,15 @@ function setKeyValue(keyValue, value = null) {
|
|
|
50
50
|
index_js_1.services.keyValues.put(keyValue);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Add
|
|
53
|
+
* Add label(s) to the test report
|
|
54
54
|
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
55
|
-
* @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
|
|
55
|
+
* @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
|
|
56
56
|
* @returns {void}
|
|
57
57
|
*/
|
|
58
58
|
function setLabel(key, value = null) {
|
|
59
59
|
if (Array.isArray(value)) {
|
|
60
|
-
|
|
60
|
+
value.forEach(val => setLabel(key, val));
|
|
61
|
+
return;
|
|
61
62
|
}
|
|
62
63
|
const labelObject = value !== null && value !== undefined && value !== ''
|
|
63
64
|
? { label: `${key}:${value}` }
|
|
@@ -73,15 +74,6 @@ function linkTest(...testIds) {
|
|
|
73
74
|
const links = testIds.map(testId => ({ test: testId }));
|
|
74
75
|
index_js_1.services.links.put(links);
|
|
75
76
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Add JIRA issue link(s) to the test report
|
|
78
|
-
* @param {...string} jiraIds - JIRA issue IDs to link
|
|
79
|
-
* @returns {void}
|
|
80
|
-
*/
|
|
81
|
-
function linkJira(...jiraIds) {
|
|
82
|
-
const links = jiraIds.map(jiraId => ({ jira: jiraId }));
|
|
83
|
-
index_js_1.services.links.put(links);
|
|
84
|
-
}
|
|
85
77
|
module.exports = {
|
|
86
78
|
artifact: saveArtifact,
|
|
87
79
|
log: logMessage,
|
|
@@ -89,5 +81,4 @@ module.exports = {
|
|
|
89
81
|
keyValue: setKeyValue,
|
|
90
82
|
label: setLabel,
|
|
91
83
|
linkTest,
|
|
92
|
-
linkJira,
|
|
93
84
|
};
|
package/lib/reporter.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const artifact: (data: string | {
|
|
|
5
5
|
}, context?: any) => void;
|
|
6
6
|
export const log: (...args: any[]) => void;
|
|
7
7
|
export const logger: {
|
|
8
|
-
"__#
|
|
8
|
+
"__#14@#originalUserLogger": {
|
|
9
9
|
assert(condition?: boolean, ...data: any[]): void;
|
|
10
10
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
11
11
|
clear(): void;
|
|
@@ -50,13 +50,13 @@ export const logger: {
|
|
|
50
50
|
profile(label?: string): void;
|
|
51
51
|
profileEnd(label?: string): void;
|
|
52
52
|
};
|
|
53
|
-
"__#
|
|
53
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
54
54
|
logLevel: string;
|
|
55
55
|
step(strings: any, ...values: any[]): void;
|
|
56
56
|
getLogs(context: string): string[];
|
|
57
|
-
"__#
|
|
57
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
58
58
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
59
|
-
"__#
|
|
59
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
60
60
|
assert(...args: any[]): void;
|
|
61
61
|
debug(...args: any[]): void;
|
|
62
62
|
error(...args: any[]): void;
|
|
@@ -76,11 +76,11 @@ export const meta: (keyValue: {
|
|
|
76
76
|
[key: string]: string;
|
|
77
77
|
} | string, value?: string | null) => void;
|
|
78
78
|
export const step: (message: string) => void;
|
|
79
|
-
export const label: (key: string, value?: string | null) => void;
|
|
79
|
+
export const label: (key: string, value?: string | string[] | null) => void;
|
|
80
80
|
export const linkTest: (...testIds: string[]) => void;
|
|
81
81
|
declare namespace _default {
|
|
82
82
|
let testomatioLogger: {
|
|
83
|
-
"__#
|
|
83
|
+
"__#14@#originalUserLogger": {
|
|
84
84
|
assert(condition?: boolean, ...data: any[]): void;
|
|
85
85
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
86
86
|
clear(): void;
|
|
@@ -125,13 +125,13 @@ declare namespace _default {
|
|
|
125
125
|
profile(label?: string): void;
|
|
126
126
|
profileEnd(label?: string): void;
|
|
127
127
|
};
|
|
128
|
-
"__#
|
|
128
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
129
129
|
logLevel: string;
|
|
130
130
|
step(strings: any, ...values: any[]): void;
|
|
131
131
|
getLogs(context: string): string[];
|
|
132
|
-
"__#
|
|
132
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
133
133
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
134
|
-
"__#
|
|
134
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
135
135
|
assert(...args: any[]): void;
|
|
136
136
|
debug(...args: any[]): void;
|
|
137
137
|
error(...args: any[]): void;
|
|
@@ -154,7 +154,7 @@ declare namespace _default {
|
|
|
154
154
|
}, context?: any) => void;
|
|
155
155
|
let log: (...args: any[]) => void;
|
|
156
156
|
let logger: {
|
|
157
|
-
"__#
|
|
157
|
+
"__#14@#originalUserLogger": {
|
|
158
158
|
assert(condition?: boolean, ...data: any[]): void;
|
|
159
159
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
160
160
|
clear(): void;
|
|
@@ -199,13 +199,13 @@ declare namespace _default {
|
|
|
199
199
|
profile(label?: string): void;
|
|
200
200
|
profileEnd(label?: string): void;
|
|
201
201
|
};
|
|
202
|
-
"__#
|
|
202
|
+
"__#14@#userLoggerWithOverridenMethods": any;
|
|
203
203
|
logLevel: string;
|
|
204
204
|
step(strings: any, ...values: any[]): void;
|
|
205
205
|
getLogs(context: string): string[];
|
|
206
|
-
"__#
|
|
206
|
+
"__#14@#stringifyLogs"(...args: any[]): string;
|
|
207
207
|
_templateLiteralLog(strings: any, ...args: any[]): void;
|
|
208
|
-
"__#
|
|
208
|
+
"__#14@#logWrapper"(argsArray: any, level: any): void;
|
|
209
209
|
assert(...args: any[]): void;
|
|
210
210
|
debug(...args: any[]): void;
|
|
211
211
|
error(...args: any[]): void;
|
|
@@ -225,7 +225,7 @@ declare namespace _default {
|
|
|
225
225
|
[key: string]: string;
|
|
226
226
|
} | string, value?: string | null) => void;
|
|
227
227
|
let step: (message: string) => void;
|
|
228
|
-
let label: (key: string, value?: string | null) => void;
|
|
228
|
+
let label: (key: string, value?: string | string[] | null) => void;
|
|
229
229
|
let linkTest: (...testIds: string[]) => void;
|
|
230
230
|
}
|
|
231
231
|
export default _default;
|
package/lib/services/labels.d.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const labelStorage: LabelStorage;
|
|
2
|
+
declare class LabelStorage {
|
|
3
|
+
static "__#19@#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 {};
|
package/lib/services/labels.js
CHANGED
|
@@ -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('links', 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('links', 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/lib/services/links.d.ts
CHANGED
package/lib/services/logger.d.ts
CHANGED
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -113,7 +113,6 @@ class CucumberReporter extends Formatter {
|
|
|
113
113
|
const logs = services.logger.getLogs(testTitle).join('\n');
|
|
114
114
|
const artifacts = services.artifacts.get(testTitle);
|
|
115
115
|
const keyValues = services.keyValues.get(testTitle);
|
|
116
|
-
const links = services.links.get(testTitle);
|
|
117
116
|
|
|
118
117
|
this.client.addTestRun(status, {
|
|
119
118
|
// error: testCaseAttempt.worstTestStepResult.message,
|
|
@@ -124,7 +123,6 @@ class CucumberReporter extends Formatter {
|
|
|
124
123
|
.trim(),
|
|
125
124
|
example: { ...example },
|
|
126
125
|
logs,
|
|
127
|
-
links,
|
|
128
126
|
manuallyAttachedArtifacts: artifacts,
|
|
129
127
|
meta: keyValues,
|
|
130
128
|
title: scenario,
|
package/src/adapter/jest.js
CHANGED
|
@@ -59,7 +59,6 @@ export class JestReporter {
|
|
|
59
59
|
const logs = getTestLogs(result);
|
|
60
60
|
const artifacts = services.artifacts.get(result.fullName);
|
|
61
61
|
const keyValues = services.keyValues.get(result.fullName);
|
|
62
|
-
const links = services.links.get(result.fullName);
|
|
63
62
|
|
|
64
63
|
const deducedStatus = status === 'pending' ? 'skipped' : status;
|
|
65
64
|
// In jest if test is not matched with test name pattern it is considered as skipped.
|
|
@@ -73,7 +72,6 @@ export class JestReporter {
|
|
|
73
72
|
title,
|
|
74
73
|
time: duration,
|
|
75
74
|
logs,
|
|
76
|
-
links,
|
|
77
75
|
manuallyAttachedArtifacts: artifacts,
|
|
78
76
|
meta: keyValues,
|
|
79
77
|
});
|
|
@@ -51,6 +51,9 @@ class PlaywrightReporter {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Extract and normalize tags
|
|
55
|
+
const tags = extractTags(test);
|
|
56
|
+
|
|
54
57
|
const fullTestTitle = getTestContextName(test);
|
|
55
58
|
let logs = '';
|
|
56
59
|
if (result.stderr.length || result.stdout.length) {
|
|
@@ -58,7 +61,6 @@ class PlaywrightReporter {
|
|
|
58
61
|
}
|
|
59
62
|
const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
|
|
60
63
|
const testMeta = services.keyValues.get(fullTestTitle);
|
|
61
|
-
const links = services.links.get(fullTestTitle);
|
|
62
64
|
const rid = test.id || test.testId || uuidv4();
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -90,13 +92,13 @@ class PlaywrightReporter {
|
|
|
90
92
|
const reportTestPromise = this.client.addTestRun(checkStatus(status), {
|
|
91
93
|
rid: `${rid}-${project.name}`,
|
|
92
94
|
error,
|
|
93
|
-
test_id: getTestomatIdFromTestTitle(`${title} ${
|
|
95
|
+
test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
|
|
94
96
|
suite_title,
|
|
95
97
|
title,
|
|
98
|
+
tags,
|
|
96
99
|
steps: steps.length ? steps : undefined,
|
|
97
100
|
time: duration,
|
|
98
101
|
logs,
|
|
99
|
-
links,
|
|
100
102
|
manuallyAttachedArtifacts,
|
|
101
103
|
meta: {
|
|
102
104
|
browser: project.browser,
|
|
@@ -243,6 +245,45 @@ function generateTmpFilepath(filename = '') {
|
|
|
243
245
|
return path.join(tmpdir, filename);
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Extracts and normalizes tags from test title, test options, and suite level
|
|
250
|
+
* @param {*} test - testInfo object from Playwright
|
|
251
|
+
* @returns {string[]} - array of normalized tags
|
|
252
|
+
*/
|
|
253
|
+
function extractTags(test) {
|
|
254
|
+
const tagsSet = new Set();
|
|
255
|
+
|
|
256
|
+
// Extract tags from test title (@tag format)
|
|
257
|
+
const titleTagsMatch = test.title.match(/@\w+/g);
|
|
258
|
+
if (titleTagsMatch) {
|
|
259
|
+
titleTagsMatch.forEach(tag => {
|
|
260
|
+
tagsSet.add(tag.replace('@', '').toLowerCase());
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Extract tags from test.tags (Playwright built-in tags)
|
|
265
|
+
if (test.tags && Array.isArray(test.tags)) {
|
|
266
|
+
test.tags.forEach(tag => {
|
|
267
|
+
const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
|
|
268
|
+
tagsSet.add(normalizedTag);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Extract tags from suite/describe level (inherited tags)
|
|
273
|
+
let parent = test.parent;
|
|
274
|
+
while (parent) {
|
|
275
|
+
if (parent.tags && Array.isArray(parent.tags)) {
|
|
276
|
+
parent.tags.forEach(tag => {
|
|
277
|
+
const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
|
|
278
|
+
tagsSet.add(normalizedTag);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
parent = parent.parent;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Array.from(tagsSet);
|
|
285
|
+
}
|
|
286
|
+
|
|
246
287
|
/**
|
|
247
288
|
* Returns filename + test title
|
|
248
289
|
* @param {*} test - testInfo object from Playwright
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -52,10 +52,14 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
52
52
|
onTestEnd(test) {
|
|
53
53
|
test.suite = test.parent;
|
|
54
54
|
const logs = getTestLogs(test.fullTitle);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// TODO: FIX: artifacts for some reason leads to empty report on Testomat.io
|
|
56
|
+
// ^ not reproduced anymore (Jul 2025)
|
|
57
|
+
// but still be under investigation
|
|
58
|
+
const artifacts = services.artifacts.get(test.fullTitle);
|
|
59
|
+
const keyValues = services.keyValues.get(test.fullTitle);
|
|
58
60
|
test.logs = logs;
|
|
61
|
+
test.artifacts = artifacts;
|
|
62
|
+
test.meta = keyValues;
|
|
59
63
|
|
|
60
64
|
this._addTestPromises.push(this.addTest(test));
|
|
61
65
|
}
|
|
@@ -70,7 +74,7 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
70
74
|
async addTest(test) {
|
|
71
75
|
if (!this.client) return;
|
|
72
76
|
|
|
73
|
-
const { title, _duration: duration, state, error, output
|
|
77
|
+
const { title, _duration: duration, state, error, output } = test;
|
|
74
78
|
|
|
75
79
|
const testId = getTestomatIdFromTestTitle(title);
|
|
76
80
|
|
|
@@ -81,11 +85,10 @@ class WebdriverReporter extends WDIOReporter {
|
|
|
81
85
|
|
|
82
86
|
await this.client.addTestRun(state, {
|
|
83
87
|
rid: test.uid || '',
|
|
84
|
-
manuallyAttachedArtifacts: artifacts,
|
|
88
|
+
manuallyAttachedArtifacts: test.artifacts,
|
|
85
89
|
error,
|
|
86
|
-
logs,
|
|
87
|
-
meta,
|
|
88
|
-
links,
|
|
90
|
+
logs: test.logs,
|
|
91
|
+
meta: test.meta,
|
|
89
92
|
title,
|
|
90
93
|
test_id: testId,
|
|
91
94
|
time: duration,
|
package/src/client.js
CHANGED
|
@@ -11,6 +11,7 @@ import path, { sep } from 'path';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { S3Uploader } from './uploader.js';
|
|
13
13
|
import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
|
|
14
|
+
import { linkStorage } from './services/links.js';
|
|
14
15
|
import { filesize as prettyBytes } from 'filesize';
|
|
15
16
|
|
|
16
17
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
@@ -181,7 +182,6 @@ class Client {
|
|
|
181
182
|
suite_id,
|
|
182
183
|
test_id,
|
|
183
184
|
timestamp,
|
|
184
|
-
links,
|
|
185
185
|
manuallyAttachedArtifacts,
|
|
186
186
|
overwrite,
|
|
187
187
|
} = testData;
|
|
@@ -190,6 +190,35 @@ 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
|
+
})
|
|
193
222
|
.reduce((acc, [key, value]) => {
|
|
194
223
|
if (key) acc[key] = value;
|
|
195
224
|
return acc;
|
|
@@ -197,6 +226,7 @@ class Client {
|
|
|
197
226
|
|
|
198
227
|
// Get links from storage using the test context
|
|
199
228
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
229
|
+
const links = linkStorage.get(testContext) || [];
|
|
200
230
|
|
|
201
231
|
let errorFormatted = '';
|
|
202
232
|
if (error) {
|
package/src/data-storage.js
CHANGED
|
@@ -116,7 +116,7 @@ class DataStorage {
|
|
|
116
116
|
try {
|
|
117
117
|
if (global?.testomatioDataStore[dataType]) {
|
|
118
118
|
const testData = global.testomatioDataStore[dataType][context];
|
|
119
|
-
if (testData) debug(
|
|
119
|
+
if (testData) debug(`"${dataType}" data for constext "${context}":`, testData.join(', '));
|
|
120
120
|
return testData || [];
|
|
121
121
|
}
|
|
122
122
|
// debug(`No ${this.dataType} data for context ${context} in <global> storage`);
|
|
@@ -137,7 +137,7 @@ class DataStorage {
|
|
|
137
137
|
const filepath = join(dataDirPath, `${dataType}_${context}`);
|
|
138
138
|
if (fs.existsSync(filepath)) {
|
|
139
139
|
const testDataAsText = fs.readFileSync(filepath, 'utf-8');
|
|
140
|
-
if (testDataAsText) debug(
|
|
140
|
+
if (testDataAsText) debug(`"${dataType}" data for context "${context}":`, testDataAsText);
|
|
141
141
|
const testDataArr = testDataAsText?.split(os.EOL) || [];
|
|
142
142
|
return testDataArr;
|
|
143
143
|
}
|
|
@@ -156,7 +156,7 @@ class DataStorage {
|
|
|
156
156
|
* @param {*} context
|
|
157
157
|
*/
|
|
158
158
|
#putDataToGlobalVar(dataType, data, context) {
|
|
159
|
-
debug('
|
|
159
|
+
debug('Saving data to global variable for ', context, ':', data);
|
|
160
160
|
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
161
161
|
if (!global.testomatioDataStore?.[dataType]) global.testomatioDataStore[dataType] = {};
|
|
162
162
|
|
|
@@ -177,7 +177,7 @@ class DataStorage {
|
|
|
177
177
|
const filename = `${dataType}_${context}`;
|
|
178
178
|
const filepath = join(dataDirPath, filename);
|
|
179
179
|
if (!fs.existsSync(dataDirPath)) fileSystem.createDir(dataDirPath);
|
|
180
|
-
debug(
|
|
180
|
+
debug(`Saving data to file for context "${context}" to ${filepath}. Data: ${JSON.stringify(data)}`);
|
|
181
181
|
|
|
182
182
|
// append new line if file already exists (in this case its definitely includes some data)
|
|
183
183
|
if (fs.existsSync(filepath)) {
|
|
@@ -192,7 +192,8 @@ function stringToMD5Hash(str) {
|
|
|
192
192
|
const md5 = crypto.createHash('md5');
|
|
193
193
|
md5.update(str);
|
|
194
194
|
const hash = md5.digest('hex');
|
|
195
|
-
|
|
195
|
+
|
|
196
|
+
return hash;
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
export const dataStorage = DataStorage.getInstance();
|
|
@@ -53,21 +53,24 @@ function setKeyValue(keyValue, value = null) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Add
|
|
56
|
+
* Add label(s) to the test report
|
|
57
57
|
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
58
|
-
* @param {string|null} [value=null] - optional label value (e.g. 'high', 'login')
|
|
58
|
+
* @param {string|string[]|null} [value=null] - optional label value(s) (e.g. 'high', 'login') or array of values
|
|
59
59
|
* @returns {void}
|
|
60
60
|
*/
|
|
61
61
|
function setLabel(key, value = null) {
|
|
62
62
|
if (Array.isArray(value)) {
|
|
63
|
-
|
|
63
|
+
value.forEach(val => setLabel(key, val));
|
|
64
|
+
return;
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
|
|
67
|
+
const labelObject = value !== null && value !== undefined && value !== ''
|
|
68
|
+
? { label: `${key}:${value}` }
|
|
67
69
|
: { label: key };
|
|
68
70
|
services.links.put([labelObject]);
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
|
|
71
74
|
/**
|
|
72
75
|
* Add link(s) to the test report
|
|
73
76
|
* @param {...string} testIds - test IDs to link
|
|
@@ -78,16 +81,6 @@ function linkTest(...testIds) {
|
|
|
78
81
|
services.links.put(links);
|
|
79
82
|
}
|
|
80
83
|
|
|
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);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
84
|
export default {
|
|
92
85
|
artifact: saveArtifact,
|
|
93
86
|
log: logMessage,
|
|
@@ -95,5 +88,4 @@ export default {
|
|
|
95
88
|
keyValue: setKeyValue,
|
|
96
89
|
label: setLabel,
|
|
97
90
|
linkTest,
|
|
98
|
-
linkJira,
|
|
99
91
|
};
|
package/src/services/labels.js
CHANGED
|
@@ -1 +1,59 @@
|
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
|
+
import { dataStorage } from '../data-storage.js';
|
|
1
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('links', 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('links', 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();
|