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