@cementic/cementic-test 0.2.15 → 0.2.17

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 ADDED
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@cementic/cementic-test` are documented here.
4
+
5
+ ## v0.2.17
6
+
7
+ - fixed `ct gen` URL assertions so redirect and stay-on-page regexes are derived from the exact intent text, including `secure area -> /secure/` and `login page -> /login/`
8
+ - stripped leaked backticks from generated regex literals so negative URL assertions no longer emit invalid patterns such as ``/`[-_\/]?login`/``
9
+ - added a post-generation Playwright validation pass that repairs known method typos such as `.fil()` before files are written
10
+ - fixed negative auth generation so the field under test uses a deliberately invalid literal value instead of reusing valid `CT_VAR_*` credentials
11
+ - updated package metadata, capture artifact metadata, and capture user agent strings to `0.2.17`
12
+
13
+ ## v0.2.16
14
+
15
+ - fixed `ct gen` so unquoted and backticked step values such as ```${CT_VAR_EMAIL}``` now map to runnable `.fill()` and `.selectOption()` calls instead of falling back to `TODO` comments
16
+ - fixed generated auth button clicks so generic phrases like `Click the login button` use resilient role-name regex matching when an exact accessible name is unavailable
17
+ - added regression coverage for unquoted `CT_VAR_*` auth steps to keep generated login specs runnable
18
+ - hardened the publish workflow so release tags must match `package.json`, and publishing aborts if the npm version already exists
19
+ - updated package metadata, capture artifact metadata, and capture user agent strings to `0.2.16`
20
+
21
+ ## v0.2.15
22
+
23
+ - fixed capture semantics so `<a href="...">` elements are classified as links unless they explicitly expose `role="button"`
24
+ - made CTA-like presence selectors resilient by generating `Locator.or(...)` fallbacks across button and link roles when the intent is ambiguous
25
+ - tightened intent label cleanup so trailing filler phrases like `on the page` do not leak into generated locator text
26
+ - updated capture artifact metadata and capture user agent version strings to `0.2.15`
27
+
28
+ ## v0.2.14
29
+
30
+ - hardened the release gate so `npm test` now includes the prompt regression suite and `prepublishOnly` runs the full test gate
31
+ - fixed the prompt test harness so it executes the local `dist/cli.js` build instead of depending on whichever global `ct` happens to be on `PATH`
32
+ - added deterministic capture-analysis fallback for `ct tc url --ai` when no LLM key is configured or remote analysis fails
33
+ - hardened auth, navigation, negative-state, heading, and count scenario fallback behavior so capture-driven generation still produces intent-aligned Playwright output offline
34
+ - added smoke coverage for `ct new`, `ct ci`, `ct flow --no-run`, `ct report`, and both `ct serve` execution paths
35
+ - replaced the old browser bootstrap heuristic in `ct new` with explicit browser install profiles: `auto`, `all`, and `chromium`
36
+ - kept the compatibility downgrade conditional to affected older macOS versions only, while preserving the normal Playwright install path for newer machines
37
+ - changed `ct new --mode` handling to reject unsupported scaffold modes explicitly instead of silently behaving like `greenfield`
38
+
39
+ ## v0.2.13
40
+
41
+ - replaced the old Rule 10 prompt guidance with a Playwright knowledge base aligned to official Playwright docs and web-first assertion patterns
42
+ - aligned locator priority with Playwright best practices: `getByRole -> getByLabel -> getByPlaceholder -> getByText -> locator`
43
+ - made counting intents generate `toHaveCount()` or `.count()` instead of intent-text locators
44
+ - enforced that intent wording is never reused as locator text
45
+ - made negative assertions generate `.not.` or `toBeHidden()` patterns
46
+ - made error assertions prefer `getByRole('alert')` and heading assertions prefer `getByRole('heading')`
47
+ - added on-demand Playwright doc fetching for advanced categories such as uploads, mocking, accessibility, viewports, auth, downloads, dialogs, frames, and popups
48
+ - added the 12-check prompt regression suite in `test/prompt.spec.mjs`
49
+
50
+ ## v0.2.12
51
+
52
+ - made both AI generation paths intent-first so markdown and capture-based scenario generation follow the same scope and presence-only rules
53
+ - tightened capture-based scenario generation to preserve presence-only checks as visibility assertions, honor intent-based selector fallback, and avoid generic `'value'` placeholders
54
+ - added regression coverage for the new markdown prompt contract and capture-path intent enforcement
55
+ - breaking change: none
56
+
57
+ ## v0.2.11
58
+
59
+ - AI no longer writes `'value'` as a step input and now prefers matching `CT_VAR_*` references or `test-{fieldname}` fallbacks
60
+ - fixed `ct gen` so generic fill steps read matching `CT_VAR_*` values from the environment and emit env-backed constants only when they are actually needed
61
+ - fixed generated `toHaveValue(...)` assertions so they reuse the same emitted fill value for selector-backed form fields
62
+ - added a generated spec header comment that lists required `CT_VAR_*` variables with inline usage examples
63
+ - breaking change: none
64
+
65
+ ## v0.2.10
66
+
67
+ - fixed `ct gen` so selector-hinted fields like `#username` and `#password` infer matching `CT_VAR_*` constants instead of leaving `.fill('value')` placeholders behind
68
+ - fixed generated `toHaveValue(...)` assertions so they reuse the same `CT_VAR_*` binding when the related fill step was variablized
69
+ - added regression coverage for selector-driven auth generation to keep fill calls and value assertions aligned
70
+
71
+ ## v0.2.9
72
+
73
+ - added `CT_VAR_*` extraction in `ct gen` for input and select test data so generated specs use env-backed constants instead of hardcoded values
74
+ - added `ct gen --vars '{...}'` to override generated fallback values without editing the emitted spec files by hand
75
+ - added regression coverage for variableized generated specs, selector-hint preservation, and select-option extraction
76
+
77
+ ## v0.2.8
78
+
79
+ - fixed TypeScript generator identifier sanitization so scenario titles containing em dashes and other non-identifier characters no longer break generated POM class names or spec imports
80
+ - added regression coverage for generated TypeScript output from scenario titles like `Login-002 — User can enter credentials — before submission`
81
+
82
+ ## v0.2.7
83
+
84
+ - fixed project-first Playwright resolution for `ct tc url --ai`, including global and `npx` CLI usage
85
+ - added clearer capture failure categories and setup guidance for missing Playwright, missing browsers, and page-load failures
86
+ - centralized AI provider detection so `tc --ai` and capture analysis recognize the same providers and env vars
87
+ - fixed missing DeepSeek guidance in the CLI help path
88
+ - changed legacy macOS browser setup to install Chromium only by default and avoid WebKit unsupported failures
89
+
90
+ ## v0.2.6
91
+
92
+ - added an install-time banner with links to the website and community
package/README.md CHANGED
@@ -206,6 +206,8 @@ Before publishing, run the full local gate:
206
206
  npm test
