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