blastproof 0.16.0 → 0.18.0

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 CHANGED
@@ -77,6 +77,8 @@ Three questions. The first one decides most cases.
77
77
 
78
78
  **A hard requirement, not a preference.** blastproof finds elements the way a screen reader does — by role, by label, by visible text. That is what removes selectors and survives redesigns. The cost is that there is deliberately no CSS or XPath fallback, so anything the accessibility tree cannot describe cannot be driven at all.
79
79
 
80
+ The name is matched **exactly first**, then by substring if nothing matches exactly — so a control named `Add` is found even when `Add New` sits above it. When several controls answer to the same name, the first one on the page wins. Give each control an accessible name no other control on the page shares; a page that cannot offer one cannot be driven unambiguously, and no matching rule fixes that.
81
+
80
82
  | works | cannot be driven |
81
83
  | --- | --- |
82
84
  | `<button>Add to cart</button>` | a `<div>` with a click handler |
package/dist/cli.js CHANGED
@@ -424,13 +424,18 @@ function describeTarget(target) {
424
424
  async function resolveTarget(page, target, resolveTimeoutMs = 2e3) {
425
425
  const candidates = [];
426
426
  if (target.role) {
427
+ if (target.name) {
428
+ candidates.push(page.getByRole(target.role, { name: target.name, exact: true }));
429
+ }
427
430
  candidates.push(page.getByRole(target.role, target.name ? { name: target.name } : {}));
428
431
  }
429
432
  if (target.name) {
433
+ candidates.push(page.getByLabel(target.name, { exact: true }));
430
434
  candidates.push(page.getByLabel(target.name));
431
435
  }
432
436
  const text = target.text ?? target.name;
433
437
  if (text) {
438
+ candidates.push(page.getByText(text, { exact: true }));
434
439
  candidates.push(page.getByText(text));
435
440
  }
436
441
  for (const candidate of candidates) {
@@ -593,6 +598,23 @@ var StepRecovery = class {
593
598
  observe(maskedSnapshot) {
594
599
  this.readable.push(normalise(maskedSnapshot));
595
600
  }
601
+ /**
602
+ * Whether anything succeeded during this step (design close-a-step D1).
603
+ *
604
+ * Reads the history `record` already keeps rather than a flag threaded
605
+ * through the loop: `record` is reached at one place, after `performAction`
606
+ * returns without throwing, so this is "what worked" and not "what was
607
+ * attempted" — a click that raised is correctly absent. A separate counter
608
+ * would be one more thing a future branch can forget to increment, which is
609
+ * the shape that let `timeoutMs` and the budget go unset at a single call
610
+ * site while every other one set them.
611
+ *
612
+ * `observe` deliberately does not move it. Being shown a page is not doing
613
+ * something to it.
614
+ */
615
+ get acted() {
616
+ return this.history.length > 0;
617
+ }
596
618
  /** Records an action that was actually performed and succeeded. */
597
619
  record(action, description, result) {
598
620
  this.performed.add(identity(action));
@@ -686,6 +708,7 @@ function slugify(text) {
686
708
  const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
687
709
  return slug || "test";
688
710
  }
711
+ var NOTHING_DONE_REFUSAL = "refused: nothing succeeded during this step, so it was NOT closed. A step closes on what was done in it, not on being declared finished. If the outcome already holds \u2014 there was nothing left to do \u2014 assert that it holds and let the judgment close the step; an assertion is evidence, a declaration is not. If the step cannot be carried out at all, fail it and say why.";
689
712
  var StepFailure = class extends Error {
690
713
  };
691
714
  async function executeTest(page, test, options) {
@@ -770,6 +793,15 @@ async function executeTest(page, test, options) {
770
793
  }
771
794
  iterations++;
772
795
  if (action.action === "done") {
796
+ if (!recovery.acted) {
797
+ failedAttempts++;
798
+ lastResult = NOTHING_DONE_REFUSAL;
799
+ emitAction(index, action, lastResult);
800
+ if (failedAttempts >= maxRetries) {
801
+ throw new StepFailure(lastResult);
802
+ }
803
+ continue;
804
+ }
773
805
  emitAction(index, action, action.reasoning);
774
806
  break;
775
807
  }
@@ -2952,13 +2984,11 @@ async function planCommand(options) {
2952
2984
  written.push(path10.relative(options.cwd, file));
2953
2985
  console.log(` wrote ${path10.relative(options.cwd, file)}`);
2954
2986
  } catch (error) {
2955
- if (error instanceof PlannerError) {
2956
- generated.pop();
2957
- failed.push({ route, reason: error.message });
2958
- console.log(` X failed: ${error.message}`);
2959
- continue;
2960
- }
2961
- throw error;
2987
+ generated.pop();
2988
+ const reason = error instanceof Error ? error.message : String(error);
2989
+ failed.push({ route, reason });
2990
+ console.log(` X failed: ${reason}`);
2991
+ continue;
2962
2992
  }
2963
2993
  } else {
2964
2994
  console.log(`
@@ -3079,7 +3109,7 @@ function parsePositiveNumber(flag) {
3079
3109
  };
3080
3110
  }
3081
3111
  var program = new Command();
3082
- program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.16.0");
3112
+ program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.18.0");
3083
3113
  program.command("init").description("Scaffold .blastproof/ (config, tests, sample tests) in the current directory").action(async () => {
3084
3114
  try {
3085
3115
  const result = await initProject(process.cwd());