207
207
  ```
208
208
 
209
+ Do not reuse an npm version. npm will reject republishing an existing version even if the Git tag was moved later.
210
+
209
211
  If you want to inspect the prompt regression suite directly:
210
212
 
211
213
  ```bash
@@ -216,12 +218,14 @@ node --test --test-name-pattern="counting intent" test/prompt.spec.mjs
216
218
 
217
219
  Note: on current Node test runner versions, use `--test-reporter=spec`, not `--reporter=spec`.
218
220
 
219
- ### v0.2.15 acceptance criteria
221
+ ### v0.2.17 acceptance criteria
220
222
 
221
- All of the following must be true before publishing `v0.2.15`:
223
+ All of the following must be true before publishing `v0.2.17`:
222
224
 
223
225
  - `npm test` passes from a clean checkout and includes both the integration suite and `test/prompt.spec.mjs`
224
226
  - `prepublishOnly` runs the same full release gate, not just `build`
227
+ - the release tag matches `package.json`
228
+ - the target npm version is not already published
225
229
  - prompt regression suite verifies:
226
230
  - counting intents generate `toHaveCount()` or `.count()`
227
231
  - intent text is never reused as `getByText(...)`
@@ -246,8 +250,14 @@ All of the following must be true before publishing `v0.2.15`:
246
250
  - review flags and review reasons
247
251
  - `ct gen` guarantees:
248
252
  - no `.fill('value')` placeholders
253
+ - no `TODO` comments for recognized fill/select auth steps using quoted, unquoted, or backticked `CT_VAR_*` values
249
254
  - env-backed `CT_VAR_*` extraction when matching vars are available
250
255
  - field-specific fallback values when env vars are not available
