@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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  genCmd
4
- } from "./chunk-RG26I5FB.js";
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
- 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.");
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
- if (legacyMacOs) {
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
- 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.");
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(" npx playwright install chromium");
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-6Y65IYXO.js");
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
- const docContext = await fetchDocContext2(requestedFeature);
1373
- const systemPrompt = buildSystemPrompt() + docContext;
1374
- const userPrompt = buildUserPrompt(elementMap, requestedFeature);
1375
- const rawResponse = providerConfig.transport === "anthropic" ? await callAnthropic2(providerConfig.apiKey, providerConfig.model, systemPrompt, userPrompt) : await callOpenAiCompatible2(
1376
- providerConfig.apiKey,
1377
- providerConfig.model,
1378
- providerConfig.baseUrl ?? "https://api.openai.com/v1",
1379
- providerConfig.displayName,
1380
- systemPrompt,
1381
- userPrompt
1382
- );
1383
- const parsed = parseAnalysisJson(rawResponse);
1384
- return sanitizeAnalysis(parsed, elementMap, requestedFeature);
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 buildFallbackScenarios(elementMap, prefix);
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;