@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.
Files changed (57) hide show
  1. package/README.md +63 -34
  2. package/dist/cli/index.js +1 -1
  3. package/dist/generators/gherkin-parser/index.js +1 -1
  4. package/dist/generators/gherkin-parser/index.js.map +1 -1
  5. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
  6. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
  7. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
  8. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
  9. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
  10. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  11. package/dist/generators/test-generator/patterns/assertion-patterns.js +83 -57
  12. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  13. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +1 -1
  14. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  15. package/dist/generators/test-generator/patterns/interaction-patterns.js +56 -1
  16. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  17. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  18. package/dist/generators/test-generator/utils/selector-resolver.js +16 -0
  19. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  20. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -4
  21. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
  22. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +148 -21
  23. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +51 -11
  24. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
  25. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +1 -1
  26. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +1 -1
  27. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +12 -5
  28. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
  29. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +148 -21
  30. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +51 -11
  31. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
  32. package/docs/gherkin standards/gherkin-core-standard.md +163 -160
  33. package/docs/gherkin standards/gherkin-core-standard.vi.md +290 -404
  34. package/docs/gherkin-dictionary.md +71 -16
  35. package/package.json +1 -1
  36. package/src/cli/index.ts +1 -1
  37. package/src/generators/gherkin-parser/index.ts +1 -1
  38. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
  39. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
  40. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
  41. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
  42. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
  43. package/src/generators/test-generator/patterns/assertion-patterns.ts +93 -65
  44. package/src/generators/test-generator/patterns/interaction-patterns.ts +58 -1
  45. package/src/generators/test-generator/utils/selector-resolver.ts +16 -0
  46. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -4
  47. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
  48. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +148 -21
  49. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +51 -11
  50. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
  51. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +1 -1
  52. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +1 -1
  53. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +12 -5
  54. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
  55. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +148 -21
  56. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +51 -11
  57. 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"|"menu"
197
- | "menuitem"|"tab"|"tabpanel"|"list"|"listitem"|"table"|"row"
198
- | "cell"|"column"|"columnheader"|"region"|"section"|"nav"
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"|"toggle"
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': code = expect(locator).toBeVisible() with text check
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: 'Hệ thống giải thưởng SAA 2025' })).toBeVisible();
315
+ // see heading with value → toHaveText (exact match)
316
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toHaveText('Welcome back, Admin');
311
317
 
312
- // see text with exact match
313
- await expect(page.getByText('5.000.000 VNĐ', { exact: true })).toBeVisible();
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
- | `dialog` / `modal` | `dialog` | `getByRole('dialog')` |
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` | `alert` | `getByRole('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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli/index.ts CHANGED
@@ -16,7 +16,7 @@ async function main() {
16
16
  program
17
17
  .name('sungen')
18
18
  .description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
19
- .version('2.2.1');
19
+ .version('2.2.3');
20
20
 
21
21
  // Global options
22
22
  program
@@ -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
- // Label-value pattern: "User see [X] label with {{Y}}"
239
- // Uses regex getByText to match container with both label and value text
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 resolved = context.selectorResolver.resolveSelector(
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 (resolved.value !== undefined && resolved.value.trim() === '') {
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
- // Resolve selector from YAML with feature context
268
- let resolved: any = {};
269
- try {
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 value ${step.dataRef}`,
328
+ comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
292
329
  };
293
330
  }
294
331
 
295
- // Check if it's a role-based selector with data
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: isImgRole ? (hasName ? resolved.name : dataValue) : (hasName ? resolved.name : undefined),
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 value ${step.dataRef}`,
343
+ comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
321
344
  };
322
345
  }
323
346
 
324
- const selectorValue = resolved.value || '';
325
-
326
- // If selector has a value in YAML, combine with variable
327
- // Otherwise use variable-only
328
- if (selectorValue && selectorValue.trim()) {
329
- // Has selector value - combine both
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} with value ${step.dataRef}`,
356
+ comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
340
357
  };
341
- } else {
342
- // Empty selector - use variable only
343
- const code = context.templateEngine.renderStep('visible-with-variable-assertion', {
344
- selectorRef: step.selectorRef,
345
- selectorValue: resolved.value || '',
346
- dataValue,
347
- nth: resolved.nth,
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} visible with ${step.dataRef}`,
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. Run: `npx playwright test specs/screens/<screen>/<screen>.spec.ts`
22
- 5. If fail → fix selectors/test-data per `sungen-selector-fix` + `sungen-error-mapping` skills, retry (max 5).
23
- 6. After 5 fails ask user about direct `.spec.ts` fix.
24
- 7. Show: pass/fail, attempt count, files changed.
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 in `selectors.yaml` |
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
- | Navigation failed | fix page `value` path |
17
- | not a select | set `variant: 'custom'` |
18
- | Frame not found | fix `frame` value |
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