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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { ManualTestScanner } = require('./scanner');
|
|
2
|
+
const { ManualTestComparator } = require('./comparator');
|
|
3
|
+
const { ManualTestSynchronizer } = require('./synchronizer');
|
|
4
|
+
const { ManualSyncOrchestrator } = require('./orchestrator');
|
|
5
|
+
const { ManualSyncServiceFactory } = require('./factory');
|
|
6
|
+
const { ProjectResolver } = require('./project-resolver');
|
|
7
|
+
const { ManualTestHasher } = require('./hasher');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
ManualTestScanner,
|
|
11
|
+
ManualTestComparator,
|
|
12
|
+
ManualTestSynchronizer,
|
|
13
|
+
ManualSyncOrchestrator,
|
|
14
|
+
ManualSyncServiceFactory,
|
|
15
|
+
ProjectResolver,
|
|
16
|
+
ManualTestHasher,
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrates the manual sync pipeline
|
|
5
|
+
* Implements the Template Method pattern to coordinate the sync workflow
|
|
6
|
+
*/
|
|
7
|
+
class ManualSyncOrchestrator {
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('./scanner').ManualTestScanner} scanner
|
|
10
|
+
* @param {import('./comparator').ManualTestComparator} comparator
|
|
11
|
+
* @param {import('./synchronizer').ManualTestSynchronizer} synchronizer
|
|
12
|
+
* @param {import('./project-resolver').ProjectResolver} projectResolver
|
|
13
|
+
* @param {import('./hasher').ManualTestHasher} hasher
|
|
14
|
+
*/
|
|
15
|
+
constructor(scanner, comparator, synchronizer, projectResolver, hasher) {
|
|
16
|
+
this.scanner = scanner;
|
|
17
|
+
this.comparator = comparator;
|
|
18
|
+
this.synchronizer = synchronizer;
|
|
19
|
+
this.projectResolver = projectResolver;
|
|
20
|
+
this.hasher = hasher;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute the complete manual sync pipeline
|
|
25
|
+
* @param {string} directoryPath - Path to scan for manual tests
|
|
26
|
+
* @param {string} projectName - Project name to sync to
|
|
27
|
+
* @returns {Promise<Object>} Sync result with statistics and errors
|
|
28
|
+
*/
|
|
29
|
+
async execute(directoryPath, projectName) {
|
|
30
|
+
const result = {
|
|
31
|
+
success: false,
|
|
32
|
+
projectId: null,
|
|
33
|
+
statistics: {
|
|
34
|
+
totalTestCases: 0,
|
|
35
|
+
totalTestSuites: 0,
|
|
36
|
+
totalFolders: 0,
|
|
37
|
+
foldersProcessed: 0,
|
|
38
|
+
testSuitesProcessed: 0
|
|
39
|
+
},
|
|
40
|
+
errors: []
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
logger.info(`📋 Resolving project: ${projectName}`);
|
|
45
|
+
result.projectId = await this.projectResolver.resolveProject(projectName);
|
|
46
|
+
logger.info(`✅ Project resolved: ${result.projectId}`);
|
|
47
|
+
|
|
48
|
+
logger.info(`📁 Scanning directory: ${directoryPath}`);
|
|
49
|
+
const scannedStructure = await this.scanner.scanDirectory(directoryPath);
|
|
50
|
+
|
|
51
|
+
logger.debug('🔐 Adding hashes to structure...');
|
|
52
|
+
this.hasher.hashStructure(scannedStructure);
|
|
53
|
+
|
|
54
|
+
const stats = this.#calculateStatistics(scannedStructure);
|
|
55
|
+
result.statistics.totalTestCases = stats.totalTestCases;
|
|
56
|
+
result.statistics.totalTestSuites = stats.totalTestSuites;
|
|
57
|
+
result.statistics.totalFolders = stats.totalFolders;
|
|
58
|
+
|
|
59
|
+
logger.info(`📊 Found ${stats.totalTestCases} test cases, ${stats.totalTestSuites} test suites, and ${stats.totalFolders} folders`);
|
|
60
|
+
|
|
61
|
+
logger.info('🔍 Comparing with server...');
|
|
62
|
+
const comparisonResult = await this.comparator.compare(scannedStructure, result.projectId);
|
|
63
|
+
logger.info('✅ Comparison completed');
|
|
64
|
+
|
|
65
|
+
logger.info('🔄 Synchronizing resources...');
|
|
66
|
+
const syncResult = await this.synchronizer.sync(scannedStructure, comparisonResult, result.projectId);
|
|
67
|
+
|
|
68
|
+
result.statistics.foldersProcessed = syncResult.foldersProcessed;
|
|
69
|
+
result.statistics.testSuitesProcessed = syncResult.testSuitesProcessed;
|
|
70
|
+
result.errors = syncResult.errors;
|
|
71
|
+
result.success = syncResult.success;
|
|
72
|
+
|
|
73
|
+
if (result.success) {
|
|
74
|
+
logger.info('✅ Synchronization completed successfully');
|
|
75
|
+
} else {
|
|
76
|
+
logger.warn(`⚠️ Synchronization completed with ${result.errors.length} error(s)`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error(`❌ Manual sync failed: ${error.message}`);
|
|
82
|
+
result.errors.push({
|
|
83
|
+
type: 'pipeline',
|
|
84
|
+
name: 'execution',
|
|
85
|
+
error: error.message
|
|
86
|
+
});
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calculate statistics from scanned structure
|
|
93
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
94
|
+
* @returns {{totalTestCases: number, totalTestSuites: number, totalFolders: number}}
|
|
95
|
+
*/
|
|
96
|
+
#calculateStatistics(structure) {
|
|
97
|
+
const stats = {
|
|
98
|
+
totalTestCases: 0,
|
|
99
|
+
totalTestSuites: 0,
|
|
100
|
+
totalFolders: 0
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const processFolder = (folder) => {
|
|
104
|
+
for (const suite of folder.test_suites) {
|
|
105
|
+
stats.totalTestCases += suite.test_cases.length;
|
|
106
|
+
stats.totalTestSuites++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stats.totalFolders += folder.folders.length;
|
|
110
|
+
for (const subFolder of folder.folders) {
|
|
111
|
+
processFolder(subFolder);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (const folder of structure.folders) {
|
|
116
|
+
processFolder(folder);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return stats;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { ManualSyncOrchestrator };
|
|
124
|
+
|
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Base class for manual test parsers
|
|
3
|
+
* Provides common functionality for parsing test files
|
|
4
|
+
*/
|
|
3
5
|
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
6
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @param {import('../../types').IManualTestSuite} testSuite
|
|
26
|
-
* @returns {string}
|
|
7
|
+
* @param {import('fs')} fs
|
|
27
8
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
before_each: testSuite.before_each,
|
|
34
|
-
test_cases: testSuite.test_cases.map(testCase => testCase.hash)
|
|
35
|
-
});
|
|
9
|
+
constructor(fs) {
|
|
10
|
+
/**
|
|
11
|
+
* @type {import('fs')}
|
|
12
|
+
*/
|
|
13
|
+
this.fs = fs;
|
|
36
14
|
}
|
|
37
15
|
}
|
|
38
16
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
1
|
const { BaseParser } = require('./base');
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -6,8 +5,8 @@ const { BaseParser } = require('./base');
|
|
|
6
5
|
* Parses .feature files and returns structured test suite objects
|
|
7
6
|
*/
|
|
8
7
|
class GherkinParser extends BaseParser {
|
|
9
|
-
constructor() {
|
|
10
|
-
super();
|
|
8
|
+
constructor(fs) {
|
|
9
|
+
super(fs);
|
|
11
10
|
/** @type {string[]} Supported step keywords */
|
|
12
11
|
this.stepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
13
12
|
}
|
|
@@ -18,7 +17,7 @@ class GherkinParser extends BaseParser {
|
|
|
18
17
|
*/
|
|
19
18
|
parse(file_path) {
|
|
20
19
|
try {
|
|
21
|
-
const content = fs.readFileSync(file_path, 'utf8');
|
|
20
|
+
const content = this.fs.readFileSync(file_path, 'utf8');
|
|
22
21
|
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
23
22
|
|
|
24
23
|
return this.parseLines(lines);
|
|
@@ -54,25 +53,37 @@ class GherkinParser extends BaseParser {
|
|
|
54
53
|
const line = lines[i];
|
|
55
54
|
|
|
56
55
|
if (line.startsWith('@')) {
|
|
57
|
-
// Handle tags
|
|
56
|
+
// Handle tags - accumulate from consecutive @ lines
|
|
58
57
|
const tags = this.parseTags(line);
|
|
59
58
|
|
|
60
59
|
// Look ahead to see if tags belong to Feature or Scenario
|
|
61
60
|
if (i + 1 < lines.length && lines[i + 1].startsWith('Feature:')) {
|
|
62
|
-
// Tags belong to Feature
|
|
63
|
-
pendingFeatureTags
|
|
61
|
+
// Tags belong to Feature - accumulate instead of replace
|
|
62
|
+
pendingFeatureTags.push(...tags);
|
|
64
63
|
} else if (i + 1 < lines.length && lines[i + 1].startsWith('Scenario:')) {
|
|
65
|
-
// Tags belong to Scenario
|
|
66
|
-
pendingScenarioTags
|
|
64
|
+
// Tags belong to Scenario - accumulate instead of replace
|
|
65
|
+
pendingScenarioTags.push(...tags);
|
|
66
|
+
} else if (i + 1 < lines.length && lines[i + 1].startsWith('@')) {
|
|
67
|
+
// Next line is also a tag line - determine which pending array to use
|
|
68
|
+
// Look further ahead to find Feature or Scenario
|
|
69
|
+
let j = i + 1;
|
|
70
|
+
while (j < lines.length && lines[j].startsWith('@')) {
|
|
71
|
+
j++;
|
|
72
|
+
}
|
|
73
|
+
if (j < lines.length && lines[j].startsWith('Feature:')) {
|
|
74
|
+
pendingFeatureTags.push(...tags);
|
|
75
|
+
} else if (j < lines.length && lines[j].startsWith('Scenario:')) {
|
|
76
|
+
pendingScenarioTags.push(...tags);
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
} else if (line.startsWith('Feature:')) {
|
|
69
80
|
// Parse Feature
|
|
70
|
-
const
|
|
71
|
-
currentFeature = this.parseFeature(line, description);
|
|
81
|
+
const descriptionResult = this.parseMultiLineDescription(lines, i + 1);
|
|
82
|
+
currentFeature = this.parseFeature(line, descriptionResult.description);
|
|
72
83
|
testSuite.name = currentFeature.name;
|
|
73
84
|
testSuite.tags = pendingFeatureTags.map(tag => tag.name);
|
|
74
85
|
pendingFeatureTags = [];
|
|
75
|
-
i +=
|
|
86
|
+
i += descriptionResult.linesConsumed; // Skip only actual description lines
|
|
76
87
|
} else if (line.startsWith('Background:')) {
|
|
77
88
|
// Parse Background
|
|
78
89
|
currentBackground = this.parseBackground();
|
|
@@ -99,11 +110,6 @@ class GherkinParser extends BaseParser {
|
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
for (const testCase of testSuite.test_cases) {
|
|
103
|
-
testCase.hash = this.hashTestCase(testCase);
|
|
104
|
-
}
|
|
105
|
-
testSuite.hash = this.hashTestSuite(testSuite);
|
|
106
|
-
|
|
107
113
|
return testSuite;
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -121,10 +127,11 @@ class GherkinParser extends BaseParser {
|
|
|
121
127
|
* Parse multi-line description starting from a given line index
|
|
122
128
|
* @param {string[]} lines
|
|
123
129
|
* @param {number} startIndex
|
|
124
|
-
* @returns {string}
|
|
130
|
+
* @returns {{description: string, linesConsumed: number}}
|
|
125
131
|
*/
|
|
126
132
|
parseMultiLineDescription(lines, startIndex) {
|
|
127
133
|
const descriptionLines = [];
|
|
134
|
+
let linesConsumed = 0;
|
|
128
135
|
|
|
129
136
|
for (let i = startIndex; i < lines.length; i++) {
|
|
130
137
|
const line = lines[i];
|
|
@@ -137,13 +144,18 @@ class GherkinParser extends BaseParser {
|
|
|
137
144
|
break;
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
linesConsumed++;
|
|
148
|
+
|
|
140
149
|
// Add non-empty lines to description
|
|
141
150
|
if (line.trim().length > 0) {
|
|
142
151
|
descriptionLines.push(line.trim());
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
return
|
|
155
|
+
return {
|
|
156
|
+
description: descriptionLines.join('\n'),
|
|
157
|
+
linesConsumed: linesConsumed
|
|
158
|
+
};
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
/**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class ProjectResolver {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('../beats/beats.api').BeatsApi} beatsApi
|
|
4
|
+
*/
|
|
5
|
+
constructor(beatsApi) {
|
|
6
|
+
this.beatsApi = beatsApi;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve project name to project ID
|
|
11
|
+
* @param {string} projectName - Project name to resolve
|
|
12
|
+
* @returns {Promise<string>} Project ID
|
|
13
|
+
* @throws {Error} If project is not found
|
|
14
|
+
*/
|
|
15
|
+
async resolveProject(projectName) {
|
|
16
|
+
const projectsResponse = await this.beatsApi.searchProjects(projectName);
|
|
17
|
+
|
|
18
|
+
if (!projectsResponse || !projectsResponse.values) {
|
|
19
|
+
throw new Error(`Project ${projectName} not found`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const project = projectsResponse.values.find(p => p.name === projectName);
|
|
23
|
+
|
|
24
|
+
if (!project) {
|
|
25
|
+
throw new Error(`Project ${projectName} not found`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return project.id;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { ProjectResolver };
|
|
33
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_FOLDER_NAME = 'default';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scans directories for manual test cases and builds folder structures
|
|
8
|
+
*/
|
|
9
|
+
class ManualTestScanner {
|
|
10
|
+
constructor(fs, parser) {
|
|
11
|
+
/**
|
|
12
|
+
* @type {import('fs')}
|
|
13
|
+
*/
|
|
14
|
+
this.fs = fs;
|
|
15
|
+
/**
|
|
16
|
+
* @type {import('./parsers/gherkin').GherkinParser}
|
|
17
|
+
*/
|
|
18
|
+
this.parser = parser;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scan directory recursively and build folder structure
|
|
23
|
+
* @param {string} directoryPath - Path to scan
|
|
24
|
+
* @returns {Promise<import('../types').IManualTestFolder>} Folder structure with test suites and hashes
|
|
25
|
+
* @throws {Error} If directory doesn't exist or is not a directory
|
|
26
|
+
*/
|
|
27
|
+
async scanDirectory(directoryPath) {
|
|
28
|
+
const absolutePath = path.resolve(directoryPath);
|
|
29
|
+
|
|
30
|
+
if (!this.fs.existsSync(absolutePath)) {
|
|
31
|
+
throw new Error(`Directory ${directoryPath} does not exist`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!this.fs.statSync(absolutePath).isDirectory()) {
|
|
35
|
+
throw new Error(`Path ${directoryPath} is not a directory`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const structure = this.buildFolderStructure(absolutePath, directoryPath);
|
|
39
|
+
this.moveTestSuitesToDefaultFolder(structure);
|
|
40
|
+
return structure;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build folder structure recursively
|
|
45
|
+
* @param {string} absolutePath - Absolute path for file operations
|
|
46
|
+
* @param {string} relativePath - Relative path for output
|
|
47
|
+
* @returns {import('../types').IManualTestFolder} Folder structure
|
|
48
|
+
*/
|
|
49
|
+
buildFolderStructure(absolutePath, relativePath) {
|
|
50
|
+
const folderName = path.basename(absolutePath);
|
|
51
|
+
const items = this.fs.readdirSync(absolutePath);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @type {import('../types').IManualTestFolder}
|
|
55
|
+
*/
|
|
56
|
+
const structure = {
|
|
57
|
+
name: folderName,
|
|
58
|
+
path: relativePath,
|
|
59
|
+
test_suites: [],
|
|
60
|
+
folders: []
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const itemPath = path.join(absolutePath, item);
|
|
65
|
+
const itemRelativePath = path.join(relativePath, item);
|
|
66
|
+
const stats = this.fs.statSync(itemPath);
|
|
67
|
+
|
|
68
|
+
if (stats.isDirectory()) {
|
|
69
|
+
const subFolder = this.buildFolderStructure(itemPath, itemRelativePath);
|
|
70
|
+
structure.folders.push(subFolder);
|
|
71
|
+
} else if (this.isGherkinFile(item)) {
|
|
72
|
+
try {
|
|
73
|
+
const testSuite = this.parseGherkinFile(itemPath, itemRelativePath);
|
|
74
|
+
structure.test_suites.push(testSuite);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.warn(`Warning: Failed to parse ${itemPath}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return structure;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
moveTestSuitesToDefaultFolder(structure) {
|
|
85
|
+
if (structure.test_suites.length > 0) {
|
|
86
|
+
const defaultFolder = {
|
|
87
|
+
name: DEFAULT_FOLDER_NAME,
|
|
88
|
+
path: DEFAULT_FOLDER_NAME,
|
|
89
|
+
test_suites: structure.test_suites,
|
|
90
|
+
folders: []
|
|
91
|
+
};
|
|
92
|
+
structure.folders.push(defaultFolder);
|
|
93
|
+
structure.test_suites = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if file is a gherkin file
|
|
99
|
+
* @param {string} filename - Filename to check
|
|
100
|
+
* @returns {boolean} True if gherkin file
|
|
101
|
+
*/
|
|
102
|
+
isGherkinFile(filename) {
|
|
103
|
+
return filename.endsWith('.feature');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse a gherkin file and format output
|
|
108
|
+
* @param {string} filePath - Path to gherkin file
|
|
109
|
+
* @param {string} relativePath - Relative path for output
|
|
110
|
+
* @returns {import('../types').IManualTestSuite} Formatted test suite
|
|
111
|
+
*/
|
|
112
|
+
parseGherkinFile(filePath, relativePath) {
|
|
113
|
+
const parsed = this.parser.parse(filePath);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: parsed.name,
|
|
117
|
+
type: parsed.type,
|
|
118
|
+
tags: parsed.tags,
|
|
119
|
+
before_each: parsed.before_each,
|
|
120
|
+
path: relativePath,
|
|
121
|
+
test_cases: parsed.test_cases || []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { ManualTestScanner };
|
|
127
|
+
|