@sun-asterisk/sungen 2.2.1 → 2.2.3
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/README.md +63 -34
- package/dist/cli/index.js +1 -1
- package/dist/generators/gherkin-parser/index.js +1 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +83 -57
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +56 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +16 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -4
- package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +148 -21
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +51 -11
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +12 -5
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +148 -21
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +51 -11
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
- package/docs/gherkin standards/gherkin-core-standard.md +163 -160
- package/docs/gherkin standards/gherkin-core-standard.vi.md +290 -404
- package/docs/gherkin-dictionary.md +71 -16
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/generators/gherkin-parser/index.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/src/generators/test-generator/patterns/assertion-patterns.ts +93 -65
- package/src/generators/test-generator/patterns/interaction-patterns.ts +58 -1
- package/src/generators/test-generator/utils/selector-resolver.ts +16 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -4
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +148 -21
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +51 -11
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +1 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +1 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +12 -5
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +148 -21
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +51 -11
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
|
@@ -193,20 +193,22 @@ TABLE_COL_EXPR := ("has")? TARGET_EXPR ("cell")? (DATA_EXPR)?
|
|
|
193
193
|
# ─── Terminals ────────────────────────────────────────────────
|
|
194
194
|
TARGET_EXPR := "[" IDENTIFIER "]" (ELEMENT_TYPE)?
|
|
195
195
|
ELEMENT_TYPE := "button"|"link"|"field"|"heading"|"text"|"image"|"checkbox"
|
|
196
|
-
| "radio"|"switch"|"dropdown"|"option"|"dialog"|"modal"
|
|
197
|
-
| "
|
|
198
|
-
| "cell"|"column"|"columnheader"|"region"|"section"
|
|
199
|
-
| "banner"|"header"|"footer"|"alert"|"spinner"|"progressbar"
|
|
200
|
-
| "slider"|"tree"|"treeitem"|"tooltip"|"icon"|"uploader"
|
|
201
|
-
| "file"|"frame"|"iframe"|"textarea"|"page"
|
|
196
|
+
| "radio"|"switch"|"toggle"|"dropdown"|"option"|"dialog"|"modal"
|
|
197
|
+
| "drawer"|"menu"|"menuitem"|"tab"|"tabpanel"|"list"|"listitem"
|
|
198
|
+
| "table"|"row"|"cell"|"column"|"columnheader"|"region"|"section"
|
|
199
|
+
| "nav"|"banner"|"header"|"footer"|"alert"|"spinner"|"progressbar"
|
|
200
|
+
| "slider"|"tree"|"treeitem"|"tooltip"|"icon"|"uploader"|"tag"
|
|
201
|
+
| "file"|"frame"|"iframe"|"textarea"|"page"|"search"|"date-picker"
|
|
202
|
+
| "badge"|"breadcrumb"|"overlay"|"step"|"card"|"item"|"key"
|
|
203
|
+
| "message"|"label"
|
|
202
204
|
DATA_EXPR := "with" "{{" IDENTIFIER "}}"
|
|
203
205
|
STATE_EXPR := "is" STATE
|
|
204
206
|
STATE := "hidden"|"visible"|"disabled"|"enabled"|"checked"
|
|
205
207
|
| "unchecked"|"focused"|"empty"|"loading"|"selected"
|
|
206
208
|
| "sorted ascending"|"sorted descending"
|
|
207
|
-
ACTION := "click"|"fill"|"select"|"check"|"uncheck"
|
|
209
|
+
ACTION := "click"|"fill"|"select"|"check"|"uncheck"
|
|
208
210
|
| "upload"|"hover"|"drag"|"clear"|"see"|"press"|"expand"
|
|
209
|
-
| "collapse"|"double click"
|
|
211
|
+
| "collapse"|"double click"|"toggle"
|
|
210
212
|
KEY := "Enter"|"Escape"|"Tab"|"Backspace"|"Delete"|"Space"
|
|
211
213
|
| "ArrowUp"|"ArrowDown"|"ArrowLeft"|"ArrowRight"
|
|
212
214
|
| "Home"|"End"|"PageUp"|"PageDown"
|
|
@@ -281,7 +283,11 @@ if action == 'fill': code = locator.fill(value)
|
|
|
281
283
|
if action == 'select': code = locator.selectOption(value) or click+click
|
|
282
284
|
if action == 'upload': code = locator.setInputFiles(value)
|
|
283
285
|
if action == 'click': code = page.getByText(value).click() or locator.filter({hasText}).click()
|
|
284
|
-
if action == 'see':
|
|
286
|
+
if action == 'see':
|
|
287
|
+
if type in (field, textarea, search, dropdown, slider, date-picker):
|
|
288
|
+
code = expect(locator).toHaveValue(value) # input types → toHaveValue
|
|
289
|
+
else:
|
|
290
|
+
code = expect(locator).toHaveText(value) # text types → toHaveText (exact match)
|
|
285
291
|
```
|
|
286
292
|
|
|
287
293
|
**Playwright output:**
|
|
@@ -306,11 +312,14 @@ await page.getByLabel('Avatar').setInputFiles('specs/storage/avatar.png');
|
|
|
306
312
|
// click with text filter
|
|
307
313
|
await page.getByText('Nguyễn Thanh Tùng').click();
|
|
308
314
|
|
|
309
|
-
// see heading with value
|
|
310
|
-
await expect(page.getByRole('heading', { name: '
|
|
315
|
+
// see heading with value → toHaveText (exact match)
|
|
316
|
+
await expect(page.getByRole('heading', { name: 'Welcome' })).toHaveText('Welcome back, Admin');
|
|
311
317
|
|
|
312
|
-
// see
|
|
313
|
-
await expect(page.
|
|
318
|
+
// see field with value → toHaveValue (input types)
|
|
319
|
+
await expect(page.getByPlaceholder('Email')).toHaveValue('user@example.com');
|
|
320
|
+
|
|
321
|
+
// see message with value → toHaveText (text types)
|
|
322
|
+
await expect(page.getByText('Error')).toHaveText('Invalid credentials');
|
|
314
323
|
```
|
|
315
324
|
|
|
316
325
|
---
|
|
@@ -834,6 +843,49 @@ await row.getByRole('button', { name: 'Expand' }).click();
|
|
|
834
843
|
|
|
835
844
|
---
|
|
836
845
|
|
|
846
|
+
## Browser Alert (System Dialog)
|
|
847
|
+
|
|
848
|
+
For native browser dialogs (`window.alert`, `window.confirm`, `window.prompt`):
|
|
849
|
+
|
|
850
|
+
```gherkin
|
|
851
|
+
# Alert steps must appear BEFORE the action that triggers the dialog
|
|
852
|
+
When User click [OK] alert # accept
|
|
853
|
+
And User click [Delete] button # triggers the alert
|
|
854
|
+
|
|
855
|
+
When User click [Cancel] alert # dismiss
|
|
856
|
+
And User click [Delete] button
|
|
857
|
+
|
|
858
|
+
When User fill [Name] alert with {{v}} # fill prompt + accept
|
|
859
|
+
And User click [Rename] button
|
|
860
|
+
|
|
861
|
+
Then User see [Are you sure?] alert # assert dialog message
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**Compiler rule:**
|
|
865
|
+
```
|
|
866
|
+
click [OK/Accept/Yes/Confirm] alert → page.once('dialog', d => d.accept())
|
|
867
|
+
click [Cancel/Dismiss/No] alert → page.once('dialog', d => d.dismiss())
|
|
868
|
+
fill [T] alert with {{v}} → page.once('dialog', d => d.accept(value))
|
|
869
|
+
see [text] alert → Promise-based dialog.message() check
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
**Playwright output:**
|
|
873
|
+
```typescript
|
|
874
|
+
// accept alert (register BEFORE triggering action)
|
|
875
|
+
page.once('dialog', dialog => dialog.accept());
|
|
876
|
+
await page.getByRole('button', { name: 'Delete' }).click();
|
|
877
|
+
|
|
878
|
+
// dismiss alert
|
|
879
|
+
page.once('dialog', dialog => dialog.dismiss());
|
|
880
|
+
await page.getByRole('button', { name: 'Delete' }).click();
|
|
881
|
+
|
|
882
|
+
// fill prompt
|
|
883
|
+
page.once('dialog', dialog => dialog.accept('New name'));
|
|
884
|
+
await page.getByRole('button', { name: 'Rename' }).click();
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
837
889
|
## Dialog Scope
|
|
838
890
|
|
|
839
891
|
When a dialog is opened, subsequent steps are scoped within it:
|
|
@@ -902,7 +954,9 @@ await frame.getByRole('button', { name: 'Pay' }).click();
|
|
|
902
954
|
| `switch` / `toggle` | `switch` | `getByRole('switch')` |
|
|
903
955
|
| `dropdown` / `select` | `combobox` | `getByRole('combobox')` |
|
|
904
956
|
| `option` | `option` | `getByRole('option')` |
|
|
905
|
-
| `
|
|
957
|
+
| `search` | `searchbox` | `getByRole('searchbox')` |
|
|
958
|
+
| `slider` | `slider` | `getByRole('slider')` |
|
|
959
|
+
| `dialog` / `modal` / `drawer` | `dialog` | `getByRole('dialog')` |
|
|
906
960
|
| `menu` | `menu` | `getByRole('menu')` |
|
|
907
961
|
| `menuitem` | `menuitem` | `getByRole('menuitem')` |
|
|
908
962
|
| `tab` | `tab` | `getByRole('tab')` |
|
|
@@ -917,7 +971,7 @@ await frame.getByRole('button', { name: 'Pay' }).click();
|
|
|
917
971
|
| `nav` / `navigation` | `navigation` | `getByRole('navigation')` |
|
|
918
972
|
| `banner` / `header` | `banner` | `getByRole('banner')` |
|
|
919
973
|
| `footer` | `contentinfo` | `getByRole('contentinfo')` |
|
|
920
|
-
| `alert` | `
|
|
974
|
+
| `alert` | `alertdialog` | `getByRole('alertdialog')` or `page.on('dialog')` for browser alerts |
|
|
921
975
|
| `spinner` / `progressbar` | `progressbar` | `getByRole('progressbar')` |
|
|
922
976
|
| `slider` | `slider` | `getByRole('slider')` |
|
|
923
977
|
| `tree` | `tree` | `getByRole('tree')` |
|
|
@@ -982,6 +1036,8 @@ collapse → .getByRole('button', {name:'Collapse'}).click()
|
|
|
982
1036
|
|
|
983
1037
|
```
|
|
984
1038
|
see → expect(locator).toBeVisible()
|
|
1039
|
+
see with {{v}} → expect(locator).toHaveText(value) # text types (message, header, label, row...)
|
|
1040
|
+
see with {{v}} → expect(locator).toHaveValue(value) # input types (field, textarea, search, dropdown, slider, date-picker)
|
|
985
1041
|
see ... hidden → expect(locator).toBeHidden()
|
|
986
1042
|
see ... visible → expect(locator).toBeVisible()
|
|
987
1043
|
see ... enabled → expect(locator).toBeEnabled()
|
|
@@ -995,7 +1051,6 @@ see ... selected→ expect(locator).toHaveAttribute('aria-selected', 'true')
|
|
|
995
1051
|
see ... sorted ascending → expect(locator).toHaveAttribute('aria-sort', 'ascending')
|
|
996
1052
|
see ... sorted descending → expect(locator).toHaveAttribute('aria-sort', 'descending')
|
|
997
1053
|
contains → expect(locator).toContainText(value)
|
|
998
|
-
has text → expect(locator).toHaveText(value)
|
|
999
1054
|
has (attribute) → expect(locator).toHaveAttribute(attr, value)
|
|
1000
1055
|
has ... rows → expect(locator.getByRole('row')).toHaveCount(N)
|
|
1001
1056
|
has ... column → expect(locator.getByRole('columnheader', {name})).toBeVisible()
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -146,7 +146,7 @@ export class GherkinParser {
|
|
|
146
146
|
elementType = elementTypeMatch ? elementTypeMatch[1].toLowerCase() : undefined;
|
|
147
147
|
} else {
|
|
148
148
|
// Extract final word if it's a known type (page, button, field, etc)
|
|
149
|
-
const finalWordMatch = text.match(/\b(page|button|field|link|text|checkbox|radio|select|dropdown|modal|dialog|message|error|warning|success|alert|table|list|list-item|listitem|row)\b$/i);
|
|
149
|
+
const finalWordMatch = text.match(/\b(page|button|field|link|text|checkbox|radio|select|dropdown|modal|dialog|drawer|message|error|warning|success|alert|table|list|list-item|listitem|row|search|option|slider|toggle|tab|breadcrumb|overlay|step|section|card|item|icon|image|header|label|badge|tooltip|tag|menuitem|cell|column|uploader|date-picker|key)\b$/i);
|
|
150
150
|
elementType = finalWordMatch ? finalWordMatch[1].toLowerCase() : undefined;
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
page.once('dialog', dialog => dialog.accept());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
page.once('dialog', dialog => dialog.dismiss());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
page.once('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const dialogPromise{{stepCounter}} = new Promise<string>(resolve => {
|
|
2
|
+
page.once('dialog', dialog => {
|
|
3
|
+
resolve(dialog.message());
|
|
4
|
+
dialog.accept();
|
|
5
|
+
});
|
|
6
|
+
});
|
|
7
|
+
// Trigger the dialog action, then:
|
|
8
|
+
// expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await expect({{> locator}}).toHaveValue('{{escapeQuotes dataValue}}');
|
|
@@ -6,6 +6,30 @@ import { StepPattern, StepTemplateData } from './types';
|
|
|
6
6
|
* Uses template engine for framework-agnostic code generation
|
|
7
7
|
*/
|
|
8
8
|
export const assertionPatterns: StepPattern[] = [
|
|
9
|
+
// Browser alert text assertion: "see [Are you sure?] alert"
|
|
10
|
+
{
|
|
11
|
+
name: 'alert-text-assertion',
|
|
12
|
+
matcher: (step: ParsedStep) =>
|
|
13
|
+
(step.text.includes('see') || step.text.includes('sees')) &&
|
|
14
|
+
step.elementType === 'alert' &&
|
|
15
|
+
!!step.selectorRef,
|
|
16
|
+
resolver: (step, context): StepTemplateData => {
|
|
17
|
+
let dataValue = step.selectorRef || '';
|
|
18
|
+
if (step.dataRef) {
|
|
19
|
+
try {
|
|
20
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
21
|
+
} catch {
|
|
22
|
+
dataValue = `\${${step.dataRef}}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
templateName: 'alert-text-assertion',
|
|
27
|
+
data: { dataValue, stepCounter: context.stepCounter },
|
|
28
|
+
comment: `Assert browser alert contains "${step.selectorRef}"`,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
priority: 21,
|
|
32
|
+
},
|
|
9
33
|
// Column cell assertion: "see [Department] column 1 with {{value}}" -> check table cell text
|
|
10
34
|
{
|
|
11
35
|
name: 'column-cell-assertion',
|
|
@@ -218,6 +242,7 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
218
242
|
// Pattern: Then User see [error] message with {{fail_message}}
|
|
219
243
|
// If selector YAML has empty value, uses variable-only matching
|
|
220
244
|
// If selector has value, combines both (static text + variable)
|
|
245
|
+
// Input types → toHaveValue, everything else → toHaveText
|
|
221
246
|
{
|
|
222
247
|
name: 'see-with-variable',
|
|
223
248
|
matcher: (step: ParsedStep) =>
|
|
@@ -227,6 +252,12 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
227
252
|
!!step.dataRef &&
|
|
228
253
|
step.text.includes('with'),
|
|
229
254
|
generator: (step, context) => {
|
|
255
|
+
// Input element types use toHaveValue instead of toHaveText
|
|
256
|
+
const INPUT_TYPES = new Set([
|
|
257
|
+
'field', 'textarea', 'search', 'dropdown', 'slider', 'date-picker',
|
|
258
|
+
'input', 'textbox', 'editor', 'select', 'combobox',
|
|
259
|
+
]);
|
|
260
|
+
|
|
230
261
|
// Resolve data variable value
|
|
231
262
|
let dataValue: string;
|
|
232
263
|
try {
|
|
@@ -235,19 +266,42 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
235
266
|
dataValue = `\${${step.dataRef}}`;
|
|
236
267
|
}
|
|
237
268
|
|
|
238
|
-
//
|
|
239
|
-
|
|
269
|
+
// Resolve selector from YAML with feature context
|
|
270
|
+
let resolved: any = {};
|
|
271
|
+
try {
|
|
272
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
273
|
+
step.selectorRef!,
|
|
274
|
+
context.featureName,
|
|
275
|
+
step.elementType,
|
|
276
|
+
step.nth
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Selector not in YAML or context issue - will use variable-only
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Input types: toHaveValue ---
|
|
283
|
+
if (step.elementType && INPUT_TYPES.has(step.elementType)) {
|
|
284
|
+
const code = context.templateEngine.renderStep('have-value-assertion', {
|
|
285
|
+
...resolved,
|
|
286
|
+
dataValue,
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
code,
|
|
290
|
+
comment: `Assert ${step.selectorRef} has value ${step.dataRef}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- Label-value pattern: "User see [X] label with {{Y}}" ---
|
|
240
295
|
if (step.elementType === 'label' && step.dataRef) {
|
|
241
|
-
// Check if selector override has empty value — if so, omit label from regex
|
|
242
296
|
let label: string | undefined = step.selectorRef;
|
|
243
297
|
try {
|
|
244
|
-
const
|
|
298
|
+
const labelResolved = context.selectorResolver.resolveSelector(
|
|
245
299
|
step.selectorRef!,
|
|
246
300
|
context.featureName,
|
|
247
301
|
step.elementType,
|
|
248
302
|
step.nth
|
|
249
303
|
);
|
|
250
|
-
if (
|
|
304
|
+
if (labelResolved.value !== undefined && labelResolved.value.trim() === '') {
|
|
251
305
|
label = undefined;
|
|
252
306
|
}
|
|
253
307
|
} catch {
|
|
@@ -264,93 +318,67 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
264
318
|
};
|
|
265
319
|
}
|
|
266
320
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
resolved = context.selectorResolver.resolveSelector(
|
|
271
|
-
step.selectorRef!,
|
|
272
|
-
context.featureName,
|
|
273
|
-
step.elementType,
|
|
274
|
-
step.nth
|
|
275
|
-
);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// Selector not in YAML or context issue - will use variable-only
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
281
|
-
|
|
282
|
-
// Check if it's a locator strategy - use locator with filter
|
|
283
|
-
if (resolved.strategy === 'locator') {
|
|
284
|
-
const code = context.templateEngine.renderStep('visible-with-locator-variable-assertion', {
|
|
285
|
-
...resolved,
|
|
321
|
+
// --- Dialog role: check heading inside dialog ---
|
|
322
|
+
if (resolved.strategy === 'role' && resolved.role === 'dialog') {
|
|
323
|
+
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
286
324
|
dataValue,
|
|
287
|
-
nth: resolved.nth,
|
|
288
325
|
});
|
|
289
326
|
return {
|
|
290
327
|
code,
|
|
291
|
-
comment: `Assert ${step.selectorRef} with
|
|
328
|
+
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
292
329
|
};
|
|
293
330
|
}
|
|
294
331
|
|
|
295
|
-
//
|
|
296
|
-
if (resolved.strategy === 'role' && resolved.role) {
|
|
297
|
-
// Dialog role: check heading inside dialog instead of filter on full text content
|
|
298
|
-
if (resolved.role === 'dialog') {
|
|
299
|
-
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
300
|
-
dataValue,
|
|
301
|
-
});
|
|
302
|
-
return {
|
|
303
|
-
code,
|
|
304
|
-
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
332
|
+
// --- Image role: images have no text, use name ---
|
|
333
|
+
if (resolved.strategy === 'role' && resolved.role === 'img') {
|
|
308
334
|
const hasName = resolved.name && resolved.name.trim();
|
|
309
|
-
// For img role: images have no text content, so put dataValue in name instead of filter
|
|
310
|
-
const isImgRole = resolved.role === 'img';
|
|
311
335
|
const code = context.templateEngine.renderStep('visible-with-role-variable-assertion', {
|
|
312
336
|
role: resolved.role,
|
|
313
|
-
name:
|
|
314
|
-
dataValue: isImgRole ? undefined : dataValue,
|
|
337
|
+
name: hasName ? resolved.name : dataValue,
|
|
315
338
|
exact: resolved.exact || false,
|
|
316
339
|
nth: resolved.nth,
|
|
317
340
|
});
|
|
318
341
|
return {
|
|
319
342
|
code,
|
|
320
|
-
comment: `Assert ${step.selectorRef} with
|
|
343
|
+
comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
|
|
321
344
|
};
|
|
322
345
|
}
|
|
323
346
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const code = context.templateEngine.renderStep('visible-with-value-assertion', {
|
|
331
|
-
value: selectorValue,
|
|
332
|
-
dataValue,
|
|
333
|
-
dataRef: step.dataRef,
|
|
334
|
-
nth: resolved.nth,
|
|
335
|
-
exact: resolved.exact || false,
|
|
347
|
+
// --- Everything else: toHaveText (exact full match) ---
|
|
348
|
+
// Locator strategy
|
|
349
|
+
if (resolved.strategy === 'locator') {
|
|
350
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
351
|
+
...resolved,
|
|
352
|
+
expectedText: dataValue,
|
|
336
353
|
});
|
|
337
354
|
return {
|
|
338
355
|
code,
|
|
339
|
-
comment: `Assert ${step.selectorRef}
|
|
356
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
340
357
|
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Role-based selector
|
|
361
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
362
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
363
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
364
|
+
...resolved,
|
|
365
|
+
expectedText: dataValue,
|
|
348
366
|
});
|
|
349
367
|
return {
|
|
350
368
|
code,
|
|
351
|
-
comment: `Assert ${step.selectorRef}
|
|
369
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
352
370
|
};
|
|
353
371
|
}
|
|
372
|
+
|
|
373
|
+
// Text/placeholder/testid/other strategies
|
|
374
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
375
|
+
...resolved,
|
|
376
|
+
expectedText: dataValue,
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
code,
|
|
380
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
381
|
+
};
|
|
354
382
|
},
|
|
355
383
|
priority: 13,
|
|
356
384
|
},
|
|
@@ -27,9 +27,66 @@ function elementKeywordToRole(text: string): string | null {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Interaction patterns: click, hover, press, wait
|
|
30
|
+
* Interaction patterns: click, hover, press, wait, alert
|
|
31
31
|
*/
|
|
32
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
|
+
},
|
|
33
90
|
{
|
|
34
91
|
name: 'unknown-element-action',
|
|
35
92
|
matcher: (step: ParsedStep) => {
|
|
@@ -211,6 +211,13 @@ export class SelectorResolver {
|
|
|
211
211
|
'heading': () => ({ strategy: 'role', role: 'heading', name: label, value: 'heading' }),
|
|
212
212
|
'list': () => ({ strategy: 'role', role: 'list', name: label, value: 'list' }),
|
|
213
213
|
'listitem': () => ({ strategy: 'role', role: 'listitem', value: 'listitem' }),
|
|
214
|
+
'searchbox': () => ({ strategy: 'role', role: 'searchbox', name: label, value: 'searchbox' }),
|
|
215
|
+
'option': () => ({ strategy: 'role', role: 'option', name: label, value: 'option' }),
|
|
216
|
+
'slider': () => ({ strategy: 'role', role: 'slider', name: label, value: 'slider' }),
|
|
217
|
+
'switch': () => ({ strategy: 'role', role: 'switch', name: label, value: 'switch' }),
|
|
218
|
+
'tab': () => ({ strategy: 'role', role: 'tab', name: label, value: 'tab' }),
|
|
219
|
+
'dialog': () => ({ strategy: 'role', role: 'dialog', name: label, value: 'dialog' }),
|
|
220
|
+
'alertdialog': () => ({ strategy: 'role', role: 'alertdialog', name: label, value: 'alertdialog' }),
|
|
214
221
|
};
|
|
215
222
|
|
|
216
223
|
const factory = strategyMap[normalized];
|
|
@@ -247,6 +254,15 @@ export class SelectorResolver {
|
|
|
247
254
|
'textbox': 'field',
|
|
248
255
|
'textarea': 'field',
|
|
249
256
|
'editor': 'field',
|
|
257
|
+
// Search alias → searchbox role
|
|
258
|
+
'search': 'searchbox',
|
|
259
|
+
// Toggle alias → switch role
|
|
260
|
+
'toggle': 'switch',
|
|
261
|
+
// Alert alias → alertdialog role
|
|
262
|
+
'alert': 'alertdialog',
|
|
263
|
+
// Modal/drawer alias → dialog role
|
|
264
|
+
'modal': 'dialog',
|
|
265
|
+
'drawer': 'dialog',
|
|
250
266
|
};
|
|
251
267
|
return aliasMap[elementType] || elementType;
|
|
252
268
|
}
|
|
@@ -18,7 +18,14 @@ Parse **screen** from `$ARGUMENTS`. If missing, ask the user.
|
|
|
18
18
|
1. Verify `qa/screens/<screen>/` has `.feature` + `test-data.yaml`. If not → `/sungen:make-tc` first.
|
|
19
19
|
2. Generate `selectors.yaml` from live page using `sungen-selector-fix` and `sungen-selector-keys` skills.
|
|
20
20
|
3. Compile: `sungen generate --screen <screen>`
|
|
21
|
-
4.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
4. **Initial run** — run ALL tests to get the full failure picture:
|
|
22
|
+
`npx playwright test specs/generated/<screen>/<screen>.spec.ts --reporter=line`
|
|
23
|
+
5. **Batched fix loop** (max 5 attempts) — see `sungen-selector-fix` skill for details:
|
|
24
|
+
- Group failures by root cause (same selector, same error type)
|
|
25
|
+
- Fix selectors/test-data for the current batch
|
|
26
|
+
- Recompile, then re-run ONLY the previously-failing tests (max 20 at a time via `--grep`)
|
|
27
|
+
- If the batch passes, pick the next batch of remaining failures
|
|
28
|
+
- Repeat until no failures remain or max attempts reached
|
|
29
|
+
6. **Final confirmation** — run ALL tests once to ensure no regressions.
|
|
30
|
+
7. After 5 fix attempts still failing → ask user about direct `.spec.ts` fix.
|
|
31
|
+
8. Show: pass/fail, attempt count, files changed.
|
|
@@ -6,16 +6,29 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
## Playwright Errors → Fix
|
|
8
8
|
|
|
9
|
-
| Error | Fix
|
|
9
|
+
| Error | Fix |
|
|
10
10
|
|---|---|
|
|
11
|
-
| strict mode violation | add `nth`, `exact: true`, or specific `name` |
|
|
12
|
-
| Element is not an input | change `type` or `value` |
|
|
13
|
-
| Timeout waiting for selector | fix `type`/`value`/`name` to match element |
|
|
14
|
-
| toBeVisible Timeout | verify accessible name or use `testid` |
|
|
15
|
-
| toHaveText mismatch | fix in `test-data.yaml` |
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
11
|
+
| strict mode violation | add `nth`, `exact: true`, or specific `name` in selectors.yaml |
|
|
12
|
+
| Element is not an input | change `type` or `value` in selectors.yaml |
|
|
13
|
+
| Timeout waiting for selector | fix `type`/`value`/`name` to match element in selectors.yaml |
|
|
14
|
+
| toBeVisible Timeout | verify accessible name or use `testid` in selectors.yaml |
|
|
15
|
+
| toHaveText mismatch | fix expected text in `test-data.yaml` — OR if element is an input (field/textarea/search/dropdown), change Gherkin type so compiler uses `toHaveValue` instead |
|
|
16
|
+
| toHaveValue mismatch | fix expected value in `test-data.yaml` — this is for input elements (field, textarea, search, dropdown, slider, date-picker) |
|
|
17
|
+
| toContainText mismatch | fix expected partial text in `test-data.yaml` |
|
|
18
|
+
| Navigation failed | fix page `value` path in selectors.yaml |
|
|
19
|
+
| not a select | set `variant: 'custom'` in selectors.yaml |
|
|
20
|
+
| Frame not found | fix `frame` value in selectors.yaml |
|
|
21
|
+
| dialog was dismissed | alert step is AFTER the trigger — move `click [OK] alert` BEFORE the button click that opens the dialog |
|
|
22
|
+
| page.once('dialog') never fired | no browser dialog appeared — check if the action actually triggers a `window.alert/confirm/prompt`, not a CSS modal |
|
|
23
|
+
|
|
24
|
+
### Assertion type mismatch (toHaveText vs toHaveValue)
|
|
25
|
+
|
|
26
|
+
Sungen picks the assertion based on element type:
|
|
27
|
+
- **Input types** (`field`, `textarea`, `search`, `dropdown`, `slider`, `date-picker`) → `toHaveValue()`
|
|
28
|
+
- **Text types** (everything else: `message`, `header`, `label`, `row`, etc.) → `toHaveText()`
|
|
29
|
+
- **Partial match** (uses `contains` keyword) → `toContainText()`
|
|
30
|
+
|
|
31
|
+
If you see `toHaveText` failing on an input element, the Gherkin step has the wrong target type. Fix: change the target type in the `.feature` file (e.g., `see [Email] message with {{v}}` → `see [Email] field with {{v}}`).
|
|
19
32
|
|
|
20
33
|
## Auth Errors → Fix
|
|
21
34
|
|