@sun-asterisk/sungen 2.2.3 → 2.3.0

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 (78) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/commands/update.d.ts +3 -0
  3. package/dist/cli/commands/update.d.ts.map +1 -0
  4. package/dist/cli/commands/update.js +21 -0
  5. package/dist/cli/commands/update.js.map +1 -0
  6. package/dist/cli/index.js +3 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  9. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  10. package/dist/generators/gherkin-parser/index.js +16 -2
  11. package/dist/generators/gherkin-parser/index.js.map +1 -1
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
  15. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  16. package/dist/generators/test-generator/patterns/assertion-patterns.js +12 -0
  17. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  18. package/dist/generators/test-generator/patterns/index.d.ts +9 -0
  19. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  20. package/dist/generators/test-generator/patterns/index.js +32 -0
  21. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  22. package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +1 -1
  23. package/dist/generators/test-generator/patterns/table-patterns.js +8 -5
  24. package/dist/generators/test-generator/patterns/table-patterns.js.map +1 -1
  25. package/dist/orchestrator/ai-rules-updater.d.ts +13 -0
  26. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -0
  27. package/dist/orchestrator/ai-rules-updater.js +157 -0
  28. package/dist/orchestrator/ai-rules-updater.js.map +1 -0
  29. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  30. package/dist/orchestrator/project-initializer.js +2 -27
  31. package/dist/orchestrator/project-initializer.js.map +1 -1
  32. package/dist/orchestrator/screen-manager.d.ts +1 -0
  33. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  34. package/dist/orchestrator/screen-manager.js +70 -3
  35. package/dist/orchestrator/screen-manager.js.map +1 -1
  36. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
  37. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
  38. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +9 -11
  39. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +27 -8
  40. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +91 -25
  41. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
  42. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
  43. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
  44. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +9 -11
  45. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +27 -8
  46. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +72 -31
  47. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
  48. package/dist/orchestrator/templates/readme.md +13 -8
  49. package/package.json +1 -1
  50. package/src/cli/commands/update.ts +18 -0
  51. package/src/cli/index.ts +3 -1
  52. package/src/generators/gherkin-parser/index.ts +19 -2
  53. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
  54. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
  55. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
  56. package/src/generators/test-generator/patterns/assertion-patterns.ts +13 -0
  57. package/src/generators/test-generator/patterns/index.ts +41 -0
  58. package/src/generators/test-generator/patterns/table-patterns.ts +8 -5
  59. package/src/orchestrator/ai-rules-updater.ts +139 -0
  60. package/src/orchestrator/project-initializer.ts +2 -32
  61. package/src/orchestrator/screen-manager.ts +72 -3
  62. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
  63. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
  64. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +9 -11
  65. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +27 -8
  66. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +91 -25
  67. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
  68. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
  69. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
  70. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +9 -11
  71. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +27 -8
  72. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +72 -31
  73. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
  74. package/src/orchestrator/templates/readme.md +13 -8
  75. package/docs/gherkin standards/gherkin-core-standard.md +0 -431
  76. package/docs/gherkin standards/gherkin-core-standard.vi.md +0 -399
  77. package/docs/gherkin-dictionary.md +0 -1126
  78. package/docs/makeauth.md +0 -225
