@cementic/cementic-test 0.2.4 → 0.2.6
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/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +61 -0
- package/README.md +69 -25
- package/dist/{chunk-3EE7LWWT.js → chunk-5QRDTCSM.js} +20 -5
- package/dist/chunk-5QRDTCSM.js.map +1 -0
- package/dist/cli.js +991 -132
- package/dist/cli.js.map +1 -1
- package/dist/{gen-IO4KKGYY.js → gen-RMRQOAD3.js} +2 -2
- package/package.json +7 -3
- package/scripts/postinstall-banner.cjs +14 -0
- package/dist/chunk-3EE7LWWT.js.map +0 -1
- /package/dist/{gen-IO4KKGYY.js.map → gen-RMRQOAD3.js.map} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as contributors and maintainers pledge to make participation in the CementicTest community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
|
8
|
+
|
|
9
|
+
## Our Standards
|
|
10
|
+
|
|
11
|
+
Examples of behavior that contributes to a positive environment for this project include:
|
|
12
|
+
|
|
13
|
+
- demonstrating empathy and kindness toward other people
|
|
14
|
+
- being respectful of differing opinions, viewpoints, and experiences
|
|
15
|
+
- giving and gracefully accepting constructive feedback
|
|
16
|
+
- taking responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
|
17
|
+
- focusing on what is best not just for us as individuals, but for the overall community
|
|
18
|
+
|
|
19
|
+
Examples of unacceptable behavior include:
|
|
20
|
+
|
|
21
|
+
- the use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
22
|
+
- trolling, insulting or derogatory comments, and personal or political attacks
|
|
23
|
+
- public or private harassment
|
|
24
|
+
- publishing others' private information, such as a physical or email address, without their explicit permission
|
|
25
|
+
- other conduct which could reasonably be considered inappropriate in a professional setting
|
|
26
|
+
|
|
27
|
+
## Enforcement Responsibilities
|
|
28
|
+
|
|
29
|
+
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
|
30
|
+
|
|
31
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This Code of Conduct applies within all project spaces and also applies when an individual is officially representing the project in public spaces. Examples of representing the project include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
|
36
|
+
|
|
37
|
+
## Enforcement
|
|
38
|
+
|
|
39
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers through the repository hosting platform or the project contact channels. All complaints will be reviewed and investigated promptly and fairly.
|
|
40
|
+
|
|
41
|
+
All project maintainers are obligated to respect the privacy and security of the reporter of any incident.
|
|
42
|
+
|
|
43
|
+
## Attribution
|
|
44
|
+
|
|
45
|
+
This Code of Conduct is adapted from the Contributor Covenant, version 2.1:
|
|
46
|
+
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for contributing to CementicTest CLI.
|
|
4
|
+
|
|
5
|
+
## What to contribute
|
|
6
|
+
|
|
7
|
+
Useful contributions include:
|
|
8
|
+
|
|
9
|
+
- bug fixes
|
|
10
|
+
- test coverage improvements
|
|
11
|
+
- CLI workflow improvements
|
|
12
|
+
- generator accuracy improvements
|
|
13
|
+
- documentation updates
|
|
14
|
+
- template updates for scaffolded projects
|
|
15
|
+
|
|
16
|
+
If your change affects generated output, command behavior, or project scaffolding, include tests or a clear explanation of why tests are not practical.
|
|
17
|
+
|
|
18
|
+
## Local setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
npm run build
|
|
23
|
+
npm test
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Development workflow
|
|
27
|
+
|
|
28
|
+
1. Create a branch for your change.
|
|
29
|
+
2. Make the smallest coherent change that solves the problem.
|
|
30
|
+
3. Run `npm test`.
|
|
31
|
+
4. If your change affects generation, also verify a manual `tc -> normalize -> gen -> test` flow.
|
|
32
|
+
5. Update docs when user-facing behavior changes.
|
|
33
|
+
|
|
34
|
+
## Manual smoke flow
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
mkdir demo
|
|
38
|
+
cd demo
|
|
39
|
+
|
|
40
|
+
ct new sample --lang ts --no-browsers
|
|
41
|
+
cd sample
|
|
42
|
+
|
|
43
|
+
ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 1
|
|
44
|
+
ct normalize ./cases --and-gen --lang ts
|
|
45
|
+
ct test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Pull requests
|
|
49
|
+
|
|
50
|
+
Please make sure your pull request includes:
|
|
51
|
+
|
|
52
|
+
- a short summary of the behavior change
|
|
53
|
+
- any relevant tests
|
|
54
|
+
- documentation updates when needed
|
|
55
|
+
- notes about manual verification if generation behavior changed
|
|
56
|
+
|
|
57
|
+
## Issues and discussion
|
|
58
|
+
|
|
59
|
+
- Bugs and feature requests: use the GitHub issue tracker
|
|
60
|
+
- Community and product updates: https://t.me/+Wbx7oK7ivqgxZGJh
|
|
61
|
+
- Website and upcoming web app: https://cementic.testamplify.io/
|
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
CementicTest CLI (`ct`) turns plain-English test cases into runnable Playwright tests with a Page Object Model-oriented workflow.
|
|
4
4
|
|
|
5
|
+
## Project links
|
|
6
|
+
|
|
7
|
+
- Website: https://cementic.testamplify.io/
|
|
8
|
+
- Community: https://t.me/+Wbx7oK7ivqgxZGJh
|
|
9
|
+
- Web app: browser-based AI testing workflow coming soon
|
|
10
|
+
|
|
5
11
|
Current pipeline:
|
|
6
12
|
|
|
7
13
|
```text
|
|
@@ -10,11 +16,17 @@ ct tc -> ct normalize -> ct gen -> ct test
|
|
|
10
16
|
|
|
11
17
|
It can scaffold a project, write or AI-generate case files, normalize them into JSON, generate Playwright specs plus page objects, and run the resulting suite.
|
|
12
18
|
|
|
19
|
+
## Project status
|
|
20
|
+
|
|
21
|
+
This repository currently ships the CLI workflow.
|
|
22
|
+
|
|
23
|
+
The browser-based web app is in progress and early product updates will be shared on the website and in the community channel.
|
|
24
|
+
|
|
13
25
|
## What this version does
|
|
14
26
|
|
|
15
27
|
- Scaffolds new Playwright projects in JavaScript or TypeScript
|
|
16
28
|
- Generates Markdown test cases manually or with AI
|
|
17
|
-
- Supports URL-aware case generation with live page
|
|
29
|
+
- Supports URL-aware case generation with live page capture
|
|
18
30
|
- Normalizes case files into structured JSON
|
|
19
31
|
- Generates Playwright specs and POM classes from normalized cases
|
|
20
32
|
- Avoids overwriting existing generated page-object files
|
|
@@ -124,13 +136,17 @@ Generates cases with awareness of a live page.
|
|
|
124
136
|
```bash
|
|
125
137
|
ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 2
|
|
126
138
|
ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 2
|
|
139
|
+
ct tc url https://mini-bank.testamplify.com/login --ai --headed --feature "Login"
|
|
140
|
+
ct tc url https://mini-bank.testamplify.com/login --ai --capture-only --feature "Login"
|
|
127
141
|
```
|
|
128
142
|
|
|
129
143
|
Current behavior:
|
|
130
144
|
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
145
|
+
- captures a live page with Playwright and extracts headings, buttons, links, inputs, status regions, and selector candidates
|
|
146
|
+
- sends that captured element map to the capture-aware AI flow when `--ai` is enabled
|
|
147
|
+
- saves the full capture artifact to `.cementic/capture/capture-*.json`
|
|
148
|
+
- saves a runnable preview spec to `tests/preview/spec-preview-*.spec.cjs` when AI scenarios are generated
|
|
149
|
+
- writes `<!-- ct:url ... -->` metadata plus selector and `playwright` hints into generated Markdown so normalization and generation preserve the capture output
|
|
134
150
|
|
|
135
151
|
### 4. `ct normalize <path>`
|
|
136
152
|
|
|
@@ -263,34 +279,42 @@ Current workflow behavior:
|
|
|
263
279
|
|
|
264
280
|
## AI provider support
|
|
265
281
|
|
|
266
|
-
Current AI
|
|
282
|
+
Current AI flows live in `src/core/llm.ts` and `src/core/analyse.ts`.
|
|
267
283
|
|
|
268
284
|
Provider behavior in this version:
|
|
269
285
|
|
|
270
|
-
-
|
|
271
|
-
-
|
|
286
|
+
- `ct tc --ai` uses the standard Markdown case writer in `src/core/llm.ts`
|
|
287
|
+
- `ct tc url --ai` uses the capture-aware analysis flow in `src/core/analyse.ts`
|
|
272
288
|
- manual template generation is used if AI generation fails
|
|
273
289
|
|
|
274
290
|
Supported environment variables:
|
|
275
291
|
|
|
276
292
|
| Variable | Purpose |
|
|
277
293
|
| --- | --- |
|
|
294
|
+
| `DEEPSEEK_API_KEY` | DeepSeek key for capture-aware analysis |
|
|
278
295
|
| `ANTHROPIC_API_KEY` | Primary Anthropic key |
|
|
279
296
|
| `CT_ANTHROPIC_API_KEY` | Alternate Anthropic key |
|
|
297
|
+
| `GEMINI_API_KEY` | Gemini key for capture-aware analysis |
|
|
280
298
|
| `OPENAI_API_KEY` | OpenAI key |
|
|
299
|
+
| `QWEN_API_KEY` | Qwen key for capture-aware analysis |
|
|
300
|
+
| `KIMI_API_KEY` | Kimi / Moonshot key for capture-aware analysis |
|
|
281
301
|
| `CT_LLM_API_KEY` | Generic OpenAI-compatible API key |
|
|
282
|
-
| `CT_LLM_PROVIDER` | Optional provider override (`anthropic` or `openai`) |
|
|
302
|
+
| `CT_LLM_PROVIDER` | Optional provider override (`deepseek`, `anthropic`, `gemini`, `qwen`, `kimi`, or `openai`) |
|
|
283
303
|
| `CT_LLM_MODEL` | Model override |
|
|
284
304
|
| `CT_LLM_BASE_URL` | OpenAI-compatible base URL override |
|
|
285
305
|
|
|
286
306
|
Current defaults:
|
|
287
307
|
|
|
288
|
-
-
|
|
289
|
-
-
|
|
308
|
+
- DeepSeek default: `deepseek-chat`
|
|
309
|
+
- Anthropic default: `claude-sonnet-4-5`
|
|
310
|
+
- Gemini default: `gemini-2.5-flash`
|
|
311
|
+
- Qwen default: `qwen-plus`
|
|
312
|
+
- Kimi default: `moonshot-v1-8k`
|
|
313
|
+
- OpenAI-compatible default: `gpt-4o-mini`
|
|
290
314
|
|
|
291
|
-
## URL
|
|
315
|
+
## URL capture
|
|
292
316
|
|
|
293
|
-
Current
|
|
317
|
+
Current capture behavior lives in `src/core/capture.ts`.
|
|
294
318
|
|
|
295
319
|
Primary path:
|
|
296
320
|
|
|
@@ -301,11 +325,11 @@ Primary path:
|
|
|
301
325
|
- buttons
|
|
302
326
|
- links
|
|
303
327
|
- inputs with label, placeholder, name, type, and `data-testid`
|
|
304
|
-
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
-
|
|
328
|
+
- status and alert regions
|
|
329
|
+
- selector confidence and alternative selectors
|
|
330
|
+
- builds a structured `ElementMap`
|
|
331
|
+
- passes that capture into `src/core/analyse.ts` for evidence-backed scenario generation
|
|
332
|
+
- formats markdown, JSON, and preview-spec outputs through `src/core/report.ts`
|
|
309
333
|
|
|
310
334
|
## Generated project structure
|
|
311
335
|
|
|
@@ -354,8 +378,9 @@ This version is intentionally heuristic and file-oriented.
|
|
|
354
378
|
Important current characteristics:
|
|
355
379
|
|
|
356
380
|
- generated specs are POM-oriented, not raw recorded scripts
|
|
357
|
-
-
|
|
358
|
-
-
|
|
381
|
+
- capture-generated selector and assertion hints are preserved through `normalize` and `gen`
|
|
382
|
+
- generator rules still cover non-capture cases heuristically, but capture-backed cases now keep exact selectors and assertions
|
|
383
|
+
- Playwright itself is used for capture and test execution
|
|
359
384
|
- Playwright CLI and Playwright agents are not yet first-class runtime backends in this version
|
|
360
385
|
|
|
361
386
|
## Developer setup
|
|
@@ -382,6 +407,7 @@ What `npm test` currently verifies:
|
|
|
382
407
|
|
|
383
408
|
- build succeeds
|
|
384
409
|
- `ct tc url` writes URL metadata
|
|
410
|
+
- capture-generated selector and `playwright` hints survive `normalize` and `gen`
|
|
385
411
|
- `ct normalize` and `ct gen` preserve the recent generator fixes
|
|
386
412
|
- the JavaScript scaffold contains the expected page objects and sample specs
|
|
387
413
|
|
|
@@ -403,7 +429,7 @@ cd demo
|
|
|
403
429
|
ct new sample --lang ts --no-browsers
|
|
404
430
|
cd sample
|
|
405
431
|
|
|
406
|
-
ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 1
|
|
432
|
+
ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 1
|
|
407
433
|
ct normalize ./cases --and-gen --lang ts
|
|
408
434
|
ct test
|
|
409
435
|
```
|
|
@@ -418,13 +444,34 @@ src/commands/normalize.ts
|
|
|
418
444
|
src/commands/gen.ts
|
|
419
445
|
src/commands/test.ts
|
|
420
446
|
src/commands/flow.ts
|
|
447
|
+
src/core/capture.ts
|
|
448
|
+
src/core/analyse.ts
|
|
421
449
|
src/core/llm.ts
|
|
422
|
-
src/core/
|
|
450
|
+
src/core/report.ts
|
|
423
451
|
src/core/prefix.ts
|
|
424
452
|
templates/student-framework/
|
|
425
453
|
templates/student-framework-ts/
|
|
426
454
|
```
|
|
427
455
|
|
|
456
|
+
## Changelog
|
|
457
|
+
|
|
458
|
+
### v0.2.6
|
|
459
|
+
|
|
460
|
+
- added an install-time banner with links to the website and community
|
|
461
|
+
- surfaced project links and web app status near the top of the README
|
|
462
|
+
- added a standalone `CONTRIBUTING.md`
|
|
463
|
+
- added a `CODE_OF_CONDUCT.md`
|
|
464
|
+
|
|
465
|
+
### v0.2.5
|
|
466
|
+
|
|
467
|
+
- replaced the legacy URL scraping path with full Playwright-based live page capture
|
|
468
|
+
- integrated the POC capture and AI analysis flow into `ct tc url --ai`
|
|
469
|
+
- added capture artifacts at `.cementic/capture/capture-*.json`
|
|
470
|
+
- added preview spec output at `tests/preview/spec-preview-*.spec.cjs`
|
|
471
|
+
- preserved capture-generated selector hints and exact `playwright` assertions through `normalize` and `gen`
|
|
472
|
+
- added `--headed` and `--capture-only` support for the URL capture flow
|
|
473
|
+
- expanded capture-aware AI provider support to DeepSeek, Anthropic, Gemini, Qwen, Kimi, and OpenAI-compatible endpoints
|
|
474
|
+
|
|
428
475
|
## Current limitations
|
|
429
476
|
|
|
430
477
|
- generator assertions and step mapping are heuristic
|
|
@@ -434,7 +481,4 @@ templates/student-framework-ts/
|
|
|
434
481
|
|
|
435
482
|
## Contributing
|
|
436
483
|
|
|
437
|
-
|
|
438
|
-
2. Run `npm test`
|
|
439
|
-
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
|
|
484
|
+
Contribution guidelines live in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
@@ -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
|
|
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-
|
|
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":[]}
|