@sun-asterisk/sungen 3.1.1 → 3.1.2-beta.100

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 (162) hide show
  1. package/README.md +4 -428
  2. package/dist/capabilities/builtins.d.ts +31 -0
  3. package/dist/capabilities/builtins.d.ts.map +1 -0
  4. package/dist/capabilities/builtins.js +84 -0
  5. package/dist/capabilities/builtins.js.map +1 -0
  6. package/dist/capabilities/context-router.d.ts +34 -0
  7. package/dist/capabilities/context-router.d.ts.map +1 -0
  8. package/dist/capabilities/context-router.js +49 -0
  9. package/dist/capabilities/context-router.js.map +1 -0
  10. package/dist/capabilities/context.d.ts +51 -0
  11. package/dist/capabilities/context.d.ts.map +1 -0
  12. package/dist/capabilities/context.js +17 -0
  13. package/dist/capabilities/context.js.map +1 -0
  14. package/dist/capabilities/discover.d.ts +2 -0
  15. package/dist/capabilities/discover.d.ts.map +1 -0
  16. package/dist/capabilities/discover.js +48 -0
  17. package/dist/capabilities/discover.js.map +1 -0
  18. package/dist/capabilities/registry.d.ts +90 -0
  19. package/dist/capabilities/registry.d.ts.map +1 -0
  20. package/dist/capabilities/registry.js +43 -0
  21. package/dist/capabilities/registry.js.map +1 -0
  22. package/dist/capabilities/sensor.d.ts +49 -0
  23. package/dist/capabilities/sensor.d.ts.map +1 -0
  24. package/dist/capabilities/sensor.js +3 -0
  25. package/dist/capabilities/sensor.js.map +1 -0
  26. package/dist/cli/commands/generate.d.ts.map +1 -1
  27. package/dist/cli/commands/generate.js +7 -3
  28. package/dist/cli/commands/generate.js.map +1 -1
  29. package/dist/cli/index.js +10 -1
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/exporters/spec-parser.d.ts.map +1 -1
  32. package/dist/exporters/spec-parser.js +4 -1
  33. package/dist/exporters/spec-parser.js.map +1 -1
  34. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  35. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  36. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  37. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  38. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  39. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  40. package/dist/generators/test-generator/code-generator.d.ts +18 -9
  41. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  42. package/dist/generators/test-generator/code-generator.js +76 -119
  43. package/dist/generators/test-generator/code-generator.js.map +1 -1
  44. package/dist/generators/test-generator/patterns/index.d.ts +0 -10
  45. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  46. package/dist/generators/test-generator/patterns/index.js +10 -47
  47. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  48. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  49. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  50. package/dist/generators/test-generator/template-engine.js +1 -1
  51. package/dist/generators/test-generator/template-engine.js.map +1 -1
  52. package/dist/harness/annotation-overrides.d.ts +9 -0
  53. package/dist/harness/annotation-overrides.d.ts.map +1 -0
  54. package/dist/harness/annotation-overrides.js +36 -0
  55. package/dist/harness/annotation-overrides.js.map +1 -0
  56. package/dist/harness/audit.d.ts.map +1 -1
  57. package/dist/harness/audit.js +35 -7
  58. package/dist/harness/audit.js.map +1 -1
  59. package/dist/harness/catalog/drivers.yaml +35 -12
  60. package/dist/harness/parse.d.ts +1 -0
  61. package/dist/harness/parse.d.ts.map +1 -1
  62. package/dist/harness/parse.js +13 -4
  63. package/dist/harness/parse.js.map +1 -1
  64. package/dist/index.d.ts +20 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +32 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  69. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  70. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
  71. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  72. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  73. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
  74. package/dist/orchestrator/templates/specs-api.d.ts +19 -0
  75. package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
  76. package/dist/orchestrator/templates/specs-api.js +128 -0
  77. package/dist/orchestrator/templates/specs-api.js.map +1 -0
  78. package/dist/orchestrator/templates/specs-api.ts +101 -0
  79. package/package.json +7 -30
  80. package/src/capabilities/builtins.ts +85 -0
  81. package/src/capabilities/context-router.ts +66 -0
  82. package/src/capabilities/context.ts +46 -0
  83. package/src/capabilities/discover.ts +42 -0
  84. package/src/capabilities/registry.ts +111 -0
  85. package/src/capabilities/sensor.ts +47 -0
  86. package/src/cli/commands/generate.ts +7 -3
  87. package/src/cli/index.ts +10 -1
  88. package/src/exporters/spec-parser.ts +4 -1
  89. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  90. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  91. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  92. package/src/generators/test-generator/code-generator.ts +71 -118
  93. package/src/generators/test-generator/patterns/index.ts +9 -35
  94. package/src/generators/test-generator/template-engine.ts +2 -2
  95. package/src/harness/annotation-overrides.ts +25 -0
  96. package/src/harness/audit.ts +37 -8
  97. package/src/harness/catalog/drivers.yaml +35 -12
  98. package/src/harness/parse.ts +7 -2
  99. package/src/index.ts +30 -0
  100. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
  101. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +1 -0
  102. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +3 -0
  103. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
  104. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +1 -0
  105. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +3 -0
  106. package/src/orchestrator/templates/specs-api.ts +101 -0
  107. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
  108. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
  109. package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
  110. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
  111. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
  112. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
  113. package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
  114. package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
  115. package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -6
  116. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
  117. package/dist/generators/test-generator/patterns/database-patterns.js +0 -95
  118. package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
  119. package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
  120. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
  121. package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
  122. package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
  123. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
  124. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
  125. package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
  126. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
  127. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
  128. package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
  129. package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
  130. package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
  131. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
  132. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
  133. package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
  134. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
  135. package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
  136. package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
  137. package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
  138. package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
  139. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
  140. package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
  141. package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
  142. package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
  143. package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
  144. package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
  145. package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
  146. package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
  147. package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
  148. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
  149. package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
  150. package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
  151. package/docs/orchestration-spec.md +0 -267
  152. package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
  153. package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
  154. package/src/generators/test-generator/patterns/database-patterns.ts +0 -96
  155. package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
  156. package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
  157. package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
  158. package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
  159. package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
  160. package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
  161. package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
  162. package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
