@sun-asterisk/sungen 1.0.14 → 1.0.16

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 (125) hide show
  1. package/dist/generators/cli.js +1 -1
  2. package/dist/generators/gherkin-parser/index.d.ts +3 -0
  3. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  4. package/dist/generators/gherkin-parser/index.js +14 -1
  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 +46 -18
  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/radio-select-action.hbs +1 -1
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -0
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +1 -2
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +5 -0
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +1 -2
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +1 -2
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +5 -0
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +5 -1
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
  35. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
  36. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
  37. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
  38. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
  41. package/dist/generators/test-generator/code-generator.d.ts +3 -1
  42. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  43. package/dist/generators/test-generator/code-generator.js +66 -14
  44. package/dist/generators/test-generator/code-generator.js.map +1 -1
  45. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  46. package/dist/generators/test-generator/patterns/assertion-patterns.js +148 -73
  47. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  48. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
  49. package/dist/generators/test-generator/patterns/form-patterns.js +37 -5
  50. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  51. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  52. package/dist/generators/test-generator/patterns/interaction-patterns.js +116 -20
  53. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  54. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
  55. package/dist/generators/test-generator/patterns/navigation-patterns.js +1 -1
  56. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  57. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  58. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  59. package/dist/generators/test-generator/step-mapper.js +15 -0
  60. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  61. package/dist/generators/test-generator/template-engine.d.ts +5 -0
  62. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  63. package/dist/generators/test-generator/template-engine.js +67 -3
  64. package/dist/generators/test-generator/template-engine.js.map +1 -1
  65. package/dist/generators/test-generator/utils/selector-resolver.d.ts +14 -1
  66. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  67. package/dist/generators/test-generator/utils/selector-resolver.js +32 -5
  68. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  69. package/dist/input/cli-adapter.js +1 -1
  70. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  71. package/dist/orchestrator/project-initializer.js +2 -1
  72. package/dist/orchestrator/project-initializer.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/generators/cli.ts +1 -1
  75. package/src/generators/gherkin-parser/index.ts +18 -1
  76. package/src/generators/scaffold-generator/index.ts +50 -22
  77. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  78. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/radio-select-action.hbs +1 -1
  79. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -0
  80. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
  81. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +1 -2
  82. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
  83. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +5 -0
  84. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
  85. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +1 -2
  86. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +1 -2
  87. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
  88. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +5 -0
  89. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
  90. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
  91. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -1
  92. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
  93. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +5 -1
  94. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
  95. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
  96. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
  97. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
  98. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
  99. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
  100. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
  101. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
  102. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
  103. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
  104. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
  105. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
  106. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
  107. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
  108. package/src/generators/test-generator/code-generator.ts +70 -15
  109. package/src/generators/test-generator/patterns/assertion-patterns.ts +171 -78
  110. package/src/generators/test-generator/patterns/form-patterns.ts +39 -5
  111. package/src/generators/test-generator/patterns/interaction-patterns.ts +126 -24
  112. package/src/generators/test-generator/patterns/navigation-patterns.ts +2 -1
  113. package/src/generators/test-generator/step-mapper.ts +18 -2
  114. package/src/generators/test-generator/template-engine.ts +74 -3
  115. package/src/generators/test-generator/utils/selector-resolver.ts +34 -5
  116. package/src/input/cli-adapter.ts +1 -1
  117. package/src/orchestrator/project-initializer.ts +2 -1
  118. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/target-element-action.hbs +0 -1
  119. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +0 -1
  120. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +0 -1
  121. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/page-navigation.hbs +0 -1
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/target-element-action.hbs +0 -1
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +0 -1
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +0 -1
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/page-navigation.hbs +0 -1
@@ -1,16 +1,42 @@
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
7
33
  */
