testbeats 2.2.9 → 2.3.1
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/package.json +1 -1
- package/src/beats/beats.api.js +43 -0
- package/src/cli.js +3 -0
- package/src/commands/base.command.js +47 -0
- package/src/commands/manual-sync.command.js +262 -30
- package/src/commands/publish.command.js +6 -29
- package/src/manual/parsers/base.js +39 -0
- package/src/manual/parsers/gherkin.js +18 -8
- package/src/manual/sync.helper.js +51 -6
- package/src/types.d.ts +104 -0
package/package.json
CHANGED
package/src/beats/beats.api.js
CHANGED
|
@@ -9,6 +9,15 @@ class BeatsApi {
|
|
|
9
9
|
this.config = config;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
searchProjects(search_text) {
|
|
13
|
+
return request.get({
|
|
14
|
+
url: `${this.getBaseUrl()}/api/core/v1/projects?search_text=${search_text}`,
|
|
15
|
+
headers: {
|
|
16
|
+
'x-api-key': this.config.api_key
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
postTestRun(payload) {
|
|
13
22
|
return request.post({
|
|
14
23
|
url: `${this.getBaseUrl()}/api/core/v1/test-runs`,
|
|
@@ -75,6 +84,40 @@ class BeatsApi {
|
|
|
75
84
|
}
|
|
76
85
|
});
|
|
77
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import('../types').IManualSyncComparePayload} payload
|
|
90
|
+
* @returns {Promise<import('../types').IManualSyncCompareResponse>}
|
|
91
|
+
*/
|
|
92
|
+
compareManualTests(payload) {
|
|
93
|
+
return request.post({
|
|
94
|
+
url: `${this.getBaseUrl()}/api/core/v1/manual/sync/compare`,
|
|
95
|
+
headers: {
|
|
96
|
+
'x-api-key': this.config.api_key
|
|
97
|
+
},
|
|
98
|
+
body: payload
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
syncManualFolders(payload) {
|
|
103
|
+
return request.post({
|
|
104
|
+
url: `${this.getBaseUrl()}/api/core/v1/manual/sync/folders`,
|
|
105
|
+
headers: {
|
|
106
|
+
'x-api-key': this.config.api_key
|
|
107
|
+
},
|
|
108
|
+
body: payload
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
syncManualTestSuites(payload) {
|
|
113
|
+
return request.post({
|
|
114
|
+
url: `${this.getBaseUrl()}/api/core/v1/manual/sync/suites`,
|
|
115
|
+
headers: {
|
|
116
|
+
'x-api-key': this.config.api_key
|
|
117
|
+
},
|
|
118
|
+
body: payload
|
|
119
|
+
});
|
|
120
|
+
}
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
module.exports = { BeatsApi }
|
package/src/cli.js
CHANGED
|
@@ -68,6 +68,9 @@ prog.command('init')
|
|
|
68
68
|
prog.command('manual sync')
|
|
69
69
|
.describe('Sync manual test cases from gherkin files in a directory')
|
|
70
70
|
.option('-p, --path', 'Path to directory to sync (default: current directory)')
|
|
71
|
+
.option('-c, --config', 'path to config file')
|
|
72
|
+
.option('--api-key', 'api key')
|
|
73
|
+
.option('--project', 'project name')
|
|
71
74
|
.example('manual sync')
|
|
72
75
|
.example('manual sync --path ./tests/features')
|
|
73
76
|
.action(async (opts) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const pkg = require('../../package.json');
|
|
2
|
+
const { MIN_NODE_VERSION } = require('../helpers/constants');
|
|
3
|
+
const { ConfigBuilder } = require('../utils/config.builder');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
class BaseCommand {
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
printBanner() {
|
|
13
|
+
const banner = `
|
|
14
|
+
_____ _ ___ _
|
|
15
|
+
(_ _) ( )_ ( _'\\ ( )_
|
|
16
|
+
| | __ ___ | ,_)| (_) ) __ _ _ | ,_) ___
|
|
17
|
+
| | /'__'\\/',__)| | | _ <' /'__'\\ /'_' )| | /',__)
|
|
18
|
+
| |( ___/\\__, \\| |_ | (_) )( ___/( (_| || |_ \\__, \\
|
|
19
|
+
(_)'\\____)(____/'\\__)(____/''\\____)'\\__,_)'\\__)(____/
|
|
20
|
+
|
|
21
|
+
v${pkg.version}
|
|
22
|
+
`;
|
|
23
|
+
console.log(banner);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validateEnvDetails() {
|
|
27
|
+
try {
|
|
28
|
+
const current_major_version = parseInt(process.version.split('.')[0].replace('v', ''));
|
|
29
|
+
if (current_major_version >= MIN_NODE_VERSION) {
|
|
30
|
+
logger.info(`💻 NodeJS: ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, Arch: ${os.machine()}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.warn(`⚠️ Unable to verify NodeJS version: ${error.message}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
buildConfig() {
|
|
41
|
+
const config_builder = new ConfigBuilder(this.opts);
|
|
42
|
+
config_builder.build();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { BaseCommand };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
1
|
const { ManualSyncHelper } = require('../manual/sync.helper');
|
|
3
2
|
const logger = require('../utils/logger');
|
|
3
|
+
const { BeatsApi } = require('../beats/beats.api');
|
|
4
|
+
const { BaseCommand } = require('./base.command');
|
|
4
5
|
|
|
5
|
-
class ManualSyncCommand {
|
|
6
|
+
class ManualSyncCommand extends BaseCommand {
|
|
6
7
|
/**
|
|
7
8
|
* @param {Object} opts - Command options
|
|
8
9
|
*/
|
|
9
10
|
constructor(opts) {
|
|
10
|
-
|
|
11
|
+
super(opts);
|
|
11
12
|
this.syncHelper = new ManualSyncHelper();
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -16,6 +17,10 @@ class ManualSyncCommand {
|
|
|
16
17
|
*/
|
|
17
18
|
async execute() {
|
|
18
19
|
try {
|
|
20
|
+
this.printBanner();
|
|
21
|
+
this.validateEnvDetails();
|
|
22
|
+
this.buildConfig();
|
|
23
|
+
|
|
19
24
|
logger.info('🔄 Starting manual test case sync...');
|
|
20
25
|
|
|
21
26
|
const targetPath = this.opts.path || '.';
|
|
@@ -23,17 +28,29 @@ class ManualSyncCommand {
|
|
|
23
28
|
|
|
24
29
|
const result = await this.syncHelper.scanDirectory(targetPath);
|
|
25
30
|
|
|
31
|
+
const counts = this.getCounts(result);
|
|
32
|
+
logger.info(`📊 Found ${counts.totalTestCases} test cases, ${counts.totalTestSuites} test suites, and ${counts.totalFolders} folders`);
|
|
33
|
+
|
|
26
34
|
// Format output as specified in requirements
|
|
27
35
|
const output = {
|
|
28
|
-
folders:
|
|
36
|
+
folders: result.folders
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
// Display results
|
|
32
|
-
this.displayResults(output);
|
|
33
39
|
|
|
34
|
-
logger.info('✅ Manual sync completed successfully!');
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
const beats = new BeatsApi(this.opts.config);
|
|
42
|
+
const projectsResponse = await beats.searchProjects(this.opts.config.project);
|
|
43
|
+
const project = projectsResponse.values.find(p => p.name === this.opts.config.project);
|
|
44
|
+
if (!project) {
|
|
45
|
+
throw new Error(`Project ${this.opts.config.project} not found`);
|
|
46
|
+
}
|
|
47
|
+
this.opts.config.project = project.id;
|
|
48
|
+
|
|
49
|
+
const compare = new ManualSyncCompare(this.opts.config);
|
|
50
|
+
const compareResult = await compare.compare(output);
|
|
51
|
+
|
|
52
|
+
const resources = new ManualSyncResources(this.opts.config);
|
|
53
|
+
await resources.sync(output, compareResult);
|
|
37
54
|
|
|
38
55
|
} catch (error) {
|
|
39
56
|
logger.error(`❌ Manual sync failed: ${error.message}`);
|
|
@@ -41,39 +58,254 @@ class ManualSyncCommand {
|
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
getCounts(result, counts = { totalTestCases: 0, totalTestSuites: 0, totalFolders: 0 }) {
|
|
62
|
+
// return the total number of test cases, test suites, and folders
|
|
63
|
+
for (const folder of result.folders) {
|
|
64
|
+
counts.totalTestCases += folder.test_suites.reduce((acc, suite) => acc + suite.test_cases.length, 0);
|
|
65
|
+
counts.totalTestSuites += folder.test_suites.length;
|
|
66
|
+
counts.totalFolders += folder.folders.length;
|
|
67
|
+
this.getCounts(folder, counts);
|
|
68
|
+
}
|
|
69
|
+
return counts;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class ManualSyncCompare {
|
|
74
|
+
|
|
75
|
+
constructor(config) {
|
|
76
|
+
this.config = config;
|
|
77
|
+
this.beats = new BeatsApi(this.config);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
82
|
+
*/
|
|
83
|
+
async compare(structure) {
|
|
84
|
+
const payload = this.#buildPayload(structure);
|
|
85
|
+
const response = await this.beats.compareManualTests(payload);
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the payload for the compare manual tests API
|
|
92
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
93
|
+
* @returns {import('../types').IManualSyncComparePayload}
|
|
94
|
+
*/
|
|
95
|
+
#buildPayload(structure) {
|
|
96
|
+
const payload = {
|
|
97
|
+
project_id: this.config.project,
|
|
98
|
+
folders: [],
|
|
99
|
+
};
|
|
100
|
+
for (const folder of structure.folders) {
|
|
101
|
+
payload.folders.push(this.#buildPayloadFolder(folder));
|
|
102
|
+
}
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the payload for a folder
|
|
108
|
+
* @param {import('../types').IManualTestFolder} folder
|
|
109
|
+
* @returns {import('../types').IManualSyncCompareFolder}
|
|
110
|
+
*/
|
|
111
|
+
#buildPayloadFolder(folder) {
|
|
112
|
+
return {
|
|
113
|
+
name: folder.name,
|
|
114
|
+
hash: folder.hash,
|
|
115
|
+
test_suites: folder.test_suites.map(testSuite => this.#buildPayloadTestSuite(testSuite)),
|
|
116
|
+
folders: folder.folders.map(subFolder => this.#buildPayloadFolder(subFolder))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build the payload for a test suite
|
|
122
|
+
* @param {import('../types').IManualTestSuite} testSuite
|
|
123
|
+
* @returns {import('../types').IManualSyncCompareTestSuite}
|
|
124
|
+
*/
|
|
125
|
+
#buildPayloadTestSuite(testSuite) {
|
|
126
|
+
return {
|
|
127
|
+
name: testSuite.name,
|
|
128
|
+
hash: testSuite.hash,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class ManualSyncResources {
|
|
134
|
+
constructor(config) {
|
|
135
|
+
this.config = config;
|
|
136
|
+
this.beats = new BeatsApi(this.config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sync the manual resources
|
|
141
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
142
|
+
* @param {import('../types').IManualSyncCompareResponse} compareResult
|
|
143
|
+
*/
|
|
144
|
+
async sync(structure, compareResult) {
|
|
145
|
+
this.#mergeStructure(structure, compareResult);
|
|
146
|
+
await this.#syncFolders(structure.folders);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Merge the structure with the compare result
|
|
151
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
152
|
+
* @param {import('../types').IManualSyncCompareResponse} compareResult
|
|
153
|
+
*/
|
|
154
|
+
#mergeStructure(structure, compareResult) {
|
|
155
|
+
// Iterate through all folders from compare result and merge with local structure
|
|
156
|
+
for (const serverFolder of compareResult.folders) {
|
|
157
|
+
const localFolder = structure.folders.find(f => f.name === serverFolder.name);
|
|
158
|
+
if (localFolder) {
|
|
159
|
+
this.#mergeFolder(localFolder, serverFolder);
|
|
160
|
+
} else {
|
|
161
|
+
structure.folders.push(this.#convertServerFolderToLocal(serverFolder));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Merge server folder metadata into local folder
|
|
168
|
+
* @param {import('../types').IManualTestFolder} localFolder - Local folder with full data
|
|
169
|
+
* @param {import('../types').IManualSyncCompareResponseFolder} serverFolder - Server folder with metadata
|
|
170
|
+
*/
|
|
171
|
+
#mergeFolder(localFolder, serverFolder) {
|
|
172
|
+
// Enrich local folder with server metadata
|
|
173
|
+
localFolder.id = serverFolder.id;
|
|
174
|
+
localFolder.type = serverFolder.type;
|
|
175
|
+
|
|
176
|
+
// Ensure server folder has test suites and folders
|
|
177
|
+
serverFolder.test_suites = serverFolder.test_suites || [];
|
|
178
|
+
serverFolder.folders = serverFolder.folders || [];
|
|
179
|
+
|
|
180
|
+
for (const serverSuite of serverFolder.test_suites) {
|
|
181
|
+
const localSuite = localFolder.test_suites.find(s => s.name === serverSuite.name);
|
|
182
|
+
if (localSuite) {
|
|
183
|
+
this.#mergeTestSuite(localSuite, serverSuite);
|
|
184
|
+
} else {
|
|
185
|
+
localFolder.test_suites.push(this.#convertServerSuiteToLocal(serverSuite));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const serverSubFolder of serverFolder.folders) {
|
|
190
|
+
const localSubFolder = localFolder.folders.find(f => f.name === serverSubFolder.name);
|
|
191
|
+
if (localSubFolder) {
|
|
192
|
+
this.#mergeFolder(localSubFolder, serverSubFolder);
|
|
193
|
+
} else {
|
|
194
|
+
localFolder.folders.push(this.#convertServerFolderToLocal(serverSubFolder));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
44
199
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {
|
|
200
|
+
* Merge server test suite metadata into local test suite
|
|
201
|
+
* @param {import('../types').IManualTestSuite} localSuite - Local test suite with full data
|
|
202
|
+
* @param {import('../types').IManualSyncCompareResponseTestSuite} serverSuite - Server suite with metadata
|
|
47
203
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
204
|
+
#mergeTestSuite(localSuite, serverSuite) {
|
|
205
|
+
localSuite.id = serverSuite.id;
|
|
206
|
+
localSuite.type = serverSuite.type;
|
|
51
207
|
}
|
|
52
208
|
|
|
53
209
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {
|
|
56
|
-
* @
|
|
210
|
+
* Convert server folder to local folder structure (for delete operations)
|
|
211
|
+
* @param {import('../types').IManualSyncCompareResponseFolder} serverFolder - Server folder
|
|
212
|
+
* @returns {import('../types').IManualTestFolder} Local folder structure
|
|
57
213
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
214
|
+
#convertServerFolderToLocal(serverFolder) {
|
|
215
|
+
return {
|
|
216
|
+
name: serverFolder.name,
|
|
217
|
+
path: '',
|
|
218
|
+
hash: serverFolder.hash,
|
|
219
|
+
id: serverFolder.id,
|
|
220
|
+
type: serverFolder.type,
|
|
221
|
+
test_suites: serverFolder.test_suites?.map(s => this.#convertServerSuiteToLocal(s)) || [],
|
|
222
|
+
folders: serverFolder.folders?.map(f => this.#convertServerFolderToLocal(f)) || []
|
|
223
|
+
};
|
|
224
|
+
}
|
|
60
225
|
|
|
61
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Convert server test suite to local test suite structure (for delete operations)
|
|
228
|
+
* @param {import('../types').IManualSyncCompareResponseTestSuite} serverSuite - Server suite
|
|
229
|
+
* @returns {import('../types').IManualTestSuite} Local test suite structure
|
|
230
|
+
*/
|
|
231
|
+
#convertServerSuiteToLocal(serverSuite) {
|
|
232
|
+
return {
|
|
233
|
+
name: serverSuite.name,
|
|
234
|
+
tags: [],
|
|
235
|
+
before_each: [],
|
|
236
|
+
test_cases: [],
|
|
237
|
+
hash: serverSuite.hash,
|
|
238
|
+
id: serverSuite.id,
|
|
239
|
+
type: serverSuite.type
|
|
240
|
+
};
|
|
241
|
+
}
|
|
62
242
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
243
|
+
async #syncFolders(folders) {
|
|
244
|
+
const payload = {
|
|
245
|
+
project_id: this.config.project,
|
|
246
|
+
folders: folders.map(folder => ({
|
|
247
|
+
type: folder.type,
|
|
248
|
+
id: folder.id,
|
|
249
|
+
name: folder.name,
|
|
250
|
+
hash: folder.hash,
|
|
251
|
+
parent_folder_id: folder.parent_folder_id,
|
|
252
|
+
})),
|
|
253
|
+
};
|
|
254
|
+
const response = await this.beats.syncManualFolders(payload);
|
|
255
|
+
const results = response.results;
|
|
256
|
+
for (const result of results) {
|
|
257
|
+
if (result.success) {
|
|
258
|
+
logger.info(`✅ Folder '${result.name}' synced successfully`);
|
|
259
|
+
const folder = folders.find(f => f.name === result.name);
|
|
260
|
+
if (!folder) {
|
|
261
|
+
console.log(`❌ Folder ${result.name} not found in local structure`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
folder.id = folder.id || result.id;
|
|
265
|
+
for (const testSuite of folder.test_suites) {
|
|
266
|
+
testSuite.folder_id = folder.id;
|
|
267
|
+
}
|
|
268
|
+
await this.#syncTestSuites(folder.test_suites);
|
|
269
|
+
for (const subFolder of folder.folders) {
|
|
270
|
+
subFolder.parent_folder_id = folder.id;
|
|
271
|
+
}
|
|
272
|
+
await this.#syncFolders(folder.folders);
|
|
273
|
+
}
|
|
68
274
|
}
|
|
275
|
+
}
|
|
69
276
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
277
|
+
async #syncTestSuites(testSuites) {
|
|
278
|
+
const payload = {
|
|
279
|
+
project_id: this.config.project,
|
|
280
|
+
suites: testSuites.map(testSuite => ({
|
|
281
|
+
type: testSuite.type,
|
|
282
|
+
name: testSuite.name,
|
|
283
|
+
folder_id: testSuite.folder_id,
|
|
284
|
+
hash: testSuite.hash,
|
|
285
|
+
tags: testSuite.tags,
|
|
286
|
+
before_each: testSuite.before_each.map(step => step.name),
|
|
287
|
+
test_cases: testSuite.test_cases.map(tc => {
|
|
288
|
+
return {
|
|
289
|
+
name: tc.name,
|
|
290
|
+
type: tc.type,
|
|
291
|
+
tags: tc.tags,
|
|
292
|
+
steps: tc.steps.map(step => step.name),
|
|
293
|
+
hash: tc.hash,
|
|
294
|
+
};
|
|
295
|
+
}),
|
|
296
|
+
})),
|
|
297
|
+
};
|
|
298
|
+
const response = await this.beats.syncManualTestSuites(payload);
|
|
299
|
+
const results = response.results;
|
|
300
|
+
for (const result of results) {
|
|
301
|
+
if (result.success) {
|
|
302
|
+
logger.info(`✅ Test Suite '${result.name}' synced successfully`);
|
|
303
|
+
} else {
|
|
304
|
+
logger.error(`❌ Test Suite '${result.name}' failed to sync: ${result.error}`);
|
|
305
|
+
}
|
|
75
306
|
}
|
|
76
307
|
}
|
|
308
|
+
|
|
77
309
|
}
|
|
78
310
|
|
|
79
|
-
module.exports = { ManualSyncCommand };
|
|
311
|
+
module.exports = { ManualSyncCommand, ManualSyncCompare };
|
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const trp = require('test-results-parser');
|
|
3
3
|
const prp = require('performance-results-parser');
|
|
4
|
-
const os = require('os');
|
|
5
4
|
|
|
6
5
|
const beats = require('../beats');
|
|
7
|
-
const { ConfigBuilder } = require('../utils/config.builder');
|
|
8
6
|
const target_manager = require('../targets');
|
|
9
7
|
const logger = require('../utils/logger');
|
|
10
8
|
const { processData } = require('../helpers/helper');
|
|
11
9
|
const { ExtensionsSetup } = require('../setups/extensions.setup');
|
|
12
|
-
const pkg = require('../../package.json');
|
|
13
|
-
const { MIN_NODE_VERSION } = require('../helpers/constants');
|
|
14
10
|
const { sortExtensionsByOrder } = require('../helpers/extension.helper');
|
|
11
|
+
const { BaseCommand } = require('./base.command');
|
|
15
12
|
|
|
16
|
-
class PublishCommand {
|
|
13
|
+
class PublishCommand extends BaseCommand {
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
16
|
* @param {import('../index').CommandLineOptions} opts
|
|
20
17
|
*/
|
|
21
18
|
constructor(opts) {
|
|
22
|
-
|
|
19
|
+
super(opts);
|
|
23
20
|
this.errors = [];
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
async publish() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this
|
|
30
|
-
this.#buildConfig();
|
|
24
|
+
this.printBanner();
|
|
25
|
+
this.validateEnvDetails();
|
|
26
|
+
this.buildConfig();
|
|
31
27
|
this.#validateOptions();
|
|
32
28
|
this.#setConfigFromFile();
|
|
33
29
|
this.#mergeConfigOptions();
|
|
@@ -39,25 +35,6 @@ class PublishCommand {
|
|
|
39
35
|
await this.#publishErrors();
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
#validateEnvDetails() {
|
|
43
|
-
try {
|
|
44
|
-
const current_major_version = parseInt(process.version.split('.')[0].replace('v', ''));
|
|
45
|
-
if (current_major_version >= MIN_NODE_VERSION) {
|
|
46
|
-
logger.info(`💻 NodeJS: ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, Arch: ${os.machine()}`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
logger.warn(`⚠️ Unable to verify NodeJS version: ${error.message}`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
#buildConfig() {
|
|
57
|
-
const config_builder = new ConfigBuilder(this.opts);
|
|
58
|
-
config_builder.build();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
38
|
#validateOptions() {
|
|
62
39
|
if (!this.opts) {
|
|
63
40
|
throw new Error('Missing publish options');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const hash = require('object-hash');
|
|
2
|
+
|
|
3
|
+
class BaseParser {
|
|
4
|
+
hash(obj) {
|
|
5
|
+
return hash(obj, { algorithm: 'md5' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hash a test case
|
|
10
|
+
* @param {import('../../types').IManualTestCase} testCase
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
hashTestCase(testCase) {
|
|
14
|
+
return this.hash({
|
|
15
|
+
name: testCase.name,
|
|
16
|
+
type: testCase.type,
|
|
17
|
+
tags: testCase.tags,
|
|
18
|
+
steps: testCase.steps,
|
|
19
|
+
path: testCase.path
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hash a test suite
|
|
25
|
+
* @param {import('../../types').IManualTestSuite} testSuite
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
hashTestSuite(testSuite) {
|
|
29
|
+
return this.hash({
|
|
30
|
+
name: testSuite.name,
|
|
31
|
+
type: testSuite.type,
|
|
32
|
+
tags: testSuite.tags,
|
|
33
|
+
before_each: testSuite.before_each,
|
|
34
|
+
test_cases: testSuite.test_cases.map(testCase => testCase.hash)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { BaseParser };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const { BaseParser } = require('./base');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Simple and extendable Gherkin parser for Cucumber feature files
|
|
5
6
|
* Parses .feature files and returns structured test suite objects
|
|
6
7
|
*/
|
|
7
|
-
class GherkinParser {
|
|
8
|
+
class GherkinParser extends BaseParser {
|
|
8
9
|
constructor() {
|
|
10
|
+
super();
|
|
9
11
|
/** @type {string[]} Supported step keywords */
|
|
10
12
|
this.stepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
11
13
|
}
|
|
@@ -31,12 +33,15 @@ class GherkinParser {
|
|
|
31
33
|
* @returns {Object}
|
|
32
34
|
*/
|
|
33
35
|
parseLines(lines) {
|
|
36
|
+
/**
|
|
37
|
+
* @type {import('../../types').IManualTestSuite}
|
|
38
|
+
*/
|
|
34
39
|
const testSuite = {
|
|
35
40
|
name: '',
|
|
36
41
|
type: 'feature',
|
|
37
42
|
tags: [],
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
before_each: [],
|
|
44
|
+
test_cases: []
|
|
40
45
|
};
|
|
41
46
|
|
|
42
47
|
let currentFeature = null;
|
|
@@ -71,12 +76,12 @@ class GherkinParser {
|
|
|
71
76
|
} else if (line.startsWith('Background:')) {
|
|
72
77
|
// Parse Background
|
|
73
78
|
currentBackground = this.parseBackground();
|
|
74
|
-
testSuite.
|
|
79
|
+
testSuite.before_each.push(currentBackground);
|
|
75
80
|
} else if (line.startsWith('Scenario:')) {
|
|
76
81
|
// Parse Scenario
|
|
77
82
|
currentScenario = this.parseScenario(line);
|
|
78
83
|
currentScenario.tags = pendingScenarioTags.map(tag => tag.name);
|
|
79
|
-
testSuite.
|
|
84
|
+
testSuite.test_cases.push(currentScenario);
|
|
80
85
|
pendingScenarioTags = [];
|
|
81
86
|
currentBackground = null; // Reset Background context when Scenario starts
|
|
82
87
|
} else if (this.isStep(line)) {
|
|
@@ -94,6 +99,11 @@ class GherkinParser {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
for (const testCase of testSuite.test_cases) {
|
|
103
|
+
testCase.hash = this.hashTestCase(testCase);
|
|
104
|
+
}
|
|
105
|
+
testSuite.hash = this.hashTestSuite(testSuite);
|
|
106
|
+
|
|
97
107
|
return testSuite;
|
|
98
108
|
}
|
|
99
109
|
|
|
@@ -169,7 +179,7 @@ class GherkinParser {
|
|
|
169
179
|
/**
|
|
170
180
|
* Parse Scenario line
|
|
171
181
|
* @param {string} line
|
|
172
|
-
* @returns {
|
|
182
|
+
* @returns {import('../../types').IManualTestCase}
|
|
173
183
|
*/
|
|
174
184
|
parseScenario(line) {
|
|
175
185
|
const name = line.replace('Scenario:', '').trim();
|
|
@@ -229,8 +239,8 @@ class GherkinParser {
|
|
|
229
239
|
document.name &&
|
|
230
240
|
document.type === 'feature' &&
|
|
231
241
|
Array.isArray(document.tags) &&
|
|
232
|
-
Array.isArray(document.
|
|
233
|
-
Array.isArray(document.
|
|
242
|
+
Array.isArray(document.before_each) &&
|
|
243
|
+
Array.isArray(document.test_cases);
|
|
234
244
|
}
|
|
235
245
|
}
|
|
236
246
|
|
|
@@ -56,6 +56,35 @@ class ManualSyncHelper {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Hash a folder
|
|
61
|
+
* @param {import('../types').IManualTestFolder} folder
|
|
62
|
+
* @returns {string} Hash string
|
|
63
|
+
*/
|
|
64
|
+
hashFolder(folder) {
|
|
65
|
+
return this.generateHash({
|
|
66
|
+
name: folder.name,
|
|
67
|
+
path: folder.path,
|
|
68
|
+
test_suites: folder.test_suites.map(testSuite => testSuite.hash),
|
|
69
|
+
folders: folder.folders.map(subFolder => subFolder.hash)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Hash a test suite
|
|
75
|
+
* @param {import('../types').IManualTestSuite} testSuite
|
|
76
|
+
* @returns {string} Hash string
|
|
77
|
+
*/
|
|
78
|
+
hashTestSuite(testSuite) {
|
|
79
|
+
return this.generateHash({
|
|
80
|
+
name: testSuite.name,
|
|
81
|
+
tags: testSuite.tags,
|
|
82
|
+
before_each: testSuite.before_each,
|
|
83
|
+
test_cases: testSuite.test_cases.map(testCase => testCase.hash)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
59
88
|
/**
|
|
60
89
|
* Scan directory recursively and build folder structure
|
|
61
90
|
* @param {string} directoryPath - Path to scan
|
|
@@ -72,20 +101,22 @@ class ManualSyncHelper {
|
|
|
72
101
|
throw new Error(`Path is not a directory: ${directoryPath}`);
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
return this.addFolderHash(structure);
|
|
104
|
+
return this.buildFolderStructure(absolutePath, directoryPath);
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
/**
|
|
80
108
|
* Build folder structure recursively
|
|
81
109
|
* @param {string} absolutePath - Absolute path for file operations
|
|
82
110
|
* @param {string} relativePath - Relative path for output
|
|
83
|
-
* @returns {
|
|
111
|
+
* @returns {import('../types').IManualTestFolder} Folder structure
|
|
84
112
|
*/
|
|
85
113
|
buildFolderStructure(absolutePath, relativePath) {
|
|
86
114
|
const folderName = path.basename(absolutePath);
|
|
87
115
|
const items = fs.readdirSync(absolutePath);
|
|
88
116
|
|
|
117
|
+
/**
|
|
118
|
+
* @type {import('../types').IManualTestFolder}
|
|
119
|
+
*/
|
|
89
120
|
const structure = {
|
|
90
121
|
name: folderName,
|
|
91
122
|
path: relativePath,
|
|
@@ -106,12 +137,26 @@ class ManualSyncHelper {
|
|
|
106
137
|
// Parse gherkin files
|
|
107
138
|
try {
|
|
108
139
|
const testSuite = this.parseGherkinFile(itemPath, itemRelativePath);
|
|
140
|
+
testSuite.hash = this.hashTestSuite(testSuite);
|
|
109
141
|
structure.test_suites.push(testSuite);
|
|
110
142
|
} catch (error) {
|
|
111
143
|
console.warn(`Warning: Failed to parse ${itemPath}: ${error.message}`);
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
}
|
|
147
|
+
structure.hash = this.hashFolder(structure);
|
|
148
|
+
if (structure.test_suites.length > 0) {
|
|
149
|
+
const defaultFolder = {
|
|
150
|
+
name: 'default',
|
|
151
|
+
path: 'default',
|
|
152
|
+
hash: '',
|
|
153
|
+
test_suites: structure.test_suites,
|
|
154
|
+
folders: []
|
|
155
|
+
};
|
|
156
|
+
defaultFolder.hash = this.hashFolder(defaultFolder);
|
|
157
|
+
structure.folders.push(defaultFolder);
|
|
158
|
+
structure.test_suites = [];
|
|
159
|
+
}
|
|
115
160
|
|
|
116
161
|
return structure;
|
|
117
162
|
}
|
|
@@ -129,7 +174,7 @@ class ManualSyncHelper {
|
|
|
129
174
|
* Parse a gherkin file and format output
|
|
130
175
|
* @param {string} filePath - Path to gherkin file
|
|
131
176
|
* @param {string} relativePath - Relative path for output
|
|
132
|
-
* @returns {
|
|
177
|
+
* @returns {import('../types').IManualTestSuite} Formatted test suite
|
|
133
178
|
*/
|
|
134
179
|
parseGherkinFile(filePath, relativePath) {
|
|
135
180
|
const parsed = this.parser.parse(filePath);
|
|
@@ -138,9 +183,9 @@ class ManualSyncHelper {
|
|
|
138
183
|
name: parsed.name,
|
|
139
184
|
type: parsed.type,
|
|
140
185
|
tags: parsed.tags,
|
|
141
|
-
|
|
186
|
+
before_each: parsed.before_each,
|
|
142
187
|
path: relativePath,
|
|
143
|
-
test_cases: parsed.
|
|
188
|
+
test_cases: parsed.test_cases || [] // Map 'cases' to 'test_cases' for consistency
|
|
144
189
|
};
|
|
145
190
|
}
|
|
146
191
|
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type IManualTestCase = {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
tags: string[];
|
|
5
|
+
steps: string[];
|
|
6
|
+
path: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type IManualTestSuite = {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
before_each: string[];
|
|
15
|
+
test_cases: IManualTestCase[];
|
|
16
|
+
hash: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type IManualTestFolder = {
|
|
20
|
+
name: string;
|
|
21
|
+
path: string;
|
|
22
|
+
test_suites: IManualTestSuite[];
|
|
23
|
+
folders: IManualTestFolder[];
|
|
24
|
+
hash: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type IManualSyncComparePayload = {
|
|
28
|
+
project_id: string;
|
|
29
|
+
folders: IManualSyncCompareFolder[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type IManualSyncCompareFolder = {
|
|
33
|
+
name: string;
|
|
34
|
+
hash: string;
|
|
35
|
+
test_suites: IManualSyncCompareTestSuite[];
|
|
36
|
+
folders: IManualSyncCompareFolder[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type IManualSyncCompareTestSuite = {
|
|
40
|
+
name: string;
|
|
41
|
+
hash: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type IManualSyncCompareTestCase = {
|
|
45
|
+
name: string;
|
|
46
|
+
hash: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type IManualSyncCompareResponse = {
|
|
50
|
+
folders: IManualSyncCompareResponseFolder[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SyncOperationType = 'create' | 'update' | 'delete' | 'no_change';
|
|
54
|
+
|
|
55
|
+
export type IManualSyncCompareResponseFolder = {
|
|
56
|
+
type: SyncOperationType;
|
|
57
|
+
id?: string;
|
|
58
|
+
name: string;
|
|
59
|
+
hash: string;
|
|
60
|
+
test_suites: IManualSyncCompareResponseTestSuite[];
|
|
61
|
+
folders: IManualSyncCompareResponseFolder[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type IManualSyncCompareResponseTestSuite = {
|
|
65
|
+
type: SyncOperationType;
|
|
66
|
+
id?: string;
|
|
67
|
+
name: string;
|
|
68
|
+
hash: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type IManualSyncFoldersPayload = {
|
|
72
|
+
project_id: string;
|
|
73
|
+
folders: IManualSyncFoldersFolder[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type IManualSyncFoldersFolder = {
|
|
77
|
+
type: SyncOperationType;
|
|
78
|
+
name: string;
|
|
79
|
+
path: string;
|
|
80
|
+
hash: string;
|
|
81
|
+
id?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type ISyncManualSuitesPayload = {
|
|
85
|
+
project_id: string;
|
|
86
|
+
suites: ISyncManualSuitesSuite[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ISyncManualSuitesSuite = {
|
|
90
|
+
type: SyncOperationType;
|
|
91
|
+
name: string;
|
|
92
|
+
folder_id: string;
|
|
93
|
+
hash: string;
|
|
94
|
+
tags?: string[];
|
|
95
|
+
before_each?: string[];
|
|
96
|
+
test_cases?: ISyncManualSuitesTestCase[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ISyncManualSuitesTestCase = {
|
|
100
|
+
name: string;
|
|
101
|
+
hash: string;
|
|
102
|
+
tags?: string[];
|
|
103
|
+
steps: string[];
|
|
104
|
+
}
|