@test-bro/cli 0.1.3 → 0.1.4
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/index.js +93 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6621,13 +6621,14 @@ var HybridTestExecutor = class {
|
|
|
6621
6621
|
const strategies = generateSelectors(aiLog.element);
|
|
6622
6622
|
if (strategies.length === 0) return void 0;
|
|
6623
6623
|
const elementDescription = aiLog.action.elementDescription || "";
|
|
6624
|
-
|
|
6625
|
-
|
|
6624
|
+
const pageUrl = this.page?.url();
|
|
6625
|
+
if (pageUrl) {
|
|
6626
6626
|
const tagName = aiLog.element.tagName?.toLowerCase();
|
|
6627
6627
|
this.learnToLibrary(pageUrl, elementDescription, tagName, strategies).catch(() => {
|
|
6628
6628
|
});
|
|
6629
|
+
this.addToLocalCache(pageUrl, elementDescription, strategies);
|
|
6629
6630
|
}
|
|
6630
|
-
return { stepId, elementDescription, strategies };
|
|
6631
|
+
return { stepId, elementDescription, strategies, pageUrl };
|
|
6631
6632
|
}
|
|
6632
6633
|
/**
|
|
6633
6634
|
* Call the selector-learn API to persist learned selectors to the library.
|
|
@@ -6660,6 +6661,37 @@ var HybridTestExecutor = class {
|
|
|
6660
6661
|
} catch {
|
|
6661
6662
|
}
|
|
6662
6663
|
}
|
|
6664
|
+
/**
|
|
6665
|
+
* Add a newly learned selector to the in-memory library index so subsequent
|
|
6666
|
+
* steps in the same run can use it without a fresh API fetch.
|
|
6667
|
+
*/
|
|
6668
|
+
addToLocalCache(pageUrl, elementDescription, strategies) {
|
|
6669
|
+
if (!this.libraryIndex) return;
|
|
6670
|
+
let urlPath;
|
|
6671
|
+
try {
|
|
6672
|
+
urlPath = new URL(pageUrl).pathname;
|
|
6673
|
+
} catch {
|
|
6674
|
+
urlPath = pageUrl;
|
|
6675
|
+
}
|
|
6676
|
+
urlPath = urlPath.replace(/\/$/, "") || "/";
|
|
6677
|
+
let page = this.libraryIndex.pages.find((p) => p.urlPattern === urlPath);
|
|
6678
|
+
if (!page) {
|
|
6679
|
+
page = { urlPattern: urlPath, elements: [] };
|
|
6680
|
+
this.libraryIndex.pages.push(page);
|
|
6681
|
+
}
|
|
6682
|
+
page.elements.push({
|
|
6683
|
+
name: elementDescription,
|
|
6684
|
+
aliases: [],
|
|
6685
|
+
elementRole: null,
|
|
6686
|
+
strategies: strategies.map((s, i) => ({
|
|
6687
|
+
type: s.type,
|
|
6688
|
+
value: s.value,
|
|
6689
|
+
playwrightLocator: s.playwrightLocator,
|
|
6690
|
+
confidence: s.confidence,
|
|
6691
|
+
rank: i
|
|
6692
|
+
}))
|
|
6693
|
+
});
|
|
6694
|
+
}
|
|
6663
6695
|
/**
|
|
6664
6696
|
* Fetch the full library export and cache it for the duration of plan execution.
|
|
6665
6697
|
* Called once before executeTestPlan; failures are non-fatal.
|
|
@@ -8384,24 +8416,31 @@ async function handleHybridSelectorLearning(learnedSelectors, options) {
|
|
|
8384
8416
|
async function saveToLibrary(projectId, learnedSelectors, targetUrl, isPretty, verbose) {
|
|
8385
8417
|
const saveSpinner = isPretty ? createSpinner("Saving selectors to library...").start() : null;
|
|
8386
8418
|
try {
|
|
8387
|
-
const
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8419
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8420
|
+
for (const ls of learnedSelectors) {
|
|
8421
|
+
const url = ls.pageUrl || targetUrl || "";
|
|
8422
|
+
if (!url) continue;
|
|
8423
|
+
if (!groups.has(url)) groups.set(url, []);
|
|
8424
|
+
groups.get(url).push(ls);
|
|
8425
|
+
}
|
|
8426
|
+
let totalCount = 0;
|
|
8427
|
+
for (const [pageUrl, selectors] of groups) {
|
|
8428
|
+
const elements = selectors.map((ls) => ({
|
|
8429
|
+
elementDescription: ls.elementDescription || "Unknown element",
|
|
8430
|
+
strategies: ls.strategies.map((s) => ({
|
|
8431
|
+
type: s.type || "css",
|
|
8432
|
+
value: s.value || "",
|
|
8433
|
+
playwrightLocator: s.playwrightLocator || s.value || "",
|
|
8434
|
+
confidence: s.confidence ?? 0.8
|
|
8435
|
+
}))
|
|
8436
|
+
}));
|
|
8437
|
+
const validElements = elements.filter((e) => e.strategies.length > 0);
|
|
8438
|
+
if (validElements.length === 0) continue;
|
|
8439
|
+
const result = await learnSelectors(projectId, pageUrl, validElements);
|
|
8440
|
+
totalCount += result.created.elements + result.updated.elements;
|
|
8441
|
+
}
|
|
8403
8442
|
saveSpinner?.succeed(
|
|
8404
|
-
`Saved ${
|
|
8443
|
+
`Saved ${totalCount} selector${totalCount === 1 ? "" : "s"} to library`
|
|
8405
8444
|
);
|
|
8406
8445
|
} catch (saveError) {
|
|
8407
8446
|
saveSpinner?.fail("Failed to save selectors to library");
|
|
@@ -8435,6 +8474,7 @@ async function runCommand(options) {
|
|
|
8435
8474
|
recordTrace = false,
|
|
8436
8475
|
env: envName,
|
|
8437
8476
|
credential: credentialName,
|
|
8477
|
+
feature: featureFlag,
|
|
8438
8478
|
sequential = true
|
|
8439
8479
|
} = mergedOptions;
|
|
8440
8480
|
const mode = file && !requestedMode ? "ai" : requestedMode || "selector";
|
|
@@ -8551,6 +8591,34 @@ async function runCommand(options) {
|
|
|
8551
8591
|
}
|
|
8552
8592
|
}
|
|
8553
8593
|
}
|
|
8594
|
+
let resolvedFeatureId;
|
|
8595
|
+
if (featureFlag && projectId) {
|
|
8596
|
+
try {
|
|
8597
|
+
const { features } = await listFeatures(projectId);
|
|
8598
|
+
const byId = features.find((f) => f.id === featureFlag);
|
|
8599
|
+
if (byId) {
|
|
8600
|
+
resolvedFeatureId = byId.id;
|
|
8601
|
+
if (isPretty) log.info(`Using feature "${byId.name}" ${chalk16.dim(`(${byId.id.slice(0, 8)}...)`)}`);
|
|
8602
|
+
} else {
|
|
8603
|
+
const byName = features.find((f) => f.name.toLowerCase() === featureFlag.toLowerCase());
|
|
8604
|
+
if (byName) {
|
|
8605
|
+
resolvedFeatureId = byName.id;
|
|
8606
|
+
if (isPretty) log.info(`Using feature "${byName.name}" ${chalk16.dim(`(${byName.id.slice(0, 8)}...)`)}`);
|
|
8607
|
+
} else {
|
|
8608
|
+
if (isPretty) log.info(`Creating feature "${featureFlag}"...`);
|
|
8609
|
+
const { feature: newFeature } = await createFeature(projectId, { name: featureFlag });
|
|
8610
|
+
resolvedFeatureId = newFeature.id;
|
|
8611
|
+
if (isPretty) log.info(`Created feature "${newFeature.name}" ${chalk16.dim(`(${newFeature.id.slice(0, 8)}...)`)}`);
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
if (isPretty) log.newline();
|
|
8615
|
+
} catch (error) {
|
|
8616
|
+
if (isPretty) {
|
|
8617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8618
|
+
log.warn(`Failed to resolve feature "${featureFlag}": ${message}`);
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8554
8622
|
const { generatedFromFile, fileGeneratedTestCases } = await handleFileOption(
|
|
8555
8623
|
file,
|
|
8556
8624
|
projectId,
|
|
@@ -8580,7 +8648,8 @@ async function runCommand(options) {
|
|
|
8580
8648
|
projectConfig,
|
|
8581
8649
|
isPretty,
|
|
8582
8650
|
environment: resolvedEnvName,
|
|
8583
|
-
authEnabled: !!envAuth
|
|
8651
|
+
authEnabled: !!envAuth,
|
|
8652
|
+
featureId: resolvedFeatureId
|
|
8584
8653
|
});
|
|
8585
8654
|
await runTestExecution({
|
|
8586
8655
|
testPlan,
|
|
@@ -10758,7 +10827,7 @@ var package_default = {
|
|
|
10758
10827
|
publishConfig: {
|
|
10759
10828
|
access: "public"
|
|
10760
10829
|
},
|
|
10761
|
-
version: "0.1.
|
|
10830
|
+
version: "0.1.4",
|
|
10762
10831
|
description: "TestBro CLI - AI-powered browser testing from your terminal",
|
|
10763
10832
|
type: "module",
|
|
10764
10833
|
bin: {
|
|
@@ -11086,7 +11155,7 @@ program.command("generate").description("Generate test cases from a file or desc
|
|
|
11086
11155
|
process.exit(1);
|
|
11087
11156
|
}
|
|
11088
11157
|
});
|
|
11089
|
-
program.command("run").description("Run tests against a URL").option("-u, --url <url>", "Target URL to test (or use baseUrl from test-bro.config.json)").option("--file <path>", "Path to file (AC/PRD) to generate and run tests from").option("-d, --description <text>", "Feature description to test").option("-p, --project <id>", "Project ID to use test cases from (overrides active)").option("-t, --test-case <id>", "Run specific test case only").option("-m, --mode <mode>", "Execution mode: selector (default), ai, or hybrid", "selector").option("--headed", "Run browser in headed mode (visible)", false).option("--remote", "Run tests on remote staging environment", false).option("-f, --format <format>", "Output format: pretty or json", "pretty").option("-v, --verbose", "Show verbose output", false).option("--timeout <ms>", "Timeout per step in milliseconds", "30000").option("--learn-selectors", "Learn selectors from AI actions", false).option("--yes", "Auto-confirm selector learning", false).option("--no-dedup", "Skip deduplication check when using --file").option("--auto-merge", "Auto-apply smart merge without prompting when using --file").option("--record-video", "Record video of the test execution", false).option("--record-trace", "Capture Playwright trace for debugging", false).option("-e, --env <name>", "Use a named environment (fetches baseUrl from API)").option("--credential <name>", "Use a named credential from the vault").option("--no-sequential", "Run test cases independently (re-navigate for each)").action(async (options) => {
|
|
11158
|
+
program.command("run").description("Run tests against a URL").option("-u, --url <url>", "Target URL to test (or use baseUrl from test-bro.config.json)").option("--file <path>", "Path to file (AC/PRD) to generate and run tests from").option("-d, --description <text>", "Feature description to test").option("-p, --project <id>", "Project ID to use test cases from (overrides active)").option("-t, --test-case <id>", "Run specific test case only").option("-m, --mode <mode>", "Execution mode: selector (default), ai, or hybrid", "selector").option("--headed", "Run browser in headed mode (visible)", false).option("--remote", "Run tests on remote staging environment", false).option("-f, --format <format>", "Output format: pretty or json", "pretty").option("-v, --verbose", "Show verbose output", false).option("--timeout <ms>", "Timeout per step in milliseconds", "30000").option("--learn-selectors", "Learn selectors from AI actions", false).option("--yes", "Auto-confirm selector learning", false).option("--no-dedup", "Skip deduplication check when using --file").option("--auto-merge", "Auto-apply smart merge without prompting when using --file").option("--record-video", "Record video of the test execution", false).option("--record-trace", "Capture Playwright trace for debugging", false).option("-e, --env <name>", "Use a named environment (fetches baseUrl from API)").option("--credential <name>", "Use a named credential from the vault").option("--feature <id|name>", "Associate this run with a feature (by ID or name)").option("--no-sequential", "Run test cases independently (re-navigate for each)").action(async (options) => {
|
|
11090
11159
|
if (options.url && !validateUrl(options.url)) {
|
|
11091
11160
|
displayError("Invalid URL format", `Provided URL: ${options.url}`);
|
|
11092
11161
|
console.log(chalk29.dim(' Example: testbro run --url https://example.com --description "..."'));
|
|
@@ -11133,6 +11202,7 @@ program.command("run").description("Run tests against a URL").option("-u, --url
|
|
|
11133
11202
|
recordTrace: options.recordTrace,
|
|
11134
11203
|
env: options.env,
|
|
11135
11204
|
credential: options.credential,
|
|
11205
|
+
feature: options.feature,
|
|
11136
11206
|
sequential: options.sequential
|
|
11137
11207
|
});
|
|
11138
11208
|
} catch (error) {
|