@cementic/cementic-test 0.2.14 → 0.2.16
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/CHANGELOG.md +84 -0
- package/README.md +27 -3
- package/dist/capture.js +18 -0
- package/dist/{chunk-3S26OWNR.js → chunk-PYKYHIO3.js} +72 -9
- package/dist/chunk-PYKYHIO3.js.map +1 -0
- package/dist/chunk-WUGSOKKY.js +622 -0
- package/dist/chunk-WUGSOKKY.js.map +1 -0
- package/dist/cli.js +76 -594
- package/dist/cli.js.map +1 -1
- package/dist/{gen-AGWFMHTO.js → gen-WYG3JOGJ.js} +2 -2
- package/dist/gen-WYG3JOGJ.js.map +1 -0
- package/package.json +4 -3
- package/dist/chunk-3S26OWNR.js.map +0 -1
- /package/dist/{gen-AGWFMHTO.js.map → capture.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
captureElements,
|
|
4
|
+
formatCaptureFailure,
|
|
5
|
+
toPageSummary
|
|
6
|
+
} from "./chunk-WUGSOKKY.js";
|
|
2
7
|
import {
|
|
3
8
|
genCmd
|
|
4
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-PYKYHIO3.js";
|
|
5
10
|
|
|
6
11
|
// src/cli.ts
|
|
7
12
|
import { Command as Command9 } from "commander";
|
|
8
|
-
import { createRequire
|
|
13
|
+
import { createRequire } from "module";
|
|
9
14
|
|
|
10
15
|
// src/commands/new.ts
|
|
11
16
|
import { Command } from "commander";
|
|
@@ -365,7 +370,7 @@ Examples:
|
|
|
365
370
|
}
|
|
366
371
|
console.log(`\u2705 Normalized ${index.summary.parsed} case(s). Output \u2192 .cementic/normalized/`);
|
|
367
372
|
if (opts.andGen) {
|
|
368
|
-
const { gen } = await import("./gen-
|
|
373
|
+
const { gen } = await import("./gen-WYG3JOGJ.js");
|
|
369
374
|
await gen({ lang: opts.lang || "ts", out: "tests/generated" });
|
|
370
375
|
}
|
|
371
376
|
});
|
|
@@ -396,7 +401,7 @@ function testCmd() {
|
|
|
396
401
|
// src/commands/tc.ts
|
|
397
402
|
import { Command as Command4 } from "commander";
|
|
398
403
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
399
|
-
import { join as
|
|
404
|
+
import { join as join4 } from "path";
|
|
400
405
|
import { createInterface } from "readline/promises";
|
|
401
406
|
import { stdin as input, stdout as output } from "process";
|
|
402
407
|
|
|
@@ -1463,10 +1468,11 @@ RULES:
|
|
|
1463
1468
|
9. For presence-only intents such as "verify X" or "X is present", only assert visibility. Do not click and do not assert outcomes after clicking.
|
|
1464
1469
|
${RULE_10_PLAYWRIGHT_KNOWLEDGE_BASE2}
|
|
1465
1470
|
11. For fill steps, never use the literal word "value". If matching CT_VAR_* variables are listed in the prompt, use them in human-readable step text and use a realistic non-generic runtime value in the JSON value field. Otherwise use a field-specific fallback such as test-username or test-password.
|
|
1466
|
-
12.
|
|
1467
|
-
13.
|
|
1468
|
-
14.
|
|
1469
|
-
15.
|
|
1471
|
+
12. When a CTA could realistically be a button or a link, prefer a resilient selector such as getByRole('button', { name: /Join Now/i }).or(page.getByRole('link', { name: /Join Now/i })).
|
|
1472
|
+
13. The "selector" field must be a raw selector expression such as locator("#username") or getByRole('button', { name: "Login" }). Never wrap selectors in page. or page.locator("...") inside JSON, except inside Locator.or(...) where the alternate locator must be page-scoped.
|
|
1473
|
+
14. Tie-breaker for multiple matching elements: prefer highest confidence, then interactive elements, then the first visible-looking candidate.
|
|
1474
|
+
15. 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 when the intent asks for auth interaction.
|
|
1475
|
+
16. "verify title" means the main visible heading on the page, not the browser tab title, unless the intent explicitly says browser title, tab title, or page title.
|
|
1470
1476
|
|
|
1471
1477
|
OUTPUT SCHEMA:
|
|
1472
1478
|
{
|
|
@@ -2243,7 +2249,7 @@ function extractPresenceClaims(feature, elementMap) {
|
|
|
2243
2249
|
const text = extractIntentLabel(segment);
|
|
2244
2250
|
if (!text) continue;
|
|
2245
2251
|
const match = findBestMatchingElement(elementMap, text, normalized);
|
|
2246
|
-
const selector =
|
|
2252
|
+
const selector = buildIntentSelector(text, normalized, match);
|
|
2247
2253
|
claims.push({
|
|
2248
2254
|
selector,
|
|
2249
2255
|
human: `${text} is visible`
|
|
@@ -2259,16 +2265,23 @@ function chooseHeadingElement(elementMap) {
|
|
|
2259
2265
|
function extractIntentLabel(segment) {
|
|
2260
2266
|
const unquoted = Array.from(segment.matchAll(/["“”']([^"“”']+)["“”']/g)).map((match) => match[1].trim());
|
|
2261
2267
|
if (unquoted.length > 0) return unquoted[0];
|
|
2262
|
-
return segment.replace(/\b(?:verify|check|confirm|ensure)\b/gi, " ").replace(/\b(?:the|a|an)\b/gi, " ").replace(/\b(?:is|are|be|should be)\b/gi, " ").replace(/\b(?:present|visible|shown|showing|available|exists?)\b/gi, " ").replace(/\b(?:button|link|cta|label|text)\b/gi, " ").replace(/[.?!,:;]+/g, " ").replace(/\s+/g, " ").trim();
|
|
2268
|
+
return segment.replace(/\b(?:verify|check|confirm|ensure)\b/gi, " ").replace(/\b(?:the|a|an)\b/gi, " ").replace(/\b(?:is|are|be|should be)\b/gi, " ").replace(/\b(?:present|visible|shown|showing|available|exists?)\b/gi, " ").replace(/\b(?:on|in)\s+(?:the\s+)?page\b/gi, " ").replace(/\b(?:button|link|cta|label|text)\b/gi, " ").replace(/[.?!,:;]+/g, " ").replace(/\s+/g, " ").trim();
|
|
2263
2269
|
}
|
|
2264
2270
|
function findBestMatchingElement(elementMap, text, segment) {
|
|
2265
2271
|
const preferredCategory = /\bbutton\b/.test(segment) ? "button" : /\blink\b/.test(segment) ? "link" : /\bheading\b/.test(segment) ? "heading" : void 0;
|
|
2266
2272
|
const query = text.toLowerCase();
|
|
2267
2273
|
const candidates = elementMap.elements.filter((element) => {
|
|
2268
|
-
if (preferredCategory && element.category !== preferredCategory) return false;
|
|
2269
2274
|
const haystack = `${element.name ?? ""} ${element.purpose}`.toLowerCase();
|
|
2270
2275
|
return haystack.includes(query);
|
|
2271
2276
|
});
|
|
2277
|
+
if (!preferredCategory) return rankElements(candidates)[0];
|
|
2278
|
+
const preferred = candidates.filter((element) => element.category === preferredCategory);
|
|
2279
|
+
if (preferred.length > 0) return rankElements(preferred)[0];
|
|
2280
|
+
if ((preferredCategory === "button" || preferredCategory === "link") && shouldUseAmbiguousCtaSelector(segment, text)) {
|
|
2281
|
+
const alternateCategory = preferredCategory === "button" ? "link" : "button";
|
|
2282
|
+
const alternates = candidates.filter((element) => element.category === alternateCategory);
|
|
2283
|
+
if (alternates.length > 0) return rankElements(alternates)[0];
|
|
2284
|
+
}
|
|
2272
2285
|
return rankElements(candidates)[0];
|
|
2273
2286
|
}
|
|
2274
2287
|
function rankElements(elements) {
|
|
@@ -2282,11 +2295,32 @@ function rankElements(elements) {
|
|
|
2282
2295
|
return 0;
|
|
2283
2296
|
});
|
|
2284
2297
|
}
|
|
2285
|
-
function
|
|
2298
|
+
function buildIntentSelector(text, segment, match) {
|
|
2299
|
+
if (shouldUseAmbiguousCtaSelector(segment, text)) {
|
|
2300
|
+
if (/\blink\b/.test(segment)) return buildAmbiguousRoleSelector(text, "link");
|
|
2301
|
+
if (/\bbutton\b/.test(segment) || match?.category === "link" || match?.category === "button") {
|
|
2302
|
+
return buildAmbiguousRoleSelector(text, "button");
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return match?.selector ?? buildFallbackSelector(text, segment);
|
|
2306
|
+
}
|
|
2307
|
+
function shouldUseAmbiguousCtaSelector(segment, text) {
|
|
2308
|
+
const haystack = `${segment} ${text}`.toLowerCase();
|
|
2309
|
+
return /\b(?:button|link|cta)\b/.test(haystack) && (/\b(?:join|start|get started|sign up|signup|subscribe|buy|purchase|learn more|see how it works|continue)\b/.test(haystack) || /[$€£]/.test(text));
|
|
2310
|
+
}
|
|
2311
|
+
function buildAmbiguousRoleSelector(text, preferredRole) {
|
|
2312
|
+
const primary = buildRoleSelector(preferredRole, text);
|
|
2313
|
+
const alternate = buildRoleSelector(preferredRole === "button" ? "link" : "button", text);
|
|
2314
|
+
return `${primary}.or(page.${alternate})`;
|
|
2315
|
+
}
|
|
2316
|
+
function buildRoleSelector(role, text) {
|
|
2286
2317
|
const safeRegex = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2318
|
+
return `getByRole('${role}', { name: /${safeRegex}/i })`;
|
|
2319
|
+
}
|
|
2320
|
+
function buildFallbackSelector(text, segment) {
|
|
2321
|
+
if (/\bbutton\b/.test(segment)) return buildRoleSelector("button", text);
|
|
2322
|
+
if (/\blink\b/.test(segment)) return buildRoleSelector("link", text);
|
|
2323
|
+
if (/\b(?:title|heading)\b/.test(segment)) return buildRoleSelector("heading", text);
|
|
2290
2324
|
return `getByText(${JSON.stringify(text)})`;
|
|
2291
2325
|
}
|
|
2292
2326
|
function buildAudioSummary(feature, scenarios) {
|
|
@@ -2497,6 +2531,10 @@ function extractCtVarTemplate(value) {
|
|
|
2497
2531
|
function isAllowedSelector(selector, selectors) {
|
|
2498
2532
|
if (selector === "page") return true;
|
|
2499
2533
|
if (selectors.has(selector)) return true;
|
|
2534
|
+
if (/\.or\(page\.(?:getByRole|getByText|getByLabel|getByPlaceholder|getByTestId|locator)\(/.test(selector)) {
|
|
2535
|
+
const primary = selector.replace(/\.or\(page\.(?:getByRole|getByText|getByLabel|getByPlaceholder|getByTestId|locator)\([\s\S]*\)\)\s*$/, "");
|
|
2536
|
+
return isAllowedSelector(primary, selectors);
|
|
2537
|
+
}
|
|
2500
2538
|
return /^(?:getByRole|getByText|getByLabel|getByPlaceholder|getByTestId)\(/.test(selector) || /^locator\((['"])(#.+?)\1\)$/.test(selector);
|
|
2501
2539
|
}
|
|
2502
2540
|
function ensureStatement(value) {
|
|
@@ -2521,565 +2559,9 @@ function log(verbose, message) {
|
|
|
2521
2559
|
if (verbose) console.log(message);
|
|
2522
2560
|
}
|
|
2523
2561
|
|
|
2524
|
-
// src/core/playwright.ts
|
|
2525
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2526
|
-
import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
|
|
2527
|
-
import { createRequire } from "module";
|
|
2528
|
-
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
2529
|
-
var require2 = createRequire(import.meta.url);
|
|
2530
|
-
var moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
2531
|
-
async function resolvePlaywrightChromium(cwd = process.cwd()) {
|
|
2532
|
-
const searchRoots = buildSearchRoots(cwd);
|
|
2533
|
-
for (const packageName of ["@playwright/test", "playwright-core"]) {
|
|
2534
|
-
for (const searchRoot of searchRoots) {
|
|
2535
|
-
const resolvedPath = resolveFromRoot(packageName, searchRoot);
|
|
2536
|
-
if (!resolvedPath) continue;
|
|
2537
|
-
const imported = await import(pathToFileURL(resolvedPath).href);
|
|
2538
|
-
const chromium = imported.chromium ?? imported.default?.chromium;
|
|
2539
|
-
if (chromium) {
|
|
2540
|
-
return {
|
|
2541
|
-
packageName,
|
|
2542
|
-
resolvedPath,
|
|
2543
|
-
searchRoot,
|
|
2544
|
-
chromium
|
|
2545
|
-
};
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
throw new Error(
|
|
2550
|
-
"Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium"
|
|
2551
|
-
);
|
|
2552
|
-
}
|
|
2553
|
-
function buildSearchRoots(cwd) {
|
|
2554
|
-
const roots = [resolve3(cwd)];
|
|
2555
|
-
const projectRoot = findNearestProjectRoot(cwd);
|
|
2556
|
-
if (projectRoot && projectRoot !== roots[0]) {
|
|
2557
|
-
roots.push(projectRoot);
|
|
2558
|
-
}
|
|
2559
|
-
roots.push(moduleDir);
|
|
2560
|
-
return Array.from(new Set(roots));
|
|
2561
|
-
}
|
|
2562
|
-
function findNearestProjectRoot(startDir) {
|
|
2563
|
-
let currentDir = resolve3(startDir);
|
|
2564
|
-
while (true) {
|
|
2565
|
-
if (existsSync2(join3(currentDir, "package.json"))) {
|
|
2566
|
-
return currentDir;
|
|
2567
|
-
}
|
|
2568
|
-
const parentDir = dirname2(currentDir);
|
|
2569
|
-
if (parentDir === currentDir) return void 0;
|
|
2570
|
-
currentDir = parentDir;
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
function resolveFromRoot(packageName, searchRoot) {
|
|
2574
|
-
try {
|
|
2575
|
-
return require2.resolve(packageName, { paths: [searchRoot] });
|
|
2576
|
-
} catch {
|
|
2577
|
-
return void 0;
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
// src/core/capture.ts
|
|
2582
|
-
var SETTLE_MS = 1200;
|
|
2583
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
2584
|
-
var MAX_PER_CATEGORY = 50;
|
|
2585
|
-
var CaptureRuntimeError = class extends Error {
|
|
2586
|
-
constructor(code, message, options) {
|
|
2587
|
-
super(message);
|
|
2588
|
-
this.name = "CaptureRuntimeError";
|
|
2589
|
-
this.code = code;
|
|
2590
|
-
if (options?.cause !== void 0) {
|
|
2591
|
-
this.cause = options.cause;
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
};
|
|
2595
|
-
async function captureElements(url, options = {}) {
|
|
2596
|
-
const {
|
|
2597
|
-
headless = true,
|
|
2598
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
2599
|
-
verbose = false,
|
|
2600
|
-
userAgent = "Mozilla/5.0 (compatible; CementicTest/0.2.7 capture)"
|
|
2601
|
-
} = options;
|
|
2602
|
-
const chromium = await loadChromium();
|
|
2603
|
-
const mode = headless ? "headless" : "headed";
|
|
2604
|
-
log2(verbose, `
|
|
2605
|
-
[capture] Starting ${mode} capture: ${url}`);
|
|
2606
|
-
let browser;
|
|
2607
|
-
try {
|
|
2608
|
-
browser = await chromium.launch({
|
|
2609
|
-
headless,
|
|
2610
|
-
slowMo: headless ? 0 : 150
|
|
2611
|
-
});
|
|
2612
|
-
} catch (error) {
|
|
2613
|
-
throw classifyCaptureError(error);
|
|
2614
|
-
}
|
|
2615
|
-
const context = await browser.newContext({
|
|
2616
|
-
userAgent,
|
|
2617
|
-
viewport: { width: 1280, height: 800 },
|
|
2618
|
-
ignoreHTTPSErrors: true
|
|
2619
|
-
});
|
|
2620
|
-
const page = await context.newPage();
|
|
2621
|
-
try {
|
|
2622
|
-
log2(verbose, ` -> Navigating (timeout: ${timeoutMs}ms)`);
|
|
2623
|
-
try {
|
|
2624
|
-
await page.goto(url, {
|
|
2625
|
-
waitUntil: "domcontentloaded",
|
|
2626
|
-
timeout: timeoutMs
|
|
2627
|
-
});
|
|
2628
|
-
} catch (error) {
|
|
2629
|
-
throw new CaptureRuntimeError(
|
|
2630
|
-
"PAGE_LOAD_FAILED",
|
|
2631
|
-
`Page load failed for ${url}. ${error?.message ?? error}`,
|
|
2632
|
-
{ cause: error }
|
|
2633
|
-
);
|
|
2634
|
-
}
|
|
2635
|
-
log2(verbose, ` -> Waiting ${SETTLE_MS}ms for page settle`);
|
|
2636
|
-
await page.waitForTimeout(SETTLE_MS);
|
|
2637
|
-
log2(verbose, " -> Extracting accessibility snapshot");
|
|
2638
|
-
const a11ySnapshot = await getAccessibilitySnapshot(page, verbose);
|
|
2639
|
-
log2(verbose, " -> Extracting DOM data");
|
|
2640
|
-
const domData = await page.evaluate(extractDomData);
|
|
2641
|
-
const title = await page.title();
|
|
2642
|
-
const finalUrl = page.url();
|
|
2643
|
-
const elements = buildElementMap(domData);
|
|
2644
|
-
const warnings = buildWarnings(elements, domData, a11ySnapshot);
|
|
2645
|
-
const byCategory = {};
|
|
2646
|
-
for (const element of elements) {
|
|
2647
|
-
byCategory[element.category] = (byCategory[element.category] ?? 0) + 1;
|
|
2648
|
-
}
|
|
2649
|
-
const result = {
|
|
2650
|
-
url: finalUrl,
|
|
2651
|
-
title,
|
|
2652
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2653
|
-
mode,
|
|
2654
|
-
summary: {
|
|
2655
|
-
totalElements: elements.length,
|
|
2656
|
-
byCategory
|
|
2657
|
-
},
|
|
2658
|
-
elements,
|
|
2659
|
-
warnings
|
|
2660
|
-
};
|
|
2661
|
-
log2(verbose, ` -> Captured ${elements.length} testable elements from "${title}"`);
|
|
2662
|
-
return result;
|
|
2663
|
-
} catch (error) {
|
|
2664
|
-
throw classifyCaptureError(error);
|
|
2665
|
-
} finally {
|
|
2666
|
-
await browser?.close();
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
function toPageSummary(elementMap) {
|
|
2670
|
-
const inputs = elementMap.elements.filter((element) => element.category === "input").slice(0, 30).map((element) => ({
|
|
2671
|
-
label: asString(element.attributes.label),
|
|
2672
|
-
placeholder: asString(element.attributes.placeholder),
|
|
2673
|
-
name: asString(element.attributes.name),
|
|
2674
|
-
type: asString(element.attributes.type),
|
|
2675
|
-
testId: asString(element.attributes.testId)
|
|
2676
|
-
}));
|
|
2677
|
-
return {
|
|
2678
|
-
url: elementMap.url,
|
|
2679
|
-
title: elementMap.title,
|
|
2680
|
-
headings: elementMap.elements.filter((element) => element.category === "heading").map((element) => element.name ?? "").filter(Boolean).slice(0, 20),
|
|
2681
|
-
buttons: elementMap.elements.filter((element) => element.category === "button").map((element) => element.name ?? "").filter(Boolean).slice(0, 30),
|
|
2682
|
-
links: elementMap.elements.filter((element) => element.category === "link").map((element) => element.name ?? "").filter(Boolean).slice(0, 50),
|
|
2683
|
-
inputs,
|
|
2684
|
-
landmarks: [],
|
|
2685
|
-
rawLength: elementMap.elements.length
|
|
2686
|
-
};
|
|
2687
|
-
}
|
|
2688
|
-
async function loadChromium() {
|
|
2689
|
-
try {
|
|
2690
|
-
const resolution = await resolvePlaywrightChromium(process.cwd());
|
|
2691
|
-
return resolution.chromium;
|
|
2692
|
-
} catch (error) {
|
|
2693
|
-
throw new CaptureRuntimeError(
|
|
2694
|
-
"PLAYWRIGHT_NOT_FOUND",
|
|
2695
|
-
error?.message ?? "Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium",
|
|
2696
|
-
{ cause: error }
|
|
2697
|
-
);
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
function extractDomData() {
|
|
2701
|
-
const localMaxPerCategory = 50;
|
|
2702
|
-
const attr = (el, name) => el.getAttribute(name)?.trim() || void 0;
|
|
2703
|
-
const text = (el) => el?.textContent?.replace(/\s+/g, " ").trim() || void 0;
|
|
2704
|
-
const jsString = (value) => JSON.stringify(value);
|
|
2705
|
-
const findLabel = (el) => {
|
|
2706
|
-
const id = attr(el, "id");
|
|
2707
|
-
if (id) {
|
|
2708
|
-
const labelEl = document.querySelector(`label[for="${id}"]`);
|
|
2709
|
-
if (labelEl) return text(labelEl);
|
|
2710
|
-
}
|
|
2711
|
-
const ariaLabel = attr(el, "aria-label");
|
|
2712
|
-
if (ariaLabel) return ariaLabel;
|
|
2713
|
-
const labelledBy = attr(el, "aria-labelledby");
|
|
2714
|
-
if (labelledBy) {
|
|
2715
|
-
const labelEl = document.getElementById(labelledBy);
|
|
2716
|
-
if (labelEl) return text(labelEl);
|
|
2717
|
-
}
|
|
2718
|
-
const closestLabel = el.closest("label");
|
|
2719
|
-
if (closestLabel) {
|
|
2720
|
-
const raw = text(closestLabel) || "";
|
|
2721
|
-
const placeholder = attr(el, "placeholder") || "";
|
|
2722
|
-
return raw.replace(placeholder, "").trim() || void 0;
|
|
2723
|
-
}
|
|
2724
|
-
const previous = el.previousElementSibling;
|
|
2725
|
-
if (previous?.tagName === "LABEL") return text(previous);
|
|
2726
|
-
return void 0;
|
|
2727
|
-
};
|
|
2728
|
-
const buildSelector = (el, labelText, buttonText) => {
|
|
2729
|
-
const testId = attr(el, "data-testid");
|
|
2730
|
-
if (testId) return `getByTestId(${jsString(testId)})`;
|
|
2731
|
-
const id = attr(el, "id");
|
|
2732
|
-
if (id && !id.match(/^(ember|react|vue|ng|auto|rand)/i)) {
|
|
2733
|
-
return `locator(${jsString(`#${id}`)})`;
|
|
2734
|
-
}
|
|
2735
|
-
const ariaLabel = attr(el, "aria-label");
|
|
2736
|
-
if (ariaLabel) {
|
|
2737
|
-
return `getByRole(${jsString(el.getAttribute("role") || el.tagName.toLowerCase())}, { name: ${jsString(ariaLabel)} })`;
|
|
2738
|
-
}
|
|
2739
|
-
if (labelText) {
|
|
2740
|
-
const tag = el.tagName.toLowerCase();
|
|
2741
|
-
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
2742
|
-
return `getByLabel(${jsString(labelText)})`;
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
if (buttonText) return `getByRole('button', { name: ${jsString(buttonText)} })`;
|
|
2746
|
-
const name = attr(el, "name");
|
|
2747
|
-
if (name) return `locator(${jsString(`[name="${name}"]`)})`;
|
|
2748
|
-
return null;
|
|
2749
|
-
};
|
|
2750
|
-
const buttons = [];
|
|
2751
|
-
document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]').forEach((el) => {
|
|
2752
|
-
const buttonText = attr(el, "aria-label") || text(el) || attr(el, "value");
|
|
2753
|
-
const testId = attr(el, "data-testid");
|
|
2754
|
-
const id = attr(el, "id");
|
|
2755
|
-
const type = attr(el, "type");
|
|
2756
|
-
const disabled = el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
2757
|
-
const selector = buildSelector(el, void 0, buttonText);
|
|
2758
|
-
if (buttonText || testId) {
|
|
2759
|
-
buttons.push({
|
|
2760
|
-
tag: el.tagName.toLowerCase(),
|
|
2761
|
-
text: buttonText,
|
|
2762
|
-
testId,
|
|
2763
|
-
id,
|
|
2764
|
-
type,
|
|
2765
|
-
disabled,
|
|
2766
|
-
selector,
|
|
2767
|
-
cssPath: testId ? `[data-testid="${testId}"]` : id ? `#${id}` : null
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
});
|
|
2771
|
-
const inputs = [];
|
|
2772
|
-
document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select').forEach((el) => {
|
|
2773
|
-
const label = findLabel(el);
|
|
2774
|
-
const placeholder = attr(el, "placeholder");
|
|
2775
|
-
const name = attr(el, "name");
|
|
2776
|
-
const id = attr(el, "id");
|
|
2777
|
-
const type = attr(el, "type") || el.tagName.toLowerCase();
|
|
2778
|
-
const testId = attr(el, "data-testid");
|
|
2779
|
-
const required = el.hasAttribute("required");
|
|
2780
|
-
const selector = buildSelector(el, label);
|
|
2781
|
-
if (label || placeholder || name || testId || id) {
|
|
2782
|
-
inputs.push({
|
|
2783
|
-
tag: el.tagName.toLowerCase(),
|
|
2784
|
-
type,
|
|
2785
|
-
label,
|
|
2786
|
-
placeholder,
|
|
2787
|
-
name,
|
|
2788
|
-
id,
|
|
2789
|
-
testId,
|
|
2790
|
-
required,
|
|
2791
|
-
selector,
|
|
2792
|
-
cssPath: testId ? `[data-testid="${testId}"]` : id ? `#${id}` : name ? `[name="${name}"]` : null
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
});
|
|
2796
|
-
const links = [];
|
|
2797
|
-
document.querySelectorAll("a[href]").forEach((el) => {
|
|
2798
|
-
const linkText = attr(el, "aria-label") || text(el);
|
|
2799
|
-
const href = attr(el, "href");
|
|
2800
|
-
const testId = attr(el, "data-testid");
|
|
2801
|
-
if (!linkText || href === "#") return;
|
|
2802
|
-
links.push({
|
|
2803
|
-
text: linkText,
|
|
2804
|
-
href,
|
|
2805
|
-
testId,
|
|
2806
|
-
external: Boolean(href?.startsWith("http") && !href.includes(window.location.hostname)),
|
|
2807
|
-
selector: testId ? `getByTestId(${jsString(testId)})` : `getByRole('link', { name: ${jsString(linkText)} })`
|
|
2808
|
-
});
|
|
2809
|
-
});
|
|
2810
|
-
const headings = [];
|
|
2811
|
-
document.querySelectorAll("h1, h2, h3").forEach((el) => {
|
|
2812
|
-
const headingText = text(el);
|
|
2813
|
-
if (headingText) {
|
|
2814
|
-
headings.push({
|
|
2815
|
-
level: el.tagName.toLowerCase(),
|
|
2816
|
-
text: headingText,
|
|
2817
|
-
selector: `getByRole('heading', { name: ${jsString(headingText)} })`
|
|
2818
|
-
});
|
|
2819
|
-
}
|
|
2820
|
-
});
|
|
2821
|
-
const landmarks = [];
|
|
2822
|
-
document.querySelectorAll("[role], main, nav, header, footer, aside, section[aria-label]").forEach((el) => {
|
|
2823
|
-
const role = attr(el, "role") || el.tagName.toLowerCase();
|
|
2824
|
-
const label = attr(el, "aria-label") || text(el)?.slice(0, 40);
|
|
2825
|
-
if (role && label) landmarks.push({ role, label });
|
|
2826
|
-
});
|
|
2827
|
-
const statusRegions = [];
|
|
2828
|
-
document.querySelectorAll('[role="alert"], [role="status"], [aria-live]').forEach((el) => {
|
|
2829
|
-
const role = attr(el, "role") || "live";
|
|
2830
|
-
statusRegions.push({
|
|
2831
|
-
role,
|
|
2832
|
-
ariaLive: attr(el, "aria-live"),
|
|
2833
|
-
text: text(el),
|
|
2834
|
-
selector: el.getAttribute("role") ? `getByRole(${jsString(role)})` : `locator('[aria-live]')`
|
|
2835
|
-
});
|
|
2836
|
-
});
|
|
2837
|
-
const forms = [];
|
|
2838
|
-
document.querySelectorAll("form").forEach((form, index) => {
|
|
2839
|
-
forms.push({
|
|
2840
|
-
id: attr(form, "id"),
|
|
2841
|
-
label: attr(form, "aria-label") || attr(form, "aria-labelledby"),
|
|
2842
|
-
action: attr(form, "action"),
|
|
2843
|
-
method: attr(form, "method") || "get",
|
|
2844
|
-
fieldCount: form.querySelectorAll("input, textarea, select").length,
|
|
2845
|
-
index
|
|
2846
|
-
});
|
|
2847
|
-
});
|
|
2848
|
-
return {
|
|
2849
|
-
buttons: buttons.slice(0, localMaxPerCategory),
|
|
2850
|
-
inputs: inputs.slice(0, localMaxPerCategory),
|
|
2851
|
-
links: links.slice(0, localMaxPerCategory),
|
|
2852
|
-
headings: headings.slice(0, 20),
|
|
2853
|
-
landmarks,
|
|
2854
|
-
statusRegions,
|
|
2855
|
-
forms,
|
|
2856
|
-
pageUrl: window.location.href,
|
|
2857
|
-
pageTitle: document.title
|
|
2858
|
-
};
|
|
2859
|
-
}
|
|
2860
|
-
function buildElementMap(domData) {
|
|
2861
|
-
const elements = [];
|
|
2862
|
-
for (const input2 of domData.inputs) {
|
|
2863
|
-
const displayName = input2.label || input2.placeholder || input2.name || input2.testId;
|
|
2864
|
-
if (!displayName) continue;
|
|
2865
|
-
const selector = input2.selector || (input2.testId ? `getByTestId(${JSON.stringify(input2.testId)})` : null) || (input2.label ? `getByLabel(${JSON.stringify(input2.label)})` : null) || (input2.id ? `locator(${JSON.stringify(`#${input2.id}`)})` : null) || (input2.name ? `locator(${JSON.stringify(`[name="${input2.name}"]`)})` : null) || `locator(${JSON.stringify(`${input2.tag}[placeholder="${input2.placeholder ?? ""}"]`)})`;
|
|
2866
|
-
const selectorAlt = [];
|
|
2867
|
-
if (input2.id && !selector.includes(`#${input2.id}`)) selectorAlt.push(`locator(${JSON.stringify(`#${input2.id}`)})`);
|
|
2868
|
-
if (input2.name && !selector.includes(input2.name)) selectorAlt.push(`locator(${JSON.stringify(`[name="${input2.name}"]`)})`);
|
|
2869
|
-
if (input2.testId && !selector.includes(input2.testId)) selectorAlt.push(`getByTestId(${JSON.stringify(input2.testId)})`);
|
|
2870
|
-
if (input2.placeholder && !selector.includes(input2.placeholder)) selectorAlt.push(`getByPlaceholder(${JSON.stringify(input2.placeholder)})`);
|
|
2871
|
-
if (input2.label && !selector.includes(input2.label)) selectorAlt.push(`getByLabel(${JSON.stringify(input2.label)})`);
|
|
2872
|
-
elements.push({
|
|
2873
|
-
category: "input",
|
|
2874
|
-
role: input2.type === "checkbox" ? "checkbox" : "textbox",
|
|
2875
|
-
name: displayName,
|
|
2876
|
-
selector,
|
|
2877
|
-
selectorAlt,
|
|
2878
|
-
purpose: input2.required ? `Required ${input2.type} field - "${displayName}"` : `${input2.type} field - "${displayName}"`,
|
|
2879
|
-
confidence: input2.testId || input2.label || input2.id ? "high" : input2.placeholder ? "medium" : "low",
|
|
2880
|
-
attributes: {
|
|
2881
|
-
type: input2.type,
|
|
2882
|
-
label: input2.label,
|
|
2883
|
-
placeholder: input2.placeholder,
|
|
2884
|
-
name: input2.name,
|
|
2885
|
-
id: input2.id,
|
|
2886
|
-
testId: input2.testId,
|
|
2887
|
-
required: input2.required,
|
|
2888
|
-
tag: input2.tag
|
|
2889
|
-
}
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
for (const button of domData.buttons) {
|
|
2893
|
-
const displayName = button.text || button.testId;
|
|
2894
|
-
if (!displayName) continue;
|
|
2895
|
-
const selector = button.selector || (button.testId ? `getByTestId(${JSON.stringify(button.testId)})` : null) || (button.text ? `getByRole('button', { name: ${JSON.stringify(button.text)} })` : null) || (button.id ? `locator(${JSON.stringify(`#${button.id}`)})` : null) || `locator('button')`;
|
|
2896
|
-
const selectorAlt = [];
|
|
2897
|
-
if (button.id && !selector.includes(button.id)) selectorAlt.push(`locator(${JSON.stringify(`#${button.id}`)})`);
|
|
2898
|
-
if (button.testId && !selector.includes(button.testId)) selectorAlt.push(`getByTestId(${JSON.stringify(button.testId)})`);
|
|
2899
|
-
if (button.text && !selector.includes(button.text)) selectorAlt.push(`getByText(${JSON.stringify(button.text)})`);
|
|
2900
|
-
if (button.cssPath) selectorAlt.push(`locator(${JSON.stringify(button.cssPath)})`);
|
|
2901
|
-
elements.push({
|
|
2902
|
-
category: "button",
|
|
2903
|
-
role: "button",
|
|
2904
|
-
name: displayName,
|
|
2905
|
-
selector,
|
|
2906
|
-
selectorAlt,
|
|
2907
|
-
purpose: button.disabled ? `Disabled button - "${displayName}"` : button.type === "submit" ? `Form submit button - "${displayName}"` : `Button - "${displayName}"`,
|
|
2908
|
-
confidence: button.testId || button.text ? "high" : button.id ? "medium" : "low",
|
|
2909
|
-
attributes: {
|
|
2910
|
-
text: button.text,
|
|
2911
|
-
testId: button.testId,
|
|
2912
|
-
id: button.id,
|
|
2913
|
-
type: button.type,
|
|
2914
|
-
disabled: button.disabled,
|
|
2915
|
-
tag: button.tag
|
|
2916
|
-
}
|
|
2917
|
-
});
|
|
2918
|
-
}
|
|
2919
|
-
for (const link of domData.links) {
|
|
2920
|
-
elements.push({
|
|
2921
|
-
category: "link",
|
|
2922
|
-
role: "link",
|
|
2923
|
-
name: link.text,
|
|
2924
|
-
selector: link.selector,
|
|
2925
|
-
selectorAlt: link.testId ? [`getByTestId(${JSON.stringify(link.testId)})`] : [],
|
|
2926
|
-
purpose: link.external ? `External link to "${link.href}" - "${link.text}"` : `Internal navigation link - "${link.text}" -> ${link.href}`,
|
|
2927
|
-
confidence: link.testId ? "high" : "medium",
|
|
2928
|
-
attributes: {
|
|
2929
|
-
text: link.text,
|
|
2930
|
-
href: link.href,
|
|
2931
|
-
testId: link.testId,
|
|
2932
|
-
external: link.external
|
|
2933
|
-
}
|
|
2934
|
-
});
|
|
2935
|
-
}
|
|
2936
|
-
for (const heading of domData.headings) {
|
|
2937
|
-
elements.push({
|
|
2938
|
-
category: "heading",
|
|
2939
|
-
role: "heading",
|
|
2940
|
-
name: heading.text,
|
|
2941
|
-
selector: heading.selector,
|
|
2942
|
-
selectorAlt: [`getByText(${JSON.stringify(heading.text)})`],
|
|
2943
|
-
purpose: `Page ${heading.level} heading - use to assert the correct page or section loaded`,
|
|
2944
|
-
confidence: "medium",
|
|
2945
|
-
attributes: {
|
|
2946
|
-
level: heading.level,
|
|
2947
|
-
text: heading.text
|
|
2948
|
-
}
|
|
2949
|
-
});
|
|
2950
|
-
}
|
|
2951
|
-
for (const status of domData.statusRegions) {
|
|
2952
|
-
elements.push({
|
|
2953
|
-
category: "status",
|
|
2954
|
-
role: status.role,
|
|
2955
|
-
name: status.text || status.role,
|
|
2956
|
-
selector: status.selector,
|
|
2957
|
-
selectorAlt: [],
|
|
2958
|
-
purpose: "Live region - use to assert error messages, success toasts, and validation feedback",
|
|
2959
|
-
confidence: "medium",
|
|
2960
|
-
attributes: {
|
|
2961
|
-
role: status.role,
|
|
2962
|
-
ariaLive: status.ariaLive,
|
|
2963
|
-
currentText: status.text
|
|
2964
|
-
}
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
return elements;
|
|
2968
|
-
}
|
|
2969
|
-
function buildWarnings(elements, domData, a11ySnapshot) {
|
|
2970
|
-
const warnings = [];
|
|
2971
|
-
if (!a11ySnapshot) {
|
|
2972
|
-
warnings.push("Playwright accessibility snapshot was unavailable. Capture continued using DOM extraction only.");
|
|
2973
|
-
}
|
|
2974
|
-
const lowConfidenceInputs = elements.filter((element) => element.category === "input" && element.confidence === "low");
|
|
2975
|
-
if (lowConfidenceInputs.length > 0) {
|
|
2976
|
-
warnings.push(
|
|
2977
|
-
`${lowConfidenceInputs.length} input(s) have low-confidence selectors. Consider adding data-testid attributes to: ${lowConfidenceInputs.map((element) => element.name).filter(Boolean).join(", ")}`
|
|
2978
|
-
);
|
|
2979
|
-
}
|
|
2980
|
-
if (domData.statusRegions.length === 0) {
|
|
2981
|
-
warnings.push("No ARIA alert or status regions detected. Error message assertions may need manual selector adjustments after generation.");
|
|
2982
|
-
}
|
|
2983
|
-
for (const form of domData.forms) {
|
|
2984
|
-
const hasSubmit = domData.buttons.some((button) => button.type === "submit");
|
|
2985
|
-
if (form.fieldCount > 0 && !hasSubmit) {
|
|
2986
|
-
warnings.push(
|
|
2987
|
-
`Form ${form.id || `#${form.index}`} has ${form.fieldCount} field(s) but no detected submit button. It may use keyboard submit or a custom handler.`
|
|
2988
|
-
);
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
if (domData.links.length >= MAX_PER_CATEGORY) {
|
|
2992
|
-
warnings.push(`Link count hit the ${MAX_PER_CATEGORY} capture limit. Generation will focus on forms and buttons.`);
|
|
2993
|
-
}
|
|
2994
|
-
const interactive = elements.filter((element) => element.category === "button" || element.category === "input" || element.category === "link");
|
|
2995
|
-
if (interactive.length === 0) {
|
|
2996
|
-
warnings.push("No interactive elements detected. The page may require authentication, render later than the current settle window, or be mostly static content.");
|
|
2997
|
-
}
|
|
2998
|
-
return warnings;
|
|
2999
|
-
}
|
|
3000
|
-
function log2(verbose, message) {
|
|
3001
|
-
if (verbose) console.log(message);
|
|
3002
|
-
}
|
|
3003
|
-
async function getAccessibilitySnapshot(page, verbose) {
|
|
3004
|
-
if (!page.accessibility || typeof page.accessibility.snapshot !== "function") {
|
|
3005
|
-
log2(verbose, " -> Accessibility snapshot API unavailable; continuing with DOM-only capture");
|
|
3006
|
-
return null;
|
|
3007
|
-
}
|
|
3008
|
-
try {
|
|
3009
|
-
return await page.accessibility.snapshot({ interestingOnly: false });
|
|
3010
|
-
} catch (error) {
|
|
3011
|
-
log2(verbose, ` -> Accessibility snapshot failed (${error?.message ?? error}); continuing with DOM-only capture`);
|
|
3012
|
-
return null;
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
function asString(value) {
|
|
3016
|
-
return typeof value === "string" ? value : void 0;
|
|
3017
|
-
}
|
|
3018
|
-
function formatCaptureFailure(error) {
|
|
3019
|
-
const resolved = classifyCaptureError(error);
|
|
3020
|
-
if (resolved.code === "PLAYWRIGHT_NOT_FOUND") {
|
|
3021
|
-
return [
|
|
3022
|
-
"Playwright not resolvable.",
|
|
3023
|
-
"Playwright runtime not found in this project.",
|
|
3024
|
-
"Try:",
|
|
3025
|
-
" npm install",
|
|
3026
|
-
" npx playwright install chromium"
|
|
3027
|
-
];
|
|
3028
|
-
}
|
|
3029
|
-
if (resolved.code === "BROWSER_NOT_INSTALLED") {
|
|
3030
|
-
return [
|
|
3031
|
-
"Browser not installed.",
|
|
3032
|
-
"Playwright resolved, but Chromium is not installed for this project.",
|
|
3033
|
-
"Try:",
|
|
3034
|
-
" npx playwright install chromium"
|
|
3035
|
-
];
|
|
3036
|
-
}
|
|
3037
|
-
if (resolved.code === "PAGE_LOAD_FAILED") {
|
|
3038
|
-
return [
|
|
3039
|
-
"Page load failed.",
|
|
3040
|
-
resolved.message
|
|
3041
|
-
];
|
|
3042
|
-
}
|
|
3043
|
-
return [
|
|
3044
|
-
"Capture failed.",
|
|
3045
|
-
resolved.message
|
|
3046
|
-
];
|
|
3047
|
-
}
|
|
3048
|
-
function classifyCaptureError(error) {
|
|
3049
|
-
if (error instanceof CaptureRuntimeError) {
|
|
3050
|
-
return error;
|
|
3051
|
-
}
|
|
3052
|
-
const message = errorMessage(error);
|
|
3053
|
-
if (isBrowserInstallError(message)) {
|
|
3054
|
-
return new CaptureRuntimeError(
|
|
3055
|
-
"BROWSER_NOT_INSTALLED",
|
|
3056
|
-
"Playwright resolved, but Chromium is not installed for this project.",
|
|
3057
|
-
{ cause: error }
|
|
3058
|
-
);
|
|
3059
|
-
}
|
|
3060
|
-
if (isPlaywrightNotFoundError(message)) {
|
|
3061
|
-
return new CaptureRuntimeError(
|
|
3062
|
-
"PLAYWRIGHT_NOT_FOUND",
|
|
3063
|
-
"Playwright runtime not found in this project.",
|
|
3064
|
-
{ cause: error }
|
|
3065
|
-
);
|
|
3066
|
-
}
|
|
3067
|
-
return new CaptureRuntimeError("CAPTURE_FAILED", message || "Unknown capture failure.", { cause: error });
|
|
3068
|
-
}
|
|
3069
|
-
function isBrowserInstallError(message) {
|
|
3070
|
-
return /Executable doesn't exist|browserType\.launch: Executable doesn't exist|Please run the following command|playwright install/i.test(message);
|
|
3071
|
-
}
|
|
3072
|
-
function isPlaywrightNotFoundError(message) {
|
|
3073
|
-
return /Playwright runtime not found|Cannot find package ['"](?:@playwright\/test|playwright-core)['"]/i.test(message);
|
|
3074
|
-
}
|
|
3075
|
-
function errorMessage(error) {
|
|
3076
|
-
if (error instanceof Error) return error.message;
|
|
3077
|
-
return String(error ?? "");
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
2562
|
// src/core/report.ts
|
|
3081
2563
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3082
|
-
import { join as
|
|
2564
|
+
import { join as join3 } from "path";
|
|
3083
2565
|
function printCaptureReport(elementMap, analysis) {
|
|
3084
2566
|
console.log("");
|
|
3085
2567
|
console.log("=".repeat(60));
|
|
@@ -3111,10 +2593,10 @@ function printCaptureReport(elementMap, analysis) {
|
|
|
3111
2593
|
function saveCaptureJson(elementMap, analysis, outputDir = ".cementic/capture") {
|
|
3112
2594
|
mkdirSync3(outputDir, { recursive: true });
|
|
3113
2595
|
const fileName = `capture-${slugify(elementMap.url)}-${Date.now()}.json`;
|
|
3114
|
-
const filePath =
|
|
2596
|
+
const filePath = join3(outputDir, fileName);
|
|
3115
2597
|
writeFileSync3(filePath, JSON.stringify({
|
|
3116
2598
|
_meta: {
|
|
3117
|
-
version: "0.2.
|
|
2599
|
+
version: "0.2.16",
|
|
3118
2600
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3119
2601
|
tool: "@cementic/cementic-test"
|
|
3120
2602
|
},
|
|
@@ -3153,7 +2635,7 @@ function buildCasesMarkdown(analysis) {
|
|
|
3153
2635
|
function saveCasesMarkdown(analysis, outputDir = "cases", fileName) {
|
|
3154
2636
|
mkdirSync3(outputDir, { recursive: true });
|
|
3155
2637
|
const resolvedFileName = fileName ?? `${slugify(analysis.feature || analysis.url)}.md`;
|
|
3156
|
-
const filePath =
|
|
2638
|
+
const filePath = join3(outputDir, resolvedFileName);
|
|
3157
2639
|
writeFileSync3(filePath, buildCasesMarkdown(analysis));
|
|
3158
2640
|
return filePath;
|
|
3159
2641
|
}
|
|
@@ -3161,7 +2643,7 @@ function saveSpecPreview(analysis, outputDir = "tests/preview") {
|
|
|
3161
2643
|
if (analysis.scenarios.length === 0) return null;
|
|
3162
2644
|
mkdirSync3(outputDir, { recursive: true });
|
|
3163
2645
|
const fileName = `spec-preview-${slugify(analysis.url)}-${Date.now()}.spec.cjs`;
|
|
3164
|
-
const filePath =
|
|
2646
|
+
const filePath = join3(outputDir, fileName);
|
|
3165
2647
|
const lines = [];
|
|
3166
2648
|
lines.push("/**");
|
|
3167
2649
|
lines.push(" * CementicTest Capture Preview");
|
|
@@ -3294,7 +2776,7 @@ async function runTcInteractive(params) {
|
|
|
3294
2776
|
});
|
|
3295
2777
|
mkdirSync4("cases", { recursive: true });
|
|
3296
2778
|
const fileName = `${prefix.toLowerCase()}-${slugify2(feature)}.md`;
|
|
3297
|
-
const fullPath =
|
|
2779
|
+
const fullPath = join4("cases", fileName);
|
|
3298
2780
|
let markdown;
|
|
3299
2781
|
let pageSummary = void 0;
|
|
3300
2782
|
if (params.useAi) {
|
|
@@ -3448,15 +2930,15 @@ function reportCmd() {
|
|
|
3448
2930
|
// src/commands/serve.ts
|
|
3449
2931
|
import { Command as Command6 } from "commander";
|
|
3450
2932
|
import { spawn as spawn3 } from "child_process";
|
|
3451
|
-
import { existsSync as
|
|
3452
|
-
import { join as
|
|
2933
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2934
|
+
import { join as join5 } from "path";
|
|
3453
2935
|
function serveCmd() {
|
|
3454
2936
|
const cmd = new Command6("serve").description("Serve the Allure report").action(() => {
|
|
3455
2937
|
console.log("\u{1F4CA} Serving Allure report...");
|
|
3456
|
-
const localAllureBin =
|
|
2938
|
+
const localAllureBin = join5(process.cwd(), "node_modules", "allure-commandline", "bin", "allure");
|
|
3457
2939
|
let executable = "npx";
|
|
3458
2940
|
let args = ["allure", "serve", "./allure-results"];
|
|
3459
|
-
if (
|
|
2941
|
+
if (existsSync2(localAllureBin)) {
|
|
3460
2942
|
executable = "node";
|
|
3461
2943
|
args = [localAllureBin, "serve", "./allure-results"];
|
|
3462
2944
|
}
|
|
@@ -3479,9 +2961,9 @@ function serveCmd() {
|
|
|
3479
2961
|
// src/commands/flow.ts
|
|
3480
2962
|
import { Command as Command7 } from "commander";
|
|
3481
2963
|
import { spawn as spawn4 } from "child_process";
|
|
3482
|
-
import { resolve as
|
|
2964
|
+
import { resolve as resolve3 } from "path";
|
|
3483
2965
|
function runStep(cmd, args, stepName) {
|
|
3484
|
-
return new Promise((
|
|
2966
|
+
return new Promise((resolve4, reject) => {
|
|
3485
2967
|
console.log(`
|
|
3486
2968
|
\u{1F30A} Flow Step: ${stepName}`);
|
|
3487
2969
|
console.log(`> ${cmd} ${args.join(" ")}`);
|
|
@@ -3491,7 +2973,7 @@ function runStep(cmd, args, stepName) {
|
|
|
3491
2973
|
});
|
|
3492
2974
|
child.on("exit", (code) => {
|
|
3493
2975
|
if (code === 0) {
|
|
3494
|
-
|
|
2976
|
+
resolve4();
|
|
3495
2977
|
} else {
|
|
3496
2978
|
reject(new Error(`${stepName} failed with exit code ${code}`));
|
|
3497
2979
|
}
|
|
@@ -3500,7 +2982,7 @@ function runStep(cmd, args, stepName) {
|
|
|
3500
2982
|
}
|
|
3501
2983
|
function flowCmd() {
|
|
3502
2984
|
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) => {
|
|
3503
|
-
const cliBin =
|
|
2985
|
+
const cliBin = resolve3(process.argv[1]);
|
|
3504
2986
|
try {
|
|
3505
2987
|
await runStep(process.execPath, [cliBin, "normalize", casesDir], "Normalize Cases");
|
|
3506
2988
|
await runStep(process.execPath, [cliBin, "gen", "--lang", opts.lang], "Generate Tests");
|
|
@@ -3521,8 +3003,8 @@ function flowCmd() {
|
|
|
3521
3003
|
|
|
3522
3004
|
// src/commands/ci.ts
|
|
3523
3005
|
import { Command as Command8 } from "commander";
|
|
3524
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as
|
|
3525
|
-
import { join as
|
|
3006
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync3 } from "fs";
|
|
3007
|
+
import { join as join6 } from "path";
|
|
3526
3008
|
var WORKFLOW_CONTENT = `name: Playwright Tests
|
|
3527
3009
|
on:
|
|
3528
3010
|
push:
|
|
@@ -3553,15 +3035,15 @@ jobs:
|
|
|
3553
3035
|
`;
|
|
3554
3036
|
function ciCmd() {
|
|
3555
3037
|
const cmd = new Command8("ci").description("Generate GitHub Actions workflow for CI").action(() => {
|
|
3556
|
-
const githubDir =
|
|
3557
|
-
const workflowsDir =
|
|
3558
|
-
const workflowFile =
|
|
3038
|
+
const githubDir = join6(process.cwd(), ".github");
|
|
3039
|
+
const workflowsDir = join6(githubDir, "workflows");
|
|
3040
|
+
const workflowFile = join6(workflowsDir, "cementic.yml");
|
|
3559
3041
|
console.log("\u{1F916} Setting up CI/CD workflow...");
|
|
3560
|
-
if (!
|
|
3042
|
+
if (!existsSync3(workflowsDir)) {
|
|
3561
3043
|
mkdirSync5(workflowsDir, { recursive: true });
|
|
3562
3044
|
console.log(`Created directory: ${workflowsDir}`);
|
|
3563
3045
|
}
|
|
3564
|
-
if (
|
|
3046
|
+
if (existsSync3(workflowFile)) {
|
|
3565
3047
|
console.warn(`\u26A0\uFE0F Workflow file already exists at ${workflowFile}. Skipping.`);
|
|
3566
3048
|
return;
|
|
3567
3049
|
}
|
|
@@ -3575,8 +3057,8 @@ function ciCmd() {
|
|
|
3575
3057
|
}
|
|
3576
3058
|
|
|
3577
3059
|
// src/cli.ts
|
|
3578
|
-
var
|
|
3579
|
-
var { version } =
|
|
3060
|
+
var require2 = createRequire(import.meta.url);
|
|
3061
|
+
var { version } = require2("../package.json");
|
|
3580
3062
|
var program = new Command9();
|
|
3581
3063
|
program.name("ct").description("CementicTest CLI: cases \u2192 normalized \u2192 POM tests \u2192 Playwright").version(version);
|
|
3582
3064
|
program.addCommand(newCmd());
|