256
+ - redirect and stay-on-page assertions derive `toHaveURL(/.../)` from the exact intent text or explicit path instead of generic hardcoded alternates
257
+ - backticks never leak into generated regex literals
258
+ - field-specific negative auth tests use deliberately invalid literal values for the field under test
259
+ - generic auth button click steps fall back to resilient role-name matching when exact labels are missing
260
+ - generated Playwright method calls are validated before files are written, and known typos such as `.fil()` are repaired to valid API names
251
261
  - existing POM files are not overwritten
252
262
  - generated specs compile as valid Playwright TypeScript or JavaScript
253
263
  - command smoke coverage passes for:
@@ -258,7 +268,7 @@ All of the following must be true before publishing `v0.2.15`:
258
268
  - unsupported `ct new --mode` values failing explicitly
259
269
  - `ct ci`
260
270
  - `ct flow --no-run`
261
- - README, package version, and documented changelog entries match the release being cut
271
+ - README, package version, runtime metadata strings, and changelog entries match the release being cut
262
272
 
263
273
  ---
264
274
 
@@ -451,6 +461,20 @@ And start automating.
451
461
 
452
462
  ## Changelog
453
463
 
464
+ See [CHANGELOG.md](./CHANGELOG.md) for the full release history.
465
+
466
+ ### v0.2.17
467
+
468
+ - fixed `ct gen` URL assertions so redirect and stay-on-page regexes now come from the exact intent text, including `secure area -> /secure/` and `login page -> /login/`
469
+ - stripped leaked backticks from generated regex literals and added a post-generation Playwright validation pass that repairs known typos such as `.fil()`
470
+ - fixed negative auth generation so the field under test uses a deliberately invalid literal instead of reusing valid `CT_VAR_*` credentials
471
+
472
+ ### v0.2.16
473
+
474
+ - fixed `ct gen` so unquoted and backticked `CT_VAR_*` fill steps now generate runnable `.fill()` calls instead of `TODO` comments
475
+ - made generated auth button clicks more resilient for generic phrases like `Click the login button`
476
+ - added release guards so publish only proceeds when the Git tag matches `package.json` and the npm version has not already been published
477
+
454
478
  ### v0.2.15
455
479
 
456
480
  - fixed capture semantics so `<a href="...">` elements are classified as links unless they explicitly expose `role="button"`
package/dist/capture.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  formatCaptureFailure,
7
7
  inferInteractiveRole,
8
8
  toPageSummary
9
- } from "./chunk-XUWFEWJZ.js";
9
+ } from "./chunk-JWGYAQ3O.js";
10
10
  export {
11
11
  CaptureRuntimeError,
12
12
  captureElements,
@@ -30,12 +30,27 @@ function urlPatternFromAbsoluteUrl(value) {
30
30
  return void 0;
31
31
  }
32
32
  }
