@cementic/cementic-test 0.2.4 → 0.2.5

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 CHANGED
@@ -14,7 +14,7 @@ It can scaffold a project, write or AI-generate case files, normalize them into
14
14
 
15
15
  - Scaffolds new Playwright projects in JavaScript or TypeScript
16
16
  - Generates Markdown test cases manually or with AI
17
- - Supports URL-aware case generation with live page scraping
17
+ - Supports URL-aware case generation with live page capture
18
18
  - Normalizes case files into structured JSON
19
19
  - Generates Playwright specs and POM classes from normalized cases
20
20
  - Avoids overwriting existing generated page-object files
@@ -124,13 +124,17 @@ Generates cases with awareness of a live page.
124
124
  ```bash
125
125
  ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 2
126
126
  ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 2
127
+ ct tc url https://mini-bank.testamplify.com/login --ai --headed --feature "Login"
128
+ ct tc url https://mini-bank.testamplify.com/login --ai --capture-only --feature "Login"
127
129
  ```
128
130
 
129
131
  Current behavior:
130
132
 
131
- - scrapes the page for headings, buttons, links, inputs, and metadata
132
- - passes that context to the AI flow when `--ai` is enabled
133
- - writes `<!-- ct:url ... -->` metadata into generated Markdown so normalization and generation preserve the target URL
133
+ - captures a live page with Playwright and extracts headings, buttons, links, inputs, status regions, and selector candidates
134
+ - sends that captured element map to the capture-aware AI flow when `--ai` is enabled
135
+ - saves the full capture artifact to `.cementic/capture/capture-*.json`
136
+ - saves a runnable preview spec to `tests/preview/spec-preview-*.spec.cjs` when AI scenarios are generated
137
+ - writes `<!-- ct:url ... -->` metadata plus selector and `playwright` hints into generated Markdown so normalization and generation preserve the capture output
134
138
 
135
139
  ### 4. `ct normalize <path>`
136
140
 
@@ -263,34 +267,42 @@ Current workflow behavior:
263
267
 
264
268
  ## AI provider support
265
269
 
266
- Current AI flow lives in `src/core/llm.ts`.
270
+ Current AI flows live in `src/core/llm.ts` and `src/core/analyse.ts`.
267
271
 
268
272
  Provider behavior in this version:
269
273
 
270
- - Anthropic is first-class and preferred when `ANTHROPIC_API_KEY` is set
271
- - OpenAI-compatible mode is used when Anthropic is not configured
274
+ - `ct tc --ai` uses the standard Markdown case writer in `src/core/llm.ts`
275
+ - `ct tc url --ai` uses the capture-aware analysis flow in `src/core/analyse.ts`
272
276
  - manual template generation is used if AI generation fails
273
277
 
274
278
  Supported environment variables:
275
279
 
276
280
  | Variable | Purpose |
277
281
  | --- | --- |
282
+ | `DEEPSEEK_API_KEY` | DeepSeek key for capture-aware analysis |
278
283
  | `ANTHROPIC_API_KEY` | Primary Anthropic key |
279
284
  | `CT_ANTHROPIC_API_KEY` | Alternate Anthropic key |
285
+ | `GEMINI_API_KEY` | Gemini key for capture-aware analysis |
280
286
  | `OPENAI_API_KEY` | OpenAI key |
287
+ | `QWEN_API_KEY` | Qwen key for capture-aware analysis |
288
+ | `KIMI_API_KEY` | Kimi / Moonshot key for capture-aware analysis |
281
289
  | `CT_LLM_API_KEY` | Generic OpenAI-compatible API key |
282
- | `CT_LLM_PROVIDER` | Optional provider override (`anthropic` or `openai`) |
290
+ | `CT_LLM_PROVIDER` | Optional provider override (`deepseek`, `anthropic`, `gemini`, `qwen`, `kimi`, or `openai`) |
283
291
  | `CT_LLM_MODEL` | Model override |
284
292
  | `CT_LLM_BASE_URL` | OpenAI-compatible base URL override |
