@cementic/cementic-test 0.2.13 → 0.2.14
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 +106 -1
- package/dist/{chunk-RG26I5FB.js → chunk-3S26OWNR.js} +5 -2
- package/dist/{chunk-RG26I5FB.js.map → chunk-3S26OWNR.js.map} +1 -1
- package/dist/cli.js +316 -50
- package/dist/cli.js.map +1 -1
- package/dist/{gen-6Y65IYXO.js → gen-AGWFMHTO.js} +2 -2
- package/package.json +3 -3
- /package/dist/{gen-6Y65IYXO.js.map → gen-AGWFMHTO.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
genCmd
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3S26OWNR.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { Command as Command9 } from "commander";
|
|
@@ -18,6 +18,8 @@ import { platform, release } from "os";
|
|
|
18
18
|
var __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
var __dirname = dirname(__filename);
|
|
20
20
|
var LEGACY_MACOS_DARWIN_MAJOR = 23;
|
|
21
|
+
var LEGACY_PLAYWRIGHT_VERSION = "^1.48.2";
|
|
22
|
+
var LEGACY_ALLURE_VERSION = "^2.15.1";
|
|
21
23
|
function resolveTemplatePath(templateDir) {
|
|
22
24
|
const candidates = [
|
|
23
25
|
resolve(__dirname, `templates/${templateDir}`),
|
|
@@ -27,13 +29,73 @@ function resolveTemplatePath(templateDir) {
|
|
|
27
29
|
];
|
|
28
30
|
return candidates.find((candidate) => existsSync(candidate));
|
|
29
31
|
}
|
|
32
|
+
function getHostPlatform(env = process.env) {
|
|
33
|
+
return (env.CT_OS_PLATFORM_OVERRIDE ?? platform()).trim().toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
function getHostRelease(env = process.env) {
|
|
36
|
+
return (env.CT_OS_RELEASE_OVERRIDE ?? release()).trim();
|
|
37
|
+
}
|
|
38
|
+
function isLegacyMacOs(env = process.env) {
|
|
39
|
+
if (getHostPlatform(env) !== "darwin") return false;
|
|
40
|
+
const majorVersion = parseInt(getHostRelease(env).split(".")[0], 10);
|
|
41
|
+
return Number.isFinite(majorVersion) && majorVersion < LEGACY_MACOS_DARWIN_MAJOR;
|
|
42
|
+
}
|
|
43
|
+
function resolveBrowserInstallProfile(raw) {
|
|
44
|
+
const profile = String(raw ?? "auto").trim().toLowerCase();
|
|
45
|
+
if (profile === "auto" || profile === "all" || profile === "chromium") return profile;
|
|
46
|
+
console.error(`\u274C Unsupported browser install profile "${raw}". Use "auto", "all", or "chromium".`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
function resolveBootstrapPlan(browserProfile, env = process.env) {
|
|
50
|
+
const legacyMacOs = isLegacyMacOs(env);
|
|
51
|
+
const resolvedProfile = browserProfile === "auto" ? legacyMacOs ? "chromium" : "all" : browserProfile;
|
|
52
|
+
return {
|
|
53
|
+
browserInstallArgs: resolvedProfile === "chromium" ? ["playwright", "install", "chromium"] : ["playwright", "install"],
|
|
54
|
+
browserProfile: resolvedProfile,
|
|
55
|
+
isLegacyMacOs: legacyMacOs,
|
|
56
|
+
packageVersionOverrides: legacyMacOs ? {
|
|
57
|
+
"@playwright/test": LEGACY_PLAYWRIGHT_VERSION,
|
|
58
|
+
"allure-playwright": LEGACY_ALLURE_VERSION
|
|
59
|
+
} : {},
|
|
60
|
+
reason: legacyMacOs ? "Detected macOS 13 or older. Using a legacy-compatible Playwright toolchain and Chromium-only browser install." : void 0
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function applyPackageVersionOverrides(projectPath, versionOverrides) {
|
|
64
|
+
if (Object.keys(versionOverrides).length === 0) return;
|
|
65
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
66
|
+
if (!existsSync(pkgJsonPath)) return;
|
|
67
|
+
try {
|
|
68
|
+
const pkgContent = readFileSync(pkgJsonPath, "utf-8");
|
|
69
|
+
const pkg = JSON.parse(pkgContent);
|
|
70
|
+
const devDependencies = pkg.devDependencies ?? {};
|
|
71
|
+
let changed = false;
|
|
72
|
+
for (const [name, version2] of Object.entries(versionOverrides)) {
|
|
73
|
+
if (typeof devDependencies[name] !== "string") continue;
|
|
74
|
+
devDependencies[name] = version2;
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
if (!changed) return;
|
|
78
|
+
pkg.devDependencies = devDependencies;
|
|
79
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
80
|
+
console.log("\u2705 Applied compatibility dependency pins for this machine.");
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn("\u26A0\uFE0F Failed to adjust package.json for OS compatibility:", err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
30
85
|
function newCmd() {
|
|
31
86
|
const cmd = new Command("new").arguments("<projectName>").description("Scaffold a new CementicTest + Playwright project from scratch").addHelpText("after", `
|
|
32
87
|
Examples:
|
|
33
88
|
$ ct new my-awesome-test-suite
|
|
34
89
|
$ ct new e2e-ts --lang ts
|
|
35
90
|
$ ct new e2e-tests --no-browsers
|
|
36
|
-
`).option("--mode <mode>", "greenfield|enhance", "greenfield").option("--lang <lang>", "Scaffold language (js|ts)", "js").option("--no-browsers", 'do not run "npx playwright install" during setup').action((projectName, opts) => {
|
|
91
|
+
`).option("--mode <mode>", "greenfield|enhance", "greenfield").option("--lang <lang>", "Scaffold language (js|ts)", "js").option("--browser-set <profile>", "Browser install profile (auto|all|chromium)", "auto").option("--no-browsers", 'do not run "npx playwright install" during setup').action((projectName, opts) => {
|
|
92
|
+
const mode = String(opts.mode ?? "greenfield").trim().toLowerCase();
|
|
93
|
+
if (mode !== "greenfield") {
|
|
94
|
+
console.error(`\u274C Unsupported scaffold mode "${opts.mode}". Only "greenfield" is currently implemented.`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const browserInstallProfile = resolveBrowserInstallProfile(opts.browserSet);
|
|
98
|
+
const bootstrapPlan = resolveBootstrapPlan(browserInstallProfile);
|
|
37
99
|
const root = process.cwd();
|
|
38
100
|
const projectPath = join(root, projectName);
|
|
39
101
|
console.log(`\u{1F680} Initializing new CementicTest project in ${projectName}...`);
|
|
@@ -62,25 +124,10 @@ Examples:
|
|
|
62
124
|
}
|
|
63
125
|
}
|
|
64
126
|
copyRecursive(templatePath, projectPath);
|
|
65
|
-
|
|
66
|
-
|
|
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.");
|
|
78
|
-
}
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.warn("\u26A0\uFE0F Failed to adjust package.json for OS compatibility:", err);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
127
|
+
if (bootstrapPlan.reason) {
|
|
128
|
+
console.log(`\u{1F34E} ${bootstrapPlan.reason}`);
|
|
83
129
|
}
|
|
130
|
+
applyPackageVersionOverrides(projectPath, bootstrapPlan.packageVersionOverrides);
|
|
84
131
|
try {
|
|
85
132
|
execSync("git init", { cwd: projectPath, stdio: "ignore" });
|
|
86
133
|
const gitignorePath = join(projectPath, ".gitignore");
|
|
@@ -101,17 +148,19 @@ Examples:
|
|
|
101
148
|
if (opts.browsers !== false && dependenciesInstalled) {
|
|
102
149
|
console.log("\u{1F310} Installing Playwright browsers...");
|
|
103
150
|
try {
|
|
104
|
-
|
|
151
|
+
const installCommand = `npx ${bootstrapPlan.browserInstallArgs.join(" ")}`;
|
|
152
|
+
if (bootstrapPlan.browserProfile === "chromium" && bootstrapPlan.isLegacyMacOs) {
|
|
105
153
|
console.log("\u26A0\uFE0F WebKit is not supported on this macOS version. Installing Chromium only...");
|
|
106
|
-
|
|
107
|
-
console.log("\
|
|
108
|
-
} else {
|
|
109
|
-
execSync("npx playwright install", { cwd: projectPath, stdio: "inherit" });
|
|
110
|
-
console.log("\u2705 Playwright browsers installed successfully.");
|
|
154
|
+
} else if (bootstrapPlan.browserProfile === "chromium") {
|
|
155
|
+
console.log("\u2139\uFE0F Installing Chromium only because --browser-set chromium was requested.");
|
|
111
156
|
}
|
|
157
|
+
execSync(installCommand, { cwd: projectPath, stdio: "inherit" });
|
|
158
|
+
console.log(
|
|
159
|
+
bootstrapPlan.browserProfile === "chromium" ? "\u2705 Chromium installed successfully." : "\u2705 Playwright browsers installed successfully."
|
|
160
|
+
);
|
|
112
161
|
} catch (e) {
|
|
113
162
|
console.warn("\u26A0\uFE0F Browser installation did not complete. You can finish setup with:");
|
|
114
|
-
console.warn(
|
|
163
|
+
console.warn(` npx ${bootstrapPlan.browserInstallArgs.join(" ")}`);
|
|
115
164
|
}
|
|
116
165
|
}
|
|
117
166
|
console.log(`
|
|
@@ -126,11 +175,6 @@ Happy testing! \u{1F9EA}`);
|
|
|
126
175
|
});
|
|
127
176
|
return cmd;
|
|
128
177
|
}
|
|
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
|
-
}
|
|
134
178
|
|
|
135
179
|
// src/commands/normalize.ts
|
|
136
180
|
import { Command as Command2 } from "commander";
|
|
@@ -321,7 +365,7 @@ Examples:
|
|
|
321
365
|
}
|
|
322
366
|
console.log(`\u2705 Normalized ${index.summary.parsed} case(s). Output \u2192 .cementic/normalized/`);
|
|
323
367
|
if (opts.andGen) {
|
|
324
|
-
const { gen } = await import("./gen-
|
|
368
|
+
const { gen } = await import("./gen-AGWFMHTO.js");
|
|
325
369
|
await gen({ lang: opts.lang || "ts", out: "tests/generated" });
|
|
326
370
|
}
|
|
327
371
|
});
|
|
@@ -1054,6 +1098,9 @@ async function generateTcMarkdownWithAi(ctx) {
|
|
|
1054
1098
|
}
|
|
1055
1099
|
|
|
1056
1100
|
// src/core/analyse.ts
|
|
1101
|
+
function escapeForRegex(value) {
|
|
1102
|
+
return value.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
|
|
1103
|
+
}
|
|
1057
1104
|
var RULE_10_PLAYWRIGHT_KNOWLEDGE_BASE2 = `
|
|
1058
1105
|
RULE 10 - PLAYWRIGHT KNOWLEDGE BASE
|
|
1059
1106
|
(sourced from playwright.dev official documentation)
|
|
@@ -1365,23 +1412,37 @@ ADDITIONAL PLAYWRIGHT DOCS (fetched for this intent)
|
|
|
1365
1412
|
}
|
|
1366
1413
|
async function analyseElements(elementMap, options = {}) {
|
|
1367
1414
|
const { verbose = false, feature } = options;
|
|
1368
|
-
const providerConfig = resolveLlmProvider();
|
|
1369
1415
|
const requestedFeature = normalizeRequestedFeature(feature, elementMap);
|
|
1416
|
+
let providerConfig;
|
|
1417
|
+
try {
|
|
1418
|
+
providerConfig = resolveLlmProvider();
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
log(verbose, `
|
|
1421
|
+
[analyse] ${shortErrorMessage(error)}`);
|
|
1422
|
+
log(verbose, "[analyse] Falling back to deterministic capture analysis.");
|
|
1423
|
+
return buildDeterministicAnalysis(elementMap, requestedFeature);
|
|
1424
|
+
}
|
|
1370
1425
|
log(verbose, `
|
|
1371
1426
|
[analyse] Sending capture to ${providerConfig.displayName} (${providerConfig.model})`);
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
providerConfig.apiKey,
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1427
|
+
try {
|
|
1428
|
+
const docContext = await fetchDocContext2(requestedFeature);
|
|
1429
|
+
const systemPrompt = buildSystemPrompt() + docContext;
|
|
1430
|
+
const userPrompt = buildUserPrompt(elementMap, requestedFeature);
|
|
1431
|
+
const rawResponse = providerConfig.transport === "anthropic" ? await callAnthropic2(providerConfig.apiKey, providerConfig.model, systemPrompt, userPrompt) : await callOpenAiCompatible2(
|
|
1432
|
+
providerConfig.apiKey,
|
|
1433
|
+
providerConfig.model,
|
|
1434
|
+
providerConfig.baseUrl ?? "https://api.openai.com/v1",
|
|
1435
|
+
providerConfig.displayName,
|
|
1436
|
+
systemPrompt,
|
|
1437
|
+
userPrompt
|
|
1438
|
+
);
|
|
1439
|
+
const parsed = parseAnalysisJson(rawResponse);
|
|
1440
|
+
return sanitizeAnalysis(parsed, elementMap, requestedFeature);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
log(verbose, `[analyse] Remote analysis failed: ${shortErrorMessage(error)}`);
|
|
1443
|
+
log(verbose, "[analyse] Falling back to deterministic capture analysis.");
|
|
1444
|
+
return buildDeterministicAnalysis(elementMap, requestedFeature);
|
|
1445
|
+
}
|
|
1385
1446
|
}
|
|
1386
1447
|
function buildSystemPrompt() {
|
|
1387
1448
|
return `
|
|
@@ -1605,7 +1666,7 @@ function sanitizeAnalysis(analysis, elementMap, requestedFeature) {
|
|
|
1605
1666
|
resolvedPrefix
|
|
1606
1667
|
).slice(0, intentProfile.maxScenarios);
|
|
1607
1668
|
const useAuthFallback = shouldUseAuthFallback(authElements, intentAlignedScenarios, intentProfile);
|
|
1608
|
-
const fallbackScenarios = intentProfile.mode === "presence" ? buildPresenceOnlyScenarios(elementMap, requestedFeature, resolvedPrefix) : useAuthFallback ? buildAuthFallbackScenarios(elementMap, resolvedPrefix, authElements) : buildFallbackScenarios(elementMap, resolvedPrefix);
|
|
1669
|
+
const fallbackScenarios = intentProfile.mode === "presence" ? buildPresenceOnlyScenarios(elementMap, requestedFeature, resolvedPrefix) : useAuthFallback ? buildAuthFallbackScenarios(elementMap, resolvedPrefix, authElements, requestedFeature) : buildFallbackScenarios(elementMap, resolvedPrefix, requestedFeature);
|
|
1609
1670
|
const finalScenarios = useAuthFallback ? fallbackScenarios.slice(0, intentProfile.maxScenarios) : intentAlignedScenarios.length > 0 ? intentAlignedScenarios : fallbackScenarios.slice(0, intentProfile.maxScenarios);
|
|
1610
1671
|
return {
|
|
1611
1672
|
...analysis,
|
|
@@ -1671,13 +1732,72 @@ function normalizeAssertion(candidate, selectors) {
|
|
|
1671
1732
|
return assertion;
|
|
1672
1733
|
}
|
|
1673
1734
|
function buildFallbackScenarios(elementMap, prefix) {
|
|
1735
|
+
return buildFallbackScenariosForFeature(elementMap, prefix, "");
|
|
1736
|
+
}
|
|
1737
|
+
function buildFallbackScenariosForFeature(elementMap, prefix, feature) {
|
|
1738
|
+
const normalizedFeature = feature.toLowerCase();
|
|
1674
1739
|
const heading = elementMap.elements.find((element) => element.category === "heading");
|
|
1675
1740
|
const emailInput = elementMap.elements.find((element) => element.category === "input" && (element.attributes.type === "email" || /email/.test(`${element.name ?? ""} ${String(element.attributes.label ?? "")}`.toLowerCase())));
|
|
1676
1741
|
const passwordInput = elementMap.elements.find((element) => element.category === "input" && element.attributes.type === "password");
|
|
1677
1742
|
const submitButton = elementMap.elements.find((element) => element.category === "button" && /login|sign in|submit|continue/i.test(element.name ?? "")) ?? elementMap.elements.find((element) => element.category === "button");
|
|
1743
|
+
const alert = findAlertElement(elementMap);
|
|
1678
1744
|
const scenarios = [];
|
|
1679
1745
|
const tag = (value) => normalizeTag(value);
|
|
1680
1746
|
const nextId = (index) => `${prefix}-${String(900 + index).padStart(3, "0")}`;
|
|
1747
|
+
const wantsHiddenAlertOnLoad = /\b(error|alert|message)\b/.test(normalizedFeature) && /\b(not shown|not visible|hidden|not present|gone|absent)\b/.test(normalizedFeature) && /\b(first load|first loads|first loads?|page first loads|initial load|on load)\b/.test(normalizedFeature);
|
|
1748
|
+
const wantsVisibleAlert = /\b(error|alert|message|invalid|incorrect|required|validation)\b/.test(normalizedFeature) && !wantsHiddenAlertOnLoad;
|
|
1749
|
+
if (wantsHiddenAlertOnLoad && alert) {
|
|
1750
|
+
return [{
|
|
1751
|
+
id: `${prefix}-001`,
|
|
1752
|
+
title: "Error message is hidden on initial load",
|
|
1753
|
+
tags: [tag("negative"), tag("ui")],
|
|
1754
|
+
steps: [
|
|
1755
|
+
{
|
|
1756
|
+
action: "navigate",
|
|
1757
|
+
selector: "page",
|
|
1758
|
+
value: elementMap.url,
|
|
1759
|
+
human: "Navigate to the captured page"
|
|
1760
|
+
}
|
|
1761
|
+
],
|
|
1762
|
+
assertions: [
|
|
1763
|
+
{
|
|
1764
|
+
type: "hidden",
|
|
1765
|
+
selector: alert.selector,
|
|
1766
|
+
expected: "hidden",
|
|
1767
|
+
human: `${alert.name || "Error message"} is not shown when the page first loads`,
|
|
1768
|
+
playwright: `await expect(page.${alert.selector}).toBeHidden();`
|
|
1769
|
+
}
|
|
1770
|
+
],
|
|
1771
|
+
narrator: "We verify that the page does not expose an error surface before any user action.",
|
|
1772
|
+
codeLevel: "beginner"
|
|
1773
|
+
}];
|
|
1774
|
+
}
|
|
1775
|
+
if (wantsVisibleAlert && alert) {
|
|
1776
|
+
return [{
|
|
1777
|
+
id: `${prefix}-001`,
|
|
1778
|
+
title: "Page exposes an alert message when requested",
|
|
1779
|
+
tags: [tag("negative"), tag("ui")],
|
|
1780
|
+
steps: [
|
|
1781
|
+
{
|
|
1782
|
+
action: "navigate",
|
|
1783
|
+
selector: "page",
|
|
1784
|
+
value: elementMap.url,
|
|
1785
|
+
human: "Navigate to the captured page"
|
|
1786
|
+
}
|
|
1787
|
+
],
|
|
1788
|
+
assertions: [
|
|
1789
|
+
{
|
|
1790
|
+
type: "text",
|
|
1791
|
+
selector: alert.selector,
|
|
1792
|
+
expected: alert.name || "error",
|
|
1793
|
+
human: "An alert message is shown",
|
|
1794
|
+
playwright: `await expect(page.${alert.selector}).toContainText(/invalid|error|required/i);`
|
|
1795
|
+
}
|
|
1796
|
+
],
|
|
1797
|
+
narrator: "We validate the alert channel directly when the intent asks about an error message.",
|
|
1798
|
+
codeLevel: "beginner"
|
|
1799
|
+
}];
|
|
1800
|
+
}
|
|
1681
1801
|
if (heading) {
|
|
1682
1802
|
scenarios.push({
|
|
1683
1803
|
id: nextId(scenarios.length + 1),
|
|
@@ -1752,16 +1872,115 @@ function buildFallbackScenarios(elementMap, prefix) {
|
|
|
1752
1872
|
}
|
|
1753
1873
|
return scenarios.slice(0, 5);
|
|
1754
1874
|
}
|
|
1755
|
-
function buildAuthFallbackScenarios(elementMap, prefix, authElements) {
|
|
1875
|
+
function buildAuthFallbackScenarios(elementMap, prefix, authElements, feature) {
|
|
1756
1876
|
const { usernameInput, passwordInput, submitButton, heading } = authElements;
|
|
1757
1877
|
if (!usernameInput || !passwordInput || !submitButton) {
|
|
1758
|
-
return
|
|
1878
|
+
return buildFallbackScenariosForFeature(elementMap, prefix, feature);
|
|
1759
1879
|
}
|
|
1880
|
+
const normalizedFeature = feature.toLowerCase();
|
|
1760
1881
|
const scenarios = [];
|
|
1761
1882
|
const tag = (value) => normalizeTag(value);
|
|
1762
1883
|
const nextId = (index) => `${prefix}-${String(index).padStart(3, "0")}`;
|
|
1763
1884
|
const usernameValue = inferAuthValue(usernameInput, "username");
|
|
1764
1885
|
const passwordValue = inferAuthValue(passwordInput, "password");
|
|
1886
|
+
const alert = findAlertElement(elementMap);
|
|
1887
|
+
const wantsNavigation = /\b(valid credentials|correct credentials|successful login|log in successfully|login succeeds|sign in succeeds)\b/.test(normalizedFeature) || /\b(redirect|redirects|redirected|secure area|dashboard|redirected to|goes to|navigates? to)\b/.test(normalizedFeature);
|
|
1888
|
+
const wantsError = /\b(wrong password|wrong credentials|invalid|incorrect|error message|shows an error|alert)\b/.test(normalizedFeature);
|
|
1889
|
+
const successUrlPattern = deriveSuccessUrlPattern(elementMap, normalizedFeature);
|
|
1890
|
+
if (wantsNavigation) {
|
|
1891
|
+
return [{
|
|
1892
|
+
id: `${prefix}-001`,
|
|
1893
|
+
title: "Valid credentials redirect to the authenticated area",
|
|
1894
|
+
tags: [tag("auth"), tag("happy-path")],
|
|
1895
|
+
steps: [
|
|
1896
|
+
{
|
|
1897
|
+
action: "navigate",
|
|
1898
|
+
selector: "page",
|
|
1899
|
+
value: elementMap.url,
|
|
1900
|
+
human: "Navigate to the login page"
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
action: "fill",
|
|
1904
|
+
selector: usernameInput.selector,
|
|
1905
|
+
value: usernameValue,
|
|
1906
|
+
human: buildFillHuman("Fill in the username field", usernameInput.selector, usernameValue)
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
action: "fill",
|
|
1910
|
+
selector: passwordInput.selector,
|
|
1911
|
+
value: passwordValue,
|
|
1912
|
+
human: buildFillHuman("Fill in the password field", passwordInput.selector, passwordValue)
|
|
1913
|
+
},
|
|
1914
|
+
{
|
|
1915
|
+
action: "click",
|
|
1916
|
+
selector: submitButton.selector,
|
|
1917
|
+
value: "",
|
|
1918
|
+
human: "Click the login button"
|
|
1919
|
+
}
|
|
1920
|
+
],
|
|
1921
|
+
assertions: [
|
|
1922
|
+
{
|
|
1923
|
+
type: "url",
|
|
1924
|
+
selector: "page",
|
|
1925
|
+
expected: successUrlPattern,
|
|
1926
|
+
human: "User is redirected to the secure area",
|
|
1927
|
+
playwright: `await expect(page).toHaveURL(/${successUrlPattern}/);`
|
|
1928
|
+
}
|
|
1929
|
+
],
|
|
1930
|
+
narrator: "We submit valid credentials and confirm that authentication changes the page URL.",
|
|
1931
|
+
codeLevel: "beginner"
|
|
1932
|
+
}];
|
|
1933
|
+
}
|
|
1934
|
+
if (wantsError) {
|
|
1935
|
+
return [{
|
|
1936
|
+
id: `${prefix}-001`,
|
|
1937
|
+
title: "Wrong password shows an authentication error",
|
|
1938
|
+
tags: [tag("auth"), tag("negative")],
|
|
1939
|
+
steps: [
|
|
1940
|
+
{
|
|
1941
|
+
action: "navigate",
|
|
1942
|
+
selector: "page",
|
|
1943
|
+
value: elementMap.url,
|
|
1944
|
+
human: "Navigate to the login page"
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
action: "fill",
|
|
1948
|
+
selector: usernameInput.selector,
|
|
1949
|
+
value: usernameValue,
|
|
1950
|
+
human: buildFillHuman("Fill in the username field", usernameInput.selector, usernameValue)
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
action: "fill",
|
|
1954
|
+
selector: passwordInput.selector,
|
|
1955
|
+
value: "wrong-password",
|
|
1956
|
+
human: buildFillHuman("Fill in the password field", passwordInput.selector, "wrong-password")
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
action: "click",
|
|
1960
|
+
selector: submitButton.selector,
|
|
1961
|
+
value: "",
|
|
1962
|
+
human: "Click the login button"
|
|
1963
|
+
}
|
|
1964
|
+
],
|
|
1965
|
+
assertions: [
|
|
1966
|
+
alert ? {
|
|
1967
|
+
type: "text",
|
|
1968
|
+
selector: alert.selector,
|
|
1969
|
+
expected: alert.name || "error",
|
|
1970
|
+
human: "An alert communicates the login failure",
|
|
1971
|
+
playwright: `await expect(page.${alert.selector}).toContainText(/invalid|error|required/i);`
|
|
1972
|
+
} : {
|
|
1973
|
+
type: "text",
|
|
1974
|
+
selector: usernameInput.selector,
|
|
1975
|
+
expected: "error",
|
|
1976
|
+
human: "An authentication error message is shown",
|
|
1977
|
+
playwright: `await expect(page.getByRole('alert')).toContainText(/invalid|error|required/i);`
|
|
1978
|
+
}
|
|
1979
|
+
],
|
|
1980
|
+
narrator: "We use an invalid password and confirm the page surfaces an authentication error.",
|
|
1981
|
+
codeLevel: "beginner"
|
|
1982
|
+
}];
|
|
1983
|
+
}
|
|
1765
1984
|
scenarios.push({
|
|
1766
1985
|
id: nextId(1),
|
|
1767
1986
|
title: "Login form renders expected controls",
|
|
@@ -2084,6 +2303,53 @@ function detectAuthElements(elementMap) {
|
|
|
2084
2303
|
const heading = elementMap.elements.find((element) => element.category === "heading" && /login|sign in|auth/.test((element.name ?? "").toLowerCase())) ?? elementMap.elements.find((element) => element.category === "heading");
|
|
2085
2304
|
return { usernameInput, passwordInput, submitButton, heading };
|
|
2086
2305
|
}
|
|
2306
|
+
function findAlertElement(elementMap) {
|
|
2307
|
+
return elementMap.elements.find((element) => element.role === "alert" || element.category === "status" || String(element.attributes.role ?? "").toLowerCase() === "alert");
|
|
2308
|
+
}
|
|
2309
|
+
function deriveSuccessUrlPattern(elementMap, normalizedFeature) {
|
|
2310
|
+
const explicitPath = normalizedFeature.match(/\/[a-z0-9/_-]+/i)?.[0] ?? (/\bsecure area\b/.test(normalizedFeature) ? "/secure" : void 0);
|
|
2311
|
+
if (explicitPath) {
|
|
2312
|
+
return explicitPath.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/");
|
|
2313
|
+
}
|
|
2314
|
+
const authLink = elementMap.elements.find((element) => {
|
|
2315
|
+
if (element.category !== "link") return false;
|
|
2316
|
+
const href2 = String(element.attributes.href ?? "");
|
|
2317
|
+
const label = `${element.name ?? ""} ${href2}`.toLowerCase();
|
|
2318
|
+
return /\b(secure|dashboard|home|app)\b/.test(label);
|
|
2319
|
+
});
|
|
2320
|
+
const href = String(authLink?.attributes.href ?? "");
|
|
2321
|
+
if (href.startsWith("/")) {
|
|
2322
|
+
return href.replace(/^\/+/, "").split("/").map(escapeForRegex).join("\\/");
|
|
2323
|
+
}
|
|
2324
|
+
return "secure|dashboard|home|app";
|
|
2325
|
+
}
|
|
2326
|
+
function buildDeterministicAnalysis(elementMap, requestedFeature) {
|
|
2327
|
+
const intentProfile = buildIntentProfile(requestedFeature);
|
|
2328
|
+
const suggestedPrefix = inferPrefix({
|
|
2329
|
+
featureText: requestedFeature,
|
|
2330
|
+
url: elementMap.url
|
|
2331
|
+
}).toUpperCase();
|
|
2332
|
+
const authElements = detectAuthElements(elementMap);
|
|
2333
|
+
let scenarios = intentProfile.mode === "count" ? buildCountScenarios(elementMap, requestedFeature, suggestedPrefix) : intentProfile.mode === "presence" ? buildPresenceOnlyScenarios(elementMap, requestedFeature, suggestedPrefix) : intentProfile.mode === "auth" || intentProfile.mode === "form" ? buildAuthFallbackScenarios(elementMap, suggestedPrefix, authElements, requestedFeature) : buildFallbackScenariosForFeature(elementMap, suggestedPrefix, requestedFeature);
|
|
2334
|
+
if (scenarios.length === 0 && (authElements.usernameInput || authElements.passwordInput || authElements.submitButton)) {
|
|
2335
|
+
scenarios = buildAuthFallbackScenarios(elementMap, suggestedPrefix, authElements, requestedFeature);
|
|
2336
|
+
}
|
|
2337
|
+
if (scenarios.length === 0) {
|
|
2338
|
+
scenarios = buildFallbackScenariosForFeature(elementMap, suggestedPrefix, requestedFeature);
|
|
2339
|
+
}
|
|
2340
|
+
const finalScenarios = scenarios.slice(0, intentProfile.maxScenarios);
|
|
2341
|
+
return {
|
|
2342
|
+
url: elementMap.url,
|
|
2343
|
+
feature: requestedFeature,
|
|
2344
|
+
suggestedPrefix,
|
|
2345
|
+
scenarios: finalScenarios,
|
|
2346
|
+
analysisNotes: "Generated deterministic capture-backed scenarios because LLM analysis was unavailable.",
|
|
2347
|
+
audioSummary: buildAudioSummary(requestedFeature, finalScenarios)
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
function shortErrorMessage(error) {
|
|
2351
|
+
return String(error?.message ?? error ?? "Unknown error").split("\n")[0];
|
|
2352
|
+
}
|
|
2087
2353
|
function shouldUseAuthFallback(authElements, scenarios, intentProfile) {
|
|
2088
2354
|
if (intentProfile.mode !== "auth" && intentProfile.mode !== "form") return false;
|
|
2089
2355
|
if (!authElements.usernameInput || !authElements.passwordInput || !authElements.submitButton) return false;
|