@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 +92 -0
- package/README.md +27 -3
- package/dist/capture.js +1 -1
- package/dist/{chunk-3S26OWNR.js → chunk-A4IHRXON.js} +219 -25
- package/dist/chunk-A4IHRXON.js.map +1 -0
- package/dist/{chunk-XUWFEWJZ.js → chunk-JWGYAQ3O.js} +2 -2
- package/dist/{chunk-XUWFEWJZ.js.map → chunk-JWGYAQ3O.js.map} +1 -1
- package/dist/cli.js +62 -19
- package/dist/cli.js.map +1 -1
- package/dist/gen-J4HZWS5T.js +12 -0
- package/package.json +4 -3
- package/dist/chunk-3S26OWNR.js.map +0 -1
- package/dist/gen-AGWFMHTO.js +0 -10
- /package/dist/{gen-AGWFMHTO.js.map → gen-J4HZWS5T.js.map} +0 -0
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.
|
|
221
|
+
### v0.2.17 acceptance criteria
|
|
220
222
|
|
|
221
|
-
All of the following must be true before publishing `v0.2.
|
|
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
|
|
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
|
@@ -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
|
|
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?.
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
431
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
863
|
+
//# sourceMappingURL=chunk-A4IHRXON.js.map
|