@sun-asterisk/sungen 1.0.14 → 1.0.15

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 (94) hide show
  1. package/dist/generators/cli.js +1 -1
  2. package/dist/generators/gherkin-parser/index.d.ts +1 -0
  3. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  4. package/dist/generators/gherkin-parser/index.js +8 -0
  5. package/dist/generators/gherkin-parser/index.js.map +1 -1
  6. package/dist/generators/scaffold-generator/index.d.ts +4 -1
  7. package/dist/generators/scaffold-generator/index.d.ts.map +1 -1
  8. package/dist/generators/scaffold-generator/index.js +39 -16
  9. package/dist/generators/scaffold-generator/index.js.map +1 -1
  10. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  11. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -0
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +1 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
  32. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  33. package/dist/generators/test-generator/patterns/assertion-patterns.js +119 -16
  34. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  35. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
  36. package/dist/generators/test-generator/patterns/form-patterns.js +37 -5
  37. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  38. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  39. package/dist/generators/test-generator/patterns/interaction-patterns.js +108 -9
  40. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  41. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
  42. package/dist/generators/test-generator/patterns/navigation-patterns.js +1 -1
  43. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  44. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  45. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  46. package/dist/generators/test-generator/step-mapper.js +15 -0
  47. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  48. package/dist/generators/test-generator/template-engine.d.ts +5 -0
  49. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  50. package/dist/generators/test-generator/template-engine.js +61 -1
  51. package/dist/generators/test-generator/template-engine.js.map +1 -1
  52. package/dist/generators/test-generator/utils/selector-resolver.d.ts +14 -1
  53. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  54. package/dist/generators/test-generator/utils/selector-resolver.js +28 -3
  55. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  56. package/dist/input/cli-adapter.js +1 -1
  57. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  58. package/dist/orchestrator/project-initializer.js +2 -1
  59. package/dist/orchestrator/project-initializer.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/generators/cli.ts +1 -1
  62. package/src/generators/gherkin-parser/index.ts +10 -0
  63. package/src/generators/scaffold-generator/index.ts +43 -20
  64. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  65. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
  66. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
  67. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -0
  68. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
  69. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
  70. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -0
  71. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
  72. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +1 -1
  73. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
  74. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  75. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
  76. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
  77. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
  78. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
  79. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
  80. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
  81. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
  82. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
  83. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
  84. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
  85. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
  86. package/src/generators/test-generator/patterns/assertion-patterns.ts +142 -16
  87. package/src/generators/test-generator/patterns/form-patterns.ts +39 -5
  88. package/src/generators/test-generator/patterns/interaction-patterns.ts +118 -11
  89. package/src/generators/test-generator/patterns/navigation-patterns.ts +2 -1
  90. package/src/generators/test-generator/step-mapper.ts +18 -2
  91. package/src/generators/test-generator/template-engine.ts +70 -1
  92. package/src/generators/test-generator/utils/selector-resolver.ts +30 -3
  93. package/src/input/cli-adapter.ts +1 -1
  94. package/src/orchestrator/project-initializer.ts +2 -1
@@ -1,6 +1,32 @@
1
1
  import { ParsedStep } from '../../gherkin-parser';
2
2
  import { StepPattern } from './types';
3
3
 
