@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.
- package/dist/generators/cli.js +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +3 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +14 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/scaffold-generator/index.d.ts +4 -1
- package/dist/generators/scaffold-generator/index.d.ts.map +1 -1
- package/dist/generators/scaffold-generator/index.js +46 -18
- package/dist/generators/scaffold-generator/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/radio-select-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
- package/dist/generators/test-generator/code-generator.d.ts +3 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +66 -14
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +148 -73
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +37 -5
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +116 -20
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js +1 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +15 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +5 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +67 -3
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +14 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +32 -5
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/input/cli-adapter.js +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +2 -1
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/package.json +1 -1
- package/src/generators/cli.ts +1 -1
- package/src/generators/gherkin-parser/index.ts +18 -1
- package/src/generators/scaffold-generator/index.ts +50 -22
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/radio-select-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/upload-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +10 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-nth.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/label.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/placeholder.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/role.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/text.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +10 -1
- package/src/generators/test-generator/code-generator.ts +70 -15
- package/src/generators/test-generator/patterns/assertion-patterns.ts +171 -78
- package/src/generators/test-generator/patterns/form-patterns.ts +39 -5
- package/src/generators/test-generator/patterns/interaction-patterns.ts +126 -24
- package/src/generators/test-generator/patterns/navigation-patterns.ts +2 -1
- package/src/generators/test-generator/step-mapper.ts +18 -2
- package/src/generators/test-generator/template-engine.ts +74 -3
- package/src/generators/test-generator/utils/selector-resolver.ts +34 -5
- package/src/input/cli-adapter.ts +1 -1
- package/src/orchestrator/project-initializer.ts +2 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/target-element-action.hbs +0 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +0 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +0 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/page-navigation.hbs +0 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/target-element-action.hbs +0 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +0 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-filter-assertion.hbs +0 -1
- 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
|
-
//
|
|
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(
|
|
37
|
+
// Generates: await page.getByText('').filter({ hasText: /.*value of order_name$/ }).click();
|
|
12
38
|
{
|
|
13
|
-
name: '
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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')
|
|
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
|
|
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;
|
|
32
|
-
private currentScenarioSteps?: ParsedStep[];
|
|
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
|
-
|
|
25
|
+
if (text == null) return '';
|
|
26
|
+
return String(text).replace(/'/g, "\\'");
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
Handlebars.registerHelper('escapeRegex', function(text: string) {
|
|
28
|
-
|
|
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);
|
package/src/input/cli-adapter.ts
CHANGED
|
@@ -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.goto('{{path}}');
|
|
@@ -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.goto('{{path}}');
|