@vijaypjavvadi/bdd2pw 1.1.5 → 1.1.6

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