@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.
Files changed (36) hide show
  1. package/dist/cli/index.js +79 -0
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/core/validator/data-validator.d.ts +38 -0
  4. package/dist/core/validator/data-validator.d.ts.map +1 -0
  5. package/dist/core/validator/data-validator.js +212 -0
  6. package/dist/core/validator/data-validator.js.map +1 -0
  7. package/dist/core/validator/feature-validator.d.ts +27 -0
  8. package/dist/core/validator/feature-validator.d.ts.map +1 -0
  9. package/dist/core/validator/feature-validator.js +182 -0
  10. package/dist/core/validator/feature-validator.js.map +1 -0
  11. package/dist/core/validator/index.d.ts +46 -0
  12. package/dist/core/validator/index.d.ts.map +1 -0
  13. package/dist/core/validator/index.js +17 -0
  14. package/dist/core/validator/index.js.map +1 -0
  15. package/dist/core/validator/screen-validator.d.ts +35 -0
  16. package/dist/core/validator/screen-validator.d.ts.map +1 -0
  17. package/dist/core/validator/screen-validator.js +195 -0
  18. package/dist/core/validator/screen-validator.js.map +1 -0
  19. package/dist/core/validator/selector-validator.d.ts +35 -0
  20. package/dist/core/validator/selector-validator.d.ts.map +1 -0
  21. package/dist/core/validator/selector-validator.js +210 -0
  22. package/dist/core/validator/selector-validator.js.map +1 -0
  23. package/dist/generators/cli.js +1 -1
  24. package/dist/input/cli-adapter.d.ts +4 -0
  25. package/dist/input/cli-adapter.d.ts.map +1 -1
  26. package/dist/input/cli-adapter.js +12 -1
  27. package/dist/input/cli-adapter.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/cli/index.ts +85 -0
  30. package/src/core/validator/data-validator.ts +202 -0
  31. package/src/core/validator/feature-validator.ts +176 -0
  32. package/src/core/validator/index.ts +57 -0
  33. package/src/core/validator/screen-validator.ts +209 -0
  34. package/src/core/validator/selector-validator.ts +208 -0
  35. package/src/generators/cli.ts +1 -1
  36. 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
+ }
@@ -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.4');
55
+ .version('1.0.6');
56
56
 
57
57
  // ============================================================================
58
58
  // Command: discover
@@ -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.4');
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
  */