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 +2 -0
- package/dist/cli.js +38 -8
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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.
|
|
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());
|