285
293
 
286
294
  Current defaults:
287
295
 
288
- - Anthropic model default: `claude-sonnet-4-5`
289
- - OpenAI-compatible model default: `gpt-4o-mini`
296
+ - DeepSeek default: `deepseek-chat`
297
+ - Anthropic default: `claude-sonnet-4-5`
298
+ - Gemini default: `gemini-2.5-flash`
299
+ - Qwen default: `qwen-plus`
300
+ - Kimi default: `moonshot-v1-8k`
301
+ - OpenAI-compatible default: `gpt-4o-mini`
290
302
 
291
- ## URL scraping
303
+ ## URL capture
292
304
 
293
- Current scraping behavior lives in `src/core/scrape.ts`.
305
+ Current capture behavior lives in `src/core/capture.ts`.
294
306
 
295
307
  Primary path:
296
308
 
@@ -301,11 +313,11 @@ Primary path:
301
313
  - buttons
302
314
  - links
303
315
  - inputs with label, placeholder, name, type, and `data-testid`
304
- - landmark roles
305
-
306
- Fallback path:
307
-
308
- - falls back to `fetch` + regex parsing when Playwright is unavailable
316
+ - status and alert regions
317
+ - selector confidence and alternative selectors
318
+ - builds a structured `ElementMap`
319
+ - passes that capture into `src/core/analyse.ts` for evidence-backed scenario generation
320
+ - formats markdown, JSON, and preview-spec outputs through `src/core/report.ts`
309
321
 
310
322
  ## Generated project structure
311
323
 
@@ -354,8 +366,9 @@ This version is intentionally heuristic and file-oriented.
354
366
  Important current characteristics:
355
367
 
356
368
  - generated specs are POM-oriented, not raw recorded scripts
357
- - generator rules cover common cases well but are not a replacement for live self-healing
358
- - Playwright itself is used for scraping and test execution
369
+ - capture-generated selector and assertion hints are preserved through `normalize` and `gen`
370
+ - generator rules still cover non-capture cases heuristically, but capture-backed cases now keep exact selectors and assertions
371
+ - Playwright itself is used for capture and test execution
359
372
  - Playwright CLI and Playwright agents are not yet first-class runtime backends in this version
360
373
 
361
374
  ## Developer setup
@@ -382,6 +395,7 @@ What `npm test` currently verifies:
382
395
 
383
396
  - build succeeds
384
397
  - `ct tc url` writes URL metadata
398
+ - capture-generated selector and `playwright` hints survive `normalize` and `gen`
385
399
  - `ct normalize` and `ct gen` preserve the recent generator fixes
386
400
  - the JavaScript scaffold contains the expected page objects and sample specs
387
401
 
@@ -403,7 +417,7 @@ cd demo
403
417
  ct new sample --lang ts --no-browsers
404
418
  cd sample
405
419
 
406
- ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 1
420
+ ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 1
407
421
  ct normalize ./cases --and-gen --lang ts
408
422
  ct test
409
423
  ```
@@ -418,13 +432,27 @@ src/commands/normalize.ts
418
432
  src/commands/gen.ts
419
433
  src/commands/test.ts
420
434
  src/commands/flow.ts
435
+ src/core/capture.ts
436
+ src/core/analyse.ts
421
437
  src/core/llm.ts
422
- src/core/scrape.ts
438
+ src/core/report.ts
423
439
  src/core/prefix.ts
424
440
  templates/student-framework/
425
441
  templates/student-framework-ts/
426
442
  ```
427
443
 
444
+ ## Changelog
445
+
446
+ ### v0.2.5
447
+
448
+ - replaced the legacy URL scraping path with full Playwright-based live page capture
449
+ - integrated the POC capture and AI analysis flow into `ct tc url --ai`
450
+ - added capture artifacts at `.cementic/capture/capture-*.json`
451
+ - added preview spec output at `tests/preview/spec-preview-*.spec.cjs`
452
+ - preserved capture-generated selector hints and exact `playwright` assertions through `normalize` and `gen`
453
+ - added `--headed` and `--capture-only` support for the URL capture flow
454
+ - expanded capture-aware AI provider support to DeepSeek, Anthropic, Gemini, Qwen, Kimi, and OpenAI-compatible endpoints
455
+
428
456
  ## Current limitations
