mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
feat(spec): typing builtin for the web (V8) action path
This commit is contained in:
1 parent
9df104e5b2
commit
64d0633bd6
1 file changed
+57
@@ -347,6 +347,7 @@ const runtime = {
|
|||||||
return { kind: "Wait", durationMillis: p.durationMillis };
|
return { kind: "Wait", durationMillis: p.durationMillis };
|
||||||
},
|
},
|
||||||
taps: { __sanderlingActionGenerator: true, __sanderlingKind: "taps" } as ActionGeneratorHandle,
|
taps: { __sanderlingActionGenerator: true, __sanderlingKind: "taps" } as ActionGeneratorHandle,
|
||||||
|
typing: { __sanderlingActionGenerator: true, __sanderlingKind: "typing" } as ActionGeneratorHandle,
|
||||||
swipes: { __sanderlingActionGenerator: true, __sanderlingKind: "swipes" } as ActionGeneratorHandle,
|
swipes: { __sanderlingActionGenerator: true, __sanderlingKind: "swipes" } as ActionGeneratorHandle,
|
||||||
waitOnce: { __sanderlingActionGenerator: true, __sanderlingKind: "waitOnce" } as ActionGeneratorHandle,
|
waitOnce: { __sanderlingActionGenerator: true, __sanderlingKind: "waitOnce" } as ActionGeneratorHandle,
|
||||||
pressKeys: { __sanderlingActionGenerator: true, __sanderlingKind: "pressKey" } as ActionGeneratorHandle,
|
pressKeys: { __sanderlingActionGenerator: true, __sanderlingKind: "pressKey" } as ActionGeneratorHandle,
|
||||||
@@ -441,6 +442,8 @@ function resolveGenerator(handle: ActionGeneratorHandle): unknown {
|
|||||||
}
|
}
|
||||||
case "taps":
|
case "taps":
|
||||||
return randomTap();
|
return randomTap();
|
||||||
|
case "typing":
|
||||||
|
return randomInput();
|
||||||
case "swipes":
|
case "swipes":
|
||||||
return randomSwipe();
|
return randomSwipe();
|
||||||
case "waitOnce":
|
case "waitOnce":
|
||||||
@@ -482,6 +485,59 @@ function randomTap(): unknown {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-tick cache mirroring randomTapCandidates, reset each NextAction tick.
|
||||||
|
let randomInputCandidates: HTMLElement[] | null = null;
|
||||||
|
|
||||||
|
const NON_TEXT_INPUT_TYPES = [
|
||||||
|
"button", "submit", "checkbox", "radio", "range", "color", "file", "image", "reset",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Edge-case input pool mirroring the goja-side inputCorpus: empty, whitespace,
|
||||||
|
// overflow length, unicode, numeric boundaries, and common injection payloads.
|
||||||
|
const WEB_INPUT_CORPUS = [
|
||||||
|
"", "a", "a".repeat(4096), "🙂🔥💸", " ", "\t\n", "-1",
|
||||||
|
"999999999999999999999", "0.0000001", "1e10", "'; DROP TABLE--",
|
||||||
|
"<script>alert(1)</script>", "../../etc/passwd", "%s%n", "NaN",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isEditableElement(element: HTMLElement): boolean {
|
||||||
|
if (element.isContentEditable) return true;
|
||||||
|
const tag = element.tagName.toLowerCase();
|
||||||
|
if (tag === "textarea") return true;
|
||||||
|
if (tag === "input") {
|
||||||
|
const type = ((element as HTMLInputElement).type || "").toLowerCase();
|
||||||
|
return !NON_TEXT_INPUT_TYPES.includes(type);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInput(): unknown {
|
||||||
|
if (!randomInputCandidates) {
|
||||||
|
randomInputCandidates = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>("input, textarea, [contenteditable]"),
|
||||||
|
).filter((element) => {
|
||||||
|
if (!isEditableElement(element)) return false;
|
||||||
|
if ((element as HTMLInputElement).disabled) return false;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const candidates = randomInputCandidates;
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
const picked = candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
|
if (!picked) return null;
|
||||||
|
const rect = picked.getBoundingClientRect();
|
||||||
|
const text = WEB_INPUT_CORPUS[Math.floor(Math.random() * WEB_INPUT_CORPUS.length)];
|
||||||
|
return {
|
||||||
|
kind: "InputText",
|
||||||
|
into: {
|
||||||
|
x: Math.round(rect.left + rect.width / 2),
|
||||||
|
y: Math.round(rect.top + rect.height / 2),
|
||||||
|
},
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function randomSwipe(): unknown {
|
function randomSwipe(): unknown {
|
||||||
// Why: web has no swipe gesture; pointer drags into empty divs are noise.
|
// Why: web has no swipe gesture; pointer drags into empty divs are noise.
|
||||||
return null;
|
return null;
|
||||||
@@ -569,6 +625,7 @@ defineLockedGlobal("__sanderlingNextAction__", function (): unknown {
|
|||||||
if (!actionsRoot) return null;
|
if (!actionsRoot) return null;
|
||||||
// Reset per-tick caches so each invocation gets a fresh DOM scan.
|
// Reset per-tick caches so each invocation gets a fresh DOM scan.
|
||||||
randomTapCandidates = null;
|
randomTapCandidates = null;
|
||||||
|
randomInputCandidates = null;
|
||||||
// Match the goja runtime: retry up to 16 times when a weighted entry's
|
// Match the goja runtime: retry up to 16 times when a weighted entry's
|
||||||
// generator returns []. Otherwise on routes where most generators are
|
// generator returns []. Otherwise on routes where most generators are
|
||||||
// gated to other pages, ~80% of ticks would emit no action.
|
// gated to other pages, ~80% of ticks would emit no action.
|
||||||
|
|||||||
Reference in new issue
Block a user