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:
co-authored by
Claude Opus 5
parent
3ac9fe060f
commit
3cc7ddcc5c
+4
-2
@@ -113,7 +113,8 @@ class Dashboard:
|
||||
return self._request("/api/autobuyer/status")
|
||||
|
||||
def locate(self, selector: str, index: int, url_pattern: str, timeout: float,
|
||||
open_url: str = "", navigate_url: str = "") -> dict:
|
||||
open_url: str = "", navigate_url: str = "",
|
||||
options: dict | None = None) -> dict:
|
||||
"""Queue a lookup and block until the extension answers it.
|
||||
|
||||
`open_url` is the page the extension should open if no tab matches
|
||||
@@ -123,7 +124,8 @@ class Dashboard:
|
||||
"/api/autobuyer/locate",
|
||||
"POST",
|
||||
{"selector": selector, "index": index, "urlPattern": url_pattern,
|
||||
"openUrl": open_url, "navigateUrl": navigate_url},
|
||||
"openUrl": open_url, "navigateUrl": navigate_url,
|
||||
"options": options or {}},
|
||||
)
|
||||
request_id = queued["id"]
|
||||
|
||||
|
||||
+46
-9
@@ -33,7 +33,7 @@ HEARTBEAT_SECONDS = 2.0
|
||||
# Bumped whenever the step vocabulary or the locate protocol changes. Reported in
|
||||
# the heartbeat so the dashboard can say "restart your runner" instead of letting
|
||||
# a stale process fail on a step type it has never heard of.
|
||||
VERSION = "0.14.0"
|
||||
VERSION = "0.16.0"
|
||||
|
||||
# Shared with the heartbeat thread: whether a run is currently executing.
|
||||
_busy = threading.Event()
|
||||
@@ -75,10 +75,13 @@ def run_one_step(dash: Dashboard, step: dict, opts, rng, report, run_id: int | N
|
||||
report(f"tab is on {landed.get('url', target)}")
|
||||
|
||||
elif step["action"] == "waitFor":
|
||||
# A gate, not an action: block until the element exists. Nothing is
|
||||
# clicked or typed. Whatever has to make it appear — a person solving a
|
||||
# challenge, a slow server, a background job — happens outside this run.
|
||||
# A gate, not an action: block until the element exists — or, with
|
||||
# `absent`, until it is gone. Nothing is clicked or typed. Whatever has to
|
||||
# change the page — a person solving a challenge, a modal closing itself,
|
||||
# a slow server — happens outside this run.
|
||||
selector = step["selector"]
|
||||
absent = bool(step.get("absent"))
|
||||
goal = "disappear" if absent else "appear"
|
||||
timeout_s = float(step.get("timeoutSeconds", 120))
|
||||
deadline = time.time() + timeout_s
|
||||
announced = False
|
||||
@@ -92,20 +95,54 @@ def run_one_step(dash: Dashboard, step: dict, opts, rng, report, run_id: int | N
|
||||
dash.locate(selector, int(step.get("index", 0)),
|
||||
step.get("urlPattern", "") or opts.url,
|
||||
opts.timeout, step.get("openUrl", "") or "")
|
||||
report(f"{selector} appeared")
|
||||
break
|
||||
if not absent:
|
||||
report(f"{selector} appeared")
|
||||
break
|
||||
# Still there. Keep waiting for it to go.
|
||||
except NotFoundError:
|
||||
if absent:
|
||||
report(f"{selector} is gone")
|
||||
break
|
||||
# Not there yet; keep waiting for it to arrive.
|
||||
except DashboardError:
|
||||
pass # not there yet, or the tab is mid-render
|
||||
# A dead extension or an unreachable dashboard must not be read
|
||||
# as "the element is gone" — that would satisfy an absent gate
|
||||
# for entirely the wrong reason.
|
||||
pass
|
||||
|
||||
if time.time() >= deadline:
|
||||
raise actions.StepError(
|
||||
f"{selector} did not appear within {timeout_s:.0f}s"
|
||||
f"{selector} did not {goal} within {timeout_s:.0f}s"
|
||||
)
|
||||
if not announced:
|
||||
report(f"waiting for {selector} (up to {timeout_s:.0f}s)")
|
||||
report(f"waiting for {selector} to {goal} (up to {timeout_s:.0f}s)")
|
||||
announced = True
|
||||
time.sleep(2.0)
|
||||
|
||||
elif step["action"] == "scrollToLoad":
|
||||
# Lists that load progressively need walking to the bottom before the
|
||||
# steps that act on their items can see everything.
|
||||
result = dash.locate(
|
||||
step["selector"], 0,
|
||||
step.get("urlPattern", "") or opts.url,
|
||||
max(opts.timeout, 120.0), # scrolling a long list outlasts a normal step
|
||||
step.get("openUrl", "") or "",
|
||||
options={
|
||||
"op": "scrollToLoad",
|
||||
"containerSelector": step.get("containerSelector", ""),
|
||||
"maxScrolls": int(step.get("maxScrolls", 25)),
|
||||
"settleMs": int(step.get("settleMs", 800)),
|
||||
},
|
||||
)
|
||||
found_n = result.get("after", 0)
|
||||
report(f"{found_n} match(es) after {result.get('scrolls', 0)} scroll(s) "
|
||||
f"of {result.get('container', '?')} (was {result.get('before', 0)})")
|
||||
if not result.get("exhausted"):
|
||||
# Stopping on the scroll cap is not a failure, but it does mean the
|
||||
# list may still have more below — worth saying so rather than
|
||||
# letting a later step quietly work on a partial list.
|
||||
report("hit the scroll limit — there may be more not loaded")
|
||||
|
||||
elif step["action"] not in ("click", "type"):
|
||||
# Almost always a stale runner: the server defines the step vocabulary,
|
||||
# so a step type this process has never heard of means automations.ts has
|
||||
|
||||
Reference in New Issue
Block a user