4
+ /**
5
+ * Extract Playwright waitFor state from step text.
6
+ * Defaults to 'visible' if no known state keyword is found.
7
+ */
8
+ function extractWaitState(text: string): string {
9
+ if (/\bhidden\b/.test(text)) return 'hidden';
10
+ if (/\bdetached\b/.test(text)) return 'detached';
11
+ if (/\bvisible\b/.test(text)) return 'visible';
12
+ return 'visible';
13
+ }
14
+
15
+ /**
16
+ * Map a plain element keyword (no brackets) to a Playwright ARIA role.
17
+ * e.g. "dialog" → "dialog", "modal" → "dialog", "alert" → "alertdialog"
18
+ */
19
+ function elementKeywordToRole(text: string): string | null {
20
+ const match = text.match(/\bwait(?:s)?\s+for\s+(dialog|modal|alert|alertdialog|tooltip|menu|listbox|combobox|grid|table|status|banner|navigation|main|region)\b/i);
21
+ if (!match) return null;
22
+ const keyword = match[1].toLowerCase();
23
+ const roleMap: Record<string, string> = {
24
+ modal: 'dialog',
25
+ alert: 'alertdialog',
26
+ };
27
+ return roleMap[keyword] || keyword;
28
+ }
29
+
4
30
  /**
5
31
  * Interaction patterns: click, hover, press, wait
6
32
  * Uses template engine for framework-agnostic code generation
@@ -27,7 +53,8 @@ export const interactionPatterns: StepPattern[] = [
27
53
  const resolved = context.selectorResolver.resolveSelector(
28
54
  step.selectorRef!,
29
55
  context.featureName,
30
- step.elementType
56
+ step.elementType,
57
+ step.nth
31
58
  );
32
59
  selectorValue = resolved.value || '';
33
60
  nth = resolved.nth || 0;
@@ -81,7 +108,7 @@ export const interactionPatterns: StepPattern[] = [
81
108
  && step.text.includes('with'),
82
109
  generator: (step, context) => {
83
110
  const resolved = context.selectorResolver.resolveSelector(
84
- step.selectorRef!, undefined, step.elementType
111
+ step.selectorRef!, undefined, step.elementType, step.nth
85
112
  );
86
113
 
87
114
  let dataValue: string;
@@ -115,7 +142,7 @@ export const interactionPatterns: StepPattern[] = [
115
142
  matcher: (step: ParsedStep) =>
116
143
  (step.text.includes('clicks') || step.text.includes('click')) && !!step.selectorRef,
117
144
  generator: (step, context) => {
118
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
145
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
119
146
 
120
147
  const code = context.templateEngine.renderStep('click-action', { ...resolved, selectorRef: step.selectorRef });
121
148
 
@@ -131,7 +158,7 @@ export const interactionPatterns: StepPattern[] = [
131
158
  matcher: (step: ParsedStep) =>
132
159
  step.text.includes('double') && step.text.includes('clicks') && !!step.selectorRef,
133
160
  generator: (step, context) => {
134
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
161
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
135
162
 
136
163
  const code = context.templateEngine.renderStep('double-click-action', { ...resolved, selectorRef: step.selectorRef });
137
164
 
@@ -147,7 +174,7 @@ export const interactionPatterns: StepPattern[] = [
147
174
  matcher: (step: ParsedStep) =>
148
175
  step.text.includes('hovers') && !!step.selectorRef,
149
176
  generator: (step, context) => {
150
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
177
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
151
178
 
152
179
  const code = context.templateEngine.renderStep('hover-action', { ...resolved, selectorRef: step.selectorRef });
153
180
 
@@ -163,7 +190,7 @@ export const interactionPatterns: StepPattern[] = [
163
190
  matcher: (step: ParsedStep) =>
164
191
  step.text.includes('presses') && step.text.includes('Enter') && !!step.selectorRef,
165
192
  generator: (step, context) => {
166
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
193
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
167
194
 
168
195
  const code = context.templateEngine.renderStep('press-action', {
169
196
  ...resolved,
@@ -226,6 +253,53 @@ export const interactionPatterns: StepPattern[] = [
226
253
  },
227
254
  priority: 7,
228
255
  },
256
+ // "wait for dialog with {{kudo.title}} hidden" - role keyword inline, no selector brackets, with data filter
257
+ {
258
+ name: 'wait-for-role-with-text',
259
+ matcher: (step: ParsedStep) =>
260
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
261
+ !step.selectorRef &&
262
+ !!step.dataRef &&
263
+ !!elementKeywordToRole(step.text),
264
+ generator: (step, context) => {
265
+ const role = elementKeywordToRole(step.text)!;
266
+
267
+ let dataValue: string;
268
+ try {
269
+ dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
270
+ } catch (error) {
271
+ dataValue = `\${${step.dataRef}}`;
272
+ }
273
+
274
+ const state = extractWaitState(step.text);
275
+
276
+ // Render inline — no locator partial needed, role is known directly
277
+ return {
278
+ code: `await page.getByRole('${role}').filter({ hasText: '${dataValue.replace(/'/g, "\\'")}' }).waitFor({ state: '${state}' });`,
279
+ comment: `Wait for ${role} with ${step.dataRef} to be ${state}`,
280
+ };
281
+ },
282
+ priority: 11,
283
+ },
284
+ // "wait for dialog hidden" - role keyword inline, no selector brackets, no data filter
285
+ {
286
+ name: 'wait-for-role',
287
+ matcher: (step: ParsedStep) =>
288
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
289
+ !step.selectorRef &&
290
+ !step.dataRef &&
291
+ !!elementKeywordToRole(step.text),
292
+ generator: (step, context) => {
293
+ const role = elementKeywordToRole(step.text)!;
294
+ const state = extractWaitState(step.text);
295
+
296
+ return {
297
+ code: `await page.getByRole('${role}').waitFor({ state: '${state}' });`,
298
+ comment: `Wait for ${role} to be ${state}`,
299
+ };
300
+ },
301
+ priority: 11,
302
+ },
229
303
  // NEW: Wait for page pattern
230
304
  {
231
305
  name: 'wait-for-page',
@@ -242,22 +316,55 @@ export const interactionPatterns: StepPattern[] = [
242
316
  },
243
317
  priority: 9, // Higher than wait-for-element
244
318
  },
319
+ {
320
+ name: 'wait-for-element-with-text',
321
+ matcher: (step: ParsedStep) =>
322
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
323
+ !!step.selectorRef &&
324
+ !!step.dataRef,
325
+ generator: (step, context) => {
326
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
327
+
328
+ let dataValue: string;
329
+ try {
330
+ dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
331
+ } catch (error) {
332
+ dataValue = `\${${step.dataRef}}`;
333
+ }
334
+
335
+ const state = extractWaitState(step.text);
336
+
337
+ const code = context.templateEngine.renderStep('wait-for-element-with-text', {
338
+ ...resolved,
339
+ selectorRef: step.selectorRef,
340
+ dataValue,
341
+ state,
342
+ });
343
+
344
+ return {
345
+ code,
346
+ comment: `Wait for ${step.selectorRef} with ${step.dataRef} to be ${state}`,
347
+ };
348
+ },
349
+ priority: 10,
350
+ },
245
351
  {
246
352
  name: 'wait-for-element',
247
353
  matcher: (step: ParsedStep) =>
248
354
  (step.text.includes('wait for') || step.text.includes('waits for')) && !!step.selectorRef,
249
355
  generator: (step, context) => {
250
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
251
-
356
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
357
+ const state = extractWaitState(step.text);
358
+
252
359
  const code = context.templateEngine.renderStep('wait-for-element', {
253
360
  ...resolved,
254
361
  selectorRef: step.selectorRef,
255
- state: 'visible',
362
+ state,
256
363
  });
257
-
364
+
258
365
  return {
259
366
  code,
260
- comment: `Wait for ${step.selectorRef}`,
367
+ comment: `Wait for ${step.selectorRef} to be ${state}`,
261
368
  };
262
369
  },
263
370
  priority: 8,
@@ -23,7 +23,8 @@ export const navigationPatterns: StepPattern[] = [
23
23
  const resolved = context.selectorResolver.resolveSelector(
24
24
  step.selectorRef,
25
25
  context.featureName,
26
- step.elementType
26
+ step.elementType,
27
+ step.nth
27
28
  );
28
29
  // Use selector's value as the path for navigation
29
30
  path = resolved.value || path;
@@ -28,8 +28,9 @@ export class StepMapper {
28
28
  private baseURL: string;
29
29
  private featureName?: string;
30
30
  private screenName?: string;
31
- private featurePath?: string; // Path metadata from feature file (e.g., "/login")
32
- private currentScenarioSteps?: ParsedStep[]; // All steps in current scenario for context
31
+ private featurePath?: string;
32
+ private currentScenarioSteps?: ParsedStep[];
33
+ private inDialogScope: boolean = false;
33
34
 
34
35
  constructor(options: { useAI?: boolean; verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string } = {}) {
35
36
  this.useAI = options.useAI ?? false;
@@ -74,6 +75,9 @@ export class StepMapper {
74
75
  */