33
+ function sanitizeUrlIntentPhrase(value) {
34
+ return value.replace(/`+/g, " ").replace(/^\/+/, " ").replace(/\/[dgimsuvy]*$/i, " ").replace(/["']/g, " ").replace(/\b(?:the|a|an|user|same|current)\b/gi, " ").replace(/\b(?:page|screen|view|area|section|route|path|url|form)\b/gi, " ").replace(/\s+/g, " ").trim();
35
+ }
33
36
  function pageNameToUrlPattern(value) {
34
- const cleaned = value.replace(/\b(the|a|an|user)\b/gi, " ").replace(/\b(page|screen)\b/gi, " ").trim();
37
+ const cleaned = sanitizeUrlIntentPhrase(value);
35
38
  const tokens = cleaned.split(/[\s/-]+/).map((token) => token.trim()).filter(Boolean);
36
39
  if (tokens.length === 0) return void 0;
37
40
  return tokens.map((token) => escapeForRegex(token.toLowerCase())).join("[-_\\/]?");
38
41
  }
42
+ function urlPatternFromIntentText(value) {
43
+ const sanitized = value.replace(/`+/g, "").trim();
44
+ const explicitPathMatch = sanitized.match(/["'](\/[^"']+)["']/) ?? sanitized.match(/\b(?:to|on|at)\s+(\/[\w/-]+)/i) ?? sanitized.match(/https?:\/\/[^\s'"]+/i);
45
+ const explicitPath = explicitPathMatch?.[1] ?? explicitPathMatch?.[0];
46
+ if (explicitPath) {
47
+ const fromUrl = explicitPath.startsWith("http") ? urlPatternFromAbsoluteUrl(explicitPath) : void 0;
48
+ const fromPath = explicitPath.startsWith("/") ? explicitPath.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/") : void 0;
49
+ return fromUrl ?? fromPath;
50
+ }
51
+ const phraseMatch = sanitized.match(/\b(?:redirect(?:s|ed)?|navigat(?:e|es|ed)|go(?:es|ne)?|route|path|url)\b(?:\s+(?:to|on|contains))?\s+(?:the\s+)?(.+?)(?:[.?!]|$)/i) ?? sanitized.match(/\b(?:remain|remains|stays|stay|still)\s+(?:on|at)\s+(?:the\s+)?(.+?)(?:[.?!]|$)/i);
52
+ return phraseMatch?.[1] ? pageNameToUrlPattern(phraseMatch[1]) : void 0;
53
+ }
39
54
  function fieldNameFromSentence(value) {
40
55
  const match = value.match(/\bfor\s+([a-zA-Z][a-zA-Z\s-]{0,30}?)\s+field\b/i) ?? value.match(/\b([a-zA-Z][a-zA-Z\s-]{0,30}?)\s+field\b/i) ?? value.match(/\b([a-zA-Z][a-zA-Z\s-]{0,30}?)\s+input\b/i);
41
56
  return match?.[1]?.trim();
@@ -92,6 +107,16 @@ function parseQuotedLiteral(value) {
92
107
  }
93
108
  return void 0;
94
109
  }
110
+ function unwrapStepValue(value) {
111
+ if (!value) return void 0;
112
+ const trimmed = value.trim();
113
+ if (!trimmed) return void 0;
114
+ const quoted = parseQuotedLiteral(trimmed);
115
+ if (quoted !== void 0) return quoted;
116
+ const inlineCode = trimmed.match(/^`([^`]+)`$/);
117
+ if (inlineCode) return inlineCode[1].trim();
118
+ return trimmed;
119
+ }
95
120
  function selectorTargetToEnvSuffix(value) {
96
121
  const selector = normalizeSelectorTarget(value);
97
122
  if (!selector) return void 0;
@@ -118,7 +143,7 @@ function isGenericFallbackValue(value) {
118
143
  return /^(?:value|text|input|option|selection|selected value|default)$/i.test(value.trim());
119
144
  }
120
145
  function extractCtVarTemplate(value) {
121
- const match = value?.trim().match(/^\$\{(CT_VAR_[A-Z0-9_]+)\}$/);
146
+ const match = unwrapStepValue(value)?.match(/^\$\{(CT_VAR_[A-Z0-9_]+)\}$/);
122
147
  return match?.[1];
123
148
  }
124
149
  function placeholderTokenFromSuffix(value) {
@@ -151,7 +176,16 @@ function parseStepBinding(step) {
151
176
  kind: "fill",
152
177
  rawField: fillMatch[1].trim(),
153
178
  fieldLabel: cleanFieldLabel(fillMatch[1]),
154
- value: fillMatch[3].trim()
179
+ value: unwrapStepValue(fillMatch[3])
180
+ };
181
+ }
182
+ const fillLooseMatch = s.match(/\b(?:fill|input|write)\s+(?:in\s+)?(.+?)\s+with\s+(.+)$/i);
183
+ if (fillLooseMatch) {
184
+ return {
185
+ kind: "fill",
186
+ rawField: fillLooseMatch[1].trim(),
187
+ fieldLabel: cleanFieldLabel(fillLooseMatch[1]),
188
+ value: unwrapStepValue(fillLooseMatch[2])
155
189
  };
156
190
  }
157
191
  const fillWithoutValueMatch = s.match(/\b(?:fill|input|write)\s+(?:in\s+)?(.+?)$/i);
@@ -168,7 +202,16 @@ function parseStepBinding(step) {
168
202
  kind: "fill",
169
203
  rawField: enterMatch[3].trim(),
170
204
  fieldLabel: cleanFieldLabel(enterMatch[3]),
171
- value: enterMatch[2].trim()
205
+ value: unwrapStepValue(enterMatch[2])
206
+ };
207
+ }
208
+ const enterLooseMatch = s.match(/\b(?:enter|type)\s+(.+?)\s+(?:in|into)\s+(.+)$/i);
209
+ if (enterLooseMatch) {
210
+ return {
211
+ kind: "fill",
212
+ rawField: enterLooseMatch[2].trim(),
213
+ fieldLabel: cleanFieldLabel(enterLooseMatch[2]),
214
+ value: unwrapStepValue(enterLooseMatch[1])
172
215
  };
173
216
  }
174
217
  const enterWithoutValueMatch = s.match(/\b(?:enter|type)\s+(?:in|into)\s+(.+)/i);
@@ -185,7 +228,16 @@ function parseStepBinding(step) {
185
228
  kind: "select",
186
229
  rawField: selectMatch[3].trim(),
187
230
  fieldLabel: cleanFieldLabel(selectMatch[3]),
188
- value: selectMatch[2].trim()
231
+ value: unwrapStepValue(selectMatch[2])
232
+ };
233
+ }
234
+ const selectLooseMatch = s.match(/\b(?:select|choose|pick)\s+(.+?)\s+(?:from|in|into|on)\s+(.+)$/i);
235
+ if (selectLooseMatch) {
236
+ return {
237
+ kind: "select",
238
+ rawField: selectLooseMatch[2].trim(),
239
+ fieldLabel: cleanFieldLabel(selectLooseMatch[2]),
240
+ value: unwrapStepValue(selectLooseMatch[1])
189
241
  };
190
242
  }
191
243
  const selectWithoutValueMatch = s.match(/\b(?:select|choose|pick)\s+(.+?)\s+(?:dropdown|combobox|select|menu)\b/i);
@@ -198,6 +250,38 @@ function parseStepBinding(step) {
198
250
  }
199
251
  return void 0;
200
252
  }
253
+ function inferNegativeAuthTarget(norm) {
254
+ const tags = (norm.tags ?? []).map((tag) => tag.toLowerCase());
255
+ const content = [norm.id, norm.title, ...norm.expected ?? []].filter(Boolean).join(" ").toLowerCase();
256
+ const isNegativeCase = tags.includes("negative") || /\b(invalid|wrong|incorrect|bad|nonexistent|not an email)\b/.test(content);
257
+ if (!isNegativeCase) return void 0;
258
+ if (/\b(?:invalid|wrong|incorrect|bad|not an email)\s+(?:e-?mail|email)\b|\b(?:e-?mail|email)\b.{0,24}\b(?:invalid|wrong|incorrect|bad|not an email)\b/.test(content)) {
259
+ return "EMAIL";
260
+ }
261
+ if (/\b(?:invalid|wrong|incorrect|bad|nonexistent)\s+(?:user(?:name)?|login)\b|\b(?:user(?:name)?|login)\b.{0,24}\b(?:invalid|wrong|incorrect|bad|nonexistent)\b/.test(content)) {
262
+ return "USERNAME";
263
+ }
264
+ if (/\b(?:invalid|wrong|incorrect|bad)\s+password\b|\bpassword\b.{0,24}\b(?:invalid|wrong|incorrect|bad)\b/.test(content)) {
265
+ return "PASSWORD";
266
+ }
267
+ return void 0;
268
+ }
269
+ function negativeAuthFallbackValue(target) {
270
+ return target === "EMAIL" ? "invalid-not-an-email" : target === "PASSWORD" ? "wrong-password-123" : "nonexistent-user";
271
+ }
272
+ function rewriteNegativeAuthSteps(steps, stepHints, norm) {
273
+ const target = inferNegativeAuthTarget(norm);
274
+ if (!target) return steps;
275
+ return steps.map((step, index) => {
276
+ const binding = parseStepBinding(step);
277
+ if (binding?.kind !== "fill") return step;
278
+ const selectorTarget = normalizeSelectorTarget(stepHints?.[index]?.selector);
279
+ const fieldSuffix = fieldNameToEnvSuffix(binding.rawField) ?? fieldNameToEnvSuffix(binding.fieldLabel) ?? selectorTargetToEnvSuffix(selectorTarget);
280
+ if (fieldSuffix !== target) return step;
281
+ const rawField = binding.rawField ?? binding.fieldLabel ?? "field";
282
+ return `Fill in ${rawField} with '${negativeAuthFallbackValue(target)}'`;
283
+ });
284
+ }
201
285
  function resolveStepVarOverride(overrides, envKey, keySuffix, rawField, fieldLabel) {
202
286
  const candidates = [envKey, keySuffix, rawField, fieldLabel];
203
287
  for (const candidate of candidates) {
@@ -233,10 +317,12 @@ function extractStepVars(steps, stepHints, assertionHints, overrides) {
233
317
  const overrideValue = resolveStepVarOverride(overrides, envKey, keySuffix, binding.rawField, binding.fieldLabel);
234
318
  const fallbackFromBinding = binding.value?.trim();
235
319
  const hasRuntimeEnvValue = process.env[envKey] !== void 0;
236
- const shouldDeclareVariable = binding.kind === "select" || templateEnvKey !== void 0 || overrideValue !== void 0 || hasRuntimeEnvValue;
320
+ const shouldUseRuntimeEnvValue = hasRuntimeEnvValue && (!fallbackFromBinding || isGenericFallbackValue(fallbackFromBinding));
321
+ const shouldDeclareVariable = binding.kind === "select" || templateEnvKey !== void 0 || overrideValue !== void 0 || shouldUseRuntimeEnvValue;
237
322
  let stepVar = shouldDeclareVariable ? seen.get(envKey) : void 0;
238
323
  if (shouldDeclareVariable && !stepVar) {
239
- const fallback = overrideValue ?? process.env[envKey] ?? ((!fallbackFromBinding || isGenericFallbackValue(fallbackFromBinding) || templateEnvKey) && assertionFallback ? assertionFallback : fallbackFromBinding) ?? (binding.kind === "fill" ? buildFillPlaceholder(binding.rawField, binding.fieldLabel, selectorTarget) : assertionFallback ?? "option");
324
+ const placeholderFallback = binding.kind === "fill" ? buildFillPlaceholder(binding.rawField, binding.fieldLabel, selectorTarget) : assertionFallback ?? "option";
325
+ const fallback = overrideValue ?? process.env[envKey] ?? (templateEnvKey ? assertionFallback ?? placeholderFallback : (!fallbackFromBinding || isGenericFallbackValue(fallbackFromBinding)) && assertionFallback ? assertionFallback : fallbackFromBinding) ?? placeholderFallback;
240
326
  stepVar = {
241
327
  envKey,
242
328
  constName: envKey,
@@ -306,6 +392,33 @@ function ensureStatement(value) {
306
392
  if (!trimmed) return trimmed;
307
393
  return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
308
394
  }
395
+ function phraseToRegexPattern(value) {
396
+ return value.trim().split(/\s+/).filter(Boolean).map((token) => escapeForRegex(token)).join("\\s+");
397
+ }
398
+ function buttonNameExpressionFromStep(step) {
399
+ const quotedMatch = step.match(/["']([^"']+)["']/);
400
+ if (quotedMatch) return `'${escapeForSingleQuotedString(quotedMatch[1])}'`;
401
+ const label = step.replace(/^(click|press|tap)\s+(?:on\s+)?(?:the\s+)?/i, "").replace(/\b(button|btn)\b/gi, "").replace(/[.?!,:;]+$/g, "").trim();
402
+ if (!label) return `'button'`;
403
+ const lowered = label.toLowerCase();
404
+ const variants = /* @__PURE__ */ new Set();
405
+ if (/\b(?:login|log in|sign in|sign-in)\b/.test(lowered)) {
406
+ variants.add("login");
407
+ variants.add("log in");
408
+ variants.add("sign in");
409
+ } else if (/\b(?:register|sign up|sign-up|signup|create account)\b/.test(lowered)) {
410
+ variants.add("register");
411
+ variants.add("sign up");
412
+ variants.add("create account");
413
+ } else if (/\bcontinue\b/.test(lowered)) {
414
+ variants.add("continue");
415
+ variants.add("next");
416
+ } else {
417
+ variants.add(label);
418
+ }
419
+ const pattern = Array.from(variants).map(phraseToRegexPattern).join("|");
420
+ return `/${pattern}/i`;
421
+ }
309
422
  function parseValueAssertionHint(playwright) {
310
423
  const statement = ensureStatement(String(playwright ?? "").trim());
311
424
  if (!statement) return void 0;
@@ -350,9 +463,7 @@ function stepToPlaywright(step, url, hint, stepVar) {
350
463
  }
351
464
  if (/\bclick\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {
352
465
  if (hintedSelector) return `await ${hintedSelector}.click();`;
353
- const nameMatch = s.match(/["']([^"']+)["']/);
354
- const name = nameMatch?.[1] ?? s.replace(/click\s+(the\s+)?/i, "").trim();
355
- return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;
466
+ return `await page.getByRole('button', { name: ${buttonNameExpressionFromStep(s)} }).click();`;
356
467
  }
357
468
  if (/\bclick\b.*(link|anchor|nav)/i.test(s)) {
358
469
  if (hintedSelector) return `await ${hintedSelector}.click();`;
@@ -422,20 +533,12 @@ function expectedToAssertion(expected, norm, hint, stepState) {
422
533
  return `await expect(page).toHaveURL(/${fallbackPattern}/); // form did not navigate`;
423
534
  }
424
535
  if (/\b(remains?|stays?|still)\s+on\b/i.test(s)) {
425
- const pageMatch = s.match(/\b(?:remains?|stays?|still)\s+on\s+(?:the\s+)?(.+?)(?:\s+page|\s+screen)?$/i);
426
- const pagePattern = pageMatch?.[1] ? pageNameToUrlPattern(pageMatch[1]) : void 0;
536
+ const pagePattern = urlPatternFromIntentText(s);
427
537
  return `await expect(page).toHaveURL(/${pagePattern || currentUrlPattern || "login"}/);`;
428
538
  }
429
539
  if (/\b(url|redirect(?:s|ed)?|navigate(?:s|d)?|route|path)\b/i.test(s)) {
430
- const pathMatch = s.match(/["'](\/[^"']+)["']/) ?? s.match(/to\s+(\/[\w/-]+)/i) ?? s.match(/https?:\/\/[^\s'"]+/i);
431
- const matchedPath = pathMatch?.[1] ?? pathMatch?.[0];
432
- if (matchedPath) {
433
- const fromUrl = matchedPath.startsWith("http") ? urlPatternFromAbsoluteUrl(matchedPath) : void 0;
434
- const directPath = matchedPath.startsWith("/") ? matchedPath.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/") : void 0;
435
- const finalPattern = fromUrl ?? directPath ?? currentUrlPattern;
436
- if (finalPattern !== void 0) return `await expect(page).toHaveURL(/${finalPattern}/);`;
437
- }
438
- return `await expect(page).toHaveURL(/dashboard|success|home/);`;
540
+ const finalPattern = urlPatternFromIntentText(s) ?? currentUrlPattern ?? ".+";
541
+ return `await expect(page).toHaveURL(/${finalPattern}/);`;
439
542
  }
440
543
  if (/\bpage title\b|\btitle should\b|\bdocument title\b/i.test(s)) {
441
544
  const titleMatch = s.match(/["']([^"']+)["']/);
@@ -568,7 +671,7 @@ function buildTestTitle(norm) {
568
671
  }
569
672
  function buildSpecFile(norm, pomClassName, pomImportPath, overrides) {
570
673
  const title = buildTestTitle(norm);
571
- const steps = norm.steps ?? [];
674
+ const steps = rewriteNegativeAuthSteps(norm.steps ?? [], norm.step_hints, norm);
572
675
  const expected = norm.expected ?? [];
573
676
  const stepVars = extractStepVars(steps, norm.step_hints, norm.assertion_hints, overrides);
574
677
  let importPath = pomImportPath.replace(/\\/g, "/");
@@ -601,6 +704,87 @@ ${assertionLines}
601
704
  });
602
705
  `;
603
706
  }
707
+ var PLAYWRIGHT_METHOD_NAMES = /* @__PURE__ */ new Set([
708
+ "all",
709
+ "check",
710
+ "click",
711
+ "evaluate",
712
+ "fill",
713
+ "filter",
714
+ "first",
715
+ "getByLabel",
716
+ "getByPlaceholder",
717
+ "getByRole",
718
+ "getByTestId",
719
+ "getByText",
720
+ "goto",
721
+ "hover",
722
+ "locator",
723
+ "nth",
724
+ "or",
725
+ "press",
726
+ "reload",
727
+ "selectOption",
728
+ "toBeChecked",
729
+ "toBeDisabled",
730
+ "toBeEnabled",
731
+ "toBeHidden",
732
+ "toBeVisible",
733
+ "toContainText",
734
+ "toHaveCount",
735
+ "toHaveTitle",
736
+ "toHaveURL",
737
+ "toHaveValue",
738
+ "uncheck",
739
+ "waitForEvent",
740
+ "waitForLoad",
741
+ "waitForLoadState"
742
+ ]);
743
+ function levenshteinDistance(left, right) {
744
+ const distances = Array.from({ length: right.length + 1 }, (_, index) => index);
745
+ for (let row = 1; row <= left.length; row++) {
746
+ let previousDiagonal = distances[0];
747
+ distances[0] = row;
748
+ for (let column = 1; column <= right.length; column++) {
749
+ const temp = distances[column];
750
+ distances[column] = Math.min(
751
+ distances[column] + 1,
752
+ distances[column - 1] + 1,
753
+ previousDiagonal + (left[row - 1] === right[column - 1] ? 0 : 1)
754
+ );
755
+ previousDiagonal = temp;
756
+ }
757
+ }
758
+ return distances[right.length];
759
+ }
760
+ function findClosestPlaywrightMethod(methodName) {
761
+ let bestCandidate;
762
+ let bestDistance = Number.POSITIVE_INFINITY;
763
+ for (const candidate of PLAYWRIGHT_METHOD_NAMES) {
764
+ if (candidate[0] !== methodName[0]) continue;
765
+ const distance = levenshteinDistance(methodName, candidate);
766
+ if (distance < bestDistance) {
767
+ bestDistance = distance;
768
+ bestCandidate = candidate;
769
+ }
770
+ }
771
+ return bestDistance <= 2 ? bestCandidate : void 0;
772
+ }
773
+ function stripBackticksInsideRegexLiterals(line) {
774
+ return line.includes("`") ? line.replace(/`/g, "") : line;
775
+ }
776
+ function repairPlaywrightMethodsInLine(line, fileLabel) {
777
+ if (!/\bawait\b/.test(line)) return line;
778
+ return line.replace(/\.(\w+)\(/g, (match, methodName) => {
779
+ if (PLAYWRIGHT_METHOD_NAMES.has(methodName)) return match;
780
+ const repaired = findClosestPlaywrightMethod(methodName);
781
+ if (repaired) return `.${repaired}(`;
782
+ throw new Error(`Generated unsupported Playwright method ".${methodName}(" in ${fileLabel}`);
783
+ });
784
+ }
785
+ function finalizeGeneratedPlaywrightSource(source, fileLabel) {
786
+ return source.split("\n").map((line) => repairPlaywrightMethodsInLine(stripBackticksInsideRegexLiterals(line), fileLabel)).join("\n");
787
+ }
604
788
  async function gen(opts) {
605
789
  if (opts.lang !== "ts" && opts.lang !== "js") {
606
790
  console.error("\u274C --lang must be ts or js");
@@ -632,14 +816,23 @@ async function gen(opts) {
632
816
  const pomFileName = pomClassName + pomExt;
633
817
  const pomFilePath = join(pagesOutDir, pomFileName);
634
818
  if (!existsSync(pomFilePath)) {
635
- writeFileSync(pomFilePath, buildPomClass(pomClassName, norm, lang));
819
+ writeFileSync(
820
+ pomFilePath,
821
+ finalizeGeneratedPlaywrightSource(buildPomClass(pomClassName, norm, lang), pomFileName)
822
+ );
636
823
  pomCount++;
637
824
  }
638
825
  const relToPages = relative(testsOutDir, pagesOutDir);
639
826
  const pomImportPath = join(relToPages, pomFileName);
640
827
  const stem = basename(f).replace(/\.json$/, "").replace(/[^\w-]+/g, "-");
641
828
  const specPath = join(testsOutDir, `${stem}.${specExt}`);
642
- writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath, varsOverride));
829
+ writeFileSync(
830
+ specPath,
831
+ finalizeGeneratedPlaywrightSource(
832
+ buildSpecFile(norm, pomClassName, pomImportPath, varsOverride),
833
+ basename(specPath)
834
+ )
835
+ );
643
836
  specCount++;
644
837
  }
645
838
  console.log(`\u2705 Generated ${specCount} spec file(s) \u2192 ${opts.out}/`);
@@ -663,7 +856,8 @@ Examples:
663
856
  }
664
857
 
665
858
  export {
859
+ finalizeGeneratedPlaywrightSource,
666
860
  gen,
667
861
  genCmd
668
862
  };
669
- //# sourceMappingURL=chunk-3S26OWNR.js.map
863
+ //# sourceMappingURL=chunk-A4IHRXON.js.map