@sun-asterisk/sungen 1.0.5 → 1.0.6
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/dist/cli/index.js +79 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/validator/data-validator.d.ts +38 -0
- package/dist/core/validator/data-validator.d.ts.map +1 -0
- package/dist/core/validator/data-validator.js +212 -0
- package/dist/core/validator/data-validator.js.map +1 -0
- package/dist/core/validator/feature-validator.d.ts +27 -0
- package/dist/core/validator/feature-validator.d.ts.map +1 -0
- package/dist/core/validator/feature-validator.js +182 -0
- package/dist/core/validator/feature-validator.js.map +1 -0
- package/dist/core/validator/index.d.ts +46 -0
- package/dist/core/validator/index.d.ts.map +1 -0
- package/dist/core/validator/index.js +17 -0
- package/dist/core/validator/index.js.map +1 -0
- package/dist/core/validator/screen-validator.d.ts +35 -0
- package/dist/core/validator/screen-validator.d.ts.map +1 -0
- package/dist/core/validator/screen-validator.js +195 -0
- package/dist/core/validator/screen-validator.js.map +1 -0
- package/dist/core/validator/selector-validator.d.ts +35 -0
- package/dist/core/validator/selector-validator.d.ts.map +1 -0
- package/dist/core/validator/selector-validator.js +210 -0
- package/dist/core/validator/selector-validator.js.map +1 -0
- package/dist/generators/cli.js +1 -1
- package/dist/input/cli-adapter.d.ts +4 -0
- package/dist/input/cli-adapter.d.ts.map +1 -1
- package/dist/input/cli-adapter.js +12 -1
- package/dist/input/cli-adapter.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +85 -0
- package/src/core/validator/data-validator.ts +202 -0
- package/src/core/validator/feature-validator.ts +176 -0
- package/src/core/validator/index.ts +57 -0
- package/src/core/validator/screen-validator.ts +209 -0
- package/src/core/validator/selector-validator.ts +208 -0
- package/src/generators/cli.ts +1 -1
- package/src/input/cli-adapter.ts +13 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen Validator
|
|
3
|
+
* Orchestrates validation for a screen (all features in qa/screens/<name>/)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import {
|
|
9
|
+
ValidationResult,
|
|
10
|
+
FileValidationResult,
|
|
11
|
+
ValidationSummary,
|
|
12
|
+
ValidationIssue,
|
|
13
|
+
} from './index';
|
|
14
|
+
import { FeatureValidator } from './feature-validator';
|
|
15
|
+
import { SelectorValidator } from './selector-validator';
|
|
16
|
+
import { DataValidator } from './data-validator';
|
|
17
|
+
|
|
18
|
+
export interface ScreenValidatorOptions {
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
baseDir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ScreenValidator {
|
|
24
|
+
private baseDir: string;
|
|
25
|
+
private verbose: boolean;
|
|
26
|
+
|
|
27
|
+
constructor(options: ScreenValidatorOptions = {}) {
|
|
28
|
+
this.baseDir = options.baseDir || process.cwd();
|
|
29
|
+
this.verbose = options.verbose || false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate a specific screen
|
|
34
|
+
*/
|
|
35
|
+
validateScreen(screenName: string): ValidationResult {
|
|
36
|
+
const results: FileValidationResult[] = [];
|
|
37
|
+
|
|
38
|
+
const screenDir = path.join(this.baseDir, 'qa', 'screens', screenName);
|
|
39
|
+
const featuresDir = path.join(screenDir, 'features');
|
|
40
|
+
|
|
41
|
+
// Check if screen exists
|
|
42
|
+
if (!fs.existsSync(screenDir)) {
|
|
43
|
+
return this.createErrorResult(`Screen "${screenName}" not found at ${screenDir}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(featuresDir)) {
|
|
47
|
+
return this.createErrorResult(`Features directory not found at ${featuresDir}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Find all feature files
|
|
51
|
+
const featureFiles = this.discoverFeatureFiles(featuresDir);
|
|
52
|
+
|
|
53
|
+
if (featureFiles.length === 0) {
|
|
54
|
+
return this.createErrorResult(`No .feature files found in ${featuresDir}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Initialize validators
|
|
58
|
+
const featureValidator = new FeatureValidator();
|
|
59
|
+
const selectorValidator = new SelectorValidator(screenName, this.baseDir);
|
|
60
|
+
const dataValidator = new DataValidator(screenName, this.baseDir);
|
|
61
|
+
|
|
62
|
+
// Validate each feature file
|
|
63
|
+
for (const featureFile of featureFiles) {
|
|
64
|
+
const featureName = path.basename(featureFile, '.feature');
|
|
65
|
+
const relativeFile = path.relative(this.baseDir, featureFile);
|
|
66
|
+
|
|
67
|
+
const fileResult: FileValidationResult = {
|
|
68
|
+
file: relativeFile,
|
|
69
|
+
valid: true,
|
|
70
|
+
errors: [],
|
|
71
|
+
warnings: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// 1. Validate Gherkin syntax and extract refs
|
|
75
|
+
const featureResult = featureValidator.validate(featureFile);
|
|
76
|
+
fileResult.errors.push(...featureResult.errors);
|
|
77
|
+
fileResult.warnings.push(...featureResult.warnings);
|
|
78
|
+
|
|
79
|
+
// 2. Validate selector file
|
|
80
|
+
const selectorFileResult = selectorValidator.validateFile(featureName);
|
|
81
|
+
fileResult.errors.push(...selectorFileResult.errors);
|
|
82
|
+
fileResult.warnings.push(...selectorFileResult.warnings);
|
|
83
|
+
|
|
84
|
+
// 3. Validate selector refs exist
|
|
85
|
+
if (featureResult.refs.selectors.length > 0) {
|
|
86
|
+
const selectorRefErrors = selectorValidator.validateRefs(
|
|
87
|
+
featureName,
|
|
88
|
+
featureResult.refs.selectors,
|
|
89
|
+
relativeFile
|
|
90
|
+
);
|
|
91
|
+
fileResult.errors.push(...selectorRefErrors);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Validate data file
|
|
95
|
+
const dataFileResult = dataValidator.validateFile(featureName);
|
|
96
|
+
fileResult.errors.push(...dataFileResult.errors);
|
|
97
|
+
fileResult.warnings.push(...dataFileResult.warnings);
|
|
98
|
+
|
|
99
|
+
// 5. Validate data refs exist
|
|
100
|
+
if (featureResult.refs.dataRefs.length > 0) {
|
|
101
|
+
const dataRefErrors = dataValidator.validateRefs(
|
|
102
|
+
featureName,
|
|
103
|
+
featureResult.refs.dataRefs,
|
|
104
|
+
relativeFile
|
|
105
|
+
);
|
|
106
|
+
fileResult.errors.push(...dataRefErrors);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Update validity
|
|
110
|
+
fileResult.valid = fileResult.errors.length === 0;
|
|
111
|
+
results.push(fileResult);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.buildResult(results);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate all screens
|
|
119
|
+
*/
|
|
120
|
+
validateAll(): ValidationResult {
|
|
121
|
+
const screensDir = path.join(this.baseDir, 'qa', 'screens');
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(screensDir)) {
|
|
124
|
+
return this.createErrorResult(`Screens directory not found at ${screensDir}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const screens = fs.readdirSync(screensDir, { withFileTypes: true })
|
|
128
|
+
.filter(entry => entry.isDirectory())
|
|
129
|
+
.map(entry => entry.name);
|
|
130
|
+
|
|
131
|
+
if (screens.length === 0) {
|
|
132
|
+
return this.createErrorResult(`No screens found in ${screensDir}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const allResults: FileValidationResult[] = [];
|
|
136
|
+
|
|
137
|
+
for (const screenName of screens) {
|
|
138
|
+
const screenResult = this.validateScreen(screenName);
|
|
139
|
+
allResults.push(...screenResult.results);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.buildResult(allResults);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Discover .feature files in a directory
|
|
147
|
+
*/
|
|
148
|
+
private discoverFeatureFiles(dir: string): string[] {
|
|
149
|
+
const files: string[] = [];
|
|
150
|
+
|
|
151
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const fullPath = path.join(dir, entry.name);
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
files.push(...this.discoverFeatureFiles(fullPath));
|
|
156
|
+
} else if (entry.isFile() && entry.name.endsWith('.feature')) {
|
|
157
|
+
files.push(fullPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return files;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build final validation result
|
|
166
|
+
*/
|
|
167
|
+
private buildResult(results: FileValidationResult[]): ValidationResult {
|
|
168
|
+
const summary: ValidationSummary = {
|
|
169
|
+
totalFiles: results.length,
|
|
170
|
+
passedFiles: results.filter(r => r.valid).length,
|
|
171
|
+
failedFiles: results.filter(r => !r.valid).length,
|
|
172
|
+
totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0),
|
|
173
|
+
totalWarnings: results.reduce((sum, r) => sum + r.warnings.length, 0),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
valid: summary.failedFiles === 0,
|
|
178
|
+
summary,
|
|
179
|
+
results,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create error result for invalid setup
|
|
185
|
+
*/
|
|
186
|
+
private createErrorResult(message: string): ValidationResult {
|
|
187
|
+
return {
|
|
188
|
+
valid: false,
|
|
189
|
+
summary: {
|
|
190
|
+
totalFiles: 0,
|
|
191
|
+
passedFiles: 0,
|
|
192
|
+
failedFiles: 0,
|
|
193
|
+
totalErrors: 1,
|
|
194
|
+
totalWarnings: 0,
|
|
195
|
+
},
|
|
196
|
+
results: [{
|
|
197
|
+
file: '',
|
|
198
|
+
valid: false,
|
|
199
|
+
errors: [{
|
|
200
|
+
type: 'error',
|
|
201
|
+
code: 'GHERKIN_SYNTAX_ERROR',
|
|
202
|
+
message,
|
|
203
|
+
file: '',
|
|
204
|
+
}],
|
|
205
|
+
warnings: [],
|
|
206
|
+
}],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector Validator
|
|
3
|
+
* Validates selector YAML files and checks referenced selectors exist
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import yaml from 'yaml';
|
|
9
|
+
import { ValidationIssue } from './index';
|
|
10
|
+
|
|
11
|
+
interface SelectorEntry {
|
|
12
|
+
selector?: string;
|
|
13
|
+
type?: 'placeholder' | 'role' | 'testid' | 'label' | 'text';
|
|
14
|
+
value?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
nth?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SelectorFile = Record<string, SelectorEntry>;
|
|
20
|
+
|
|
21
|
+
const VALID_TYPES = ['placeholder', 'role', 'testid', 'label', 'text'];
|
|
22
|
+
|
|
23
|
+
export class SelectorValidator {
|
|
24
|
+
private screenName: string;
|
|
25
|
+
private baseDir: string;
|
|
26
|
+
private selectorCache = new Map<string, SelectorFile>();
|
|
27
|
+
|
|
28
|
+
constructor(screenName: string, baseDir?: string) {
|
|
29
|
+
this.screenName = screenName;
|
|
30
|
+
this.baseDir = baseDir || process.cwd();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate selector file for a feature
|
|
35
|
+
*/
|
|
36
|
+
validateFile(featureName: string): { errors: ValidationIssue[]; warnings: ValidationIssue[] } {
|
|
37
|
+
const errors: ValidationIssue[] = [];
|
|
38
|
+
const warnings: ValidationIssue[] = [];
|
|
39
|
+
|
|
40
|
+
const filePath = this.findSelectorPath(featureName);
|
|
41
|
+
|
|
42
|
+
if (!filePath) {
|
|
43
|
+
errors.push({
|
|
44
|
+
type: 'error',
|
|
45
|
+
code: 'MISSING_SELECTOR_FILE',
|
|
46
|
+
message: `Selector file not found for feature "${featureName}"`,
|
|
47
|
+
file: `qa/screens/${this.screenName}/selectors/${featureName}.yaml`,
|
|
48
|
+
});
|
|
49
|
+
return { errors, warnings };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load and parse YAML
|
|
53
|
+
let content: string;
|
|
54
|
+
let selectors: SelectorFile;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
errors.push({
|
|
60
|
+
type: 'error',
|
|
61
|
+
code: 'INVALID_YAML_SYNTAX',
|
|
62
|
+
message: `Cannot read file: ${(error as Error).message}`,
|
|
63
|
+
file: filePath,
|
|
64
|
+
});
|
|
65
|
+
return { errors, warnings };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
selectors = yaml.parse(content) as SelectorFile;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
errors.push({
|
|
72
|
+
type: 'error',
|
|
73
|
+
code: 'INVALID_YAML_SYNTAX',
|
|
74
|
+
message: `YAML parse error: ${(error as Error).message}`,
|
|
75
|
+
file: filePath,
|
|
76
|
+
});
|
|
77
|
+
return { errors, warnings };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!selectors || typeof selectors !== 'object') {
|
|
81
|
+
errors.push({
|
|
82
|
+
type: 'error',
|
|
83
|
+
code: 'INVALID_YAML_SYNTAX',
|
|
84
|
+
message: 'Selector file must be a valid YAML object',
|
|
85
|
+
file: filePath,
|
|
86
|
+
});
|
|
87
|
+
return { errors, warnings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate each selector entry
|
|
91
|
+
for (const [key, entry] of Object.entries(selectors)) {
|
|
92
|
+
if (!entry || typeof entry !== 'object') {
|
|
93
|
+
warnings.push({
|
|
94
|
+
type: 'warning',
|
|
95
|
+
code: 'INVALID_SELECTOR_TYPE',
|
|
96
|
+
message: `Selector "${key}" should be an object with type/value properties`,
|
|
97
|
+
file: filePath,
|
|
98
|
+
element: key,
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check type is valid
|
|
104
|
+
if (entry.type && !VALID_TYPES.includes(entry.type)) {
|
|
105
|
+
warnings.push({
|
|
106
|
+
type: 'warning',
|
|
107
|
+
code: 'INVALID_SELECTOR_TYPE',
|
|
108
|
+
message: `Selector "${key}" has invalid type "${entry.type}". Valid types: ${VALID_TYPES.join(', ')}`,
|
|
109
|
+
file: filePath,
|
|
110
|
+
element: key,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cache for later ref validation
|
|
116
|
+
this.selectorCache.set(featureName, selectors);
|
|
117
|
+
|
|
118
|
+
return { errors, warnings };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if selector references exist
|
|
123
|
+
*/
|
|
124
|
+
validateRefs(
|
|
125
|
+
featureName: string,
|
|
126
|
+
refs: Array<{ ref: string; line: number }>,
|
|
127
|
+
featureFile: string
|
|
128
|
+
): ValidationIssue[] {
|
|
129
|
+
const errors: ValidationIssue[] = [];
|
|
130
|
+
|
|
131
|
+
// Load selectors if not cached
|
|
132
|
+
if (!this.selectorCache.has(featureName)) {
|
|
133
|
+
const filePath = this.findSelectorPath(featureName);
|
|
134
|
+
if (!filePath) {
|
|
135
|
+
// Already reported in validateFile
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
140
|
+
const selectors = yaml.parse(content) as SelectorFile;
|
|
141
|
+
this.selectorCache.set(featureName, selectors);
|
|
142
|
+
} catch {
|
|
143
|
+
// Error already reported in validateFile
|
|
144
|
+
return errors;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const selectors = this.selectorCache.get(featureName);
|
|
149
|
+
if (!selectors) {
|
|
150
|
+
return errors;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check each ref
|
|
154
|
+
for (const { ref, line } of refs) {
|
|
155
|
+
const key = this.generateKey(ref);
|
|
156
|
+
|
|
157
|
+
if (!(key in selectors)) {
|
|
158
|
+
errors.push({
|
|
159
|
+
type: 'error',
|
|
160
|
+
code: 'MISSING_SELECTOR',
|
|
161
|
+
message: `Selector [${ref}] (key: ${key}) not found in selectors/${featureName}.yaml`,
|
|
162
|
+
file: featureFile,
|
|
163
|
+
line,
|
|
164
|
+
element: ref,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return errors;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate selector key from natural language label
|
|
174
|
+
* Matches logic in SelectorResolver.generateKey()
|
|
175
|
+
*/
|
|
176
|
+
private generateKey(label: string): string {
|
|
177
|
+
return label
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.replace(/['\u2019]s/g, 's')
|
|
180
|
+
.replace(/['\u2019]/g, '')
|
|
181
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
182
|
+
.trim()
|
|
183
|
+
.replace(/\s+/g, '.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find selector file path (checks override first)
|
|
188
|
+
*/
|
|
189
|
+
private findSelectorPath(featureName: string): string | null {
|
|
190
|
+
const possiblePaths = [
|
|
191
|
+
// Override (highest priority)
|
|
192
|
+
path.join(this.baseDir, 'qa', 'screens', this.screenName, 'selectors', `${featureName}-override.yaml`),
|
|
193
|
+
// Normal
|
|
194
|
+
path.join(this.baseDir, 'qa', 'screens', this.screenName, 'selectors', `${featureName}.yaml`),
|
|
195
|
+
// Legacy paths
|
|
196
|
+
path.join(this.baseDir, 'qa', 'selectors', 'screens', `${featureName}-override.yaml`),
|
|
197
|
+
path.join(this.baseDir, 'qa', 'selectors', 'screens', `${featureName}.yaml`),
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
for (const p of possiblePaths) {
|
|
201
|
+
if (fs.existsSync(p)) {
|
|
202
|
+
return p;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/generators/cli.ts
CHANGED
|
@@ -52,7 +52,7 @@ const program = new Command();
|
|
|
52
52
|
program
|
|
53
53
|
.name('qa-generator')
|
|
54
54
|
.description('AI-Native QA Selector DSL Generator')
|
|
55
|
-
.version('1.0.
|
|
55
|
+
.version('1.0.6');
|
|
56
56
|
|
|
57
57
|
// ============================================================================
|
|
58
58
|
// Command: discover
|
package/src/input/cli-adapter.ts
CHANGED
|
@@ -36,7 +36,7 @@ export class CLIAdapter implements InputAdapter {
|
|
|
36
36
|
program
|
|
37
37
|
.name('sungen')
|
|
38
38
|
.description('AI-Native E2E Test Generator - Generate Playwright tests from Gherkin features')
|
|
39
|
-
.version('1.0.
|
|
39
|
+
.version('1.0.6');
|
|
40
40
|
|
|
41
41
|
// Global options
|
|
42
42
|
program
|
|
@@ -157,6 +157,18 @@ export class CLIAdapter implements InputAdapter {
|
|
|
157
157
|
.option('-d, --description <text>', 'Screen description');
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Add validate command
|
|
162
|
+
*/
|
|
163
|
+
addValidateCommand(program: Command): Command {
|
|
164
|
+
return program
|
|
165
|
+
.command('validate')
|
|
166
|
+
.description('Validate Gherkin features and selector/test-data mappings')
|
|
167
|
+
.option('-s, --screen <name>', 'Validate specific screen (optional, defaults to all)')
|
|
168
|
+
.option('--json', 'Output results as JSON')
|
|
169
|
+
.option('-v, --verbose', 'Show all checks, not just failures');
|
|
170
|
+
}
|
|
171
|
+
|
|
160
172
|
/**
|
|
161
173
|
* Extract options from commander Command
|
|
162
174
|
*/
|