429
457
 
430
458
  - generator assertions and step mapping are heuristic
@@ -437,4 +465,4 @@ templates/student-framework-ts/
437
465
  1. Create a branch
438
466
  2. Run `npm test`
439
467
  3. Verify a manual `tc -> normalize -> gen -> test` flow if your change affects generation
440
- 4. Open a PR with the behavior change clearly described
468
+ 4. Open a PR with the behavior change clearly described
@@ -46,8 +46,14 @@ function visibleTextRegexFromPhrase(value) {
46
46
  if (tokens.length === 0) return ".+";
47
47
  return tokens.map((token) => escapeForRegex(token)).join("\\s+");
48
48
  }
49
- function stepToPlaywright(step, url) {
49
+ function ensureStatement(value) {
50
+ const trimmed = value.trim();
51
+ if (!trimmed) return trimmed;
52
+ return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
53
+ }
54
+ function stepToPlaywright(step, url, hint) {
50
55
  const s = step.trim();
56
+ const hintedSelector = hint?.selector ? `page.${hint.selector}` : void 0;
51
57
  if (/^(navigate|go to|open|visit|load)/i.test(s)) {
52
58
  const urlMatch = s.match(/https?:\/\/[^\s'"]+/) || s.match(/["']([^"']+)["']/);
53
59
  const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? "/";
@@ -59,6 +65,7 @@ function stepToPlaywright(step, url) {
59
65
  if (/\b(type|enter|fill|input|write)\b/i.test(s)) {
60
66
  const values = s.match(/["']([^"']+)["']/g) ?? [];
61
67
  const value = values[values.length - 1]?.replace(/['"]/g, "") ?? "value";
68
+ if (hintedSelector) return `await ${hintedSelector}.fill('${escapeForSingleQuotedString(value)}');`;
62
69
  const inWithMatch = s.match(/\bin\s+([a-zA-Z][a-zA-Z\s]{1,25})\s+with\b/i);
63
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);
64
71
  const fieldMatch = s.match(/([a-zA-Z][a-zA-Z\s]{1,25})\s+(?:field|input|box|area)/i);
@@ -66,16 +73,19 @@ function stepToPlaywright(step, url) {
66
73
  return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill('${escapeForSingleQuotedString(value)}');`;
67
74
  }
68
75
  if (/\bclick\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {
76
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
69
77
  const nameMatch = s.match(/["']([^"']+)["']/);
70
78
  const name = nameMatch?.[1] ?? s.replace(/click\s+(the\s+)?/i, "").trim();
71
79
  return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;
72
80
  }
73
81
  if (/\bclick\b.*(link|anchor|nav)/i.test(s)) {
82
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
74
83
  const nameMatch = s.match(/["']([^"']+)["']/);
75
84
  const name = nameMatch?.[1] ?? s.replace(/click\s+(the\s+)?/i, "").trim();
76
85
  return `await page.getByRole('link', { name: '${escapeForSingleQuotedString(name)}' }).click();`;
77
86
  }
78
87
  if (/^(click|press|tap)\b/i.test(s)) {
88
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
79
89
  const nameMatch = s.match(/["']([^"']+)["']/);
80
90
  if (nameMatch) return `await page.getByText('${escapeForSingleQuotedString(nameMatch[1])}').click();`;
81
91
  const target = s.replace(/^(click|press|tap)\s+(on\s+)?/i, "").trim();
@@ -84,14 +94,17 @@ function stepToPlaywright(step, url) {
84
94
  if (/\b(select|choose|pick)\b/i.test(s)) {
85
95
  const valueMatch = s.match(/["']([^"']+)["']/);
86
96
  const value = valueMatch?.[1] ?? "option";
97
+ if (hintedSelector) return `await ${hintedSelector}.selectOption('${escapeForSingleQuotedString(value)}');`;
87
98
  return `await page.getByRole('combobox').selectOption('${escapeForSingleQuotedString(value)}');`;
88
99
  }
89
100
  if (/\b(uncheck|untick|disable)\b/i.test(s)) {
101
+ if (hintedSelector) return `await ${hintedSelector}.uncheck();`;
90
102
  const nameMatch = s.match(/["']([^"']+)["']/);
91
103
  const name = nameMatch?.[1] ?? s.replace(/uncheck|untick|disable/gi, "").trim();
92
104
  return `await page.getByLabel('${escapeForSingleQuotedString(name)}').uncheck();`;
93
105
  }
94
106
  if (/\b(check|tick|enable)\b/i.test(s)) {
107
+ if (hintedSelector) return `await ${hintedSelector}.check();`;
95
108
  const nameMatch = s.match(/["']([^"']+)["']/);
96
109
  const name = nameMatch?.[1] ?? s.replace(/check|tick|enable/gi, "").trim();
97
110
  return `await page.getByLabel('${escapeForSingleQuotedString(name)}').check();`;
@@ -105,6 +118,7 @@ function stepToPlaywright(step, url) {
105
118
  return `await page.reload();`;
106
119
  }
107
120
  if (/\bhover\b/i.test(s)) {
121
+ if (hintedSelector) return `await ${hintedSelector}.hover();`;
108
122
  const nameMatch = s.match(/["']([^"']+)["']/);
109
123
  const name = nameMatch?.[1] ?? s.replace(/hover\s+(over\s+)?/i, "").trim();
110
124
  return `await page.getByText('${escapeForSingleQuotedString(name)}').hover();`;
@@ -114,7 +128,8 @@ function stepToPlaywright(step, url) {
114
128
  }
115
129
  return `// TODO: map to Playwright action \u2192 "${s}"`;
116
130
  }
117
- function expectedToAssertion(expected, norm) {
131
+ function expectedToAssertion(expected, norm, hint) {
132
+ if (hint?.playwright) return ensureStatement(hint.playwright);
118
133
  const s = expected.trim();
119
134
  const fieldName = fieldNameFromSentence(s);
120
135
  const currentUrlPattern = urlPatternFromAbsoluteUrl(norm?.url);
@@ -264,8 +279,8 @@ function buildSpecFile(norm, pomClassName, pomImportPath) {
264
279
  let importPath = pomImportPath.replace(/\\/g, "/");
265
280
  if (!importPath.startsWith(".")) importPath = `./${importPath}`;
266
281
  importPath = importPath.replace(/\.ts$/, "");
267
- const stepLines = steps.length ? steps.map((s) => ` ${stepToPlaywright(s, norm.url)}`).join("\n") : " // TODO: add steps";
268
- const assertionLines = expected.length ? expected.map((e) => ` ${expectedToAssertion(e, norm)}`).join("\n") : " // TODO: add assertions";
282
+ const stepLines = steps.length ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index])}`).join("\n") : " // TODO: add steps";
283
+ const assertionLines = expected.length ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join("\n") : " // TODO: add assertions";
269
284
  const reviewNote = norm.needs_review ? `
270
285
  // \u26A0\uFE0F Flagged for review \u2014 steps or assertions may need manual refinement
271
286
  ` : "";
@@ -350,4 +365,4 @@ export {
350
365
  gen,
351
366
  genCmd
352
367
  };
353
- //# sourceMappingURL=chunk-3EE7LWWT.js.map
368
+ //# sourceMappingURL=chunk-5QRDTCSM.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\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 derivePomClassName(norm: NormalizedCase): string {\n const raw = norm.id ?? norm.title ?? 'Landing';\n const withoutNum = raw.replace(/-\\d+$/, '').replace(/[-_\\s]+/g, ' ');\n const words = withoutNum\n .split(/\\s+/)\n .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());\n return words.join('') + '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":[]}