75
76
  setScenarioContext(steps: ParsedStep[]): void {
76
77
  this.currentScenarioSteps = steps;
78
+ // Reset dialog scope at the start of each new scenario
79
+ this.inDialogScope = false;
80
+ this.templateEngine.resetBaseContext();
77
81
  }
78
82
 
79
83
  /**
@@ -83,6 +87,18 @@ export class StepMapper {
83
87
  mapStep(step: ParsedStep): MappedStep | Promise<MappedStep> {
84
88
  this.stepCounter++;
85
89
 
90
+ // Dialog scope directives — intercept before pattern matching
91
+ if (/\buse dialog\b/i.test(step.text)) {
92
+ this.inDialogScope = true;
93
+ this.templateEngine.setBaseContext({ inDialog: true });
94
+ return { code: '', comment: 'Enter dialog scope' };
95
+ }
96
+ if (/\b(?:close|dismiss|exit)\s+dialog\b/i.test(step.text)) {
97
+ this.inDialogScope = false;
98
+ this.templateEngine.setBaseContext({ inDialog: false });
99
+ return { code: '', comment: 'Exit dialog scope' };
100
+ }
101
+
86
102
  // Try pattern registry first
87
103
  const context = this.createPatternContext();
88
104
  const mappedStep = this.patternRegistry.generateStep(step, context);
@@ -6,6 +6,7 @@ export class TemplateEngine {
6
6
  private templates: Map<string, HandlebarsTemplateDelegate> = new Map();
7
7
  private templatesDir: string;
8
8
  private stepsTemplatesDir: string;
9
+ private baseContext: Record<string, any> = {};
9
10
 
10
11
  constructor(templatesDir: string) {
11
12
  this.templatesDir = templatesDir;
@@ -83,6 +84,37 @@ export class TemplateEngine {
83
84
  const selectorName = selectorRef.split('.')[0].trim();
84
85
  return selectorName.length < 6 && !selectorName.includes(' ');
85
86
  });
87
+
88
+ // Switch/case helpers for cleaner strategy routing
89
+ Handlebars.registerHelper('switch', function(value: any, options: any) {
90
+ this.switchValue = value;
91
+ this.switchDefaulted = false;
92
+ return options.fn(this);
93
+ });
94
+
95
+ Handlebars.registerHelper('case', function(value: any, options: any) {
96
+ // Skip if already matched a case
97
+ if (this.switchMatched) {
98
+ return;
99
+ }
100
+ // Check if this case matches
101
+ if (value === this.switchValue) {
102
+ this.switchMatched = true;
103
+ return options.fn(this);
104
+ }
105
+ });
106
+
107
+ Handlebars.registerHelper('default', function(options: any) {
108
+ // Execute if no case matched
109
+ if (!this.switchMatched) {
110
+ return options.fn(this);
111
+ }
112
+ });
113
+
114
+ // Returns the Playwright root object — scoped to dialog when inDialog is true
115
+ Handlebars.registerHelper('pageRoot', function(this: any) {
116
+ return this.inDialog ? "page.getByRole('dialog')" : 'page';
117
+ });
86
118
  }
87
119
 
88
120
  private registerPartials(): void {
@@ -92,6 +124,33 @@ export class TemplateEngine {
92
124
  const locatorContent = fs.readFileSync(locatorPath, 'utf-8');
93
125
  Handlebars.registerPartial('locator', locatorContent);
94
126
  }
127
+
128
+ // Register locator-base partial (same as locator but without nth — for filter chaining)
129
+ const locatorBasePath = path.join(this.stepsTemplatesDir, 'partials', 'locator-base.hbs');
130
+ if (fs.existsSync(locatorBasePath)) {
131
+ const locatorBaseContent = fs.readFileSync(locatorBasePath, 'utf-8');
132
+ Handlebars.registerPartial('locator-base', locatorBaseContent);
133
+ }
134
+
135
+ // Register locator strategy partials
136
+ const strategiesDir = path.join(this.stepsTemplatesDir, 'partials', 'locator-strategies');
137
+ if (fs.existsSync(strategiesDir)) {
138
+ const strategies = ['testid', 'role', 'placeholder', 'label', 'text', 'locator', 'id', 'default'];
139
+ strategies.forEach(strategy => {
140
+ const strategyPath = path.join(strategiesDir, `${strategy}.hbs`);
141
+ if (fs.existsSync(strategyPath)) {
142
+ const strategyContent = fs.readFileSync(strategyPath, 'utf-8');
143
+ Handlebars.registerPartial(`locator-strategies/${strategy}`, strategyContent);
144
+ }
145
+ });
146
+ }
147
+
148
+ // Register locator modifier partials
149
+ const locatorNthPath = path.join(this.stepsTemplatesDir, 'partials', 'locator-nth.hbs');
150
+ if (fs.existsSync(locatorNthPath)) {
151
+ const locatorNthContent = fs.readFileSync(locatorNthPath, 'utf-8');
152
+ Handlebars.registerPartial('locator-nth', locatorNthContent);
153
+ }
95
154
  }
96
155
 
97
156
  private loadTemplate(templateName: string, isStepTemplate: boolean = false): HandlebarsTemplateDelegate {
@@ -140,7 +199,17 @@ export class TemplateEngine {
140
199
  }
141
200
 
142
201
  renderStep(stepTemplateName: string, data: any): string {
143
- return this.render(stepTemplateName, data, true);
202
+ return this.render(stepTemplateName, { ...this.baseContext, ...data }, true);
203
+ }
204
+
205
+ /** Set base context merged into every renderStep call (e.g., { inDialog: true }) */
206
+ setBaseContext(ctx: Record<string, any>): void {
207
+ this.baseContext = { ...this.baseContext, ...ctx };
208
+ }
209
+
210
+ /** Reset base context (e.g., on new scenario) */
211
+ resetBaseContext(): void {
212
+ this.baseContext = {};
144
213
  }
