testbeats 2.2.7 → 2.2.9
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 +3 -2
- package/src/beats/beats.attachments.js +1 -1
- package/src/beats/beats.js +2 -2
- package/src/cli.js +18 -0
- package/src/commands/manual-sync.command.js +79 -0
- package/src/helpers/constants.js +1 -0
- package/src/index.d.ts +8 -3
- package/src/manual/parsers/gherkin.js +237 -0
- package/src/manual/sync.helper.js +148 -0
- package/src/platforms/base.platform.js +13 -108
- package/src/platforms/chat.platform.js +4 -1
- package/src/platforms/slack.platform.js +14 -1
- package/src/targets/base.target.js +114 -1
- package/src/targets/chat.js +11 -4
- package/src/targets/custom.target.js +3 -2
- package/src/targets/github-output.target.js +64 -0
- package/src/targets/github.target.js +316 -0
- package/src/targets/index.js +5 -2
- package/src/targets/slack.js +32 -12
- package/src/targets/teams.js +14 -8
- package/src/utils/context.utils.js +5 -0
- package/src/targets/github.js +0 -310
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testbeats",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.9",
|
|
4
4
|
"description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -50,13 +50,14 @@
|
|
|
50
50
|
"dotenv": "^16.4.5",
|
|
51
51
|
"form-data-lite": "^1.0.3",
|
|
52
52
|
"influxdb-lite": "^1.0.0",
|
|
53
|
+
"object-hash": "^3.0.0",
|
|
53
54
|
"performance-results-parser": "latest",
|
|
54
55
|
"phin-retry": "^1.0.3",
|
|
55
56
|
"pretty-ms": "^7.0.1",
|
|
56
57
|
"prompts": "^2.4.2",
|
|
57
58
|
"rosters": "0.0.1",
|
|
58
59
|
"sade": "^1.8.1",
|
|
59
|
-
"test-results-parser": "0.2.
|
|
60
|
+
"test-results-parser": "0.2.9"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"c8": "^10.1.2",
|
|
@@ -10,7 +10,7 @@ const logger = require('../utils/logger');
|
|
|
10
10
|
const TestAttachment = require('test-results-parser/src/models/TestAttachment');
|
|
11
11
|
|
|
12
12
|
const MAX_ATTACHMENTS_PER_REQUEST = 5;
|
|
13
|
-
const MAX_ATTACHMENTS_PER_RUN =
|
|
13
|
+
const MAX_ATTACHMENTS_PER_RUN = 50;
|
|
14
14
|
const MAX_ATTACHMENT_SIZE = 2 * 1024 * 1024;
|
|
15
15
|
|
|
16
16
|
class BeatsAttachments {
|
package/src/beats/beats.js
CHANGED
|
@@ -186,14 +186,14 @@ class Beats {
|
|
|
186
186
|
if (process.env.TEST_BEATS_DELAY) {
|
|
187
187
|
return parseInt(process.env.TEST_BEATS_DELAY);
|
|
188
188
|
}
|
|
189
|
-
return
|
|
189
|
+
return 5000;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
async #setTestRun(text, wait_for = 'smart_analysis_status') {
|
|
193
193
|
if (this.test_run && this.test_run[wait_for] === PROCESS_STATUS.COMPLETED) {
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
|
-
let retry =
|
|
196
|
+
let retry = 5;
|
|
197
197
|
while (retry >= 0) {
|
|
198
198
|
retry = retry - 1;
|
|
199
199
|
await new Promise(resolve => setTimeout(resolve, this.#getDelay()));
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const sade = require('sade');
|
|
|
6
6
|
const prog = sade('testbeats');
|
|
7
7
|
const { PublishCommand } = require('./commands/publish.command');
|
|
8
8
|
const { GenerateConfigCommand } = require('./commands/generate-config.command');
|
|
9
|
+
const { ManualSyncCommand } = require('./commands/manual-sync.command');
|
|
9
10
|
const logger = require('./utils/logger');
|
|
10
11
|
const pkg = require('../package.json');
|
|
11
12
|
|
|
@@ -63,4 +64,21 @@ prog.command('init')
|
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// Command to sync manual test cases from gherkin files
|
|
68
|
+
prog.command('manual sync')
|
|
69
|
+
.describe('Sync manual test cases from gherkin files in a directory')
|
|
70
|
+
.option('-p, --path', 'Path to directory to sync (default: current directory)')
|
|
71
|
+
.example('manual sync')
|
|
72
|
+
.example('manual sync --path ./tests/features')
|
|
73
|
+
.action(async (opts) => {
|
|
74
|
+
try {
|
|
75
|
+
logger.setLevel(opts.logLevel);
|
|
76
|
+
const manual_sync_command = new ManualSyncCommand(opts);
|
|
77
|
+
await manual_sync_command.execute();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error(`Manual sync failed: ${error.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
66
84
|
prog.parse(process.argv);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { ManualSyncHelper } = require('../manual/sync.helper');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
class ManualSyncCommand {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} opts - Command options
|
|
8
|
+
*/
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
this.syncHelper = new ManualSyncHelper();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute the manual sync command
|
|
16
|
+
*/
|
|
17
|
+
async execute() {
|
|
18
|
+
try {
|
|
19
|
+
logger.info('🔄 Starting manual test case sync...');
|
|
20
|
+
|
|
21
|
+
const targetPath = this.opts.path || '.';
|
|
22
|
+
logger.info(`📁 Scanning directory: ${targetPath}`);
|
|
23
|
+
|
|
24
|
+
const result = await this.syncHelper.scanDirectory(targetPath);
|
|
25
|
+
|
|
26
|
+
// Format output as specified in requirements
|
|
27
|
+
const output = {
|
|
28
|
+
folders: [result]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Display results
|
|
32
|
+
this.displayResults(output);
|
|
33
|
+
|
|
34
|
+
logger.info('✅ Manual sync completed successfully!');
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error(`❌ Manual sync failed: ${error.message}`);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display sync results in a readable format
|
|
46
|
+
* @param {Object} output - Sync results
|
|
47
|
+
*/
|
|
48
|
+
displayResults(output) {
|
|
49
|
+
logger.info('\n📊 Sync Results:');
|
|
50
|
+
this.displayFolderRecursive(output.folders[0], 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively display folder structure
|
|
55
|
+
* @param {Object} folder - Folder object
|
|
56
|
+
* @param {number} depth - Current depth for indentation
|
|
57
|
+
*/
|
|
58
|
+
displayFolderRecursive(folder, depth) {
|
|
59
|
+
const indent = ' '.repeat(depth);
|
|
60
|
+
|
|
61
|
+
logger.info(`${indent}📁 ${folder.name} (${folder.path})`);
|
|
62
|
+
|
|
63
|
+
if (folder.test_suites.length > 0) {
|
|
64
|
+
logger.info(`${indent} 📋 Test Suites: ${folder.test_suites.length}`);
|
|
65
|
+
folder.test_suites.forEach(suite => {
|
|
66
|
+
logger.info(`${indent} • ${suite.name} (${suite.test_cases.length} test cases)`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (folder.folders.length > 0) {
|
|
71
|
+
logger.info(`${indent} 📂 Subfolders: ${folder.folders.length}`);
|
|
72
|
+
folder.folders.forEach(subFolder => {
|
|
73
|
+
this.displayFolderRecursive(subFolder, depth + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { ManualSyncCommand };
|
package/src/helpers/constants.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ITarget {
|
|
|
10
10
|
name: TargetName;
|
|
11
11
|
enable?: string | boolean;
|
|
12
12
|
condition?: Condition;
|
|
13
|
-
inputs?: SlackInputs | TeamsInputs | ChatInputs |
|
|
13
|
+
inputs?: SlackInputs | TeamsInputs | ChatInputs | IGitHubInputs | IGitHubOutputInputs | ICustomTargetInputs | InfluxDBTargetInputs;
|
|
14
14
|
extensions?: IExtension[];
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ export interface IExtension {
|
|
|
25
25
|
|
|
26
26
|
export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom' | 'ai-failure-summary';
|
|
27
27
|
export type Hook = 'start' | 'end' | 'after-summary';
|
|
28
|
-
export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'custom' | 'delay';
|
|
28
|
+
export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'github-output' | 'custom' | 'delay';
|
|
29
29
|
export type PublishReportType = 'test-summary' | 'test-summary-slim' | 'failure-details';
|
|
30
30
|
|
|
31
31
|
export interface ConditionFunctionContext {
|
|
@@ -244,13 +244,18 @@ export interface TeamsInputs extends TargetInputs {
|
|
|
244
244
|
|
|
245
245
|
export interface ChatInputs extends TargetInputs { }
|
|
246
246
|
|
|
247
|
-
export interface
|
|
247
|
+
export interface IGitHubInputs extends TargetInputs {
|
|
248
248
|
token?: string;
|
|
249
249
|
owner?: string;
|
|
250
250
|
repo?: string;
|
|
251
251
|
pull_number?: string;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
export interface IGitHubOutputInputs {
|
|
255
|
+
output_file?: string;
|
|
256
|
+
key?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
export interface InfluxDBTargetInputs {
|
|
255
260
|
url: string;
|
|
256
261
|
version?: string;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple and extendable Gherkin parser for Cucumber feature files
|
|
5
|
+
* Parses .feature files and returns structured test suite objects
|
|
6
|
+
*/
|
|
7
|
+
class GherkinParser {
|
|
8
|
+
constructor() {
|
|
9
|
+
/** @type {string[]} Supported step keywords */
|
|
10
|
+
this.stepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} file_path
|
|
15
|
+
* @returns {Object} Parsed test suite structure
|
|
16
|
+
*/
|
|
17
|
+
parse(file_path) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(file_path, 'utf8');
|
|
20
|
+
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
21
|
+
|
|
22
|
+
return this.parseLines(lines);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Failed to parse Gherkin file: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse lines and build the test suite structure
|
|
30
|
+
* @param {string[]} lines
|
|
31
|
+
* @returns {Object}
|
|
32
|
+
*/
|
|
33
|
+
parseLines(lines) {
|
|
34
|
+
const testSuite = {
|
|
35
|
+
name: '',
|
|
36
|
+
type: 'feature',
|
|
37
|
+
tags: [],
|
|
38
|
+
beforeEach: [],
|
|
39
|
+
cases: []
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let currentFeature = null;
|
|
43
|
+
let currentBackground = null;
|
|
44
|
+
let currentScenario = null;
|
|
45
|
+
let pendingFeatureTags = [];
|
|
46
|
+
let pendingScenarioTags = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
|
|
51
|
+
if (line.startsWith('@')) {
|
|
52
|
+
// Handle tags
|
|
53
|
+
const tags = this.parseTags(line);
|
|
54
|
+
|
|
55
|
+
// Look ahead to see if tags belong to Feature or Scenario
|
|
56
|
+
if (i + 1 < lines.length && lines[i + 1].startsWith('Feature:')) {
|
|
57
|
+
// Tags belong to Feature
|
|
58
|
+
pendingFeatureTags = tags;
|
|
59
|
+
} else if (i + 1 < lines.length && lines[i + 1].startsWith('Scenario:')) {
|
|
60
|
+
// Tags belong to Scenario
|
|
61
|
+
pendingScenarioTags = tags;
|
|
62
|
+
}
|
|
63
|
+
} else if (line.startsWith('Feature:')) {
|
|
64
|
+
// Parse Feature
|
|
65
|
+
const description = this.parseMultiLineDescription(lines, i + 1);
|
|
66
|
+
currentFeature = this.parseFeature(line, description);
|
|
67
|
+
testSuite.name = currentFeature.name;
|
|
68
|
+
testSuite.tags = pendingFeatureTags.map(tag => tag.name);
|
|
69
|
+
pendingFeatureTags = [];
|
|
70
|
+
i += description.split('\n').length; // Skip all description lines
|
|
71
|
+
} else if (line.startsWith('Background:')) {
|
|
72
|
+
// Parse Background
|
|
73
|
+
currentBackground = this.parseBackground();
|
|
74
|
+
testSuite.beforeEach.push(currentBackground);
|
|
75
|
+
} else if (line.startsWith('Scenario:')) {
|
|
76
|
+
// Parse Scenario
|
|
77
|
+
currentScenario = this.parseScenario(line);
|
|
78
|
+
currentScenario.tags = pendingScenarioTags.map(tag => tag.name);
|
|
79
|
+
testSuite.cases.push(currentScenario);
|
|
80
|
+
pendingScenarioTags = [];
|
|
81
|
+
currentBackground = null; // Reset Background context when Scenario starts
|
|
82
|
+
} else if (this.isStep(line)) {
|
|
83
|
+
// Parse Step
|
|
84
|
+
const step = this.parseStep(line);
|
|
85
|
+
|
|
86
|
+
// Determine where to add the step based on current context
|
|
87
|
+
if (currentBackground && currentBackground.steps) {
|
|
88
|
+
// Add step to Background
|
|
89
|
+
currentBackground.steps.push(step);
|
|
90
|
+
} else if (currentScenario && currentScenario.steps) {
|
|
91
|
+
// Add step to Scenario
|
|
92
|
+
currentScenario.steps.push(step);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return testSuite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse tags from a line
|
|
102
|
+
* @param {string} line
|
|
103
|
+
* @returns {Array}
|
|
104
|
+
*/
|
|
105
|
+
parseTags(line) {
|
|
106
|
+
const tagMatches = line.match(/@\w+/g);
|
|
107
|
+
return tagMatches ? tagMatches.map(tag => ({ name: tag })) : [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse multi-line description starting from a given line index
|
|
112
|
+
* @param {string[]} lines
|
|
113
|
+
* @param {number} startIndex
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
parseMultiLineDescription(lines, startIndex) {
|
|
117
|
+
const descriptionLines = [];
|
|
118
|
+
|
|
119
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
120
|
+
const line = lines[i];
|
|
121
|
+
|
|
122
|
+
// Stop if we hit a keyword or step
|
|
123
|
+
if (line.startsWith('Background:') ||
|
|
124
|
+
line.startsWith('Scenario:') ||
|
|
125
|
+
line.startsWith('@') ||
|
|
126
|
+
this.isStep(line)) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add non-empty lines to description
|
|
131
|
+
if (line.trim().length > 0) {
|
|
132
|
+
descriptionLines.push(line.trim());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return descriptionLines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse Feature line
|
|
141
|
+
* @param {string} line
|
|
142
|
+
* @param {string} description
|
|
143
|
+
* @returns {Object}
|
|
144
|
+
*/
|
|
145
|
+
parseFeature(line, description) {
|
|
146
|
+
const name = line.replace('Feature:', '').trim();
|
|
147
|
+
return {
|
|
148
|
+
type: 'Feature',
|
|
149
|
+
tags: [],
|
|
150
|
+
keyword: 'Feature',
|
|
151
|
+
name: name,
|
|
152
|
+
description: description.trim(),
|
|
153
|
+
children: []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse Background line
|
|
159
|
+
* @returns {Object}
|
|
160
|
+
*/
|
|
161
|
+
parseBackground() {
|
|
162
|
+
return {
|
|
163
|
+
name: '',
|
|
164
|
+
type: 'background',
|
|
165
|
+
steps: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse Scenario line
|
|
171
|
+
* @param {string} line
|
|
172
|
+
* @returns {Object}
|
|
173
|
+
*/
|
|
174
|
+
parseScenario(line) {
|
|
175
|
+
const name = line.replace('Scenario:', '').trim();
|
|
176
|
+
return {
|
|
177
|
+
name: name,
|
|
178
|
+
type: 'scenario',
|
|
179
|
+
tags: [],
|
|
180
|
+
steps: []
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a line is a step
|
|
186
|
+
* @param {string} line
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
isStep(line) {
|
|
190
|
+
return this.stepKeywords.some(keyword =>
|
|
191
|
+
line.startsWith(keyword + ' ') ||
|
|
192
|
+
line.startsWith('And ') ||
|
|
193
|
+
line.startsWith('But ')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse a step line
|
|
199
|
+
* @param {string} line
|
|
200
|
+
* @returns {Object}
|
|
201
|
+
*/
|
|
202
|
+
parseStep(line) {
|
|
203
|
+
return {
|
|
204
|
+
name: line.trim()
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse from string content instead of file
|
|
210
|
+
* @param {string} content
|
|
211
|
+
* @returns {Object} Parsed test suite structure
|
|
212
|
+
*/
|
|
213
|
+
parseString(content) {
|
|
214
|
+
try {
|
|
215
|
+
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
216
|
+
return this.parseLines(lines);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Failed to parse Gherkin content: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate if the parsed document has required structure
|
|
224
|
+
* @param {Object} document
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
validate(document) {
|
|
228
|
+
return document &&
|
|
229
|
+
document.name &&
|
|
230
|
+
document.type === 'feature' &&
|
|
231
|
+
Array.isArray(document.tags) &&
|
|
232
|
+
Array.isArray(document.beforeEach) &&
|
|
233
|
+
Array.isArray(document.cases);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { GherkinParser };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const hash = require('object-hash');
|
|
4
|
+
const { GherkinParser } = require('./parsers/gherkin');
|
|
5
|
+
|
|
6
|
+
class ManualSyncHelper {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.parser = new GherkinParser();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate hash for an object
|
|
13
|
+
* @param {Object} obj - Object to hash
|
|
14
|
+
* @returns {string} Hash string
|
|
15
|
+
*/
|
|
16
|
+
generateHash(obj) {
|
|
17
|
+
return hash(obj, { algorithm: 'md5' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add hash to test case
|
|
22
|
+
* @param {Object} testCase - Test case object
|
|
23
|
+
* @returns {Object} Test case with hash
|
|
24
|
+
*/
|
|
25
|
+
addTestCaseHash(testCase) {
|
|
26
|
+
return {
|
|
27
|
+
...testCase,
|
|
28
|
+
hash: this.generateHash(testCase)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Add hash to test suite
|
|
34
|
+
* @param {Object} testSuite - Test suite object
|
|
35
|
+
* @returns {Object} Test suite with hash
|
|
36
|
+
*/
|
|
37
|
+
addTestSuiteHash(testSuite) {
|
|
38
|
+
return {
|
|
39
|
+
...testSuite,
|
|
40
|
+
hash: this.generateHash(testSuite),
|
|
41
|
+
test_cases: testSuite.test_cases.map(testCase => this.addTestCaseHash(testCase))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add hash to folder
|
|
47
|
+
* @param {Object} folder - Folder object
|
|
48
|
+
* @returns {Object} Folder with hash
|
|
49
|
+
*/
|
|
50
|
+
addFolderHash(folder) {
|
|
51
|
+
return {
|
|
52
|
+
...folder,
|
|
53
|
+
hash: this.generateHash(folder),
|
|
54
|
+
test_suites: folder.test_suites.map(testSuite => this.addTestSuiteHash(testSuite)),
|
|
55
|
+
folders: folder.folders.map(subFolder => this.addFolderHash(subFolder))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scan directory recursively and build folder structure
|
|
61
|
+
* @param {string} directoryPath - Path to scan
|
|
62
|
+
* @returns {Object} Folder structure with test suites and hashes
|
|
63
|
+
*/
|
|
64
|
+
async scanDirectory(directoryPath) {
|
|
65
|
+
const absolutePath = path.resolve(directoryPath);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(absolutePath)) {
|
|
68
|
+
throw new Error(`Directory does not exist: ${directoryPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
72
|
+
throw new Error(`Path is not a directory: ${directoryPath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const structure = this.buildFolderStructure(absolutePath, directoryPath);
|
|
76
|
+
return this.addFolderHash(structure);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build folder structure recursively
|
|
81
|
+
* @param {string} absolutePath - Absolute path for file operations
|
|
82
|
+
* @param {string} relativePath - Relative path for output
|
|
83
|
+
* @returns {Object} Folder structure
|
|
84
|
+
*/
|
|
85
|
+
buildFolderStructure(absolutePath, relativePath) {
|
|
86
|
+
const folderName = path.basename(absolutePath);
|
|
87
|
+
const items = fs.readdirSync(absolutePath);
|
|
88
|
+
|
|
89
|
+
const structure = {
|
|
90
|
+
name: folderName,
|
|
91
|
+
path: relativePath,
|
|
92
|
+
test_suites: [],
|
|
93
|
+
folders: []
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
const itemPath = path.join(absolutePath, item);
|
|
98
|
+
const itemRelativePath = path.join(relativePath, item);
|
|
99
|
+
const stats = fs.statSync(itemPath);
|
|
100
|
+
|
|
101
|
+
if (stats.isDirectory()) {
|
|
102
|
+
// Recursively process subdirectories
|
|
103
|
+
const subFolder = this.buildFolderStructure(itemPath, itemRelativePath);
|
|
104
|
+
structure.folders.push(subFolder);
|
|
105
|
+
} else if (this.isGherkinFile(item)) {
|
|
106
|
+
// Parse gherkin files
|
|
107
|
+
try {
|
|
108
|
+
const testSuite = this.parseGherkinFile(itemPath, itemRelativePath);
|
|
109
|
+
structure.test_suites.push(testSuite);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`Warning: Failed to parse ${itemPath}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return structure;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if file is a gherkin file
|
|
121
|
+
* @param {string} filename - Filename to check
|
|
122
|
+
* @returns {boolean} True if gherkin file
|
|
123
|
+
*/
|
|
124
|
+
isGherkinFile(filename) {
|
|
125
|
+
return filename.endsWith('.feature');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse a gherkin file and format output
|
|
130
|
+
* @param {string} filePath - Path to gherkin file
|
|
131
|
+
* @param {string} relativePath - Relative path for output
|
|
132
|
+
* @returns {Object} Formatted test suite
|
|
133
|
+
*/
|
|
134
|
+
parseGherkinFile(filePath, relativePath) {
|
|
135
|
+
const parsed = this.parser.parse(filePath);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: parsed.name,
|
|
139
|
+
type: parsed.type,
|
|
140
|
+
tags: parsed.tags,
|
|
141
|
+
beforeEach: parsed.beforeEach,
|
|
142
|
+
path: relativePath,
|
|
143
|
+
test_cases: parsed.cases || [] // Map 'cases' to 'test_cases' for consistency
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { ManualSyncHelper };
|