@@ -1,465 +0,0 @@
1
- import { ParsedStep } from '../../gherkin-parser';
2
- import { StepPattern } from './types';
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
- */
18
- function elementKeywordToRole(text: string): string | null {
19
- 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);
20
- if (!match) return null;
21
- const keyword = match[1].toLowerCase();
22
- const roleMap: Record<string, string> = {
23
- modal: 'dialog',
24
- alert: 'alertdialog',
25
- };
26
- return roleMap[keyword] || keyword;
27
- }
28
-
29
- /**
30
- * Interaction patterns: click, hover, press, wait, alert
31
- */
32
- export const interactionPatterns: StepPattern[] = [
33
- // === Browser Alert (system dialog) patterns ===
34
- // These must appear BEFORE the click that triggers the alert in Gherkin:
35
- // When User click [OK] alert ← registers page.once('dialog', ...)
36
- // And User click [Delete] button ← triggers the alert
37
- {
38
- name: 'alert-accept',
39
- matcher: (step: ParsedStep) =>
40
- step.elementType === 'alert' &&
41
- (step.text.includes('click') || step.text.includes('clicks')) &&
42
- !step.dataRef &&
43
- !!(step.selectorRef && /^(ok|accept|yes|confirm)$/i.test(step.selectorRef)),
44
- resolver: (step, context) => {
45
- return {
46
- templateName: 'alert-accept-action',
47
- data: {},
48
- comment: `Accept browser alert`,
49
- };
50
- },
51
- priority: 20,
52
- },
53
- {
54
- name: 'alert-dismiss',
55
- matcher: (step: ParsedStep) =>
56
- step.elementType === 'alert' &&
57
- (step.text.includes('click') || step.text.includes('clicks')) &&
58
- !step.dataRef &&
59
- !!(step.selectorRef && /^(cancel|dismiss|no)$/i.test(step.selectorRef)),
60
- resolver: (step, context) => {
61
- return {
62
- templateName: 'alert-dismiss-action',
63
- data: {},
64
- comment: `Dismiss browser alert`,
65
- };
66
- },
67
- priority: 20,
68
- },
69
- {
70
- name: 'alert-fill',
71
- matcher: (step: ParsedStep) =>
72
- step.elementType === 'alert' &&
73
- step.text.includes('fill') &&
74
- !!step.dataRef,
75
- resolver: (step, context) => {
76
- let fillValue: string;
77
- try {
78
- fillValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
79
- } catch {
80
- fillValue = `\${${step.dataRef}}`;
81
- }
82
- return {
83
- templateName: 'alert-fill-action',
84
- data: { fillValue },
85
- comment: `Fill browser prompt with ${step.dataRef}`,
86
- };
87
- },
88
- priority: 20,
89
- },
90
- {
91
- name: 'unknown-element-action',
92
- matcher: (step: ParsedStep) => {
93
- const isTargetElement = step.selectorRef && /^target\s/i.test(step.selectorRef);
94
- const isInteractionAction = !step.text.includes('see') && !step.text.includes('sees');
95
- return isTargetElement && !!step.dataRef && step.text.includes('with') && isInteractionAction;
96
- },
97
- resolver: (step, context) => {
98
- let selectorValue = '';
99
- let nth: number | undefined;
100
- try {
101
- const resolved = context.selectorResolver.resolveSelector(
102
- step.selectorRef!, context.featureName, step.elementType, step.nth
103
- );
104
- selectorValue = resolved.value || '';
105
- nth = resolved.nth;
106
- } catch (error) {
107
- selectorValue = '';
108
- nth = undefined;
109
- }
110
-
111
- let dataValue: string;
112
- try {
113
- dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
114
- } catch (error) {
115
- dataValue = `\${${step.dataRef}}`;
116
- }
117
-
118
- let actionMethod = 'click';
119
- if (step.text.includes('hover')) actionMethod = 'hover';
120
- else if (step.text.includes('double')) actionMethod = 'dblclick';
121
-
122
- return {
123
- templateName: 'unknown-element-action',
124
- data: { selectorValue, dataValue, action: actionMethod, nth },
125
- comment: `${actionMethod.charAt(0).toUpperCase() + actionMethod.slice(1)} ${step.selectorRef} with ${step.dataRef}`,
126
- };
127
- },
128
- priority: 14, // Below click-element-with-text (15) so known selectors use proper locator
129
- },
130
- {
131
- name: 'click-element-with-text',
132
- matcher: (step: ParsedStep) =>
133
- (step.text.includes('clicks') || step.text.includes('click'))
134
- && !!step.selectorRef
135
- && !!(step.dataRef || step.value)
136
- && step.text.includes('with'),
137
- resolver: (step, context) => {
138
- const resolved = context.selectorResolver.resolveSelector(
139
- step.selectorRef!, undefined, step.elementType, step.nth
140
- );
141
-
142
- let dataValue: string;
143
- if (step.value) {
144
- dataValue = step.value;
145
- } else {
146
- try {
147
- dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
148
- } catch {
149
- dataValue = `\${${step.dataRef}}`;
150
- }
151
- }
152
-
153
- const { name: _name, ...resolvedWithoutName } = resolved;
154
-
155
- return {
156
- templateName: 'click-element-with-text',
157
- data: { ...resolvedWithoutName, dataValue },
158
- comment: `Click ${step.selectorRef} with text ${step.dataRef || step.value}`,
159
- };
160
- },
161
- priority: 15,
162
- },
163
- {
164
- name: 'click-element',
165
- matcher: (step: ParsedStep) =>
166
- (step.text.includes('clicks') || step.text.includes('click')) && !!step.selectorRef,
167
- resolver: (step, context) => {
168
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
169
- return {
170
- templateName: 'click-action',
171
- data: { ...resolved, selectorRef: step.selectorRef },
172
- comment: `Click ${step.selectorRef}`,
173
- };
174
- },
175
- priority: 10,
176
- },
177
- {
178
- name: 'double-click',
179
- matcher: (step: ParsedStep) =>
180
- (step.text.includes('double click') || step.text.includes('double-click') ||
181
- (step.text.includes('double') && step.text.includes('clicks'))) && !!step.selectorRef,
182
- resolver: (step, context) => {
183
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
184
- return {
185
- templateName: 'double-click-action',
186
- data: { ...resolved, selectorRef: step.selectorRef },
187
- comment: `Double click ${step.selectorRef}`,
188
- };
189
- },
190
- priority: 11,
191
- },
192
- {
193
- name: 'hover-element',
194
- matcher: (step: ParsedStep) =>
195
- (step.text.includes('hovers') || step.text.match(/\bhover\b/)) && !!step.selectorRef,
196
- resolver: (step, context) => {
197
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
198
- return {
199
- templateName: 'hover-action',
200
- data: { ...resolved, selectorRef: step.selectorRef },
201
- comment: `Hover ${step.selectorRef}`,
202
- };
203
- },
204
- priority: 8,
205
- },
206
- {
207
- name: 'click-by-data-text',
208
- matcher: (step: ParsedStep) =>
209
- (step.text.includes('clicks') || step.text.includes('click')) && !step.selectorRef && !!step.dataRef,
210
- resolver: (step, context) => {
211
- let value: string;
212
- try {
213
- value = context.dataResolver.resolveData(step.dataRef!, context.featureName);
214
- } catch (error) {
215
- value = `\${${step.dataRef}}`;
216
- }
217
-
218
- return {
219
- templateName: 'click-action',
220
- data: { strategy: 'text', value },
221
- comment: `Click ${step.dataRef}`,
222
- };
223
- },
224
- priority: 5,
225
- },
226
- {
227
- name: 'wait-for-time',
228
- matcher: (step: ParsedStep) =>
229
- (step.text.includes('wait for') || step.text.includes('waits for')) && /\d+/.test(step.text),
230
- resolver: (step, context) => {
231
- const match = step.text.match(/(\d+)\s*(seconds?|ms|milliseconds?)/);
232
- let duration = 1000;
233
-
234
- if (match) {
235
- const num = parseInt(match[1]);
236
- const unit = match[2].toLowerCase();
237
- duration = unit.startsWith('s') ? num * 1000 : num;
238
- }
239
-
240
- return {
241
- templateName: 'wait-timeout',
242
- data: { duration },
243
- comment: `Wait for ${duration}ms`,
244
- };
245
- },
246
- priority: 7,
247
- },
248
- {
249
- name: 'wait-for-role-with-text',
250
- matcher: (step: ParsedStep) =>
251
- (step.text.includes('wait for') || step.text.includes('waits for')) &&
252
- !step.selectorRef &&
253
- !!step.dataRef &&
254
- !!elementKeywordToRole(step.text),
255
- resolver: (step, context) => {
256
- const role = elementKeywordToRole(step.text)!;
257
-
258
- let dataValue: string;
259
- try {
260
- dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
261
- } catch (error) {
262
- dataValue = `\${${step.dataRef}}`;
263
- }
264
-
265
- const state = extractWaitState(step.text);
266
-
267
- return {
268
- templateName: 'wait-for-role-with-data',
269
- data: { role, dataValue, state },
270
- comment: `Wait for ${role} with ${step.dataRef} to be ${state}`,
271
- };
272
- },
273
- priority: 11,
274
- },
275
- {
276
- name: 'wait-for-role',
277
- matcher: (step: ParsedStep) =>
278
- (step.text.includes('wait for') || step.text.includes('waits for')) &&
279
- !step.selectorRef &&
280
- !step.dataRef &&
281
- !!elementKeywordToRole(step.text),
282
- resolver: (step, context) => {
283
- const role = elementKeywordToRole(step.text)!;
284
- const state = extractWaitState(step.text);
285
-
286
- return {
287
- templateName: 'wait-for-role',
288
- data: { role, state },
289
- comment: `Wait for ${role} to be ${state}`,
290
- };
291
- },
292
- priority: 11,
293
- },
294
- {
295
- name: 'wait-for-page',
296
- matcher: (step: ParsedStep) =>
297
- (step.text.includes('wait for') || step.text.includes('waits for')) && step.elementType === 'page',
298
- resolver: (step, context) => {
299
- let path = step.featurePath || '/';
300
-
301
- if (step.selectorRef) {
302
- try {
303
- const resolved = context.selectorResolver.resolveSelector(
304
- step.selectorRef, context.featureName, step.elementType, step.nth
305
- );
306
- path = resolved.value || path;
307
- } catch (error) {
308
- // fallback to featurePath
309
- }
310
- }
311
-
312
- const isAbsoluteUrl = /^https?:\/\//.test(path);
313
- let pathRegex: string;
314
- if (isAbsoluteUrl) {
315
- const url = new URL(path);
316
- const hostEscaped = url.hostname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
317
- const pathEscaped = url.pathname !== '/' ? url.pathname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') : '';
318
- pathRegex = hostEscaped + pathEscaped;
319
- } else {
320
- pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
321
- }
322
-
323
- return {
324
- templateName: 'wait-for-page',
325
- data: { pathRegex },
326
- comment: step.selectorRef ? `Wait for ${step.selectorRef} page` : `Wait for page`,
327
- };
328
- },
329
- priority: 9,
330
- },
331
- {
332
- name: 'wait-for-element-with-text',
333
- matcher: (step: ParsedStep) =>
334
- (step.text.includes('wait for') || step.text.includes('waits for')) &&
335
- !!step.selectorRef &&
336
- !!step.dataRef,
337
- resolver: (step, context) => {
338
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
339
-
340
- let dataValue: string;
341
- try {
342
- dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
343
- } catch (error) {
344
- dataValue = `\${${step.dataRef}}`;
345
- }
346
-
347
- const state = extractWaitState(step.text);
348
-
349
- return {
350
- templateName: 'wait-for-element-with-text',
351
- data: { ...resolved, selectorRef: step.selectorRef, dataValue, state },
352
- comment: `Wait for ${step.selectorRef} with ${step.dataRef} to be ${state}`,
353
- };
354
- },
355
- priority: 10,
356
- },
357
- {
358
- name: 'wait-for-element',
359
- matcher: (step: ParsedStep) =>
360
- (step.text.includes('wait for') || step.text.includes('waits for')) && !!step.selectorRef,
361
- resolver: (step, context) => {
362
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
363
- const state = extractWaitState(step.text);
364
-
365
- return {
366
- templateName: 'wait-for-element',
367
- data: { ...resolved, selectorRef: step.selectorRef, state },
368
- comment: `Wait for ${step.selectorRef} to be ${state}`,
369
- };
370
- },
371
- priority: 8,
372
- },
373
- {
374
- name: 'toggle-switch',
375
- matcher: (step: ParsedStep) =>
376
- (step.text.includes('toggles') || step.text.match(/\btoggle\b/)) && !!step.selectorRef,
377
- resolver: (step, context) => {
378
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
379
- return {
380
- templateName: 'toggle-action',
381
- data: { ...resolved, selectorRef: step.selectorRef },
382
- comment: `Toggle ${step.selectorRef}`,
383
- };
384
- },
385
- priority: 9,
386
- },
387
- {
388
- name: 'drag-to',
389
- matcher: (step: ParsedStep) =>
390
- (step.text.includes('drags') || step.text.match(/\bdrag\b/)) &&
391
- /\bto\s+\[/.test(step.text) && !!step.selectorRef,
392
- resolver: (step, context) => {
393
- // Extract both selector refs: [Source] ... to [Target]
394
- const allRefs = step.text.match(/\[([^\]]+)\]/g);
395
- if (!allRefs || allRefs.length < 2) {
396
- return {
397
- templateName: 'drag-action',
398
- data: { strategy: 'text', value: step.selectorRef, targetLocator: 'page.locator(\'TODO\')' },
399
- comment: `Drag ${step.selectorRef} (missing target)`,
400
- };
401
- }
402
- const sourceRef = allRefs[0].slice(1, -1);
403
- const targetRef = allRefs[1].slice(1, -1);
404
-
405
- const sourceResolved = context.selectorResolver.resolveSelector(sourceRef, undefined, step.elementType, step.nth);
406
- const targetResolved = context.selectorResolver.resolveSelector(targetRef, undefined, undefined, 0);
407
-
408
- // Build target locator expression from resolved selector
409
- let targetLocator: string;
410
- switch (targetResolved.strategy) {
411
- case 'role':
412
- targetLocator = targetResolved.name
413
- ? `page.getByRole('${targetResolved.role}', { name: '${targetResolved.name}' })`
414
- : `page.getByRole('${targetResolved.role}')`;
415
- break;
416
- case 'testid':
417
- targetLocator = `page.getByTestId('${targetResolved.value}')`;
418
- break;
419
- case 'text':
420
- targetLocator = `page.getByText('${targetResolved.value}')`;
421
- break;
422
- case 'locator':
423
- targetLocator = `page.locator('${targetResolved.value}')`;
424
- break;
425
- default:
426
- targetLocator = `page.getByText('${targetRef}')`;
427
- }
428
-
429
- return {
430
- templateName: 'drag-action',
431
- data: { ...sourceResolved, targetLocator },
432
- comment: `Drag ${sourceRef} to ${targetRef}`,
433
- };
434
- },
435
- priority: 10,
436
- },
437
- {
438
- name: 'expand-element',
439
- matcher: (step: ParsedStep) =>
440
- (step.text.includes('expands') || step.text.match(/\bexpand\b/)) && !!step.selectorRef,
441
- resolver: (step, context) => {
442
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
443
- return {
444
- templateName: 'expand-action',
445
- data: { ...resolved, selectorRef: step.selectorRef, direction: 'expand' },
446
- comment: `Expand ${step.selectorRef}`,
447
- };
448
- },
449
- priority: 9,
450
- },
451
- {
452
- name: 'collapse-element',
453
- matcher: (step: ParsedStep) =>
454
- (step.text.includes('collapses') || step.text.match(/\bcollapse\b/)) && !!step.selectorRef,
455
- resolver: (step, context) => {
456
- const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
457
- return {
458
- templateName: 'expand-action',
459
- data: { ...resolved, selectorRef: step.selectorRef, direction: 'collapse' },
460
- comment: `Collapse ${step.selectorRef}`,
461
- };
462
- },
463
- priority: 9,
464
- },
465
- ];
@@ -1,51 +0,0 @@
1
- /**
2
- * Keyboard Patterns
3
- * Handles: press Key key, press Key on [Target] type
4
- */
5
-
6
- import { StepPattern } from './types';
7
-
8
- export const keyboardPatterns: StepPattern[] = [
9
- {
10
- name: 'press-key-global',
11
- matcher: (step) => {
12
- const text = step.text.toLowerCase();
13
- return /\bpress(?:es)?\s+\w+\s+key\b/.test(text) && !text.includes(' on ');
14
- },
15
- resolver: (step, context) => {
16
- // Extract key name: "User press Escape key" → "Escape"
17
- const match = step.text.match(/press(?:es)?\s+(\w+)\s+key/i);
18
- const key = match ? match[1] : 'Enter';
19
-
20
- return {
21
- templateName: 'keyboard-global-action',
22
- data: { key },
23
- comment: `Press ${key} key`,
24
- };
25
- },
26
- priority: 12,
27
- },
28
- {
29
- name: 'press-key-on-element',
30
- matcher: (step) => {
31
- const text = step.text.toLowerCase();
32
- return /\bpress(?:es)?\s+\w+\s+on\b/.test(text) && !!step.selectorRef;
33
- },
34
- resolver: (step, context) => {
35
- // Extract key: "User press Enter on [Search] field" → "Enter"
36
- const match = step.text.match(/press(?:es)?\s+(\w+)\s+on/i);
37
- const key = match ? match[1] : 'Enter';
38
-
39
- const resolved = context.selectorResolver.resolveSelector(
40
- step.selectorRef!, context.featureName, step.elementType, step.nth
41
- );
42
-
43
- return {
44
- templateName: 'press-action',
45
- data: { ...resolved, key },
46
- comment: `Press ${key} on ${step.selectorRef}`,
47
- };
48
- },
49
- priority: 12,
50
- },
51
- ];
@@ -1,140 +0,0 @@
1
- import { ParsedStep } from '../../gherkin-parser';
2
- import { StepPattern } from './types';
3
- import { inferPath, resolvePathVariables, getPathCode } from '../utils/path-inference';
4
-
5
- /**
6
- * Navigation patterns: goto, navigate, open page
7
- */
8
- export const navigationPatterns: StepPattern[] = [
9
- {
10
- name: 'open-page-type',
11
- matcher: (step: ParsedStep) =>
12
- (step.text.includes('open') || step.text.includes('opens') || step.text.includes('is on') || step.text.includes('navigate to')) &&
13
- step.elementType === 'page',
14
- resolver: (step, context) => {
15
- const isThen = context.effectiveKeyword === 'Then';
16
- // Cross-screen page refs (e.g. Then User is on [dashboard] page) won't be in the
17
- // current screen's selectors. Fall back to /<ref>/ for assertion paths instead of
18
- // the current featurePath, which would silently make the assertion always pass.
19
- const pathFallback = isThen && step.selectorRef
20
- ? `/${step.selectorRef}/`
21
- : (context.featurePath || '/');
22
- let path = pathFallback;
23
-
24
- if (step.selectorRef) {
25
- try {
26
- const resolved = context.selectorResolver.resolveSelector(
27
- step.selectorRef, context.featureName, step.elementType, step.nth
28
- );
29
- path = resolved.value || pathFallback;
30
- } catch (error) {
31
- path = pathFallback;
32
- }
33
- }
34
-
35
- const finalPath = resolvePathVariables(path, context.scenarioSteps || []);
36
- const isAbsoluteUrl = /^https?:\/\//.test(finalPath);
37
-
38
- // Then User is on [X] page — assert URL, don't navigate.
39
- if (isThen) {
40
- return {
41
- templateName: 'route-assertion',
42
- data: { path: finalPath },
43
- comment: step.selectorRef ? `Assert on ${step.selectorRef} page` : `Assert page route`,
44
- };
45
- }
46
-
47
- return {
48
- templateName: 'navigation',
49
- data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: finalPath },
50
- comment: step.selectorRef ? `Open ${step.selectorRef} page` : `Navigate to page`,
51
- };
52
- },
53
- priority: 16,
54
- },
55
- {
56
- name: 'open-page',
57
- matcher: (step: ParsedStep) =>
58
- (step.text.includes('open') || step.text.includes('opens')) &&
59
- step.text.includes('page') &&
60
- step.elementType !== 'page',
61
- resolver: (step, context) => {
62
- const pageMatch = step.text.match(/open[s]?\s+\[([^\]]+)\]/i) ||
63
- step.text.match(/open[s]?\s+"([^"]+)"/i);
64
- const pageName = pageMatch ? pageMatch[1] : 'page';
65
-
66
- const inferredPath = inferPath(context.featurePath, {
67
- featureName: context.featureName,
68
- screenName: context.screenName,
69
- });
70
- const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
71
- const pathCode = getPathCode(resolvedPath);
72
- const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
73
- const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
74
-
75
- return {
76
- templateName: 'navigation',
77
- data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
78
- comment: `Open ${pageName}`,
79
- };
80
- },
81
- priority: 15,
82
- },
83
- {
84
- name: 'navigate-to-route',
85
- matcher: (step: ParsedStep) =>
86
- (step.text.includes('navigate to') || step.text.includes('navigates to') || step.text.includes('is on')) &&
87
- !!(step.selectorRef || step.dataRef),
88
- resolver: (step, context) => {
89
- const route = step.selectorRef || step.dataRef;
90
- const isThen = context.effectiveKeyword === 'Then';
91
-
92
- const inferredPath = inferPath(context.featurePath, {
93
- featureName: context.featureName,
94
- screenName: context.screenName,
95
- });
96
- const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
97
- const pathCode = getPathCode(resolvedPath);
98
- const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
99
- const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
100
-
101
- if (isThen) {
102
- return {
103
- templateName: 'route-assertion',
104
- data: { path: cleanPath },
105
- comment: `Assert on ${route}`,
106
- };
107
- }
108
-
109
- return {
110
- templateName: 'navigation',
111
- data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
112
- comment: `Navigate to ${route}`,
113
- };
114
- },
115
- priority: 10,
116
- },
117
- {
118
- name: 'route-assertion',
119
- matcher: (step: ParsedStep) =>
120
- (step.text.includes('should see route') || step.text.includes('should remain on')) &&
121
- !!(step.selectorRef || step.dataRef),
122
- resolver: (step, context) => {
123
- const route = step.selectorRef || step.dataRef;
124
-
125
- const inferredPath = inferPath(context.featurePath, {
126
- featureName: context.featureName,
127
- screenName: context.screenName,
128
- });
129
- const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
130
- const pathCode = getPathCode(resolvedPath);
131
-
132
- return {
133
- templateName: 'route-assertion',
134
- data: { path: pathCode.replace(/^['`]|['`]$/g, '') },
135
- comment: `Assert current route is ${route}`,
136
- };
137
- },
138
- priority: 10,
139
- },
140
- ];
@@ -1,40 +0,0 @@
1
- /**
2
- * Scope Patterns
3
- * Handles: switch to [Target] frame, switch to [main] frame
4
- */
5
-
6
- import { StepPattern } from './types';
7
-
8
- export const scopePatterns: StepPattern[] = [
9
- {
10
- name: 'switch-to-frame',
11
- matcher: (step) => {
12
- return /\bswitch(?:es)?\s+to\b/i.test(step.text) &&
13
- (step.elementType === 'frame' || step.elementType === 'iframe');
14
- },
15
- resolver: (step, context) => {
16
- const selectorRef = step.selectorRef || '';
17
-
18
- // "switch to [main] frame" → reset to page context
19
- if (selectorRef.toLowerCase() === 'main') {
20
- return {
21
- templateName: 'frame-exit-action',
22
- data: {},
23
- comment: 'Exit frame scope, return to main page',
24
- };
25
- }
26
-
27
- // "switch to [Payment] frame" → enter frame scope
28
- const resolved = context.selectorResolver.resolveSelector(
29
- selectorRef, context.featureName, 'frame', step.nth
30
- );
31
-
32
- return {
33
- templateName: 'frame-enter-action',
34
- data: { ...resolved, frameName: selectorRef },
35
- comment: `Switch to ${selectorRef} frame`,
36
- };
37
- },
38
- priority: 11,
39
- },
40
- ];