145
214
 
146
215
  renderImports(): string {
@@ -270,13 +270,31 @@ export class SelectorResolver {
270
270
  .replace(/\s+/g, '.'); // Spaces to dots
271
271
  }
272
272
 
273
+ /**
274
+ * Extract the positional nth index from the text that follows an element reference.
275
+ * Shared by scaffold-generator (map) and gherkin-parser (generate) so both produce
276
+ * identical results from the same Gherkin step.
277
+ *
278
+ * Pass the text that comes AFTER the closing `]`, e.g.:
279
+ * "[Email] field 3 with …" → afterElement = " field 3 with …" → returns 3
280
+ * "[Submit] button 2" → afterElement = " button 2" → returns 2
281
+ * "[Name]" → afterElement = "" → returns 0
282
+ *
283
+ * NOTE: textbox/textarea must precede "text" to prevent partial matching.
284
+ */
285
+ static extractNthFromStep(afterElement: string): number {
286
+ const nthRegex = /^\s*(?:field|button|textbox|textarea|text|link|input|element|item|logo|image|img|icon|raw|table|columnheader|list|listitem|row|checkbox|radio|dropdown|select|uploader)?\s*(\d+)\b/i;
287
+ const match = nthRegex.exec(afterElement);
288
+ return match ? parseInt(match[1], 10) : 0;
289
+ }
290
+
273
291
  /**
274
292
  * Resolve selector reference to Playwright locator code
275
293
  * Supports:
276
294
  * - Natural language: "Email Address", "Password" (with feature context)
277
295
  * - Legacy format: "login:email_field" or "login.email_field" (with screen prefix)
278
296
  */
279
- resolveSelector(selectorRef: string, featureName?: string, elementType?: string): ResolvedSelector {
297
+ resolveSelector(selectorRef: string, featureName?: string, elementType?: string, nth?: number): ResolvedSelector {
280
298
  const contextName = featureName || this.featureName;
281
299
 
282
300
  // Check if it's legacy format (contains : or has dot with screen prefix)
@@ -303,7 +321,7 @@ export class SelectorResolver {
303
321
  );
304
322
  }
305
323
 
306
- return this.resolveNaturalLanguage(selectorRef, contextName, elementType);
324
+ return this.resolveNaturalLanguage(selectorRef, contextName, elementType, nth);
307
325
  }
308
326
 
309
327
  /**
@@ -373,10 +391,19 @@ export class SelectorResolver {
373
391
  /**
374
392
  * Resolve natural language selector
375
393
  */
376
- private resolveNaturalLanguage(label: string, featureName: string, elementType?: string): ResolvedSelector {
394
+ private resolveNaturalLanguage(label: string, featureName: string, elementType?: string, nth?: number): ResolvedSelector {
377
395
  const key = SelectorResolver.generateKey(label);
378
396
  const selectorFile = this.loadNewSelectorFile(featureName);
379
397
 
398
+ // Try nth-suffixed key first: hay.gui.loi.cam.on--3
399
+ if (nth && nth > 0) {
400
+ const nthKey = `${key}--${nth}`;
401
+ const nthEntry = selectorFile[nthKey];
402
+ if (nthEntry) {
403
+ return this.resolveFromEntry(nthEntry, label);
404
+ }
405
+ }
406
+
380
407
  // Try type-suffixed key first: add.campaign--button, add.campaign--text
381
408
  if (elementType) {
382
409
  const normalizedType = SelectorResolver.normalizeElementType(elementType);
@@ -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.14');
39
+ .version('1.0.15');
40
40
 
41
41
  // Global options
42
42
  program
@@ -45,7 +45,8 @@ export class ProjectInitializer {
45
45
  'qa',
46
46
  'qa/screens',
47
47
  'specs',
48
- 'specs/generated'
48
+ 'specs/generated',
49
+ 'specs/storage',
49
50
  ];
50
51
 
51
52
  for (const dir of dirs) {