testbeats 2.4.2 → 2.5.0
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.js +0 -16
- package/src/commands/base.command.js +5 -1
- package/src/commands/manual-sync.command.js +32 -269
- package/src/commands/publish.command.js +3 -24
- package/src/index.d.ts +2 -0
- package/src/manual/comparator.js +71 -0
- package/src/manual/factory.js +60 -0
- package/src/manual/hasher.js +93 -0
- package/src/manual/index.js +18 -0
- package/src/manual/orchestrator.js +124 -0
- package/src/manual/parsers/base.js +10 -32
- package/src/manual/parsers/gherkin.js +31 -19
- package/src/manual/project-resolver.js +33 -0
- package/src/manual/scanner.js +127 -0
- package/src/manual/synchronizer.js +329 -0
- package/src/utils/config.builder.js +91 -16
- package/src/manual/sync.helper.js +0 -193
package/package.json
CHANGED
package/src/beats/beats.js
CHANGED
|
@@ -20,14 +20,11 @@ class Beats {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async publish() {
|
|
23
|
-
this.#setApiKey();
|
|
24
23
|
if (!this.config.api_key) {
|
|
25
24
|
logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal...');
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
27
|
this.#setCIInfo();
|
|
29
|
-
this.#setProjectName();
|
|
30
|
-
this.#setRunName();
|
|
31
28
|
await this.#publishTestResults();
|
|
32
29
|
await this.#uploadAttachments();
|
|
33
30
|
this.#updateTitleLink();
|
|
@@ -38,19 +35,6 @@ class Beats {
|
|
|
38
35
|
this.ci = getCIInformation();
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
#setProjectName() {
|
|
42
|
-
this.config.project = this.config.project || process.env.TEST_BEATS_PROJECT || (this.ci && this.ci.repository_name) || 'demo-project';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
#setApiKey() {
|
|
46
|
-
this.config.api_key = this.config.api_key || process.env.TEST_BEATS_API_KEY;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
#setRunName() {
|
|
50
|
-
this.config.run = this.config.run || process.env.TEST_BEATS_RUN || (this.ci && this.ci.build_name) || 'demo-run';
|
|
51
|
-
this.result.name = this.config.run;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
38
|
async #publishTestResults() {
|
|
55
39
|
logger.info("🚀 Publishing results to TestBeats Portal...");
|
|
56
40
|
try {
|
|
@@ -3,9 +3,13 @@ const { MIN_NODE_VERSION } = require('../helpers/constants');
|
|
|
3
3
|
const { ConfigBuilder } = require('../utils/config.builder');
|
|
4
4
|
const logger = require('../utils/logger');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { getCIInformation } = require('../helpers/ci');
|
|
6
7
|
|
|
7
8
|
class BaseCommand {
|
|
8
9
|
constructor(opts) {
|
|
10
|
+
/**
|
|
11
|
+
* @type {import('../index').CommandLineOptions}
|
|
12
|
+
*/
|
|
9
13
|
this.opts = opts;
|
|
10
14
|
}
|
|
11
15
|
|
|
@@ -38,7 +42,7 @@ class BaseCommand {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
buildConfig() {
|
|
41
|
-
const config_builder = new ConfigBuilder(this.opts);
|
|
45
|
+
const config_builder = new ConfigBuilder(this.opts, getCIInformation(), process.env);
|
|
42
46
|
config_builder.build();
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { ManualSyncServiceFactory } = require('../manual');
|
|
2
2
|
const logger = require('../utils/logger');
|
|
3
|
-
const { BeatsApi } = require('../beats/beats.api');
|
|
4
3
|
const { BaseCommand } = require('./base.command');
|
|
5
4
|
|
|
6
5
|
class ManualSyncCommand extends BaseCommand {
|
|
@@ -9,7 +8,6 @@ class ManualSyncCommand extends BaseCommand {
|
|
|
9
8
|
*/
|
|
10
9
|
constructor(opts) {
|
|
11
10
|
super(opts);
|
|
12
|
-
this.syncHelper = new ManualSyncHelper();
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
@@ -24,288 +22,53 @@ class ManualSyncCommand extends BaseCommand {
|
|
|
24
22
|
logger.info('🔄 Starting manual test case sync...');
|
|
25
23
|
|
|
26
24
|
const targetPath = this.opts.path || '.';
|
|
27
|
-
|
|
25
|
+
const projectName = this.opts.config.project;
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
// Create orchestrator with all dependencies wired
|
|
28
|
+
const orchestrator = ManualSyncServiceFactory.create(this.opts.config);
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Execute the complete pipeline
|
|
31
|
+
const result = await orchestrator.execute(targetPath, projectName);
|
|
33
32
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
folders: result.folders
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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);
|
|
33
|
+
// Log final results
|
|
34
|
+
this.#logResults(result);
|
|
54
35
|
|
|
36
|
+
return result;
|
|
55
37
|
} catch (error) {
|
|
56
38
|
logger.error(`❌ Manual sync failed: ${error.message}`);
|
|
57
39
|
throw error;
|
|
58
40
|
}
|
|
59
41
|
}
|
|
60
42
|
|
|
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
|
-
|
|
199
43
|
/**
|
|
200
|
-
*
|
|
201
|
-
* @param {
|
|
202
|
-
* @param {import('../types').IManualSyncCompareResponseTestSuite} serverSuite - Server suite with metadata
|
|
44
|
+
* Log the final sync results
|
|
45
|
+
* @param {Object} result - Sync result from orchestrator
|
|
203
46
|
*/
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
}
|
|
225
|
-
|
|
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
|
-
}
|
|
242
|
-
|
|
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);
|
|
47
|
+
#logResults(result) {
|
|
48
|
+
logger.info('');
|
|
49
|
+
logger.info('📊 Sync Summary:');
|
|
50
|
+
logger.info(` • Test Cases Found: ${result.statistics.totalTestCases}`);
|
|
51
|
+
logger.info(` • Test Suites Found: ${result.statistics.totalTestSuites}`);
|
|
52
|
+
logger.info(` • Folders Found: ${result.statistics.totalFolders}`);
|
|
53
|
+
logger.info(` • Folders Synced: ${result.statistics.foldersProcessed}`);
|
|
54
|
+
logger.info(` • Test Suites Synced: ${result.statistics.testSuitesProcessed}`);
|
|
55
|
+
|
|
56
|
+
if (result.errors.length > 0) {
|
|
57
|
+
logger.info('');
|
|
58
|
+
logger.warn(`⚠️ Encountered ${result.errors.length} error(s):`);
|
|
59
|
+
for (const error of result.errors) {
|
|
60
|
+
logger.warn(` • [${error.type}] ${error.name}: ${error.error}`);
|
|
273
61
|
}
|
|
274
62
|
}
|
|
275
|
-
}
|
|
276
63
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
}
|
|
64
|
+
if (result.success) {
|
|
65
|
+
logger.info('');
|
|
66
|
+
logger.info('✅ Manual sync completed successfully!');
|
|
67
|
+
} else {
|
|
68
|
+
logger.info('');
|
|
69
|
+
logger.warn('⚠️ Manual sync completed with errors');
|
|
306
70
|
}
|
|
307
71
|
}
|
|
308
|
-
|
|
309
72
|
}
|
|
310
73
|
|
|
311
|
-
module.exports = { ManualSyncCommand
|
|
74
|
+
module.exports = { ManualSyncCommand };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
1
|
const trp = require('test-results-parser');
|
|
3
2
|
const prp = require('performance-results-parser');
|
|
4
3
|
|
|
@@ -25,8 +24,6 @@ class PublishCommand extends BaseCommand {
|
|
|
25
24
|
this.validateEnvDetails();
|
|
26
25
|
this.buildConfig();
|
|
27
26
|
this.#validateOptions();
|
|
28
|
-
this.#setConfigFromFile();
|
|
29
|
-
this.#mergeConfigOptions();
|
|
30
27
|
this.#processConfig();
|
|
31
28
|
this.#validateConfig();
|
|
32
29
|
this.#processResults();
|
|
@@ -44,27 +41,6 @@ class PublishCommand extends BaseCommand {
|
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
#setConfigFromFile() {
|
|
48
|
-
if (typeof this.opts.config === 'string') {
|
|
49
|
-
const cwd = process.cwd();
|
|
50
|
-
const file_path = path.join(cwd, this.opts.config);
|
|
51
|
-
try {
|
|
52
|
-
const config_json = require(path.join(cwd, this.opts.config));
|
|
53
|
-
this.opts.config = config_json;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
throw new Error(`Failed to read config file: '${file_path}' with error: '${error.message}'`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#mergeConfigOptions() {
|
|
61
|
-
if (this.opts.config && typeof this.opts.config === 'object') {
|
|
62
|
-
this.opts.config.project = this.opts.project || this.opts.config.project;
|
|
63
|
-
this.opts.config.run = this.opts.run || this.opts.config.run;
|
|
64
|
-
this.opts.config.api_key = this.opts['api-key'] || this.opts.config.api_key;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
44
|
#processConfig() {
|
|
69
45
|
const processed_config = processData(this.opts.config);
|
|
70
46
|
/**@type {import('../index').PublishConfig[]} */
|
|
@@ -230,6 +206,9 @@ class PublishCommand extends BaseCommand {
|
|
|
230
206
|
for (const config of this.configs) {
|
|
231
207
|
for (let i = 0; i < this.results.length; i++) {
|
|
232
208
|
const result = this.results[i];
|
|
209
|
+
if (config.api_key) {
|
|
210
|
+
result.name = config.run || result.name || 'demo-run';
|
|
211
|
+
}
|
|
233
212
|
config.extensions = config.extensions || [];
|
|
234
213
|
await beats.run(config, result);
|
|
235
214
|
if (config.targets) {
|
package/src/index.d.ts
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class ManualTestComparator {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('../beats/beats.api').BeatsApi} beatsApi
|
|
4
|
+
*/
|
|
5
|
+
constructor(beatsApi) {
|
|
6
|
+
this.beatsApi = beatsApi;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compare local structure with server
|
|
11
|
+
* @param {import('../types').IManualTestFolder} structure - Local folder structure
|
|
12
|
+
* @param {string} projectId - Project ID
|
|
13
|
+
* @returns {Promise<import('../types').IManualSyncCompareResponse>} Comparison result
|
|
14
|
+
* @throws {Error} If comparison fails
|
|
15
|
+
*/
|
|
16
|
+
async compare(structure, projectId) {
|
|
17
|
+
try {
|
|
18
|
+
const payload = this.#buildPayload(structure, projectId);
|
|
19
|
+
const response = await this.beatsApi.compareManualTests(payload);
|
|
20
|
+
return response;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(error.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the payload for the compare manual tests API
|
|
28
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
29
|
+
* @param {string} projectId
|
|
30
|
+
* @returns {import('../types').IManualSyncComparePayload}
|
|
31
|
+
*/
|
|
32
|
+
#buildPayload(structure, projectId) {
|
|
33
|
+
const payload = {
|
|
34
|
+
project_id: projectId,
|
|
35
|
+
folders: [],
|
|
36
|
+
};
|
|
37
|
+
for (const folder of structure.folders) {
|
|
38
|
+
payload.folders.push(this.#buildPayloadFolder(folder));
|
|
39
|
+
}
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the payload for a folder
|
|
45
|
+
* @param {import('../types').IManualTestFolder} folder
|
|
46
|
+
* @returns {import('../types').IManualSyncCompareFolder}
|
|
47
|
+
*/
|
|
48
|
+
#buildPayloadFolder(folder) {
|
|
49
|
+
return {
|
|
50
|
+
name: folder.name,
|
|
51
|
+
hash: folder.hash,
|
|
52
|
+
test_suites: folder.test_suites.map(testSuite => this.#buildPayloadTestSuite(testSuite)),
|
|
53
|
+
folders: folder.folders.map(subFolder => this.#buildPayloadFolder(subFolder))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the payload for a test suite
|
|
59
|
+
* @param {import('../types').IManualTestSuite} testSuite
|
|
60
|
+
* @returns {import('../types').IManualSyncCompareTestSuite}
|
|
61
|
+
*/
|
|
62
|
+
#buildPayloadTestSuite(testSuite) {
|
|
63
|
+
return {
|
|
64
|
+
name: testSuite.name,
|
|
65
|
+
hash: testSuite.hash,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { ManualTestComparator };
|
|
71
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { BeatsApi } = require('../beats/beats.api');
|
|
3
|
+
const { ManualTestScanner } = require('./scanner');
|
|
4
|
+
const { ManualTestComparator } = require('./comparator');
|
|
5
|
+
const { ManualTestSynchronizer } = require('./synchronizer');
|
|
6
|
+
const { ProjectResolver } = require('./project-resolver');
|
|
7
|
+
const { ManualSyncOrchestrator } = require('./orchestrator');
|
|
8
|
+
const { ManualTestHasher } = require('./hasher');
|
|
9
|
+
const { GherkinParser } = require('./parsers/gherkin');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factory for creating properly wired manual sync services
|
|
13
|
+
* Implements dependency injection and ensures single BeatsApi instance
|
|
14
|
+
*/
|
|
15
|
+
class ManualSyncServiceFactory {
|
|
16
|
+
/**
|
|
17
|
+
* Create a fully configured orchestrator with all dependencies
|
|
18
|
+
* @param {Object} config - Configuration object for BeatsApi
|
|
19
|
+
* @returns {ManualSyncOrchestrator} Configured orchestrator
|
|
20
|
+
*/
|
|
21
|
+
static create(config) {
|
|
22
|
+
const beatsApi = new BeatsApi(config);
|
|
23
|
+
const hasher = new ManualTestHasher();
|
|
24
|
+
const parser = new GherkinParser(fs);
|
|
25
|
+
const scanner = new ManualTestScanner(fs, parser);
|
|
26
|
+
const comparator = new ManualTestComparator(beatsApi);
|
|
27
|
+
const synchronizer = new ManualTestSynchronizer(beatsApi);
|
|
28
|
+
const projectResolver = new ProjectResolver(beatsApi);
|
|
29
|
+
|
|
30
|
+
return new ManualSyncOrchestrator(
|
|
31
|
+
scanner,
|
|
32
|
+
comparator,
|
|
33
|
+
synchronizer,
|
|
34
|
+
projectResolver,
|
|
35
|
+
hasher
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create individual services for testing or custom workflows
|
|
41
|
+
* @param {Object} config - Configuration object for BeatsApi
|
|
42
|
+
* @returns {Object} Object containing all services
|
|
43
|
+
*/
|
|
44
|
+
static createServices(config) {
|
|
45
|
+
const beatsApi = new BeatsApi(config);
|
|
46
|
+
const hasher = new ManualTestHasher();
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
beatsApi,
|
|
50
|
+
hasher,
|
|
51
|
+
scanner: new ManualTestScanner(),
|
|
52
|
+
comparator: new ManualTestComparator(beatsApi),
|
|
53
|
+
synchronizer: new ManualTestSynchronizer(beatsApi),
|
|
54
|
+
projectResolver: new ProjectResolver(beatsApi)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { ManualSyncServiceFactory };
|
|
60
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const hash = require('object-hash');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles all hashing operations for manual test entities
|
|
5
|
+
* Centralizes hashing logic with configurable algorithm
|
|
6
|
+
*/
|
|
7
|
+
class ManualTestHasher {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} algorithm - Hashing algorithm (default: 'md5')
|
|
10
|
+
*/
|
|
11
|
+
constructor(algorithm = 'md5') {
|
|
12
|
+
this.algorithm = algorithm;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate hash for any object
|
|
17
|
+
* @param {Object} obj - Object to hash
|
|
18
|
+
* @returns {string} Hash string
|
|
19
|
+
*/
|
|
20
|
+
hash(obj) {
|
|
21
|
+
return hash(obj, { algorithm: this.algorithm });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hash a test case
|
|
26
|
+
* @param {import('../types').IManualTestCase} testCase
|
|
27
|
+
* @returns {string} Hash string
|
|
28
|
+
*/
|
|
29
|
+
hashTestCase(testCase) {
|
|
30
|
+
return this.hash({
|
|
31
|
+
name: testCase.name,
|
|
32
|
+
type: testCase.type,
|
|
33
|
+
tags: testCase.tags,
|
|
34
|
+
steps: testCase.steps,
|
|
35
|
+
path: testCase.path
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hash a test suite
|
|
41
|
+
* @param {import('../types').IManualTestSuite} testSuite
|
|
42
|
+
* @returns {string} Hash string
|
|
43
|
+
*/
|
|
44
|
+
hashTestSuite(testSuite) {
|
|
45
|
+
return this.hash({
|
|
46
|
+
name: testSuite.name,
|
|
47
|
+
type: testSuite.type,
|
|
48
|
+
tags: testSuite.tags,
|
|
49
|
+
before_each: testSuite.before_each,
|
|
50
|
+
test_cases: testSuite.test_cases.map(tc => tc.hash)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hash a folder
|
|
56
|
+
* @param {import('../types').IManualTestFolder} folder
|
|
57
|
+
* @returns {string} Hash string
|
|
58
|
+
*/
|
|
59
|
+
hashFolder(folder) {
|
|
60
|
+
return this.hash({
|
|
61
|
+
name: folder.name,
|
|
62
|
+
path: folder.path,
|
|
63
|
+
test_suites: folder.test_suites.map(ts => ts.hash),
|
|
64
|
+
folders: folder.folders.map(f => f.hash)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Recursively hash an entire folder structure
|
|
70
|
+
* Adds hash properties to all test cases, test suites, and folders
|
|
71
|
+
* @param {import('../types').IManualTestFolder} structure - Folder structure to hash
|
|
72
|
+
* @returns {import('../types').IManualTestFolder} Same structure with hashes added (mutates in place)
|
|
73
|
+
*/
|
|
74
|
+
hashStructure(structure) {
|
|
75
|
+
for (const testSuite of structure.test_suites) {
|
|
76
|
+
for (const testCase of testSuite.test_cases) {
|
|
77
|
+
testCase.hash = this.hashTestCase(testCase);
|
|
78
|
+
}
|
|
79
|
+
testSuite.hash = this.hashTestSuite(testSuite);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const subFolder of structure.folders) {
|
|
83
|
+
this.hashStructure(subFolder);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
structure.hash = this.hashFolder(structure);
|
|
87
|
+
|
|
88
|
+
return structure;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { ManualTestHasher };
|
|
93
|
+
|