@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.
@@ -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.replace(/\b(the|a|an|user)\b/gi, " ").replace(/\b(page|screen)\b/gi, " ").trim();
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.replace(/[.?!,:;]+$/g, "").replace(/^(?:the|a|an)\s+/i, "").replace(/\s+(?:field|input|box|area|dropdown|combobox|select|menu)\b/gi, "").trim();
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
- if (!value) return false;
62
- const cleaned = value.trim().toLowerCase();
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
- if (!value || isPositionalFieldReference(value)) return void 0;
67
- const cleaned = value.replace(/[.?!,:;]+$/g, "").replace(/^(?:the|a|an)\s+/i, "").replace(/\b(?:field|input|box|area|dropdown|combobox|select|menu)\b/gi, " ").trim();
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 normalizeSelectorTarget(value) {
105
+ function sanitizeSelectorExpression(value) {
74
106
  if (!value) return void 0;
75
- const trimmed = value.trim();
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) return fieldNameToEnvSuffix(labelArg) ?? sanitizeEnvSegment(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 shouldDeclareVariable = binding.kind === "select" || templateEnvKey !== void 0 || overrideValue !== void 0 || hasRuntimeEnvValue;
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
- if (hintedSelector) return `await ${hintedSelector}.fill(${valueExpression(fallbackValue)});`;
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
- if (hintedSelector) return `await ${hintedSelector}.selectOption(${valueExpression(fallbackValue)});`;
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 pageMatch = s.match(/\b(?:remains?|stays?|still)\s+on\s+(?:the\s+)?(.+?)(?:\s+page|\s+screen)?$/i);
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 pathMatch = s.match(/["'](\/[^"']+)["']/) ?? s.match(/to\s+(\/[\w/-]+)/i) ?? s.match(/https?:\/\/[^\s'"]+/i);
494
- const matchedPath = pathMatch?.[1] ?? pathMatch?.[0];
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(pomFilePath, buildPomClass(pomClassName, norm, lang));
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(specPath, buildSpecFile(norm, pomClassName, pomImportPath, varsOverride));
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-PYKYHIO3.js.map
1008
+ //# sourceMappingURL=chunk-P6WZO7G7.js.map