fix(web-runtime): retry action resolution + implement taps/swipes

V8-side runtime previously returned null when weighted picked a
generator that returned [] (page-gated), causing 80%+ of ticks on
narrow routes to emit no action and no post-screenshot. Now retries
up to 16x like goja, and the taps/swipes builtins query the live DOM
for clickable elements / dispatch random swipes instead of returning
null.
This commit is contained in:
pj committed 2026-04-27 20:29:27 +07:00
1 parent b1ede54dab
commit 82cf889e16
1 file changed
+54 -3
+54 -3
View File
@@ -386,9 +386,9 @@ function resolveGenerator(handle: ActionGeneratorHandle): unknown {
return resolveGenerator(inner);
}
case "taps":
return null; // host-side fallback handles random taps using hierarchy
return randomTap();
case "swipes":
return null;
return randomSwipe();
case "waitOnce":
return { kind: "Wait", durationMillis: 500 };
case "pressKey":
@@ -398,6 +398,50 @@ function resolveGenerator(handle: ActionGeneratorHandle): unknown {
}
}
function randomTap(): unknown {
const candidates = Array.from(
document.querySelectorAll<HTMLElement>(
'a, button, input, select, textarea, [role="button"], [onclick]',
),
).filter((element) => {
if ((element as HTMLButtonElement).disabled) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
if (candidates.length === 0) return null;
const picked = candidates[Math.floor(Math.random() * candidates.length)];
if (!picked) return null;
const rect = picked.getBoundingClientRect();
return {
kind: "Tap",
on: {
x: Math.round(rect.left + rect.width / 2),
y: Math.round(rect.top + rect.height / 2),
},
};
}
function randomSwipe(): unknown {
const width = window.innerWidth || 320;
const height = window.innerHeight || 480;
const cx = Math.round(width / 2);
const cy = Math.round(height / 2);
const magnitude = 200 + Math.floor(Math.random() * 401);
const direction = Math.floor(Math.random() * 4);
let toX = cx;
let toY = cy;
if (direction === 0) toY = Math.max(0, cy - magnitude);
else if (direction === 1) toY = cy + magnitude;
else if (direction === 2) toX = Math.max(0, cx - magnitude);
else toX = cx + magnitude;
return {
kind: "Swipe",
from: { x: cx, y: cy },
to: { x: toX, y: toY },
durationMillis: 250,
};
}
function pickFromArray(value: unknown): unknown {
if (!value) return null;
if (!Array.isArray(value)) return value;
@@ -458,7 +502,14 @@ function pointOf(value: unknown): { x: number; y: number } | undefined {
(globalThis as Record<string, unknown>).__sanderlingNextAction__ = function (): unknown {
if (!actionsRoot) return null;
return serializeAction(resolveGenerator(actionsRoot));
// Match the goja runtime: retry up to 16 times when a weighted entry's
// generator returns []. Otherwise on routes where most generators are
// gated to other pages, ~80% of ticks would emit no action.
for (let attempt = 0; attempt < 16; attempt++) {
const action = serializeAction(resolveGenerator(actionsRoot));
if (action !== null) return action;
}
return null;
};
export {};