@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.
Files changed (2) hide show
  1. package/dist/index.js +93 -23
  2. 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
- if (this.page) {
6625
- const pageUrl = this.page.url();
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 elements = learnedSelectors.map((ls) => ({
8388
- elementDescription: ls.elementDescription || "Unknown element",
8389
- strategies: ls.strategies.map((s) => ({
8390
- type: s.type || "css",
8391
- value: s.value || "",
8392
- playwrightLocator: s.playwrightLocator || s.value || "",
8393
- confidence: s.confidence ?? 0.8
8394
- }))
8395
- }));
8396
- const validElements = elements.filter((e) => e.strategies.length > 0);
8397
- const result = await learnSelectors(
8398
- projectId,
8399
- targetUrl || "",
8400
- validElements
8401
- );
8402
- const count = result.created.elements + result.updated.elements;
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 ${count} selector${count === 1 ? "" : "s"} to library`
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.3",
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) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.3",
6
+ "version": "0.1.4",
7
7
  "description": "TestBro CLI - AI-powered browser testing from your terminal",
8
8
  "type": "module",
9
9
  "bin": {