@cementic/cementic-test 0.2.8 → 0.2.9
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/README.md +12 -0
- package/dist/{chunk-TYWO3CBC.js → chunk-XM2TYPU7.js} +160 -23
- package/dist/chunk-XM2TYPU7.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{gen-QEQJ3Z7S.js → gen-EST5UZNZ.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-TYWO3CBC.js.map +0 -1
- /package/dist/{gen-QEQJ3Z7S.js.map → gen-EST5UZNZ.js.map} +0 -0
package/README.md
CHANGED
|
@@ -171,6 +171,12 @@ ct tc url https://example.com/login --ai --feature "Login"
|
|
|
171
171
|
ct normalize ./cases --and-gen
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
Use `ct gen --vars` when you want generated specs to keep env-backed test data with custom defaults:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
ct gen --lang ts --vars '{"email":"admin@example.com","password":"supersecret"}'
|
|
178
|
+
```
|
|
179
|
+
|
|
174
180
|
### Run tests
|
|
175
181
|
|
|
176
182
|
```bash
|
|
@@ -359,6 +365,12 @@ And start automating.
|
|
|
359
365
|
|
|
360
366
|
## Changelog
|
|
361
367
|
|
|
368
|
+
### v0.2.9
|
|
369
|
+
|
|
370
|
+
- added `CT_VAR_*` extraction in `ct gen` for input and select test data so generated specs use env-backed constants instead of hardcoded values
|
|
371
|
+
- added `ct gen --vars '{...}'` to override generated fallback values without editing the emitted spec files by hand
|
|
372
|
+
- added regression coverage for variableized generated specs, selector-hint preservation, and select-option extraction
|
|
373
|
+
|
|
362
374
|
### v0.2.8
|
|
363
375
|
|
|
364
376
|
- 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
|
|
@@ -46,14 +46,149 @@ function visibleTextRegexFromPhrase(value) {
|
|
|
46
46
|
if (tokens.length === 0) return ".+";
|
|
47
47
|
return tokens.map((token) => escapeForRegex(token)).join("\\s+");
|
|
48
48
|
}
|
|
49
|
+
function sanitizeEnvSegment(value) {
|
|
50
|
+
return value.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
51
|
+
}
|
|
52
|
+
function normalizeOverrideKey(value) {
|
|
53
|
+
return sanitizeEnvSegment(value.replace(/^CT_VAR_/i, ""));
|
|
54
|
+
}
|
|
55
|
+
function cleanFieldLabel(value) {
|
|
56
|
+
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();
|
|
58
|
+
return cleaned || void 0;
|
|
59
|
+
}
|
|
60
|
+
function isPositionalFieldReference(value) {
|
|
61
|
+
if (!value) return false;
|
|
62
|
+
const cleaned = value.trim().toLowerCase();
|
|
63
|
+
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
|
+
}
|
|
65
|
+
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();
|
|
68
|
+
const suffix = sanitizeEnvSegment(cleaned);
|
|
69
|
+
if (!suffix) return void 0;
|
|
70
|
+
if (/^(?:FIELD|INPUT|BOX|AREA|DROPDOWN|COMBOBOX|SELECT|MENU)$/.test(suffix)) return void 0;
|
|
71
|
+
return suffix;
|
|
72
|
+
}
|
|
73
|
+
function parseStepBinding(step) {
|
|
74
|
+
const s = step.trim();
|
|
75
|
+
const fillMatch = s.match(/\b(?:fill|input|write)\s+(?:in\s+)?(.+?)\s+with\s+(['"])(.*?)\2/i);
|
|
76
|
+
if (fillMatch) {
|
|
77
|
+
return {
|
|
78
|
+
kind: "fill",
|
|
79
|
+
rawField: fillMatch[1].trim(),
|
|
80
|
+
fieldLabel: cleanFieldLabel(fillMatch[1]),
|
|
81
|
+
value: fillMatch[3].trim()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const enterMatch = s.match(/\b(?:enter|type)\s+(['"])(.*?)\1\s+(?:in|into)\s+(.+)/i);
|
|
85
|
+
if (enterMatch) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "fill",
|
|
88
|
+
rawField: enterMatch[3].trim(),
|
|
89
|
+
fieldLabel: cleanFieldLabel(enterMatch[3]),
|
|
90
|
+
value: enterMatch[2].trim()
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const selectMatch = s.match(/\b(?:select|choose|pick)\s+(['"])(.*?)\1\s+(?:from|in|into|on)\s+(.+)/i);
|
|
94
|
+
if (selectMatch) {
|
|
95
|
+
return {
|
|
96
|
+
kind: "select",
|
|
97
|
+
rawField: selectMatch[3].trim(),
|
|
98
|
+
fieldLabel: cleanFieldLabel(selectMatch[3]),
|
|
99
|
+
value: selectMatch[2].trim()
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
function resolveStepVarOverride(overrides, envKey, keySuffix, rawField, fieldLabel) {
|
|
105
|
+
const candidates = [envKey, keySuffix, rawField, fieldLabel];
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (!candidate) continue;
|
|
108
|
+
const normalized = normalizeOverrideKey(candidate);
|
|
109
|
+
if (normalized && overrides.has(normalized)) return overrides.get(normalized);
|
|
110
|
+
}
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
function extractStepVars(steps, overrides) {
|
|
114
|
+
const declarations = [];
|
|
115
|
+
const byStepIndex = /* @__PURE__ */ new Map();
|
|
116
|
+
const seen = /* @__PURE__ */ new Map();
|
|
117
|
+
let unnamedFillIndex = 0;
|
|
118
|
+
let unnamedSelectIndex = 0;
|
|
119
|
+
for (const [index, step] of steps.entries()) {
|
|
120
|
+
const binding = parseStepBinding(step);
|
|
121
|
+
if (!binding) continue;
|
|
122
|
+
const keySuffix = fieldNameToEnvSuffix(binding.rawField) ?? `${binding.kind === "select" ? "SELECT" : "INPUT"}_${binding.kind === "select" ? ++unnamedSelectIndex : ++unnamedFillIndex}`;
|
|
123
|
+
const envKey = `CT_VAR_${keySuffix}`;
|
|
124
|
+
let stepVar = seen.get(envKey);
|
|
125
|
+
if (!stepVar) {
|
|
126
|
+
stepVar = {
|
|
127
|
+
envKey,
|
|
128
|
+
constName: envKey,
|
|
129
|
+
fallback: resolveStepVarOverride(overrides, envKey, keySuffix, binding.rawField, binding.fieldLabel) ?? binding.value
|
|
130
|
+
};
|
|
131
|
+
seen.set(envKey, stepVar);
|
|
132
|
+
declarations.push(stepVar);
|
|
133
|
+
}
|
|
134
|
+
byStepIndex.set(index, stepVar);
|
|
135
|
+
}
|
|
136
|
+
return { declarations, byStepIndex };
|
|
137
|
+
}
|
|
138
|
+
function buildVariableHeaderComment(stepVars) {
|
|
139
|
+
if (stepVars.length === 0) return "";
|
|
140
|
+
const inlineExample = stepVars.map((stepVar) => `${stepVar.envKey}=${stepVar.fallback}`).join(" ");
|
|
141
|
+
return [
|
|
142
|
+
"// Test variables:",
|
|
143
|
+
"// Set these environment variables before running this spec:",
|
|
144
|
+
"//",
|
|
145
|
+
...stepVars.map((stepVar) => `// ${stepVar.envKey}=your-value`),
|
|
146
|
+
"//",
|
|
147
|
+
"// Or pass them inline:",
|
|
148
|
+
`// ${inlineExample} npx playwright test`,
|
|
149
|
+
""
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
function buildVariableDeclarations(stepVars) {
|
|
153
|
+
if (stepVars.length === 0) return "";
|
|
154
|
+
return [
|
|
155
|
+
" // Test variables",
|
|
156
|
+
...stepVars.map(
|
|
157
|
+
(stepVar) => ` const ${stepVar.constName} = process.env['${stepVar.envKey}'] ?? '${escapeForSingleQuotedString(stepVar.fallback)}';`
|
|
158
|
+
),
|
|
159
|
+
""
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
function parseVarsOverride(raw) {
|
|
163
|
+
if (!raw) return /* @__PURE__ */ new Map();
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
console.error("\u274C --vars must be a valid JSON object");
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
172
|
+
console.error("\u274C --vars must be a valid JSON object");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
177
|
+
const normalizedKey = normalizeOverrideKey(key);
|
|
178
|
+
if (!normalizedKey) continue;
|
|
179
|
+
overrides.set(normalizedKey, String(value));
|
|
180
|
+
}
|
|
181
|
+
return overrides;
|
|
182
|
+
}
|
|
49
183
|
function ensureStatement(value) {
|
|
50
184
|
const trimmed = value.trim();
|
|
51
185
|
if (!trimmed) return trimmed;
|
|
52
186
|
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
|
53
187
|
}
|
|
54
|
-
function stepToPlaywright(step, url, hint) {
|
|
188
|
+
function stepToPlaywright(step, url, hint, stepVar) {
|
|
55
189
|
const s = step.trim();
|
|
56
190
|
const hintedSelector = hint?.selector ? `page.${hint.selector}` : void 0;
|
|
191
|
+
const valueExpression = (value) => stepVar?.constName ?? `'${escapeForSingleQuotedString(value)}'`;
|
|
57
192
|
if (/^(navigate|go to|open|visit|load)/i.test(s)) {
|
|
58
193
|
const urlMatch = s.match(/https?:\/\/[^\s'"]+/) || s.match(/["']([^"']+)["']/);
|
|
59
194
|
const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? "/";
|
|
@@ -62,15 +197,11 @@ function stepToPlaywright(step, url, hint) {
|
|
|
62
197
|
}
|
|
63
198
|
return `await page.goto('${escapeForSingleQuotedString(dest)}');`;
|
|
64
199
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const intoMatch = s.match(/\bin(?:to)?\s+(?:the\s+)?([a-zA-Z][a-zA-Z\s]{1,25})(?:\s+field|\s+input|\s+box|\s+area)?/i);
|
|
71
|
-
const fieldMatch = s.match(/([a-zA-Z][a-zA-Z\s]{1,25})\s+(?:field|input|box|area)/i);
|
|
72
|
-
const field = (inWithMatch?.[1] ?? intoMatch?.[1] ?? fieldMatch?.[1] ?? "field").trim();
|
|
73
|
-
return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill('${escapeForSingleQuotedString(value)}');`;
|
|
200
|
+
const binding = parseStepBinding(s);
|
|
201
|
+
if (binding?.kind === "fill") {
|
|
202
|
+
if (hintedSelector) return `await ${hintedSelector}.fill(${valueExpression(binding.value)});`;
|
|
203
|
+
const field = binding.fieldLabel ?? "field";
|
|
204
|
+
return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill(${valueExpression(binding.value)});`;
|
|
74
205
|
}
|
|
75
206
|
if (/\bclick\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {
|
|
76
207
|
if (hintedSelector) return `await ${hintedSelector}.click();`;
|
|
@@ -91,11 +222,12 @@ function stepToPlaywright(step, url, hint) {
|
|
|
91
222
|
const target = s.replace(/^(click|press|tap)\s+(on\s+)?/i, "").trim();
|
|
92
223
|
return `await page.getByText('${escapeForSingleQuotedString(target)}').click();`;
|
|
93
224
|
}
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
225
|
+
if (binding?.kind === "select") {
|
|
226
|
+
if (hintedSelector) return `await ${hintedSelector}.selectOption(${valueExpression(binding.value)});`;
|
|
227
|
+
if (binding.fieldLabel) {
|
|
228
|
+
return `await page.getByLabel('${escapeForSingleQuotedString(binding.fieldLabel)}').selectOption(${valueExpression(binding.value)});`;
|
|
229
|
+
}
|
|
230
|
+
return `await page.getByRole('combobox').selectOption(${valueExpression(binding.value)});`;
|
|
99
231
|
}
|
|
100
232
|
if (/\b(uncheck|untick|disable)\b/i.test(s)) {
|
|
101
233
|
if (hintedSelector) return `await ${hintedSelector}.uncheck();`;
|
|
@@ -277,25 +409,28 @@ function buildTestTitle(norm) {
|
|
|
277
409
|
const tagSuffix = (norm.tags ?? []).map((t) => `@${t}`).join(" ");
|
|
278
410
|
return [idPart, cleanTitle, tagSuffix].filter(Boolean).join(" \u2014 ").trim();
|
|
279
411
|
}
|
|
280
|
-
function buildSpecFile(norm, pomClassName, pomImportPath) {
|
|
412
|
+
function buildSpecFile(norm, pomClassName, pomImportPath, overrides) {
|
|
281
413
|
const title = buildTestTitle(norm);
|
|
282
414
|
const steps = norm.steps ?? [];
|
|
283
415
|
const expected = norm.expected ?? [];
|
|
416
|
+
const stepVars = extractStepVars(steps, overrides);
|
|
284
417
|
let importPath = pomImportPath.replace(/\\/g, "/");
|
|
285
418
|
if (!importPath.startsWith(".")) importPath = `./${importPath}`;
|
|
286
419
|
importPath = importPath.replace(/\.ts$/, "");
|
|
287
|
-
const stepLines = steps.length ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index])}`).join("\n") : " // TODO: add steps";
|
|
420
|
+
const stepLines = steps.length ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index], stepVars.byStepIndex.get(index))}`).join("\n") : " // TODO: add steps";
|
|
288
421
|
const assertionLines = expected.length ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join("\n") : " // TODO: add assertions";
|
|
289
422
|
const reviewNote = norm.needs_review ? `
|
|
290
423
|
// \u26A0\uFE0F Flagged for review \u2014 steps or assertions may need manual refinement
|
|
291
424
|
` : "";
|
|
425
|
+
const variableHeader = buildVariableHeaderComment(stepVars.declarations);
|
|
426
|
+
const variableDeclarations = buildVariableDeclarations(stepVars.declarations);
|
|
292
427
|
return `import { test, expect } from '@playwright/test';
|
|
293
428
|
import { ${pomClassName} } from '${importPath}';
|
|
294
429
|
|
|
295
|
-
test('${title}', async ({ page }) => {${reviewNote}
|
|
430
|
+
${variableHeader}test('${title}', async ({ page }) => {${reviewNote}
|
|
296
431
|
const pomPage = new ${pomClassName}(page);
|
|
297
432
|
|
|
298
|
-
// \u2500\u2500 Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
433
|
+
${variableDeclarations} // \u2500\u2500 Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
299
434
|
await pomPage.goto();
|
|
300
435
|
await pomPage.waitForLoad();
|
|
301
436
|
|
|
@@ -315,6 +450,7 @@ async function gen(opts) {
|
|
|
315
450
|
const lang = opts.lang;
|
|
316
451
|
const specExt = lang === "js" ? "spec.js" : "spec.ts";
|
|
317
452
|
const pomExt = lang === "js" ? ".js" : ".ts";
|
|
453
|
+
const varsOverride = parseVarsOverride(opts.vars);
|
|
318
454
|
const normalized = await fg([
|
|
319
455
|
".cementic/normalized/*.json",
|
|
320
456
|
"!.cementic/normalized/_index.json"
|
|
@@ -344,7 +480,7 @@ async function gen(opts) {
|
|
|
344
480
|
const pomImportPath = join(relToPages, pomFileName);
|
|
345
481
|
const stem = basename(f).replace(/\.json$/, "").replace(/[^\w-]+/g, "-");
|
|
346
482
|
const specPath = join(testsOutDir, `${stem}.${specExt}`);
|
|
347
|
-
writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath));
|
|
483
|
+
writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath, varsOverride));
|
|
348
484
|
specCount++;
|
|
349
485
|
}
|
|
350
486
|
console.log(`\u2705 Generated ${specCount} spec file(s) \u2192 ${opts.out}/`);
|
|
@@ -360,8 +496,9 @@ function genCmd() {
|
|
|
360
496
|
Examples:
|
|
361
497
|
$ ct gen --lang ts
|
|
362
498
|
$ ct gen --lang js --out tests/e2e
|
|
363
|
-
|
|
364
|
-
|
|
499
|
+
$ ct gen --lang ts --vars '{"email":"admin@example.com"}'
|
|
500
|
+
`).option("--lang <lang>", "Target language (ts|js)", "ts").option("--out <dir>", "Output directory for spec files", "tests/generated").option("--vars <json>", "JSON object of fallback values for generated CT_VAR_* constants").action(async (opts) => {
|
|
501
|
+
await gen({ lang: opts.lang, out: opts.out, vars: opts.vars });
|
|
365
502
|
});
|
|
366
503
|
return cmd;
|
|
367
504
|
}
|
|
@@ -370,4 +507,4 @@ export {
|
|
|
370
507
|
gen,
|
|
371
508
|
genCmd
|
|
372
509
|
};
|
|
373
|
-
//# sourceMappingURL=chunk-
|
|
510
|
+
//# sourceMappingURL=chunk-XM2TYPU7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/gen.ts"],"sourcesContent":["import { Command } from 'commander';\nimport fg from 'fast-glob';\nimport { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, basename, relative, resolve } from 'node:path';\n\ntype NormalizedCase = {\n id?: string;\n title: string;\n tags?: string[];\n steps?: string[];\n step_hints?: Array<{ selector?: string }>;\n expected?: string[];\n assertion_hints?: Array<{ playwright?: string }>;\n needs_review?: boolean;\n review_reasons?: string[];\n source?: string;\n url?: string;\n};\n\ntype VarsOverrideMap = Map<string, string>;\n\ntype StepBinding = {\n kind: 'fill' | 'select';\n rawField?: string;\n fieldLabel?: string;\n value: string;\n};\n\ntype StepVar = {\n envKey: string;\n constName: string;\n fallback: string;\n};\n\nfunction escapeForSingleQuotedString(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nfunction escapeForRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\\\/]/g, '\\\\$&');\n}\n\nfunction normalizeUrlForComparison(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n return new URL(value).toString().replace(/\\/$/, '');\n } catch {\n return value.trim().replace(/\\/$/, '');\n }\n}\n\nfunction urlPatternFromAbsoluteUrl(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n const parsed = new URL(value);\n const normalizedPath = `${parsed.pathname}${parsed.search}`.replace(/\\/$/, '') || '/';\n if (normalizedPath === '/') return '\\\\/';\n return normalizedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/');\n } catch {\n return undefined;\n }\n}\n\nfunction pageNameToUrlPattern(value: string): string | undefined {\n const cleaned = value\n .replace(/\\b(the|a|an|user)\\b/gi, ' ')\n .replace(/\\b(page|screen)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/[\\s/-]+/).map(token => token.trim()).filter(Boolean);\n if (tokens.length === 0) return undefined;\n return tokens.map(token => escapeForRegex(token.toLowerCase())).join('[-_\\\\/]?');\n}\n\nfunction fieldNameFromSentence(value: string): string | undefined {\n const match =\n value.match(/\\bfor\\s+([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+input\\b/i);\n return match?.[1]?.trim();\n}\n\nfunction visibleTextRegexFromPhrase(value: string): string {\n const cleaned = value\n .replace(/[\"']/g, '')\n .replace(/\\b(the|a|an|user|should|must|is|are|be|visible|shown|showing|displayed|present|appears?|rendered)\\b/gi, ' ')\n .replace(/\\b(message|text|content|heading|label)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return '.+';\n return tokens.map(token => escapeForRegex(token)).join('\\\\s+');\n}\n\nfunction sanitizeEnvSegment(value: string): string {\n return value\n .toUpperCase()\n .replace(/[^A-Z0-9]+/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n}\n\nfunction normalizeOverrideKey(value: string): string {\n return sanitizeEnvSegment(value.replace(/^CT_VAR_/i, ''));\n}\n\nfunction cleanFieldLabel(value?: string): string | undefined {\n if (!value) return undefined;\n const cleaned = value\n .replace(/[.?!,:;]+$/g, '')\n .replace(/^(?:the|a|an)\\s+/i, '')\n .replace(/\\s+(?:field|input|box|area|dropdown|combobox|select|menu)\\b/gi, '')\n .trim();\n return cleaned || undefined;\n}\n\nfunction isPositionalFieldReference(value?: string): boolean {\n if (!value) return false;\n const cleaned = value.trim().toLowerCase();\n 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);\n}\n\nfunction fieldNameToEnvSuffix(value?: string): string | undefined {\n if (!value || isPositionalFieldReference(value)) return undefined;\n const cleaned = value\n .replace(/[.?!,:;]+$/g, '')\n .replace(/^(?:the|a|an)\\s+/i, '')\n .replace(/\\b(?:field|input|box|area|dropdown|combobox|select|menu)\\b/gi, ' ')\n .trim();\n const suffix = sanitizeEnvSegment(cleaned);\n if (!suffix) return undefined;\n if (/^(?:FIELD|INPUT|BOX|AREA|DROPDOWN|COMBOBOX|SELECT|MENU)$/.test(suffix)) return undefined;\n return suffix;\n}\n\nfunction parseStepBinding(step: string): StepBinding | undefined {\n const s = step.trim();\n\n const fillMatch = s.match(/\\b(?:fill|input|write)\\s+(?:in\\s+)?(.+?)\\s+with\\s+(['\"])(.*?)\\2/i);\n if (fillMatch) {\n return {\n kind: 'fill',\n rawField: fillMatch[1].trim(),\n fieldLabel: cleanFieldLabel(fillMatch[1]),\n value: fillMatch[3].trim(),\n };\n }\n\n const enterMatch = s.match(/\\b(?:enter|type)\\s+(['\"])(.*?)\\1\\s+(?:in|into)\\s+(.+)/i);\n if (enterMatch) {\n return {\n kind: 'fill',\n rawField: enterMatch[3].trim(),\n fieldLabel: cleanFieldLabel(enterMatch[3]),\n value: enterMatch[2].trim(),\n };\n }\n\n const selectMatch = s.match(/\\b(?:select|choose|pick)\\s+(['\"])(.*?)\\1\\s+(?:from|in|into|on)\\s+(.+)/i);\n if (selectMatch) {\n return {\n kind: 'select',\n rawField: selectMatch[3].trim(),\n fieldLabel: cleanFieldLabel(selectMatch[3]),\n value: selectMatch[2].trim(),\n };\n }\n\n return undefined;\n}\n\nfunction resolveStepVarOverride(\n overrides: VarsOverrideMap,\n envKey: string,\n keySuffix: string,\n rawField?: string,\n fieldLabel?: string,\n): string | undefined {\n const candidates = [envKey, keySuffix, rawField, fieldLabel];\n for (const candidate of candidates) {\n if (!candidate) continue;\n const normalized = normalizeOverrideKey(candidate);\n if (normalized && overrides.has(normalized)) return overrides.get(normalized);\n }\n return undefined;\n}\n\nfunction extractStepVars(\n steps: string[],\n overrides: VarsOverrideMap,\n): { declarations: StepVar[]; byStepIndex: Map<number, StepVar> } {\n const declarations: StepVar[] = [];\n const byStepIndex = new Map<number, StepVar>();\n const seen = new Map<string, StepVar>();\n let unnamedFillIndex = 0;\n let unnamedSelectIndex = 0;\n\n for (const [index, step] of steps.entries()) {\n const binding = parseStepBinding(step);\n if (!binding) continue;\n\n const keySuffix =\n fieldNameToEnvSuffix(binding.rawField) ??\n `${binding.kind === 'select' ? 'SELECT' : 'INPUT'}_${binding.kind === 'select' ? ++unnamedSelectIndex : ++unnamedFillIndex}`;\n const envKey = `CT_VAR_${keySuffix}`;\n\n let stepVar = seen.get(envKey);\n if (!stepVar) {\n stepVar = {\n envKey,\n constName: envKey,\n fallback:\n resolveStepVarOverride(overrides, envKey, keySuffix, binding.rawField, binding.fieldLabel) ??\n binding.value,\n };\n seen.set(envKey, stepVar);\n declarations.push(stepVar);\n }\n\n byStepIndex.set(index, stepVar);\n }\n\n return { declarations, byStepIndex };\n}\n\nfunction buildVariableHeaderComment(stepVars: StepVar[]): string {\n if (stepVars.length === 0) return '';\n const inlineExample = stepVars\n .map(stepVar => `${stepVar.envKey}=${stepVar.fallback}`)\n .join(' ');\n return [\n '// Test variables:',\n '// Set these environment variables before running this spec:',\n '//',\n ...stepVars.map(stepVar => `// ${stepVar.envKey}=your-value`),\n '//',\n '// Or pass them inline:',\n `// ${inlineExample} npx playwright test`,\n '',\n ].join('\\n');\n}\n\nfunction buildVariableDeclarations(stepVars: StepVar[]): string {\n if (stepVars.length === 0) return '';\n return [\n ' // Test variables',\n ...stepVars.map(\n stepVar =>\n ` const ${stepVar.constName} = process.env['${stepVar.envKey}'] ?? '${escapeForSingleQuotedString(stepVar.fallback)}';`,\n ),\n '',\n ].join('\\n');\n}\n\nfunction parseVarsOverride(raw?: string): VarsOverrideMap {\n if (!raw) return new Map();\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n console.error('❌ --vars must be a valid JSON object');\n process.exit(1);\n }\n\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n console.error('❌ --vars must be a valid JSON object');\n process.exit(1);\n }\n\n const overrides = new Map<string, string>();\n for (const [key, value] of Object.entries(parsed)) {\n const normalizedKey = normalizeOverrideKey(key);\n if (!normalizedKey) continue;\n overrides.set(normalizedKey, String(value));\n }\n return overrides;\n}\n\n// ─── Step → Playwright action ─────────────────────────────────────────────────\n\nfunction ensureStatement(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return trimmed;\n return trimmed.endsWith(';') ? trimmed : `${trimmed};`;\n}\n\nfunction stepToPlaywright(\n step: string,\n url?: string,\n hint?: { selector?: string },\n stepVar?: StepVar,\n): string {\n const s = step.trim();\n const hintedSelector = hint?.selector ? `page.${hint.selector}` : undefined;\n const valueExpression = (value: string): string =>\n stepVar?.constName ?? `'${escapeForSingleQuotedString(value)}'`;\n\n // Navigate\n if (/^(navigate|go to|open|visit|load)/i.test(s)) {\n const urlMatch = s.match(/https?:\\/\\/[^\\s'\"]+/) || s.match(/[\"']([^\"']+)[\"']/);\n const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? '/';\n if (\n normalizeUrlForComparison(dest) &&\n normalizeUrlForComparison(dest) === normalizeUrlForComparison(url)\n ) {\n return `// navigation handled by pomPage.goto()`;\n }\n return `await page.goto('${escapeForSingleQuotedString(dest)}');`;\n }\n\n // Fill / type / enter into a field\n // Pattern: \"Fill in <field> with '<value>'\" or \"Enter '<value>' in <field>\"\n const binding = parseStepBinding(s);\n if (binding?.kind === 'fill') {\n if (hintedSelector) return `await ${hintedSelector}.fill(${valueExpression(binding.value)});`;\n const field = binding.fieldLabel ?? 'field';\n return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill(${valueExpression(binding.value)});`;\n }\n\n // Click a button\n if (/\\bclick\\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Click a link\n if (/\\bclick\\b.*(link|anchor|nav)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('link', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Generic click\n if (/^(click|press|tap)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (nameMatch) return `await page.getByText('${escapeForSingleQuotedString(nameMatch[1])}').click();`;\n const target = s.replace(/^(click|press|tap)\\s+(on\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(target)}').click();`;\n }\n\n // Select dropdown\n if (binding?.kind === 'select') {\n if (hintedSelector) return `await ${hintedSelector}.selectOption(${valueExpression(binding.value)});`;\n if (binding.fieldLabel) {\n return `await page.getByLabel('${escapeForSingleQuotedString(binding.fieldLabel)}').selectOption(${valueExpression(binding.value)});`;\n }\n return `await page.getByRole('combobox').selectOption(${valueExpression(binding.value)});`;\n }\n\n // Check / uncheck\n if (/\\b(uncheck|untick|disable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.uncheck();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/uncheck|untick|disable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').uncheck();`;\n }\n if (/\\b(check|tick|enable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.check();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/check|tick|enable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').check();`;\n }\n\n // Keyboard key\n if (/press.*(enter|tab|escape|esc|space|backspace)/i.test(s)) {\n const keyMatch = s.match(/enter|tab|escape|esc|space|backspace/i);\n const key = (keyMatch?.[0] ?? 'Enter');\n return `await page.keyboard.press('${key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}');`;\n }\n\n // Reload\n if (/\\b(reload|refresh)\\b/i.test(s)) {\n return `await page.reload();`;\n }\n\n // Hover\n if (/\\bhover\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.hover();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/hover\\s+(over\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(name)}').hover();`;\n }\n\n // Scroll\n if (/\\bscroll\\b/i.test(s)) {\n return `await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));`;\n }\n\n // Fallback — preserve the step as a comment so the file is still valid\n return `// TODO: map to Playwright action → \"${s}\"`;\n}\n\n// ─── Expected result → assertion ──────────────────────────────────────────────\n\nfunction expectedToAssertion(\n expected: string,\n norm?: NormalizedCase,\n hint?: { playwright?: string },\n): string {\n if (hint?.playwright) return ensureStatement(hint.playwright);\n const s = expected.trim();\n const fieldName = fieldNameFromSentence(s);\n const currentUrlPattern = urlPatternFromAbsoluteUrl(norm?.url);\n\n // Cleared / empty field\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s) && fieldName) {\n return `await expect(page.getByLabel('${escapeForSingleQuotedString(fieldName)}')).toHaveValue('');`;\n }\n\n // Stayed on the same page / did not submit\n if (/\\bform\\b.*\\b(?:does not submit|doesn't submit|not submit|not submitted)\\b/i.test(s)) {\n const fallbackPattern = currentUrlPattern || 'login';\n return `await expect(page).toHaveURL(/${fallbackPattern}/); // form did not navigate`;\n }\n if (/\\b(remains?|stays?|still)\\s+on\\b/i.test(s)) {\n const pageMatch = s.match(/\\b(?:remains?|stays?|still)\\s+on\\s+(?:the\\s+)?(.+?)(?:\\s+page|\\s+screen)?$/i);\n const pagePattern = pageMatch?.[1] ? pageNameToUrlPattern(pageMatch[1]) : undefined;\n return `await expect(page).toHaveURL(/${pagePattern || currentUrlPattern || 'login'}/);`;\n }\n\n // URL / redirect check\n if (/\\b(url|redirect(?:s|ed)?|navigate(?:s|d)?|route|path)\\b/i.test(s)) {\n const pathMatch =\n s.match(/[\"'](\\/[^\"']+)[\"']/) ??\n s.match(/to\\s+(\\/[\\w/-]+)/i) ??\n s.match(/https?:\\/\\/[^\\s'\"]+/i);\n const matchedPath = pathMatch?.[1] ?? pathMatch?.[0];\n if (matchedPath) {\n const fromUrl = matchedPath.startsWith('http') ? urlPatternFromAbsoluteUrl(matchedPath) : undefined;\n const directPath = matchedPath.startsWith('/') ? matchedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/') : undefined;\n const finalPattern = fromUrl ?? directPath ?? currentUrlPattern;\n if (finalPattern !== undefined) return `await expect(page).toHaveURL(/${finalPattern}/);`;\n }\n return `await expect(page).toHaveURL(/dashboard|success|home/);`;\n }\n\n // Page title\n if (/\\bpage title\\b|\\btitle should\\b|\\bdocument title\\b/i.test(s)) {\n const titleMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (titleMatch) return `await expect(page).toHaveTitle('${escapeForSingleQuotedString(titleMatch[1])}');`;\n return `await expect(page).toHaveTitle(/.+/);`;\n }\n\n // Error / validation message\n if (/\\b(error|invalid|fail|incorrect|required|validation)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (fieldName) return `await expect(page.getByText(/${escapeForRegex(fieldName)}/i)).toBeVisible();`;\n return `await expect(page.getByRole('alert')).toBeVisible();`;\n }\n\n // Success / confirmation\n if (/\\b(success|confirm|complete|thank|welcome|sent|saved)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (/\\bwelcome\\b/i.test(s)) return `await expect(page.getByText(/welcome/i)).toBeVisible();`;\n return `await expect(page.getByRole('status')).toBeVisible();`;\n }\n\n // Not visible / hidden\n if (/\\b(not visible|hidden|disappear|removed|gone)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).not.toBeVisible();`;\n return `await expect(page.locator('.modal, [role=\"dialog\"]').first()).not.toBeVisible();`;\n }\n\n // Visible / present\n if (/\\b(visible|appear|display|show|render|present)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).toBeVisible();`;\n const subjectMatch =\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:is|are|should|must|becomes?|appears?|renders?|shows?|displays?)\\b/i) ??\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:visible|present)\\b/i);\n const subject = subjectMatch?.[1]?.trim();\n if (subject) {\n if (/\\bnavigation\\b.*\\bmenu\\b|\\bmenu\\b.*\\bnavigation\\b|\\bnavigation\\b/i.test(subject)) {\n return `await expect(page.getByRole('navigation')).toBeVisible();`;\n }\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(subject)}/i)).toBeVisible();`;\n }\n return `await expect(page.locator('[data-testid]').first()).toBeVisible();`;\n }\n\n // Count of items\n if (/\\b(count|number of|list of|\\d+\\s+item)\\b/i.test(s)) {\n const countMatch = s.match(/(\\d+)/);\n if (countMatch) return `await expect(page.locator('li, tr, [role=\"listitem\"]')).toHaveCount(${countMatch[1]});`;\n return `await expect(page.locator('li, tr').first()).toBeVisible();`;\n }\n\n // Enabled / disabled\n if (/\\b(enabled|clickable|active)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeEnabled();`;\n }\n if (/\\b(disabled|inactive)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeDisabled();`;\n }\n\n // Input value\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s)) {\n return `await expect(page.getByRole('textbox').first()).toHaveValue('');`;\n }\n if (/\\b(field|input|value|filled)\\b/i.test(s)) {\n const valMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (valMatch) return `await expect(page.getByRole('textbox').first()).toHaveValue('${escapeForSingleQuotedString(valMatch[1])}');`;\n return `await expect(page.getByRole('textbox').first()).not.toBeEmpty();`;\n }\n\n // Text / heading / content (broad fallback before final fallback)\n if (/\\b(text|content|label|message|heading)\\b/i.test(s)) {\n const textMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (textMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(textMatch[1])}')).toBeVisible();`;\n const words = s.replace(/\\b(the|should|must|contain|display|show|have|text|content)\\b/gi, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(words)}/i)).toBeVisible();`;\n }\n\n // Final fallback\n const cleaned = s.replace(/[\"']/g, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(cleaned)}/i)).toBeVisible(); // TODO: refine assertion`;\n}\n\n// ─── POM class name / filename ────────────────────────────────────────────────\n\nfunction toIdentifier(value: string, fallback = 'Landing'): string {\n const tokens = value\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .split(/[^a-zA-Z0-9_]+/)\n .filter(Boolean);\n\n const identifier = tokens\n .map((token) => token.charAt(0).toUpperCase() + token.slice(1))\n .join('');\n\n if (!identifier) return fallback;\n return /^[0-9]/.test(identifier) ? `Case${identifier}` : identifier;\n}\n\nfunction derivePomClassName(norm: NormalizedCase): string {\n const raw = norm.id ?? norm.title ?? 'Landing';\n const withoutNum = raw.replace(/-\\d+$/, '');\n return `${toIdentifier(withoutNum)}Page`;\n}\n\n// ─── POM class source ─────────────────────────────────────────────────────────\n\nfunction buildPomClass(className: string, norm: NormalizedCase, lang: 'ts' | 'js'): string {\n const pageUrl = norm.url ?? '/';\n const isTs = lang === 'ts';\n\n return isTs\n ? `import { Page } from '@playwright/test';\n\nexport class ${className} {\n readonly page: Page;\n\n constructor(page: Page) {\n this.page = page;\n }\n\n async goto(): Promise<void> {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad(): Promise<void> {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`\n : `// @ts-check\nexport class ${className} {\n /** @param {import('@playwright/test').Page} page */\n constructor(page) {\n /** @type {import('@playwright/test').Page} */\n this.page = page;\n }\n\n async goto() {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad() {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`;\n}\n\n// ─── Spec file source ─────────────────────────────────────────────────────────\n\nfunction buildTestTitle(norm: NormalizedCase): string {\n const idPart = norm.id ?? '';\n\n // norm.title may already include the ID prefix (e.g. \"AUTH-001 — User can log in\")\n // Strip it to avoid \"AUTH-001 — AUTH-001 — User can log in\" in the test name.\n let cleanTitle = norm.title || 'Untitled';\n if (idPart && cleanTitle.startsWith(idPart)) {\n cleanTitle = cleanTitle.slice(idPart.length).replace(/^\\s*[—\\-–]+\\s*/, '').trim();\n }\n\n const tagSuffix = (norm.tags ?? []).map(t => `@${t}`).join(' ');\n return [idPart, cleanTitle, tagSuffix].filter(Boolean).join(' — ').trim();\n}\n\nfunction buildSpecFile(\n norm: NormalizedCase,\n pomClassName: string,\n pomImportPath: string,\n overrides: VarsOverrideMap,\n): string {\n const title = buildTestTitle(norm);\n const steps = norm.steps ?? [];\n const expected = norm.expected ?? [];\n const stepVars = extractStepVars(steps, overrides);\n\n let importPath = pomImportPath.replace(/\\\\/g, '/');\n if (!importPath.startsWith('.')) importPath = `./${importPath}`;\n // Strip .ts extension — TypeScript resolves without it\n importPath = importPath.replace(/\\.ts$/, '');\n\n const stepLines = steps.length\n ? steps\n .map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index], stepVars.byStepIndex.get(index))}`)\n .join('\\n')\n : ' // TODO: add steps';\n\n const assertionLines = expected.length\n ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join('\\n')\n : ' // TODO: add assertions';\n\n const reviewNote = norm.needs_review\n ? `\\n // ⚠️ Flagged for review — steps or assertions may need manual refinement\\n`\n : '';\n const variableHeader = buildVariableHeaderComment(stepVars.declarations);\n const variableDeclarations = buildVariableDeclarations(stepVars.declarations);\n\n return `import { test, expect } from '@playwright/test';\nimport { ${pomClassName} } from '${importPath}';\n\n${variableHeader}test('${title}', async ({ page }) => {${reviewNote}\n const pomPage = new ${pomClassName}(page);\n\n${variableDeclarations} // ── Setup ─────────────────────────────────────────────────────────────────\n await pomPage.goto();\n await pomPage.waitForLoad();\n\n // ── Steps ─────────────────────────────────────────────────────────────────\n${stepLines}\n\n // ── Assertions ────────────────────────────────────────────────────────────\n${assertionLines}\n});\n`;\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport async function gen(opts: { lang: string; out: string; vars?: string }) {\n if (opts.lang !== 'ts' && opts.lang !== 'js') {\n console.error('❌ --lang must be ts or js');\n process.exit(1);\n }\n const lang = opts.lang as 'ts' | 'js';\n const specExt = lang === 'js' ? 'spec.js' : 'spec.ts';\n const pomExt = lang === 'js' ? '.js' : '.ts';\n const varsOverride = parseVarsOverride(opts.vars);\n\n const normalized = await fg([\n '.cementic/normalized/*.json',\n '!.cementic/normalized/_index.json',\n ]);\n\n if (normalized.length === 0) {\n console.warn('⚠️ No normalized cases found in .cementic/normalized/');\n console.warn(' Run: ct normalize ./cases');\n process.exit(1);\n }\n\n const projectRoot = process.cwd();\n const testsOutDir = resolve(projectRoot, opts.out);\n const pagesOutDir = resolve(projectRoot, 'pages');\n\n mkdirSync(testsOutDir, { recursive: true });\n mkdirSync(pagesOutDir, { recursive: true });\n\n let specCount = 0;\n let pomCount = 0;\n\n for (const f of normalized) {\n const norm = JSON.parse(readFileSync(f, 'utf8')) as NormalizedCase;\n\n const pomClassName = derivePomClassName(norm);\n const pomFileName = pomClassName + pomExt;\n const pomFilePath = join(pagesOutDir, pomFileName);\n\n // Only write a POM file if one doesn't exist yet — never overwrite user edits\n if (!existsSync(pomFilePath)) {\n writeFileSync(pomFilePath, buildPomClass(pomClassName, norm, lang));\n pomCount++;\n }\n\n // Relative import from the test file location to the POM file\n const relToPages = relative(testsOutDir, pagesOutDir);\n const pomImportPath = join(relToPages, pomFileName);\n\n const stem = basename(f).replace(/\\.json$/, '').replace(/[^\\w-]+/g, '-');\n const specPath = join(testsOutDir, `${stem}.${specExt}`);\n writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath, varsOverride));\n specCount++;\n }\n\n console.log(`✅ Generated ${specCount} spec file(s) → ${opts.out}/`);\n if (pomCount > 0) {\n console.log(`✅ Generated ${pomCount} POM class file(s) → pages/`);\n }\n if (pomCount === 0 && specCount > 0) {\n console.log(`ℹ️ POM classes already exist — skipped regeneration`);\n }\n}\n\nexport function genCmd() {\n const cmd = new Command('gen')\n .description('Generate Playwright POM spec + page-object files from normalized cases')\n .addHelpText('after', `\nExamples:\n $ ct gen --lang ts\n $ ct gen --lang js --out tests/e2e\n $ ct gen --lang ts --vars '{\"email\":\"admin@example.com\"}'\n`)\n .option('--lang <lang>', 'Target language (ts|js)', 'ts')\n .option('--out <dir>', 'Output directory for spec files', 'tests/generated')\n .option('--vars <json>', 'JSON object of fallback values for generated CT_VAR_* constants')\n .action(async opts => {\n await gen({ lang: opts.lang, out: opts.out, vars: opts.vars });\n });\n return cmd;\n}\n"],"mappings":";;;AAAA,SAAS,eAAe;AACxB,OAAO,QAAQ;AACf,SAAS,cAAc,WAAW,eAAe,kBAAkB;AACnE,SAAS,MAAM,UAAU,UAAU,eAAe;AA+BlD,SAAS,4BAA4B,OAAuB;AAC1D,SAAO,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACzD;AAEA,SAAS,eAAe,OAAuB;AAC7C,SAAO,MAAM,QAAQ,yBAAyB,MAAM;AACtD;AAEA,SAAS,0BAA0B,OAAoC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,OAAO,EAAE;AAAA,EACpD,QAAQ;AACN,WAAO,MAAM,KAAK,EAAE,QAAQ,OAAO,EAAE;AAAA,EACvC;AACF;AAEA,SAAS,0BAA0B,OAAoC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAM,iBAAiB,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM,GAAG,QAAQ,OAAO,EAAE,KAAK;AAClF,QAAI,mBAAmB,IAAK,QAAO;AACnC,WAAO,eAAe,QAAQ,QAAQ,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,KAAK,KAAK;AAAA,EACrF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,OAAmC;AAC/D,QAAM,UAAU,MACb,QAAQ,yBAAyB,GAAG,EACpC,QAAQ,uBAAuB,GAAG,EAClC,KAAK;AACR,QAAM,SAAS,QAAQ,MAAM,SAAS,EAAE,IAAI,WAAS,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO;AACjF,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,IAAI,WAAS,eAAe,MAAM,YAAY,CAAC,CAAC,EAAE,KAAK,UAAU;AACjF;AAEA,SAAS,sBAAsB,OAAmC;AAChE,QAAM,QACJ,MAAM,MAAM,iDAAiD,KAC7D,MAAM,MAAM,2CAA2C,KACvD,MAAM,MAAM,2CAA2C;AACzD,SAAO,QAAQ,CAAC,GAAG,KAAK;AAC1B;AAEA,SAAS,2BAA2B,OAAuB;AACzD,QAAM,UAAU,MACb,QAAQ,SAAS,EAAE,EACnB,QAAQ,yGAAyG,GAAG,EACpH,QAAQ,8CAA8C,GAAG,EACzD,KAAK;AACR,QAAM,SAAS,QAAQ,MAAM,KAAK,EAAE,OAAO,OAAO;AAClD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,IAAI,WAAS,eAAe,KAAK,CAAC,EAAE,KAAK,MAAM;AAC/D;AAEA,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MACJ,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACzB;AAEA,SAAS,qBAAqB,OAAuB;AACnD,SAAO,mBAAmB,MAAM,QAAQ,aAAa,EAAE,CAAC;AAC1D;AAEA,SAAS,gBAAgB,OAAoC;AAC3D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MACb,QAAQ,eAAe,EAAE,EACzB,QAAQ,qBAAqB,EAAE,EAC/B,QAAQ,iEAAiE,EAAE,EAC3E,KAAK;AACR,SAAO,WAAW;AACpB;AAEA,SAAS,2BAA2B,OAAyB;AAC3D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,KAAK,EAAE,YAAY;AACzC,SAAO,yLAAyL,KAAK,OAAO;AAC9M;AAEA,SAAS,qBAAqB,OAAoC;AAChE,MAAI,CAAC,SAAS,2BAA2B,KAAK,EAAG,QAAO;AACxD,QAAM,UAAU,MACb,QAAQ,eAAe,EAAE,EACzB,QAAQ,qBAAqB,EAAE,EAC/B,QAAQ,gEAAgE,GAAG,EAC3E,KAAK;AACR,QAAM,SAAS,mBAAmB,OAAO;AACzC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,2DAA2D,KAAK,MAAM,EAAG,QAAO;AACpF,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAuC;AAC/D,QAAM,IAAI,KAAK,KAAK;AAEpB,QAAM,YAAY,EAAE,MAAM,kEAAkE;AAC5F,MAAI,WAAW;AACb,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,UAAU,CAAC,EAAE,KAAK;AAAA,MAC5B,YAAY,gBAAgB,UAAU,CAAC,CAAC;AAAA,MACxC,OAAO,UAAU,CAAC,EAAE,KAAK;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,aAAa,EAAE,MAAM,wDAAwD;AACnF,MAAI,YAAY;AACd,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,WAAW,CAAC,EAAE,KAAK;AAAA,MAC7B,YAAY,gBAAgB,WAAW,CAAC,CAAC;AAAA,MACzC,OAAO,WAAW,CAAC,EAAE,KAAK;AAAA,IAC5B;AAAA,EACF;AAEA,QAAM,cAAc,EAAE,MAAM,wEAAwE;AACpG,MAAI,aAAa;AACf,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,YAAY,CAAC,EAAE,KAAK;AAAA,MAC9B,YAAY,gBAAgB,YAAY,CAAC,CAAC;AAAA,MAC1C,OAAO,YAAY,CAAC,EAAE,KAAK;AAAA,IAC7B;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,uBACP,WACA,QACA,WACA,UACA,YACoB;AACpB,QAAM,aAAa,CAAC,QAAQ,WAAW,UAAU,UAAU;AAC3D,aAAW,aAAa,YAAY;AAClC,QAAI,CAAC,UAAW;AAChB,UAAM,aAAa,qBAAqB,SAAS;AACjD,QAAI,cAAc,UAAU,IAAI,UAAU,EAAG,QAAO,UAAU,IAAI,UAAU;AAAA,EAC9E;AACA,SAAO;AACT;AAEA,SAAS,gBACP,OACA,WACgE;AAChE,QAAM,eAA0B,CAAC;AACjC,QAAM,cAAc,oBAAI,IAAqB;AAC7C,QAAM,OAAO,oBAAI,IAAqB;AACtC,MAAI,mBAAmB;AACvB,MAAI,qBAAqB;AAEzB,aAAW,CAAC,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG;AAC3C,UAAM,UAAU,iBAAiB,IAAI;AACrC,QAAI,CAAC,QAAS;AAEd,UAAM,YACJ,qBAAqB,QAAQ,QAAQ,KACrC,GAAG,QAAQ,SAAS,WAAW,WAAW,OAAO,IAAI,QAAQ,SAAS,WAAW,EAAE,qBAAqB,EAAE,gBAAgB;AAC5H,UAAM,SAAS,UAAU,SAAS;AAElC,QAAI,UAAU,KAAK,IAAI,MAAM;AAC7B,QAAI,CAAC,SAAS;AACZ,gBAAU;AAAA,QACR;AAAA,QACA,WAAW;AAAA,QACX,UACE,uBAAuB,WAAW,QAAQ,WAAW,QAAQ,UAAU,QAAQ,UAAU,KACzF,QAAQ;AAAA,MACZ;AACA,WAAK,IAAI,QAAQ,OAAO;AACxB,mBAAa,KAAK,OAAO;AAAA,IAC3B;AAEA,gBAAY,IAAI,OAAO,OAAO;AAAA,EAChC;AAEA,SAAO,EAAE,cAAc,YAAY;AACrC;AAEA,SAAS,2BAA2B,UAA6B;AAC/D,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,gBAAgB,SACnB,IAAI,aAAW,GAAG,QAAQ,MAAM,IAAI,QAAQ,QAAQ,EAAE,EACtD,KAAK,GAAG;AACX,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG,SAAS,IAAI,aAAW,QAAQ,QAAQ,MAAM,aAAa;AAAA,IAC9D;AAAA,IACA;AAAA,IACA,QAAQ,aAAa;AAAA,IACrB;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEA,SAAS,0BAA0B,UAA6B;AAC9D,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,SAAO;AAAA,IACL;AAAA,IACA,GAAG,SAAS;AAAA,MACV,aACE,WAAW,QAAQ,SAAS,mBAAmB,QAAQ,MAAM,UAAU,4BAA4B,QAAQ,QAAQ,CAAC;AAAA,IACxH;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEA,SAAS,kBAAkB,KAA+B;AACxD,MAAI,CAAC,IAAK,QAAO,oBAAI,IAAI;AAEzB,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,QAAQ;AACN,YAAQ,MAAM,2CAAsC;AACpD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAClE,YAAQ,MAAM,2CAAsC;AACpD,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,YAAY,oBAAI,IAAoB;AAC1C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,gBAAgB,qBAAqB,GAAG;AAC9C,QAAI,CAAC,cAAe;AACpB,cAAU,IAAI,eAAe,OAAO,KAAK,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;AAIA,SAAS,gBAAgB,OAAuB;AAC9C,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,GAAG,IAAI,UAAU,GAAG,OAAO;AACrD;AAEA,SAAS,iBACP,MACA,KACA,MACA,SACQ;AACR,QAAM,IAAI,KAAK,KAAK;AACpB,QAAM,iBAAiB,MAAM,WAAW,QAAQ,KAAK,QAAQ,KAAK;AAClE,QAAM,kBAAkB,CAAC,UACvB,SAAS,aAAa,IAAI,4BAA4B,KAAK,CAAC;AAG9D,MAAI,qCAAqC,KAAK,CAAC,GAAG;AAChD,UAAM,WAAW,EAAE,MAAM,qBAAqB,KAAK,EAAE,MAAM,kBAAkB;AAC7E,UAAM,OAAO,WAAW,CAAC,KAAK,WAAW,CAAC,KAAK,OAAO;AACtD,QACE,0BAA0B,IAAI,KAC9B,0BAA0B,IAAI,MAAM,0BAA0B,GAAG,GACjE;AACA,aAAO;AAAA,IACT;AACA,WAAO,oBAAoB,4BAA4B,IAAI,CAAC;AAAA,EAC9D;AAIA,QAAM,UAAU,iBAAiB,CAAC;AAClC,MAAI,SAAS,SAAS,QAAQ;AAC5B,QAAI,eAAgB,QAAO,SAAS,cAAc,SAAS,gBAAgB,QAAQ,KAAK,CAAC;AACzF,UAAM,QAAQ,QAAQ,cAAc;AACpC,WAAO,0BAA0B,4BAA4B,KAAK,CAAC,WAAW,gBAAgB,QAAQ,KAAK,CAAC;AAAA,EAC9G;AAGA,MAAI,2FAA2F,KAAK,CAAC,GAAG;AACtG,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,sBAAsB,EAAE,EAAE,KAAK;AACxE,WAAO,2CAA2C,4BAA4B,IAAI,CAAC;AAAA,EACrF;AAGA,MAAI,gCAAgC,KAAK,CAAC,GAAG;AAC3C,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,sBAAsB,EAAE,EAAE,KAAK;AACxE,WAAO,yCAAyC,4BAA4B,IAAI,CAAC;AAAA,EACnF;AAGA,MAAI,wBAAwB,KAAK,CAAC,GAAG;AACnC,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,QAAI,UAAW,QAAO,yBAAyB,4BAA4B,UAAU,CAAC,CAAC,CAAC;AACxF,UAAM,SAAS,EAAE,QAAQ,kCAAkC,EAAE,EAAE,KAAK;AACpE,WAAO,yBAAyB,4BAA4B,MAAM,CAAC;AAAA,EACrE;AAGA,MAAI,SAAS,SAAS,UAAU;AAC9B,QAAI,eAAgB,QAAO,SAAS,cAAc,iBAAiB,gBAAgB,QAAQ,KAAK,CAAC;AACjG,QAAI,QAAQ,YAAY;AACtB,aAAO,0BAA0B,4BAA4B,QAAQ,UAAU,CAAC,mBAAmB,gBAAgB,QAAQ,KAAK,CAAC;AAAA,IACnI;AACA,WAAO,iDAAiD,gBAAgB,QAAQ,KAAK,CAAC;AAAA,EACxF;AAGA,MAAI,gCAAgC,KAAK,CAAC,GAAG;AAC3C,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,4BAA4B,EAAE,EAAE,KAAK;AAC9E,WAAO,0BAA0B,4BAA4B,IAAI,CAAC;AAAA,EACpE;AACA,MAAI,2BAA2B,KAAK,CAAC,GAAG;AACtC,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,uBAAuB,EAAE,EAAE,KAAK;AACzE,WAAO,0BAA0B,4BAA4B,IAAI,CAAC;AAAA,EACpE;AAGA,MAAI,iDAAiD,KAAK,CAAC,GAAG;AAC5D,UAAM,WAAW,EAAE,MAAM,uCAAuC;AAChE,UAAM,MAAO,WAAW,CAAC,KAAK;AAC9B,WAAO,8BAA8B,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC,EAAE,YAAY,CAAC;AAAA,EAC/F;AAGA,MAAI,wBAAwB,KAAK,CAAC,GAAG;AACnC,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,KAAK,CAAC,GAAG;AACxB,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,uBAAuB,EAAE,EAAE,KAAK;AACzE,WAAO,yBAAyB,4BAA4B,IAAI,CAAC;AAAA,EACnE;AAGA,MAAI,cAAc,KAAK,CAAC,GAAG;AACzB,WAAO;AAAA,EACT;AAGA,SAAO,6CAAwC,CAAC;AAClD;AAIA,SAAS,oBACP,UACA,MACA,MACQ;AACR,MAAI,MAAM,WAAY,QAAO,gBAAgB,KAAK,UAAU;AAC5D,QAAM,IAAI,SAAS,KAAK;AACxB,QAAM,YAAY,sBAAsB,CAAC;AACzC,QAAM,oBAAoB,0BAA0B,MAAM,GAAG;AAG7D,MAAI,mCAAmC,KAAK,CAAC,KAAK,2BAA2B,KAAK,CAAC,KAAK,WAAW;AACjG,WAAO,iCAAiC,4BAA4B,SAAS,CAAC;AAAA,EAChF;AAGA,MAAI,6EAA6E,KAAK,CAAC,GAAG;AACxF,UAAM,kBAAkB,qBAAqB;AAC7C,WAAO,iCAAiC,eAAe;AAAA,EACzD;AACA,MAAI,oCAAoC,KAAK,CAAC,GAAG;AAC/C,UAAM,YAAY,EAAE,MAAM,6EAA6E;AACvG,UAAM,cAAc,YAAY,CAAC,IAAI,qBAAqB,UAAU,CAAC,CAAC,IAAI;AAC1E,WAAO,iCAAiC,eAAe,qBAAqB,OAAO;AAAA,EACrF;AAGA,MAAI,2DAA2D,KAAK,CAAC,GAAG;AACtE,UAAM,YACJ,EAAE,MAAM,oBAAoB,KAC5B,EAAE,MAAM,mBAAmB,KAC3B,EAAE,MAAM,sBAAsB;AAChC,UAAM,cAAc,YAAY,CAAC,KAAK,YAAY,CAAC;AACnD,QAAI,aAAa;AACf,YAAM,UAAU,YAAY,WAAW,MAAM,IAAI,0BAA0B,WAAW,IAAI;AAC1F,YAAM,aAAa,YAAY,WAAW,GAAG,IAAI,YAAY,QAAQ,QAAQ,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,KAAK,KAAK,IAAI;AAC9H,YAAM,eAAe,WAAW,cAAc;AAC9C,UAAI,iBAAiB,OAAW,QAAO,iCAAiC,YAAY;AAAA,IACtF;AACA,WAAO;AAAA,EACT;AAGA,MAAI,sDAAsD,KAAK,CAAC,GAAG;AACjE,UAAM,aAAa,EAAE,MAAM,kBAAkB;AAC7C,QAAI,WAAY,QAAO,mCAAmC,4BAA4B,WAAW,CAAC,CAAC,CAAC;AACpG,WAAO;AAAA,EACT;AAGA,MAAI,0DAA0D,KAAK,CAAC,GAAG;AACrE,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gCAAgC,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7F,QAAI,UAAW,QAAO,gCAAgC,eAAe,SAAS,CAAC;AAC/E,WAAO;AAAA,EACT;AAGA,MAAI,2DAA2D,KAAK,CAAC,GAAG;AACtE,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gCAAgC,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7F,QAAI,eAAe,KAAK,CAAC,EAAG,QAAO;AACnC,WAAO;AAAA,EACT;AAGA,MAAI,mDAAmD,KAAK,CAAC,GAAG;AAC9D,UAAM,eAAe,EAAE,MAAM,kBAAkB;AAC/C,QAAI,aAAc,QAAO,gCAAgC,4BAA4B,aAAa,CAAC,CAAC,CAAC;AACrG,WAAO;AAAA,EACT;AAGA,MAAI,oDAAoD,KAAK,CAAC,GAAG;AAC/D,UAAM,eAAe,EAAE,MAAM,kBAAkB;AAC/C,QAAI,aAAc,QAAO,gCAAgC,4BAA4B,aAAa,CAAC,CAAC,CAAC;AACrG,UAAM,eACJ,EAAE,MAAM,2FAA2F,KACnG,EAAE,MAAM,4CAA4C;AACtD,UAAM,UAAU,eAAe,CAAC,GAAG,KAAK;AACxC,QAAI,SAAS;AACX,UAAI,oEAAoE,KAAK,OAAO,GAAG;AACrF,eAAO;AAAA,MACT;AACA,aAAO,gCAAgC,2BAA2B,OAAO,CAAC;AAAA,IAC5E;AACA,WAAO;AAAA,EACT;AAGA,MAAI,4CAA4C,KAAK,CAAC,GAAG;AACvD,UAAM,aAAa,EAAE,MAAM,OAAO;AAClC,QAAI,WAAY,QAAO,uEAAuE,WAAW,CAAC,CAAC;AAC3G,WAAO;AAAA,EACT;AAGA,MAAI,kCAAkC,KAAK,CAAC,GAAG;AAC7C,WAAO;AAAA,EACT;AACA,MAAI,2BAA2B,KAAK,CAAC,GAAG;AACtC,WAAO;AAAA,EACT;AAGA,MAAI,mCAAmC,KAAK,CAAC,KAAK,2BAA2B,KAAK,CAAC,GAAG;AACpF,WAAO;AAAA,EACT;AACA,MAAI,kCAAkC,KAAK,CAAC,GAAG;AAC7C,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gEAAgE,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7H,WAAO;AAAA,EACT;AAGA,MAAI,4CAA4C,KAAK,CAAC,GAAG;AACvD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,QAAI,UAAW,QAAO,gCAAgC,4BAA4B,UAAU,CAAC,CAAC,CAAC;AAC/F,UAAM,QAAQ,EAAE,QAAQ,kEAAkE,EAAE,EAAE,KAAK;AACnG,WAAO,gCAAgC,2BAA2B,KAAK,CAAC;AAAA,EAC1E;AAGA,QAAM,UAAU,EAAE,QAAQ,SAAS,EAAE,EAAE,KAAK;AAC5C,SAAO,gCAAgC,2BAA2B,OAAO,CAAC;AAC5E;AAIA,SAAS,aAAa,OAAe,WAAW,WAAmB;AACjE,QAAM,SAAS,MACZ,UAAU,MAAM,EAChB,QAAQ,oBAAoB,EAAE,EAC9B,MAAM,gBAAgB,EACtB,OAAO,OAAO;AAEjB,QAAM,aAAa,OAChB,IAAI,CAAC,UAAU,MAAM,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,MAAM,CAAC,CAAC,EAC7D,KAAK,EAAE;AAEV,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,SAAS,KAAK,UAAU,IAAI,OAAO,UAAU,KAAK;AAC3D;AAEA,SAAS,mBAAmB,MAA8B;AACxD,QAAM,MAAM,KAAK,MAAM,KAAK,SAAS;AACrC,QAAM,aAAa,IAAI,QAAQ,SAAS,EAAE;AAC1C,SAAO,GAAG,aAAa,UAAU,CAAC;AACpC;AAIA,SAAS,cAAc,WAAmB,MAAsB,MAA2B;AACzF,QAAM,UAAU,KAAK,OAAO;AAC5B,QAAM,OAAO,SAAS;AAEtB,SAAO,OACH;AAAA;AAAA,eAES,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAQI,4BAA4B,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQ1D;AAAA,eACS,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAQI,4BAA4B,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQhE;AAIA,SAAS,eAAe,MAA8B;AACpD,QAAM,SAAS,KAAK,MAAM;AAI1B,MAAI,aAAa,KAAK,SAAS;AAC/B,MAAI,UAAU,WAAW,WAAW,MAAM,GAAG;AAC3C,iBAAa,WAAW,MAAM,OAAO,MAAM,EAAE,QAAQ,kBAAkB,EAAE,EAAE,KAAK;AAAA,EAClF;AAEA,QAAM,aAAa,KAAK,QAAQ,CAAC,GAAG,IAAI,OAAK,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG;AAC9D,SAAO,CAAC,QAAQ,YAAY,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,UAAK,EAAE,KAAK;AAC1E;AAEA,SAAS,cACP,MACA,cACA,eACA,WACQ;AACR,QAAM,QAAQ,eAAe,IAAI;AACjC,QAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,QAAM,WAAW,KAAK,YAAY,CAAC;AACnC,QAAM,WAAW,gBAAgB,OAAO,SAAS;AAEjD,MAAI,aAAa,cAAc,QAAQ,OAAO,GAAG;AACjD,MAAI,CAAC,WAAW,WAAW,GAAG,EAAG,cAAa,KAAK,UAAU;AAE7D,eAAa,WAAW,QAAQ,SAAS,EAAE;AAE3C,QAAM,YAAY,MAAM,SACpB,MACG,IAAI,CAAC,GAAG,UAAU,KAAK,iBAAiB,GAAG,KAAK,KAAK,KAAK,aAAa,KAAK,GAAG,SAAS,YAAY,IAAI,KAAK,CAAC,CAAC,EAAE,EACjH,KAAK,IAAI,IACZ;AAEJ,QAAM,iBAAiB,SAAS,SAC5B,SAAS,IAAI,CAAC,GAAG,UAAU,KAAK,oBAAoB,GAAG,MAAM,KAAK,kBAAkB,KAAK,CAAC,CAAC,EAAE,EAAE,KAAK,IAAI,IACxG;AAEJ,QAAM,aAAa,KAAK,eACpB;AAAA;AAAA,IACA;AACJ,QAAM,iBAAiB,2BAA2B,SAAS,YAAY;AACvE,QAAM,uBAAuB,0BAA0B,SAAS,YAAY;AAE5E,SAAO;AAAA,WACE,YAAY,YAAY,UAAU;AAAA;AAAA,EAE3C,cAAc,SAAS,KAAK,2BAA2B,UAAU;AAAA,wBAC3C,YAAY;AAAA;AAAA,EAElC,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpB,SAAS;AAAA;AAAA;AAAA,EAGT,cAAc;AAAA;AAAA;AAGhB;AAIA,eAAsB,IAAI,MAAoD;AAC5E,MAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAQ,MAAM,gCAA2B;AACzC,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,OAAO,KAAK;AAClB,QAAM,UAAU,SAAS,OAAO,YAAY;AAC5C,QAAM,SAAU,SAAS,OAAO,QAAQ;AACxC,QAAM,eAAe,kBAAkB,KAAK,IAAI;AAEhD,QAAM,aAAa,MAAM,GAAG;AAAA,IAC1B;AAAA,IACA;AAAA,EACF,CAAC;AAED,MAAI,WAAW,WAAW,GAAG;AAC3B,YAAQ,KAAK,kEAAwD;AACrE,YAAQ,KAAK,+BAA+B;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,cAAc,QAAQ,IAAI;AAChC,QAAM,cAAc,QAAQ,aAAa,KAAK,GAAG;AACjD,QAAM,cAAc,QAAQ,aAAa,OAAO;AAEhD,YAAU,aAAa,EAAE,WAAW,KAAK,CAAC;AAC1C,YAAU,aAAa,EAAE,WAAW,KAAK,CAAC;AAE1C,MAAI,YAAY;AAChB,MAAI,WAAY;AAEhB,aAAW,KAAK,YAAY;AAC1B,UAAM,OAAO,KAAK,MAAM,aAAa,GAAG,MAAM,CAAC;AAE/C,UAAM,eAAe,mBAAmB,IAAI;AAC5C,UAAM,cAAe,eAAe;AACpC,UAAM,cAAe,KAAK,aAAa,WAAW;AAGlD,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,oBAAc,aAAa,cAAc,cAAc,MAAM,IAAI,CAAC;AAClE;AAAA,IACF;AAGA,UAAM,aAAe,SAAS,aAAa,WAAW;AACtD,UAAM,gBAAgB,KAAK,YAAY,WAAW;AAElD,UAAM,OAAW,SAAS,CAAC,EAAE,QAAQ,WAAW,EAAE,EAAE,QAAQ,YAAY,GAAG;AAC3E,UAAM,WAAW,KAAK,aAAa,GAAG,IAAI,IAAI,OAAO,EAAE;AACvD,kBAAc,UAAU,cAAc,MAAM,cAAc,eAAe,YAAY,CAAC;AACtF;AAAA,EACF;AAEA,UAAQ,IAAI,oBAAe,SAAS,yBAAoB,KAAK,GAAG,GAAG;AACnE,MAAI,WAAW,GAAG;AAChB,YAAQ,IAAI,oBAAe,QAAQ,kCAA6B;AAAA,EAClE;AACA,MAAI,aAAa,KAAK,YAAY,GAAG;AACnC,YAAQ,IAAI,qEAAsD;AAAA,EACpE;AACF;AAEO,SAAS,SAAS;AACvB,QAAM,MAAM,IAAI,QAAQ,KAAK,EAC1B,YAAY,wEAAwE,EACpF,YAAY,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,CAKzB,EACI,OAAO,iBAAiB,2BAA2B,IAAI,EACvD,OAAO,eAAgB,mCAAmC,iBAAiB,EAC3E,OAAO,iBAAiB,iEAAiE,EACzF,OAAO,OAAM,SAAQ;AACpB,UAAM,IAAI,EAAE,MAAM,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,KAAK,KAAK,CAAC;AAAA,EAC/D,CAAC;AACH,SAAO;AACT;","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
genCmd
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XM2TYPU7.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command as Command9 } from "commander";
|
|
@@ -321,7 +321,7 @@ Examples:
|
|
|
321
321
|
}
|
|
322
322
|
console.log(`\u2705 Normalized ${index.summary.parsed} case(s). Output \u2192 .cementic/normalized/`);
|
|
323
323
|
if (opts.andGen) {
|
|
324
|
-
const { gen } = await import("./gen-
|
|
324
|
+
const { gen } = await import("./gen-EST5UZNZ.js");
|
|
325
325
|
await gen({ lang: opts.lang || "ts", out: "tests/generated" });
|
|
326
326
|
}
|
|
327
327
|
});
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/gen.ts"],"sourcesContent":["import { Command } from 'commander';\nimport fg from 'fast-glob';\nimport { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, basename, relative, resolve } from 'node:path';\n\ntype NormalizedCase = {\n id?: string;\n title: string;\n tags?: string[];\n steps?: string[];\n step_hints?: Array<{ selector?: string }>;\n expected?: string[];\n assertion_hints?: Array<{ playwright?: string }>;\n needs_review?: boolean;\n review_reasons?: string[];\n source?: string;\n url?: string;\n};\n\nfunction escapeForSingleQuotedString(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nfunction escapeForRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\\\/]/g, '\\\\$&');\n}\n\nfunction normalizeUrlForComparison(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n return new URL(value).toString().replace(/\\/$/, '');\n } catch {\n return value.trim().replace(/\\/$/, '');\n }\n}\n\nfunction urlPatternFromAbsoluteUrl(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n const parsed = new URL(value);\n const normalizedPath = `${parsed.pathname}${parsed.search}`.replace(/\\/$/, '') || '/';\n if (normalizedPath === '/') return '\\\\/';\n return normalizedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/');\n } catch {\n return undefined;\n }\n}\n\nfunction pageNameToUrlPattern(value: string): string | undefined {\n const cleaned = value\n .replace(/\\b(the|a|an|user)\\b/gi, ' ')\n .replace(/\\b(page|screen)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/[\\s/-]+/).map(token => token.trim()).filter(Boolean);\n if (tokens.length === 0) return undefined;\n return tokens.map(token => escapeForRegex(token.toLowerCase())).join('[-_\\\\/]?');\n}\n\nfunction fieldNameFromSentence(value: string): string | undefined {\n const match =\n value.match(/\\bfor\\s+([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+input\\b/i);\n return match?.[1]?.trim();\n}\n\nfunction visibleTextRegexFromPhrase(value: string): string {\n const cleaned = value\n .replace(/[\"']/g, '')\n .replace(/\\b(the|a|an|user|should|must|is|are|be|visible|shown|showing|displayed|present|appears?|rendered)\\b/gi, ' ')\n .replace(/\\b(message|text|content|heading|label)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return '.+';\n return tokens.map(token => escapeForRegex(token)).join('\\\\s+');\n}\n\n// ─── Step → Playwright action ─────────────────────────────────────────────────\n\nfunction ensureStatement(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return trimmed;\n return trimmed.endsWith(';') ? trimmed : `${trimmed};`;\n}\n\nfunction stepToPlaywright(step: string, url?: string, hint?: { selector?: string }): string {\n const s = step.trim();\n const hintedSelector = hint?.selector ? `page.${hint.selector}` : undefined;\n\n // Navigate\n if (/^(navigate|go to|open|visit|load)/i.test(s)) {\n const urlMatch = s.match(/https?:\\/\\/[^\\s'\"]+/) || s.match(/[\"']([^\"']+)[\"']/);\n const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? '/';\n if (\n normalizeUrlForComparison(dest) &&\n normalizeUrlForComparison(dest) === normalizeUrlForComparison(url)\n ) {\n return `// navigation handled by pomPage.goto()`;\n }\n return `await page.goto('${escapeForSingleQuotedString(dest)}');`;\n }\n\n // Fill / type / enter into a field\n // Pattern: \"Fill in <field> with '<value>'\" or \"Enter '<value>' in <field>\"\n if (/\\b(type|enter|fill|input|write)\\b/i.test(s)) {\n const values = s.match(/[\"']([^\"']+)[\"']/g) ?? [];\n const value = values[values.length - 1]?.replace(/['\"]/g, '') ?? 'value';\n if (hintedSelector) return `await ${hintedSelector}.fill('${escapeForSingleQuotedString(value)}');`;\n\n // \"Fill in <field> with ...\" — capture the word(s) between \"in\" and \"with\"\n const inWithMatch = s.match(/\\bin\\s+([a-zA-Z][a-zA-Z\\s]{1,25})\\s+with\\b/i);\n // \"Enter/type <value> in/into <field>\"\n const intoMatch = s.match(/\\bin(?:to)?\\s+(?:the\\s+)?([a-zA-Z][a-zA-Z\\s]{1,25})(?:\\s+field|\\s+input|\\s+box|\\s+area)?/i);\n // \"... <field> field/input\"\n const fieldMatch = s.match(/([a-zA-Z][a-zA-Z\\s]{1,25})\\s+(?:field|input|box|area)/i);\n\n const field = (inWithMatch?.[1] ?? intoMatch?.[1] ?? fieldMatch?.[1] ?? 'field').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill('${escapeForSingleQuotedString(value)}');`;\n }\n\n // Click a button\n if (/\\bclick\\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Click a link\n if (/\\bclick\\b.*(link|anchor|nav)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('link', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Generic click\n if (/^(click|press|tap)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (nameMatch) return `await page.getByText('${escapeForSingleQuotedString(nameMatch[1])}').click();`;\n const target = s.replace(/^(click|press|tap)\\s+(on\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(target)}').click();`;\n }\n\n // Select dropdown\n if (/\\b(select|choose|pick)\\b/i.test(s)) {\n const valueMatch = s.match(/[\"']([^\"']+)[\"']/);\n const value = valueMatch?.[1] ?? 'option';\n if (hintedSelector) return `await ${hintedSelector}.selectOption('${escapeForSingleQuotedString(value)}');`;\n return `await page.getByRole('combobox').selectOption('${escapeForSingleQuotedString(value)}');`;\n }\n\n // Check / uncheck\n if (/\\b(uncheck|untick|disable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.uncheck();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/uncheck|untick|disable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').uncheck();`;\n }\n if (/\\b(check|tick|enable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.check();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/check|tick|enable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').check();`;\n }\n\n // Keyboard key\n if (/press.*(enter|tab|escape|esc|space|backspace)/i.test(s)) {\n const keyMatch = s.match(/enter|tab|escape|esc|space|backspace/i);\n const key = (keyMatch?.[0] ?? 'Enter');\n return `await page.keyboard.press('${key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}');`;\n }\n\n // Reload\n if (/\\b(reload|refresh)\\b/i.test(s)) {\n return `await page.reload();`;\n }\n\n // Hover\n if (/\\bhover\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.hover();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/hover\\s+(over\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(name)}').hover();`;\n }\n\n // Scroll\n if (/\\bscroll\\b/i.test(s)) {\n return `await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));`;\n }\n\n // Fallback — preserve the step as a comment so the file is still valid\n return `// TODO: map to Playwright action → \"${s}\"`;\n}\n\n// ─── Expected result → assertion ──────────────────────────────────────────────\n\nfunction expectedToAssertion(\n expected: string,\n norm?: NormalizedCase,\n hint?: { playwright?: string },\n): string {\n if (hint?.playwright) return ensureStatement(hint.playwright);\n const s = expected.trim();\n const fieldName = fieldNameFromSentence(s);\n const currentUrlPattern = urlPatternFromAbsoluteUrl(norm?.url);\n\n // Cleared / empty field\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s) && fieldName) {\n return `await expect(page.getByLabel('${escapeForSingleQuotedString(fieldName)}')).toHaveValue('');`;\n }\n\n // Stayed on the same page / did not submit\n if (/\\bform\\b.*\\b(?:does not submit|doesn't submit|not submit|not submitted)\\b/i.test(s)) {\n const fallbackPattern = currentUrlPattern || 'login';\n return `await expect(page).toHaveURL(/${fallbackPattern}/); // form did not navigate`;\n }\n if (/\\b(remains?|stays?|still)\\s+on\\b/i.test(s)) {\n const pageMatch = s.match(/\\b(?:remains?|stays?|still)\\s+on\\s+(?:the\\s+)?(.+?)(?:\\s+page|\\s+screen)?$/i);\n const pagePattern = pageMatch?.[1] ? pageNameToUrlPattern(pageMatch[1]) : undefined;\n return `await expect(page).toHaveURL(/${pagePattern || currentUrlPattern || 'login'}/);`;\n }\n\n // URL / redirect check\n if (/\\b(url|redirect(?:s|ed)?|navigate(?:s|d)?|route|path)\\b/i.test(s)) {\n const pathMatch =\n s.match(/[\"'](\\/[^\"']+)[\"']/) ??\n s.match(/to\\s+(\\/[\\w/-]+)/i) ??\n s.match(/https?:\\/\\/[^\\s'\"]+/i);\n const matchedPath = pathMatch?.[1] ?? pathMatch?.[0];\n if (matchedPath) {\n const fromUrl = matchedPath.startsWith('http') ? urlPatternFromAbsoluteUrl(matchedPath) : undefined;\n const directPath = matchedPath.startsWith('/') ? matchedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/') : undefined;\n const finalPattern = fromUrl ?? directPath ?? currentUrlPattern;\n if (finalPattern !== undefined) return `await expect(page).toHaveURL(/${finalPattern}/);`;\n }\n return `await expect(page).toHaveURL(/dashboard|success|home/);`;\n }\n\n // Page title\n if (/\\bpage title\\b|\\btitle should\\b|\\bdocument title\\b/i.test(s)) {\n const titleMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (titleMatch) return `await expect(page).toHaveTitle('${escapeForSingleQuotedString(titleMatch[1])}');`;\n return `await expect(page).toHaveTitle(/.+/);`;\n }\n\n // Error / validation message\n if (/\\b(error|invalid|fail|incorrect|required|validation)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (fieldName) return `await expect(page.getByText(/${escapeForRegex(fieldName)}/i)).toBeVisible();`;\n return `await expect(page.getByRole('alert')).toBeVisible();`;\n }\n\n // Success / confirmation\n if (/\\b(success|confirm|complete|thank|welcome|sent|saved)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (/\\bwelcome\\b/i.test(s)) return `await expect(page.getByText(/welcome/i)).toBeVisible();`;\n return `await expect(page.getByRole('status')).toBeVisible();`;\n }\n\n // Not visible / hidden\n if (/\\b(not visible|hidden|disappear|removed|gone)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).not.toBeVisible();`;\n return `await expect(page.locator('.modal, [role=\"dialog\"]').first()).not.toBeVisible();`;\n }\n\n // Visible / present\n if (/\\b(visible|appear|display|show|render|present)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).toBeVisible();`;\n const subjectMatch =\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:is|are|should|must|becomes?|appears?|renders?|shows?|displays?)\\b/i) ??\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:visible|present)\\b/i);\n const subject = subjectMatch?.[1]?.trim();\n if (subject) {\n if (/\\bnavigation\\b.*\\bmenu\\b|\\bmenu\\b.*\\bnavigation\\b|\\bnavigation\\b/i.test(subject)) {\n return `await expect(page.getByRole('navigation')).toBeVisible();`;\n }\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(subject)}/i)).toBeVisible();`;\n }\n return `await expect(page.locator('[data-testid]').first()).toBeVisible();`;\n }\n\n // Count of items\n if (/\\b(count|number of|list of|\\d+\\s+item)\\b/i.test(s)) {\n const countMatch = s.match(/(\\d+)/);\n if (countMatch) return `await expect(page.locator('li, tr, [role=\"listitem\"]')).toHaveCount(${countMatch[1]});`;\n return `await expect(page.locator('li, tr').first()).toBeVisible();`;\n }\n\n // Enabled / disabled\n if (/\\b(enabled|clickable|active)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeEnabled();`;\n }\n if (/\\b(disabled|inactive)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeDisabled();`;\n }\n\n // Input value\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s)) {\n return `await expect(page.getByRole('textbox').first()).toHaveValue('');`;\n }\n if (/\\b(field|input|value|filled)\\b/i.test(s)) {\n const valMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (valMatch) return `await expect(page.getByRole('textbox').first()).toHaveValue('${escapeForSingleQuotedString(valMatch[1])}');`;\n return `await expect(page.getByRole('textbox').first()).not.toBeEmpty();`;\n }\n\n // Text / heading / content (broad fallback before final fallback)\n if (/\\b(text|content|label|message|heading)\\b/i.test(s)) {\n const textMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (textMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(textMatch[1])}')).toBeVisible();`;\n const words = s.replace(/\\b(the|should|must|contain|display|show|have|text|content)\\b/gi, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(words)}/i)).toBeVisible();`;\n }\n\n // Final fallback\n const cleaned = s.replace(/[\"']/g, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(cleaned)}/i)).toBeVisible(); // TODO: refine assertion`;\n}\n\n// ─── POM class name / filename ────────────────────────────────────────────────\n\nfunction toIdentifier(value: string, fallback = 'Landing'): string {\n const tokens = value\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .split(/[^a-zA-Z0-9_]+/)\n .filter(Boolean);\n\n const identifier = tokens\n .map((token) => token.charAt(0).toUpperCase() + token.slice(1))\n .join('');\n\n if (!identifier) return fallback;\n return /^[0-9]/.test(identifier) ? `Case${identifier}` : identifier;\n}\n\nfunction derivePomClassName(norm: NormalizedCase): string {\n const raw = norm.id ?? norm.title ?? 'Landing';\n const withoutNum = raw.replace(/-\\d+$/, '');\n return `${toIdentifier(withoutNum)}Page`;\n}\n\n// ─── POM class source ─────────────────────────────────────────────────────────\n\nfunction buildPomClass(className: string, norm: NormalizedCase, lang: 'ts' | 'js'): string {\n const pageUrl = norm.url ?? '/';\n const isTs = lang === 'ts';\n\n return isTs\n ? `import { Page } from '@playwright/test';\n\nexport class ${className} {\n readonly page: Page;\n\n constructor(page: Page) {\n this.page = page;\n }\n\n async goto(): Promise<void> {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad(): Promise<void> {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`\n : `// @ts-check\nexport class ${className} {\n /** @param {import('@playwright/test').Page} page */\n constructor(page) {\n /** @type {import('@playwright/test').Page} */\n this.page = page;\n }\n\n async goto() {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad() {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`;\n}\n\n// ─── Spec file source ─────────────────────────────────────────────────────────\n\nfunction buildTestTitle(norm: NormalizedCase): string {\n const idPart = norm.id ?? '';\n\n // norm.title may already include the ID prefix (e.g. \"AUTH-001 — User can log in\")\n // Strip it to avoid \"AUTH-001 — AUTH-001 — User can log in\" in the test name.\n let cleanTitle = norm.title || 'Untitled';\n if (idPart && cleanTitle.startsWith(idPart)) {\n cleanTitle = cleanTitle.slice(idPart.length).replace(/^\\s*[—\\-–]+\\s*/, '').trim();\n }\n\n const tagSuffix = (norm.tags ?? []).map(t => `@${t}`).join(' ');\n return [idPart, cleanTitle, tagSuffix].filter(Boolean).join(' — ').trim();\n}\n\nfunction buildSpecFile(\n norm: NormalizedCase,\n pomClassName: string,\n pomImportPath: string,\n): string {\n const title = buildTestTitle(norm);\n const steps = norm.steps ?? [];\n const expected = norm.expected ?? [];\n\n let importPath = pomImportPath.replace(/\\\\/g, '/');\n if (!importPath.startsWith('.')) importPath = `./${importPath}`;\n // Strip .ts extension — TypeScript resolves without it\n importPath = importPath.replace(/\\.ts$/, '');\n\n const stepLines = steps.length\n ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index])}`).join('\\n')\n : ' // TODO: add steps';\n\n const assertionLines = expected.length\n ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join('\\n')\n : ' // TODO: add assertions';\n\n const reviewNote = norm.needs_review\n ? `\\n // ⚠️ Flagged for review — steps or assertions may need manual refinement\\n`\n : '';\n\n return `import { test, expect } from '@playwright/test';\nimport { ${pomClassName} } from '${importPath}';\n\ntest('${title}', async ({ page }) => {${reviewNote}\n const pomPage = new ${pomClassName}(page);\n\n // ── Setup ─────────────────────────────────────────────────────────────────\n await pomPage.goto();\n await pomPage.waitForLoad();\n\n // ── Steps ─────────────────────────────────────────────────────────────────\n${stepLines}\n\n // ── Assertions ────────────────────────────────────────────────────────────\n${assertionLines}\n});\n`;\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport async function gen(opts: { lang: string; out: string }) {\n if (opts.lang !== 'ts' && opts.lang !== 'js') {\n console.error('❌ --lang must be ts or js');\n process.exit(1);\n }\n const lang = opts.lang as 'ts' | 'js';\n const specExt = lang === 'js' ? 'spec.js' : 'spec.ts';\n const pomExt = lang === 'js' ? '.js' : '.ts';\n\n const normalized = await fg([\n '.cementic/normalized/*.json',\n '!.cementic/normalized/_index.json',\n ]);\n\n if (normalized.length === 0) {\n console.warn('⚠️ No normalized cases found in .cementic/normalized/');\n console.warn(' Run: ct normalize ./cases');\n process.exit(1);\n }\n\n const projectRoot = process.cwd();\n const testsOutDir = resolve(projectRoot, opts.out);\n const pagesOutDir = resolve(projectRoot, 'pages');\n\n mkdirSync(testsOutDir, { recursive: true });\n mkdirSync(pagesOutDir, { recursive: true });\n\n let specCount = 0;\n let pomCount = 0;\n\n for (const f of normalized) {\n const norm = JSON.parse(readFileSync(f, 'utf8')) as NormalizedCase;\n\n const pomClassName = derivePomClassName(norm);\n const pomFileName = pomClassName + pomExt;\n const pomFilePath = join(pagesOutDir, pomFileName);\n\n // Only write a POM file if one doesn't exist yet — never overwrite user edits\n if (!existsSync(pomFilePath)) {\n writeFileSync(pomFilePath, buildPomClass(pomClassName, norm, lang));\n pomCount++;\n }\n\n // Relative import from the test file location to the POM file\n const relToPages = relative(testsOutDir, pagesOutDir);\n const pomImportPath = join(relToPages, pomFileName);\n\n const stem = basename(f).replace(/\\.json$/, '').replace(/[^\\w-]+/g, '-');\n const specPath = join(testsOutDir, `${stem}.${specExt}`);\n writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath));\n specCount++;\n }\n\n console.log(`✅ Generated ${specCount} spec file(s) → ${opts.out}/`);\n if (pomCount > 0) {\n console.log(`✅ Generated ${pomCount} POM class file(s) → pages/`);\n }\n if (pomCount === 0 && specCount > 0) {\n console.log(`ℹ️ POM classes already exist — skipped regeneration`);\n }\n}\n\nexport function genCmd() {\n const cmd = new Command('gen')\n .description('Generate Playwright POM spec + page-object files from normalized cases')\n .addHelpText('after', `\nExamples:\n $ ct gen --lang ts\n $ ct gen --lang js --out tests/e2e\n`)\n .option('--lang <lang>', 'Target language (ts|js)', 'ts')\n .option('--out <dir>', 'Output directory for spec files', 'tests/generated')\n .action(async opts => {\n await gen({ lang: opts.lang, out: opts.out });\n });\n return cmd;\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|