8
34
  export const interactionPatterns: StepPattern[] = [
9
- // NEW: Target element with variable pattern
35
+ // Unknown element with variable pattern (selector value is empty, identified by filter)
10
36
  // Matches: "When User click [target order] row with {{order_name}}"
11
- // Generates: await page.getByText(/value of target.order.*value of order_name/).click();
37
+ // Generates: await page.getByText('').filter({ hasText: /.*value of order_name$/ }).click();
12
38
  {
13
- name: 'target-element-action',
39
+ name: 'unknown-element-action',
14
40
  matcher: (step: ParsedStep) => {
15
41
  // Check if selectorRef starts with "target " (target element indicator)
16
42
  const isTargetElement = step.selectorRef && /^target\s/i.test(step.selectorRef);
@@ -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;
@@ -44,8 +71,6 @@ export const interactionPatterns: StepPattern[] = [
44
71
  dataValue = `\${${step.dataRef}}`;
45
72
  }
46
73
 
47
- const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
48
-
49
74
  // Determine the action method (click, hover, etc.)
50
75
  let actionMethod = 'click';
51
76
  if (step.text.includes('hover')) {
@@ -54,13 +79,9 @@ export const interactionPatterns: StepPattern[] = [
54
79
  actionMethod = 'dblclick';
55
80
  }
56
81
 
57
- // Build regex pattern: combine selector value + variable
58
- const regexPattern = selectorValue && selectorValue.trim()
59
- ? `${selectorValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}.*${escapedVariable}`
60
- : `.*${escapedVariable}`;
61
-
62
- const code = context.templateEngine.renderStep('target-element-action', {
63
- pattern: regexPattern,
82
+ const code = context.templateEngine.renderStep('unknown-element-action', {
83
+ selectorValue,
84
+ dataValue,
64
85
  action: actionMethod,
65
86
  nth,
66
87
  });
@@ -81,7 +102,7 @@ export const interactionPatterns: StepPattern[] = [
81
102
  && step.text.includes('with'),
82
103
  generator: (step, context) => {
83
104
  const resolved = context.selectorResolver.resolveSelector(
84
- step.selectorRef!, undefined, step.elementType
105
+ step.selectorRef!, undefined, step.elementType, step.nth
85
106
  );
86
107
 
87
108
  let dataValue: string;
@@ -115,7 +136,7 @@ export const interactionPatterns: StepPattern[] = [
115
136
  matcher: (step: ParsedStep) =>
116
137
  (step.text.includes('clicks') || step.text.includes('click')) && !!step.selectorRef,
117
138
  generator: (step, context) => {
118
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
139
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
119
140
 
120
141
  const code = context.templateEngine.renderStep('click-action', { ...resolved, selectorRef: step.selectorRef });
121
142
 
@@ -129,9 +150,10 @@ export const interactionPatterns: StepPattern[] = [
129
150
  {
130
151
  name: 'double-click',
131
152
  matcher: (step: ParsedStep) =>
132
- step.text.includes('double') && step.text.includes('clicks') && !!step.selectorRef,
153
+ (step.text.includes('double click') || step.text.includes('double-click') ||
154
+ (step.text.includes('double') && step.text.includes('clicks'))) && !!step.selectorRef,
133
155
  generator: (step, context) => {
134
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
156
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
135
157
 
136
158
  const code = context.templateEngine.renderStep('double-click-action', { ...resolved, selectorRef: step.selectorRef });
137
159
 
@@ -147,7 +169,7 @@ export const interactionPatterns: StepPattern[] = [
147
169
  matcher: (step: ParsedStep) =>
148
170
  step.text.includes('hovers') && !!step.selectorRef,
149
171
  generator: (step, context) => {
150
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
172
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
151
173
 
152
174
  const code = context.templateEngine.renderStep('hover-action', { ...resolved, selectorRef: step.selectorRef });
153
175
 
@@ -163,7 +185,7 @@ export const interactionPatterns: StepPattern[] = [
163
185
  matcher: (step: ParsedStep) =>
164
186
  step.text.includes('presses') && step.text.includes('Enter') && !!step.selectorRef,
165
187
  generator: (step, context) => {
166
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
188
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
167
189
 
168
190
  const code = context.templateEngine.renderStep('press-action', {
169
191
  ...resolved,
@@ -226,6 +248,53 @@ export const interactionPatterns: StepPattern[] = [
226
248
  },
227
249
  priority: 7,
228
250
  },
251
+ // "wait for dialog with {{kudo.title}} hidden" - role keyword inline, no selector brackets, with data filter
252
+ {
253
+ name: 'wait-for-role-with-text',
254
+ matcher: (step: ParsedStep) =>
255
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
256
+ !step.selectorRef &&
257
+ !!step.dataRef &&
258
+ !!elementKeywordToRole(step.text),
259
+ generator: (step, context) => {
260
+ const role = elementKeywordToRole(step.text)!;
261
+
262
+ let dataValue: string;
263
+ try {
264
+ dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
265
+ } catch (error) {
266
+ dataValue = `\${${step.dataRef}}`;
267
+ }
268
+
269
+ const state = extractWaitState(step.text);
270
+
271
+ // Render inline — no locator partial needed, role is known directly
272
+ return {
273
+ code: `await page.getByRole('${role}').filter({ hasText: '${dataValue.replace(/'/g, "\\'")}' }).waitFor({ state: '${state}' });`,
274
+ comment: `Wait for ${role} with ${step.dataRef} to be ${state}`,
275
+ };
276
+ },
277
+ priority: 11,
278
+ },
279
+ // "wait for dialog hidden" - role keyword inline, no selector brackets, no data filter
280
+ {
281
+ name: 'wait-for-role',
282
+ matcher: (step: ParsedStep) =>
283
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
284
+ !step.selectorRef &&
285
+ !step.dataRef &&
286
+ !!elementKeywordToRole(step.text),
287
+ generator: (step, context) => {
288
+ const role = elementKeywordToRole(step.text)!;
289
+ const state = extractWaitState(step.text);
290
+
291
+ return {
292
+ code: `await page.getByRole('${role}').waitFor({ state: '${state}' });`,
293
+ comment: `Wait for ${role} to be ${state}`,
294
+ };
295
+ },
296
+ priority: 11,
297
+ },
229
298
  // NEW: Wait for page pattern
230
299
  {
231
300
  name: 'wait-for-page',
@@ -242,22 +311,55 @@ export const interactionPatterns: StepPattern[] = [
242
311
  },
243
312
  priority: 9, // Higher than wait-for-element
244
313
  },
314
+ {
315
+ name: 'wait-for-element-with-text',
316
+ matcher: (step: ParsedStep) =>
317
+ (step.text.includes('wait for') || step.text.includes('waits for')) &&
318
+ !!step.selectorRef &&
319
+ !!step.dataRef,
320
+ generator: (step, context) => {
321
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
322
+
323
+ let dataValue: string;
324
+ try {
325
+ dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
326
+ } catch (error) {
327
+ dataValue = `\${${step.dataRef}}`;
328
+ }
329
+
330
+ const state = extractWaitState(step.text);
331
+
332
+ const code = context.templateEngine.renderStep('wait-for-element-with-text', {
333
+ ...resolved,
334
+ selectorRef: step.selectorRef,
335
+ dataValue,
336
+ state,
337
+ });
338
+
339
+ return {
340
+ code,
341
+ comment: `Wait for ${step.selectorRef} with ${step.dataRef} to be ${state}`,
342
+ };
343
+ },
344
+ priority: 10,
345
+ },
245
346
  {
246
347
  name: 'wait-for-element',
247
348
  matcher: (step: ParsedStep) =>
248
349
  (step.text.includes('wait for') || step.text.includes('waits for')) && !!step.selectorRef,
249
350
  generator: (step, context) => {
250
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType);
251
-
351
+ const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
352
+ const state = extractWaitState(step.text);
353
+
252
354
  const code = context.templateEngine.renderStep('wait-for-element', {
253
355
  ...resolved,
254
356
  selectorRef: step.selectorRef,
255
- state: 'visible',
357
+ state,
256
358
  });
257
-
359
+
258
360
  return {
259
361
  code,
260
- comment: `Wait for ${step.selectorRef}`,
362
+ comment: `Wait for ${step.selectorRef} to be ${state}`,
261
363
  };
262
364
  },
263
365
  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;
@@ -21,11 +22,13 @@ export class TemplateEngine {
21
22
  });
22
23
 
23
24
  Handlebars.registerHelper('escapeQuotes', function(text: string) {
24
- return text.replace(/'/g, "\\'");
25
+ if (text == null) return '';
26
+ return String(text).replace(/'/g, "\\'");
25
27
  });
26
28
 
27
29
  Handlebars.registerHelper('escapeRegex', function(text: string) {
28
- return text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
30
+ if (text == null) return '';
31
+ return String(text).replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
29
32
  });
30
33
 
31
34
  // Comparison helpers for conditional logic
@@ -83,6 +86,37 @@ export class TemplateEngine {
83
86
  const selectorName = selectorRef.split('.')[0].trim();
84
87
  return selectorName.length < 6 && !selectorName.includes(' ');
85
88
  });
89
+
90
+ // Switch/case helpers for cleaner strategy routing
91
+ Handlebars.registerHelper('switch', function(value: any, options: any) {
92
+ this.switchValue = value;
93
+ this.switchDefaulted = false;
94
+ return options.fn(this);
95
+ });
96
+
97
+ Handlebars.registerHelper('case', function(value: any, options: any) {
98
+ // Skip if already matched a case
99
+ if (this.switchMatched) {
100
+ return;
101
+ }
102
+ // Check if this case matches
103
+ if (value === this.switchValue) {
104
+ this.switchMatched = true;
105
+ return options.fn(this);
106
+ }
107
+ });
108
+
109
+ Handlebars.registerHelper('default', function(options: any) {
110
+ // Execute if no case matched
111
+ if (!this.switchMatched) {
112
+ return options.fn(this);
113
+ }
114
+ });
115
+
116
+ // Returns the Playwright root object — scoped to dialog when inDialog is true
117
+ Handlebars.registerHelper('pageRoot', function(this: any) {
118
+ return this.inDialog ? "page.getByRole('dialog')" : 'page';
119
+ });
86
120
  }
87
121
 
88
122
  private registerPartials(): void {
@@ -92,6 +126,33 @@ export class TemplateEngine {
92
126
  const locatorContent = fs.readFileSync(locatorPath, 'utf-8');
93
127
  Handlebars.registerPartial('locator', locatorContent);
94
128
  }
129
+
130
+ // Register locator-base partial (same as locator but without nth — for filter chaining)
131
+ const locatorBasePath = path.join(this.stepsTemplatesDir, 'partials', 'locator-base.hbs');
132
+ if (fs.existsSync(locatorBasePath)) {
133
+ const locatorBaseContent = fs.readFileSync(locatorBasePath, 'utf-8');
134
+ Handlebars.registerPartial('locator-base', locatorBaseContent);
135
+ }
136
+
137
+ // Register locator strategy partials
138
+ const strategiesDir = path.join(this.stepsTemplatesDir, 'partials', 'locator-strategies');
139
+ if (fs.existsSync(strategiesDir)) {
140
+ const strategies = ['testid', 'role', 'placeholder', 'label', 'text', 'locator', 'id', 'default'];
141
+ strategies.forEach(strategy => {
142
+ const strategyPath = path.join(strategiesDir, `${strategy}.hbs`);
143
+ if (fs.existsSync(strategyPath)) {
144
+ const strategyContent = fs.readFileSync(strategyPath, 'utf-8');
145
+ Handlebars.registerPartial(`locator-strategies/${strategy}`, strategyContent);
146
+ }
147
+ });
148
+ }
149
+
150
+ // Register locator modifier partials
151
+ const locatorNthPath = path.join(this.stepsTemplatesDir, 'partials', 'locator-nth.hbs');
152
+ if (fs.existsSync(locatorNthPath)) {
153
+ const locatorNthContent = fs.readFileSync(locatorNthPath, 'utf-8');
154
+ Handlebars.registerPartial('locator-nth', locatorNthContent);
155
+ }
95
156
  }
96
157
 
97
158
  private loadTemplate(templateName: string, isStepTemplate: boolean = false): HandlebarsTemplateDelegate {
@@ -140,7 +201,17 @@ export class TemplateEngine {
140
201
  }
141
202
 
142
203
  renderStep(stepTemplateName: string, data: any): string {
143
- return this.render(stepTemplateName, data, true);
204
+ return this.render(stepTemplateName, { ...this.baseContext, ...data }, true);
205
+ }
206
+
207
+ /** Set base context merged into every renderStep call (e.g., { inDialog: true }) */
208
+ setBaseContext(ctx: Record<string, any>): void {
209
+ this.baseContext = { ...this.baseContext, ...ctx };
210
+ }
211
+
212
+ /** Reset base context (e.g., on new scenario) */
213
+ resetBaseContext(): void {
214
+ this.baseContext = {};
144
215
  }
145
216
 
146
217
  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|heading|header)?\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
  /**
@@ -330,6 +348,7 @@ export class SelectorResolver {
330
348
  'row': () => ({ strategy: 'role', role: 'row', name: label, value: 'row', nth: 0 }),
331
349
  'table': () => ({ strategy: 'role', role: 'table', name: label, value: 'table', nth: 0 }),
332
350
  'columnheader': () => ({ strategy: 'role', role: 'columnheader', name: label, value: 'columnheader', nth: 0 }),
351
+ 'heading': () => ({ strategy: 'role', role: 'heading', name: label, value: 'heading', nth: 0 }),
333
352
  'list': () => ({ strategy: 'role', role: 'list', name: label, value: 'list', nth: 0 }),
334
353
  'listitem': () => ({ strategy: 'role', role: 'listitem', value: 'listitem', nth: 0 }),
335
354
  };
@@ -348,9 +367,10 @@ export class SelectorResolver {
348
367
  // Text-like aliases → "text"
349
368
  'title': 'text',
350
369
  'label': 'text',
351
- 'heading': 'text',
352
- 'header': 'text',
353
370
  'caption': 'text',
371
+ // Heading aliases → "heading" (role-based)
372
+ 'heading': 'heading',
373
+ 'header': 'heading',
354
374
  'message': 'text',
355
375
  // Table-related aliases
356
376
  'column': 'columnheader',
@@ -373,10 +393,19 @@ export class SelectorResolver {
373
393
  /**
374
394
  * Resolve natural language selector
375
395
  */
376
- private resolveNaturalLanguage(label: string, featureName: string, elementType?: string): ResolvedSelector {
396
+ private resolveNaturalLanguage(label: string, featureName: string, elementType?: string, nth?: number): ResolvedSelector {
377
397
  const key = SelectorResolver.generateKey(label);
378
398
  const selectorFile = this.loadNewSelectorFile(featureName);
379
399
 
400
+ // Try nth-suffixed key first: hay.gui.loi.cam.on--3
401
+ if (nth && nth > 0) {
402
+ const nthKey = `${key}--${nth}`;
403
+ const nthEntry = selectorFile[nthKey];
404
+ if (nthEntry) {
405
+ return this.resolveFromEntry(nthEntry, label);
406
+ }
407
+ }
408
+
380
409
  // Try type-suffixed key first: add.campaign--button, add.campaign--text
381
410
  if (elementType) {
382
411
  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.16');
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) {
@@ -1 +0,0 @@
1
- await page.getByText(/{{pattern}}/i{{#if (shouldUseExact selectorRef)}}, { exact: true }{{/if}}){{#if (gt nth 0)}}.nth({{subtract nth 1}}){{/if}}.{{action}}();
@@ -1 +0,0 @@
1
- await expect({{> locator}}).not.toBeVisible();
@@ -1 +0,0 @@
1
- await expect({{> locator}}.filter({ hasText: '{{escapeQuotes dataValue}}' })).toBeVisible();
@@ -1 +0,0 @@
1
- await page.getByText(/{{pattern}}/i{{#if (shouldUseExact selectorRef)}}, { exact: true }{{/if}}){{#if (gt nth 0)}}.nth({{subtract nth 1}}){{/if}}.{{action}}();
@@ -1 +0,0 @@
1
- await expect({{> locator}}).not.toBeVisible();
@@ -1 +0,0 @@
1
- await expect({{> locator}}.filter({ hasText: '{{escapeQuotes dataValue}}' })).toBeVisible();