Add scrollToLoad and an absent variant of waitFor

scrollToLoad walks a progressively-loading list to the bottom before the steps
that act on its items run. Stopping is two-part: no new matches appeared AND the
container was already pinned to the bottom — counting alone stops early on a slow
fetch. Hitting the scroll cap is reported rather than passed off as done, so a
later step never works quietly on a partial list.

The scrolling element is usually not the window. Lists like this live in a div
with its own overflow, and scrolling the document does nothing at all, so the
step walks up from a matched item to the ancestor that actually scrolls —
overflow allows it and there is more content than fits — with containerSelector
to name one outright when the guess is wrong. Verified against a page whose
document also scrolls, which is the case that tells the two apart: it found the
inner div and pulled 12 items up to 60 in 7 scrolls.

waitFor gains `absent`, for waiting on something to go rather than arrive — a
modal closing after a reset. It only accepts a genuine "selector matched
nothing"; an unreachable extension looks the same from a distance and would
otherwise satisfy the gate for the wrong reason, sending the next iteration into
a page that still has the modal open.

The locate queue carries a free-form options blob now, so a new kind of request
stops meaning a new column each time.

Also fixes a missing comma in the reset flow that broke the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-30 14:31:36 -05:00
co-authored by Claude Opus 5
parent 3ac9fe060f
commit 3cc7ddcc5c
8 changed files with 190 additions and 23 deletions
+3 -2
View File
@@ -5,7 +5,7 @@ import { corsJson, corsPreflight } from '../cors';
/** Python enqueues "find this selector and tell me where it is on screen". */
export async function POST(req: NextRequest) {
try {
const body = await req.json() as { selector?: unknown; index?: unknown; urlPattern?: unknown; openUrl?: unknown; navigateUrl?: unknown };
const body = await req.json() as { selector?: unknown; index?: unknown; urlPattern?: unknown; openUrl?: unknown; navigateUrl?: unknown; options?: unknown };
if (typeof body.selector !== 'string' || !body.selector.trim()) {
return corsJson({ error: '`selector` is required' }, { status: 400 });
}
@@ -13,8 +13,9 @@ export async function POST(req: NextRequest) {
const urlPattern = typeof body.urlPattern === 'string' ? body.urlPattern : '';
const openUrl = typeof body.openUrl === 'string' ? body.openUrl : '';
const navigateUrl = typeof body.navigateUrl === 'string' ? body.navigateUrl : '';
const options = body.options && typeof body.options === 'object' ? JSON.stringify(body.options) : '{}';
const row = createLocateRequest(body.selector.trim(), index, urlPattern, openUrl, navigateUrl);
const row = createLocateRequest(body.selector.trim(), index, urlPattern, openUrl, navigateUrl, options);
return corsJson({ id: row.id, status: row.status });
} catch (err: any) {
return corsJson({ error: err?.message ?? 'Failed to queue request' }, { status: 500 });