@cementic/cementic-test 0.2.3
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/LICENSE +201 -0
- package/README.md +625 -0
- package/dist/chunk-J63TUHIV.js +80 -0
- package/dist/chunk-J63TUHIV.js.map +1 -0
- package/dist/cli.js +823 -0
- package/dist/cli.js.map +1 -0
- package/dist/gen-54KYT3RO.js +10 -0
- package/dist/gen-54KYT3RO.js.map +1 -0
- package/dist/templates/student-framework/README.md +13 -0
- package/dist/templates/student-framework/package-lock.json +204 -0
- package/dist/templates/student-framework/package.json +33 -0
- package/dist/templates/student-framework/pages/LandingPage.js +18 -0
- package/dist/templates/student-framework/playwright.config.js +75 -0
- package/dist/templates/student-framework/tests/landing.spec.js +10 -0
- package/dist/templates/student-framework/workflows/playwright.yml +114 -0
- package/package.json +48 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
genCmd
|
|
4
|
+
} from "./chunk-J63TUHIV.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { Command as Command9 } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/commands/new.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, cpSync, readdirSync, statSync } from "fs";
|
|
12
|
+
import { join, resolve } from "path";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { dirname } from "path";
|
|
16
|
+
import { platform, release } from "os";
|
|
17
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
var __dirname = dirname(__filename);
|
|
19
|
+
function newCmd() {
|
|
20
|
+
const cmd = new Command("new").arguments("<projectName>").description("Scaffold a new CementicTest + Playwright project from scratch").addHelpText("after", `
|
|
21
|
+
Examples:
|
|
22
|
+
$ ct new my-awesome-test-suite
|
|
23
|
+
$ ct new e2e-tests --no-browsers
|
|
24
|
+
`).option("--mode <mode>", "greenfield|enhance", "greenfield").option("--no-browsers", 'do not run "npx playwright install" during setup').action((projectName, opts) => {
|
|
25
|
+
const root = process.cwd();
|
|
26
|
+
const projectPath = join(root, projectName);
|
|
27
|
+
console.log(`\u{1F680} Initializing new CementicTest project in ${projectName}...`);
|
|
28
|
+
if (existsSync(projectPath)) {
|
|
29
|
+
console.error(`\u274C Directory ${projectName} already exists.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
mkdirSync(projectPath, { recursive: true });
|
|
33
|
+
let templatePath = resolve(__dirname, "templates/student-framework");
|
|
34
|
+
if (!existsSync(templatePath)) {
|
|
35
|
+
templatePath = resolve(__dirname, "../templates/student-framework");
|
|
36
|
+
}
|
|
37
|
+
if (!existsSync(templatePath)) {
|
|
38
|
+
templatePath = resolve(__dirname, "../../templates/student-framework");
|
|
39
|
+
}
|
|
40
|
+
if (!existsSync(templatePath)) {
|
|
41
|
+
templatePath = resolve(process.cwd(), "templates/student-framework");
|
|
42
|
+
}
|
|
43
|
+
if (!existsSync(templatePath)) {
|
|
44
|
+
console.error(`\u274C Could not locate template at ${templatePath}`);
|
|
45
|
+
console.error("Please ensure the package is built correctly with templates included.");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.log(`\u{1F4E6} Copying template from ${templatePath}...`);
|
|
49
|
+
function copyRecursive(src, dest) {
|
|
50
|
+
if (statSync(src).isDirectory()) {
|
|
51
|
+
mkdirSync(dest, { recursive: true });
|
|
52
|
+
readdirSync(src).forEach((child) => {
|
|
53
|
+
copyRecursive(join(src, child), join(dest, child));
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
cpSync(src, dest);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
copyRecursive(templatePath, projectPath);
|
|
60
|
+
if (platform() === "darwin") {
|
|
61
|
+
const osRelease = release();
|
|
62
|
+
const majorVersion = parseInt(osRelease.split(".")[0], 10);
|
|
63
|
+
if (majorVersion < 23) {
|
|
64
|
+
console.log("\u{1F34E} Detected macOS 13 or older. Adjusting versions for compatibility...");
|
|
65
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
66
|
+
if (existsSync(pkgJsonPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const pkgContent = readFileSync(pkgJsonPath, "utf-8");
|
|
69
|
+
const pkg = JSON.parse(pkgContent);
|
|
70
|
+
if (pkg.devDependencies) {
|
|
71
|
+
pkg.devDependencies["@playwright/test"] = "^1.48.2";
|
|
72
|
+
pkg.devDependencies["allure-playwright"] = "^2.15.1";
|
|
73
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
74
|
+
console.log("\u2705 Downgraded @playwright/test and allure-playwright for legacy macOS support.");
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.warn("\u26A0\uFE0F Failed to adjust package.json for OS compatibility:", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
execSync("git init", { cwd: projectPath, stdio: "ignore" });
|
|
84
|
+
const gitignorePath = join(projectPath, ".gitignore");
|
|
85
|
+
if (!existsSync(gitignorePath)) {
|
|
86
|
+
writeFileSync(gitignorePath, "node_modules\n.env\ntest-results\nplaywright-report\n.cementic\n");
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.warn("\u26A0\uFE0F Failed to initialize git repository.");
|
|
90
|
+
}
|
|
91
|
+
console.log("\u{1F4E6} Installing dependencies...");
|
|
92
|
+
try {
|
|
93
|
+
execSync("npm install", { cwd: projectPath, stdio: "inherit" });
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('\u274C Failed to install dependencies. Please run "npm install" manually.');
|
|
96
|
+
}
|
|
97
|
+
if (opts.browsers !== false) {
|
|
98
|
+
console.log("\u{1F310} Installing Playwright browsers...");
|
|
99
|
+
try {
|
|
100
|
+
execSync("npx playwright install", { cwd: projectPath, stdio: "inherit" });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.warn('\u26A0\uFE0F Failed to install browsers. Run "npx playwright install" manually.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log(`
|
|
106
|
+
\u2705 Project ${projectName} created successfully!`);
|
|
107
|
+
console.log(`
|
|
108
|
+
To get started:
|
|
109
|
+
`);
|
|
110
|
+
console.log(` cd ${projectName}`);
|
|
111
|
+
console.log(` npx playwright test`);
|
|
112
|
+
console.log(`
|
|
113
|
+
Happy testing! \u{1F9EA}`);
|
|
114
|
+
});
|
|
115
|
+
return cmd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/commands/normalize.ts
|
|
119
|
+
import { Command as Command2 } from "commander";
|
|
120
|
+
import fg from "fast-glob";
|
|
121
|
+
import { readFileSync as readFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
|
|
122
|
+
import { join as join2, basename, resolve as resolve2 } from "path";
|
|
123
|
+
function parseTags(title) {
|
|
124
|
+
const tags = Array.from(title.matchAll(/@([\w-]+)/g)).map((m) => m[1]);
|
|
125
|
+
const clean = title.replace(/@[\w-]+/g, "").trim();
|
|
126
|
+
return { clean, tags };
|
|
127
|
+
}
|
|
128
|
+
function parseId(title) {
|
|
129
|
+
const m = title.match(/\b([A-Z]+-\d+)\b/);
|
|
130
|
+
return m?.[1];
|
|
131
|
+
}
|
|
132
|
+
function splitCasesByHeading(fileText) {
|
|
133
|
+
const lines = fileText.split(/\r?\n/);
|
|
134
|
+
const blocks = [];
|
|
135
|
+
let currentTitle = null;
|
|
136
|
+
let buf = [];
|
|
137
|
+
const flush = () => {
|
|
138
|
+
if (currentTitle !== null) {
|
|
139
|
+
blocks.push({ titleLine: currentTitle, body: buf.join("\n") });
|
|
140
|
+
}
|
|
141
|
+
buf = [];
|
|
142
|
+
};
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const h1 = line.match(/^\s*#\s+(.+)$/);
|
|
145
|
+
if (h1) {
|
|
146
|
+
if (currentTitle !== null) flush();
|
|
147
|
+
currentTitle = h1[1].trim();
|
|
148
|
+
} else {
|
|
149
|
+
buf.push(line);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (currentTitle !== null) flush();
|
|
153
|
+
if (blocks.length === 0) {
|
|
154
|
+
const first = lines.find((l) => l.trim());
|
|
155
|
+
const title = first?.replace(/^#\s*/, "").trim() || "Untitled";
|
|
156
|
+
return [{ titleLine: title, body: lines.join("\n") }];
|
|
157
|
+
}
|
|
158
|
+
return blocks;
|
|
159
|
+
}
|
|
160
|
+
function extractSections(body) {
|
|
161
|
+
const sectionRegex = /^\s*##\s*(.+?)\s*$/gim;
|
|
162
|
+
const sections = {};
|
|
163
|
+
let match;
|
|
164
|
+
const indices = [];
|
|
165
|
+
while (match = sectionRegex.exec(body)) {
|
|
166
|
+
indices.push({ name: match[1].toLowerCase(), index: match.index });
|
|
167
|
+
}
|
|
168
|
+
indices.push({ name: "__END__", index: body.length });
|
|
169
|
+
for (let i = 0; i < indices.length - 1; i++) {
|
|
170
|
+
const name = indices[i].name;
|
|
171
|
+
const slice = body.slice(indices[i].index, indices[i + 1].index);
|
|
172
|
+
sections[name] = slice;
|
|
173
|
+
}
|
|
174
|
+
const stepsBlock = sections["steps"] ?? "";
|
|
175
|
+
const expectedBlock = sections["expected"] ?? sections["expected results"] ?? sections["then"] ?? "";
|
|
176
|
+
const bullet = /^\s*(?:\d+\.|[-*])\s+(.+)$/gm;
|
|
177
|
+
const steps = Array.from(stepsBlock.matchAll(bullet)).map((m) => m[1].trim()) || [];
|
|
178
|
+
if (steps.length === 0) {
|
|
179
|
+
const alt = Array.from(body.matchAll(bullet)).map((m) => m[1].trim());
|
|
180
|
+
steps.push(...alt);
|
|
181
|
+
}
|
|
182
|
+
const expectedLines = Array.from(expectedBlock.matchAll(bullet)).map(
|
|
183
|
+
(m) => m[1].trim()
|
|
184
|
+
);
|
|
185
|
+
if (expectedLines.length === 0) {
|
|
186
|
+
const exp = Array.from(
|
|
187
|
+
body.matchAll(/^\s*(?:Expected|Then|Verify|Assert)[^\n]*:?[\s-]*(.+)$/gim)
|
|
188
|
+
).map((m) => m[1].trim());
|
|
189
|
+
expectedLines.push(...exp);
|
|
190
|
+
}
|
|
191
|
+
return { steps, expected: expectedLines };
|
|
192
|
+
}
|
|
193
|
+
function normalizeOne(titleLine, body, source) {
|
|
194
|
+
const { clean, tags } = parseTags(titleLine);
|
|
195
|
+
const id = parseId(clean);
|
|
196
|
+
const { steps, expected } = extractSections(body);
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
title: clean,
|
|
200
|
+
tags: tags.length ? tags : void 0,
|
|
201
|
+
steps,
|
|
202
|
+
expected,
|
|
203
|
+
needs_review: steps.length === 0 || expected.length === 0,
|
|
204
|
+
source
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function normalizeCmd() {
|
|
208
|
+
const cmd = new Command2("normalize").argument("<path>", "Input directory or file pattern containing test cases (Markdown, Text, CSV)").description("Convert human-readable test cases into machine-readable JSON format").addHelpText("after", `
|
|
209
|
+
Examples:
|
|
210
|
+
$ ct normalize ./cases
|
|
211
|
+
$ ct normalize "cases/**/*.md"
|
|
212
|
+
$ ct normalize ./cases --and-gen --lang ts (Normalize and generate tests in one go)
|
|
213
|
+
`).option("--report", "Generate a summary report of the normalization process", true).option("--and-gen", "Automatically run test generation after normalization", false).option("--lang <lang>", "Target language for generation (ts|js) when using --and-gen", "ts").action(async (inputPath, opts) => {
|
|
214
|
+
let patterns = [];
|
|
215
|
+
try {
|
|
216
|
+
const abs = resolve2(inputPath);
|
|
217
|
+
if (statSync2(abs).isDirectory()) {
|
|
218
|
+
const base = inputPath.replace(/\/$/, "");
|
|
219
|
+
patterns = [`${base}/**/*.{md,markdown,txt,feature,csv,json}`];
|
|
220
|
+
} else {
|
|
221
|
+
patterns = [inputPath];
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
patterns = [inputPath];
|
|
225
|
+
}
|
|
226
|
+
const files = await fg(patterns, { dot: false, onlyFiles: true });
|
|
227
|
+
if (files.length === 0) {
|
|
228
|
+
console.error(`No files found for: ${inputPath}`);
|
|
229
|
+
process.exit(2);
|
|
230
|
+
}
|
|
231
|
+
const outDir = ".cementic/normalized";
|
|
232
|
+
mkdirSync2(outDir, { recursive: true });
|
|
233
|
+
const index = {
|
|
234
|
+
summary: { total: 0, parsed: 0, withWarnings: 0 },
|
|
235
|
+
cases: []
|
|
236
|
+
};
|
|
237
|
+
for (const f of files) {
|
|
238
|
+
const content = readFileSync2(f, "utf8");
|
|
239
|
+
const blocks = splitCasesByHeading(content);
|
|
240
|
+
for (const block of blocks) {
|
|
241
|
+
const norm = normalizeOne(block.titleLine, block.body, f);
|
|
242
|
+
const stem = basename(f).replace(/\.[^/.]+$/, "");
|
|
243
|
+
const suffix = (norm.id || norm.title).replace(/[^\w-]+/g, "-");
|
|
244
|
+
const outFile = join2(outDir, `${stem}.${suffix}.json`);
|
|
245
|
+
writeFileSync2(outFile, JSON.stringify(norm, null, 2));
|
|
246
|
+
index.summary.total++;
|
|
247
|
+
index.summary.parsed++;
|
|
248
|
+
if (norm.needs_review) index.summary.withWarnings++;
|
|
249
|
+
index.cases.push({ file: f, normalized: outFile, status: norm.needs_review ? "warning" : "ok" });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
writeFileSync2(join2(outDir, "_index.json"), JSON.stringify(index, null, 2));
|
|
253
|
+
if (opts.report !== false) {
|
|
254
|
+
const lines = [
|
|
255
|
+
"# Normalize Report",
|
|
256
|
+
"",
|
|
257
|
+
`Total cases: ${index.summary.total} | Parsed: ${index.summary.parsed} | With warnings: ${index.summary.withWarnings}`,
|
|
258
|
+
"",
|
|
259
|
+
"| Source File | Normalized JSON | Status |",
|
|
260
|
+
"|-------------|-----------------|--------|",
|
|
261
|
+
...index.cases.map((c) => `| ${c.file} | ${c.normalized} | ${c.status} |`)
|
|
262
|
+
];
|
|
263
|
+
mkdirSync2(".cementic/reports", { recursive: true });
|
|
264
|
+
writeFileSync2(".cementic/reports/normalize-report.md", lines.join("\n"));
|
|
265
|
+
}
|
|
266
|
+
console.log(`\u2705 Normalized ${index.summary.parsed} case(s). Output \u2192 .cementic/normalized/`);
|
|
267
|
+
if (opts.andGen) {
|
|
268
|
+
const { gen } = await import("./gen-54KYT3RO.js");
|
|
269
|
+
await gen({ lang: opts.lang || "ts", out: "tests/generated" });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return cmd;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/commands/test.ts
|
|
276
|
+
import { Command as Command3 } from "commander";
|
|
277
|
+
import { spawn } from "child_process";
|
|
278
|
+
function testCmd() {
|
|
279
|
+
const cmd = new Command3("test").description('Run Playwright tests via "npx playwright test"').allowUnknownOption(true).allowExcessArguments(true).argument("[...pwArgs]", "Arguments to pass through to Playwright").action((pwArgs = []) => {
|
|
280
|
+
const child = spawn(
|
|
281
|
+
"npx",
|
|
282
|
+
["playwright", "test", ...pwArgs],
|
|
283
|
+
{
|
|
284
|
+
stdio: "inherit",
|
|
285
|
+
shell: process.platform === "win32"
|
|
286
|
+
// needed for Windows
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
child.on("exit", (code) => {
|
|
290
|
+
process.exit(code ?? 0);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
return cmd;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/commands/tc.ts
|
|
297
|
+
import { Command as Command4 } from "commander";
|
|
298
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
299
|
+
import { join as join3 } from "path";
|
|
300
|
+
import { createInterface } from "readline/promises";
|
|
301
|
+
import { stdin as input, stdout as output } from "process";
|
|
302
|
+
|
|
303
|
+
// src/core/prefix.ts
|
|
304
|
+
var PREFIX_MAP = [
|
|
305
|
+
{ keywords: ["login", "sign in", "signin", "auth", "authentication"], prefix: "AUTH" },
|
|
306
|
+
{ keywords: ["dashboard", "home"], prefix: "DASH" },
|
|
307
|
+
{ keywords: ["profile", "account"], prefix: "PROF" },
|
|
308
|
+
{ keywords: ["cart", "basket"], prefix: "CART" },
|
|
309
|
+
{ keywords: ["checkout", "payment", "pay"], prefix: "CHK" },
|
|
310
|
+
{ keywords: ["order", "orders"], prefix: "ORD" },
|
|
311
|
+
{ keywords: ["settings", "preferences", "config"], prefix: "SET" }
|
|
312
|
+
];
|
|
313
|
+
function normalizeText(text) {
|
|
314
|
+
return text.toLowerCase().trim();
|
|
315
|
+
}
|
|
316
|
+
function deriveFromFreeText(text) {
|
|
317
|
+
const norm = normalizeText(text);
|
|
318
|
+
for (const entry of PREFIX_MAP) {
|
|
319
|
+
if (entry.keywords.some((k) => norm.includes(k))) {
|
|
320
|
+
return entry.prefix;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const firstWord = norm.split(/\s+/).find((w) => /[a-z0-9]/.test(w));
|
|
324
|
+
if (!firstWord) return void 0;
|
|
325
|
+
return firstWord.replace(/[^a-z0-9]/gi, "").slice(0, 4).toUpperCase() || void 0;
|
|
326
|
+
}
|
|
327
|
+
function deriveFromUrl(url) {
|
|
328
|
+
try {
|
|
329
|
+
const u = new URL(url);
|
|
330
|
+
const segments = u.pathname.split("/").filter(Boolean);
|
|
331
|
+
const last = segments[segments.length - 1] || "";
|
|
332
|
+
if (!last) return void 0;
|
|
333
|
+
return deriveFromFreeText(last);
|
|
334
|
+
} catch {
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function inferPrefix(params) {
|
|
339
|
+
if (params.explicitPrefix) {
|
|
340
|
+
return params.explicitPrefix.trim().toUpperCase();
|
|
341
|
+
}
|
|
342
|
+
if (params.featureText) {
|
|
343
|
+
const fromFeature = deriveFromFreeText(params.featureText);
|
|
344
|
+
if (fromFeature) return fromFeature;
|
|
345
|
+
}
|
|
346
|
+
if (params.url) {
|
|
347
|
+
const fromUrl = deriveFromUrl(params.url);
|
|
348
|
+
if (fromUrl) return fromUrl;
|
|
349
|
+
}
|
|
350
|
+
return "TC";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/core/llm.ts
|
|
354
|
+
function buildSystemMessage() {
|
|
355
|
+
return `
|
|
356
|
+
You are a senior QA engineer and test case designer.
|
|
357
|
+
|
|
358
|
+
Your job:
|
|
359
|
+
- Take the context of a web feature or page,
|
|
360
|
+
- And generate high-quality UI test cases
|
|
361
|
+
- In a very strict Markdown format that another tool will parse.
|
|
362
|
+
|
|
363
|
+
You MUST follow this format exactly for each test case:
|
|
364
|
+
|
|
365
|
+
# <ID> \u2014 <Short title> @<tag1> @<tag2> ...
|
|
366
|
+
## Steps
|
|
367
|
+
1. <step>
|
|
368
|
+
2. <step>
|
|
369
|
+
|
|
370
|
+
## Expected Results
|
|
371
|
+
- <assertion>
|
|
372
|
+
- <assertion>
|
|
373
|
+
|
|
374
|
+
Rules:
|
|
375
|
+
- <ID> must be PREFIX-XXX where PREFIX is provided to you (e.g., AUTH, DASH, CART).
|
|
376
|
+
- XXX must be a 3-digit number starting from the startIndex provided in the context.
|
|
377
|
+
For example: DASH-005, DASH-006, DASH-007 if startIndex is 5.
|
|
378
|
+
- Use 1\u20133 tags per test (e.g., @smoke, @regression, @auth, @ui, @critical).
|
|
379
|
+
- "Steps" should describe user actions in sequence.
|
|
380
|
+
- "Expected Results" should describe verifiable outcomes (URL change, element visible, message shown, etc.).
|
|
381
|
+
- Do NOT add any explanation before or after the test cases.
|
|
382
|
+
- Output ONLY the test cases, back-to-back, in Markdown.
|
|
383
|
+
- No code blocks, no extra headings outside the pattern described.
|
|
384
|
+
`.trim();
|
|
385
|
+
}
|
|
386
|
+
function buildUserMessage(ctx) {
|
|
387
|
+
const lines = [];
|
|
388
|
+
lines.push(`App / Product description (optional):`);
|
|
389
|
+
lines.push(ctx.appDescription || "N/A");
|
|
390
|
+
lines.push("");
|
|
391
|
+
lines.push(`Feature or page to test:`);
|
|
392
|
+
lines.push(ctx.feature);
|
|
393
|
+
lines.push("");
|
|
394
|
+
if (ctx.url) {
|
|
395
|
+
lines.push(`Page URL:`);
|
|
396
|
+
lines.push(ctx.url);
|
|
397
|
+
lines.push("");
|
|
398
|
+
}
|
|
399
|
+
if (ctx.pageSummaryJson) {
|
|
400
|
+
lines.push(`Page structure summary (JSON):`);
|
|
401
|
+
lines.push("```json");
|
|
402
|
+
lines.push(JSON.stringify(ctx.pageSummaryJson, null, 2));
|
|
403
|
+
lines.push("```");
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
406
|
+
lines.push(`Test ID prefix to use: ${ctx.prefix}`);
|
|
407
|
+
lines.push(
|
|
408
|
+
`Start numbering from: ${String(ctx.startIndex).padStart(3, "0")}`
|
|
409
|
+
);
|
|
410
|
+
lines.push(`Number of test cases to generate: ${ctx.numCases}`);
|
|
411
|
+
lines.push("");
|
|
412
|
+
lines.push(`Important formatting rules:`);
|
|
413
|
+
lines.push(`- Use IDs like ${ctx.prefix}-NNN where NNN is 3-digit, sequential from the start index.`);
|
|
414
|
+
lines.push(`- Each test case must follow this pattern exactly:`);
|
|
415
|
+
lines.push(`# ${ctx.prefix}-NNN \u2014 <short title> @tag1 @tag2`);
|
|
416
|
+
lines.push(`## Steps`);
|
|
417
|
+
lines.push(`1. ...`);
|
|
418
|
+
lines.push(`2. ...`);
|
|
419
|
+
lines.push(``);
|
|
420
|
+
lines.push(`## Expected Results`);
|
|
421
|
+
lines.push(`- ...`);
|
|
422
|
+
lines.push(`- ...`);
|
|
423
|
+
lines.push("");
|
|
424
|
+
lines.push(`Do NOT add any explanation before or after the test cases. Only output the test cases.`);
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
async function generateTcMarkdownWithAi(ctx) {
|
|
428
|
+
const apiKey = process.env.CT_LLM_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
429
|
+
if (!apiKey) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
"No LLM API key found. Set CT_LLM_API_KEY or OPENAI_API_KEY."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const baseUrl = process.env.CT_LLM_BASE_URL || "https://api.openai.com/v1";
|
|
435
|
+
const model = process.env.CT_LLM_MODEL || "gpt-4.1-mini";
|
|
436
|
+
const system = buildSystemMessage();
|
|
437
|
+
const user = buildUserMessage(ctx);
|
|
438
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
headers: {
|
|
441
|
+
Authorization: `Bearer ${apiKey}`,
|
|
442
|
+
"Content-Type": "application/json"
|
|
443
|
+
},
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
model,
|
|
446
|
+
messages: [
|
|
447
|
+
{ role: "system", content: system },
|
|
448
|
+
{ role: "user", content: user }
|
|
449
|
+
],
|
|
450
|
+
temperature: 0.2
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
const text = await response.text();
|
|
455
|
+
throw new Error(
|
|
456
|
+
`LLM request failed: ${response.status} ${response.statusText} \u2014 ${text}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const json = await response.json();
|
|
460
|
+
const content = json.choices?.[0]?.message?.content?.trim() || "";
|
|
461
|
+
if (!content) {
|
|
462
|
+
throw new Error("LLM response had no content");
|
|
463
|
+
}
|
|
464
|
+
return content;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/core/scrape.ts
|
|
468
|
+
async function scrapePageSummary(url) {
|
|
469
|
+
const res = await fetch(url);
|
|
470
|
+
if (!res.ok) {
|
|
471
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
472
|
+
}
|
|
473
|
+
const html = await res.text();
|
|
474
|
+
const rawLength = html.length;
|
|
475
|
+
const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
476
|
+
const title = titleMatch?.[1]?.trim() || void 0;
|
|
477
|
+
const headings = Array.from(html.matchAll(/<(h1|h2)[^>]*>([^<]*)<\/\1>/gi)).map((m) => m[2].replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
478
|
+
const buttons = Array.from(html.matchAll(/<button[^>]*>([^<]*)<\/button>/gi)).map((m) => m[1].replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
479
|
+
const links = Array.from(html.matchAll(/<a[^>]*>([^<]*)<\/a>/gi)).map((m) => m[1].replace(/\s+/g, " ").trim()).filter(Boolean).slice(0, 50);
|
|
480
|
+
const inputs = [];
|
|
481
|
+
const labelMap = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const m of html.matchAll(/<label[^>]*for=["']?([^"'>\s]+)["']?[^>]*>([^<]*)<\/label>/gi)) {
|
|
483
|
+
const id = m[1];
|
|
484
|
+
const text = m[2].replace(/\s+/g, " ").trim();
|
|
485
|
+
if (id && text) labelMap.set(id, text);
|
|
486
|
+
}
|
|
487
|
+
for (const m of html.matchAll(/<input([^>]*)>/gi)) {
|
|
488
|
+
const attrs = m[1];
|
|
489
|
+
const nameMatch = attrs.match(/\bname=["']?([^"'>\s]+)["']?/i);
|
|
490
|
+
const idMatch = attrs.match(/\bid=["']?([^"'>\s]+)["']?/i);
|
|
491
|
+
const phMatch = attrs.match(/\bplaceholder=["']([^"']*)["']/i);
|
|
492
|
+
const id = idMatch?.[1];
|
|
493
|
+
const label = id ? labelMap.get(id) : void 0;
|
|
494
|
+
const ph = phMatch?.[1]?.trim();
|
|
495
|
+
const name = nameMatch?.[1];
|
|
496
|
+
const descriptor = label || ph || name;
|
|
497
|
+
if (descriptor) {
|
|
498
|
+
inputs.push(descriptor);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
url,
|
|
503
|
+
title,
|
|
504
|
+
headings: headings.slice(0, 20),
|
|
505
|
+
buttons: buttons.slice(0, 30),
|
|
506
|
+
links,
|
|
507
|
+
inputs: inputs.slice(0, 30),
|
|
508
|
+
rawLength
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/commands/tc.ts
|
|
513
|
+
function slugify(text) {
|
|
514
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tc";
|
|
515
|
+
}
|
|
516
|
+
function buildManualCasesMarkdown(opts) {
|
|
517
|
+
const { prefix, feature, url, numCases } = opts;
|
|
518
|
+
const startIndex = opts.startIndex ?? 1;
|
|
519
|
+
const lines = [];
|
|
520
|
+
for (let i = 0; i < numCases; i++) {
|
|
521
|
+
const idx = startIndex + i;
|
|
522
|
+
const id = `${prefix}-${String(idx).padStart(3, "0")}`;
|
|
523
|
+
const title = `${feature} - scenario ${idx}`;
|
|
524
|
+
const tags = "@regression @ui";
|
|
525
|
+
lines.push(`# ${id} \u2014 ${title} ${tags}`);
|
|
526
|
+
lines.push(`## Steps`);
|
|
527
|
+
lines.push(`1. Navigate to ${url ?? "<PAGE_URL>"}`);
|
|
528
|
+
lines.push(`2. Perform the main user action for this scenario`);
|
|
529
|
+
lines.push(`3. Observe the result`);
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push(`## Expected Results`);
|
|
532
|
+
lines.push(`- The page responds correctly for this scenario`);
|
|
533
|
+
lines.push(`- UI reflects the expected change`);
|
|
534
|
+
lines.push("");
|
|
535
|
+
lines.push("");
|
|
536
|
+
}
|
|
537
|
+
return lines.join("\n");
|
|
538
|
+
}
|
|
539
|
+
async function promptBasicQuestions(opts) {
|
|
540
|
+
if (opts.feature) {
|
|
541
|
+
let n = opts.numCases ?? 3;
|
|
542
|
+
if (n < 1) n = 3;
|
|
543
|
+
if (n > 10) n = 10;
|
|
544
|
+
return {
|
|
545
|
+
feature: opts.feature,
|
|
546
|
+
appDescription: opts.appDescription || "",
|
|
547
|
+
numCases: n,
|
|
548
|
+
url: opts.url
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const rl = createInterface({ input, output });
|
|
552
|
+
const feature = (await rl.question("\u{1F9E9} Feature or page to test: ")).trim();
|
|
553
|
+
const appDescription = (await rl.question("\u{1F4DD} Short app description (optional): ")).trim();
|
|
554
|
+
const numCasesRaw = (await rl.question("\u{1F522} How many test cases? (1-10) [3]: ")).trim();
|
|
555
|
+
rl.close();
|
|
556
|
+
let numCases = parseInt(numCasesRaw, 10);
|
|
557
|
+
if (isNaN(numCases) || numCases < 1) numCases = 3;
|
|
558
|
+
if (numCases > 10) numCases = 10;
|
|
559
|
+
return { feature, appDescription, numCases, url: opts.url };
|
|
560
|
+
}
|
|
561
|
+
function hasAiFlagInArgv() {
|
|
562
|
+
return process.argv.includes("--ai");
|
|
563
|
+
}
|
|
564
|
+
async function runTcInteractive(params) {
|
|
565
|
+
const { feature, appDescription, numCases, url } = await promptBasicQuestions({
|
|
566
|
+
url: params.url,
|
|
567
|
+
feature: params.feature,
|
|
568
|
+
appDescription: params.appDescription,
|
|
569
|
+
numCases: params.numCases
|
|
570
|
+
});
|
|
571
|
+
const prefix = inferPrefix({
|
|
572
|
+
featureText: feature,
|
|
573
|
+
url,
|
|
574
|
+
explicitPrefix: params.explicitPrefix
|
|
575
|
+
});
|
|
576
|
+
mkdirSync3("cases", { recursive: true });
|
|
577
|
+
const fileName = `${prefix.toLowerCase()}-${slugify(feature)}.md`;
|
|
578
|
+
const fullPath = join3("cases", fileName);
|
|
579
|
+
const useAi = hasAiFlagInArgv();
|
|
580
|
+
console.log(`\u2699\uFE0F Debug: useAi=${useAi}, argv=${JSON.stringify(process.argv)}`);
|
|
581
|
+
let markdown;
|
|
582
|
+
if (useAi) {
|
|
583
|
+
let pageSummaryJson = void 0;
|
|
584
|
+
if (url) {
|
|
585
|
+
try {
|
|
586
|
+
console.log(`\u{1F50D} Scraping page for AI context: ${url}`);
|
|
587
|
+
pageSummaryJson = await scrapePageSummary(url);
|
|
588
|
+
console.log(
|
|
589
|
+
`\u{1F50E} Scrape summary: title="${pageSummaryJson.title || ""}", headings=${pageSummaryJson.headings.length}, buttons=${pageSummaryJson.buttons.length}, inputs=${pageSummaryJson.inputs.length}`
|
|
590
|
+
);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
console.warn(
|
|
593
|
+
`\u26A0\uFE0F Failed to scrape ${url} (${e?.message || e}). Continuing without page summary.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
console.log("\u{1F916} AI: generating test cases...");
|
|
599
|
+
markdown = await generateTcMarkdownWithAi({
|
|
600
|
+
appDescription: appDescription || void 0,
|
|
601
|
+
feature,
|
|
602
|
+
url,
|
|
603
|
+
pageSummaryJson,
|
|
604
|
+
prefix,
|
|
605
|
+
startIndex: 1,
|
|
606
|
+
numCases
|
|
607
|
+
});
|
|
608
|
+
console.log("\u2705 AI: generated test case markdown.");
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.warn(
|
|
611
|
+
`\u26A0\uFE0F AI generation failed (${err?.message || err}). Falling back to manual templates.`
|
|
612
|
+
);
|
|
613
|
+
markdown = buildManualCasesMarkdown({
|
|
614
|
+
prefix,
|
|
615
|
+
feature,
|
|
616
|
+
url,
|
|
617
|
+
numCases,
|
|
618
|
+
startIndex: 1
|
|
619
|
+
});
|
|
620
|
+
console.log("\u{1F4DD} Manual: generated test case templates instead.");
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
markdown = buildManualCasesMarkdown({
|
|
624
|
+
prefix,
|
|
625
|
+
feature,
|
|
626
|
+
url,
|
|
627
|
+
numCases,
|
|
628
|
+
startIndex: 1
|
|
629
|
+
});
|
|
630
|
+
console.log("\u{1F4DD} Manual: generated test case templates (no --ai).");
|
|
631
|
+
}
|
|
632
|
+
writeFileSync3(fullPath, markdown);
|
|
633
|
+
console.log(`\u270D\uFE0F Wrote ${numCases} test case(s) \u2192 ${fullPath}`);
|
|
634
|
+
console.log("Next steps:");
|
|
635
|
+
console.log(" ct normalize ./cases --and-gen --lang ts");
|
|
636
|
+
console.log(" ct test");
|
|
637
|
+
}
|
|
638
|
+
function tcCmd() {
|
|
639
|
+
const root = new Command4("tc").description("Create CT-style test cases (Markdown) under ./cases").option("--ai", "Use AI if configured (BYO LLM API key).", false).option("--prefix <prefix>", "Explicit ID prefix, e.g. AUTH, DASH, CART").option("--feature <name>", "Feature name (non-interactive)").option("--desc <text>", "App description (non-interactive)").option("--count <n>", "Number of cases (non-interactive)", parseInt).action(async (opts) => {
|
|
640
|
+
await runTcInteractive({
|
|
641
|
+
url: void 0,
|
|
642
|
+
explicitPrefix: opts.prefix,
|
|
643
|
+
feature: opts.feature,
|
|
644
|
+
appDescription: opts.desc,
|
|
645
|
+
numCases: opts.count
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
root.command("url").argument("<url>", "Page URL to use as context").option("--ai", "Use AI if configured (BYO LLM API key).", false).option("--prefix <prefix>", "Explicit ID prefix, e.g. AUTH, DASH, CART").option("--feature <name>", "Feature name (non-interactive)").option("--desc <text>", "App description (non-interactive)").option("--count <n>", "Number of cases (non-interactive)", parseInt).description("Create test cases with awareness of a specific page URL").action(async (url, opts) => {
|
|
649
|
+
await runTcInteractive({
|
|
650
|
+
url,
|
|
651
|
+
explicitPrefix: opts.prefix,
|
|
652
|
+
feature: opts.feature,
|
|
653
|
+
appDescription: opts.desc,
|
|
654
|
+
numCases: opts.count
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
return root;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/commands/report.ts
|
|
661
|
+
import { Command as Command5 } from "commander";
|
|
662
|
+
import { spawn as spawn2 } from "child_process";
|
|
663
|
+
function reportCmd() {
|
|
664
|
+
const cmd = new Command5("report").description("Open the Playwright HTML report").action(() => {
|
|
665
|
+
console.log("\u{1F4CA} Opening Playwright HTML report...");
|
|
666
|
+
const child = spawn2(
|
|
667
|
+
"npx",
|
|
668
|
+
["playwright", "show-report"],
|
|
669
|
+
{
|
|
670
|
+
stdio: "inherit",
|
|
671
|
+
shell: process.platform === "win32"
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
child.on("exit", (code) => {
|
|
675
|
+
process.exit(code ?? 0);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
return cmd;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/commands/serve.ts
|
|
682
|
+
import { Command as Command6 } from "commander";
|
|
683
|
+
import { spawn as spawn3 } from "child_process";
|
|
684
|
+
import { existsSync as existsSync2 } from "fs";
|
|
685
|
+
import { join as join4 } from "path";
|
|
686
|
+
function serveCmd() {
|
|
687
|
+
const cmd = new Command6("serve").description("Serve the Allure report").action(() => {
|
|
688
|
+
console.log("\u{1F4CA} Serving Allure report...");
|
|
689
|
+
const localAllureBin = join4(process.cwd(), "node_modules", "allure-commandline", "bin", "allure");
|
|
690
|
+
let executable = "npx";
|
|
691
|
+
let args = ["allure", "serve", "./allure-results"];
|
|
692
|
+
if (existsSync2(localAllureBin)) {
|
|
693
|
+
executable = "node";
|
|
694
|
+
args = [localAllureBin, "serve", "./allure-results"];
|
|
695
|
+
}
|
|
696
|
+
console.log(`> ${executable} ${args.join(" ")}`);
|
|
697
|
+
const child = spawn3(
|
|
698
|
+
executable,
|
|
699
|
+
args,
|
|
700
|
+
{
|
|
701
|
+
stdio: "inherit",
|
|
702
|
+
shell: process.platform === "win32"
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
child.on("exit", (code) => {
|
|
706
|
+
process.exit(code ?? 0);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
return cmd;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/commands/flow.ts
|
|
713
|
+
import { Command as Command7 } from "commander";
|
|
714
|
+
import { spawn as spawn4 } from "child_process";
|
|
715
|
+
import { resolve as resolve3 } from "path";
|
|
716
|
+
function runStep(cmd, args, stepName) {
|
|
717
|
+
return new Promise((resolve4, reject) => {
|
|
718
|
+
console.log(`
|
|
719
|
+
\u{1F30A} Flow Step: ${stepName}`);
|
|
720
|
+
console.log(`> ${cmd} ${args.join(" ")}`);
|
|
721
|
+
const child = spawn4(cmd, args, {
|
|
722
|
+
stdio: "inherit",
|
|
723
|
+
shell: process.platform === "win32"
|
|
724
|
+
});
|
|
725
|
+
child.on("exit", (code) => {
|
|
726
|
+
if (code === 0) {
|
|
727
|
+
resolve4();
|
|
728
|
+
} else {
|
|
729
|
+
reject(new Error(`${stepName} failed with exit code ${code}`));
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function flowCmd() {
|
|
735
|
+
const cmd = new Command7("flow").description("End-to-end flow: Normalize -> Generate -> Run Tests").argument("[casesDir]", "Directory containing test cases", "./cases").option("--lang <lang>", "Target language (ts|js)", "ts").option("--no-run", "Skip running tests").action(async (casesDir, opts) => {
|
|
736
|
+
const cliBin = resolve3(process.argv[1]);
|
|
737
|
+
try {
|
|
738
|
+
await runStep(process.execPath, [cliBin, "normalize", casesDir], "Normalize Cases");
|
|
739
|
+
await runStep(process.execPath, [cliBin, "gen", "--lang", opts.lang], "Generate Tests");
|
|
740
|
+
if (opts.run) {
|
|
741
|
+
await runStep(process.execPath, [cliBin, "test"], "Run Playwright Tests");
|
|
742
|
+
} else {
|
|
743
|
+
console.log("\n\u23ED\uFE0F Skipping test execution (--no-run)");
|
|
744
|
+
}
|
|
745
|
+
console.log("\n\u2705 Flow completed successfully!");
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error(`
|
|
748
|
+
\u274C Flow failed: ${err.message}`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return cmd;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/commands/ci.ts
|
|
756
|
+
import { Command as Command8 } from "commander";
|
|
757
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync3 } from "fs";
|
|
758
|
+
import { join as join5 } from "path";
|
|
759
|
+
var WORKFLOW_CONTENT = `name: Playwright Tests
|
|
760
|
+
on:
|
|
761
|
+
push:
|
|
762
|
+
branches: [ main, master ]
|
|
763
|
+
pull_request:
|
|
764
|
+
branches: [ main, master ]
|
|
765
|
+
jobs:
|
|
766
|
+
test:
|
|
767
|
+
timeout-minutes: 60
|
|
768
|
+
runs-on: ubuntu-latest
|
|
769
|
+
steps:
|
|
770
|
+
- uses: actions/checkout@v4
|
|
771
|
+
- uses: actions/setup-node@v4
|
|
772
|
+
with:
|
|
773
|
+
node-version: lts/*
|
|
774
|
+
- name: Install dependencies
|
|
775
|
+
run: npm ci
|
|
776
|
+
- name: Install Playwright Browsers
|
|
777
|
+
run: npx playwright install --with-deps
|
|
778
|
+
- name: Run Playwright tests
|
|
779
|
+
run: npx playwright test
|
|
780
|
+
- uses: actions/upload-artifact@v4
|
|
781
|
+
if: always()
|
|
782
|
+
with:
|
|
783
|
+
name: playwright-report
|
|
784
|
+
path: playwright-report/
|
|
785
|
+
retention-days: 30
|
|
786
|
+
`;
|
|
787
|
+
function ciCmd() {
|
|
788
|
+
const cmd = new Command8("ci").description("Generate GitHub Actions workflow for CI").action(() => {
|
|
789
|
+
const githubDir = join5(process.cwd(), ".github");
|
|
790
|
+
const workflowsDir = join5(githubDir, "workflows");
|
|
791
|
+
const workflowFile = join5(workflowsDir, "cementic.yml");
|
|
792
|
+
console.log("\u{1F916} Setting up CI/CD workflow...");
|
|
793
|
+
if (!existsSync3(workflowsDir)) {
|
|
794
|
+
mkdirSync4(workflowsDir, { recursive: true });
|
|
795
|
+
console.log(`Created directory: ${workflowsDir}`);
|
|
796
|
+
}
|
|
797
|
+
if (existsSync3(workflowFile)) {
|
|
798
|
+
console.warn(`\u26A0\uFE0F Workflow file already exists at ${workflowFile}. Skipping.`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
writeFileSync4(workflowFile, WORKFLOW_CONTENT.trim() + "\n");
|
|
802
|
+
console.log(`\u2705 CI workflow generated at: ${workflowFile}`);
|
|
803
|
+
console.log("Next steps:");
|
|
804
|
+
console.log("1. Commit and push the new file");
|
|
805
|
+
console.log('2. Check the "Actions" tab in your GitHub repository');
|
|
806
|
+
});
|
|
807
|
+
return cmd;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/cli.ts
|
|
811
|
+
var program = new Command9();
|
|
812
|
+
program.name("cementic-test").description("CementicTest CLI: cases \u2192 normalized \u2192 POM tests \u2192 Playwright").version("0.2.0");
|
|
813
|
+
program.addCommand(newCmd());
|
|
814
|
+
program.addCommand(normalizeCmd());
|
|
815
|
+
program.addCommand(genCmd());
|
|
816
|
+
program.addCommand(testCmd());
|
|
817
|
+
program.addCommand(tcCmd());
|
|
818
|
+
program.addCommand(reportCmd());
|
|
819
|
+
program.addCommand(serveCmd());
|
|
820
|
+
program.addCommand(flowCmd());
|
|
821
|
+
program.addCommand(ciCmd());
|
|
822
|
+
program.parseAsync(process.argv);
|
|
823
|
+
//# sourceMappingURL=cli.js.map
|