@@ -1,1126 +0,0 @@
1
- # Sungen Gherkin Dictionary v2
2
-
3
- Complete mapping from Gherkin steps to Playwright code.
4
- This is the compiler rulebook — deterministic, no AI needed at compile time.
5
-
6
- ---
7
-
8
- ## Project Structure
9
-
10
- ### Screen directory (multi-file)
11
-
12
- ```
13
- qa/screens/<screen-name>/
14
- ├── features/
15
- │ ├── <screen>.feature # main scenarios (page load, content)
16
- │ ├── <screen>-navigation.feature # navigation scenarios
17
- │ ├── <screen>-<feature>.feature # additional feature-specific scenarios
18
- │ └── ...
19
- ├── selectors/
20
- │ ├── <screen>.yaml # shared selectors (header, nav, page entry)
21
- │ ├── <screen>-<feature>.yaml # feature-specific selectors
22
- │ ├── <screen>.override.yaml # manual overrides (never auto-modified)
23
- │ └── ...
24
- └── test-data/
25
- ├── <screen>.yaml # shared test data
26
- ├── <screen>-<feature>.yaml # feature-specific test data
27
- ├── <screen>.override.yaml # manual overrides (never auto-modified)
28
- └── ...
29
- ```
30
-
31
- ### Merge rules at compile time
32
-
33
- 1. **Selectors:** All `.yaml` in `selectors/` are merged into one map. `.override.yaml` takes precedence.
34
- 2. **Test-data:** All `.yaml` in `test-data/` are merged. `.override.yaml` takes precedence.
35
- 3. **Features:** Each `.feature` compiles to its own `.spec.ts`:
36
-
37
- ```
38
- features/awards.feature → specs/generated/awards/awards.spec.ts
39
- features/awards-navigation.feature → specs/generated/awards/awards-navigation.spec.ts
40
- features/awards-table.feature → specs/generated/awards/awards-table.spec.ts
41
- ```
42
-
43
- ### CLI commands
44
-
45
- ```bash
46
- sungen init # scaffold project + AI rules
47
- sungen add --screen awards --path /awards # create screen with main feature
48
- sungen add --screen awards --feature navigation # add feature file to existing screen
49
- sungen makeauth <role> # capture browser auth state
50
- sungen generate --screen awards # compile all features → .spec.ts
51
- sungen generate --all # compile all screens
52
- ```
53
-
54
- ### Feature file template
55
-
56
- Every `.feature` must include `Path:` so AI knows which URL to visit:
57
-
58
- ```gherkin
59
- Feature: awards Screen
60
-
61
- As a user
62
- I want to interact with the awards screen
63
- So that I can accomplish my tasks
64
- Path: /awards
65
- ```
66
-
67
- ### AI rules files
68
-
69
- Generated by `sungen init`:
70
-
71
- ```
72
- .github/copilot-instructions.md # GitHub Copilot
73
- CLAUDE.md # Claude Code
74
- ```
75
-
76
- Both contain this dictionary, YAML schema, and instructions for AI to generate/fix files.
77
-
78
- ---
79
-
80
- ## Selector YAML Schema v2
81
-
82
- Every `[element]` in Gherkin resolves to a selector entry:
83
-
84
- ```yaml
85
- element.key:
86
- # === Core ===
87
- type: 'testid'|'role'|'text'|'label'|'placeholder'|'locator'|'page'|'upload'|'frame'
88
- value: 'button' # role name, testid value, CSS selector, etc.
89
- name: 'Submit' # accessible name (for role-based)
90
- nth: 0 # element index when multiple matches
91
-
92
- # === Disambiguation ===
93
- exact: true|false # exact name/text match (default: false)
94
- scope: 'desktop navigation' # parent landmark aria-label to scope within
95
- match: 'exact'|'partial' # for getByText matching (default: partial)
96
-
97
- # === Complex Elements ===
98
- variant: 'native'|'custom'|'dragdrop' # dropdown / upload type
99
- trigger: 'Upload Photo' # visible button that triggers hidden input
100
- frame: '#payment-iframe' # iframe selector for cross-frame elements
101
- contenteditable: true # rich text editor (contenteditable div)
102
-
103
- # === Table Definition ===
104
- columns: # only for type: 'table'
105
- name:
106
- index: 0
107
- header: 'Name'
108
- status:
109
- index: 2
110
- header: 'Status'
111
- elements: # interactive elements inside column cells
112
- edit:
113
- type: 'role'
114
- value: 'button'
115
- name: 'Edit'
116
- pagination: # table pagination selectors
117
- next: '[data-testid="next-page"]'
118
- prev: '[data-testid="prev-page"]'
119
- pageSize: '[data-testid="page-size"]'
120
-
121
- # === Assertion Helpers ===
122
- attribute: 'src' # attribute to check (for "has" assertions)
123
- pattern: '/.*\\.png$/' # regex for attribute value
124
- ```
125
-
126
- ### Locator Resolution Priority
127
-
128
- ```
129
- 1. data-testid → page.getByTestId(value)
130
- 2. role + name → page.getByRole(value, { name, exact })
131
- 3. label → page.getByLabel(value, { exact })
132
- 4. placeholder → page.getByPlaceholder(value, { exact })
133
- 5. text → page.getByText(value, { exact: match=='exact' })
134
- 6. locator → page.locator(value)
135
- 7. page → page.goto(value)
136
- 8. upload → page.locator(value).setInputFiles()
137
- 9. frame → page.frameLocator(value)
138
- ```
139
-
140
- ---
141
-
142
- ## Formal Grammar
143
-
144
- ```
145
- STEP := ACTOR VERB_PHRASE
146
- ACTOR := "User"
147
-
148
- VERB_PHRASE := ACTION_VP
149
- | WAIT_VP
150
- | NAV_VP
151
- | SCROLL_VP
152
- | SWITCH_VP
153
- | TABLE_VP
154
-
155
- # ─── Basic actions ────────────────────────────────────────────
156
- ACTION_VP := ACTION TARGET_EXPR (DATA_EXPR)? (STATE_EXPR)? # ①②③④
157
- | ACTION TARGET_EXPR "to" TARGET_EXPR # ⑤ drag
158
- | ACTION KEY "key" ("on" TARGET_EXPR)? # ⑥⑦ keyboard
159
- | ACTION TARGET_EXPR ("has"|"contains") DATA_EXPR # ⑫⑰ has/contains
160
- | ACTION TARGET_EXPR "has" TARGET_EXPR # ⑫ has column
161
- | "double click" TARGET_EXPR # double click
162
-
163
- # ─── Wait ─────────────────────────────────────────────────────
164
- WAIT_VP := "wait for" TIMEOUT # ⑧
165
- | "wait for" TARGET_EXPR (DATA_EXPR)? (STATE_EXPR)? # ⑨
166
-
167
- # ─── Navigation ───────────────────────────────────────────────
168
- NAV_VP := "is on" TARGET_EXPR "page" # ⑯
169
- | "navigate to" TARGET_EXPR "page" (DATA_EXPR)? # ⑯
170
-
171
- # ─── Scroll ───────────────────────────────────────────────────
172
- SCROLL_VP := "scroll to" TARGET_EXPR # ⑩
173
-
174
- # ─── Frame switch ─────────────────────────────────────────────
175
- SWITCH_VP := "switch to" TARGET_EXPR "frame" # ⑪
176
-
177
- # ─── Table ────────────────────────────────────────────────────
178
- TABLE_VP := "see" TARGET_EXPR "table" TABLE_EXPR # ⑬⑭
179
- | ACTION TARGET_EXPR "in" TARGET_EXPR "table"
180
- TABLE_ROW_EXPR # ⑮
181
-
182
- TABLE_EXPR := TABLE_ROW_EXPR
183
- | "has" DATA_EXPR "rows" # row count
184
- | "has" TARGET_EXPR "column" # column exists
185
- | "is empty" # empty state
186
- | "empty message" DATA_EXPR # empty message
187
-
188
- TABLE_ROW_EXPR := "row" (NUMBER)? (DATA_EXPR)? (TABLE_COL_EXPR)?
189
- | "row" (DATA_EXPR)? ELEMENT_TYPE # row checkbox
190
-
191
- TABLE_COL_EXPR := ("has")? TARGET_EXPR ("cell")? (DATA_EXPR)?
192
-
193
- # ─── Terminals ────────────────────────────────────────────────
194
- TARGET_EXPR := "[" IDENTIFIER "]" (ELEMENT_TYPE)?
195
- ELEMENT_TYPE := "button"|"link"|"field"|"heading"|"text"|"image"|"checkbox"
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"
204
- DATA_EXPR := "with" "{{" IDENTIFIER "}}"
205
- STATE_EXPR := "is" STATE
206
- STATE := "hidden"|"visible"|"disabled"|"enabled"|"checked"
207
- | "unchecked"|"focused"|"empty"|"loading"|"selected"
208
- | "sorted ascending"|"sorted descending"
209
- ACTION := "click"|"fill"|"select"|"check"|"uncheck"
210
- | "upload"|"hover"|"drag"|"clear"|"see"|"press"|"expand"
211
- | "collapse"|"double click"|"toggle"
212
- KEY := "Enter"|"Escape"|"Tab"|"Backspace"|"Delete"|"Space"
213
- | "ArrowUp"|"ArrowDown"|"ArrowLeft"|"ArrowRight"
214
- | "Home"|"End"|"PageUp"|"PageDown"
215
- TIMEOUT := NUMBER ("seconds"|"ms")
216
- NUMBER := [0-9]+
217
- IDENTIFIER := [a-zA-Z0-9_ ./*:-]+
218
- ```
219
-
220
- ---
221
-
222
- ## 17 Pattern Shapes
223
-
224
- ### ① Simple action: `action [Target] type`
225
-
226
- ```gherkin
227
- When User click [Submit] button
228
- When User check [Remember me] checkbox
229
- When User uncheck [Newsletter] checkbox
230
- When User toggle [Dark mode] switch
231
- When User clear [Search] field
232
- When User hover [Info] icon
233
- When User expand [Row 1] row
234
- When User collapse [Row 1] row
235
- ```
236
-
237
- **Compiler rule:**
238
- ```
239
- locator = resolve([Target], type)
240
- code = locator.{action}()
241
- ```
242
-
243
- **Playwright output:**
244
- ```typescript
245
- // click button
246
- await page.getByRole('button', { name: 'Submit', exact: true }).click();
247
-
248
- // check checkbox
249
- await page.getByRole('checkbox', { name: 'Remember me' }).check();
250
-
251
- // uncheck checkbox
252
- await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck();
253
-
254
- // toggle switch
255
- await page.getByRole('switch', { name: 'Dark mode' }).click();
256
-
257
- // clear field
258
- await page.getByRole('textbox', { name: 'Search' }).clear();
259
-
260
- // hover
261
- await page.getByRole('img', { name: 'Info' }).hover();
262
- ```
263
-
264
- ---
265
-
266
- ### ② Action with data: `action [Target] type with {{Value}}`
267
-
268
- ```gherkin
269
- When User fill [Email] field with {{valid_email}}
270
- When User fill [Message] textarea with {{message_body}}
271
- When User select [Country] dropdown with {{country_name}}
272
- When User upload [Avatar] file with {{avatar_path}}
273
- When User click [Teammate] button with {{teammate_name}}
274
- Then User see [Title] heading with {{page_title}}
275
- Then User see [Price] text with {{item_price}}
276
- ```
277
-
278
- **Compiler rule:**
279
- ```
280
- locator = resolve([Target], type)
281
- value = resolveData({{Value}})
282
- if action == 'fill': code = locator.fill(value)
283
- if action == 'select': code = locator.selectOption(value) or click+click
284
- if action == 'upload': code = locator.setInputFiles(value)
285
- if action == 'click': code = page.getByText(value).click() or locator.filter({hasText}).click()
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)
291
- ```
292
-
293
- **Playwright output:**
294
- ```typescript
295
- // fill text field
296
- await page.getByRole('textbox', { name: 'Email', exact: true }).fill('test@example.com');
297
-
298
- // fill contenteditable
299
- await page.locator('[contenteditable="true"]').click();
300
- await page.locator('[contenteditable="true"]').pressSequentially('Hello world!');
301
-
302
- // select native dropdown
303
- await page.getByRole('combobox', { name: 'Country' }).selectOption('Vietnam');
304
-
305
- // select custom dropdown
306
- await page.getByRole('combobox', { name: 'Country' }).click();
307
- await page.getByRole('option', { name: 'Vietnam' }).click();
308
-
309
- // upload file
310
- await page.getByLabel('Avatar').setInputFiles('specs/storage/avatar.png');
311
-
312
- // click with text filter
313
- await page.getByText('Nguyễn Thanh Tùng').click();
314
-
315
- // see heading with value → toHaveText (exact match)
316
- await expect(page.getByRole('heading', { name: 'Welcome' })).toHaveText('Welcome back, Admin');
317
-
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');
323
- ```
324
-
325
- ---
326
-
327
- ### ③ State assertion: `action [Target] type is state`
328
-
329
- ```gherkin
330
- Then User see [Submit] button is disabled
331
- Then User see [Submit] button is enabled
332
- Then User see [Modal] dialog is hidden
333
- Then User see [Welcome] heading is visible
334
- Then User see [Remember me] checkbox is checked
335
- Then User see [Newsletter] checkbox is unchecked
336
- Then User see [Email] field is focused
337
- Then User see [Search] field is empty
338
- ```
339
-
340
- **Compiler rule:**
341
- ```
342
- locator = resolve([Target], type)
343
- switch(state):
344
- hidden → expect(locator).toBeHidden()
345
- visible → expect(locator).toBeVisible()
346
- disabled → expect(locator).toBeDisabled()
347
- enabled → expect(locator).toBeEnabled()
348
- checked → expect(locator).toBeChecked()
349
- unchecked → expect(locator).not.toBeChecked()
350
- focused → expect(locator).toBeFocused()
351
- empty → expect(locator).toHaveText('')
352
- ```
353
-
354
- **Playwright output:**
355
- ```typescript
356
- await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
357
- await expect(page.getByRole('dialog')).toBeHidden();
358
- await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();
359
- await expect(page.getByRole('textbox', { name: 'Email' })).toBeFocused();
360
- await expect(page.getByRole('textbox', { name: 'Search' })).toHaveText('');
361
- ```
362
-
363
- ---
364
-
365
- ### ④ State with data: `action [Target] type with {{Value}} is state`
366
-
367
- ```gherkin
368
- Then User see [Panel] dialog with {{dialog_title}} is hidden
369
- Then User see [Email] field with {{error_message}} is disabled
370
- ```
371
-
372
- **Compiler rule:**
373
- ```
374
- locator = resolve([Target], type).filter({ hasText: value })
375
- apply state assertion on locator
376
- ```
377
-
378
- **Playwright output:**
379
- ```typescript
380
- await expect(
381
- page.getByRole('dialog').filter({ hasText: 'Confirm Delete' })
382
- ).toBeHidden();
383
- ```
384
-
385
- ---
386
-
387
- ### ⑤ Two targets: `action [Source] to [Destination]`
388
-
389
- ```gherkin
390
- When User drag [Card A] to [Column B]
391
- ```
392
-
393
- **Compiler rule:**
394
- ```
395
- source = resolve([Source])
396
- dest = resolve([Destination])
397
- code = source.dragTo(dest)
398
- ```
399
-
400
- **Playwright output:**
401
- ```typescript
402
- await page.getByTestId('card-a').dragTo(page.getByTestId('column-b'));
403
- ```
404
-
405
- ---
406
-
407
- ### ⑥ Global key: `action Key key`
408
-
409
- ```gherkin
410
- When User press Escape key
411
- When User press Tab key
412
- When User press Space key
413
- ```
414
-
415
- **Compiler rule:**
416
- ```
417
- code = page.keyboard.press(Key)
418
- ```
419
-
420
- **Playwright output:**
421
- ```typescript
422
- await page.keyboard.press('Escape');
423
- ```
424
-
425
- ---
426
-
427
- ### ⑦ Key on target: `action Key on [Target] type`
428
-
429
- ```gherkin
430
- When User press Enter on [Search] field
431
- When User press Escape on [Modal] dialog
432
- ```
433
-
434
- **Compiler rule:**
435
- ```
436
- locator = resolve([Target], type)
437
- code = locator.press(Key)
438
- ```
439
-
440
- **Playwright output:**
441
- ```typescript
442
- await page.getByRole('textbox', { name: 'Search' }).press('Enter');
443
- ```
444
-
445
- ---
446
-
447
- ### ⑧ Wait timeout: `wait for N seconds`
448
-
449
- ```gherkin
450
- When User wait for 3 seconds
451
- When User wait for 500 ms
452
- ```
453
-
454
- **Compiler rule:**
455
- ```
456
- code = page.waitForTimeout(N * multiplier)
457
- ```
458
-
459
- **Playwright output:**
460
- ```typescript
461
- await page.waitForTimeout(3000);
462
- ```
463
-
464
- ---
465
-
466
- ### ⑨ Wait for element: `wait for [Target] type (with {{Value}}) (is state)`
467
-
468
- ```gherkin
469
- When User wait for [Modal] dialog
470
- When User wait for [Loading] spinner is hidden
471
- When User wait for [Dialog] dialog with {{dialog_title}}
472
- When User wait for [Dialog] dialog with {{dialog_title}} is hidden
473
- When User wait for [dashboard] page
474
- ```
475
-
476
- **Compiler rule:**
477
- ```
478
- locator = resolve([Target], type)
479
- if DATA_EXPR: locator = locator.filter({ hasText: value })
480
- state = STATE_EXPR ? mapState(state) : 'visible'
481
- if type == 'page': code = page.waitForURL(value)
482
- else: code = locator.waitFor({ state })
483
- ```
484
-
485
- **Playwright output:**
486
- ```typescript
487
- // wait for dialog visible
488
- await page.getByRole('dialog').waitFor({ state: 'visible' });
489
-
490
- // wait for spinner hidden
491
- await page.getByRole('progressbar').waitFor({ state: 'hidden' });
492
-
493
- // wait for dialog with title
494
- await page.getByRole('dialog').filter({ hasText: 'Confirm' }).waitFor({ state: 'visible' });
495
-
496
- // wait for dialog with title hidden
497
- await page.getByRole('dialog').filter({ hasText: 'Confirm' }).waitFor({ state: 'hidden' });
498
-
499
- // wait for page
500
- await page.waitForURL(/\/dashboard/);
501
- ```
502
-
503
- ---
504
-
505
- ### ⑩ Scroll: `scroll to [Target] type`
506
-
507
- ```gherkin
508
- When User scroll to [Footer] section
509
- When User scroll to [Comments] region
510
- ```
511
-
512
- **Compiler rule:**
513
- ```
514
- locator = resolve([Target], type)
515
- code = locator.scrollIntoViewIfNeeded()
516
- ```
517
-
518
- **Playwright output:**
519
- ```typescript
520
- await page.getByRole('contentinfo').scrollIntoViewIfNeeded();
521
- ```
522
-
523
- ---
524
-
525
- ### ⑪ Frame switch: `switch to [Target] frame`
526
-
527
- ```gherkin
528
- When User switch to [Payment] frame
529
- ```
530
-
531
- After this step, all subsequent locators are scoped to the frame until the next `switch to [main] frame` or end of scenario.
532
-
533
- **Selector YAML:**
534
- ```yaml
535
- payment:
536
- type: 'frame'
537
- value: '#payment-iframe'
538
- ```
539
-
540
- **Compiler rule:**
541
- ```
542
- frameContext = page.frameLocator(value)
543
- // All subsequent locators use frameContext instead of page
544
- ```
545
-
546
- **Playwright output:**
547
- ```typescript
548
- // Subsequent steps use frame scope:
549
- const frame = page.frameLocator('#payment-iframe');
550
- await frame.getByRole('textbox', { name: 'Card number' }).fill('4242...');
551
- ```
552
-
553
- ---
554
-
555
- ### ⑫ Has (count/attribute): `action [Target] type has {{Value}}`
556
-
557
- ```gherkin
558
- Then User see [Avatar] image has {{avatar_url}}
559
- Then User see [Link] link has {{href_value}}
560
- ```
561
-
562
- **Selector YAML:**
563
- ```yaml
564
- avatar:
565
- type: 'role'
566
- value: 'img'
567
- name: 'Avatar'
568
- attribute: 'src'
569
- ```
570
-
571
- **Compiler rule:**
572
- ```
573
- locator = resolve([Target], type)
574
- if selector.attribute:
575
- code = expect(locator).toHaveAttribute(attribute, value)
576
- ```
577
-
578
- **Playwright output:**
579
- ```typescript
580
- // image has src
581
- await expect(page.getByRole('img', { name: 'Avatar' })).toHaveAttribute('src', /avatar\.png/);
582
-
583
- // link has href
584
- await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');
585
- ```
586
-
587
- ---
588
-
589
- ### ⑬ Table cell by row index: `see [Table] table row N [Col] cell with {{Value}}`
590
-
591
- ```gherkin
592
- Then User see [Users] table row 1 [Name] cell with {{first_name}}
593
- Then User see [Users] table row 1 [Email] cell with {{first_email}}
594
- Then User see [Users] table row 3 [Status] cell with {{third_status}}
595
- ```
596
-
597
- **Selector YAML:**
598
- ```yaml
599
- users:
600
- type: 'table'
601
- value: 'Users'
602
- columns:
603
- name:
604
- index: 0
605
- header: 'Name'
606
- email:
607
- index: 1
608
- header: 'Email'
609
- status:
610
- index: 2
611
- header: 'Status'
612
- ```
613
-
614
- **Compiler rule:**
615
- ```
616
- table = page.getByRole('table', { name: selector.value })
617
- row = table.getByRole('row').nth(N) // N+1 to skip header
618
- cell = row.getByRole('cell').nth(columns[Col].index)
619
- code = expect(cell).toHaveText(value)
620
- ```
621
-
622
- **Playwright output:**
623
- ```typescript
624
- const table = page.getByRole('table', { name: 'Users' });
625
- const row = table.getByRole('row').nth(1); // skip header
626
- const cell = row.getByRole('cell').nth(0); // Name column
627
- await expect(cell).toHaveText('John Doe');
628
- ```
629
-
630
- ---
631
-
632
- ### ⑭ Table cell by row filter: `see [Table] table row with {{Filter}} has [Col] with {{Value}}`
633
-
634
- ```gherkin
635
- Then User see [Users] table row with {{user_name}} has [Status] with {{expected_status}}
636
- Then User see [Users] table row with {{user_name}} has [Email] with {{expected_email}}
637
- ```
638
-
639
- **Compiler rule:**
640
- ```
641
- table = page.getByRole('table', { name: selector.value })
642
- row = table.getByRole('row').filter({ hasText: filterValue })
643
- cell = row.getByRole('cell').nth(columns[Col].index)
644
- code = expect(cell).toHaveText(value)
645
- ```
646
-
647
- **Playwright output:**
648
- ```typescript
649
- const table = page.getByRole('table', { name: 'Users' });
650
- const row = table.getByRole('row').filter({ hasText: 'John Doe' });
651
- const cell = row.getByRole('cell').nth(2); // Status column
652
- await expect(cell).toHaveText('Active');
653
- ```
654
-
655
- ---
656
-
657
- ### ⑮ Action in table row: `action [Element] in [Table] table row with {{Filter}}`
658
-
659
- ```gherkin
660
- When User click [Edit] in [Users] table row with {{user_name}}
661
- When User click [Delete] in [Users] table row with {{user_name}}
662
- When User check [Users] table row with {{user_name}} checkbox
663
- When User check [Users] table select all checkbox
664
- ```
665
-
666
- **Compiler rule:**
667
- ```
668
- table = page.getByRole('table', { name: tableSelector.value })
669
- row = table.getByRole('row').filter({ hasText: filterValue })
670
- element = row.resolve([Element])
671
- code = element.{action}()
672
- ```
673
-
674
- **Playwright output:**
675
- ```typescript
676
- // click button in row
677
- const table = page.getByRole('table', { name: 'Users' });
678
- const row = table.getByRole('row').filter({ hasText: 'John Doe' });
679
- await row.getByRole('button', { name: 'Edit' }).click();
680
-
681
- // check row checkbox
682
- const row = table.getByRole('row').filter({ hasText: 'John Doe' });
683
- await row.getByRole('checkbox').check();
684
-
685
- // select all
686
- await table.getByRole('checkbox', { name: 'Select all' }).check();
687
- ```
688
-
689
- ---
690
-
691
- ### ⑯ Navigation: `is on [Target] page` / `navigate to [Target] page`
692
-
693
- ```gherkin
694
- Given User is on [login] page
695
- Given User is on [awards] page
696
- When User navigate to [profile] page with {{user_id}}
697
- Then User see [dashboard] page
698
- ```
699
-
700
- **Selector YAML:**
701
- ```yaml
702
- login:
703
- type: 'page'
704
- value: '/login'
705
-
706
- profile:
707
- type: 'page'
708
- value: '/users/{{user_id}}/profile'
709
- ```
710
-
711
- **Compiler rule:**
712
- ```
713
- if 'is on' or 'navigate to':
714
- code = page.goto(value, { waitUntil: 'networkidle' })
715
- if 'see':
716
- code = expect(page).toHaveURL(regex_from_value)
717
- ```
718
-
719
- **Playwright output:**
720
- ```typescript
721
- // Given: navigate
722
- await page.goto('/login', { waitUntil: 'networkidle' });
723
-
724
- // navigate with data interpolation
725
- await page.goto('/users/12345/profile', { waitUntil: 'networkidle' });
726
-
727
- // Then: assert URL
728
- await page.waitForLoadState('networkidle');
729
- await expect(page).toHaveURL(/\/dashboard/);
730
- ```
731
-
732
- ---
733
-
734
- ### ⑰ Text assertions: `see [Target] type contains/has text {{Value}}`
735
-
736
- ```gherkin
737
- Then User see [Message] text contains {{partial_text}}
738
- Then User see [Counter] text has text {{exact_count}}
739
- Then User see [Description] text has text {{full_description}}
740
- ```
741
-
742
- **Compiler rule:**
743
- ```
744
- locator = resolve([Target], type)
745
- if 'contains': code = expect(locator).toContainText(value)
746
- if 'has text': code = expect(locator).toHaveText(value)
747
- ```
748
-
749
- **Playwright output:**
750
- ```typescript
751
- // contains (partial match)
752
- await expect(page.getByText('Message')).toContainText('successfully');
753
-
754
- // has text (exact match)
755
- await expect(locator).toHaveText('42');
756
- ```
757
-
758
- ---
759
-
760
- ## Table-Specific Patterns (extended)
761
-
762
- ### Row count
763
-
764
- ```gherkin
765
- Then User see [Users] table has {{row_count}} rows
766
- ```
767
-
768
- ```typescript
769
- const table = page.getByRole('table', { name: 'Users' });
770
- // subtract 1 for header row, or use tbody rows
771
- await expect(table.locator('tbody').getByRole('row')).toHaveCount(10);
772
- ```
773
-
774
- ### Column exists
775
-
776
- ```gherkin
777
- Then User see [Users] table has [Email] column
778
- ```
779
-
780
- ```typescript
781
- await expect(
782
- page.getByRole('table', { name: 'Users' }).getByRole('columnheader', { name: 'Email' })
783
- ).toBeVisible();
784
- ```
785
-
786
- ### Empty state
787
-
788
- ```gherkin
789
- Then User see [Users] table is empty
790
- Then User see [Users] table empty message with {{no_data_text}}
791
- ```
792
-
793
- ```typescript
794
- // empty: only header row
795
- await expect(
796
- page.getByRole('table', { name: 'Users' }).locator('tbody').getByRole('row')
797
- ).toHaveCount(0);
798
-
799
- // empty message
800
- await expect(page.getByText('No data available')).toBeVisible();
801
- ```
802
-
803
- ### Sort column
804
-
805
- ```gherkin
806
- When User click [Name] columnheader
807
- Then User see [Name] columnheader is sorted ascending
808
- ```
809
-
810
- ```typescript
811
- await page.getByRole('columnheader', { name: 'Name' }).click();
812
- await expect(
813
- page.getByRole('columnheader', { name: 'Name' })
814
- ).toHaveAttribute('aria-sort', 'ascending');
815
- ```
816
-
817
- ### Pagination
818
-
819
- ```gherkin
820
- When User click [Users] table next page
821
- When User select [Users] table page size with {{page_size}}
822
- Then User see [Users] table page info with {{page_info}}
823
- ```
824
-
825
- Uses `pagination` from selector YAML:
826
- ```typescript
827
- await page.locator('[data-testid="next-page"]').click();
828
- await page.locator('[data-testid="page-size"]').selectOption('25');
829
- await expect(page.locator('[data-testid="page-info"]')).toHaveText('Showing 1-25 of 100');
830
- ```
831
-
832
- ### Row expansion
833
-
834
- ```gherkin
835
- When User expand [Users] table row with {{user_name}}
836
- When User collapse [Users] table row with {{user_name}}
837
- ```
838
-
839
- ```typescript
840
- const row = table.getByRole('row').filter({ hasText: 'John' });
841
- await row.getByRole('button', { name: 'Expand' }).click();
842
- ```
843
-
844
- ---
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
-
889
- ## Dialog Scope
890
-
891
- When a dialog is opened, subsequent steps are scoped within it:
892
-
893
- ```gherkin
894
- Then User see [panel] dialog with {{dialog_title}}
895
- # All steps below are scoped to this dialog:
896
- When User fill [Email] field with {{email}}
897
- When User click [Submit] button
898
- When User wait for [panel] dialog with {{dialog_title}} is hidden
899
- # Scope ends
900
- ```
901
-
902
- **Compiler rule:** Track dialog context. When dialog is visible, wrap all locators:
903
-
904
- ```typescript
905
- const dialog = page.getByRole('dialog').filter({ hasText: 'Gửi lời cảm ơn' });
906
- await expect(dialog).toBeVisible();
907
-
908
- // Scoped steps
909
- await dialog.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
910
- await dialog.getByRole('button', { name: 'Submit' }).click();
911
- await dialog.waitFor({ state: 'hidden' });
912
- ```
913
-
914
- ---
915
-
916
- ## Frame Scope
917
-
918
- Similar to dialog scope, but for iframes:
919
-
920
- ```gherkin
921
- When User switch to [Payment] frame
922
- When User fill [Card number] field with {{card_number}}
923
- When User fill [Expiry] field with {{expiry_date}}
924
- When User click [Pay] button
925
- When User switch to [main] frame
926
- ```
927
-
928
- **Compiler rule:** Track frame context.
929
-
930
- ```typescript
931
- const frame = page.frameLocator('#payment-iframe');
932
- await frame.getByRole('textbox', { name: 'Card number' }).fill('4242424242424242');
933
- await frame.getByRole('textbox', { name: 'Expiry' }).fill('12/28');
934
- await frame.getByRole('button', { name: 'Pay' }).click();
935
- // switch to main = reset to page context
936
- ```
937
-
938
- ---
939
-
940
- ## Element Type → Role Mapping
941
-
942
- | Gherkin type | Playwright role | Locator method |
943
- |---|---|---|
944
- | `page` | — | `page.goto(value)` |
945
- | `button` | `button` | `getByRole('button')` |
946
- | `link` | `link` | `getByRole('link')` |
947
- | `field` / `input` | `textbox` | `getByRole('textbox')` |
948
- | `textarea` | `textbox` | `getByRole('textbox')` or locator for contenteditable |
949
- | `heading` | `heading` | `getByRole('heading')` |
950
- | `text` | — | `getByText(value)` |
951
- | `image` / `img` / `icon` | `img` | `getByRole('img')` |
952
- | `checkbox` | `checkbox` | `getByRole('checkbox')` |
953
- | `radio` | `radio` | `getByRole('radio')` |
954
- | `switch` / `toggle` | `switch` | `getByRole('switch')` |
955
- | `dropdown` / `select` | `combobox` | `getByRole('combobox')` |
956
- | `option` | `option` | `getByRole('option')` |
957
- | `search` | `searchbox` | `getByRole('searchbox')` |
958
- | `slider` | `slider` | `getByRole('slider')` |
959
- | `dialog` / `modal` / `drawer` | `dialog` | `getByRole('dialog')` |
960
- | `menu` | `menu` | `getByRole('menu')` |
961
- | `menuitem` | `menuitem` | `getByRole('menuitem')` |
962
- | `tab` | `tab` | `getByRole('tab')` |
963
- | `tabpanel` | `tabpanel` | `getByRole('tabpanel')` |
964
- | `list` | `list` | `getByRole('list')` |
965
- | `listitem` | `listitem` | `getByRole('listitem')` |
966
- | `table` | `table` | `getByRole('table')` |
967
- | `row` | `row` | `getByRole('row')` |
968
- | `cell` | `cell` | `getByRole('cell')` |
969
- | `column` / `columnheader` | `columnheader` | `getByRole('columnheader')` |
970
- | `region` / `section` | `region` | `getByRole('region')` |
971
- | `nav` / `navigation` | `navigation` | `getByRole('navigation')` |
972
- | `banner` / `header` | `banner` | `getByRole('banner')` |
973
- | `footer` | `contentinfo` | `getByRole('contentinfo')` |
974
- | `alert` | `alertdialog` | `getByRole('alertdialog')` or `page.on('dialog')` for browser alerts |
975
- | `spinner` / `progressbar` | `progressbar` | `getByRole('progressbar')` |
976
- | `slider` | `slider` | `getByRole('slider')` |
977
- | `tree` | `tree` | `getByRole('tree')` |
978
- | `treeitem` | `treeitem` | `getByRole('treeitem')` |
979
- | `tooltip` | `tooltip` | `getByRole('tooltip')` |
980
- | `uploader` / `file` | — | `setInputFiles()` |
981
- | `frame` / `iframe` | — | `frameLocator()` |
982
-
983
- ---
984
-
985
- ## Compiler Rules Summary
986
-
987
- ### Rule 1: Build locator from selector YAML
988
-
989
- ```
990
- locator = buildLocator(selector)
991
-
992
- if selector.frame: base = page.frameLocator(frame)
993
- elif dialogContext: base = dialogLocator
994
- else: base = page
995
-
996
- if selector.scope: base = base.getByLabel(scope)
997
-
998
- switch selector.type:
999
- testid → base.getByTestId(value)
1000
- role → base.getByRole(value, { name, exact })
1001
- label → base.getByLabel(value, { exact })
1002
- placeholder → base.getByPlaceholder(value, { exact })
1003
- text → base.getByText(value, { exact: match=='exact' })
1004
- locator → base.locator(value)
1005
- page → page.goto(value) or expect(page).toHaveURL()
1006
- upload → base.locator(value)
1007
- frame → page.frameLocator(value)
1008
- table → base.getByRole('table', { name: value })
1009
-
1010
- if selector.nth > 0: locator = locator.nth(selector.nth)
1011
- ```
1012
-
1013
- ### Rule 2: Map Gherkin verb to Playwright method
1014
-
1015
- ```
1016
- click → .click()
1017
- double click → .dblclick()
1018
- fill → .fill(value) or .pressSequentially(value) if contenteditable
1019
- clear → .clear()
1020
- check → .check()
1021
- uncheck → .uncheck()
1022
- toggle → .click()
1023
- select (native) → .selectOption(value)
1024
- select (custom) → .click() then getByRole('option', {name}).click()
1025
- upload → .setInputFiles('specs/storage/' + value)
1026
- hover → .hover()
1027
- drag ... to → .dragTo(targetLocator)
1028
- press (global) → page.keyboard.press(key)
1029
- press (on elem) → locator.press(key)
1030
- scroll to → .scrollIntoViewIfNeeded()
1031
- expand → .getByRole('button', {name:'Expand'}).click()
1032
- collapse → .getByRole('button', {name:'Collapse'}).click()
1033
- ```
1034
-
1035
- ### Rule 3: Map state to Playwright assertion
1036
-
1037
- ```
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)
1041
- see ... hidden → expect(locator).toBeHidden()
1042
- see ... visible → expect(locator).toBeVisible()
1043
- see ... enabled → expect(locator).toBeEnabled()
1044
- see ... disabled→ expect(locator).toBeDisabled()
1045
- see ... checked → expect(locator).toBeChecked()
1046
- see ... unchecked→ expect(locator).not.toBeChecked()
1047
- see ... focused → expect(locator).toBeFocused()
1048
- see ... empty → expect(locator).toHaveText('')
1049
- see ... loading → expect(locator).toBeVisible() (spinner/progressbar)
1050
- see ... selected→ expect(locator).toHaveAttribute('aria-selected', 'true')
1051
- see ... sorted ascending → expect(locator).toHaveAttribute('aria-sort', 'ascending')
1052
- see ... sorted descending → expect(locator).toHaveAttribute('aria-sort', 'descending')
1053
- contains → expect(locator).toContainText(value)
1054
- has (attribute) → expect(locator).toHaveAttribute(attr, value)
1055
- has ... rows → expect(locator.getByRole('row')).toHaveCount(N)
1056
- has ... column → expect(locator.getByRole('columnheader', {name})).toBeVisible()
1057
- ```
1058
-
1059
- ### Rule 4: Scope management
1060
-
1061
- ```
1062
- Dialog enter: "see [X] dialog with {{title}}"
1063
- → dialogLocator = page.getByRole('dialog').filter({ hasText: title })
1064
- → all subsequent locators use dialogLocator as base
1065
-
1066
- Dialog exit: "wait for [X] dialog with {{title}} is hidden"
1067
- → dialogLocator.waitFor({ state: 'hidden' })
1068
- → reset to page context
1069
-
1070
- Frame enter: "switch to [X] frame"
1071
- → frameLocator = page.frameLocator(value)
1072
- → all subsequent locators use frameLocator as base
1073
-
1074
- Frame exit: "switch to [main] frame"
1075
- → reset to page context
1076
- ```
1077
-
1078
- ### Rule 5: Table operations
1079
-
1080
- ```
1081
- Table locator: page.getByRole('table', { name: selector.value })
1082
-
1083
- Row by index: table.getByRole('row').nth(N)
1084
- Row by filter: table.getByRole('row').filter({ hasText: filterValue })
1085
- Cell by col: row.getByRole('cell').nth(columns[colName].index)
1086
- Action in row: row.resolve([element])
1087
-
1088
- Count: expect(table.locator('tbody').getByRole('row')).toHaveCount(N)
1089
- Empty: expect(table.locator('tbody').getByRole('row')).toHaveCount(0)
1090
- Column exists: expect(table.getByRole('columnheader', { name })).toBeVisible()
1091
- Sort: table.getByRole('columnheader', { name }).click()
1092
- Pagination: page.locator(pagination.next).click()
1093
- ```
1094
-
1095
- ---
1096
-
1097
- ## What AI Discover Must Output
1098
-
1099
- When AI visits a page via MCP Playwright, it must produce YAML entries with:
1100
-
1101
- 1. **Correct `type`** — detected from accessibility snapshot role
1102
- 2. **Correct `name`** — from accessible name in snapshot
1103
- 3. **`exact: true`** — when name is a substring of another element's name
1104
- 4. **`scope`** — when element appears in multiple landmarks (header + footer)
1105
- 5. **`match: exact`** — when text value is substring of other text on page
1106
- 6. **`variant`** — for dropdowns: inspect if `<select>` or custom div
1107
- 7. **`trigger`** — for uploads: find the visible button near hidden `input[type=file]`
1108
- 8. **`contenteditable`** — for rich editors: check if `[contenteditable="true"]`
1109
- 9. **`frame`** — for iframes: detect if element is inside a frame
1110
- 10. **`columns`** — for tables: map column headers to indices
1111
-
1112
- ## What AI Verify Must Fix
1113
-
1114
- When tests fail, AI reads error messages and fixes YAML:
1115
-
1116
- | Error pattern | YAML fix |
1117
- |---|---|
1118
- | `strict mode violation: resolved to N elements` | Add `exact: true` or `scope` or increase `nth` |
1119
- | `element(s) not found` | Fix `name`, `type`, or `value` in YAML |
1120
- | `resolved to N elements` (getByText) | Add `match: 'exact'` |
1121
- | `not visible` / timeout | Element may need scroll, wait, or page didn't load (auth issue) |
1122
- | `intercepted by overlay` | Need to close overlay first (press Escape, click backdrop) |
1123
- | timeout on click | Element in iframe, behind overlay, or auth redirect |
1124
- | `toHaveText` mismatch | Fix expected value in test-data.yaml |
1125
- | `toHaveURL` mismatch | Fix page value in selectors.yaml |
1126
- | `toHaveAttribute` mismatch | Fix attribute/pattern in selectors.yaml |