@vijaypjavvadi/bdd2pw 1.1.5 → 1.1.7

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/CHANGELOG.md CHANGED
@@ -1,755 +1,855 @@
1
- # bdd2pw — CHANGELOG
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- _Nothing yet._
11
-
12
- ## [1.1.5] — 2026-05-05
13
-
14
- ### Fixed — subject-less compact 'enters password "X"' form
15
-
16
- LLM produces compact verb-first input forms freely:
17
- ```
18
- And enters password "Password123"
19
- When enters username "alice"
20
- And types the password "secret"
21
- ```
22
-
23
- Rule 2b previously required a `${SUBJ}` prefix (`I` / `User` / `the user`).
24
- The compact form was hitting TODO. Rule 2b's SUBJ is now optional via
25
- `(?:${SUBJ}\s+)?`, so all four shapes match the same way:
26
-
27
- | Form | Status before 1.1.5 | Status after 1.1.5 |
28
- |---|---|---|
29
- | `I enter username "alice"` | ✅ rule 2b | ✅ rule 2b |
30
- | `User enters username "alice"` | ✅ rule 2b | ✅ rule 2b |
31
- | `the user enters username "alice"` | ✅ rule 2b | ✅ rule 2b |
32
- | `enters password "Password123"` | ❌ TODO | ✅ rule 2b |
33
- | `And enters username "alice"` | ❌ TODO | ✅ rule 2b |
34
-
35
- #### Why this is a one-line change
36
-
37
- The fix is purely the regex prefix:
38
- - Before: `^${SUBJ} (?:enter|enters|...) (?:the )?(.+?) ["']([^"']*)["']$`
39
- - After: `^(?:${SUBJ}\s+)?(?:enter|enters|...) (?:the )?(.+?) ["']([^"']*)["']$`
40
-
41
- The build function is unchanged — same `fillFieldBinding(...)` flow.
42
- Existing rule 2a (`I enter "X" into the field`) still claims its territory
43
- because its pattern has `["']<value>["']` early; the subject-less compact
44
- form ends with the value, which only matches rule 2b's structure.
45
-
46
- #### Known limitation (deferred)
47
-
48
- The vague form `user enters valid password` (no quoted value) is still a
49
- TODO. Without a value, we can't generate a working `.fill()` call. A human
50
- has to provide the credentials. This is a generation-input gap, not a rule
51
- gap — adding a TODO with explanatory comment would be misleading.
52
-
53
- #### Tests
54
-
55
- - `tests/unit/stepMatcher.test.ts` +4 new tests under
56
- `v1.1.5 — subject-less 'enters <field> "V"'`:
57
- - subject-less `enters password "X"`
58
- - subject-less `enters username "X"`
59
- - regression: subject-prefixed `User enters username "X"` still matches
60
- - subject-less `types the password "X"` (alt verb + article)
61
-
62
- #### Files
63
-
64
- - Modified: `src/transformers/stepMatcher.ts` (rule 2b regex prefix only).
65
- - Modified: `tests/unit/stepMatcher.test.ts` (+4 tests).
66
-
67
- ## [1.1.4] — 2026-05-05
68
-
69
- ### Fixed — article-leak in URL slugs + N5d without URL
70
-
71
- A run of `R-5D89B426-001.feature` on bdd2pw 1.1.3 cut failures from 6 → 2,
72
- but two real bugs remain:
73
-
74
- #### 1. English articles leak into URL regex slugs
75
-
76
- **Symptom:** `Then the user is redirected to a logged-in page` →
77
- emitted regex `/a[-_/]?logged-in/` → fails against
78
- `https://practicetestautomation.com/logged-in-successfully/` because the
79
- URL has no "a" before "logged-in".
80
-
81
- **Root cause:** rule 11b (and rules 10, N4, N5e) capture the page-name
82
- description and slugify it. The capture preserves English articles
83
- ("a", "an", "the") if they're not stripped by the rule's `(?:the )?`
84
- prefix — which only handles "the".
85
-
86
- **Fix:** new `stripArticles(s)` helper removes leading/internal/trailing
87
- "a"/"an"/"the" tokens before slugifying. Applied to rules 10, 11b, N4,
88
- N5e — every rule that converts a description to a regex slug.
89
-
90
- ```ts
91
- // Before: "redirected to a logged-in page" → "a[-_/]?logged-in"
92
- // After: "redirected to a logged-in page" → "logged-in" ✓ matches /logged-in-successfully/
93
-
94
- // Before: "remain on a login page" → "a[-_/]?login"
95
- // After: "remain on a login page" → "login" ✓
96
- ```
97
-
98
- #### 2. N5d required `at "URL"` suffix
99
-
100
- **Symptom:** 6 TODOs for `Given the user is on the login page` (no URL).
101
- N5d's pattern from 1.1.3 mandated `at "URL"`. LLM produces both forms.
102
-
103
- **Fix:** extended N5d pattern — `at "URL"` is now optional. Both
104
- `Given the user is on the login page` and
105
- `Given the user is on the login page at "URL"` map to `goto()`.
106
-
107
- #### Tests
108
-
109
- - `tests/unit/stepMatcher.test.ts` +5 new tests across 2 describe blocks
110
- (3 article-stripping cases + 2 N5d optional-URL cases).
111
-
112
- #### Production impact
113
-
114
- The 2 remaining failures from R-5D89B426-001 (`toHaveURL` timeouts on
115
- `/a[-_/]?logged-in/`) and the 6 TODOs (`Given the user is on the login
116
- page`) are both eliminated. Repin cloud-jobs-template to 1.1.4 to verify.
117
-
118
- #### Files
119
-
120
- - Modified: `src/transformers/stepMatcher.ts` (+stripArticles helper,
121
- applied to rules 10/11b/N4/N5e; N5d pattern extended).
122
- - Modified: `tests/unit/stepMatcher.test.ts` (+5 tests).
123
-
124
- ## [1.1.3] — 2026-05-05
125
-
126
- ### Fixed — production failure modes from cloud-jobs run on 1.1.2
127
-
128
- A run of `R-5D89B426-001.feature` on bdd2pw 1.1.2 produced **6 test failures**
129
- across 12 scenarios (Chromium + Firefox), with three distinct root causes —
130
- all in synthesised locators when the POM lacked the field. 1.1.3 is a
131
- correctness fix release: no new dialect coverage, but the assertions we
132
- already emit now actually work against real pages.
133
-
134
- #### 1. Cross-role synthesis (button-vs-link)
135
-
136
- **Symptom:** `await expect(page.getByRole("button", { name: "Logout" })).toBeVisible()`
137
- times out because the page has `<a>Log out</a>` (a link, not a button).
138
-
139
- **Fix:** new helper `synthRoleNameLocator(pageVar, name)` emits
140
- ```ts
141
- page.locator("a, button, [role='button'], [role='link']")
142
- .filter({ hasText: new RegExp("^L\\s*o\\s*g\\s*o\\s*u\\s*t$", "i") })
143
- .first()
144
- ```
145
- This handles both:
146
- - **Role mismatch** (LLM says "button", page uses `<a>` or vice versa).
147
- - **Spelling variance** (LLM writes "Logout", page renders "Log out") via a
148
- flexible regex with `\s*` between every character.
149
-
150
- Applied to N6 (visibility) and N7 (negative visibility) when synthesising.
151
- POM-field references stay unchanged (the picker already produces correct
152
- locators from the snapshot).
153
-
154
- #### 2. Strict-mode violations everywhere
155
-
156
- **Symptom:** `await expect(page.getByText("Your username is invalid")).toBeVisible()`
157
- fails with `strict mode violation: ... resolved to 2 elements` because the
158
- text appears in BOTH `<div id="error">` AND a `<b>` repeat highlight.
159
-
160
- **Fix:** every synthesised text locator now ends in `.first()`:
161
- - N5b page-level text → `getByText("X").first()`
162
- - N5 / N5c severity-message fallback → `getByText("V").first()`
163
- - N7 success / generic fallback → `getByText(desc).first()`
164
- - N7 error fallback → `getByRole("alert").first()`
165
-
166
- POM-field references untouched (POM fields are unique by construction).
167
-
168
- #### 3. New rules N5d + N5e — `is on page at URL` / negative redirect
169
-
170
- **Symptom:** 7 TODOs in BDD_REVIEW for `Given the user is on the login page
171
- at "URL"` (Background-style precondition with embedded URL) and 1 TODO for
172
- `And the user is NOT redirected away from the login page`.
173
-
174
- **Fix:** two new rules.
175
-
176
- | # | Pattern | Emits |
177
- |---|---|---|
178
- | N5d | `<subject> is/are/am on (the )? <X> page at 'URL'` | `goto()` (Background handles real navigation; this is narrative context) |
179
- | N5e | `<subject> is/are/am NOT redirected (away from\|from) (the )? <X> page` | `toHaveURL(<slug>)` (URL still contains page name → still on page) |
180
-
181
- #### Helpers (internal)
182
-
183
- - `flexibleNameRegex(name)` — builds `new RegExp("^X\\s*Y\\s*Z$", "i")` from
184
- a descriptive name. Used by N6/N7 synthesis.
185
- - `synthRoleNameLocator(pageVar, name)` — cross-role + flexible text filter.
186
- - `synthFlexibleTextLocator(pageVar, text)` — `getByText("X").first()`.
187
-
188
- #### Total rule count
189
-
190
- 26 → **30** (N5d, N5e new; N5b, N5c, N5, N6, N7 strict-mode-fixed).
191
-
192
- #### Tests
193
-
194
- Existing unit + e2e tests still pass — the synthesis changes don't affect
195
- test pom fixtures (POM fields are present, so synthesis branches don't fire).
196
- The fixes apply at runtime against real pages with non-trivial DOM.
197
-
198
- #### Production impact
199
-
200
- After 1.1.3 + repin, the same `R-5D89B426-001.feature` run that produced 6
201
- failures on 1.1.2 should produce 0 failures (or surface real bugs in the
202
- test design — neither the page nor the test was at fault for the 1.1.2
203
- failures, they were synthesis bugs).
204
-
205
- #### Files
206
-
207
- - Modified: `src/transformers/stepMatcher.ts` (+3 helpers, +2 rules N5d/N5e,
208
- N5/N5b/N5c/N5e/N6/N7 fallbacks all updated to use helpers).
209
-
210
- ## [1.1.2] — 2026-05-05
211
-
212
- ### Fixed — second batch of LLM-narrative dialect gaps
213
-
214
- A real cloud-jobs run of `R-5D89B426-001.feature` on bdd2pw 1.1.1 produced
215
- **11 unmatched warnings** across 4 scenarios — six new patterns the LLM
216
- service is generating that 1.1.1's 22 rules don't cover. All 12 tests
217
- "passed" but most assertions were silent TODOs. Same false-positive issue
218
- as before 1.1.1 fixed it for the first batch.
219
-
220
- 1.1.2 closes the second batch with **+4 new rules + extensions to N2/N6**.
221
- Total rule count: **22 → 26**.
222
-
223
- #### New / extended rules
224
-
225
- | # | Pattern | Emits |
226
- |---|---|---|
227
- | N1.5 | `[Given\|And] the <X> page is (displayed\|loaded\|shown\|visible\|present)` | `goto()` (Background-style precondition that drops the navigation verb) |
228
- | N2.5 | `<subject> leaves the <X> field empty (do not type anything)` | `// intentionally left empty: <X>` (subject-prefixed variant of N2) |
229
- | N5b | `the page (displays\|contains\|shows) [the message] 'X'` | `page.getByText("X").toBeVisible()` (page-level text assertion) |
230
- | N5c | `an <severity> message containing 'V' is displayed` | `toContainText("V")` against severity-aware field (error/success) |
231
- | N6 ext | `a Logout button is visible` (unquoted role+name) | `toBeVisible` against POM field, synthesises `getByRole` fallback |
232
-
233
- #### Why 4 rules + 2 extensions, not 6 fresh rules
234
-
235
- - N2 already handled `Leave the X field empty`; adding subject prefix
236
- (`<subject> leaves`) was a small extension in front of the existing pattern.
237
- - N6 already handled quoted `"X"` button visibility; adding a second
238
- capture branch for unquoted names reused the same build path.
239
-
240
- Both extensions are fully backwards-compatible — every step that matched in
241
- 1.1.1 still matches the same way in 1.1.2.
242
-
243
- #### Tests
244
-
245
- - Extended `tests/unit/stepMatcher.test.ts` with 5 new describe blocks (~10
246
- test cases) covering each new pattern + edge cases (POM field present
247
- vs synthesised fallback, unquoted vs quoted, severity dispatch).
248
-
249
- #### Production impact
250
-
251
- The cloud-jobs run that surfaced these gaps was hitting:
252
- - `Given the login page is displayed` (Background, every scenario × 4)
253
- - `When the user leaves the username field empty` (TC-003)
254
- - `And the page displays "Logged In Successfully"` (TC-001)
255
- - `Then the page contains the message "X"` (Outline rows × 3)
256
- - `Then an error message containing "X" is displayed` (TC-002)
257
- - `And a Logout button is visible` (TC-001)
258
-
259
- After 1.1.2 + repin, all six patterns produce real assertions instead of
260
- silent TODOs. TC-001 → genuine end-to-end check; TC-002/3/4 → actual error
261
- display assertions instead of silent passes.
262
-
263
- #### Files
264
-
265
- - Modified: `src/transformers/stepMatcher.ts` (+4 rules, N2 + N6 extended).
266
- - Modified: `tests/unit/stepMatcher.test.ts` (+5 describe blocks).
267
- - Doc bump 22 → 26 across `README.md`, `docs/SCOPE.md`, `docs/STATUS.md`.
268
-
269
- ## [1.1.1] — 2026-05-05
270
-
271
- ### Fixed — LLM-narrative Gherkin dialect
272
-
273
- Real test-case-generation services (cloud-jobs-template) produce more
274
- verbose, subject-less Gherkin than hand-authored suites. Before 1.1.1, six
275
- out of nine steps in a typical TC-001 (Successful login) fell through to
276
- `// TODO`. Result: forms didn't get filled, Submit clicked an empty form,
277
- URL never changed, `toHaveURL` timed out — the test reported as failed
278
- honestly. Worse, the negative scenarios (TC-002/3/4) reported as **passed**
279
- because every assertion was a silent no-op TODO.
280
-
281
- 1.1.1 adds **7 new step-matcher rules** plus the
282
- `examples/llm-narrative-login/` regression fixture (locked from a real
283
- production run, `j-20f3defc3044f7ec`).
284
-
285
- #### New rules
286
-
287
- | # | Pattern | Emits |
288
- |---|---|---|
289
- | N1 | `Locate the X (input)? field and (enter\|type\|fill) 'V'` | `<field>.fill("V")` via stripUiSuffix + findField |
290
- | N2 | `Leave the X field empty (do not type anything)` | `// intentionally left empty: <X>` (explicit comment) |
291
- | N3 | `Observe ...` / `Note ...` | `// observation: <X>` |
292
- | N4 | `URL does (not\|n't) change (to <description>)?` | `not.toHaveURL(new RegExp("<slug>"))` |
293
- | N5 | `... such as 'V'` / `... (e.g., 'V')` / `... indicating 'V'` / `... like 'V'` | `toContainText("V")` against error/success field |
294
- | N6 | `A 'X' (button\|link\|...)? is visible on the page` | `toBeVisible` against POM field, fallback to synthesised `getByRole(role, { name: 'X' })` |
295
- | N7 | `No 'X' (button\|...)? appears` / `No <noun> are/is displayed` | `not.toBeVisible` against POM field, fallback to synthesised role/text locator |
296
-
297
- `Navigate to <URL>` is already handled by rule 1 (the optional SUBJ prefix
298
- accepts subject-less variants); `Click the 'X' button` is already handled
299
- by rule 3. Both verified against the LLM fixture.
300
-
301
- Total rule count: **15 → 22** deterministic patterns.
302
-
303
- #### Helper: `findFieldByDescription`
304
-
305
- New module-private helper in `stepMatcher.ts`. Strips trailing UI-element
306
- words (`input field`, `field`, `box`, `element`, ...) from a description
307
- then defers to `findField` with the appropriate suffix preferences. Used
308
- by rules N1, N6, N7. Pure deterministic, no fuzzy matching.
309
-
310
- #### Per design call (4): negative assertions never drop to TODO
311
-
312
- For `No 'X' button appears` / `No error messages are displayed` / `A 'X' is
313
- visible`, when the POM has no matching field, the rule **synthesises** a
314
- `getByRole`/`getByText` locator from the description rather than emitting
315
- `// TODO`. This turns false-positive passes (silent no-op TODOs) into
316
- honest assertions that actually exercise the page.
317
-
318
- Caveat: synthesised locators can be over-permissive (e.g.
319
- `getByRole('button', { name: 'Log out' })` will match a logout link with
320
- `role="button"` aria attribute even if the page layout differs from the
321
- fixture). When this matters, hand-edit the spec — the rule report flags
322
- none of these synthesises as warnings, so they're invisible to the report.
323
-
324
- #### Tests
325
-
326
- - New e2e: `tests/e2e/llmNarrativeLogin.test.ts` — locks the
327
- `examples/llm-narrative-login/` fixture as a regression. Asserts:
328
- - 0 warning-level review items (every step matches a rule).
329
- - All 4 scenarios produce real fills/clicks (no TODOs).
330
- - The new rules emit the expected matchers (`not.toHaveURL`,
331
- `toContainText`, `toBeVisible`, etc.) against the right locators.
332
- - Extended unit tests: `tests/unit/stepMatcher.test.ts` — one `describe`
333
- block per new rule covering subject-less variants, edge cases, and
334
- POM-vs-synthesised-locator branching.
335
-
336
- #### Files
337
-
338
- - New: `examples/llm-narrative-login/{login.feature, snapshot.json, README.md}`.
339
- - New: `tests/e2e/llmNarrativeLogin.test.ts`.
340
- - Modified: `src/transformers/stepMatcher.ts` (+7 rules, +stripUiSuffix,
341
- +findFieldByDescription).
342
- - Modified: `tests/unit/stepMatcher.test.ts` (+~16 tests across 7 describe blocks).
343
-
344
- #### Migration notes
345
-
346
- Pure additive — no existing rule changed semantics. Specs that already
347
- matched 15 rules continue to match the same way. The 7 new rules **only**
348
- fire on patterns that previously produced `// TODO` warnings, so existing
349
- fixtures (`practice-test-login`, `cms-login`) regress to zero diff.
350
-
351
- If you have a cloud-jobs-template pinned to 1.1.0, bump to 1.1.1 and
352
- re-run any failing TC-001-style scenario — fills should now be real.
353
-
354
- ## [1.1.0] — 2026-05-05
355
-
356
- ### Added — `--self-healing` flag (scaffold-level integration)
357
-
358
- Optional integration with the local `self_healing_stage_services` pipeline
359
- ([source](https://github.com/...)). When enabled, generated POMs register
360
- every locator with the offline self-heal pipeline so the model knows which
361
- locators each test references.
362
-
363
- **Scope of v1.1.0** — registration + JSONL logging only. **Action-time
364
- healing is v1.2** (see "Deferred" below).
365
-
366
- #### What `--self-healing` does
367
-
368
- - New CLI flag: `bdd2pw scaffold ... --self-healing` (off by default).
369
- Programmatic API: `scaffold({ ..., selfHealing: true })`.
370
- - Plumbs `selfHealingShim: true` to `@vijaypjavvadi/pw-emit`'s
371
- `emitPageObject`. Emitted POMs now wrap every locator initialiser:
372
-
373
- ```ts
374
- // Before (default):
375
- this.usernameInput = page.getByLabel("Username");
376
-
377
- // With --self-healing:
378
- this.usernameInput = healOrThrow(page, {
379
- preferred: page.getByLabel("Username"),
380
- context: { page: "LoginPage", name: "usernameInput" },
381
- });
382
- ```
383
- - New scaffolder branch generates **`lib/heal.ts`** in the target repo —
384
- a self-contained TypeScript helper that:
385
- - Exports `healOrThrow(page, { preferred, context })` matching pw-emit's
386
- contract.
387
- - Registers every locator creation event to
388
- **`artefacts/heal-events.jsonl`** for the offline self-heal pipeline
389
- (`E:\EB1A_Research\self_healing_stage_services`) to consume.
390
- - Optional fire-and-forget heartbeat to `${SELF_HEALING_URL}/api/v1/register`
391
- when the env var is set. Best-effort — failures are silent.
392
- - Honours `HEAL_DISABLE=1` for CI runs that don't want the artefact noise.
393
- - Honours `HEAL_EVENTS_PATH` to override the JSONL path.
394
- - Patches the generated `tsconfig.json` with a path alias:
395
- ```jsonc
396
- "compilerOptions": {
397
- "baseUrl": ".",
398
- "paths": { "@platform/sdk-self-healing": ["./lib/heal"] }
399
- }
400
- ```
401
- so the POM's `import { healOrThrow } from "@platform/sdk-self-healing"`
402
- resolves to the local helper. No external SDK dependency.
403
- - Creates `artefacts/.gitkeep` so the JSONL output directory exists on a
404
- fresh checkout.
405
-
406
- #### What `--self-healing` does NOT do (v1.2 scope)
407
-
408
- **Action-time healing.** When `await loginPage.usernameInput.click()` fails
409
- because the locator no longer matches, this v1.1 helper will not catch the
410
- failure, POST `/api/v1/heal`, and retry with the suggested locator. That
411
- requires a Locator-wrapping proxy that intercepts every action method
412
- (`.click`, `.fill`, `.check`, `.selectOption`, `.hover`, `.focus`,
413
- `.textContent`, `.innerText`, `.getAttribute`, `.isVisible`,
414
- `.waitForSelector`, ...).
415
-
416
- The existing self-healing SDK (`self_healing_stage_services/sdk/playwright/self_healing_page.js`)
417
- does this — but for *string-selector*-based test code (`page.fill('#username', ...)`).
418
- pw-emit's POMs use Locator objects, so the v1.2 wrapper has a different
419
- shape. Tracked for v1.2.
420
-
421
- In the meantime, the JSONL feed gives the offline self-heal pipeline visibility
422
- into what locators each test references, which is the v1 ranker's input
423
- anyway — so v1.1 is functionally useful even without action-time recovery.
424
-
425
- #### Files
426
-
427
- - New: `templates/heal.ts.tmpl` — the heal helper template.
428
- - Modified: `src/types.ts`, `src/cli.ts`, `src/index.ts`,
429
- `src/repo/projectScaffolder.ts`.
430
-
431
- ## [1.0.1] — 2026-05-04
432
-
433
- ### Fixed
434
-
435
- - **New step-matcher rule 11a — `URL contains "X"`.** Steps like
436
- `Then user redirected to dashboard (URL contains "/dashboard")` or
437
- `Then URL contains "/foo?bar=baz"` now correctly emit
438
- `await expect(page).toHaveURL(new RegExp("..."))`. Previously these fell
439
- through to rule 11 (the greedy "redirected to ..." rule), which produced
440
- a wrong assertion or dropped the step to `BDD_REVIEW.md` as a TODO.
441
- - The captured URL fragment is **regex-escaped** before being wrapped in
442
- `new RegExp(...)`, so metacharacters like `?`, `+`, `.`, `(`, `)`, `[`, `]`
443
- in URLs (query strings, encoded paths) match literally.
444
- - Rule order matters: 11a is tried **before** 11b (the redirect rule) because
445
- 11b's pattern is greedy and would otherwise swallow the parenthetical hint.
446
-
447
- ### Tests
448
-
449
- - `tests/unit/stepMatcher.test.ts` — added two regression cases:
450
- - Plain URL fragment (`/dashboard`) → exact `new RegExp("/dashboard")`.
451
- - Fragment with regex metacharacter (`/search?q=test`) → `?` is escaped to
452
- `\?` so the literal `?` matches.
453
-
454
- ### Docs
455
-
456
- - Bumped rule count from 14 to 15 across `README.md`, `docs/SCOPE.md`,
457
- `docs/STATUS.md`.
458
-
459
- ## [1.0.0] — 2026-05-03
460
-
461
- First public npm release. Six phases shipped, two real-world fixtures locked,
462
- 101 / 101 tests green. See [`docs/STATUS.md`](./docs/STATUS.md) for the runtime
463
- snapshot and [`docs/SCOPE.md`](./docs/SCOPE.md) §17 for the full
464
- implementation-time decisions log.
465
-
466
- ### Headline
467
-
468
- `feature in → ready-to-run Playwright tests out`. Rule-based
469
- deterministic generation (no LLM in v1.0). CLI + HTTP service. Same code path
470
- through both surfaces.
471
-
472
- ### Shipped in v1.0
473
-
474
- - **Phase 0** — `@vijaypjavvadi/pw-emit@1.0.0` extracted; bdd2pw consumes it as
475
- a regular dep (`^1.0.0`).
476
- - **Phase 1a** — Gherkin parser (`@cucumber/gherkin`), repo scanner (ts-morph),
477
- POM resolver (reuse / augment / create), file-snapshot discovery, locator
478
- picker (priority chain + status-region special case), 14-rule step matcher,
479
- emitter facade, end-to-end `scaffold()`. Locked against `practice-test-login`
480
- (7/7 mapped, 0 warnings).
481
- - **Phase 1b** — Live browser discovery via direct `playwright.chromium.launch()`
482
- (chose direct Playwright over `@playwright/mcp` for simpler in-process control).
483
- Status-region visibility-filter bypass.
484
- - **Phase 1c** — Second real fixture `cms-login` (third-person dialect),
485
- 0 warnings first try. Subject-agnostic prefix locked.
486
- - **Phase 2** — `analyze` (dry-run) + `update-pom` (append-only AST surgery via
487
- ts-morph; hard-fails on missing POM). `BDD_REVIEW.md` writer. `tsc --noEmit`
488
- validator (soft-fail).
489
- - **Phase 3** — Express HTTP service `:4300`. Three async workers, ULID job
490
- IDs, Zod request validation, Pino structured logs, archiver-streamed zip
491
- artefacts. Per-job tmpdir contains blast radius for HTTP scaffold runs.
492
-
493
- ### CLI
494
-
495
- ```
496
- bdd2pw scaffold <feature> --url <url> --page <Name> --repo <dir> [opts]
497
- bdd2pw analyze <feature> --url <url> [opts]
498
- bdd2pw update-pom --page <Name> --url <url> --repo <dir>
499
- bdd2pw serve [--port 4300]
500
- ```
501
-
502
- ### HTTP
503
-
504
- `POST /scaffold | /analyze | /update-pom` → `202 + { jobId, links }`.
505
- `GET /jobs/:id`, `GET /jobs/:id/artifact` (zip), `GET /jobs/:id/log`,
506
- `GET /healthz`, `GET /readyz`, `GET /version`.
507
-
508
- ### Tests
509
-
510
- 101 / 101 across 10 Vitest files. Includes a full HTTP regression that asserts
511
- the artefact response body starts with the ZIP magic bytes `PK\x03\x04`.
512
-
513
- ### Distribution
514
-
515
- - npm: `@vijaypjavvadi/bdd2pw@1.0.0` (public, scoped, with provenance).
516
- - GitHub: tag-triggered `release.yml` workflow auto-publishes on `vX.Y.Z` push.
517
- - CI matrix: Ubuntu / macOS / Windows × Node 18 / 20 / 22.
518
-
519
- ### Deferred to v1.1+
520
-
521
- - LLM fallback for vague steps (governance-routed). Real fixtures hit 100%
522
- rule coverage so this was off the critical path.
523
- - SQLite-backed job persistence (currently in-memory `Map`).
524
- - Built-in `auth.setup.ts` generator. Today: bring your own `--storage-state`.
525
- - Multi-page navigation discovery, `playwright-bdd` runtime mode, API mocking
526
- scaffolds, visual regression — all post-v1.1.
527
-
528
- ### Earlier-phase notes (kept for history)
529
-
530
- #### Phase 3 — HTTP service is real
531
-
532
- The `POST /scaffold`, `POST /analyze`, `POST /update-pom` routes now actually do work. Same code path as the CLI; HTTP is a thin shell.
533
-
534
- #### What landed
535
-
536
- - **Workers in `src/http/routes.ts`** — three async `Promise` workers, one per `POST` endpoint. Each accepts the parsed Zod-validated body, calls the matching `scaffold()` / `analyze()` / `updatePom()` function from `src/index.ts`, and updates the in-memory job state through `running` → `completed` / `failed`.
537
- - **Per-job artifact dir.** `POST /scaffold` overrides the caller-provided `repo` with `<os.tmpdir()>/bdd2pw-jobs/<jobId>/`. This contains the blast radius — the HTTP user can't write outside that sandbox even if they pass an unsafe path.
538
- - **`streamArtifactZip()` in `src/http/artifacts.ts`** — uses `archiver` to zip the artifact dir on the fly, streaming directly to the response. Excludes `node_modules`, `.git`, `test-results`, `playwright-report`. Headers: `application/zip` + `Content-Disposition: attachment; filename="bdd2pw-<jobId>.zip"`.
539
- - **`GET /jobs/:id/log`** — returns a plain-text dump of the job's warnings + errors. Real per-job pino log file tailing deferred to v1.1.
540
- - **`GET /jobs/:id/artifact`** failure modes covered: `404 JobNotFound`, `409 ArtifactNotReady` (job still running or failed), `410 ArtifactExpired` (artifact dir already swept).
541
-
542
- #### Tests
543
-
544
- - `tests/e2e/httpWorker.test.ts` — spins up the real Express app on a random port, drives it via `fetch()`, polls for completion. 11 assertions across 4 describe blocks:
545
- - `/healthz`, `/version`, `/readyz` respond correctly.
546
- - `POST /scaffold` validates the body and returns 400 for malformed input (missing fields, bad URL).
547
- - End-to-end: POST `/scaffold` with the practice-test-login fixture → poll `/jobs/:id` → reaches `completed` with no warnings → `GET /jobs/:id/artifact` returns a real zip (verified by the ZIP magic bytes `PK\x03\x04` at the start of the response body).
548
- - `GET /jobs/:id/log` returns a text/plain dump containing the job ID and status.
549
-
550
- #### Out of scope for v1.0 (deferred to v1.1+)
551
-
552
- - **Multi-tenancy / auth** — handled at the gateway (`modern-automation-platform:3000`), bdd2pw HTTP is single-tenant per process. Same pattern as `sel2pw`.
553
- - **Job queue + worker pool** — current model is one Promise per job, bounded by Node's event loop. SQLite-backed queue with `p-limit` on MCP browsers lands in v1.1.
554
- - **Granular per-stage progress events** — workers emit `parsing` / `emitting` / `completed`, but not the full 9-stage progress chain from the architecture doc. Sufficient for the UI to show "running" → "done"; finer-grained progress is a polish task.
555
- - **Real per-job log file** — `GET /jobs/:id/log` currently returns warnings/errors from the in-memory record. Pino multi-stream routing per job is v1.1.
556
-
557
- ### Phase 2 — `updatePom` ships, append-only by construction
558
-
559
- Replaces the `NotImplementedError` stub. **The third CLI command is now real.** Re-scanning a page that already has a Page Object merges newly-discovered locators in — without ever touching what's already there.
560
-
561
- #### Design choice: append-only over lock-blocks
562
-
563
- The original SCOPE proposed `// __BDD2PW_LOCK__` markers around generated content. Switched to **append-only via AST surgery** because:
564
-
565
- - No visible markers in user code (lock comments are noise in otherwise hand-readable files).
566
- - Stronger guarantee: enforced by construction, not by comments the user could strip.
567
- - No special-casing for hand-edits — if it's already there, it's preserved.
568
- - Smaller blast radius: bugs in updatePom can only ADD wrong things, never destroy existing code.
569
-
570
- Trade-off: stale fields (when the live page removes an element) are not auto-deleted. Intentional — staleness is recoverable, deletion is not.
571
-
572
- #### Hard rules `updatePom` enforces
573
-
574
- 1. Never deletes any property, method, or import.
575
- 2. Never renames anything.
576
- 3. Never modifies any existing method body — hand-edits are preserved byte-identical.
577
- 4. Skips field-name collisions (existing field wins).
578
- 5. Method synthesis is opt-in and deferred to v1.1.
579
- 6. Hard-fails if the POM file doesn't exist (suggests `scaffold` first).
580
- 7. Hard-fails if the class name doesn't match `--page`.
581
-
582
- #### Implementation
583
-
584
- - **`updatePom()` in `src/index.ts`** — replaces the stub. Uses `ts-morph` to read the existing POM AST, computes the new-fields diff against the live scan, and uses `addProperty` + `addStatements` to splice new content into the existing class. Then `save()` writes back. No re-emit, no overwrite.
585
- - **`renderLocatorExpr` imported from `@vijaypjavvadi/pw-emit`** so the appended constructor lines use the same locator format as `scaffold` would.
586
- - Honours `--no-discovery` and `--snapshot-file` flags for offline/CI use, mirroring `scaffold`.
587
- - Surfaces a `BDD_REVIEW.md` with diff stats: existing/discovered/new/skipped-collisions counts.
588
-
589
- #### Tests
590
-
591
- - `tests/e2e/updatePom.test.ts` — 11 assertions across 3 describe blocks. The critical cases:
592
- - **Hand-edits survive byte-identical** (the must-not-regress invariant): hand-written comments, `console.log` lines, and entire method bodies stay untouched.
593
- - **Custom helper methods survive** — anything outside the discovered IR is preserved.
594
- - **Existing field collisions are skipped** — if the user already declared `usernameInput` with a `#username` selector, updatePom does NOT overwrite with whatever the live snapshot would pick.
595
- - **Idempotent** — second invocation in a row adds zero fields.
596
- - **Hard-fail on missing class** — points at `scaffold`.
597
-
598
- ### Phase 1c — second real-world fixture clean (selenium14/LoginCMS)
599
-
600
- **Validated against `selenium14/AutomationFrameworkCucumberTestNG/src/test/resources/features/LoginCMS.feature` — third-party Cucumber dialect from a real OSS Selenium codebase, against `https://cms.anhtester.com/login`.** 0 warnings, 0 errors, every step mapped.
601
-
602
- This fixture exercises:
603
- - `User navigate to Login Page for Admin "URL"` — third-person subject + bare-text + quoted URL → `goto()`
604
- - `user enter email "admin@example.com" password "123456"` — **compound input step** (one Gherkin step, two field/value pairs) → two `.fill()` lines emitted
605
- - `click Login button` — no subject prefix → `loginButton.click()`
606
- - `user is redirected to the Dashboard page` — third-person `is redirected to` → `toHaveURL(/Dashboard/)`
607
-
608
- #### Bugs fixed this round
609
-
610
- - **Step matcher: subject-agnostic rules.** All 13 rules now accept `I | user | User | the user | the User` as the subject prefix (was hard-coded `I`). Verb forms also accept singular AND plural conjugation (`click | clicks`, `enter | enters`, etc.). For rule 3 (click) the subject is now optional entirely (`click Login button` works without any subject).
611
- - **Rule 1 (navigate): handles `navigate to <text> "<URL>"`.** Previous pattern only accepted `navigate to <target>`. New pattern: `(?:.+?\s+)?["']?<target>["']?` — the optional `<text> ` prefix is consumed, the quoted URL becomes the target.
612
- - **Rule 11 (redirect): accepts `is redirected`.** Was `(should be |am )?redirected`; now `(should be |am |is )?redirected`.
613
- - **Compound input rule (2c).** New rule for `SUBJ verb FIELD1 "V1" FIELD2 "V2" [...]` — extracts every (field, value) pair via a global regex pass and emits one `.fill()` per pair. Falls back to `null` (TODO) if any field doesn't resolve, so partial matches don't produce wrong code.
614
-
615
- #### Type addition (small)
616
-
617
- - **`StepBinding.customBody?: string`** — for compound steps that produce multiple TS statements per Gherkin step. The emitter writes `customBody` verbatim instead of synthesising a single line from `pomCall`/`assertion`. Existing rules unchanged; only the new compound rule uses this path.
618
-
619
- #### Orchestrator bug fix
620
-
621
- - **Background warnings now flow into `BDD_REVIEW.md`.** Previously only scenario-step warnings were collected; Background-step warnings were silently dropped. Now surfaced under `[Background]`.
622
-
623
- ### Phase 1b milestone — live discovery 7/7 against the real site
624
-
625
- **Same `practice-test-login` feature that needed a hand-authored snapshot in Phase 1a now passes 7/7 with `--url` only — bdd2pw scans the live page itself.** No snapshot file, no hand-tuning, real Chromium against `https://practicetestautomation.com/practice-test-login/`.
626
-
627
- #### Iteration arc (live-scan rounds)
628
-
629
- | Round | Pass / Fail | Root cause + fix |
630
- |---|---|---|
631
- | 1 | 4 / 3 | Scanner missed `<div id="error">` (no role/testid/aria-label) → broaden selector list to include `[id*=error i]`, `[class*=alert i]`, etc. |
632
- | 2 | 3 / 4 | Scanner picked up error div but field name was text-derived (`yourUsernameIsInvalid`) → step matcher's `findField("error", [...])` couldn't match it. Fix: in `synthFieldName`, when CSS id matches `STATUS_REGION_RE`, prefer id-derived name → field becomes `error`. Plus `isLikelyStatusRegion` bypasses the visibility filter (status banners are intentionally hidden until triggered). Plus implicit roles for `<button>`/`<a>`/`<input type=submit>` so picker emits `getByRole`. Plus dedup-by-locator-value (collapse `username` + `usernameInput` into one). |
633
- | 3 | 3 / 4 | Field name was right (`error`) but locator was wrong: picker emitted `getByText("Your username is invalid!")` not `locator("#error")`. Volatile text content was winning over the stable id. Fix: locator picker skips the `getByText` branch when element matches `STATUS_REGION_RE` — text changes per scenario, id is the stable handle. |
634
- | 4 | **7 / 0** | — |
635
-
636
- #### Bugs fixed this round
637
-
638
- - **`src/discovery/mcpClient.ts`:** broader selector list (`[id*=error]`, `[class*=alert]`, …); `implicitRole()` for native HTML form controls; `isLikelyStatusRegion()` visibility-filter bypass; `isVisible()` zero-size + display:none filter; tsconfig `/// <reference lib="dom" />` so the `page.evaluate()` callback type-checks.
639
- - **`src/transformers/locatorPicker.ts`:** new `STATUS_REGION_RE`; `synthFieldName` priority 0 = id-derived for status regions (so `<div id="error">` → `error`, not `yourUsernameIsInvalid`); `getByText` branch skipped for status regions; `dedupeLocators` now collapses by `(api, args)` identity, keeping the longer field name.
640
-
641
- #### What this proves
642
-
643
- Three of the original Phase 1a iteration rounds were **snapshot accuracy** bugs (fictional ARIA roles, fictional placeholders). With Phase 1b they're gone — the scanner sees what's actually there. The four Phase 1b rounds were rule/picker bugs surfaced by real DOM data, all fixed. Net result: a **single CLI invocation** with `--url` produces a runnable spec from a Cucumber `.feature` file with zero hand-edits, zero snapshot authoring.
644
-
645
- ### Phase 1b — real browser-based page discovery (initial implementation, earlier this round)
646
-
647
- Replaces the file-snapshot-only discovery with a real Chromium-launching scanner. Hand-authoring `snapshot.json` is no longer required for the common case; pass `--url` and bdd2pw scans the live page itself.
648
-
649
- #### What landed
650
-
651
- - **`scanPageWithBrowser()`** in `src/discovery/mcpClient.ts` — uses `playwright.chromium.launch()` directly. Walks the DOM with a curated selector list (inputs, buttons, links, selects, headings, anything with `[role]` / `[data-testid]` / `[aria-label]`), extracts per-element role / accessible name / label / placeholder / testId / text / best CSS selector, and returns a flat `ElementIR[]`-shaped list. Snapshot parser handles it without changes.
652
- - **`playwright`** added as an `optionalDependencies` entry. Dynamic-imported in the scanner with a clear install-hint error if missing (`"npm install -D playwright && npx playwright install chromium"`).
653
- - **Storage state support** — pass `--storage-state <path.json>` to scan an authenticated page; passed through to `browser.newContext({ storageState })`.
654
- - **Headed mode** — `--headed` shows the browser window, useful for debugging snapshot accuracy.
655
- - **Network-idle wait with fallback** — first attempt waits 5s for network idle (catches SPAs that finish loading lazily); falls back to `domcontentloaded` if idle never settles.
656
-
657
- #### Removed framing
658
-
659
- - Dropped `@playwright/mcp` from optional dependencies. **AQ-2 resolved:** MCP is an LLM-control protocol, not a programmatic browser API. We just need one URL → one snapshot, so direct `playwright` is one fewer process and one fewer protocol layer. The file `src/discovery/mcpClient.ts` keeps its name for now (rename is cosmetic, deferred).
660
-
661
- #### Backward compatibility
662
-
663
- - `--snapshot-file <path>` still works exactly as before. The orchestrator dispatches: snapshot file present → file path; otherwise → real browser. CI runs that don't want network access keep using pinned snapshot files (e.g. `examples/practice-test-login/snapshot.json`).
664
- - `practiceTestLogin.test.ts` still uses the file-snapshot path so the regression test runs without network.
665
-
666
- #### What this fixes
667
-
668
- The 3-of-6 iteration rounds in Phase 1a milestone that were "snapshot accuracy" bugs (fictional ARIA roles, fictional placeholders, etc.) — those scenarios should now Just Work because the scanner sees the actual DOM.
669
-
670
- ### Phase 1a milestone — first end-to-end green run (earlier this round)
671
-
672
- **The full rule-based pipeline produced a runnable Playwright spec from a real-world Cucumber `.feature` file, with zero hand-edits.** Validated against `https://practicetestautomation.com/practice-test-login/` — `npx playwright test --project=chromium` reports **7 passed (4.2s)**.
673
-
674
- #### Scope
675
-
676
- - Source feature: `examples/practice-test-login/login.feature` — 5 scenarios + 1 Scenario Outline (2 example rows) + Background = 7 generated `test()` blocks
677
- - Discovery source: hand-authored `examples/practice-test-login/snapshot.json` (Phase 1a fallback; real `@playwright/mcp` lands in Phase 1b)
678
- - Output target: `pages/login.page.ts`, `tests/login.spec.ts`, `playwright.config.ts`, `package.json`, `tsconfig.json`, `.gitignore`, `BDD_REVIEW.md`
679
-
680
- #### Iteration arc that got us there
681
-
682
- | Round | Pass / Fail | Root cause |
683
- |---|---|---|
684
- | 1 | 0 / 7 | Undeclared `loginPage` (facade emitted POM calls without instantiation) |
685
- | 2 | 4 / 3 | Rule 3 lost role hint; rule 8 required "the" not "an"; word-order mismatch in rule 2 |
686
- | 3 | 2 / 5 | Snapshot fictional ARIA roles (`role: "alert"` not on page) |
687
- | 4 | 1 / 6 | Snapshot fictional placeholders (`placeholder: "Username"` not on page) |
688
- | 5 | 6 / 1 | Rule 9 emitted `toHaveText` (exact); step said "containing" |
689
- | 6 | **7 / 0** | (none) |
690
-
691
- 3 of 6 rounds were rule/code bugs. 3 were snapshot accuracy — exactly what real `@playwright/mcp` integration will eliminate (Phase 1b).
692
-
693
- #### Bugs fixed this round (rule + code)
694
-
695
- - **`emitTestFile` facade (`src/emitters/facade.ts`):** prepend `const <pageVar> = new <Class>(page);` to every test body and every hook body. Previously the spec emitted `await loginPage.goto()` without ever declaring `loginPage`.
696
- - **`scaffold()` orchestrator (`src/index.ts`):** synthesise a `goto(): Promise<void>` POM method that calls `await this.page.goto(<url>)` when no goto exists. Without it, rule 1 emitted `await loginPage.goto()` against a method with no body.
697
- - **POM file naming (`src/index.ts`):** use `pageObjectFileName` from `@vijaypjavvadi/pw-emit` instead of `${camelCase(opts.page)}.page.ts`. Was emitting `loginPage.page.ts`; now emits `login.page.ts`.
698
- - **Reserved field-name filter (`src/index.ts`):** `page`, `context`, `browser` are now excluded from the "missing fields" set. Previously `loginPage.page.toHaveURL(...)` made `page` look like a missing field.
699
- - **Step matcher rule 2b (`src/transformers/stepMatcher.ts`):** new — handles reversed word order `I enter <field> "<value>"`. Original rule expected `I enter "<value>" into <field>`.
700
- - **Step matcher rule 3:** captures the explicit role suffix (`button` / `link` / `icon` / `tab`) and passes only that to `findField` so "click the login button" doesn't match `testLoginHeading`.
701
- - **Step matcher rule 8:** accepts `the | an | a error message`, prefers `Alert > Error > Message`-suffixed fields.
702
- - **Step matcher rule 9a:** new — `toContainText` semantics for `containing | that contains | with text`. Must run before rule 9b (exact text equality) which now also accepts `the | an | a`.
703
- - **Step matcher rules 11–13:** new — redirect (`toHaveURL` regex), attribute check (`toHaveAttribute`), URL prefix/contains (`toHaveURL` anchored regex).
704
- - **`findField` resolution order (`src/transformers/stepMatcher.ts`):** added a suffix-constrained pass — when preferredSuffixes are provided, only consider fields with those suffixes; if exactly one matches, use it. Stops "login" from matching `testLoginHeading` when there's a `submitButton` on the same page.
705
- - **`synthFieldName` (`src/transformers/locatorPicker.ts`):** added `cssSelectorToName()` fallback. When no name/label/placeholder/text is available, derive the field name from the CSS id (`#username` → `usernameInput`) instead of the ugly `inputElement` placeholder.
706
-
707
- #### Added
708
-
709
- - `examples/practice-test-login/` — pinned regression fixture (login.feature + snapshot.json + README)
710
- - `tests/e2e/practiceTestLogin.test.ts` — 22-assertion regression test for the full pipeline. Calls `scaffold()`, reads back POM + spec, asserts every binding pattern that should have landed.
711
- - `vitest.config.ts` — `hookTimeout: 30_000` for E2E test cold-start reads.
712
-
713
- ### Added — Phase 1a (initial implementation, earlier this round)
714
- - **Real Gherkin parser** — `src/parser/gherkinParser.ts`. Uses `@cucumber/gherkin`. Handles Background, Scenario, Scenario Outline + Examples, doc strings, data tables.
715
- - **Step matcher rules** — `src/transformers/stepMatcher.ts`. 10 rules covering navigation, input, click, select, check/uncheck, visibility (positive + negative), text equality, URL, error message. Falls through to a warning when no rule matches.
716
- - **Locator picker** — `src/transformers/locatorPicker.ts`. Priority chain `getByRole > getByLabel > getByPlaceholder > getByTestId > getByText > css > xpath`. Ambiguity flagged when rivals exist. Field-name synthesis with role-based suffixes (`usernameInput`, `signInButton`).
717
- - **POM resolver** — `src/transformers/pomResolver.ts`. Pure decision: REUSE / AUGMENT / CREATE based on existing fields vs referenced fields.
718
- - **Repo scanner** — `src/repo/repoScanner.ts`. `ts-morph`-based AST scan of `pages/*.page.ts`. Extracts class name, locator fields, methods (preserves bodies for AUGMENT mode).
719
- - **Project scaffolder** — `src/repo/projectScaffolder.ts`. Delegates to `@vijaypjavvadi/pw-emit`'s `emitProject()`. Idempotent.
720
- - **Page discovery (file-snapshot fallback)** — `src/discovery/mcpClient.ts`. Phase 1a reads a pre-captured JSON snapshot via `--snapshot-file`. Real `@playwright/mcp` integration scheduled for Phase 1b.
721
- - **Snapshot parser** — `src/discovery/snapshotParser.ts`. Accepts both flat-array and tree-shaped snapshots; flattens to `ElementIR[]`.
722
- - **Review report** — `src/reports/reviewReport.ts`. Writes `BDD_REVIEW.md` with severity sections (Errors / Warnings / Info).
723
- - **TSC validator** — `src/validate/tscRunner.ts`. Runs `tsc --noEmit` in the target repo, parses diagnostics into `ReviewItem`s. Soft-fails with a warning if `tsc` isn't installed.
724
- - **`scaffold()` end-to-end wired** — `src/index.ts`. Orchestrates: parse → scan repo → scaffold → discover → pick locators → match steps (provisional) → resolve POM → re-match against final POM → expand Scenario Outlines → emit POM + spec → tsc validate → review report.
725
- - **`analyze()` working** — same pipeline minus emit/validate. Returns the plan.
726
- - **CLI `--snapshot-file` flag** for the file-based discovery path.
727
- - **Example fixture** `examples/login-feature/snapshot.json` — hand-authored a11y tree for end-to-end testing.
728
- - **3 new unit test files**: `stepMatcher.test.ts`, `locatorPicker.test.ts`, `pomResolver.test.ts`.
729
-
730
- ### Still stubbed (next phases)
731
- - `updatePom()` — needs lock-block enforcement (Phase 2)
732
- - Real Playwright MCP integration (Phase 1b)
733
- - HTTP worker that calls `scaffold()` (Phase 3)
734
- - LLM fallback in stepMatcher (Phase 4)
735
-
736
- ### Added — Phase 0 (previous round)
737
- - Repository scaffold: package.json, tsconfig, ESLint, Prettier, Vitest, TypeDoc, MIT LICENSE
738
- - CI workflow (Node 18/20/22 × Ubuntu/macOS/Windows), Release workflow (tag-triggered npm publish with provenance)
739
- - `.changeset` config for versioning
740
- - `src/` skeleton: `cli.ts`, `server.ts`, `index.ts`, `types.ts`, `utils/{naming,logger}.ts`
741
- - Module stubs (now most replaced by Phase 1a implementations)
742
- - Project templates: `package.json.tmpl`, `playwright.config.ts.tmpl`, `tsconfig.json.tmpl`, `gitignore.tmpl`
743
- - Example fixture: `examples/login-feature/login.feature`
744
- - First passing unit test: `tests/unit/naming.test.ts`
745
- - `@vijaypjavvadi/pw-emit` consumed via `file:../pw-emit`
746
- - `src/emitters/facade.ts` rewritten to delegate to `pw-emit`
747
-
748
- ### Documents
749
- - `README.md` — project orientation, decisions log, phasing
750
- - `docs/SCOPE.md` — v1.0 scope, FR/NFR, CLI + HTTP surface
751
- - `docs/ARCHITECTURE.md` — component diagram, sequence flows, contracts, error model
752
-
753
- ## [0.1.0] — TBD
754
-
755
- Initial published placeholder. **No runtime functionality yet.** Phase 0 (extracting `@vijaypjavvadi/pw-emit` from `sel2pw`) blocks Phase 1 of this package.
1
+ # bdd2pw — CHANGELOG
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ _Nothing yet._
11
+
12
+ ## [1.1.7] — 2026-05-07
13
+
14
+ ### Fixed — N5d 'is on page "URL"' (no 'at') fell through to TODO
15
+
16
+ LLM produces three variants of the same Background-style precondition:
17
+
18
+ ```
19
+ Given the user is on the login page ← matched (N5d, 1.1.4)
20
+ Given the user is on the login page at "https://example.com/login" ← matched (N5d, 1.1.3)
21
+ Given the user is on the login page "https://example.com/login" ← TODO (1.1.6)
22
+ ```
23
+
24
+ The third form — quoted URL appended directly without the word "at" — was
25
+ dropping to TODO. No correctness impact in practice (cloud-jobs-template
26
+ injects a clean `Given I am on the login page` Background at the top of
27
+ every feature, which matched rule 1 → goto), but it filled
28
+ `BDD_REVIEW.md` with noise that obscured real issues.
29
+
30
+ Fix: inside N5d's optional URL group, the word "at" is now also optional:
31
+
32
+ - Before: `(?:\s+at\s+["']([^"']+)["'])?`
33
+ - After: `(?:\s+(?:at\s+)?["']([^"']+)["'])?`
34
+
35
+ All three forms above resolve to `goto()`.
36
+
37
+ ### Tests
38
+
39
+ - `tests/unit/stepMatcher.test.ts` +1 test:
40
+ `the user is on the login page "URL"` → goto.
41
+ Existing `at "URL"` and bare-page tests still pass (regression).
42
+
43
+ ## [1.1.6] — 2026-05-07
44
+
45
+ ### Fixed — subject-less 'Enter "X" in the field' fell through to TODO
46
+
47
+ Rule 2a (Input with explicit value + field) required a `${SUBJ}` prefix
48
+ (`I` / `User` / `the user`). Cloud-jobs-template runs against the LLM
49
+ stack with cache OFF surfaced subject-less variants like:
50
+
51
+ ```
52
+ When Enter 'student' in the username field
53
+ And Enter 'Password123' into the password field
54
+ ```
55
+
56
+ These hit no rule and silently passed (no fill, no failure). Same fix
57
+ shape as v1.1.5 applied to rule 2b — wrap `${SUBJ}` in
58
+ `(?:${SUBJ}\s+)?` so all four shapes match equivalently.
59
+
60
+ | Form | Status before 1.1.6 | Status after 1.1.6 |
61
+ |---|---|---|
62
+ | `I enter "alice" into the username field` | ✅ rule 2a | ✅ rule 2a |
63
+ | `User enters "alice" into the username field` | ✅ rule 2a | ✅ rule 2a |
64
+ | `Enter 'alice' in the username field` | ❌ TODO | ✅ rule 2a |
65
+ | `Enter "alice" into the password field` | ❌ TODO | ✅ rule 2a |
66
+
67
+ ### Fixed — descriptive parentheticals leaked into URL slug regexes
68
+
69
+ Rules 10 (remains-on), 11b (redirected-to), N4 (URL doesn't change),
70
+ and N5e (NOT redirected away) all slugify a captured page-name
71
+ description into a URL regex. Production runs hit:
72
+
73
+ ```
74
+ Then User is redirected to logged-in page (URL changes away from login page)
75
+ And user remains on login page (URL does not change away from login page)
76
+ ```
77
+
78
+ The end-anchor `(?: page)?$` in those rules doesn't fire when the string
79
+ ends in `)`, so the entire parenthetical was being captured as part of
80
+ the description. Slug became
81
+ `/logged-in[-_/]?page[-_/]?\(URL[-_/]?changes[-_/]?away.../`
82
+ which never matches a real URL — assertion silently failed in a way that
83
+ cucumber reported as pass-with-warning rather than actionable failure.
84
+
85
+ Added `stripParentheticals()` helper and `cleanSlugTarget()` combiner
86
+ that strips parentheticals + articles + trailing ` page`, applied to
87
+ all four affected rules. Rule 11a (`(URL contains 'X')`) still runs
88
+ first and still extracts authoritative URL fragments — only descriptive
89
+ parentheticals fall through to the cleaned slugifier.
90
+
91
+ ### Tests
92
+
93
+ - `tests/unit/stepMatcher.test.ts` +7 new tests under
94
+ `v1.1.6 — subject-less 'Enter ...'` and
95
+ `v1.1.6 — parenthetical prose in URL slug rules`:
96
+ - subject-less `Enter 'X' in the username field`
97
+ - subject-less `Enter "X" into the password field`
98
+ - regression: subject-prefixed `I enter "X" in ...` still matches
99
+ - rule 11b: parenthetical stripped, slug = `logged-in`
100
+ - rule 10: parenthetical stripped, slug = `login`
101
+ - rule N5e: parenthetical stripped, slug = `login`
102
+ - regression: rule 11a still wins for authoritative parentheticals
103
+
104
+ ### Why this matters
105
+
106
+ Today's cloud-jobs-template runs went 14/14 → 2/8 between two
107
+ consecutive runs of the same job because the LLM picked different
108
+ phrasings (`in` vs `into`) and added descriptive parentheticals to
109
+ redirect assertions. Pairing this release with TCG temperature=0 and
110
+ re-enabled response cache should stabilise pass-rate run-to-run.
111
+
112
+ ## [1.1.5] — 2026-05-05
113
+
114
+ ### Fixed — subject-less compact 'enters password "X"' form
115
+
116
+ LLM produces compact verb-first input forms freely:
117
+ ```
118
+ And enters password "Password123"
119
+ When enters username "alice"
120
+ And types the password "secret"
121
+ ```
122
+
123
+ Rule 2b previously required a `${SUBJ}` prefix (`I` / `User` / `the user`).
124
+ The compact form was hitting TODO. Rule 2b's SUBJ is now optional via
125
+ `(?:${SUBJ}\s+)?`, so all four shapes match the same way:
126
+
127
+ | Form | Status before 1.1.5 | Status after 1.1.5 |
128
+ |---|---|---|
129
+ | `I enter username "alice"` | ✅ rule 2b | ✅ rule 2b |
130
+ | `User enters username "alice"` | ✅ rule 2b | ✅ rule 2b |
131
+ | `the user enters username "alice"` | ✅ rule 2b | ✅ rule 2b |
132
+ | `enters password "Password123"` | ❌ TODO | ✅ rule 2b |
133
+ | `And enters username "alice"` | ❌ TODO | ✅ rule 2b |
134
+
135
+ #### Why this is a one-line change
136
+
137
+ The fix is purely the regex prefix:
138
+ - Before: `^${SUBJ} (?:enter|enters|...) (?:the )?(.+?) ["']([^"']*)["']$`
139
+ - After: `^(?:${SUBJ}\s+)?(?:enter|enters|...) (?:the )?(.+?) ["']([^"']*)["']$`
140
+
141
+ The build function is unchanged — same `fillFieldBinding(...)` flow.
142
+ Existing rule 2a (`I enter "X" into the field`) still claims its territory
143
+ because its pattern has `["']<value>["']` early; the subject-less compact
144
+ form ends with the value, which only matches rule 2b's structure.
145
+
146
+ #### Known limitation (deferred)
147
+
148
+ The vague form `user enters valid password` (no quoted value) is still a
149
+ TODO. Without a value, we can't generate a working `.fill()` call. A human
150
+ has to provide the credentials. This is a generation-input gap, not a rule
151
+ gap — adding a TODO with explanatory comment would be misleading.
152
+
153
+ #### Tests
154
+
155
+ - `tests/unit/stepMatcher.test.ts` +4 new tests under
156
+ `v1.1.5 — subject-less 'enters <field> "V"'`:
157
+ - subject-less `enters password "X"`
158
+ - subject-less `enters username "X"`
159
+ - regression: subject-prefixed `User enters username "X"` still matches
160
+ - subject-less `types the password "X"` (alt verb + article)
161
+
162
+ #### Files
163
+
164
+ - Modified: `src/transformers/stepMatcher.ts` (rule 2b regex prefix only).
165
+ - Modified: `tests/unit/stepMatcher.test.ts` (+4 tests).
166
+
167
+ ## [1.1.4] — 2026-05-05
168
+
169
+ ### Fixed — article-leak in URL slugs + N5d without URL
170
+
171
+ A run of `R-5D89B426-001.feature` on bdd2pw 1.1.3 cut failures from 6 → 2,
172
+ but two real bugs remain:
173
+
174
+ #### 1. English articles leak into URL regex slugs
175
+
176
+ **Symptom:** `Then the user is redirected to a logged-in page` →
177
+ emitted regex `/a[-_/]?logged-in/` → fails against
178
+ `https://practicetestautomation.com/logged-in-successfully/` because the
179
+ URL has no "a" before "logged-in".
180
+
181
+ **Root cause:** rule 11b (and rules 10, N4, N5e) capture the page-name
182
+ description and slugify it. The capture preserves English articles
183
+ ("a", "an", "the") if they're not stripped by the rule's `(?:the )?`
184
+ prefix — which only handles "the".
185
+
186
+ **Fix:** new `stripArticles(s)` helper removes leading/internal/trailing
187
+ "a"/"an"/"the" tokens before slugifying. Applied to rules 10, 11b, N4,
188
+ N5e — every rule that converts a description to a regex slug.
189
+
190
+ ```ts
191
+ // Before: "redirected to a logged-in page" → "a[-_/]?logged-in"
192
+ // After: "redirected to a logged-in page" → "logged-in" ✓ matches /logged-in-successfully/
193
+
194
+ // Before: "remain on a login page" → "a[-_/]?login"
195
+ // After: "remain on a login page" → "login" ✓
196
+ ```
197
+
198
+ #### 2. N5d required `at "URL"` suffix
199
+
200
+ **Symptom:** 6 TODOs for `Given the user is on the login page` (no URL).
201
+ N5d's pattern from 1.1.3 mandated `at "URL"`. LLM produces both forms.
202
+
203
+ **Fix:** extended N5d pattern — `at "URL"` is now optional. Both
204
+ `Given the user is on the login page` and
205
+ `Given the user is on the login page at "URL"` map to `goto()`.
206
+
207
+ #### Tests
208
+
209
+ - `tests/unit/stepMatcher.test.ts` +5 new tests across 2 describe blocks
210
+ (3 article-stripping cases + 2 N5d optional-URL cases).
211
+
212
+ #### Production impact
213
+
214
+ The 2 remaining failures from R-5D89B426-001 (`toHaveURL` timeouts on
215
+ `/a[-_/]?logged-in/`) and the 6 TODOs (`Given the user is on the login
216
+ page`) are both eliminated. Repin cloud-jobs-template to 1.1.4 to verify.
217
+
218
+ #### Files
219
+
220
+ - Modified: `src/transformers/stepMatcher.ts` (+stripArticles helper,
221
+ applied to rules 10/11b/N4/N5e; N5d pattern extended).
222
+ - Modified: `tests/unit/stepMatcher.test.ts` (+5 tests).
223
+
224
+ ## [1.1.3] — 2026-05-05
225
+
226
+ ### Fixed — production failure modes from cloud-jobs run on 1.1.2
227
+
228
+ A run of `R-5D89B426-001.feature` on bdd2pw 1.1.2 produced **6 test failures**
229
+ across 12 scenarios (Chromium + Firefox), with three distinct root causes —
230
+ all in synthesised locators when the POM lacked the field. 1.1.3 is a
231
+ correctness fix release: no new dialect coverage, but the assertions we
232
+ already emit now actually work against real pages.
233
+
234
+ #### 1. Cross-role synthesis (button-vs-link)
235
+
236
+ **Symptom:** `await expect(page.getByRole("button", { name: "Logout" })).toBeVisible()`
237
+ times out because the page has `<a>Log out</a>` (a link, not a button).
238
+
239
+ **Fix:** new helper `synthRoleNameLocator(pageVar, name)` emits
240
+ ```ts
241
+ page.locator("a, button, [role='button'], [role='link']")
242
+ .filter({ hasText: new RegExp("^L\\s*o\\s*g\\s*o\\s*u\\s*t$", "i") })
243
+ .first()
244
+ ```
245
+ This handles both:
246
+ - **Role mismatch** (LLM says "button", page uses `<a>` or vice versa).
247
+ - **Spelling variance** (LLM writes "Logout", page renders "Log out") via a
248
+ flexible regex with `\s*` between every character.
249
+
250
+ Applied to N6 (visibility) and N7 (negative visibility) when synthesising.
251
+ POM-field references stay unchanged (the picker already produces correct
252
+ locators from the snapshot).
253
+
254
+ #### 2. Strict-mode violations everywhere
255
+
256
+ **Symptom:** `await expect(page.getByText("Your username is invalid")).toBeVisible()`
257
+ fails with `strict mode violation: ... resolved to 2 elements` because the
258
+ text appears in BOTH `<div id="error">` AND a `<b>` repeat highlight.
259
+
260
+ **Fix:** every synthesised text locator now ends in `.first()`:
261
+ - N5b page-level text → `getByText("X").first()`
262
+ - N5 / N5c severity-message fallback → `getByText("V").first()`
263
+ - N7 success / generic fallback → `getByText(desc).first()`
264
+ - N7 error fallback → `getByRole("alert").first()`
265
+
266
+ POM-field references untouched (POM fields are unique by construction).
267
+
268
+ #### 3. New rules N5d + N5e — `is on page at URL` / negative redirect
269
+
270
+ **Symptom:** 7 TODOs in BDD_REVIEW for `Given the user is on the login page
271
+ at "URL"` (Background-style precondition with embedded URL) and 1 TODO for
272
+ `And the user is NOT redirected away from the login page`.
273
+
274
+ **Fix:** two new rules.
275
+
276
+ | # | Pattern | Emits |
277
+ |---|---|---|
278
+ | N5d | `<subject> is/are/am on (the )? <X> page at 'URL'` | `goto()` (Background handles real navigation; this is narrative context) |
279
+ | N5e | `<subject> is/are/am NOT redirected (away from\|from) (the )? <X> page` | `toHaveURL(<slug>)` (URL still contains page name → still on page) |
280
+
281
+ #### Helpers (internal)
282
+
283
+ - `flexibleNameRegex(name)` — builds `new RegExp("^X\\s*Y\\s*Z$", "i")` from
284
+ a descriptive name. Used by N6/N7 synthesis.
285
+ - `synthRoleNameLocator(pageVar, name)` — cross-role + flexible text filter.
286
+ - `synthFlexibleTextLocator(pageVar, text)` — `getByText("X").first()`.
287
+
288
+ #### Total rule count
289
+
290
+ 26 → **30** (N5d, N5e new; N5b, N5c, N5, N6, N7 strict-mode-fixed).
291
+
292
+ #### Tests
293
+
294
+ Existing unit + e2e tests still pass — the synthesis changes don't affect
295
+ test pom fixtures (POM fields are present, so synthesis branches don't fire).
296
+ The fixes apply at runtime against real pages with non-trivial DOM.
297
+
298
+ #### Production impact
299
+
300
+ After 1.1.3 + repin, the same `R-5D89B426-001.feature` run that produced 6
301
+ failures on 1.1.2 should produce 0 failures (or surface real bugs in the
302
+ test design — neither the page nor the test was at fault for the 1.1.2
303
+ failures, they were synthesis bugs).
304
+
305
+ #### Files
306
+
307
+ - Modified: `src/transformers/stepMatcher.ts` (+3 helpers, +2 rules N5d/N5e,
308
+ N5/N5b/N5c/N5e/N6/N7 fallbacks all updated to use helpers).
309
+
310
+ ## [1.1.2] — 2026-05-05
311
+
312
+ ### Fixed — second batch of LLM-narrative dialect gaps
313
+
314
+ A real cloud-jobs run of `R-5D89B426-001.feature` on bdd2pw 1.1.1 produced
315
+ **11 unmatched warnings** across 4 scenarios — six new patterns the LLM
316
+ service is generating that 1.1.1's 22 rules don't cover. All 12 tests
317
+ "passed" but most assertions were silent TODOs. Same false-positive issue
318
+ as before 1.1.1 fixed it for the first batch.
319
+
320
+ 1.1.2 closes the second batch with **+4 new rules + extensions to N2/N6**.
321
+ Total rule count: **22 → 26**.
322
+
323
+ #### New / extended rules
324
+
325
+ | # | Pattern | Emits |
326
+ |---|---|---|
327
+ | N1.5 | `[Given\|And] the <X> page is (displayed\|loaded\|shown\|visible\|present)` | `goto()` (Background-style precondition that drops the navigation verb) |
328
+ | N2.5 | `<subject> leaves the <X> field empty (do not type anything)` | `// intentionally left empty: <X>` (subject-prefixed variant of N2) |
329
+ | N5b | `the page (displays\|contains\|shows) [the message] 'X'` | `page.getByText("X").toBeVisible()` (page-level text assertion) |
330
+ | N5c | `an <severity> message containing 'V' is displayed` | `toContainText("V")` against severity-aware field (error/success) |
331
+ | N6 ext | `a Logout button is visible` (unquoted role+name) | `toBeVisible` against POM field, synthesises `getByRole` fallback |
332
+
333
+ #### Why 4 rules + 2 extensions, not 6 fresh rules
334
+
335
+ - N2 already handled `Leave the X field empty`; adding subject prefix
336
+ (`<subject> leaves`) was a small extension in front of the existing pattern.
337
+ - N6 already handled quoted `"X"` button visibility; adding a second
338
+ capture branch for unquoted names reused the same build path.
339
+
340
+ Both extensions are fully backwards-compatible — every step that matched in
341
+ 1.1.1 still matches the same way in 1.1.2.
342
+
343
+ #### Tests
344
+
345
+ - Extended `tests/unit/stepMatcher.test.ts` with 5 new describe blocks (~10
346
+ test cases) covering each new pattern + edge cases (POM field present
347
+ vs synthesised fallback, unquoted vs quoted, severity dispatch).
348
+
349
+ #### Production impact
350
+
351
+ The cloud-jobs run that surfaced these gaps was hitting:
352
+ - `Given the login page is displayed` (Background, every scenario × 4)
353
+ - `When the user leaves the username field empty` (TC-003)
354
+ - `And the page displays "Logged In Successfully"` (TC-001)
355
+ - `Then the page contains the message "X"` (Outline rows × 3)
356
+ - `Then an error message containing "X" is displayed` (TC-002)
357
+ - `And a Logout button is visible` (TC-001)
358
+
359
+ After 1.1.2 + repin, all six patterns produce real assertions instead of
360
+ silent TODOs. TC-001 → genuine end-to-end check; TC-002/3/4 → actual error
361
+ display assertions instead of silent passes.
362
+
363
+ #### Files
364
+
365
+ - Modified: `src/transformers/stepMatcher.ts` (+4 rules, N2 + N6 extended).
366
+ - Modified: `tests/unit/stepMatcher.test.ts` (+5 describe blocks).
367
+ - Doc bump 22 → 26 across `README.md`, `docs/SCOPE.md`, `docs/STATUS.md`.
368
+
369
+ ## [1.1.1] — 2026-05-05
370
+
371
+ ### Fixed — LLM-narrative Gherkin dialect
372
+
373
+ Real test-case-generation services (cloud-jobs-template) produce more
374
+ verbose, subject-less Gherkin than hand-authored suites. Before 1.1.1, six
375
+ out of nine steps in a typical TC-001 (Successful login) fell through to
376
+ `// TODO`. Result: forms didn't get filled, Submit clicked an empty form,
377
+ URL never changed, `toHaveURL` timed out — the test reported as failed
378
+ honestly. Worse, the negative scenarios (TC-002/3/4) reported as **passed**
379
+ because every assertion was a silent no-op TODO.
380
+
381
+ 1.1.1 adds **7 new step-matcher rules** plus the
382
+ `examples/llm-narrative-login/` regression fixture (locked from a real
383
+ production run, `j-20f3defc3044f7ec`).
384
+
385
+ #### New rules
386
+
387
+ | # | Pattern | Emits |
388
+ |---|---|---|
389
+ | N1 | `Locate the X (input)? field and (enter\|type\|fill) 'V'` | `<field>.fill("V")` via stripUiSuffix + findField |
390
+ | N2 | `Leave the X field empty (do not type anything)` | `// intentionally left empty: <X>` (explicit comment) |
391
+ | N3 | `Observe ...` / `Note ...` | `// observation: <X>` |
392
+ | N4 | `URL does (not\|n't) change (to <description>)?` | `not.toHaveURL(new RegExp("<slug>"))` |
393
+ | N5 | `... such as 'V'` / `... (e.g., 'V')` / `... indicating 'V'` / `... like 'V'` | `toContainText("V")` against error/success field |
394
+ | N6 | `A 'X' (button\|link\|...)? is visible on the page` | `toBeVisible` against POM field, fallback to synthesised `getByRole(role, { name: 'X' })` |
395
+ | N7 | `No 'X' (button\|...)? appears` / `No <noun> are/is displayed` | `not.toBeVisible` against POM field, fallback to synthesised role/text locator |
396
+
397
+ `Navigate to <URL>` is already handled by rule 1 (the optional SUBJ prefix
398
+ accepts subject-less variants); `Click the 'X' button` is already handled
399
+ by rule 3. Both verified against the LLM fixture.
400
+
401
+ Total rule count: **15 → 22** deterministic patterns.
402
+
403
+ #### Helper: `findFieldByDescription`
404
+
405
+ New module-private helper in `stepMatcher.ts`. Strips trailing UI-element
406
+ words (`input field`, `field`, `box`, `element`, ...) from a description
407
+ then defers to `findField` with the appropriate suffix preferences. Used
408
+ by rules N1, N6, N7. Pure deterministic, no fuzzy matching.
409
+
410
+ #### Per design call (4): negative assertions never drop to TODO
411
+
412
+ For `No 'X' button appears` / `No error messages are displayed` / `A 'X' is
413
+ visible`, when the POM has no matching field, the rule **synthesises** a
414
+ `getByRole`/`getByText` locator from the description rather than emitting
415
+ `// TODO`. This turns false-positive passes (silent no-op TODOs) into
416
+ honest assertions that actually exercise the page.
417
+
418
+ Caveat: synthesised locators can be over-permissive (e.g.
419
+ `getByRole('button', { name: 'Log out' })` will match a logout link with
420
+ `role="button"` aria attribute even if the page layout differs from the
421
+ fixture). When this matters, hand-edit the spec — the rule report flags
422
+ none of these synthesises as warnings, so they're invisible to the report.
423
+
424
+ #### Tests
425
+
426
+ - New e2e: `tests/e2e/llmNarrativeLogin.test.ts` — locks the
427
+ `examples/llm-narrative-login/` fixture as a regression. Asserts:
428
+ - 0 warning-level review items (every step matches a rule).
429
+ - All 4 scenarios produce real fills/clicks (no TODOs).
430
+ - The new rules emit the expected matchers (`not.toHaveURL`,
431
+ `toContainText`, `toBeVisible`, etc.) against the right locators.
432
+ - Extended unit tests: `tests/unit/stepMatcher.test.ts` — one `describe`
433
+ block per new rule covering subject-less variants, edge cases, and
434
+ POM-vs-synthesised-locator branching.
435
+
436
+ #### Files
437
+
438
+ - New: `examples/llm-narrative-login/{login.feature, snapshot.json, README.md}`.
439
+ - New: `tests/e2e/llmNarrativeLogin.test.ts`.
440
+ - Modified: `src/transformers/stepMatcher.ts` (+7 rules, +stripUiSuffix,
441
+ +findFieldByDescription).
442
+ - Modified: `tests/unit/stepMatcher.test.ts` (+~16 tests across 7 describe blocks).
443
+
444
+ #### Migration notes
445
+
446
+ Pure additive — no existing rule changed semantics. Specs that already
447
+ matched 15 rules continue to match the same way. The 7 new rules **only**
448
+ fire on patterns that previously produced `// TODO` warnings, so existing
449
+ fixtures (`practice-test-login`, `cms-login`) regress to zero diff.
450
+
451
+ If you have a cloud-jobs-template pinned to 1.1.0, bump to 1.1.1 and
452
+ re-run any failing TC-001-style scenario — fills should now be real.
453
+
454
+ ## [1.1.0] — 2026-05-05
455
+
456
+ ### Added — `--self-healing` flag (scaffold-level integration)
457
+
458
+ Optional integration with the local `self_healing_stage_services` pipeline
459
+ ([source](https://github.com/...)). When enabled, generated POMs register
460
+ every locator with the offline self-heal pipeline so the model knows which
461
+ locators each test references.
462
+
463
+ **Scope of v1.1.0** — registration + JSONL logging only. **Action-time
464
+ healing is v1.2** (see "Deferred" below).
465
+
466
+ #### What `--self-healing` does
467
+
468
+ - New CLI flag: `bdd2pw scaffold ... --self-healing` (off by default).
469
+ Programmatic API: `scaffold({ ..., selfHealing: true })`.
470
+ - Plumbs `selfHealingShim: true` to `@vijaypjavvadi/pw-emit`'s
471
+ `emitPageObject`. Emitted POMs now wrap every locator initialiser:
472
+
473
+ ```ts
474
+ // Before (default):
475
+ this.usernameInput = page.getByLabel("Username");
476
+
477
+ // With --self-healing:
478
+ this.usernameInput = healOrThrow(page, {
479
+ preferred: page.getByLabel("Username"),
480
+ context: { page: "LoginPage", name: "usernameInput" },
481
+ });
482
+ ```
483
+ - New scaffolder branch generates **`lib/heal.ts`** in the target repo —
484
+ a self-contained TypeScript helper that:
485
+ - Exports `healOrThrow(page, { preferred, context })` matching pw-emit's
486
+ contract.
487
+ - Registers every locator creation event to
488
+ **`artefacts/heal-events.jsonl`** for the offline self-heal pipeline
489
+ (`E:\EB1A_Research\self_healing_stage_services`) to consume.
490
+ - Optional fire-and-forget heartbeat to `${SELF_HEALING_URL}/api/v1/register`
491
+ when the env var is set. Best-effort — failures are silent.
492
+ - Honours `HEAL_DISABLE=1` for CI runs that don't want the artefact noise.
493
+ - Honours `HEAL_EVENTS_PATH` to override the JSONL path.
494
+ - Patches the generated `tsconfig.json` with a path alias:
495
+ ```jsonc
496
+ "compilerOptions": {
497
+ "baseUrl": ".",
498
+ "paths": { "@platform/sdk-self-healing": ["./lib/heal"] }
499
+ }
500
+ ```
501
+ so the POM's `import { healOrThrow } from "@platform/sdk-self-healing"`
502
+ resolves to the local helper. No external SDK dependency.
503
+ - Creates `artefacts/.gitkeep` so the JSONL output directory exists on a
504
+ fresh checkout.
505
+
506
+ #### What `--self-healing` does NOT do (v1.2 scope)
507
+
508
+ **Action-time healing.** When `await loginPage.usernameInput.click()` fails
509
+ because the locator no longer matches, this v1.1 helper will not catch the
510
+ failure, POST `/api/v1/heal`, and retry with the suggested locator. That
511
+ requires a Locator-wrapping proxy that intercepts every action method
512
+ (`.click`, `.fill`, `.check`, `.selectOption`, `.hover`, `.focus`,
513
+ `.textContent`, `.innerText`, `.getAttribute`, `.isVisible`,
514
+ `.waitForSelector`, ...).
515
+
516
+ The existing self-healing SDK (`self_healing_stage_services/sdk/playwright/self_healing_page.js`)
517
+ does this — but for *string-selector*-based test code (`page.fill('#username', ...)`).
518
+ pw-emit's POMs use Locator objects, so the v1.2 wrapper has a different
519
+ shape. Tracked for v1.2.
520
+
521
+ In the meantime, the JSONL feed gives the offline self-heal pipeline visibility
522
+ into what locators each test references, which is the v1 ranker's input
523
+ anyway — so v1.1 is functionally useful even without action-time recovery.
524
+
525
+ #### Files
526
+
527
+ - New: `templates/heal.ts.tmpl` — the heal helper template.
528
+ - Modified: `src/types.ts`, `src/cli.ts`, `src/index.ts`,
529
+ `src/repo/projectScaffolder.ts`.
530
+
531
+ ## [1.0.1] — 2026-05-04
532
+
533
+ ### Fixed
534
+
535
+ - **New step-matcher rule 11a — `URL contains "X"`.** Steps like
536
+ `Then user redirected to dashboard (URL contains "/dashboard")` or
537
+ `Then URL contains "/foo?bar=baz"` now correctly emit
538
+ `await expect(page).toHaveURL(new RegExp("..."))`. Previously these fell
539
+ through to rule 11 (the greedy "redirected to ..." rule), which produced
540
+ a wrong assertion or dropped the step to `BDD_REVIEW.md` as a TODO.
541
+ - The captured URL fragment is **regex-escaped** before being wrapped in
542
+ `new RegExp(...)`, so metacharacters like `?`, `+`, `.`, `(`, `)`, `[`, `]`
543
+ in URLs (query strings, encoded paths) match literally.
544
+ - Rule order matters: 11a is tried **before** 11b (the redirect rule) because
545
+ 11b's pattern is greedy and would otherwise swallow the parenthetical hint.
546
+
547
+ ### Tests
548
+
549
+ - `tests/unit/stepMatcher.test.ts` — added two regression cases:
550
+ - Plain URL fragment (`/dashboard`) → exact `new RegExp("/dashboard")`.
551
+ - Fragment with regex metacharacter (`/search?q=test`) → `?` is escaped to
552
+ `\?` so the literal `?` matches.
553
+
554
+ ### Docs
555
+
556
+ - Bumped rule count from 14 to 15 across `README.md`, `docs/SCOPE.md`,
557
+ `docs/STATUS.md`.
558
+
559
+ ## [1.0.0] — 2026-05-03
560
+
561
+ First public npm release. Six phases shipped, two real-world fixtures locked,
562
+ 101 / 101 tests green. See [`docs/STATUS.md`](./docs/STATUS.md) for the runtime
563
+ snapshot and [`docs/SCOPE.md`](./docs/SCOPE.md) §17 for the full
564
+ implementation-time decisions log.
565
+
566
+ ### Headline
567
+
568
+ `feature in → ready-to-run Playwright tests out`. Rule-based
569
+ deterministic generation (no LLM in v1.0). CLI + HTTP service. Same code path
570
+ through both surfaces.
571
+
572
+ ### Shipped in v1.0
573
+
574
+ - **Phase 0** — `@vijaypjavvadi/pw-emit@1.0.0` extracted; bdd2pw consumes it as
575
+ a regular dep (`^1.0.0`).
576
+ - **Phase 1a** — Gherkin parser (`@cucumber/gherkin`), repo scanner (ts-morph),
577
+ POM resolver (reuse / augment / create), file-snapshot discovery, locator
578
+ picker (priority chain + status-region special case), 14-rule step matcher,
579
+ emitter facade, end-to-end `scaffold()`. Locked against `practice-test-login`
580
+ (7/7 mapped, 0 warnings).
581
+ - **Phase 1b** — Live browser discovery via direct `playwright.chromium.launch()`
582
+ (chose direct Playwright over `@playwright/mcp` for simpler in-process control).
583
+ Status-region visibility-filter bypass.
584
+ - **Phase 1c** — Second real fixture `cms-login` (third-person dialect),
585
+ 0 warnings first try. Subject-agnostic prefix locked.
586
+ - **Phase 2** — `analyze` (dry-run) + `update-pom` (append-only AST surgery via
587
+ ts-morph; hard-fails on missing POM). `BDD_REVIEW.md` writer. `tsc --noEmit`
588
+ validator (soft-fail).
589
+ - **Phase 3** — Express HTTP service `:4300`. Three async workers, ULID job
590
+ IDs, Zod request validation, Pino structured logs, archiver-streamed zip
591
+ artefacts. Per-job tmpdir contains blast radius for HTTP scaffold runs.
592
+
593
+ ### CLI
594
+
595
+ ```
596
+ bdd2pw scaffold <feature> --url <url> --page <Name> --repo <dir> [opts]
597
+ bdd2pw analyze <feature> --url <url> [opts]
598
+ bdd2pw update-pom --page <Name> --url <url> --repo <dir>
599
+ bdd2pw serve [--port 4300]
600
+ ```
601
+
602
+ ### HTTP
603
+
604
+ `POST /scaffold | /analyze | /update-pom` → `202 + { jobId, links }`.
605
+ `GET /jobs/:id`, `GET /jobs/:id/artifact` (zip), `GET /jobs/:id/log`,
606
+ `GET /healthz`, `GET /readyz`, `GET /version`.
607
+
608
+ ### Tests
609
+
610
+ 101 / 101 across 10 Vitest files. Includes a full HTTP regression that asserts
611
+ the artefact response body starts with the ZIP magic bytes `PK\x03\x04`.
612
+
613
+ ### Distribution
614
+
615
+ - npm: `@vijaypjavvadi/bdd2pw@1.0.0` (public, scoped, with provenance).
616
+ - GitHub: tag-triggered `release.yml` workflow auto-publishes on `vX.Y.Z` push.
617
+ - CI matrix: Ubuntu / macOS / Windows × Node 18 / 20 / 22.
618
+
619
+ ### Deferred to v1.1+
620
+
621
+ - LLM fallback for vague steps (governance-routed). Real fixtures hit 100%
622
+ rule coverage so this was off the critical path.
623
+ - SQLite-backed job persistence (currently in-memory `Map`).
624
+ - Built-in `auth.setup.ts` generator. Today: bring your own `--storage-state`.
625
+ - Multi-page navigation discovery, `playwright-bdd` runtime mode, API mocking
626
+ scaffolds, visual regression — all post-v1.1.
627
+
628
+ ### Earlier-phase notes (kept for history)
629
+
630
+ #### Phase 3 — HTTP service is real
631
+
632
+ The `POST /scaffold`, `POST /analyze`, `POST /update-pom` routes now actually do work. Same code path as the CLI; HTTP is a thin shell.
633
+
634
+ #### What landed
635
+
636
+ - **Workers in `src/http/routes.ts`** — three async `Promise` workers, one per `POST` endpoint. Each accepts the parsed Zod-validated body, calls the matching `scaffold()` / `analyze()` / `updatePom()` function from `src/index.ts`, and updates the in-memory job state through `running` → `completed` / `failed`.
637
+ - **Per-job artifact dir.** `POST /scaffold` overrides the caller-provided `repo` with `<os.tmpdir()>/bdd2pw-jobs/<jobId>/`. This contains the blast radius — the HTTP user can't write outside that sandbox even if they pass an unsafe path.
638
+ - **`streamArtifactZip()` in `src/http/artifacts.ts`** — uses `archiver` to zip the artifact dir on the fly, streaming directly to the response. Excludes `node_modules`, `.git`, `test-results`, `playwright-report`. Headers: `application/zip` + `Content-Disposition: attachment; filename="bdd2pw-<jobId>.zip"`.
639
+ - **`GET /jobs/:id/log`** — returns a plain-text dump of the job's warnings + errors. Real per-job pino log file tailing deferred to v1.1.
640
+ - **`GET /jobs/:id/artifact`** failure modes covered: `404 JobNotFound`, `409 ArtifactNotReady` (job still running or failed), `410 ArtifactExpired` (artifact dir already swept).
641
+
642
+ #### Tests
643
+
644
+ - `tests/e2e/httpWorker.test.ts` — spins up the real Express app on a random port, drives it via `fetch()`, polls for completion. 11 assertions across 4 describe blocks:
645
+ - `/healthz`, `/version`, `/readyz` respond correctly.
646
+ - `POST /scaffold` validates the body and returns 400 for malformed input (missing fields, bad URL).
647
+ - End-to-end: POST `/scaffold` with the practice-test-login fixture → poll `/jobs/:id` → reaches `completed` with no warnings → `GET /jobs/:id/artifact` returns a real zip (verified by the ZIP magic bytes `PK\x03\x04` at the start of the response body).
648
+ - `GET /jobs/:id/log` returns a text/plain dump containing the job ID and status.
649
+
650
+ #### Out of scope for v1.0 (deferred to v1.1+)
651
+
652
+ - **Multi-tenancy / auth** — handled at the gateway (`modern-automation-platform:3000`), bdd2pw HTTP is single-tenant per process. Same pattern as `sel2pw`.
653
+ - **Job queue + worker pool** — current model is one Promise per job, bounded by Node's event loop. SQLite-backed queue with `p-limit` on MCP browsers lands in v1.1.
654
+ - **Granular per-stage progress events** — workers emit `parsing` / `emitting` / `completed`, but not the full 9-stage progress chain from the architecture doc. Sufficient for the UI to show "running" → "done"; finer-grained progress is a polish task.
655
+ - **Real per-job log file** — `GET /jobs/:id/log` currently returns warnings/errors from the in-memory record. Pino multi-stream routing per job is v1.1.
656
+
657
+ ### Phase 2 — `updatePom` ships, append-only by construction
658
+
659
+ Replaces the `NotImplementedError` stub. **The third CLI command is now real.** Re-scanning a page that already has a Page Object merges newly-discovered locators in — without ever touching what's already there.
660
+
661
+ #### Design choice: append-only over lock-blocks
662
+
663
+ The original SCOPE proposed `// __BDD2PW_LOCK__` markers around generated content. Switched to **append-only via AST surgery** because:
664
+
665
+ - No visible markers in user code (lock comments are noise in otherwise hand-readable files).
666
+ - Stronger guarantee: enforced by construction, not by comments the user could strip.
667
+ - No special-casing for hand-edits — if it's already there, it's preserved.
668
+ - Smaller blast radius: bugs in updatePom can only ADD wrong things, never destroy existing code.
669
+
670
+ Trade-off: stale fields (when the live page removes an element) are not auto-deleted. Intentional — staleness is recoverable, deletion is not.
671
+
672
+ #### Hard rules `updatePom` enforces
673
+
674
+ 1. Never deletes any property, method, or import.
675
+ 2. Never renames anything.
676
+ 3. Never modifies any existing method body — hand-edits are preserved byte-identical.
677
+ 4. Skips field-name collisions (existing field wins).
678
+ 5. Method synthesis is opt-in and deferred to v1.1.
679
+ 6. Hard-fails if the POM file doesn't exist (suggests `scaffold` first).
680
+ 7. Hard-fails if the class name doesn't match `--page`.
681
+
682
+ #### Implementation
683
+
684
+ - **`updatePom()` in `src/index.ts`** — replaces the stub. Uses `ts-morph` to read the existing POM AST, computes the new-fields diff against the live scan, and uses `addProperty` + `addStatements` to splice new content into the existing class. Then `save()` writes back. No re-emit, no overwrite.
685
+ - **`renderLocatorExpr` imported from `@vijaypjavvadi/pw-emit`** so the appended constructor lines use the same locator format as `scaffold` would.
686
+ - Honours `--no-discovery` and `--snapshot-file` flags for offline/CI use, mirroring `scaffold`.
687
+ - Surfaces a `BDD_REVIEW.md` with diff stats: existing/discovered/new/skipped-collisions counts.
688
+
689
+ #### Tests
690
+
691
+ - `tests/e2e/updatePom.test.ts` — 11 assertions across 3 describe blocks. The critical cases:
692
+ - **Hand-edits survive byte-identical** (the must-not-regress invariant): hand-written comments, `console.log` lines, and entire method bodies stay untouched.
693
+ - **Custom helper methods survive** — anything outside the discovered IR is preserved.
694
+ - **Existing field collisions are skipped** — if the user already declared `usernameInput` with a `#username` selector, updatePom does NOT overwrite with whatever the live snapshot would pick.
695
+ - **Idempotent** — second invocation in a row adds zero fields.
696
+ - **Hard-fail on missing class** — points at `scaffold`.
697
+
698
+ ### Phase 1c — second real-world fixture clean (selenium14/LoginCMS)
699
+
700
+ **Validated against `selenium14/AutomationFrameworkCucumberTestNG/src/test/resources/features/LoginCMS.feature` — third-party Cucumber dialect from a real OSS Selenium codebase, against `https://cms.anhtester.com/login`.** 0 warnings, 0 errors, every step mapped.
701
+
702
+ This fixture exercises:
703
+ - `User navigate to Login Page for Admin "URL"` — third-person subject + bare-text + quoted URL → `goto()`
704
+ - `user enter email "admin@example.com" password "123456"` — **compound input step** (one Gherkin step, two field/value pairs) → two `.fill()` lines emitted
705
+ - `click Login button` — no subject prefix → `loginButton.click()`
706
+ - `user is redirected to the Dashboard page` — third-person `is redirected to` → `toHaveURL(/Dashboard/)`
707
+
708
+ #### Bugs fixed this round
709
+
710
+ - **Step matcher: subject-agnostic rules.** All 13 rules now accept `I | user | User | the user | the User` as the subject prefix (was hard-coded `I`). Verb forms also accept singular AND plural conjugation (`click | clicks`, `enter | enters`, etc.). For rule 3 (click) the subject is now optional entirely (`click Login button` works without any subject).
711
+ - **Rule 1 (navigate): handles `navigate to <text> "<URL>"`.** Previous pattern only accepted `navigate to <target>`. New pattern: `(?:.+?\s+)?["']?<target>["']?` — the optional `<text> ` prefix is consumed, the quoted URL becomes the target.
712
+ - **Rule 11 (redirect): accepts `is redirected`.** Was `(should be |am )?redirected`; now `(should be |am |is )?redirected`.
713
+ - **Compound input rule (2c).** New rule for `SUBJ verb FIELD1 "V1" FIELD2 "V2" [...]` — extracts every (field, value) pair via a global regex pass and emits one `.fill()` per pair. Falls back to `null` (TODO) if any field doesn't resolve, so partial matches don't produce wrong code.
714
+
715
+ #### Type addition (small)
716
+
717
+ - **`StepBinding.customBody?: string`** — for compound steps that produce multiple TS statements per Gherkin step. The emitter writes `customBody` verbatim instead of synthesising a single line from `pomCall`/`assertion`. Existing rules unchanged; only the new compound rule uses this path.
718
+
719
+ #### Orchestrator bug fix
720
+
721
+ - **Background warnings now flow into `BDD_REVIEW.md`.** Previously only scenario-step warnings were collected; Background-step warnings were silently dropped. Now surfaced under `[Background]`.
722
+
723
+ ### Phase 1b milestone — live discovery 7/7 against the real site
724
+
725
+ **Same `practice-test-login` feature that needed a hand-authored snapshot in Phase 1a now passes 7/7 with `--url` only — bdd2pw scans the live page itself.** No snapshot file, no hand-tuning, real Chromium against `https://practicetestautomation.com/practice-test-login/`.
726
+
727
+ #### Iteration arc (live-scan rounds)
728
+
729
+ | Round | Pass / Fail | Root cause + fix |
730
+ |---|---|---|
731
+ | 1 | 4 / 3 | Scanner missed `<div id="error">` (no role/testid/aria-label) → broaden selector list to include `[id*=error i]`, `[class*=alert i]`, etc. |
732
+ | 2 | 3 / 4 | Scanner picked up error div but field name was text-derived (`yourUsernameIsInvalid`) → step matcher's `findField("error", [...])` couldn't match it. Fix: in `synthFieldName`, when CSS id matches `STATUS_REGION_RE`, prefer id-derived name → field becomes `error`. Plus `isLikelyStatusRegion` bypasses the visibility filter (status banners are intentionally hidden until triggered). Plus implicit roles for `<button>`/`<a>`/`<input type=submit>` so picker emits `getByRole`. Plus dedup-by-locator-value (collapse `username` + `usernameInput` into one). |
733
+ | 3 | 3 / 4 | Field name was right (`error`) but locator was wrong: picker emitted `getByText("Your username is invalid!")` not `locator("#error")`. Volatile text content was winning over the stable id. Fix: locator picker skips the `getByText` branch when element matches `STATUS_REGION_RE` — text changes per scenario, id is the stable handle. |
734
+ | 4 | **7 / 0** | — |
735
+
736
+ #### Bugs fixed this round
737
+
738
+ - **`src/discovery/mcpClient.ts`:** broader selector list (`[id*=error]`, `[class*=alert]`, …); `implicitRole()` for native HTML form controls; `isLikelyStatusRegion()` visibility-filter bypass; `isVisible()` zero-size + display:none filter; tsconfig `/// <reference lib="dom" />` so the `page.evaluate()` callback type-checks.
739
+ - **`src/transformers/locatorPicker.ts`:** new `STATUS_REGION_RE`; `synthFieldName` priority 0 = id-derived for status regions (so `<div id="error">` → `error`, not `yourUsernameIsInvalid`); `getByText` branch skipped for status regions; `dedupeLocators` now collapses by `(api, args)` identity, keeping the longer field name.
740
+
741
+ #### What this proves
742
+
743
+ Three of the original Phase 1a iteration rounds were **snapshot accuracy** bugs (fictional ARIA roles, fictional placeholders). With Phase 1b they're gone — the scanner sees what's actually there. The four Phase 1b rounds were rule/picker bugs surfaced by real DOM data, all fixed. Net result: a **single CLI invocation** with `--url` produces a runnable spec from a Cucumber `.feature` file with zero hand-edits, zero snapshot authoring.
744
+
745
+ ### Phase 1b — real browser-based page discovery (initial implementation, earlier this round)
746
+
747
+ Replaces the file-snapshot-only discovery with a real Chromium-launching scanner. Hand-authoring `snapshot.json` is no longer required for the common case; pass `--url` and bdd2pw scans the live page itself.
748
+
749
+ #### What landed
750
+
751
+ - **`scanPageWithBrowser()`** in `src/discovery/mcpClient.ts` — uses `playwright.chromium.launch()` directly. Walks the DOM with a curated selector list (inputs, buttons, links, selects, headings, anything with `[role]` / `[data-testid]` / `[aria-label]`), extracts per-element role / accessible name / label / placeholder / testId / text / best CSS selector, and returns a flat `ElementIR[]`-shaped list. Snapshot parser handles it without changes.
752
+ - **`playwright`** added as an `optionalDependencies` entry. Dynamic-imported in the scanner with a clear install-hint error if missing (`"npm install -D playwright && npx playwright install chromium"`).
753
+ - **Storage state support** — pass `--storage-state <path.json>` to scan an authenticated page; passed through to `browser.newContext({ storageState })`.
754
+ - **Headed mode** — `--headed` shows the browser window, useful for debugging snapshot accuracy.
755
+ - **Network-idle wait with fallback** — first attempt waits 5s for network idle (catches SPAs that finish loading lazily); falls back to `domcontentloaded` if idle never settles.
756
+
757
+ #### Removed framing
758
+
759
+ - Dropped `@playwright/mcp` from optional dependencies. **AQ-2 resolved:** MCP is an LLM-control protocol, not a programmatic browser API. We just need one URL → one snapshot, so direct `playwright` is one fewer process and one fewer protocol layer. The file `src/discovery/mcpClient.ts` keeps its name for now (rename is cosmetic, deferred).
760
+
761
+ #### Backward compatibility
762
+
763
+ - `--snapshot-file <path>` still works exactly as before. The orchestrator dispatches: snapshot file present → file path; otherwise → real browser. CI runs that don't want network access keep using pinned snapshot files (e.g. `examples/practice-test-login/snapshot.json`).
764
+ - `practiceTestLogin.test.ts` still uses the file-snapshot path so the regression test runs without network.
765
+
766
+ #### What this fixes
767
+
768
+ The 3-of-6 iteration rounds in Phase 1a milestone that were "snapshot accuracy" bugs (fictional ARIA roles, fictional placeholders, etc.) — those scenarios should now Just Work because the scanner sees the actual DOM.
769
+
770
+ ### Phase 1a milestone — first end-to-end green run (earlier this round)
771
+
772
+ **The full rule-based pipeline produced a runnable Playwright spec from a real-world Cucumber `.feature` file, with zero hand-edits.** Validated against `https://practicetestautomation.com/practice-test-login/` — `npx playwright test --project=chromium` reports **7 passed (4.2s)**.
773
+
774
+ #### Scope
775
+
776
+ - Source feature: `examples/practice-test-login/login.feature` — 5 scenarios + 1 Scenario Outline (2 example rows) + Background = 7 generated `test()` blocks
777
+ - Discovery source: hand-authored `examples/practice-test-login/snapshot.json` (Phase 1a fallback; real `@playwright/mcp` lands in Phase 1b)
778
+ - Output target: `pages/login.page.ts`, `tests/login.spec.ts`, `playwright.config.ts`, `package.json`, `tsconfig.json`, `.gitignore`, `BDD_REVIEW.md`
779
+
780
+ #### Iteration arc that got us there
781
+
782
+ | Round | Pass / Fail | Root cause |
783
+ |---|---|---|
784
+ | 1 | 0 / 7 | Undeclared `loginPage` (facade emitted POM calls without instantiation) |
785
+ | 2 | 4 / 3 | Rule 3 lost role hint; rule 8 required "the" not "an"; word-order mismatch in rule 2 |
786
+ | 3 | 2 / 5 | Snapshot fictional ARIA roles (`role: "alert"` not on page) |
787
+ | 4 | 1 / 6 | Snapshot fictional placeholders (`placeholder: "Username"` not on page) |
788
+ | 5 | 6 / 1 | Rule 9 emitted `toHaveText` (exact); step said "containing" |
789
+ | 6 | **7 / 0** | (none) |
790
+
791
+ 3 of 6 rounds were rule/code bugs. 3 were snapshot accuracy — exactly what real `@playwright/mcp` integration will eliminate (Phase 1b).
792
+
793
+ #### Bugs fixed this round (rule + code)
794
+
795
+ - **`emitTestFile` facade (`src/emitters/facade.ts`):** prepend `const <pageVar> = new <Class>(page);` to every test body and every hook body. Previously the spec emitted `await loginPage.goto()` without ever declaring `loginPage`.
796
+ - **`scaffold()` orchestrator (`src/index.ts`):** synthesise a `goto(): Promise<void>` POM method that calls `await this.page.goto(<url>)` when no goto exists. Without it, rule 1 emitted `await loginPage.goto()` against a method with no body.
797
+ - **POM file naming (`src/index.ts`):** use `pageObjectFileName` from `@vijaypjavvadi/pw-emit` instead of `${camelCase(opts.page)}.page.ts`. Was emitting `loginPage.page.ts`; now emits `login.page.ts`.
798
+ - **Reserved field-name filter (`src/index.ts`):** `page`, `context`, `browser` are now excluded from the "missing fields" set. Previously `loginPage.page.toHaveURL(...)` made `page` look like a missing field.
799
+ - **Step matcher rule 2b (`src/transformers/stepMatcher.ts`):** new — handles reversed word order `I enter <field> "<value>"`. Original rule expected `I enter "<value>" into <field>`.
800
+ - **Step matcher rule 3:** captures the explicit role suffix (`button` / `link` / `icon` / `tab`) and passes only that to `findField` so "click the login button" doesn't match `testLoginHeading`.
801
+ - **Step matcher rule 8:** accepts `the | an | a error message`, prefers `Alert > Error > Message`-suffixed fields.
802
+ - **Step matcher rule 9a:** new — `toContainText` semantics for `containing | that contains | with text`. Must run before rule 9b (exact text equality) which now also accepts `the | an | a`.
803
+ - **Step matcher rules 11–13:** new — redirect (`toHaveURL` regex), attribute check (`toHaveAttribute`), URL prefix/contains (`toHaveURL` anchored regex).
804
+ - **`findField` resolution order (`src/transformers/stepMatcher.ts`):** added a suffix-constrained pass — when preferredSuffixes are provided, only consider fields with those suffixes; if exactly one matches, use it. Stops "login" from matching `testLoginHeading` when there's a `submitButton` on the same page.
805
+ - **`synthFieldName` (`src/transformers/locatorPicker.ts`):** added `cssSelectorToName()` fallback. When no name/label/placeholder/text is available, derive the field name from the CSS id (`#username` → `usernameInput`) instead of the ugly `inputElement` placeholder.
806
+
807
+ #### Added
808
+
809
+ - `examples/practice-test-login/` — pinned regression fixture (login.feature + snapshot.json + README)
810
+ - `tests/e2e/practiceTestLogin.test.ts` — 22-assertion regression test for the full pipeline. Calls `scaffold()`, reads back POM + spec, asserts every binding pattern that should have landed.
811
+ - `vitest.config.ts` — `hookTimeout: 30_000` for E2E test cold-start reads.
812
+
813
+ ### Added — Phase 1a (initial implementation, earlier this round)
814
+ - **Real Gherkin parser** — `src/parser/gherkinParser.ts`. Uses `@cucumber/gherkin`. Handles Background, Scenario, Scenario Outline + Examples, doc strings, data tables.
815
+ - **Step matcher rules** — `src/transformers/stepMatcher.ts`. 10 rules covering navigation, input, click, select, check/uncheck, visibility (positive + negative), text equality, URL, error message. Falls through to a warning when no rule matches.
816
+ - **Locator picker** — `src/transformers/locatorPicker.ts`. Priority chain `getByRole > getByLabel > getByPlaceholder > getByTestId > getByText > css > xpath`. Ambiguity flagged when rivals exist. Field-name synthesis with role-based suffixes (`usernameInput`, `signInButton`).
817
+ - **POM resolver** — `src/transformers/pomResolver.ts`. Pure decision: REUSE / AUGMENT / CREATE based on existing fields vs referenced fields.
818
+ - **Repo scanner** — `src/repo/repoScanner.ts`. `ts-morph`-based AST scan of `pages/*.page.ts`. Extracts class name, locator fields, methods (preserves bodies for AUGMENT mode).
819
+ - **Project scaffolder** — `src/repo/projectScaffolder.ts`. Delegates to `@vijaypjavvadi/pw-emit`'s `emitProject()`. Idempotent.
820
+ - **Page discovery (file-snapshot fallback)** — `src/discovery/mcpClient.ts`. Phase 1a reads a pre-captured JSON snapshot via `--snapshot-file`. Real `@playwright/mcp` integration scheduled for Phase 1b.
821
+ - **Snapshot parser** — `src/discovery/snapshotParser.ts`. Accepts both flat-array and tree-shaped snapshots; flattens to `ElementIR[]`.
822
+ - **Review report** — `src/reports/reviewReport.ts`. Writes `BDD_REVIEW.md` with severity sections (Errors / Warnings / Info).
823
+ - **TSC validator** — `src/validate/tscRunner.ts`. Runs `tsc --noEmit` in the target repo, parses diagnostics into `ReviewItem`s. Soft-fails with a warning if `tsc` isn't installed.
824
+ - **`scaffold()` end-to-end wired** — `src/index.ts`. Orchestrates: parse → scan repo → scaffold → discover → pick locators → match steps (provisional) → resolve POM → re-match against final POM → expand Scenario Outlines → emit POM + spec → tsc validate → review report.
825
+ - **`analyze()` working** — same pipeline minus emit/validate. Returns the plan.
826
+ - **CLI `--snapshot-file` flag** for the file-based discovery path.
827
+ - **Example fixture** `examples/login-feature/snapshot.json` — hand-authored a11y tree for end-to-end testing.
828
+ - **3 new unit test files**: `stepMatcher.test.ts`, `locatorPicker.test.ts`, `pomResolver.test.ts`.
829
+
830
+ ### Still stubbed (next phases)
831
+ - `updatePom()` — needs lock-block enforcement (Phase 2)
832
+ - Real Playwright MCP integration (Phase 1b)
833
+ - HTTP worker that calls `scaffold()` (Phase 3)
834
+ - LLM fallback in stepMatcher (Phase 4)
835
+
836
+ ### Added — Phase 0 (previous round)
837
+ - Repository scaffold: package.json, tsconfig, ESLint, Prettier, Vitest, TypeDoc, MIT LICENSE
838
+ - CI workflow (Node 18/20/22 × Ubuntu/macOS/Windows), Release workflow (tag-triggered npm publish with provenance)
839
+ - `.changeset` config for versioning
840
+ - `src/` skeleton: `cli.ts`, `server.ts`, `index.ts`, `types.ts`, `utils/{naming,logger}.ts`
841
+ - Module stubs (now most replaced by Phase 1a implementations)
842
+ - Project templates: `package.json.tmpl`, `playwright.config.ts.tmpl`, `tsconfig.json.tmpl`, `gitignore.tmpl`
843
+ - Example fixture: `examples/login-feature/login.feature`
844
+ - First passing unit test: `tests/unit/naming.test.ts`
845
+ - `@vijaypjavvadi/pw-emit` consumed via `file:../pw-emit`
846
+ - `src/emitters/facade.ts` rewritten to delegate to `pw-emit`
847
+
848
+ ### Documents
849
+ - `README.md` — project orientation, decisions log, phasing
850
+ - `docs/SCOPE.md` — v1.0 scope, FR/NFR, CLI + HTTP surface
851
+ - `docs/ARCHITECTURE.md` — component diagram, sequence flows, contracts, error model
852
+
853
+ ## [0.1.0] — TBD
854
+
855
+ Initial published placeholder. **No runtime functionality yet.** Phase 0 (extracting `@vijaypjavvadi/pw-emit` from `sel2pw`) blocks Phase 1 of this package.