@sun-asterisk/sungen 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +2 -2
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +10 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +4 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +4 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +11 -410
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-rules.md +189 -0
- package/dist/orchestrator/templates/gitignore +16 -0
- package/dist/orchestrator/templates/playwright.config.d.ts +10 -0
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -0
- package/dist/orchestrator/templates/playwright.config.js +77 -0
- package/dist/orchestrator/templates/playwright.config.js.map +1 -0
- package/dist/orchestrator/templates/playwright.config.ts +80 -0
- package/dist/orchestrator/templates/readme.md +197 -0
- package/docs/gherkin standards/gherkin-core-standard.md +377 -0
- package/docs/gherkin standards/gherkin-core-standard.vi.md +303 -0
- package/docs/gherkin-dictionary.md +1071 -0
- package/docs/makeauth.md +225 -0
- package/package.json +3 -2
- package/src/cli/index.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +2 -2
- package/src/generators/test-generator/code-generator.ts +11 -0
- package/src/generators/test-generator/step-mapper.ts +9 -0
- package/src/orchestrator/project-initializer.ts +12 -410
- package/src/orchestrator/templates/ai-rules.md +189 -0
- package/src/orchestrator/templates/gitignore +16 -0
- package/src/orchestrator/templates/playwright.config.ts +80 -0
- package/src/orchestrator/templates/readme.md +197 -0
|
@@ -0,0 +1,1071 @@
|
|
|
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"|"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"
|
|
202
|
+
DATA_EXPR := "with" "{{" IDENTIFIER "}}"
|
|
203
|
+
STATE_EXPR := "is" STATE
|
|
204
|
+
STATE := "hidden"|"visible"|"disabled"|"enabled"|"checked"
|
|
205
|
+
| "unchecked"|"focused"|"empty"|"loading"|"selected"
|
|
206
|
+
| "sorted ascending"|"sorted descending"
|
|
207
|
+
ACTION := "click"|"fill"|"select"|"check"|"uncheck"|"toggle"
|
|
208
|
+
| "upload"|"hover"|"drag"|"clear"|"see"|"press"|"expand"
|
|
209
|
+
| "collapse"|"double click"
|
|
210
|
+
KEY := "Enter"|"Escape"|"Tab"|"Backspace"|"Delete"|"Space"
|
|
211
|
+
| "ArrowUp"|"ArrowDown"|"ArrowLeft"|"ArrowRight"
|
|
212
|
+
| "Home"|"End"|"PageUp"|"PageDown"
|
|
213
|
+
TIMEOUT := NUMBER ("seconds"|"ms")
|
|
214
|
+
NUMBER := [0-9]+
|
|
215
|
+
IDENTIFIER := [a-zA-Z0-9_ ./*:-]+
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 17 Pattern Shapes
|
|
221
|
+
|
|
222
|
+
### ① Simple action: `action [Target] type`
|
|
223
|
+
|
|
224
|
+
```gherkin
|
|
225
|
+
When User click [Submit] button
|
|
226
|
+
When User check [Remember me] checkbox
|
|
227
|
+
When User uncheck [Newsletter] checkbox
|
|
228
|
+
When User toggle [Dark mode] switch
|
|
229
|
+
When User clear [Search] field
|
|
230
|
+
When User hover [Info] icon
|
|
231
|
+
When User expand [Row 1] row
|
|
232
|
+
When User collapse [Row 1] row
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Compiler rule:**
|
|
236
|
+
```
|
|
237
|
+
locator = resolve([Target], type)
|
|
238
|
+
code = locator.{action}()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Playwright output:**
|
|
242
|
+
```typescript
|
|
243
|
+
// click button
|
|
244
|
+
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
245
|
+
|
|
246
|
+
// check checkbox
|
|
247
|
+
await page.getByRole('checkbox', { name: 'Remember me' }).check();
|
|
248
|
+
|
|
249
|
+
// uncheck checkbox
|
|
250
|
+
await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck();
|
|
251
|
+
|
|
252
|
+
// toggle switch
|
|
253
|
+
await page.getByRole('switch', { name: 'Dark mode' }).click();
|
|
254
|
+
|
|
255
|
+
// clear field
|
|
256
|
+
await page.getByRole('textbox', { name: 'Search' }).clear();
|
|
257
|
+
|
|
258
|
+
// hover
|
|
259
|
+
await page.getByRole('img', { name: 'Info' }).hover();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### ② Action with data: `action [Target] type with {{Value}}`
|
|
265
|
+
|
|
266
|
+
```gherkin
|
|
267
|
+
When User fill [Email] field with {{valid_email}}
|
|
268
|
+
When User fill [Message] textarea with {{message_body}}
|
|
269
|
+
When User select [Country] dropdown with {{country_name}}
|
|
270
|
+
When User upload [Avatar] file with {{avatar_path}}
|
|
271
|
+
When User click [Teammate] button with {{teammate_name}}
|
|
272
|
+
Then User see [Title] heading with {{page_title}}
|
|
273
|
+
Then User see [Price] text with {{item_price}}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Compiler rule:**
|
|
277
|
+
```
|
|
278
|
+
locator = resolve([Target], type)
|
|
279
|
+
value = resolveData({{Value}})
|
|
280
|
+
if action == 'fill': code = locator.fill(value)
|
|
281
|
+
if action == 'select': code = locator.selectOption(value) or click+click
|
|
282
|
+
if action == 'upload': code = locator.setInputFiles(value)
|
|
283
|
+
if action == 'click': code = page.getByText(value).click() or locator.filter({hasText}).click()
|
|
284
|
+
if action == 'see': code = expect(locator).toBeVisible() with text check
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Playwright output:**
|
|
288
|
+
```typescript
|
|
289
|
+
// fill text field
|
|
290
|
+
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('test@example.com');
|
|
291
|
+
|
|
292
|
+
// fill contenteditable
|
|
293
|
+
await page.locator('[contenteditable="true"]').click();
|
|
294
|
+
await page.locator('[contenteditable="true"]').pressSequentially('Hello world!');
|
|
295
|
+
|
|
296
|
+
// select native dropdown
|
|
297
|
+
await page.getByRole('combobox', { name: 'Country' }).selectOption('Vietnam');
|
|
298
|
+
|
|
299
|
+
// select custom dropdown
|
|
300
|
+
await page.getByRole('combobox', { name: 'Country' }).click();
|
|
301
|
+
await page.getByRole('option', { name: 'Vietnam' }).click();
|
|
302
|
+
|
|
303
|
+
// upload file
|
|
304
|
+
await page.getByLabel('Avatar').setInputFiles('specs/storage/avatar.png');
|
|
305
|
+
|
|
306
|
+
// click with text filter
|
|
307
|
+
await page.getByText('Nguyễn Thanh Tùng').click();
|
|
308
|
+
|
|
309
|
+
// see heading with value
|
|
310
|
+
await expect(page.getByRole('heading', { name: 'Hệ thống giải thưởng SAA 2025' })).toBeVisible();
|
|
311
|
+
|
|
312
|
+
// see text with exact match
|
|
313
|
+
await expect(page.getByText('5.000.000 VNĐ', { exact: true })).toBeVisible();
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### ③ State assertion: `action [Target] type is state`
|
|
319
|
+
|
|
320
|
+
```gherkin
|
|
321
|
+
Then User see [Submit] button is disabled
|
|
322
|
+
Then User see [Submit] button is enabled
|
|
323
|
+
Then User see [Modal] dialog is hidden
|
|
324
|
+
Then User see [Welcome] heading is visible
|
|
325
|
+
Then User see [Remember me] checkbox is checked
|
|
326
|
+
Then User see [Newsletter] checkbox is unchecked
|
|
327
|
+
Then User see [Email] field is focused
|
|
328
|
+
Then User see [Search] field is empty
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Compiler rule:**
|
|
332
|
+
```
|
|
333
|
+
locator = resolve([Target], type)
|
|
334
|
+
switch(state):
|
|
335
|
+
hidden → expect(locator).toBeHidden()
|
|
336
|
+
visible → expect(locator).toBeVisible()
|
|
337
|
+
disabled → expect(locator).toBeDisabled()
|
|
338
|
+
enabled → expect(locator).toBeEnabled()
|
|
339
|
+
checked → expect(locator).toBeChecked()
|
|
340
|
+
unchecked → expect(locator).not.toBeChecked()
|
|
341
|
+
focused → expect(locator).toBeFocused()
|
|
342
|
+
empty → expect(locator).toHaveText('')
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Playwright output:**
|
|
346
|
+
```typescript
|
|
347
|
+
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
|
|
348
|
+
await expect(page.getByRole('dialog')).toBeHidden();
|
|
349
|
+
await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();
|
|
350
|
+
await expect(page.getByRole('textbox', { name: 'Email' })).toBeFocused();
|
|
351
|
+
await expect(page.getByRole('textbox', { name: 'Search' })).toHaveText('');
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### ④ State with data: `action [Target] type with {{Value}} is state`
|
|
357
|
+
|
|
358
|
+
```gherkin
|
|
359
|
+
Then User see [Panel] dialog with {{dialog_title}} is hidden
|
|
360
|
+
Then User see [Email] field with {{error_message}} is disabled
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Compiler rule:**
|
|
364
|
+
```
|
|
365
|
+
locator = resolve([Target], type).filter({ hasText: value })
|
|
366
|
+
apply state assertion on locator
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Playwright output:**
|
|
370
|
+
```typescript
|
|
371
|
+
await expect(
|
|
372
|
+
page.getByRole('dialog').filter({ hasText: 'Confirm Delete' })
|
|
373
|
+
).toBeHidden();
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### ⑤ Two targets: `action [Source] to [Destination]`
|
|
379
|
+
|
|
380
|
+
```gherkin
|
|
381
|
+
When User drag [Card A] to [Column B]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Compiler rule:**
|
|
385
|
+
```
|
|
386
|
+
source = resolve([Source])
|
|
387
|
+
dest = resolve([Destination])
|
|
388
|
+
code = source.dragTo(dest)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Playwright output:**
|
|
392
|
+
```typescript
|
|
393
|
+
await page.getByTestId('card-a').dragTo(page.getByTestId('column-b'));
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### ⑥ Global key: `action Key key`
|
|
399
|
+
|
|
400
|
+
```gherkin
|
|
401
|
+
When User press Escape key
|
|
402
|
+
When User press Tab key
|
|
403
|
+
When User press Space key
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Compiler rule:**
|
|
407
|
+
```
|
|
408
|
+
code = page.keyboard.press(Key)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Playwright output:**
|
|
412
|
+
```typescript
|
|
413
|
+
await page.keyboard.press('Escape');
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### ⑦ Key on target: `action Key on [Target] type`
|
|
419
|
+
|
|
420
|
+
```gherkin
|
|
421
|
+
When User press Enter on [Search] field
|
|
422
|
+
When User press Escape on [Modal] dialog
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Compiler rule:**
|
|
426
|
+
```
|
|
427
|
+
locator = resolve([Target], type)
|
|
428
|
+
code = locator.press(Key)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Playwright output:**
|
|
432
|
+
```typescript
|
|
433
|
+
await page.getByRole('textbox', { name: 'Search' }).press('Enter');
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### ⑧ Wait timeout: `wait for N seconds`
|
|
439
|
+
|
|
440
|
+
```gherkin
|
|
441
|
+
When User wait for 3 seconds
|
|
442
|
+
When User wait for 500 ms
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Compiler rule:**
|
|
446
|
+
```
|
|
447
|
+
code = page.waitForTimeout(N * multiplier)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Playwright output:**
|
|
451
|
+
```typescript
|
|
452
|
+
await page.waitForTimeout(3000);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### ⑨ Wait for element: `wait for [Target] type (with {{Value}}) (is state)`
|
|
458
|
+
|
|
459
|
+
```gherkin
|
|
460
|
+
When User wait for [Modal] dialog
|
|
461
|
+
When User wait for [Loading] spinner is hidden
|
|
462
|
+
When User wait for [Dialog] dialog with {{dialog_title}}
|
|
463
|
+
When User wait for [Dialog] dialog with {{dialog_title}} is hidden
|
|
464
|
+
When User wait for [dashboard] page
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Compiler rule:**
|
|
468
|
+
```
|
|
469
|
+
locator = resolve([Target], type)
|
|
470
|
+
if DATA_EXPR: locator = locator.filter({ hasText: value })
|
|
471
|
+
state = STATE_EXPR ? mapState(state) : 'visible'
|
|
472
|
+
if type == 'page': code = page.waitForURL(value)
|
|
473
|
+
else: code = locator.waitFor({ state })
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Playwright output:**
|
|
477
|
+
```typescript
|
|
478
|
+
// wait for dialog visible
|
|
479
|
+
await page.getByRole('dialog').waitFor({ state: 'visible' });
|
|
480
|
+
|
|
481
|
+
// wait for spinner hidden
|
|
482
|
+
await page.getByRole('progressbar').waitFor({ state: 'hidden' });
|
|
483
|
+
|
|
484
|
+
// wait for dialog with title
|
|
485
|
+
await page.getByRole('dialog').filter({ hasText: 'Confirm' }).waitFor({ state: 'visible' });
|
|
486
|
+
|
|
487
|
+
// wait for dialog with title hidden
|
|
488
|
+
await page.getByRole('dialog').filter({ hasText: 'Confirm' }).waitFor({ state: 'hidden' });
|
|
489
|
+
|
|
490
|
+
// wait for page
|
|
491
|
+
await page.waitForURL(/\/dashboard/);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
### ⑩ Scroll: `scroll to [Target] type`
|
|
497
|
+
|
|
498
|
+
```gherkin
|
|
499
|
+
When User scroll to [Footer] section
|
|
500
|
+
When User scroll to [Comments] region
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Compiler rule:**
|
|
504
|
+
```
|
|
505
|
+
locator = resolve([Target], type)
|
|
506
|
+
code = locator.scrollIntoViewIfNeeded()
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Playwright output:**
|
|
510
|
+
```typescript
|
|
511
|
+
await page.getByRole('contentinfo').scrollIntoViewIfNeeded();
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### ⑪ Frame switch: `switch to [Target] frame`
|
|
517
|
+
|
|
518
|
+
```gherkin
|
|
519
|
+
When User switch to [Payment] frame
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
After this step, all subsequent locators are scoped to the frame until the next `switch to [main] frame` or end of scenario.
|
|
523
|
+
|
|
524
|
+
**Selector YAML:**
|
|
525
|
+
```yaml
|
|
526
|
+
payment:
|
|
527
|
+
type: 'frame'
|
|
528
|
+
value: '#payment-iframe'
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Compiler rule:**
|
|
532
|
+
```
|
|
533
|
+
frameContext = page.frameLocator(value)
|
|
534
|
+
// All subsequent locators use frameContext instead of page
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Playwright output:**
|
|
538
|
+
```typescript
|
|
539
|
+
// Subsequent steps use frame scope:
|
|
540
|
+
const frame = page.frameLocator('#payment-iframe');
|
|
541
|
+
await frame.getByRole('textbox', { name: 'Card number' }).fill('4242...');
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
### ⑫ Has (count/attribute): `action [Target] type has {{Value}}`
|
|
547
|
+
|
|
548
|
+
```gherkin
|
|
549
|
+
Then User see [Avatar] image has {{avatar_url}}
|
|
550
|
+
Then User see [Link] link has {{href_value}}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Selector YAML:**
|
|
554
|
+
```yaml
|
|
555
|
+
avatar:
|
|
556
|
+
type: 'role'
|
|
557
|
+
value: 'img'
|
|
558
|
+
name: 'Avatar'
|
|
559
|
+
attribute: 'src'
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Compiler rule:**
|
|
563
|
+
```
|
|
564
|
+
locator = resolve([Target], type)
|
|
565
|
+
if selector.attribute:
|
|
566
|
+
code = expect(locator).toHaveAttribute(attribute, value)
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Playwright output:**
|
|
570
|
+
```typescript
|
|
571
|
+
// image has src
|
|
572
|
+
await expect(page.getByRole('img', { name: 'Avatar' })).toHaveAttribute('src', /avatar\.png/);
|
|
573
|
+
|
|
574
|
+
// link has href
|
|
575
|
+
await expect(page.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/');
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
### ⑬ Table cell by row index: `see [Table] table row N [Col] cell with {{Value}}`
|
|
581
|
+
|
|
582
|
+
```gherkin
|
|
583
|
+
Then User see [Users] table row 1 [Name] cell with {{first_name}}
|
|
584
|
+
Then User see [Users] table row 1 [Email] cell with {{first_email}}
|
|
585
|
+
Then User see [Users] table row 3 [Status] cell with {{third_status}}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Selector YAML:**
|
|
589
|
+
```yaml
|
|
590
|
+
users:
|
|
591
|
+
type: 'table'
|
|
592
|
+
value: 'Users'
|
|
593
|
+
columns:
|
|
594
|
+
name:
|
|
595
|
+
index: 0
|
|
596
|
+
header: 'Name'
|
|
597
|
+
email:
|
|
598
|
+
index: 1
|
|
599
|
+
header: 'Email'
|
|
600
|
+
status:
|
|
601
|
+
index: 2
|
|
602
|
+
header: 'Status'
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Compiler rule:**
|
|
606
|
+
```
|
|
607
|
+
table = page.getByRole('table', { name: selector.value })
|
|
608
|
+
row = table.getByRole('row').nth(N) // N+1 to skip header
|
|
609
|
+
cell = row.getByRole('cell').nth(columns[Col].index)
|
|
610
|
+
code = expect(cell).toHaveText(value)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Playwright output:**
|
|
614
|
+
```typescript
|
|
615
|
+
const table = page.getByRole('table', { name: 'Users' });
|
|
616
|
+
const row = table.getByRole('row').nth(1); // skip header
|
|
617
|
+
const cell = row.getByRole('cell').nth(0); // Name column
|
|
618
|
+
await expect(cell).toHaveText('John Doe');
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### ⑭ Table cell by row filter: `see [Table] table row with {{Filter}} has [Col] with {{Value}}`
|
|
624
|
+
|
|
625
|
+
```gherkin
|
|
626
|
+
Then User see [Users] table row with {{user_name}} has [Status] with {{expected_status}}
|
|
627
|
+
Then User see [Users] table row with {{user_name}} has [Email] with {{expected_email}}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Compiler rule:**
|
|
631
|
+
```
|
|
632
|
+
table = page.getByRole('table', { name: selector.value })
|
|
633
|
+
row = table.getByRole('row').filter({ hasText: filterValue })
|
|
634
|
+
cell = row.getByRole('cell').nth(columns[Col].index)
|
|
635
|
+
code = expect(cell).toHaveText(value)
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**Playwright output:**
|
|
639
|
+
```typescript
|
|
640
|
+
const table = page.getByRole('table', { name: 'Users' });
|
|
641
|
+
const row = table.getByRole('row').filter({ hasText: 'John Doe' });
|
|
642
|
+
const cell = row.getByRole('cell').nth(2); // Status column
|
|
643
|
+
await expect(cell).toHaveText('Active');
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
### ⑮ Action in table row: `action [Element] in [Table] table row with {{Filter}}`
|
|
649
|
+
|
|
650
|
+
```gherkin
|
|
651
|
+
When User click [Edit] in [Users] table row with {{user_name}}
|
|
652
|
+
When User click [Delete] in [Users] table row with {{user_name}}
|
|
653
|
+
When User check [Users] table row with {{user_name}} checkbox
|
|
654
|
+
When User check [Users] table select all checkbox
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Compiler rule:**
|
|
658
|
+
```
|
|
659
|
+
table = page.getByRole('table', { name: tableSelector.value })
|
|
660
|
+
row = table.getByRole('row').filter({ hasText: filterValue })
|
|
661
|
+
element = row.resolve([Element])
|
|
662
|
+
code = element.{action}()
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Playwright output:**
|
|
666
|
+
```typescript
|
|
667
|
+
// click button in row
|
|
668
|
+
const table = page.getByRole('table', { name: 'Users' });
|
|
669
|
+
const row = table.getByRole('row').filter({ hasText: 'John Doe' });
|
|
670
|
+
await row.getByRole('button', { name: 'Edit' }).click();
|
|
671
|
+
|
|
672
|
+
// check row checkbox
|
|
673
|
+
const row = table.getByRole('row').filter({ hasText: 'John Doe' });
|
|
674
|
+
await row.getByRole('checkbox').check();
|
|
675
|
+
|
|
676
|
+
// select all
|
|
677
|
+
await table.getByRole('checkbox', { name: 'Select all' }).check();
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
### ⑯ Navigation: `is on [Target] page` / `navigate to [Target] page`
|
|
683
|
+
|
|
684
|
+
```gherkin
|
|
685
|
+
Given User is on [login] page
|
|
686
|
+
Given User is on [awards] page
|
|
687
|
+
When User navigate to [profile] page with {{user_id}}
|
|
688
|
+
Then User see [dashboard] page
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Selector YAML:**
|
|
692
|
+
```yaml
|
|
693
|
+
login:
|
|
694
|
+
type: 'page'
|
|
695
|
+
value: '/login'
|
|
696
|
+
|
|
697
|
+
profile:
|
|
698
|
+
type: 'page'
|
|
699
|
+
value: '/users/{{user_id}}/profile'
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
**Compiler rule:**
|
|
703
|
+
```
|
|
704
|
+
if 'is on' or 'navigate to':
|
|
705
|
+
code = page.goto(value, { waitUntil: 'networkidle' })
|
|
706
|
+
if 'see':
|
|
707
|
+
code = expect(page).toHaveURL(regex_from_value)
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**Playwright output:**
|
|
711
|
+
```typescript
|
|
712
|
+
// Given: navigate
|
|
713
|
+
await page.goto('/login', { waitUntil: 'networkidle' });
|
|
714
|
+
|
|
715
|
+
// navigate with data interpolation
|
|
716
|
+
await page.goto('/users/12345/profile', { waitUntil: 'networkidle' });
|
|
717
|
+
|
|
718
|
+
// Then: assert URL
|
|
719
|
+
await page.waitForLoadState('networkidle');
|
|
720
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
### ⑰ Text assertions: `see [Target] type contains/has text {{Value}}`
|
|
726
|
+
|
|
727
|
+
```gherkin
|
|
728
|
+
Then User see [Message] text contains {{partial_text}}
|
|
729
|
+
Then User see [Counter] text has text {{exact_count}}
|
|
730
|
+
Then User see [Description] text has text {{full_description}}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Compiler rule:**
|
|
734
|
+
```
|
|
735
|
+
locator = resolve([Target], type)
|
|
736
|
+
if 'contains': code = expect(locator).toContainText(value)
|
|
737
|
+
if 'has text': code = expect(locator).toHaveText(value)
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Playwright output:**
|
|
741
|
+
```typescript
|
|
742
|
+
// contains (partial match)
|
|
743
|
+
await expect(page.getByText('Message')).toContainText('successfully');
|
|
744
|
+
|
|
745
|
+
// has text (exact match)
|
|
746
|
+
await expect(locator).toHaveText('42');
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Table-Specific Patterns (extended)
|
|
752
|
+
|
|
753
|
+
### Row count
|
|
754
|
+
|
|
755
|
+
```gherkin
|
|
756
|
+
Then User see [Users] table has {{row_count}} rows
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
const table = page.getByRole('table', { name: 'Users' });
|
|
761
|
+
// subtract 1 for header row, or use tbody rows
|
|
762
|
+
await expect(table.locator('tbody').getByRole('row')).toHaveCount(10);
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Column exists
|
|
766
|
+
|
|
767
|
+
```gherkin
|
|
768
|
+
Then User see [Users] table has [Email] column
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
await expect(
|
|
773
|
+
page.getByRole('table', { name: 'Users' }).getByRole('columnheader', { name: 'Email' })
|
|
774
|
+
).toBeVisible();
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Empty state
|
|
778
|
+
|
|
779
|
+
```gherkin
|
|
780
|
+
Then User see [Users] table is empty
|
|
781
|
+
Then User see [Users] table empty message with {{no_data_text}}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
// empty: only header row
|
|
786
|
+
await expect(
|
|
787
|
+
page.getByRole('table', { name: 'Users' }).locator('tbody').getByRole('row')
|
|
788
|
+
).toHaveCount(0);
|
|
789
|
+
|
|
790
|
+
// empty message
|
|
791
|
+
await expect(page.getByText('No data available')).toBeVisible();
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Sort column
|
|
795
|
+
|
|
796
|
+
```gherkin
|
|
797
|
+
When User click [Name] columnheader
|
|
798
|
+
Then User see [Name] columnheader is sorted ascending
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
await page.getByRole('columnheader', { name: 'Name' }).click();
|
|
803
|
+
await expect(
|
|
804
|
+
page.getByRole('columnheader', { name: 'Name' })
|
|
805
|
+
).toHaveAttribute('aria-sort', 'ascending');
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Pagination
|
|
809
|
+
|
|
810
|
+
```gherkin
|
|
811
|
+
When User click [Users] table next page
|
|
812
|
+
When User select [Users] table page size with {{page_size}}
|
|
813
|
+
Then User see [Users] table page info with {{page_info}}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Uses `pagination` from selector YAML:
|
|
817
|
+
```typescript
|
|
818
|
+
await page.locator('[data-testid="next-page"]').click();
|
|
819
|
+
await page.locator('[data-testid="page-size"]').selectOption('25');
|
|
820
|
+
await expect(page.locator('[data-testid="page-info"]')).toHaveText('Showing 1-25 of 100');
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Row expansion
|
|
824
|
+
|
|
825
|
+
```gherkin
|
|
826
|
+
When User expand [Users] table row with {{user_name}}
|
|
827
|
+
When User collapse [Users] table row with {{user_name}}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
const row = table.getByRole('row').filter({ hasText: 'John' });
|
|
832
|
+
await row.getByRole('button', { name: 'Expand' }).click();
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## Dialog Scope
|
|
838
|
+
|
|
839
|
+
When a dialog is opened, subsequent steps are scoped within it:
|
|
840
|
+
|
|
841
|
+
```gherkin
|
|
842
|
+
Then User see [panel] dialog with {{dialog_title}}
|
|
843
|
+
# All steps below are scoped to this dialog:
|
|
844
|
+
When User fill [Email] field with {{email}}
|
|
845
|
+
When User click [Submit] button
|
|
846
|
+
When User wait for [panel] dialog with {{dialog_title}} is hidden
|
|
847
|
+
# Scope ends
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Compiler rule:** Track dialog context. When dialog is visible, wrap all locators:
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
const dialog = page.getByRole('dialog').filter({ hasText: 'Gửi lời cảm ơn' });
|
|
854
|
+
await expect(dialog).toBeVisible();
|
|
855
|
+
|
|
856
|
+
// Scoped steps
|
|
857
|
+
await dialog.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
|
|
858
|
+
await dialog.getByRole('button', { name: 'Submit' }).click();
|
|
859
|
+
await dialog.waitFor({ state: 'hidden' });
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## Frame Scope
|
|
865
|
+
|
|
866
|
+
Similar to dialog scope, but for iframes:
|
|
867
|
+
|
|
868
|
+
```gherkin
|
|
869
|
+
When User switch to [Payment] frame
|
|
870
|
+
When User fill [Card number] field with {{card_number}}
|
|
871
|
+
When User fill [Expiry] field with {{expiry_date}}
|
|
872
|
+
When User click [Pay] button
|
|
873
|
+
When User switch to [main] frame
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Compiler rule:** Track frame context.
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
const frame = page.frameLocator('#payment-iframe');
|
|
880
|
+
await frame.getByRole('textbox', { name: 'Card number' }).fill('4242424242424242');
|
|
881
|
+
await frame.getByRole('textbox', { name: 'Expiry' }).fill('12/28');
|
|
882
|
+
await frame.getByRole('button', { name: 'Pay' }).click();
|
|
883
|
+
// switch to main = reset to page context
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
## Element Type → Role Mapping
|
|
889
|
+
|
|
890
|
+
| Gherkin type | Playwright role | Locator method |
|
|
891
|
+
|---|---|---|
|
|
892
|
+
| `page` | — | `page.goto(value)` |
|
|
893
|
+
| `button` | `button` | `getByRole('button')` |
|
|
894
|
+
| `link` | `link` | `getByRole('link')` |
|
|
895
|
+
| `field` / `input` | `textbox` | `getByRole('textbox')` |
|
|
896
|
+
| `textarea` | `textbox` | `getByRole('textbox')` or locator for contenteditable |
|
|
897
|
+
| `heading` | `heading` | `getByRole('heading')` |
|
|
898
|
+
| `text` | — | `getByText(value)` |
|
|
899
|
+
| `image` / `img` / `icon` | `img` | `getByRole('img')` |
|
|
900
|
+
| `checkbox` | `checkbox` | `getByRole('checkbox')` |
|
|
901
|
+
| `radio` | `radio` | `getByRole('radio')` |
|
|
902
|
+
| `switch` / `toggle` | `switch` | `getByRole('switch')` |
|
|
903
|
+
| `dropdown` / `select` | `combobox` | `getByRole('combobox')` |
|
|
904
|
+
| `option` | `option` | `getByRole('option')` |
|
|
905
|
+
| `dialog` / `modal` | `dialog` | `getByRole('dialog')` |
|
|
906
|
+
| `menu` | `menu` | `getByRole('menu')` |
|
|
907
|
+
| `menuitem` | `menuitem` | `getByRole('menuitem')` |
|
|
908
|
+
| `tab` | `tab` | `getByRole('tab')` |
|
|
909
|
+
| `tabpanel` | `tabpanel` | `getByRole('tabpanel')` |
|
|
910
|
+
| `list` | `list` | `getByRole('list')` |
|
|
911
|
+
| `listitem` | `listitem` | `getByRole('listitem')` |
|
|
912
|
+
| `table` | `table` | `getByRole('table')` |
|
|
913
|
+
| `row` | `row` | `getByRole('row')` |
|
|
914
|
+
| `cell` | `cell` | `getByRole('cell')` |
|
|
915
|
+
| `column` / `columnheader` | `columnheader` | `getByRole('columnheader')` |
|
|
916
|
+
| `region` / `section` | `region` | `getByRole('region')` |
|
|
917
|
+
| `nav` / `navigation` | `navigation` | `getByRole('navigation')` |
|
|
918
|
+
| `banner` / `header` | `banner` | `getByRole('banner')` |
|
|
919
|
+
| `footer` | `contentinfo` | `getByRole('contentinfo')` |
|
|
920
|
+
| `alert` | `alert` | `getByRole('alert')` |
|
|
921
|
+
| `spinner` / `progressbar` | `progressbar` | `getByRole('progressbar')` |
|
|
922
|
+
| `slider` | `slider` | `getByRole('slider')` |
|
|
923
|
+
| `tree` | `tree` | `getByRole('tree')` |
|
|
924
|
+
| `treeitem` | `treeitem` | `getByRole('treeitem')` |
|
|
925
|
+
| `tooltip` | `tooltip` | `getByRole('tooltip')` |
|
|
926
|
+
| `uploader` / `file` | — | `setInputFiles()` |
|
|
927
|
+
| `frame` / `iframe` | — | `frameLocator()` |
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
## Compiler Rules Summary
|
|
932
|
+
|
|
933
|
+
### Rule 1: Build locator from selector YAML
|
|
934
|
+
|
|
935
|
+
```
|
|
936
|
+
locator = buildLocator(selector)
|
|
937
|
+
|
|
938
|
+
if selector.frame: base = page.frameLocator(frame)
|
|
939
|
+
elif dialogContext: base = dialogLocator
|
|
940
|
+
else: base = page
|
|
941
|
+
|
|
942
|
+
if selector.scope: base = base.getByLabel(scope)
|
|
943
|
+
|
|
944
|
+
switch selector.type:
|
|
945
|
+
testid → base.getByTestId(value)
|
|
946
|
+
role → base.getByRole(value, { name, exact })
|
|
947
|
+
label → base.getByLabel(value, { exact })
|
|
948
|
+
placeholder → base.getByPlaceholder(value, { exact })
|
|
949
|
+
text → base.getByText(value, { exact: match=='exact' })
|
|
950
|
+
locator → base.locator(value)
|
|
951
|
+
page → page.goto(value) or expect(page).toHaveURL()
|
|
952
|
+
upload → base.locator(value)
|
|
953
|
+
frame → page.frameLocator(value)
|
|
954
|
+
table → base.getByRole('table', { name: value })
|
|
955
|
+
|
|
956
|
+
if selector.nth > 0: locator = locator.nth(selector.nth)
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Rule 2: Map Gherkin verb to Playwright method
|
|
960
|
+
|
|
961
|
+
```
|
|
962
|
+
click → .click()
|
|
963
|
+
double click → .dblclick()
|
|
964
|
+
fill → .fill(value) or .pressSequentially(value) if contenteditable
|
|
965
|
+
clear → .clear()
|
|
966
|
+
check → .check()
|
|
967
|
+
uncheck → .uncheck()
|
|
968
|
+
toggle → .click()
|
|
969
|
+
select (native) → .selectOption(value)
|
|
970
|
+
select (custom) → .click() then getByRole('option', {name}).click()
|
|
971
|
+
upload → .setInputFiles('specs/storage/' + value)
|
|
972
|
+
hover → .hover()
|
|
973
|
+
drag ... to → .dragTo(targetLocator)
|
|
974
|
+
press (global) → page.keyboard.press(key)
|
|
975
|
+
press (on elem) → locator.press(key)
|
|
976
|
+
scroll to → .scrollIntoViewIfNeeded()
|
|
977
|
+
expand → .getByRole('button', {name:'Expand'}).click()
|
|
978
|
+
collapse → .getByRole('button', {name:'Collapse'}).click()
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### Rule 3: Map state to Playwright assertion
|
|
982
|
+
|
|
983
|
+
```
|
|
984
|
+
see → expect(locator).toBeVisible()
|
|
985
|
+
see ... hidden → expect(locator).toBeHidden()
|
|
986
|
+
see ... visible → expect(locator).toBeVisible()
|
|
987
|
+
see ... enabled → expect(locator).toBeEnabled()
|
|
988
|
+
see ... disabled→ expect(locator).toBeDisabled()
|
|
989
|
+
see ... checked → expect(locator).toBeChecked()
|
|
990
|
+
see ... unchecked→ expect(locator).not.toBeChecked()
|
|
991
|
+
see ... focused → expect(locator).toBeFocused()
|
|
992
|
+
see ... empty → expect(locator).toHaveText('')
|
|
993
|
+
see ... loading → expect(locator).toBeVisible() (spinner/progressbar)
|
|
994
|
+
see ... selected→ expect(locator).toHaveAttribute('aria-selected', 'true')
|
|
995
|
+
see ... sorted ascending → expect(locator).toHaveAttribute('aria-sort', 'ascending')
|
|
996
|
+
see ... sorted descending → expect(locator).toHaveAttribute('aria-sort', 'descending')
|
|
997
|
+
contains → expect(locator).toContainText(value)
|
|
998
|
+
has text → expect(locator).toHaveText(value)
|
|
999
|
+
has (attribute) → expect(locator).toHaveAttribute(attr, value)
|
|
1000
|
+
has ... rows → expect(locator.getByRole('row')).toHaveCount(N)
|
|
1001
|
+
has ... column → expect(locator.getByRole('columnheader', {name})).toBeVisible()
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Rule 4: Scope management
|
|
1005
|
+
|
|
1006
|
+
```
|
|
1007
|
+
Dialog enter: "see [X] dialog with {{title}}"
|
|
1008
|
+
→ dialogLocator = page.getByRole('dialog').filter({ hasText: title })
|
|
1009
|
+
→ all subsequent locators use dialogLocator as base
|
|
1010
|
+
|
|
1011
|
+
Dialog exit: "wait for [X] dialog with {{title}} is hidden"
|
|
1012
|
+
→ dialogLocator.waitFor({ state: 'hidden' })
|
|
1013
|
+
→ reset to page context
|
|
1014
|
+
|
|
1015
|
+
Frame enter: "switch to [X] frame"
|
|
1016
|
+
→ frameLocator = page.frameLocator(value)
|
|
1017
|
+
→ all subsequent locators use frameLocator as base
|
|
1018
|
+
|
|
1019
|
+
Frame exit: "switch to [main] frame"
|
|
1020
|
+
→ reset to page context
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### Rule 5: Table operations
|
|
1024
|
+
|
|
1025
|
+
```
|
|
1026
|
+
Table locator: page.getByRole('table', { name: selector.value })
|
|
1027
|
+
|
|
1028
|
+
Row by index: table.getByRole('row').nth(N)
|
|
1029
|
+
Row by filter: table.getByRole('row').filter({ hasText: filterValue })
|
|
1030
|
+
Cell by col: row.getByRole('cell').nth(columns[colName].index)
|
|
1031
|
+
Action in row: row.resolve([element])
|
|
1032
|
+
|
|
1033
|
+
Count: expect(table.locator('tbody').getByRole('row')).toHaveCount(N)
|
|
1034
|
+
Empty: expect(table.locator('tbody').getByRole('row')).toHaveCount(0)
|
|
1035
|
+
Column exists: expect(table.getByRole('columnheader', { name })).toBeVisible()
|
|
1036
|
+
Sort: table.getByRole('columnheader', { name }).click()
|
|
1037
|
+
Pagination: page.locator(pagination.next).click()
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
---
|
|
1041
|
+
|
|
1042
|
+
## What AI Discover Must Output
|
|
1043
|
+
|
|
1044
|
+
When AI visits a page via MCP Playwright, it must produce YAML entries with:
|
|
1045
|
+
|
|
1046
|
+
1. **Correct `type`** — detected from accessibility snapshot role
|
|
1047
|
+
2. **Correct `name`** — from accessible name in snapshot
|
|
1048
|
+
3. **`exact: true`** — when name is a substring of another element's name
|
|
1049
|
+
4. **`scope`** — when element appears in multiple landmarks (header + footer)
|
|
1050
|
+
5. **`match: exact`** — when text value is substring of other text on page
|
|
1051
|
+
6. **`variant`** — for dropdowns: inspect if `<select>` or custom div
|
|
1052
|
+
7. **`trigger`** — for uploads: find the visible button near hidden `input[type=file]`
|
|
1053
|
+
8. **`contenteditable`** — for rich editors: check if `[contenteditable="true"]`
|
|
1054
|
+
9. **`frame`** — for iframes: detect if element is inside a frame
|
|
1055
|
+
10. **`columns`** — for tables: map column headers to indices
|
|
1056
|
+
|
|
1057
|
+
## What AI Verify Must Fix
|
|
1058
|
+
|
|
1059
|
+
When tests fail, AI reads error messages and fixes YAML:
|
|
1060
|
+
|
|
1061
|
+
| Error pattern | YAML fix |
|
|
1062
|
+
|---|---|
|
|
1063
|
+
| `strict mode violation: resolved to N elements` | Add `exact: true` or `scope` or increase `nth` |
|
|
1064
|
+
| `element(s) not found` | Fix `name`, `type`, or `value` in YAML |
|
|
1065
|
+
| `resolved to N elements` (getByText) | Add `match: 'exact'` |
|
|
1066
|
+
| `not visible` / timeout | Element may need scroll, wait, or page didn't load (auth issue) |
|
|
1067
|
+
| `intercepted by overlay` | Need to close overlay first (press Escape, click backdrop) |
|
|
1068
|
+
| timeout on click | Element in iframe, behind overlay, or auth redirect |
|
|
1069
|
+
| `toHaveText` mismatch | Fix expected value in test-data.yaml |
|
|
1070
|
+
| `toHaveURL` mismatch | Fix page value in selectors.yaml |
|
|
1071
|
+
| `toHaveAttribute` mismatch | Fix attribute/pattern in selectors.yaml |
|