@cementic/cementic-test 0.2.16 → 0.2.18
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 +16 -0
- package/README.md +18 -2
- package/dist/capture.js +1 -1
- package/dist/{chunk-WUGSOKKY.js → chunk-GSQCM62X.js} +20 -6
- package/dist/chunk-GSQCM62X.js.map +1 -0
- package/dist/{chunk-PYKYHIO3.js → chunk-P6WZO7G7.js} +304 -28
- package/dist/chunk-P6WZO7G7.js.map +1 -0
- package/dist/cli.js +62 -19
- package/dist/cli.js.map +1 -1
- package/dist/gen-25TCUBSD.js +12 -0
- package/package.json +1 -1
- package/dist/chunk-PYKYHIO3.js.map +0 -1
- package/dist/chunk-WUGSOKKY.js.map +0 -1
- package/dist/gen-WYG3JOGJ.js +0 -10
- /package/dist/{gen-WYG3JOGJ.js.map → gen-25TCUBSD.js.map} +0 -0
|
@@ -11,6 +11,21 @@ function escapeForSingleQuotedString(value) {
|
|
|
11
11
|
function escapeForRegex(value) {
|
|
12
12
|
return value.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
|
|
13
13
|
}
|
|
14
|
+
function stripWrappingQuotes(value) {
|
|
15
|
+
if (!value) return void 0;
|
|
16
|
+
let trimmed = value.trim();
|
|
17
|
+
if (!trimmed) return void 0;
|
|
18
|
+
while (trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
19
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
20
|
+
}
|
|
21
|
+
return trimmed || void 0;
|
|
22
|
+
}
|
|
23
|
+
function sanitizeLabelText(value) {
|
|
24
|
+
return stripWrappingQuotes(value)?.trim() || void 0;
|
|
25
|
+
}
|
|
26
|
+
function escapeForDoubleQuotedString(value) {
|
|
27
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
28
|
+
}
|
|
14
29
|
function normalizeUrlForComparison(value) {
|
|
15
30
|
if (!value) return void 0;
|
|
16
31
|
try {
|
|
@@ -30,12 +45,27 @@ function urlPatternFromAbsoluteUrl(value) {
|
|
|
30
45
|
return void 0;
|
|
31
46
|
}
|
|
32
47
|
}
|
|
48
|
+
function sanitizeUrlIntentPhrase(value) {
|
|
49
|
+
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();
|
|
50
|
+
}
|
|
33
51
|
function pageNameToUrlPattern(value) {
|
|
34
|
-
const cleaned = value
|
|
52
|
+
const cleaned = sanitizeUrlIntentPhrase(value);
|
|
35
53
|
const tokens = cleaned.split(/[\s/-]+/).map((token) => token.trim()).filter(Boolean);
|
|
36
54
|
if (tokens.length === 0) return void 0;
|
|
37
55
|
return tokens.map((token) => escapeForRegex(token.toLowerCase())).join("[-_\\/]?");
|
|
38
56
|
}
|
|
57
|
+
function urlPatternFromIntentText(value) {
|
|
58
|
+
const sanitized = value.replace(/`+/g, "").trim();
|
|
59
|
+
const explicitPathMatch = sanitized.match(/["'](\/[^"']+)["']/) ?? sanitized.match(/\b(?:to|on|at)\s+(\/[\w/-]+)/i) ?? sanitized.match(/https?:\/\/[^\s'"]+/i);
|
|
60
|
+
const explicitPath = explicitPathMatch?.[1] ?? explicitPathMatch?.[0];
|
|
61
|
+
if (explicitPath) {
|
|
62
|
+
const fromUrl = explicitPath.startsWith("http") ? urlPatternFromAbsoluteUrl(explicitPath) : void 0;
|
|
63
|
+
const fromPath = explicitPath.startsWith("/") ? explicitPath.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/") : void 0;
|
|
64
|
+
return fromUrl ?? fromPath;
|
|
65
|
+
}
|
|
66
|
+
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);
|
|
67
|
+
return phraseMatch?.[1] ? pageNameToUrlPattern(phraseMatch[1]) : void 0;
|
|
68
|
+
}
|
|
39
69
|
function fieldNameFromSentence(value) {
|
|
40
70
|
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
71
|
return match?.[1]?.trim();
|
|
@@ -54,25 +84,46 @@ function normalizeOverrideKey(value) {
|
|
|
54
84
|
}
|
|
55
85
|
function cleanFieldLabel(value) {
|
|
56
86
|
if (!value) return void 0;
|
|
57
|
-
const cleaned = value
|
|
87
|
+
const cleaned = sanitizeLabelText(value)?.replace(/[.?!,:;]+$/g, "").replace(/^(?:the|a|an)\s+/i, "").replace(/\s+(?:field|input|box|area|dropdown|combobox|select|menu)\b/gi, "").trim();
|
|
58
88
|
return cleaned || void 0;
|
|
59
89
|
}
|
|
60
90
|
function isPositionalFieldReference(value) {
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
const sanitized = sanitizeLabelText(value);
|
|
92
|
+
if (!sanitized) return false;
|
|
93
|
+
const cleaned = sanitized.trim().toLowerCase();
|
|
63
94
|
return /^(?:(?:the|a|an)\s+)?(?:first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|\d+(?:st|nd|rd|th)?)(?:\s+\w+){0,2}\s+(?:field|input|box|area|dropdown|combobox|select|menu)$/.test(cleaned);
|
|
64
95
|
}
|
|
65
96
|
function fieldNameToEnvSuffix(value) {
|
|
66
|
-
|
|
67
|
-
|
|
97
|
+
const sanitized = sanitizeLabelText(value);
|
|
98
|
+
if (!sanitized || isPositionalFieldReference(sanitized)) return void 0;
|
|
99
|
+
const cleaned = sanitized.replace(/[.?!,:;]+$/g, "").replace(/^(?:the|a|an)\s+/i, "").replace(/\b(?:field|input|box|area|dropdown|combobox|select|menu)\b/gi, " ").trim();
|
|
68
100
|
const suffix = sanitizeEnvSegment(cleaned);
|
|
69
101
|
if (!suffix) return void 0;
|
|
70
102
|
if (/^(?:FIELD|INPUT|BOX|AREA|DROPDOWN|COMBOBOX|SELECT|MENU)$/.test(suffix)) return void 0;
|
|
71
103
|
return suffix;
|
|
72
104
|
}
|
|
73
|
-
function
|
|
105
|
+
function sanitizeSelectorExpression(value) {
|
|
74
106
|
if (!value) return void 0;
|
|
75
|
-
|
|
107
|
+
let sanitized = value.trim();
|
|
108
|
+
if (!sanitized) return void 0;
|
|
109
|
+
const rewriteQuotedArg = (full, prefix, quote, raw) => {
|
|
110
|
+
const cleaned = sanitizeLabelText(raw) ?? raw.trim();
|
|
111
|
+
return `${prefix}${quote}${quote === '"' ? escapeForDoubleQuotedString(cleaned) : escapeForSingleQuotedString(cleaned)}${quote}`;
|
|
112
|
+
};
|
|
113
|
+
sanitized = sanitized.replace(
|
|
114
|
+
/((?:getByLabel|getByPlaceholder|getByTestId|getByText)\()(['"])(.*?)\2/g,
|
|
115
|
+
rewriteQuotedArg
|
|
116
|
+
);
|
|
117
|
+
sanitized = sanitized.replace(
|
|
118
|
+
/(getByRole\((['"])(?:textbox|searchbox|combobox|spinbutton)\2,\s*\{\s*name:\s*)(['"])(.*?)\3/g,
|
|
119
|
+
(full, prefix, _roleQuote, quote, raw) => rewriteQuotedArg(full, prefix, quote, raw)
|
|
120
|
+
);
|
|
121
|
+
return sanitized.replace(/\s+/g, " ");
|
|
122
|
+
}
|
|
123
|
+
function normalizeSelectorTarget(value) {
|
|
124
|
+
const sanitized = sanitizeSelectorExpression(value);
|
|
125
|
+
if (!sanitized) return void 0;
|
|
126
|
+
const trimmed = sanitized.trim();
|
|
76
127
|
if (!trimmed) return void 0;
|
|
77
128
|
return trimmed.replace(/^page\./, "").replace(/\s+/g, " ");
|
|
78
129
|
}
|
|
@@ -120,7 +171,10 @@ function selectorTargetToEnvSuffix(value) {
|
|
|
120
171
|
return sanitizeEnvSegment(locatorArg.replace(/^[#.]+/, ""));
|
|
121
172
|
}
|
|
122
173
|
const labelArg = quotedArg(/^(?:getByLabel|getByPlaceholder|getByTestId|getByText)\((['"])(.*?)\1/i) ?? quotedArg(/^getByRole\((['"])(?:textbox|searchbox|combobox|spinbutton)\1,\s*\{\s*name:\s*(['"])(.*?)\2/i, 3);
|
|
123
|
-
if (labelArg)
|
|
174
|
+
if (labelArg) {
|
|
175
|
+
const sanitizedLabel = sanitizeLabelText(labelArg) ?? labelArg;
|
|
176
|
+
return fieldNameToEnvSuffix(sanitizedLabel) ?? sanitizeEnvSegment(sanitizedLabel);
|
|
177
|
+
}
|
|
124
178
|
return void 0;
|
|
125
179
|
}
|
|
126
180
|
function isGenericFallbackValue(value) {
|
|
@@ -235,6 +289,49 @@ function parseStepBinding(step) {
|
|
|
235
289
|
}
|
|
236
290
|
return void 0;
|
|
237
291
|
}
|
|
292
|
+
function inferNegativeAuthTarget(norm) {
|
|
293
|
+
const tags = (norm.tags ?? []).map((tag) => tag.toLowerCase());
|
|
294
|
+
const content = [norm.id, norm.title, ...norm.expected ?? []].filter(Boolean).join(" ").toLowerCase();
|
|
295
|
+
const isNegativeCase = tags.includes("negative") || /\b(invalid|wrong|incorrect|bad|nonexistent|not an email)\b/.test(content);
|
|
296
|
+
if (!isNegativeCase) return void 0;
|
|
297
|
+
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)) {
|
|
298
|
+
return "EMAIL";
|
|
299
|
+
}
|
|
300
|
+
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)) {
|
|
301
|
+
return "USERNAME";
|
|
302
|
+
}
|
|
303
|
+
if (/\b(?:invalid|wrong|incorrect|bad)\s+password\b|\bpassword\b.{0,24}\b(?:invalid|wrong|incorrect|bad)\b/.test(content)) {
|
|
304
|
+
return "PASSWORD";
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
function negativeAuthFallbackValue(target) {
|
|
309
|
+
return target === "EMAIL" ? "invalid-not-an-email" : target === "PASSWORD" ? "wrong-password-123" : "nonexistent-user";
|
|
310
|
+
}
|
|
311
|
+
function validAuthTemplateValue(fieldSuffix) {
|
|
312
|
+
if (fieldSuffix === "EMAIL" || fieldSuffix === "USERNAME" || fieldSuffix === "PASSWORD") {
|
|
313
|
+
return `\${CT_VAR_${fieldSuffix}}`;
|
|
314
|
+
}
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
function rewriteNegativeAuthSteps(steps, stepHints, norm) {
|
|
318
|
+
const target = inferNegativeAuthTarget(norm);
|
|
319
|
+
if (!target) return steps;
|
|
320
|
+
return steps.map((step, index) => {
|
|
321
|
+
const binding = parseStepBinding(step);
|
|
322
|
+
if (binding?.kind !== "fill") return step;
|
|
323
|
+
const selectorTarget = normalizeSelectorTarget(stepHints?.[index]?.selector);
|
|
324
|
+
const fieldSuffix = fieldNameToEnvSuffix(binding.rawField) ?? fieldNameToEnvSuffix(binding.fieldLabel) ?? selectorTargetToEnvSuffix(selectorTarget);
|
|
325
|
+
if (!fieldSuffix) return step;
|
|
326
|
+
const rawField = binding.rawField ?? binding.fieldLabel ?? "field";
|
|
327
|
+
if (fieldSuffix === target) {
|
|
328
|
+
return `Fill in ${rawField} with '${negativeAuthFallbackValue(target)}'`;
|
|
329
|
+
}
|
|
330
|
+
const validTemplate = validAuthTemplateValue(fieldSuffix);
|
|
331
|
+
if (validTemplate) return `Fill in ${rawField} with \`${validTemplate}\``;
|
|
332
|
+
return step;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
238
335
|
function resolveStepVarOverride(overrides, envKey, keySuffix, rawField, fieldLabel) {
|
|
239
336
|
const candidates = [envKey, keySuffix, rawField, fieldLabel];
|
|
240
337
|
for (const candidate of candidates) {
|
|
@@ -270,7 +367,8 @@ function extractStepVars(steps, stepHints, assertionHints, overrides) {
|
|
|
270
367
|
const overrideValue = resolveStepVarOverride(overrides, envKey, keySuffix, binding.rawField, binding.fieldLabel);
|
|
271
368
|
const fallbackFromBinding = binding.value?.trim();
|
|
272
369
|
const hasRuntimeEnvValue = process.env[envKey] !== void 0;
|
|
273
|
-
const
|
|
370
|
+
const shouldUseRuntimeEnvValue = hasRuntimeEnvValue && (!fallbackFromBinding || isGenericFallbackValue(fallbackFromBinding));
|
|
371
|
+
const shouldDeclareVariable = binding.kind === "select" || templateEnvKey !== void 0 || overrideValue !== void 0 || shouldUseRuntimeEnvValue;
|
|
274
372
|
let stepVar = shouldDeclareVariable ? seen.get(envKey) : void 0;
|
|
275
373
|
if (shouldDeclareVariable && !stepVar) {
|
|
276
374
|
const placeholderFallback = binding.kind === "fill" ? buildFillPlaceholder(binding.rawField, binding.fieldLabel, selectorTarget) : assertionFallback ?? "option";
|
|
@@ -393,10 +491,89 @@ function rewriteValueAssertionToUseVariable(playwright, stepState) {
|
|
|
393
491
|
if (!valueExpression) return void 0;
|
|
394
492
|
return ensureStatement(`await expect(page.${parsed.target}).toHaveValue(${valueExpression})`);
|
|
395
493
|
}
|
|
494
|
+
function parseEmptyFieldStep(step) {
|
|
495
|
+
const s = step.trim();
|
|
496
|
+
const patterns = [
|
|
497
|
+
/\bleave\s+(.+?)\s+(?:field|input|box|area)?\s*(?:empty|blank)\b/i,
|
|
498
|
+
/\b(?:do not|don't)\s+fill\s+(?:in\s+)?(.+?)\s*(?:field|input|box|area)?\b/i,
|
|
499
|
+
/\bskip\s+(?:filling\s+)?(.+?)\s*(?:field|input|box|area)?\b/i
|
|
500
|
+
];
|
|
501
|
+
for (const pattern of patterns) {
|
|
502
|
+
const match = s.match(pattern);
|
|
503
|
+
if (!match?.[1]) continue;
|
|
504
|
+
const rawField = match[1].trim();
|
|
505
|
+
return {
|
|
506
|
+
rawField,
|
|
507
|
+
fieldLabel: cleanFieldLabel(rawField)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
function selectorTokensForField(rawField, fieldLabel, selectorTarget) {
|
|
513
|
+
const suffix = fieldNameToEnvSuffix(rawField) ?? fieldNameToEnvSuffix(fieldLabel) ?? selectorTargetToEnvSuffix(selectorTarget);
|
|
514
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
515
|
+
const add = (value) => {
|
|
516
|
+
const cleaned = cleanFieldLabel(value) ?? sanitizeLabelText(value);
|
|
517
|
+
if (!cleaned) return;
|
|
518
|
+
cleaned.toLowerCase().split(/[^a-z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3).filter((token) => !/^(?:the|field|input|box|area|dropdown|combobox|select|menu)$/.test(token)).forEach((token) => tokens.add(token));
|
|
519
|
+
};
|
|
520
|
+
add(rawField);
|
|
521
|
+
add(fieldLabel);
|
|
522
|
+
switch (suffix) {
|
|
523
|
+
case "USERNAME":
|
|
524
|
+
tokens.add("username");
|
|
525
|
+
tokens.add("user");
|
|
526
|
+
break;
|
|
527
|
+
case "PASSWORD":
|
|
528
|
+
tokens.add("password");
|
|
529
|
+
tokens.add("pass");
|
|
530
|
+
break;
|
|
531
|
+
case "EMAIL":
|
|
532
|
+
tokens.add("email");
|
|
533
|
+
break;
|
|
534
|
+
default:
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
return Array.from(tokens);
|
|
538
|
+
}
|
|
539
|
+
function buildFallbackSelector(kind, rawField, fieldLabel, selectorTarget) {
|
|
540
|
+
const tokens = selectorTokensForField(rawField, fieldLabel, selectorTarget);
|
|
541
|
+
const suffix = fieldNameToEnvSuffix(rawField) ?? fieldNameToEnvSuffix(fieldLabel) ?? selectorTargetToEnvSuffix(selectorTarget);
|
|
542
|
+
const selectors = /* @__PURE__ */ new Set();
|
|
543
|
+
if (kind === "fill" && suffix === "PASSWORD") selectors.add('input[type="password"]');
|
|
544
|
+
const baseTags = kind === "select" ? ["select"] : ["input", "textarea"];
|
|
545
|
+
const attributes = ["name", "id", "placeholder", "aria-label"];
|
|
546
|
+
for (const token of tokens) {
|
|
547
|
+
const escapedToken = escapeForDoubleQuotedString(token);
|
|
548
|
+
for (const tag of baseTags) {
|
|
549
|
+
for (const attribute of attributes) {
|
|
550
|
+
selectors.add(`${tag}[${attribute}*="${escapedToken}" i]`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (selectors.size === 0) return void 0;
|
|
555
|
+
return Array.from(selectors).join(", ");
|
|
556
|
+
}
|
|
557
|
+
function buildFieldLocatorExpression(kind, rawField, fieldLabel, selectorTarget) {
|
|
558
|
+
const normalizedSelector = normalizeSelectorTarget(selectorTarget);
|
|
559
|
+
if (normalizedSelector && !/^getByLabel\(/i.test(normalizedSelector)) {
|
|
560
|
+
return `page.${normalizedSelector}`;
|
|
561
|
+
}
|
|
562
|
+
const sanitizedField = sanitizeLabelText(fieldLabel);
|
|
563
|
+
const labelMatch = normalizedSelector?.match(/^getByLabel\((['"])(.*?)\1\)$/i);
|
|
564
|
+
const labelText = sanitizedField ?? sanitizeLabelText(labelMatch?.[2]);
|
|
565
|
+
if (!labelText) {
|
|
566
|
+
return normalizedSelector ? `page.${normalizedSelector}` : void 0;
|
|
567
|
+
}
|
|
568
|
+
const baseLocator = normalizedSelector ? `page.${normalizedSelector}` : `page.getByLabel('${escapeForSingleQuotedString(labelText)}')`;
|
|
569
|
+
const fallbackSelector = buildFallbackSelector(kind, rawField, labelText, normalizedSelector);
|
|
570
|
+
if (!fallbackSelector) return baseLocator;
|
|
571
|
+
return `${baseLocator}.or(page.locator(${JSON.stringify(fallbackSelector)})).first()`;
|
|
572
|
+
}
|
|
396
573
|
function stepToPlaywright(step, url, hint, stepVar) {
|
|
397
574
|
const s = step.trim();
|
|
398
|
-
const hintedSelector = hint?.selector ? `page.${hint.selector}` : void 0;
|
|
399
575
|
const selectorTarget = normalizeSelectorTarget(hint?.selector);
|
|
576
|
+
const hintedSelector = selectorTarget ? `page.${selectorTarget}` : void 0;
|
|
400
577
|
const valueExpression = (value) => stepVar?.constName ?? `'${escapeForSingleQuotedString(value)}'`;
|
|
401
578
|
if (/^(navigate|go to|open|visit|load)/i.test(s)) {
|
|
402
579
|
const urlMatch = s.match(/https?:\/\/[^\s'"]+/) || s.match(/["']([^"']+)["']/);
|
|
@@ -406,10 +583,19 @@ function stepToPlaywright(step, url, hint, stepVar) {
|
|
|
406
583
|
}
|
|
407
584
|
return `await page.goto('${escapeForSingleQuotedString(dest)}');`;
|
|
408
585
|
}
|
|
586
|
+
const emptyField = parseEmptyFieldStep(s);
|
|
587
|
+
if (emptyField) {
|
|
588
|
+
const locatorExpression = buildFieldLocatorExpression("fill", emptyField.rawField, emptyField.fieldLabel, selectorTarget) ?? hintedSelector;
|
|
589
|
+
if (locatorExpression) {
|
|
590
|
+
const fieldDescription = sanitizeLabelText(emptyField.fieldLabel ?? emptyField.rawField) ?? "field";
|
|
591
|
+
return `await ${locatorExpression}.fill(''); // ${fieldDescription} intentionally left empty for validation`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
409
594
|
const binding = parseStepBinding(s);
|
|
410
595
|
if (binding?.kind === "fill") {
|
|
411
596
|
const fallbackValue = stepVar?.fallback ?? resolveLiteralFillValue(binding, selectorTarget);
|
|
412
|
-
|
|
597
|
+
const locatorExpression = buildFieldLocatorExpression("fill", binding.rawField, binding.fieldLabel, selectorTarget) ?? hintedSelector;
|
|
598
|
+
if (locatorExpression) return `await ${locatorExpression}.fill(${valueExpression(fallbackValue)});`;
|
|
413
599
|
const field = binding.fieldLabel ?? "field";
|
|
414
600
|
return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill(${valueExpression(fallbackValue)});`;
|
|
415
601
|
}
|
|
@@ -432,7 +618,8 @@ function stepToPlaywright(step, url, hint, stepVar) {
|
|
|
432
618
|
}
|
|
433
619
|
if (binding?.kind === "select") {
|
|
434
620
|
const fallbackValue = binding.value ?? stepVar?.fallback ?? "option";
|
|
435
|
-
|
|
621
|
+
const locatorExpression = buildFieldLocatorExpression("select", binding.rawField, binding.fieldLabel, selectorTarget) ?? hintedSelector;
|
|
622
|
+
if (locatorExpression) return `await ${locatorExpression}.selectOption(${valueExpression(fallbackValue)});`;
|
|
436
623
|
if (binding.fieldLabel) {
|
|
437
624
|
return `await page.getByLabel('${escapeForSingleQuotedString(binding.fieldLabel)}').selectOption(${valueExpression(fallbackValue)});`;
|
|
438
625
|
}
|
|
@@ -485,20 +672,12 @@ function expectedToAssertion(expected, norm, hint, stepState) {
|
|
|
485
672
|
return `await expect(page).toHaveURL(/${fallbackPattern}/); // form did not navigate`;
|
|
486
673
|
}
|
|
487
674
|
if (/\b(remains?|stays?|still)\s+on\b/i.test(s)) {
|
|
488
|
-
const
|
|
489
|
-
const pagePattern = pageMatch?.[1] ? pageNameToUrlPattern(pageMatch[1]) : void 0;
|
|
675
|
+
const pagePattern = urlPatternFromIntentText(s);
|
|
490
676
|
return `await expect(page).toHaveURL(/${pagePattern || currentUrlPattern || "login"}/);`;
|
|
491
677
|
}
|
|
492
678
|
if (/\b(url|redirect(?:s|ed)?|navigate(?:s|d)?|route|path)\b/i.test(s)) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
if (matchedPath) {
|
|
496
|
-
const fromUrl = matchedPath.startsWith("http") ? urlPatternFromAbsoluteUrl(matchedPath) : void 0;
|
|
497
|
-
const directPath = matchedPath.startsWith("/") ? matchedPath.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/") : void 0;
|
|
498
|
-
const finalPattern = fromUrl ?? directPath ?? currentUrlPattern;
|
|
499
|
-
if (finalPattern !== void 0) return `await expect(page).toHaveURL(/${finalPattern}/);`;
|
|
500
|
-
}
|
|
501
|
-
return `await expect(page).toHaveURL(/dashboard|success|home/);`;
|
|
679
|
+
const finalPattern = urlPatternFromIntentText(s) ?? currentUrlPattern ?? ".+";
|
|
680
|
+
return `await expect(page).toHaveURL(/${finalPattern}/);`;
|
|
502
681
|
}
|
|
503
682
|
if (/\bpage title\b|\btitle should\b|\bdocument title\b/i.test(s)) {
|
|
504
683
|
const titleMatch = s.match(/["']([^"']+)["']/);
|
|
@@ -631,7 +810,7 @@ function buildTestTitle(norm) {
|
|
|
631
810
|
}
|
|
632
811
|
function buildSpecFile(norm, pomClassName, pomImportPath, overrides) {
|
|
633
812
|
const title = buildTestTitle(norm);
|
|
634
|
-
const steps = norm.steps ?? [];
|
|
813
|
+
const steps = rewriteNegativeAuthSteps(norm.steps ?? [], norm.step_hints, norm);
|
|
635
814
|
const expected = norm.expected ?? [];
|
|
636
815
|
const stepVars = extractStepVars(steps, norm.step_hints, norm.assertion_hints, overrides);
|
|
637
816
|
let importPath = pomImportPath.replace(/\\/g, "/");
|
|
@@ -664,6 +843,93 @@ ${assertionLines}
|
|
|
664
843
|
});
|
|
665
844
|
`;
|
|
666
845
|
}
|
|
846
|
+
var PLAYWRIGHT_METHOD_NAMES = /* @__PURE__ */ new Set([
|
|
847
|
+
"all",
|
|
848
|
+
"check",
|
|
849
|
+
"click",
|
|
850
|
+
"evaluate",
|
|
851
|
+
"fill",
|
|
852
|
+
"filter",
|
|
853
|
+
"first",
|
|
854
|
+
"getByLabel",
|
|
855
|
+
"getByPlaceholder",
|
|
856
|
+
"getByRole",
|
|
857
|
+
"getByTestId",
|
|
858
|
+
"getByText",
|
|
859
|
+
"goto",
|
|
860
|
+
"hover",
|
|
861
|
+
"locator",
|
|
862
|
+
"nth",
|
|
863
|
+
"or",
|
|
864
|
+
"press",
|
|
865
|
+
"reload",
|
|
866
|
+
"selectOption",
|
|
867
|
+
"toBeChecked",
|
|
868
|
+
"toBeDisabled",
|
|
869
|
+
"toBeEnabled",
|
|
870
|
+
"toBeHidden",
|
|
871
|
+
"toBeVisible",
|
|
872
|
+
"toContainText",
|
|
873
|
+
"toHaveCount",
|
|
874
|
+
"toHaveTitle",
|
|
875
|
+
"toHaveURL",
|
|
876
|
+
"toHaveValue",
|
|
877
|
+
"uncheck",
|
|
878
|
+
"waitForEvent",
|
|
879
|
+
"waitForLoad",
|
|
880
|
+
"waitForLoadState"
|
|
881
|
+
]);
|
|
882
|
+
function levenshteinDistance(left, right) {
|
|
883
|
+
const distances = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
884
|
+
for (let row = 1; row <= left.length; row++) {
|
|
885
|
+
let previousDiagonal = distances[0];
|
|
886
|
+
distances[0] = row;
|
|
887
|
+
for (let column = 1; column <= right.length; column++) {
|
|
888
|
+
const temp = distances[column];
|
|
889
|
+
distances[column] = Math.min(
|
|
890
|
+
distances[column] + 1,
|
|
891
|
+
distances[column - 1] + 1,
|
|
892
|
+
previousDiagonal + (left[row - 1] === right[column - 1] ? 0 : 1)
|
|
893
|
+
);
|
|
894
|
+
previousDiagonal = temp;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return distances[right.length];
|
|
898
|
+
}
|
|
899
|
+
function findClosestPlaywrightMethod(methodName) {
|
|
900
|
+
let bestCandidate;
|
|
901
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
902
|
+
for (const candidate of PLAYWRIGHT_METHOD_NAMES) {
|
|
903
|
+
if (candidate[0] !== methodName[0]) continue;
|
|
904
|
+
const distance = levenshteinDistance(methodName, candidate);
|
|
905
|
+
if (distance < bestDistance) {
|
|
906
|
+
bestDistance = distance;
|
|
907
|
+
bestCandidate = candidate;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return bestDistance <= 2 ? bestCandidate : void 0;
|
|
911
|
+
}
|
|
912
|
+
function stripBackticksInsideRegexLiterals(line) {
|
|
913
|
+
return line.includes("`") ? line.replace(/`/g, "") : line;
|
|
914
|
+
}
|
|
915
|
+
function sanitizeQuotedSelectorArgsInLine(line) {
|
|
916
|
+
return sanitizeSelectorExpression(line) ?? line;
|
|
917
|
+
}
|
|
918
|
+
function repairPlaywrightMethodsInLine(line, fileLabel) {
|
|
919
|
+
if (!/\bawait\b/.test(line)) return line;
|
|
920
|
+
return line.replace(/\.(\w+)\(/g, (match, methodName) => {
|
|
921
|
+
if (PLAYWRIGHT_METHOD_NAMES.has(methodName)) return match;
|
|
922
|
+
const repaired = findClosestPlaywrightMethod(methodName);
|
|
923
|
+
if (repaired) return `.${repaired}(`;
|
|
924
|
+
throw new Error(`Generated unsupported Playwright method ".${methodName}(" in ${fileLabel}`);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function finalizeGeneratedPlaywrightSource(source, fileLabel) {
|
|
928
|
+
return source.split("\n").map((line) => repairPlaywrightMethodsInLine(
|
|
929
|
+
sanitizeQuotedSelectorArgsInLine(stripBackticksInsideRegexLiterals(line)),
|
|
930
|
+
fileLabel
|
|
931
|
+
)).join("\n");
|
|
932
|
+
}
|
|
667
933
|
async function gen(opts) {
|
|
668
934
|
if (opts.lang !== "ts" && opts.lang !== "js") {
|
|
669
935
|
console.error("\u274C --lang must be ts or js");
|
|
@@ -695,14 +961,23 @@ async function gen(opts) {
|
|
|
695
961
|
const pomFileName = pomClassName + pomExt;
|
|
696
962
|
const pomFilePath = join(pagesOutDir, pomFileName);
|
|
697
963
|
if (!existsSync(pomFilePath)) {
|
|
698
|
-
writeFileSync(
|
|
964
|
+
writeFileSync(
|
|
965
|
+
pomFilePath,
|
|
966
|
+
finalizeGeneratedPlaywrightSource(buildPomClass(pomClassName, norm, lang), pomFileName)
|
|
967
|
+
);
|
|
699
968
|
pomCount++;
|
|
700
969
|
}
|
|
701
970
|
const relToPages = relative(testsOutDir, pagesOutDir);
|
|
702
971
|
const pomImportPath = join(relToPages, pomFileName);
|
|
703
972
|
const stem = basename(f).replace(/\.json$/, "").replace(/[^\w-]+/g, "-");
|
|
704
973
|
const specPath = join(testsOutDir, `${stem}.${specExt}`);
|
|
705
|
-
writeFileSync(
|
|
974
|
+
writeFileSync(
|
|
975
|
+
specPath,
|
|
976
|
+
finalizeGeneratedPlaywrightSource(
|
|
977
|
+
buildSpecFile(norm, pomClassName, pomImportPath, varsOverride),
|
|
978
|
+
basename(specPath)
|
|
979
|
+
)
|
|
980
|
+
);
|
|
706
981
|
specCount++;
|
|
707
982
|
}
|
|
708
983
|
console.log(`\u2705 Generated ${specCount} spec file(s) \u2192 ${opts.out}/`);
|
|
@@ -726,7 +1001,8 @@ Examples:
|
|
|
726
1001
|
}
|
|
727
1002
|
|
|
728
1003
|
export {
|
|
1004
|
+
finalizeGeneratedPlaywrightSource,
|
|
729
1005
|
gen,
|
|
730
1006
|
genCmd
|
|
731
1007
|
};
|
|
732
|
-
//# sourceMappingURL=chunk-
|
|
1008
|
+
//# sourceMappingURL=chunk-P6WZO7G7.js.map
|