@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
package/src/cli/index.ts CHANGED
@@ -8,6 +8,56 @@ import { CLIAdapter } from '../input/cli-adapter';
8
8
  import { ConfigLoader } from '../config/config-loader';
9
9
  import { Pipeline } from '../orchestrator/pipeline';
10
10
  import { CacheManager } from '../orchestrator/cache-manager';
11
+ import { ValidationResult } from '../core/validator';
12
+
13
+ /**
14
+ * Print validation result to console
15
+ */
16
+ function printValidationResult(result: ValidationResult, verbose: boolean = false): void {
17
+ for (const fileResult of result.results) {
18
+ if (fileResult.valid) {
19
+ if (verbose) {
20
+ console.log(`✓ ${fileResult.file}`);
21
+ console.log(` ✓ Gherkin syntax valid`);
22
+ if (fileResult.warnings.length === 0) {
23
+ console.log(` ✓ All checks passed`);
24
+ }
25
+ } else {
26
+ console.log(`✓ ${fileResult.file}`);
27
+ }
28
+ } else {
29
+ console.log(`✗ ${fileResult.file}`);
30
+ }
31
+
32
+ // Print warnings
33
+ for (const warning of fileResult.warnings) {
34
+ const lineInfo = warning.line ? `:${warning.line}` : '';
35
+ console.log(` ⊘ ${warning.message}${lineInfo}`);
36
+ }
37
+
38
+ // Print errors
39
+ for (const error of fileResult.errors) {
40
+ const lineInfo = error.line ? `:${error.line}` : '';
41
+ console.log(` ✗ ${error.message}${lineInfo}`);
42
+ }
43
+
44
+ if (!fileResult.valid || (verbose && fileResult.warnings.length > 0)) {
45
+ console.log('');
46
+ }
47
+ }
48
+
49
+ // Print summary
50
+ console.log('\nSummary:');
51
+ console.log(` Files: ${result.summary.totalFiles} total, ${result.summary.passedFiles} passed, ${result.summary.failedFiles} failed`);
52
+ console.log(` Errors: ${result.summary.totalErrors}`);
53
+ console.log(` Warnings: ${result.summary.totalWarnings}`);
54
+
55
+ if (result.valid) {
56
+ console.log('\n✅ Validation passed');
57
+ } else {
58
+ console.log('\n❌ Validation failed');
59
+ }
60
+ }
11
61
 
12
62
  async function main() {
13
63
  const adapter = new CLIAdapter();
@@ -195,6 +245,41 @@ async function main() {
195
245
  }
196
246
  });
197
247
 
248
+ const validateCmd = adapter.addValidateCommand(program);
249
+ validateCmd.action(async (options) => {
250
+ try {
251
+ const { ScreenValidator } = require('../core/validator');
252
+
253
+ const validator = new ScreenValidator({
254
+ verbose: options.verbose,
255
+ });
256
+
257
+ let result: ValidationResult;
258
+ if (options.screen) {
259
+ console.log(`Validating screen: ${options.screen}\n`);
260
+ result = validator.validateScreen(options.screen);
261
+ } else {
262
+ console.log('Validating all screens...\n');
263
+ result = validator.validateAll();
264
+ }
265
+
266
+ // Output results
267
+ if (options.json) {
268
+ console.log(JSON.stringify(result, null, 2));
269
+ } else {
270
+ printValidationResult(result, options.verbose);
271
+ }
272
+
273
+ // Exit with error code if validation failed
274
+ if (!result.valid) {
275
+ process.exit(1);
276
+ }
277
+ } catch (error) {
278
+ console.error('❌ Validation failed:', error);
279
+ process.exit(1);
280
+ }
281
+ });
282
+
198
283
  // Parse arguments
199
284
  await program.parseAsync(process.argv);
200
285
  }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Data Validator
