@cementic/cementic-test 0.2.6 → 0.2.7
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 +216 -304
- package/dist/cli.js +666 -186
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command as Command9 } from "commander";
|
|
8
|
-
import { createRequire } from "module";
|
|
8
|
+
import { createRequire as createRequire2 } from "module";
|
|
9
9
|
|
|
10
10
|
// src/commands/new.ts
|
|
11
11
|
import { Command } from "commander";
|
|
@@ -17,6 +17,7 @@ import { dirname } from "path";
|
|
|
17
17
|
import { platform, release } from "os";
|
|
18
18
|
var __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
var __dirname = dirname(__filename);
|
|
20
|
+
var LEGACY_MACOS_DARWIN_MAJOR = 23;
|
|
20
21
|
function resolveTemplatePath(templateDir) {
|
|
21
22
|
const candidates = [
|
|
22
23
|
resolve(__dirname, `templates/${templateDir}`),
|
|
@@ -61,25 +62,22 @@ Examples:
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
copyRecursive(templatePath, projectPath);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
78
|
-
console.log("\u2705 Downgraded @playwright/test and allure-playwright for legacy macOS support.");
|
|
79
|
-
}
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.warn("\u26A0\uFE0F Failed to adjust package.json for OS compatibility:", err);
|
|
65
|
+
const legacyMacOs = isLegacyMacOs();
|
|
66
|
+
if (legacyMacOs) {
|
|
67
|
+
console.log("\u{1F34E} Detected macOS 13 or older. Adjusting versions for compatibility...");
|
|
68
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
69
|
+
if (existsSync(pkgJsonPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const pkgContent = readFileSync(pkgJsonPath, "utf-8");
|
|
72
|
+
const pkg = JSON.parse(pkgContent);
|
|
73
|
+
if (pkg.devDependencies) {
|
|
74
|
+
pkg.devDependencies["@playwright/test"] = "^1.48.2";
|
|
75
|
+
pkg.devDependencies["allure-playwright"] = "^2.15.1";
|
|
76
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
77
|
+
console.log("\u2705 Pinned Playwright packages for legacy macOS compatibility.");
|
|
82
78
|
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn("\u26A0\uFE0F Failed to adjust package.json for OS compatibility:", err);
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
}
|
|
@@ -93,17 +91,27 @@ Examples:
|
|
|
93
91
|
console.warn("\u26A0\uFE0F Failed to initialize git repository.");
|
|
94
92
|
}
|
|
95
93
|
console.log("\u{1F4E6} Installing dependencies...");
|
|
94
|
+
let dependenciesInstalled = false;
|
|
96
95
|
try {
|
|
97
96
|
execSync("npm install", { cwd: projectPath, stdio: "inherit" });
|
|
97
|
+
dependenciesInstalled = true;
|
|
98
98
|
} catch (e) {
|
|
99
99
|
console.error('\u274C Failed to install dependencies. Please run "npm install" manually.');
|
|
100
100
|
}
|
|
101
|
-
if (opts.browsers !== false) {
|
|
101
|
+
if (opts.browsers !== false && dependenciesInstalled) {
|
|
102
102
|
console.log("\u{1F310} Installing Playwright browsers...");
|
|
103
103
|
try {
|
|
104
|
-
|
|
104
|
+
if (legacyMacOs) {
|
|
105
|
+
console.log("\u26A0\uFE0F WebKit is not supported on this macOS version. Installing Chromium only...");
|
|
106
|
+
execSync("npx playwright install chromium", { cwd: projectPath, stdio: "inherit" });
|
|
107
|
+
console.log("\u2705 Chromium installed successfully.");
|
|
108
|
+
} else {
|
|
109
|
+
execSync("npx playwright install", { cwd: projectPath, stdio: "inherit" });
|
|
110
|
+
console.log("\u2705 Playwright browsers installed successfully.");
|
|
111
|
+
}
|
|
105
112
|
} catch (e) {
|
|
106
|
-
console.warn(
|
|
113
|
+
console.warn("\u26A0\uFE0F Browser installation did not complete. You can finish setup with:");
|
|
114
|
+
console.warn(" npx playwright install chromium");
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
console.log(`
|
|
@@ -118,6 +126,11 @@ Happy testing! \u{1F9EA}`);
|
|
|
118
126
|
});
|
|
119
127
|
return cmd;
|
|
120
128
|
}
|
|
129
|
+
function isLegacyMacOs() {
|
|
130
|
+
if (platform() !== "darwin") return false;
|
|
131
|
+
const majorVersion = parseInt(release().split(".")[0], 10);
|
|
132
|
+
return Number.isFinite(majorVersion) && majorVersion < LEGACY_MACOS_DARWIN_MAJOR;
|
|
133
|
+
}
|
|
121
134
|
|
|
122
135
|
// src/commands/normalize.ts
|
|
123
136
|
import { Command as Command2 } from "commander";
|
|
@@ -339,7 +352,7 @@ function testCmd() {
|
|
|
339
352
|
// src/commands/tc.ts
|
|
340
353
|
import { Command as Command4 } from "commander";
|
|
341
354
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
342
|
-
import { join as
|
|
355
|
+
import { join as join5 } from "path";
|
|
343
356
|
import { createInterface } from "readline/promises";
|
|
344
357
|
import { stdin as input, stdout as output } from "process";
|
|
345
358
|
|
|
@@ -393,6 +406,97 @@ function inferPrefix(params) {
|
|
|
393
406
|
return "TC";
|
|
394
407
|
}
|
|
395
408
|
|
|
409
|
+
// src/core/llm-provider.ts
|
|
410
|
+
var PROVIDER_ORDER = ["deepseek", "anthropic", "gemini", "qwen", "kimi", "openai"];
|
|
411
|
+
function resolveLlmProvider(env = process.env) {
|
|
412
|
+
const explicitProvider = (env.CT_LLM_PROVIDER ?? "").trim().toLowerCase();
|
|
413
|
+
const providers = buildProviderConfigs(env);
|
|
414
|
+
if (explicitProvider) {
|
|
415
|
+
if (!(explicitProvider in providers)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Unsupported CT_LLM_PROVIDER="${explicitProvider}". Use deepseek, anthropic, gemini, qwen, kimi, or openai.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const selected = providers[explicitProvider];
|
|
421
|
+
if (!selected.apiKey) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`CT_LLM_PROVIDER=${explicitProvider} is set but the matching API key is missing.
|
|
424
|
+
${buildProviderSetupHelp()}`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return selected;
|
|
428
|
+
}
|
|
429
|
+
for (const providerName of PROVIDER_ORDER) {
|
|
430
|
+
if (providers[providerName].apiKey) return providers[providerName];
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`No LLM API key found.
|
|
433
|
+
${buildProviderSetupHelp()}`);
|
|
434
|
+
}
|
|
435
|
+
function buildProviderSetupHelp() {
|
|
436
|
+
return [
|
|
437
|
+
" OpenAI: export OPENAI_API_KEY=your-key",
|
|
438
|
+
" Anthropic: export ANTHROPIC_API_KEY=your-key",
|
|
439
|
+
" Gemini: export GEMINI_API_KEY=your-key",
|
|
440
|
+
" DeepSeek: export DEEPSEEK_API_KEY=your-key",
|
|
441
|
+
" Qwen: export QWEN_API_KEY=your-key",
|
|
442
|
+
" Kimi: export KIMI_API_KEY=your-key",
|
|
443
|
+
" Generic: export CT_LLM_API_KEY=your-key",
|
|
444
|
+
" export CT_LLM_BASE_URL=https://your-openai-compatible-endpoint/v1"
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
function buildProviderConfigs(env) {
|
|
448
|
+
return {
|
|
449
|
+
deepseek: {
|
|
450
|
+
provider: "deepseek",
|
|
451
|
+
displayName: "DeepSeek",
|
|
452
|
+
apiKey: env.DEEPSEEK_API_KEY ?? env.CT_DEEPSEEK_API_KEY ?? "",
|
|
453
|
+
model: env.CT_LLM_MODEL ?? env.DEEPSEEK_MODEL ?? "deepseek-chat",
|
|
454
|
+
baseUrl: env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com/v1",
|
|
455
|
+
transport: "openai-compatible"
|
|
456
|
+
},
|
|
457
|
+
anthropic: {
|
|
458
|
+
provider: "anthropic",
|
|
459
|
+
displayName: "Claude",
|
|
460
|
+
apiKey: env.ANTHROPIC_API_KEY ?? env.CT_ANTHROPIC_API_KEY ?? "",
|
|
461
|
+
model: env.CT_LLM_MODEL ?? env.ANTHROPIC_MODEL ?? "claude-sonnet-4-5",
|
|
462
|
+
baseUrl: "https://api.anthropic.com",
|
|
463
|
+
transport: "anthropic"
|
|
464
|
+
},
|
|
465
|
+
gemini: {
|
|
466
|
+
provider: "gemini",
|
|
467
|
+
displayName: "Gemini",
|
|
468
|
+
apiKey: env.GEMINI_API_KEY ?? env.GOOGLE_API_KEY ?? "",
|
|
469
|
+
model: env.CT_LLM_MODEL ?? env.GEMINI_MODEL ?? "gemini-2.5-flash",
|
|
470
|
+
baseUrl: env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
471
|
+
transport: "openai-compatible"
|
|
472
|
+
},
|
|
473
|
+
qwen: {
|
|
474
|
+
provider: "qwen",
|
|
475
|
+
displayName: "Qwen",
|
|
476
|
+
apiKey: env.QWEN_API_KEY ?? "",
|
|
477
|
+
model: env.CT_LLM_MODEL ?? env.QWEN_MODEL ?? "qwen-plus",
|
|
478
|
+
baseUrl: env.QWEN_BASE_URL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
479
|
+
transport: "openai-compatible"
|
|
480
|
+
},
|
|
481
|
+
kimi: {
|
|
482
|
+
provider: "kimi",
|
|
483
|
+
displayName: "Kimi",
|
|
484
|
+
apiKey: env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? "",
|
|
485
|
+
model: env.CT_LLM_MODEL ?? env.KIMI_MODEL ?? "moonshot-v1-8k",
|
|
486
|
+
baseUrl: env.KIMI_BASE_URL ?? "https://api.moonshot.ai/v1",
|
|
487
|
+
transport: "openai-compatible"
|
|
488
|
+
},
|
|
489
|
+
openai: {
|
|
490
|
+
provider: "openai",
|
|
491
|
+
displayName: env.CT_LLM_API_KEY && env.CT_LLM_BASE_URL ? "OpenAI-compatible" : "OpenAI",
|
|
492
|
+
apiKey: env.CT_LLM_API_KEY ?? env.OPENAI_API_KEY ?? "",
|
|
493
|
+
model: env.CT_LLM_MODEL ?? env.OPENAI_MODEL ?? "gpt-4o-mini",
|
|
494
|
+
baseUrl: env.CT_LLM_BASE_URL ?? "https://api.openai.com/v1",
|
|
495
|
+
transport: "openai-compatible"
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
396
500
|
// src/core/llm.ts
|
|
397
501
|
function buildSystemMessage() {
|
|
398
502
|
return `
|
|
@@ -472,35 +576,6 @@ function buildUserMessage(ctx) {
|
|
|
472
576
|
lines.push(`Output ONLY the test cases. No explanation, no preamble.`);
|
|
473
577
|
return lines.join("\n");
|
|
474
578
|
}
|
|
475
|
-
function detectProvider() {
|
|
476
|
-
const anthropicKey = process.env.ANTHROPIC_API_KEY ?? process.env.CT_ANTHROPIC_API_KEY ?? "";
|
|
477
|
-
const openaiKey = process.env.CT_LLM_API_KEY ?? process.env.OPENAI_API_KEY ?? "";
|
|
478
|
-
const explicitProvider = (process.env.CT_LLM_PROVIDER ?? "").toLowerCase();
|
|
479
|
-
if (explicitProvider === "anthropic" || anthropicKey && explicitProvider !== "openai") {
|
|
480
|
-
if (!anthropicKey) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
"CT_LLM_PROVIDER=anthropic but no ANTHROPIC_API_KEY found.\nSet ANTHROPIC_API_KEY=your-key or switch to OPENAI_API_KEY."
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
return {
|
|
486
|
-
provider: "anthropic",
|
|
487
|
-
apiKey: anthropicKey,
|
|
488
|
-
model: process.env.CT_LLM_MODEL ?? "claude-sonnet-4-5",
|
|
489
|
-
baseUrl: "https://api.anthropic.com"
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
if (openaiKey) {
|
|
493
|
-
return {
|
|
494
|
-
provider: "openai",
|
|
495
|
-
apiKey: openaiKey,
|
|
496
|
-
model: process.env.CT_LLM_MODEL ?? "gpt-4o-mini",
|
|
497
|
-
baseUrl: process.env.CT_LLM_BASE_URL ?? "https://api.openai.com/v1"
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
throw new Error(
|
|
501
|
-
"No LLM API key found.\n For Claude: export ANTHROPIC_API_KEY=your-key\n For OpenAI: export OPENAI_API_KEY=your-key\n Any OpenAI-compatible endpoint: set CT_LLM_API_KEY + CT_LLM_BASE_URL"
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
579
|
async function callAnthropic(apiKey, model, system, user) {
|
|
505
580
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
506
581
|
method: "POST",
|
|
@@ -525,7 +600,7 @@ async function callAnthropic(apiKey, model, system, user) {
|
|
|
525
600
|
if (!content) throw new Error("Anthropic response had no content");
|
|
526
601
|
return content;
|
|
527
602
|
}
|
|
528
|
-
async function
|
|
603
|
+
async function callOpenAiCompatible(apiKey, model, baseUrl, displayName, system, user) {
|
|
529
604
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
530
605
|
method: "POST",
|
|
531
606
|
headers: {
|
|
@@ -543,7 +618,7 @@ async function callOpenAi(apiKey, model, baseUrl, system, user) {
|
|
|
543
618
|
});
|
|
544
619
|
if (!response.ok) {
|
|
545
620
|
const text = await response.text();
|
|
546
|
-
throw new Error(
|
|
621
|
+
throw new Error(`${displayName} API error ${response.status}: ${text}`);
|
|
547
622
|
}
|
|
548
623
|
const json = await response.json();
|
|
549
624
|
const content = json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
@@ -551,90 +626,42 @@ async function callOpenAi(apiKey, model, baseUrl, system, user) {
|
|
|
551
626
|
return content;
|
|
552
627
|
}
|
|
553
628
|
async function generateTcMarkdownWithAi(ctx) {
|
|
554
|
-
const
|
|
629
|
+
const providerConfig = resolveLlmProvider();
|
|
555
630
|
const system = buildSystemMessage();
|
|
556
631
|
const user = buildUserMessage(ctx);
|
|
557
|
-
console.log(`\u{1F916} Using ${
|
|
558
|
-
if (
|
|
559
|
-
return callAnthropic(apiKey, model, system, user);
|
|
632
|
+
console.log(`\u{1F916} Using ${providerConfig.displayName} (${providerConfig.model})`);
|
|
633
|
+
if (providerConfig.transport === "anthropic") {
|
|
634
|
+
return callAnthropic(providerConfig.apiKey, providerConfig.model, system, user);
|
|
560
635
|
}
|
|
561
|
-
return
|
|
636
|
+
return callOpenAiCompatible(
|
|
637
|
+
providerConfig.apiKey,
|
|
638
|
+
providerConfig.model,
|
|
639
|
+
providerConfig.baseUrl ?? "https://api.openai.com/v1",
|
|
640
|
+
providerConfig.displayName,
|
|
641
|
+
system,
|
|
642
|
+
user
|
|
643
|
+
);
|
|
562
644
|
}
|
|
563
645
|
|
|
564
646
|
// src/core/analyse.ts
|
|
565
647
|
async function analyseElements(elementMap, options = {}) {
|
|
566
648
|
const { verbose = false } = options;
|
|
567
|
-
const
|
|
649
|
+
const providerConfig = resolveLlmProvider();
|
|
568
650
|
log(verbose, `
|
|
569
|
-
[analyse] Sending capture to ${displayName} (${model})`);
|
|
651
|
+
[analyse] Sending capture to ${providerConfig.displayName} (${providerConfig.model})`);
|
|
570
652
|
const systemPrompt = buildSystemPrompt();
|
|
571
653
|
const userPrompt = buildUserPrompt(elementMap);
|
|
572
|
-
const rawResponse =
|
|
654
|
+
const rawResponse = providerConfig.transport === "anthropic" ? await callAnthropic2(providerConfig.apiKey, providerConfig.model, systemPrompt, userPrompt) : await callOpenAiCompatible2(
|
|
655
|
+
providerConfig.apiKey,
|
|
656
|
+
providerConfig.model,
|
|
657
|
+
providerConfig.baseUrl ?? "https://api.openai.com/v1",
|
|
658
|
+
providerConfig.displayName,
|
|
659
|
+
systemPrompt,
|
|
660
|
+
userPrompt
|
|
661
|
+
);
|
|
573
662
|
const parsed = parseAnalysisJson(rawResponse);
|
|
574
663
|
return sanitizeAnalysis(parsed, elementMap);
|
|
575
664
|
}
|
|
576
|
-
function detectProvider2() {
|
|
577
|
-
const explicitProvider = (process.env.CT_LLM_PROVIDER ?? "").toLowerCase();
|
|
578
|
-
const providers = {
|
|
579
|
-
deepseek: {
|
|
580
|
-
provider: "deepseek",
|
|
581
|
-
displayName: "DeepSeek",
|
|
582
|
-
apiKey: process.env.DEEPSEEK_API_KEY ?? process.env.CT_DEEPSEEK_API_KEY ?? "",
|
|
583
|
-
model: process.env.CT_LLM_MODEL ?? process.env.DEEPSEEK_MODEL ?? "deepseek-chat",
|
|
584
|
-
baseUrl: process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com/v1"
|
|
585
|
-
},
|
|
586
|
-
anthropic: {
|
|
587
|
-
provider: "anthropic",
|
|
588
|
-
displayName: "Claude",
|
|
589
|
-
apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.CT_ANTHROPIC_API_KEY ?? "",
|
|
590
|
-
model: process.env.CT_LLM_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-5"
|
|
591
|
-
},
|
|
592
|
-
gemini: {
|
|
593
|
-
provider: "gemini",
|
|
594
|
-
displayName: "Gemini",
|
|
595
|
-
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? "",
|
|
596
|
-
model: process.env.CT_LLM_MODEL ?? process.env.GEMINI_MODEL ?? "gemini-2.5-flash",
|
|
597
|
-
baseUrl: process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta/openai"
|
|
598
|
-
},
|
|
599
|
-
qwen: {
|
|
600
|
-
provider: "qwen",
|
|
601
|
-
displayName: "Qwen",
|
|
602
|
-
apiKey: process.env.QWEN_API_KEY ?? "",
|
|
603
|
-
model: process.env.CT_LLM_MODEL ?? process.env.QWEN_MODEL ?? "qwen-plus",
|
|
604
|
-
baseUrl: process.env.QWEN_BASE_URL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
605
|
-
},
|
|
606
|
-
kimi: {
|
|
607
|
-
provider: "kimi",
|
|
608
|
-
displayName: "Kimi",
|
|
609
|
-
apiKey: process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "",
|
|
610
|
-
model: process.env.CT_LLM_MODEL ?? process.env.KIMI_MODEL ?? "moonshot-v1-8k",
|
|
611
|
-
baseUrl: process.env.KIMI_BASE_URL ?? "https://api.moonshot.ai/v1"
|
|
612
|
-
},
|
|
613
|
-
openai: {
|
|
614
|
-
provider: "openai",
|
|
615
|
-
displayName: "OpenAI-compatible",
|
|
616
|
-
apiKey: process.env.OPENAI_API_KEY ?? process.env.CT_LLM_API_KEY ?? "",
|
|
617
|
-
model: process.env.CT_LLM_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini",
|
|
618
|
-
baseUrl: process.env.CT_LLM_BASE_URL ?? "https://api.openai.com/v1"
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
if (explicitProvider) {
|
|
622
|
-
if (!(explicitProvider in providers)) {
|
|
623
|
-
throw new Error(`Unsupported CT_LLM_PROVIDER="${explicitProvider}". Use deepseek, anthropic, gemini, qwen, kimi, or openai.`);
|
|
624
|
-
}
|
|
625
|
-
const selected = providers[explicitProvider];
|
|
626
|
-
if (!selected.apiKey) {
|
|
627
|
-
throw new Error(`CT_LLM_PROVIDER=${explicitProvider} is set but the matching API key is missing.`);
|
|
628
|
-
}
|
|
629
|
-
return selected;
|
|
630
|
-
}
|
|
631
|
-
for (const providerName of ["deepseek", "anthropic", "gemini", "qwen", "kimi", "openai"]) {
|
|
632
|
-
if (providers[providerName].apiKey) return providers[providerName];
|
|
633
|
-
}
|
|
634
|
-
throw new Error(
|
|
635
|
-
"No LLM API key found.\n DeepSeek: export DEEPSEEK_API_KEY=your-key\n Anthropic: export ANTHROPIC_API_KEY=your-key\n Gemini: export GEMINI_API_KEY=your-key\n Qwen: export QWEN_API_KEY=your-key\n Kimi: export KIMI_API_KEY=your-key\n OpenAI: export OPENAI_API_KEY=your-key"
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
665
|
function buildSystemPrompt() {
|
|
639
666
|
return `
|
|
640
667
|
You are a senior QA automation engineer and Playwright expert working on CementicTest.
|
|
@@ -651,6 +678,8 @@ RULES:
|
|
|
651
678
|
6. Do not invent redirect targets, success pages, error text, password clearing, or security scenarios unless the capture explicitly supports them.
|
|
652
679
|
7. If no status or alert region was captured, avoid scenarios that depend on unseen server-side validation messages.
|
|
653
680
|
8. Prefer 3 to 5 realistic scenarios.
|
|
681
|
+
9. The "selector" field must be the raw selector expression from the ElementMap, such as locator("#username") or getByRole('button', { name: "Login" }). Never wrap selectors in page. or page.locator("...") inside JSON.
|
|
682
|
+
10. If a page clearly contains login or auth fields, do not create submit scenarios that only click the button. Include realistic fill steps for captured username/email and password inputs.
|
|
654
683
|
|
|
655
684
|
OUTPUT SCHEMA:
|
|
656
685
|
{
|
|
@@ -689,6 +718,7 @@ OUTPUT SCHEMA:
|
|
|
689
718
|
}
|
|
690
719
|
function buildUserPrompt(elementMap) {
|
|
691
720
|
const lines = [];
|
|
721
|
+
const authElements = detectAuthElements(elementMap);
|
|
692
722
|
lines.push("PAGE INFORMATION");
|
|
693
723
|
lines.push(`URL: ${elementMap.url}`);
|
|
694
724
|
lines.push(`Title: ${elementMap.title}`);
|
|
@@ -721,6 +751,10 @@ function buildUserPrompt(elementMap) {
|
|
|
721
751
|
lines.push(` - Status or alert regions captured: ${statusCount}`);
|
|
722
752
|
lines.push(" - If no redirect target is explicitly captured, do not assert a destination path.");
|
|
723
753
|
lines.push(" - If no status region was captured, avoid exact server-side credential error claims.");
|
|
754
|
+
if (authElements.usernameInput && authElements.passwordInput && authElements.submitButton) {
|
|
755
|
+
lines.push(" - This page contains a captured auth form. Include fill steps for the username/email and password fields in credential-submission scenarios.");
|
|
756
|
+
lines.push(" - For auth pages without captured post-submit evidence, prefer evidence-backed form-state assertions over invented success redirects.");
|
|
757
|
+
}
|
|
724
758
|
lines.push("");
|
|
725
759
|
lines.push("Generate only the JSON response.");
|
|
726
760
|
return lines.join("\n");
|
|
@@ -748,7 +782,7 @@ async function callAnthropic2(apiKey, model, systemPrompt, userPrompt) {
|
|
|
748
782
|
if (!content) throw new Error("Anthropic returned empty content");
|
|
749
783
|
return content;
|
|
750
784
|
}
|
|
751
|
-
async function
|
|
785
|
+
async function callOpenAiCompatible2(apiKey, model, baseUrl, displayName, systemPrompt, userPrompt) {
|
|
752
786
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
753
787
|
method: "POST",
|
|
754
788
|
headers: {
|
|
@@ -765,7 +799,7 @@ async function callOpenAi2(apiKey, model, baseUrl, systemPrompt, userPrompt) {
|
|
|
765
799
|
})
|
|
766
800
|
});
|
|
767
801
|
if (!response.ok) {
|
|
768
|
-
throw new Error(
|
|
802
|
+
throw new Error(`${displayName} API ${response.status}: ${await response.text()}`);
|
|
769
803
|
}
|
|
770
804
|
const json = await response.json();
|
|
771
805
|
const content = json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
@@ -786,9 +820,14 @@ ${raw.slice(0, 500)}`
|
|
|
786
820
|
}
|
|
787
821
|
}
|
|
788
822
|
function sanitizeAnalysis(analysis, elementMap) {
|
|
789
|
-
const selectors = new Set(
|
|
823
|
+
const selectors = /* @__PURE__ */ new Set();
|
|
824
|
+
for (const element of elementMap.elements) {
|
|
825
|
+
selectors.add(element.selector);
|
|
826
|
+
for (const alt of element.selectorAlt) selectors.add(alt);
|
|
827
|
+
}
|
|
790
828
|
const rawScenarios = Array.isArray(analysis.scenarios) ? analysis.scenarios : [];
|
|
791
829
|
const currentUrl = new URL(elementMap.url);
|
|
830
|
+
const authElements = detectAuthElements(elementMap);
|
|
792
831
|
const knownPaths = /* @__PURE__ */ new Set([
|
|
793
832
|
currentUrl.pathname,
|
|
794
833
|
...elementMap.elements.filter((element) => element.category === "link").map((element) => {
|
|
@@ -801,7 +840,7 @@ function sanitizeAnalysis(analysis, elementMap) {
|
|
|
801
840
|
}
|
|
802
841
|
}).filter(Boolean)
|
|
803
842
|
]);
|
|
804
|
-
const sanitizedScenarios = rawScenarios.map((scenario) => normalizeScenario(scenario, selectors)).filter((scenario) => scenario.steps.length > 0 && scenario.assertions.length > 0).map((scenario) => ({
|
|
843
|
+
const sanitizedScenarios = rawScenarios.map((scenario) => normalizeScenario(scenario, selectors, elementMap.url)).filter((scenario) => scenario.steps.length > 0 && scenario.assertions.length > 0).map((scenario) => ({
|
|
805
844
|
...scenario,
|
|
806
845
|
tags: scenario.tags.map(normalizeTag),
|
|
807
846
|
assertions: scenario.assertions.filter((assertion) => {
|
|
@@ -812,7 +851,8 @@ function sanitizeAnalysis(analysis, elementMap) {
|
|
|
812
851
|
return knownPaths.has(pathMatch[0]);
|
|
813
852
|
})
|
|
814
853
|
})).filter((scenario) => scenario.assertions.length > 0).slice(0, 5);
|
|
815
|
-
const
|
|
854
|
+
const useAuthFallback = shouldUseAuthFallback(authElements, sanitizedScenarios);
|
|
855
|
+
const finalScenarios = useAuthFallback ? buildAuthFallbackScenarios(elementMap, analysis.suggestedPrefix || inferPrefix2(elementMap), authElements) : sanitizedScenarios.length > 0 ? sanitizedScenarios : buildFallbackScenarios(elementMap, analysis.suggestedPrefix || "FLOW");
|
|
816
856
|
return {
|
|
817
857
|
...analysis,
|
|
818
858
|
url: analysis.url || elementMap.url,
|
|
@@ -821,22 +861,53 @@ function sanitizeAnalysis(analysis, elementMap) {
|
|
|
821
861
|
scenarios: finalScenarios,
|
|
822
862
|
analysisNotes: [
|
|
823
863
|
analysis.analysisNotes,
|
|
864
|
+
useAuthFallback ? "Replaced low-evidence auth scenarios with deterministic evidence-backed auth coverage." : "",
|
|
824
865
|
`Sanitized to ${finalScenarios.length} evidence-backed scenario(s) from ${rawScenarios.length} raw scenario(s).`
|
|
825
866
|
].filter(Boolean).join(" "),
|
|
826
867
|
audioSummary: analysis.audioSummary || buildAudioSummary(analysis.feature || inferFeatureName(elementMap), finalScenarios)
|
|
827
868
|
};
|
|
828
869
|
}
|
|
829
|
-
function normalizeScenario(candidate, selectors) {
|
|
870
|
+
function normalizeScenario(candidate, selectors, url) {
|
|
871
|
+
const steps = Array.isArray(candidate?.steps) ? candidate.steps.map((step) => normalizeStep(step, selectors)).filter(Boolean) : [];
|
|
872
|
+
const assertions = Array.isArray(candidate?.assertions) ? candidate.assertions.map((assertion) => normalizeAssertion(assertion, selectors)).filter(Boolean) : [];
|
|
873
|
+
const normalizedSteps = ensureNavigateStep(steps, url);
|
|
830
874
|
return {
|
|
831
875
|
id: candidate?.id ?? "FLOW-001",
|
|
832
876
|
title: candidate?.title ?? "Captured page flow",
|
|
833
877
|
tags: Array.isArray(candidate?.tags) ? candidate.tags : [],
|
|
834
|
-
steps:
|
|
835
|
-
assertions
|
|
878
|
+
steps: normalizedSteps,
|
|
879
|
+
assertions,
|
|
836
880
|
narrator: candidate?.narrator ?? "Let us run this captured test flow.",
|
|
837
881
|
codeLevel: candidate?.codeLevel ?? "beginner"
|
|
838
882
|
};
|
|
839
883
|
}
|
|
884
|
+
function normalizeStep(candidate, selectors) {
|
|
885
|
+
if (!candidate) return null;
|
|
886
|
+
const selector = String(candidate.selector ?? "").trim();
|
|
887
|
+
if (selector !== "page" && !selectors.has(selector)) return null;
|
|
888
|
+
const action = String(candidate.action ?? "").trim();
|
|
889
|
+
if (!["navigate", "fill", "click", "select", "check", "keyboard", "hover"].includes(action)) return null;
|
|
890
|
+
return {
|
|
891
|
+
action,
|
|
892
|
+
selector,
|
|
893
|
+
value: String(candidate.value ?? ""),
|
|
894
|
+
human: String(candidate.human ?? "").trim() || defaultStepHuman(action, selector, String(candidate.value ?? ""))
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function normalizeAssertion(candidate, selectors) {
|
|
898
|
+
if (!candidate) return null;
|
|
899
|
+
const selector = String(candidate.selector ?? "").trim();
|
|
900
|
+
if (selector !== "page" && !selectors.has(selector)) return null;
|
|
901
|
+
const assertion = {
|
|
902
|
+
type: String(candidate.type ?? "visible").trim() || "visible",
|
|
903
|
+
selector,
|
|
904
|
+
expected: String(candidate.expected ?? "").trim(),
|
|
905
|
+
human: String(candidate.human ?? "").trim() || "Expected state is verified",
|
|
906
|
+
playwright: String(candidate.playwright ?? "").trim()
|
|
907
|
+
};
|
|
908
|
+
assertion.playwright = normalizePlaywrightAssertion(assertion);
|
|
909
|
+
return assertion;
|
|
910
|
+
}
|
|
840
911
|
function buildFallbackScenarios(elementMap, prefix) {
|
|
841
912
|
const heading = elementMap.elements.find((element) => element.category === "heading");
|
|
842
913
|
const emailInput = elementMap.elements.find((element) => element.category === "input" && (element.attributes.type === "email" || /email/.test(`${element.name ?? ""} ${String(element.attributes.label ?? "")}`.toLowerCase())));
|
|
@@ -918,6 +989,163 @@ function buildFallbackScenarios(elementMap, prefix) {
|
|
|
918
989
|
}
|
|
919
990
|
return scenarios.slice(0, 5);
|
|
920
991
|
}
|
|
992
|
+
function buildAuthFallbackScenarios(elementMap, prefix, authElements) {
|
|
993
|
+
const { usernameInput, passwordInput, submitButton, heading } = authElements;
|
|
994
|
+
if (!usernameInput || !passwordInput || !submitButton) {
|
|
995
|
+
return buildFallbackScenarios(elementMap, prefix);
|
|
996
|
+
}
|
|
997
|
+
const scenarios = [];
|
|
998
|
+
const tag = (value) => normalizeTag(value);
|
|
999
|
+
const nextId = (index) => `${prefix}-${String(index).padStart(3, "0")}`;
|
|
1000
|
+
const usernameValue = inferAuthValue(usernameInput, "username");
|
|
1001
|
+
const passwordValue = inferAuthValue(passwordInput, "password");
|
|
1002
|
+
scenarios.push({
|
|
1003
|
+
id: nextId(1),
|
|
1004
|
+
title: "Login form renders expected controls",
|
|
1005
|
+
tags: [tag("smoke"), tag("auth")],
|
|
1006
|
+
steps: [
|
|
1007
|
+
{
|
|
1008
|
+
action: "navigate",
|
|
1009
|
+
selector: "page",
|
|
1010
|
+
value: elementMap.url,
|
|
1011
|
+
human: "Navigate to the login page"
|
|
1012
|
+
}
|
|
1013
|
+
],
|
|
1014
|
+
assertions: [
|
|
1015
|
+
...heading ? [{
|
|
1016
|
+
type: "visible",
|
|
1017
|
+
selector: heading.selector,
|
|
1018
|
+
expected: "visible",
|
|
1019
|
+
human: `${heading.name || "Login heading"} is visible`,
|
|
1020
|
+
playwright: visibleAssertion(heading.selector)
|
|
1021
|
+
}] : [],
|
|
1022
|
+
{
|
|
1023
|
+
type: "visible",
|
|
1024
|
+
selector: usernameInput.selector,
|
|
1025
|
+
expected: "visible",
|
|
1026
|
+
human: `${usernameInput.name || "Username field"} is visible`,
|
|
1027
|
+
playwright: visibleAssertion(usernameInput.selector)
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
type: "visible",
|
|
1031
|
+
selector: passwordInput.selector,
|
|
1032
|
+
expected: "visible",
|
|
1033
|
+
human: `${passwordInput.name || "Password field"} is visible`,
|
|
1034
|
+
playwright: visibleAssertion(passwordInput.selector)
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
type: "visible",
|
|
1038
|
+
selector: submitButton.selector,
|
|
1039
|
+
expected: "visible",
|
|
1040
|
+
human: `${submitButton.name || "Submit button"} is visible`,
|
|
1041
|
+
playwright: visibleAssertion(submitButton.selector)
|
|
1042
|
+
}
|
|
1043
|
+
],
|
|
1044
|
+
narrator: "We first confirm that the captured login form is present and interactive.",
|
|
1045
|
+
codeLevel: "beginner"
|
|
1046
|
+
});
|
|
1047
|
+
scenarios.push({
|
|
1048
|
+
id: nextId(2),
|
|
1049
|
+
title: "User can enter credentials before submission",
|
|
1050
|
+
tags: [tag("auth"), tag("happy-path")],
|
|
1051
|
+
steps: [
|
|
1052
|
+
{
|
|
1053
|
+
action: "navigate",
|
|
1054
|
+
selector: "page",
|
|
1055
|
+
value: elementMap.url,
|
|
1056
|
+
human: "Navigate to the login page"
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
action: "fill",
|
|
1060
|
+
selector: usernameInput.selector,
|
|
1061
|
+
value: usernameValue,
|
|
1062
|
+
human: "Fill in the username field"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
action: "fill",
|
|
1066
|
+
selector: passwordInput.selector,
|
|
1067
|
+
value: passwordValue,
|
|
1068
|
+
human: "Fill in the password field"
|
|
1069
|
+
}
|
|
1070
|
+
],
|
|
1071
|
+
assertions: [
|
|
1072
|
+
{
|
|
1073
|
+
type: "value",
|
|
1074
|
+
selector: usernameInput.selector,
|
|
1075
|
+
expected: usernameValue,
|
|
1076
|
+
human: "Username input contains the entered value",
|
|
1077
|
+
playwright: valueAssertion(usernameInput.selector, usernameValue)
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
type: "value",
|
|
1081
|
+
selector: passwordInput.selector,
|
|
1082
|
+
expected: passwordValue,
|
|
1083
|
+
human: "Password input contains the entered value",
|
|
1084
|
+
playwright: valueAssertion(passwordInput.selector, passwordValue)
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
type: "visible",
|
|
1088
|
+
selector: submitButton.selector,
|
|
1089
|
+
expected: "visible",
|
|
1090
|
+
human: `${submitButton.name || "Submit button"} remains visible`,
|
|
1091
|
+
playwright: visibleAssertion(submitButton.selector)
|
|
1092
|
+
}
|
|
1093
|
+
],
|
|
1094
|
+
narrator: "Next we verify that both credential fields accept input before we submit the form.",
|
|
1095
|
+
codeLevel: "beginner"
|
|
1096
|
+
});
|
|
1097
|
+
scenarios.push({
|
|
1098
|
+
id: nextId(3),
|
|
1099
|
+
title: "Submitting without a password keeps the login form visible",
|
|
1100
|
+
tags: [tag("auth"), tag("negative")],
|
|
1101
|
+
steps: [
|
|
1102
|
+
{
|
|
1103
|
+
action: "navigate",
|
|
1104
|
+
selector: "page",
|
|
1105
|
+
value: elementMap.url,
|
|
1106
|
+
human: "Navigate to the login page"
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
action: "fill",
|
|
1110
|
+
selector: usernameInput.selector,
|
|
1111
|
+
value: usernameValue,
|
|
1112
|
+
human: "Fill in the username field"
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
action: "click",
|
|
1116
|
+
selector: submitButton.selector,
|
|
1117
|
+
value: "",
|
|
1118
|
+
human: "Click the login button"
|
|
1119
|
+
}
|
|
1120
|
+
],
|
|
1121
|
+
assertions: [
|
|
1122
|
+
{
|
|
1123
|
+
type: "url",
|
|
1124
|
+
selector: "page",
|
|
1125
|
+
expected: elementMap.url,
|
|
1126
|
+
human: "User remains on the same login URL",
|
|
1127
|
+
playwright: `await expect(page).toHaveURL('${elementMap.url}');`
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
type: "visible",
|
|
1131
|
+
selector: passwordInput.selector,
|
|
1132
|
+
expected: "visible",
|
|
1133
|
+
human: "Password field remains visible for correction",
|
|
1134
|
+
playwright: visibleAssertion(passwordInput.selector)
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
type: "visible",
|
|
1138
|
+
selector: submitButton.selector,
|
|
1139
|
+
expected: "visible",
|
|
1140
|
+
human: `${submitButton.name || "Login button"} remains visible`,
|
|
1141
|
+
playwright: visibleAssertion(submitButton.selector)
|
|
1142
|
+
}
|
|
1143
|
+
],
|
|
1144
|
+
narrator: "Finally we submit incomplete credentials and confirm the captured login form remains available for correction.",
|
|
1145
|
+
codeLevel: "beginner"
|
|
1146
|
+
});
|
|
1147
|
+
return scenarios;
|
|
1148
|
+
}
|
|
921
1149
|
function inferFeatureName(elementMap) {
|
|
922
1150
|
const heading = elementMap.elements.find((element) => element.category === "heading" && element.name);
|
|
923
1151
|
return heading?.name || elementMap.title || "Captured page";
|
|
@@ -936,29 +1164,206 @@ function normalizeTag(value) {
|
|
|
936
1164
|
const cleaned = String(value ?? "").trim().replace(/^@+/, "");
|
|
937
1165
|
return cleaned ? `@${cleaned}` : "@ui";
|
|
938
1166
|
}
|
|
1167
|
+
function detectAuthElements(elementMap) {
|
|
1168
|
+
const usernameInput = elementMap.elements.find((element) => element.category === "input" && /user(name)?|email|login/.test(`${element.name ?? ""} ${String(element.attributes.label ?? "")} ${String(element.attributes.name ?? "")}`.toLowerCase()));
|
|
1169
|
+
const passwordInput = elementMap.elements.find((element) => element.category === "input" && String(element.attributes.type ?? "").toLowerCase() === "password");
|
|
1170
|
+
const submitButton = elementMap.elements.find((element) => element.category === "button" && /login|log in|sign in|submit|continue/.test((element.name ?? "").toLowerCase())) ?? elementMap.elements.find((element) => element.category === "button");
|
|
1171
|
+
const heading = elementMap.elements.find((element) => element.category === "heading" && /login|sign in|auth/.test((element.name ?? "").toLowerCase())) ?? elementMap.elements.find((element) => element.category === "heading");
|
|
1172
|
+
return { usernameInput, passwordInput, submitButton, heading };
|
|
1173
|
+
}
|
|
1174
|
+
function shouldUseAuthFallback(authElements, scenarios) {
|
|
1175
|
+
if (!authElements.usernameInput || !authElements.passwordInput || !authElements.submitButton) return false;
|
|
1176
|
+
if (scenarios.length === 0) return true;
|
|
1177
|
+
const hasCredentialEntry = scenarios.some((scenario) => {
|
|
1178
|
+
const filledSelectors = new Set(
|
|
1179
|
+
scenario.steps.filter((step) => step.action === "fill").map((step) => step.selector)
|
|
1180
|
+
);
|
|
1181
|
+
return filledSelectors.has(authElements.usernameInput.selector) && filledSelectors.has(authElements.passwordInput.selector);
|
|
1182
|
+
});
|
|
1183
|
+
const hasBrokenLocatorWrapper = scenarios.some((scenario) => scenario.assertions.some((assertion) => /page\.locator\((['"])(getBy|locator\()/i.test(assertion.playwright)));
|
|
1184
|
+
return hasBrokenLocatorWrapper || !hasCredentialEntry;
|
|
1185
|
+
}
|
|
1186
|
+
function defaultStepHuman(action, selector, value) {
|
|
1187
|
+
if (action === "navigate") return `Navigate to ${value || "the page"}`;
|
|
1188
|
+
if (action === "fill") return `Fill the field ${selector}`;
|
|
1189
|
+
if (action === "click") return `Click ${selector}`;
|
|
1190
|
+
if (action === "select") return `Select ${value} in ${selector}`;
|
|
1191
|
+
if (action === "check") return `Check ${selector}`;
|
|
1192
|
+
if (action === "keyboard") return `Press ${value}`;
|
|
1193
|
+
return `Interact with ${selector}`;
|
|
1194
|
+
}
|
|
1195
|
+
function normalizePlaywrightAssertion(assertion) {
|
|
1196
|
+
const repaired = scopeBareSelectorsInAssertion(unwrapWrappedSelectorAssertion(assertion.playwright));
|
|
1197
|
+
if (repaired) return ensureStatement(repaired);
|
|
1198
|
+
const built = buildPlaywrightAssertion(assertion);
|
|
1199
|
+
if (built) return built;
|
|
1200
|
+
return ensureStatement(assertion.playwright);
|
|
1201
|
+
}
|
|
1202
|
+
function unwrapWrappedSelectorAssertion(playwright) {
|
|
1203
|
+
const trimmed = String(playwright ?? "").trim();
|
|
1204
|
+
if (!trimmed) return "";
|
|
1205
|
+
return trimmed.replace(
|
|
1206
|
+
/page\.locator\((["'])(getBy(?:Role|Text|Label|Placeholder|TestId)\((?:\\.|(?!\1).)*?\)|locator\((?:\\.|(?!\1).)*?\))\1\)/g,
|
|
1207
|
+
"page.$2"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
function scopeBareSelectorsInAssertion(playwright) {
|
|
1211
|
+
const trimmed = String(playwright ?? "").trim();
|
|
1212
|
+
if (!trimmed) return "";
|
|
1213
|
+
return trimmed.replace(
|
|
1214
|
+
/\bexpect\((getByRole|getByText|getByLabel|getByPlaceholder|getByTestId|locator)\(/g,
|
|
1215
|
+
"expect(page.$1("
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
function buildPlaywrightAssertion(assertion) {
|
|
1219
|
+
if (assertion.selector === "page") {
|
|
1220
|
+
if (assertion.type === "url" && assertion.expected) {
|
|
1221
|
+
return `await expect(page).toHaveURL(${JSON.stringify(assertion.expected)});`;
|
|
1222
|
+
}
|
|
1223
|
+
return "";
|
|
1224
|
+
}
|
|
1225
|
+
const target = selectorTarget(assertion.selector);
|
|
1226
|
+
if (!target) return "";
|
|
1227
|
+
if (assertion.type === "value" && assertion.expected) {
|
|
1228
|
+
return `await expect(${target}).toHaveValue(${JSON.stringify(assertion.expected)});`;
|
|
1229
|
+
}
|
|
1230
|
+
const combined = `${assertion.expected} ${assertion.human} ${assertion.playwright}`.toLowerCase();
|
|
1231
|
+
if (/hidden|not visible|no longer visible|disappear/.test(combined)) {
|
|
1232
|
+
return `await expect(${target}).not.toBeVisible();`;
|
|
1233
|
+
}
|
|
1234
|
+
return `await expect(${target}).toBeVisible();`;
|
|
1235
|
+
}
|
|
1236
|
+
function selectorTarget(selector) {
|
|
1237
|
+
const trimmed = String(selector ?? "").trim();
|
|
1238
|
+
if (!trimmed || trimmed === "page") return "page";
|
|
1239
|
+
if (trimmed.startsWith("page.")) return trimmed;
|
|
1240
|
+
return `page.${trimmed}`;
|
|
1241
|
+
}
|
|
1242
|
+
function visibleAssertion(selector) {
|
|
1243
|
+
return `await expect(${selectorTarget(selector)}).toBeVisible();`;
|
|
1244
|
+
}
|
|
1245
|
+
function valueAssertion(selector, value) {
|
|
1246
|
+
return `await expect(${selectorTarget(selector)}).toHaveValue(${JSON.stringify(value)});`;
|
|
1247
|
+
}
|
|
1248
|
+
function inferAuthValue(element, kind) {
|
|
1249
|
+
if (kind === "password") return "Secret123!";
|
|
1250
|
+
const haystack = `${element.name ?? ""} ${String(element.attributes.label ?? "")} ${String(element.attributes.name ?? "")}`.toLowerCase();
|
|
1251
|
+
if (/email/.test(haystack)) return "user@example.com";
|
|
1252
|
+
return "testuser";
|
|
1253
|
+
}
|
|
1254
|
+
function ensureStatement(value) {
|
|
1255
|
+
const trimmed = String(value ?? "").trim();
|
|
1256
|
+
if (!trimmed) return "";
|
|
1257
|
+
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
|
1258
|
+
}
|
|
1259
|
+
function ensureNavigateStep(steps, url) {
|
|
1260
|
+
if (!url || steps.length === 0) return steps;
|
|
1261
|
+
if (steps.some((step) => step.action === "navigate")) return steps;
|
|
1262
|
+
return [
|
|
1263
|
+
{
|
|
1264
|
+
action: "navigate",
|
|
1265
|
+
selector: "page",
|
|
1266
|
+
value: url,
|
|
1267
|
+
human: `Navigate to ${url}`
|
|
1268
|
+
},
|
|
1269
|
+
...steps
|
|
1270
|
+
];
|
|
1271
|
+
}
|
|
939
1272
|
function log(verbose, message) {
|
|
940
1273
|
if (verbose) console.log(message);
|
|
941
1274
|
}
|
|
942
1275
|
|
|
1276
|
+
// src/core/playwright.ts
|
|
1277
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1278
|
+
import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
|
|
1279
|
+
import { createRequire } from "module";
|
|
1280
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
1281
|
+
var require2 = createRequire(import.meta.url);
|
|
1282
|
+
var moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1283
|
+
async function resolvePlaywrightChromium(cwd = process.cwd()) {
|
|
1284
|
+
const searchRoots = buildSearchRoots(cwd);
|
|
1285
|
+
for (const packageName of ["@playwright/test", "playwright-core"]) {
|
|
1286
|
+
for (const searchRoot of searchRoots) {
|
|
1287
|
+
const resolvedPath = resolveFromRoot(packageName, searchRoot);
|
|
1288
|
+
if (!resolvedPath) continue;
|
|
1289
|
+
const imported = await import(pathToFileURL(resolvedPath).href);
|
|
1290
|
+
const chromium = imported.chromium ?? imported.default?.chromium;
|
|
1291
|
+
if (chromium) {
|
|
1292
|
+
return {
|
|
1293
|
+
packageName,
|
|
1294
|
+
resolvedPath,
|
|
1295
|
+
searchRoot,
|
|
1296
|
+
chromium
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
"Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium"
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
function buildSearchRoots(cwd) {
|
|
1306
|
+
const roots = [resolve3(cwd)];
|
|
1307
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
1308
|
+
if (projectRoot && projectRoot !== roots[0]) {
|
|
1309
|
+
roots.push(projectRoot);
|
|
1310
|
+
}
|
|
1311
|
+
roots.push(moduleDir);
|
|
1312
|
+
return Array.from(new Set(roots));
|
|
1313
|
+
}
|
|
1314
|
+
function findNearestProjectRoot(startDir) {
|
|
1315
|
+
let currentDir = resolve3(startDir);
|
|
1316
|
+
while (true) {
|
|
1317
|
+
if (existsSync2(join3(currentDir, "package.json"))) {
|
|
1318
|
+
return currentDir;
|
|
1319
|
+
}
|
|
1320
|
+
const parentDir = dirname2(currentDir);
|
|
1321
|
+
if (parentDir === currentDir) return void 0;
|
|
1322
|
+
currentDir = parentDir;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function resolveFromRoot(packageName, searchRoot) {
|
|
1326
|
+
try {
|
|
1327
|
+
return require2.resolve(packageName, { paths: [searchRoot] });
|
|
1328
|
+
} catch {
|
|
1329
|
+
return void 0;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
943
1333
|
// src/core/capture.ts
|
|
944
1334
|
var SETTLE_MS = 1200;
|
|
945
1335
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
946
1336
|
var MAX_PER_CATEGORY = 50;
|
|
1337
|
+
var CaptureRuntimeError = class extends Error {
|
|
1338
|
+
constructor(code, message, options) {
|
|
1339
|
+
super(message);
|
|
1340
|
+
this.name = "CaptureRuntimeError";
|
|
1341
|
+
this.code = code;
|
|
1342
|
+
if (options?.cause !== void 0) {
|
|
1343
|
+
this.cause = options.cause;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
947
1347
|
async function captureElements(url, options = {}) {
|
|
948
1348
|
const {
|
|
949
1349
|
headless = true,
|
|
950
1350
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
951
1351
|
verbose = false,
|
|
952
|
-
userAgent = "Mozilla/5.0 (compatible; CementicTest/0.2.
|
|
1352
|
+
userAgent = "Mozilla/5.0 (compatible; CementicTest/0.2.7 capture)"
|
|
953
1353
|
} = options;
|
|
954
1354
|
const chromium = await loadChromium();
|
|
955
1355
|
const mode = headless ? "headless" : "headed";
|
|
956
1356
|
log2(verbose, `
|
|
957
1357
|
[capture] Starting ${mode} capture: ${url}`);
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1358
|
+
let browser;
|
|
1359
|
+
try {
|
|
1360
|
+
browser = await chromium.launch({
|
|
1361
|
+
headless,
|
|
1362
|
+
slowMo: headless ? 0 : 150
|
|
1363
|
+
});
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
throw classifyCaptureError(error);
|
|
1366
|
+
}
|
|
962
1367
|
const context = await browser.newContext({
|
|
963
1368
|
userAgent,
|
|
964
1369
|
viewport: { width: 1280, height: 800 },
|
|
@@ -967,10 +1372,18 @@ async function captureElements(url, options = {}) {
|
|
|
967
1372
|
const page = await context.newPage();
|
|
968
1373
|
try {
|
|
969
1374
|
log2(verbose, ` -> Navigating (timeout: ${timeoutMs}ms)`);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1375
|
+
try {
|
|
1376
|
+
await page.goto(url, {
|
|
1377
|
+
waitUntil: "domcontentloaded",
|
|
1378
|
+
timeout: timeoutMs
|
|
1379
|
+
});
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
throw new CaptureRuntimeError(
|
|
1382
|
+
"PAGE_LOAD_FAILED",
|
|
1383
|
+
`Page load failed for ${url}. ${error?.message ?? error}`,
|
|
1384
|
+
{ cause: error }
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
974
1387
|
log2(verbose, ` -> Waiting ${SETTLE_MS}ms for page settle`);
|
|
975
1388
|
await page.waitForTimeout(SETTLE_MS);
|
|
976
1389
|
log2(verbose, " -> Extracting accessibility snapshot");
|
|
@@ -999,8 +1412,10 @@ async function captureElements(url, options = {}) {
|
|
|
999
1412
|
};
|
|
1000
1413
|
log2(verbose, ` -> Captured ${elements.length} testable elements from "${title}"`);
|
|
1001
1414
|
return result;
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
throw classifyCaptureError(error);
|
|
1002
1417
|
} finally {
|
|
1003
|
-
await browser
|
|
1418
|
+
await browser?.close();
|
|
1004
1419
|
}
|
|
1005
1420
|
}
|
|
1006
1421
|
function toPageSummary(elementMap) {
|
|
@@ -1024,16 +1439,15 @@ function toPageSummary(elementMap) {
|
|
|
1024
1439
|
}
|
|
1025
1440
|
async function loadChromium() {
|
|
1026
1441
|
try {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1442
|
+
const resolution = await resolvePlaywrightChromium(process.cwd());
|
|
1443
|
+
return resolution.chromium;
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
throw new CaptureRuntimeError(
|
|
1446
|
+
"PLAYWRIGHT_NOT_FOUND",
|
|
1447
|
+
error?.message ?? "Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium",
|
|
1448
|
+
{ cause: error }
|
|
1449
|
+
);
|
|
1033
1450
|
}
|
|
1034
|
-
throw new Error(
|
|
1035
|
-
'Playwright is required for live page capture. Install it in the target project with "npm install -D @playwright/test" and "npx playwright install chromium".'
|
|
1036
|
-
);
|
|
1037
1451
|
}
|
|
1038
1452
|
function extractDomData() {
|
|
1039
1453
|
const localMaxPerCategory = 50;
|
|
@@ -1353,10 +1767,71 @@ async function getAccessibilitySnapshot(page, verbose) {
|
|
|
1353
1767
|
function asString(value) {
|
|
1354
1768
|
return typeof value === "string" ? value : void 0;
|
|
1355
1769
|
}
|
|
1770
|
+
function formatCaptureFailure(error) {
|
|
1771
|
+
const resolved = classifyCaptureError(error);
|
|
1772
|
+
if (resolved.code === "PLAYWRIGHT_NOT_FOUND") {
|
|
1773
|
+
return [
|
|
1774
|
+
"Playwright not resolvable.",
|
|
1775
|
+
"Playwright runtime not found in this project.",
|
|
1776
|
+
"Try:",
|
|
1777
|
+
" npm install",
|
|
1778
|
+
" npx playwright install chromium"
|
|
1779
|
+
];
|
|
1780
|
+
}
|
|
1781
|
+
if (resolved.code === "BROWSER_NOT_INSTALLED") {
|
|
1782
|
+
return [
|
|
1783
|
+
"Browser not installed.",
|
|
1784
|
+
"Playwright resolved, but Chromium is not installed for this project.",
|
|
1785
|
+
"Try:",
|
|
1786
|
+
" npx playwright install chromium"
|
|
1787
|
+
];
|
|
1788
|
+
}
|
|
1789
|
+
if (resolved.code === "PAGE_LOAD_FAILED") {
|
|
1790
|
+
return [
|
|
1791
|
+
"Page load failed.",
|
|
1792
|
+
resolved.message
|
|
1793
|
+
];
|
|
1794
|
+
}
|
|
1795
|
+
return [
|
|
1796
|
+
"Capture failed.",
|
|
1797
|
+
resolved.message
|
|
1798
|
+
];
|
|
1799
|
+
}
|
|
1800
|
+
function classifyCaptureError(error) {
|
|
1801
|
+
if (error instanceof CaptureRuntimeError) {
|
|
1802
|
+
return error;
|
|
1803
|
+
}
|
|
1804
|
+
const message = errorMessage(error);
|
|
1805
|
+
if (isBrowserInstallError(message)) {
|
|
1806
|
+
return new CaptureRuntimeError(
|
|
1807
|
+
"BROWSER_NOT_INSTALLED",
|
|
1808
|
+
"Playwright resolved, but Chromium is not installed for this project.",
|
|
1809
|
+
{ cause: error }
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (isPlaywrightNotFoundError(message)) {
|
|
1813
|
+
return new CaptureRuntimeError(
|
|
1814
|
+
"PLAYWRIGHT_NOT_FOUND",
|
|
1815
|
+
"Playwright runtime not found in this project.",
|
|
1816
|
+
{ cause: error }
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
return new CaptureRuntimeError("CAPTURE_FAILED", message || "Unknown capture failure.", { cause: error });
|
|
1820
|
+
}
|
|
1821
|
+
function isBrowserInstallError(message) {
|
|
1822
|
+
return /Executable doesn't exist|browserType\.launch: Executable doesn't exist|Please run the following command|playwright install/i.test(message);
|
|
1823
|
+
}
|
|
1824
|
+
function isPlaywrightNotFoundError(message) {
|
|
1825
|
+
return /Playwright runtime not found|Cannot find package ['"](?:@playwright\/test|playwright-core)['"]/i.test(message);
|
|
1826
|
+
}
|
|
1827
|
+
function errorMessage(error) {
|
|
1828
|
+
if (error instanceof Error) return error.message;
|
|
1829
|
+
return String(error ?? "");
|
|
1830
|
+
}
|
|
1356
1831
|
|
|
1357
1832
|
// src/core/report.ts
|
|
1358
1833
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1359
|
-
import { join as
|
|
1834
|
+
import { join as join4 } from "path";
|
|
1360
1835
|
function printCaptureReport(elementMap, analysis) {
|
|
1361
1836
|
console.log("");
|
|
1362
1837
|
console.log("=".repeat(60));
|
|
@@ -1388,10 +1863,10 @@ function printCaptureReport(elementMap, analysis) {
|
|
|
1388
1863
|
function saveCaptureJson(elementMap, analysis, outputDir = ".cementic/capture") {
|
|
1389
1864
|
mkdirSync3(outputDir, { recursive: true });
|
|
1390
1865
|
const fileName = `capture-${slugify(elementMap.url)}-${Date.now()}.json`;
|
|
1391
|
-
const filePath =
|
|
1866
|
+
const filePath = join4(outputDir, fileName);
|
|
1392
1867
|
writeFileSync3(filePath, JSON.stringify({
|
|
1393
1868
|
_meta: {
|
|
1394
|
-
version: "0.2.
|
|
1869
|
+
version: "0.2.7",
|
|
1395
1870
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1396
1871
|
tool: "@cementic/cementic-test"
|
|
1397
1872
|
},
|
|
@@ -1430,7 +1905,7 @@ function buildCasesMarkdown(analysis) {
|
|
|
1430
1905
|
function saveCasesMarkdown(analysis, outputDir = "cases", fileName) {
|
|
1431
1906
|
mkdirSync3(outputDir, { recursive: true });
|
|
1432
1907
|
const resolvedFileName = fileName ?? `${slugify(analysis.feature || analysis.url)}.md`;
|
|
1433
|
-
const filePath =
|
|
1908
|
+
const filePath = join4(outputDir, resolvedFileName);
|
|
1434
1909
|
writeFileSync3(filePath, buildCasesMarkdown(analysis));
|
|
1435
1910
|
return filePath;
|
|
1436
1911
|
}
|
|
@@ -1438,7 +1913,7 @@ function saveSpecPreview(analysis, outputDir = "tests/preview") {
|
|
|
1438
1913
|
if (analysis.scenarios.length === 0) return null;
|
|
1439
1914
|
mkdirSync3(outputDir, { recursive: true });
|
|
1440
1915
|
const fileName = `spec-preview-${slugify(analysis.url)}-${Date.now()}.spec.cjs`;
|
|
1441
|
-
const filePath =
|
|
1916
|
+
const filePath = join4(outputDir, fileName);
|
|
1442
1917
|
const lines = [];
|
|
1443
1918
|
lines.push("/**");
|
|
1444
1919
|
lines.push(" * CementicTest Capture Preview");
|
|
@@ -1465,7 +1940,7 @@ function saveSpecPreview(analysis, outputDir = "tests/preview") {
|
|
|
1465
1940
|
if (step.action === "hover") lines.push(` await ${selector}.hover();`);
|
|
1466
1941
|
}
|
|
1467
1942
|
for (const assertion of scenario.assertions) {
|
|
1468
|
-
lines.push(` ${
|
|
1943
|
+
lines.push(` ${ensureStatement2(assertion.playwright)}`);
|
|
1469
1944
|
}
|
|
1470
1945
|
lines.push("});");
|
|
1471
1946
|
lines.push("");
|
|
@@ -1484,7 +1959,7 @@ function normalizeTag2(value) {
|
|
|
1484
1959
|
function sanitizeComment(value) {
|
|
1485
1960
|
return String(value ?? "").replace(/-->/g, "-- >");
|
|
1486
1961
|
}
|
|
1487
|
-
function
|
|
1962
|
+
function ensureStatement2(value) {
|
|
1488
1963
|
const trimmed = String(value ?? "").trim();
|
|
1489
1964
|
if (!trimmed) return "// TODO: add assertion";
|
|
1490
1965
|
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
|
@@ -1562,10 +2037,10 @@ async function runTcInteractive(params) {
|
|
|
1562
2037
|
});
|
|
1563
2038
|
mkdirSync4("cases", { recursive: true });
|
|
1564
2039
|
const fileName = `${prefix.toLowerCase()}-${slugify2(feature)}.md`;
|
|
1565
|
-
const fullPath =
|
|
2040
|
+
const fullPath = join5("cases", fileName);
|
|
1566
2041
|
let markdown;
|
|
2042
|
+
let pageSummary = void 0;
|
|
1567
2043
|
if (params.useAi) {
|
|
1568
|
-
let pageSummary = void 0;
|
|
1569
2044
|
if (url) {
|
|
1570
2045
|
try {
|
|
1571
2046
|
console.log(`\u{1F50D} Capturing page: ${url}`);
|
|
@@ -1576,6 +2051,7 @@ async function runTcInteractive(params) {
|
|
|
1576
2051
|
printCaptureReport(elementMap);
|
|
1577
2052
|
const jsonPath = saveCaptureJson(elementMap);
|
|
1578
2053
|
console.log(`\u{1F4C4} Saved capture JSON \u2192 ${jsonPath}`);
|
|
2054
|
+
pageSummary = toPageSummary(elementMap);
|
|
1579
2055
|
if (params.captureOnly) {
|
|
1580
2056
|
console.log("Capture-only mode requested. No test cases were generated.");
|
|
1581
2057
|
return;
|
|
@@ -1594,20 +2070,14 @@ async function runTcInteractive(params) {
|
|
|
1594
2070
|
console.log(" ct test");
|
|
1595
2071
|
return;
|
|
1596
2072
|
} catch (e) {
|
|
1597
|
-
|
|
2073
|
+
printWarningBlock(formatCaptureFailure(e));
|
|
2074
|
+
console.warn("\u26A0\uFE0F Falling back to AI text generation without capture context.");
|
|
1598
2075
|
if (params.captureOnly) {
|
|
1599
2076
|
return;
|
|
1600
2077
|
}
|
|
1601
2078
|
}
|
|
1602
2079
|
}
|
|
1603
2080
|
try {
|
|
1604
|
-
if (pageSummary === void 0 && url) {
|
|
1605
|
-
const elementMap = await captureElements(url, {
|
|
1606
|
-
headless: true,
|
|
1607
|
-
verbose: false
|
|
1608
|
-
});
|
|
1609
|
-
pageSummary = toPageSummary(elementMap);
|
|
1610
|
-
}
|
|
1611
2081
|
markdown = await generateTcMarkdownWithAi({
|
|
1612
2082
|
appDescription: appDescription || void 0,
|
|
1613
2083
|
feature,
|
|
@@ -1620,7 +2090,10 @@ async function runTcInteractive(params) {
|
|
|
1620
2090
|
markdown = injectUrlMetadata(markdown, url);
|
|
1621
2091
|
console.log("\u2705 AI generated test cases.");
|
|
1622
2092
|
} catch (err) {
|
|
1623
|
-
|
|
2093
|
+
printWarningBlock([
|
|
2094
|
+
"AI generation failed.",
|
|
2095
|
+
err?.message ?? String(err)
|
|
2096
|
+
]);
|
|
1624
2097
|
markdown = buildManualCasesMarkdown({ prefix, feature, url, numCases, startIndex: 1 });
|
|
1625
2098
|
console.log("\u{1F4DD} Generated manual test case templates instead.");
|
|
1626
2099
|
}
|
|
@@ -1635,6 +2108,13 @@ async function runTcInteractive(params) {
|
|
|
1635
2108
|
console.log(" ct normalize ./cases --and-gen --lang ts");
|
|
1636
2109
|
console.log(" ct test");
|
|
1637
2110
|
}
|
|
2111
|
+
function printWarningBlock(lines) {
|
|
2112
|
+
if (lines.length === 0) return;
|
|
2113
|
+
console.warn(`\u26A0\uFE0F ${lines[0]}`);
|
|
2114
|
+
for (const line of lines.slice(1)) {
|
|
2115
|
+
console.warn(line);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1638
2118
|
function addSharedOptions(cmd) {
|
|
1639
2119
|
return cmd.option("--ai", "Use AI to generate test cases (requires API key)").option("--prefix <prefix>", "Explicit ID prefix, e.g. AUTH, DASH, CART").option("--feature <name>", "Feature name \u2014 skips interactive prompt").option("--desc <text>", "App description \u2014 skips interactive prompt").option("--count <n>", "Number of cases (1\u201310)", parseInt);
|
|
1640
2120
|
}
|
|
@@ -1711,15 +2191,15 @@ function reportCmd() {
|
|
|
1711
2191
|
// src/commands/serve.ts
|
|
1712
2192
|
import { Command as Command6 } from "commander";
|
|
1713
2193
|
import { spawn as spawn3 } from "child_process";
|
|
1714
|
-
import { existsSync as
|
|
1715
|
-
import { join as
|
|
2194
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2195
|
+
import { join as join6 } from "path";
|
|
1716
2196
|
function serveCmd() {
|
|
1717
2197
|
const cmd = new Command6("serve").description("Serve the Allure report").action(() => {
|
|
1718
2198
|
console.log("\u{1F4CA} Serving Allure report...");
|
|
1719
|
-
const localAllureBin =
|
|
2199
|
+
const localAllureBin = join6(process.cwd(), "node_modules", "allure-commandline", "bin", "allure");
|
|
1720
2200
|
let executable = "npx";
|
|
1721
2201
|
let args = ["allure", "serve", "./allure-results"];
|
|
1722
|
-
if (
|
|
2202
|
+
if (existsSync3(localAllureBin)) {
|
|
1723
2203
|
executable = "node";
|
|
1724
2204
|
args = [localAllureBin, "serve", "./allure-results"];
|
|
1725
2205
|
}
|
|
@@ -1742,9 +2222,9 @@ function serveCmd() {
|
|
|
1742
2222
|
// src/commands/flow.ts
|
|
1743
2223
|
import { Command as Command7 } from "commander";
|
|
1744
2224
|
import { spawn as spawn4 } from "child_process";
|
|
1745
|
-
import { resolve as
|
|
2225
|
+
import { resolve as resolve4 } from "path";
|
|
1746
2226
|
function runStep(cmd, args, stepName) {
|
|
1747
|
-
return new Promise((
|
|
2227
|
+
return new Promise((resolve5, reject) => {
|
|
1748
2228
|
console.log(`
|
|
1749
2229
|
\u{1F30A} Flow Step: ${stepName}`);
|
|
1750
2230
|
console.log(`> ${cmd} ${args.join(" ")}`);
|
|
@@ -1754,7 +2234,7 @@ function runStep(cmd, args, stepName) {
|
|
|
1754
2234
|
});
|
|
1755
2235
|
child.on("exit", (code) => {
|
|
1756
2236
|
if (code === 0) {
|
|
1757
|
-
|
|
2237
|
+
resolve5();
|
|
1758
2238
|
} else {
|
|
1759
2239
|
reject(new Error(`${stepName} failed with exit code ${code}`));
|
|
1760
2240
|
}
|
|
@@ -1763,7 +2243,7 @@ function runStep(cmd, args, stepName) {
|
|
|
1763
2243
|
}
|
|
1764
2244
|
function flowCmd() {
|
|
1765
2245
|
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) => {
|
|
1766
|
-
const cliBin =
|
|
2246
|
+
const cliBin = resolve4(process.argv[1]);
|
|
1767
2247
|
try {
|
|
1768
2248
|
await runStep(process.execPath, [cliBin, "normalize", casesDir], "Normalize Cases");
|
|
1769
2249
|
await runStep(process.execPath, [cliBin, "gen", "--lang", opts.lang], "Generate Tests");
|
|
@@ -1784,8 +2264,8 @@ function flowCmd() {
|
|
|
1784
2264
|
|
|
1785
2265
|
// src/commands/ci.ts
|
|
1786
2266
|
import { Command as Command8 } from "commander";
|
|
1787
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as
|
|
1788
|
-
import { join as
|
|
2267
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync4 } from "fs";
|
|
2268
|
+
import { join as join7 } from "path";
|
|
1789
2269
|
var WORKFLOW_CONTENT = `name: Playwright Tests
|
|
1790
2270
|
on:
|
|
1791
2271
|
push:
|
|
@@ -1816,15 +2296,15 @@ jobs:
|
|
|
1816
2296
|
`;
|
|
1817
2297
|
function ciCmd() {
|
|
1818
2298
|
const cmd = new Command8("ci").description("Generate GitHub Actions workflow for CI").action(() => {
|
|
1819
|
-
const githubDir =
|
|
1820
|
-
const workflowsDir =
|
|
1821
|
-
const workflowFile =
|
|
2299
|
+
const githubDir = join7(process.cwd(), ".github");
|
|
2300
|
+
const workflowsDir = join7(githubDir, "workflows");
|
|
2301
|
+
const workflowFile = join7(workflowsDir, "cementic.yml");
|
|
1822
2302
|
console.log("\u{1F916} Setting up CI/CD workflow...");
|
|
1823
|
-
if (!
|
|
2303
|
+
if (!existsSync4(workflowsDir)) {
|
|
1824
2304
|
mkdirSync5(workflowsDir, { recursive: true });
|
|
1825
2305
|
console.log(`Created directory: ${workflowsDir}`);
|
|
1826
2306
|
}
|
|
1827
|
-
if (
|
|
2307
|
+
if (existsSync4(workflowFile)) {
|
|
1828
2308
|
console.warn(`\u26A0\uFE0F Workflow file already exists at ${workflowFile}. Skipping.`);
|
|
1829
2309
|
return;
|
|
1830
2310
|
}
|
|
@@ -1838,8 +2318,8 @@ function ciCmd() {
|
|
|
1838
2318
|
}
|
|
1839
2319
|
|
|
1840
2320
|
// src/cli.ts
|
|
1841
|
-
var
|
|
1842
|
-
var { version } =
|
|
2321
|
+
var require3 = createRequire2(import.meta.url);
|
|
2322
|
+
var { version } = require3("../package.json");
|
|
1843
2323
|
var program = new Command9();
|
|
1844
2324
|
program.name("ct").description("CementicTest CLI: cases \u2192 normalized \u2192 POM tests \u2192 Playwright").version(version);
|
|
1845
2325
|
program.addCommand(newCmd());
|