3
+ * Validates test-data YAML files and checks referenced data paths 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
+ type DataFile = Record<string, unknown>;
12
+
13
+ export class DataValidator {
14
+ private screenName: string;
15
+ private baseDir: string;
16
+ private dataCache = new Map<string, DataFile>();
17
+
18
+ constructor(screenName: string, baseDir?: string) {
19
+ this.screenName = screenName;
20
+ this.baseDir = baseDir || process.cwd();
21
+ }
22
+
23
+ /**
24
+ * Validate test-data file for a feature
25
+ */
26
+ validateFile(featureName: string): { errors: ValidationIssue[]; warnings: ValidationIssue[] } {
27
+ const errors: ValidationIssue[] = [];
28
+ const warnings: ValidationIssue[] = [];
29
+
30
+ const filePath = this.findDataPath(featureName);
31
+
32
+ if (!filePath) {
33
+ // Data file is optional - only warn if there are data refs
34
+ return { errors, warnings };
35
+ }
36
+
37
+ // Load and parse YAML
38
+ let content: string;
39
+ let data: DataFile;
40
+
41
+ try {
42
+ content = fs.readFileSync(filePath, 'utf-8');
43
+ } catch (error) {
44
+ errors.push({
45
+ type: 'error',
46
+ code: 'INVALID_YAML_SYNTAX',
47
+ message: `Cannot read file: ${(error as Error).message}`,
48
+ file: filePath,
49
+ });
50
+ return { errors, warnings };
51
+ }
52
+
53
+ try {
54
+ data = yaml.parse(content) as DataFile;
55
+ } catch (error) {
56
+ errors.push({
57
+ type: 'error',
58
+ code: 'INVALID_YAML_SYNTAX',
59
+ message: `YAML parse error: ${(error as Error).message}`,
60
+ file: filePath,
61
+ });
62
+ return { errors, warnings };
63
+ }
64
+
65
+ if (data && typeof data !== 'object') {
66
+ errors.push({
67
+ type: 'error',
68
+ code: 'INVALID_YAML_SYNTAX',
69
+ message: 'Test data file must be a valid YAML object',
70
+ file: filePath,
71
+ });
72
+ return { errors, warnings };
73
+ }
74
+
75
+ // Cache for later ref validation
76
+ this.dataCache.set(featureName, data || {});
77
+
78
+ return { errors, warnings };
79
+ }
80
+
81
+ /**
82
+ * Check if data references exist and resolve to primitives
83
+ */
84
+ validateRefs(
85
+ featureName: string,
86
+ refs: Array<{ ref: string; line: number }>,
87
+ featureFile: string
88
+ ): ValidationIssue[] {
89
+ const errors: ValidationIssue[] = [];
90
+
91
+ if (refs.length === 0) {
92
+ return errors;
93
+ }
94
+
95
+ // Load data if not cached
96
+ if (!this.dataCache.has(featureName)) {
97
+ const filePath = this.findDataPath(featureName);
98
+ if (!filePath) {
99
+ // Data file doesn't exist but we have refs - error
100
+ for (const { ref, line } of refs) {
101
+ errors.push({
102
+ type: 'error',
103
+ code: 'MISSING_DATA_FILE',
104
+ message: `Test data file not found for feature "${featureName}" (needed for {{${ref}}})`,
105
+ file: featureFile,
106
+ line,
107
+ element: ref,
108
+ });
109
+ }
110
+ return errors;
111
+ }
112
+
113
+ try {
114
+ const content = fs.readFileSync(filePath, 'utf-8');
115
+ const data = yaml.parse(content) as DataFile;
116
+ this.dataCache.set(featureName, data || {});
117
+ } catch {
118
+ // Error already reported in validateFile
119
+ return errors;
120
+ }
121
+ }
122
+
123
+ const data = this.dataCache.get(featureName);
124
+ if (!data) {
125
+ return errors;
126
+ }
127
+
128
+ // Check each ref
129
+ for (const { ref, line } of refs) {
130
+ const result = this.resolvePath(data, ref);
131
+
132
+ if (!result.found) {
133
+ errors.push({
134
+ type: 'error',
135
+ code: 'MISSING_DATA_REF',
136
+ message: `Data {{${ref}}} not found in test-data/${featureName}.yaml (failed at: ${result.failedAt})`,
137
+ file: featureFile,
138
+ line,
139
+ element: ref,
140
+ });
141
+ } else if (!this.isPrimitive(result.value)) {
142
+ errors.push({
143
+ type: 'error',
144
+ code: 'INVALID_DATA_VALUE',
145
+ message: `Data {{${ref}}} must resolve to a primitive value, got: ${typeof result.value}`,
146
+ file: featureFile,
147
+ line,
148
+ element: ref,
149
+ });
150
+ }
151
+ }
152
+
153
+ return errors;
154
+ }
155
+
156
+ /**
157
+ * Resolve dotted path in data object
158
+ */
159
+ private resolvePath(data: DataFile, ref: string): { found: boolean; value?: unknown; failedAt?: string } {
160
+ const parts = ref.split('.');
161
+ let current: unknown = data;
162
+
163
+ for (const key of parts) {
164
+ if (current && typeof current === 'object' && key in (current as Record<string, unknown>)) {
165
+ current = (current as Record<string, unknown>)[key];
166
+ } else {
167
+ return { found: false, failedAt: key };
168
+ }
169
+ }
170
+
171
+ return { found: true, value: current };
172
+ }
173
+
174
+ /**
175
+ * Check if value is a primitive (string, number, boolean)
176
+ */
177
+ private isPrimitive(value: unknown): boolean {
178
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
179
+ }
180
+
181
+ /**
182
+ * Find test-data file path
183
+ */
184
+ private findDataPath(featureName: string): string | null {
185
+ const possiblePaths = [
186
+ // New structure
187
+ path.join(this.baseDir, 'qa', 'screens', this.screenName, 'test-data', `${featureName}.yaml`),
188
+ path.join(this.baseDir, 'qa', 'screens', this.screenName, 'test-data', `${featureName}.yml`),
189
+ // Legacy structure
190
+ path.join(this.baseDir, 'qa', 'test-data', `${featureName}.yaml`),
191
+ path.join(this.baseDir, 'qa', 'test-data', `${featureName}.yml`),
192
+ ];
193
+
194
+ for (const p of possiblePaths) {
195
+ if (fs.existsSync(p)) {
196
+ return p;
197
+ }
198
+ }
199
+
200
+ return null;
201
+ }
202
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Feature Validator
3
+ * Validates Gherkin feature files and extracts selector/data references
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as Gherkin from '@cucumber/gherkin';
8
+ import * as Messages from '@cucumber/messages';
9
+ import { ValidationIssue, ExtractedRefs } from './index';
10
+
11
+ export interface FeatureValidationResult {
12
+ valid: boolean;
13
+ errors: ValidationIssue[];
14
+ warnings: ValidationIssue[];
15
+ path?: string; // Extracted Path metadata
16
+ refs: ExtractedRefs;
17
+ }
18
+
19
+ export class FeatureValidator {
20
+ private builder: Gherkin.AstBuilder;
21
+ private matcher: Gherkin.GherkinClassicTokenMatcher;
22
+ // @ts-ignore - Parser typing issue with @cucumber/gherkin
23
+ private parser: Gherkin.Parser;
24
+
25
+ constructor() {
26
+ const uuidFn = Messages.IdGenerator.uuid();
27
+ this.builder = new Gherkin.AstBuilder(uuidFn);
28
+ this.matcher = new Gherkin.GherkinClassicTokenMatcher();
29
+ // @ts-ignore - Parser typing issue with @cucumber/gherkin
30
+ this.parser = new Gherkin.Parser(this.builder, this.matcher);
31
+ }
32
+
33
+ /**
34
+ * Validate a feature file
35
+ */
36
+ validate(filePath: string): FeatureValidationResult {
37
+ const errors: ValidationIssue[] = [];
38
+ const warnings: ValidationIssue[] = [];
39
+ const refs: ExtractedRefs = { selectors: [], dataRefs: [] };
40
+ let path: string | undefined;
41
+
42
+ // Check file exists
43
+ if (!fs.existsSync(filePath)) {
44
+ errors.push({
45
+ type: 'error',
46
+ code: 'GHERKIN_SYNTAX_ERROR',
47
+ message: `Feature file not found: ${filePath}`,
48
+ file: filePath,
49
+ });
50
+ return { valid: false, errors, warnings, refs };
51
+ }
52
+
53
+ // Read and parse content
54
+ let content: string;
55
+ try {
56
+ content = fs.readFileSync(filePath, 'utf-8');
57
+ } catch (error) {
58
+ errors.push({
59
+ type: 'error',
60
+ code: 'GHERKIN_SYNTAX_ERROR',
61
+ message: `Cannot read file: ${(error as Error).message}`,
62
+ file: filePath,
63
+ });
64
+ return { valid: false, errors, warnings, refs };
65
+ }
66
+
67
+ // Parse Gherkin
68
+ let gherkinDocument: Messages.GherkinDocument;
69
+ try {
70
+ gherkinDocument = this.parser.parse(content);
71
+ } catch (error) {
72
+ const errMsg = (error as Error).message;
73
+ // Try to extract line number from error message
74
+ const lineMatch = errMsg.match(/\((\d+):\d+\)/);
75
+ const line = lineMatch ? parseInt(lineMatch[1], 10) : undefined;
76
+
77
+ errors.push({
78
+ type: 'error',
79
+ code: 'GHERKIN_SYNTAX_ERROR',
80
+ message: `Gherkin parse error: ${errMsg}`,
81
+ file: filePath,
82
+ line,
83
+ });
84
+ return { valid: false, errors, warnings, refs };
85
+ }
86
+
87
+ // Check feature exists
88
+ if (!gherkinDocument.feature) {
89
+ errors.push({
90
+ type: 'error',
91
+ code: 'EMPTY_FEATURE',
92
+ message: 'No feature found in file',
93
+ file: filePath,
94
+ });
95
+ return { valid: false, errors, warnings, refs };
96
+ }
97
+
98
+ const feature = gherkinDocument.feature;
99
+
100
+ // Extract Path metadata
101
+ const description = feature.description || '';
102
+ const pathMatch = description.match(/^\s*Path:\s*(.+)$/m);
103
+ if (pathMatch) {
104
+ path = pathMatch[1].trim();
105
+ } else {
106
+ warnings.push({
107
+ type: 'warning',
108
+ code: 'MISSING_PATH_METADATA',
109
+ message: 'Missing Path: metadata in feature description',
110
+ file: filePath,
111
+ });
112
+ }
113
+
114
+ // Extract refs from background
115
+ const background = feature.children.find(child => child.background)?.background;
116
+ if (background) {
117
+ this.extractRefsFromSteps(background.steps, filePath, refs);
118
+ }
119
+
120
+ // Extract refs from scenarios
121
+ for (const child of feature.children) {
122
+ if (child.scenario) {
123
+ this.extractRefsFromSteps(child.scenario.steps, filePath, refs);
124
+ }
125
+ }
126
+
127
+ return {
128
+ valid: errors.length === 0,
129
+ errors,
130
+ warnings,
131
+ path,
132
+ refs,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Extract selector and data references from steps
138
+ */
139
+ private extractRefsFromSteps(
140
+ steps: readonly Messages.Step[],
141
+ filePath: string,
142
+ refs: ExtractedRefs
143
+ ): void {
144
+ for (const step of steps) {
145
+ const text = step.text;
146
+ const line = step.location?.line;
147
+
148
+ // Extract selector references: [Element Name]
149
+ const selectorMatches = text.matchAll(/\[([^\]]+)\]/g);
150
+ for (const match of selectorMatches) {
151
+ refs.selectors.push({
152
+ ref: match[1],
153
+ line: line || 0,
154
+ });
155
+ }
156
+
157
+ // Extract data references: {{variable}} or {{path.to.value}}
158
+ const dataMatches = text.matchAll(/\{\{([a-zA-Z0-9\-\.\_]+)\}\}/g);
159
+ for (const match of dataMatches) {
160
+ refs.dataRefs.push({
161
+ ref: match[1],
162
+ line: line || 0,
163
+ });
164
+ }
165
+
166
+ // Legacy format: <variable>
167
+ const legacyDataMatches = text.matchAll(/<([^>]+)>/g);
168
+ for (const match of legacyDataMatches) {
169
+ refs.dataRefs.push({
170
+ ref: match[1],
171
+ line: line || 0,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Validator Core - Types and Exports
3
+ * Validates Gherkin features and selector/test-data mappings
4
+ */
5
+
6
+ export interface ValidationIssue {
7
+ type: 'error' | 'warning';
8
+ code: ValidationCode;
9
+ message: string;
10
+ file: string;
11
+ line?: number;
12
+ element?: string; // The [element] or {{variable}} that caused the issue
13
+ }
14
+
15
+ export interface ValidationSummary {
16
+ totalFiles: number;
17
+ passedFiles: number;
18
+ failedFiles: number;
19
+ totalErrors: number;
20
+ totalWarnings: number;
21
+ }
22
+
23
+ export interface FileValidationResult {
24
+ file: string;
25
+ valid: boolean;
26
+ errors: ValidationIssue[];
27
+ warnings: ValidationIssue[];
28
+ }
29
+
30
+ export interface ValidationResult {
31
+ valid: boolean;
32
+ summary: ValidationSummary;
33
+ results: FileValidationResult[];
34
+ }
35
+
36
+ export type ValidationCode =
37
+ | 'GHERKIN_SYNTAX_ERROR'
38
+ | 'MISSING_PATH_METADATA'
39
+ | 'MISSING_SELECTOR_FILE'
40
+ | 'MISSING_SELECTOR'
41
+ | 'MISSING_DATA_FILE'
42
+ | 'MISSING_DATA_REF'
43
+ | 'INVALID_YAML_SYNTAX'
44
+ | 'INVALID_SELECTOR_TYPE'
45
+ | 'INVALID_DATA_VALUE'
46
+ | 'EMPTY_FEATURE';
47
+
48
+ export interface ExtractedRefs {
49
+ selectors: Array<{ ref: string; line: number }>;
50
+ dataRefs: Array<{ ref: string; line: number }>;
51
+ }
52
+
53
+ // Re-exports
54
+ export { FeatureValidator } from './feature-validator';
55
+ export { SelectorValidator } from './selector-validator';
56
+ export { DataValidator } from './data-validator';
57
+ export { ScreenValidator } from './screen-validator';