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
@@ -263,6 +263,71 @@ function waitForTabLoad(tabId, timeoutMs = 15000) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs in the page. Scrolls until the list stops growing, then reports what it
|
||||
* ended up with.
|
||||
*
|
||||
* The scrolling element is often NOT the window — lists like this usually live
|
||||
* in a div with its own overflow, and scrolling the document does nothing at
|
||||
* all. So walk up from a matched item looking for the ancestor that actually
|
||||
* scrolls, and let the caller name one outright when the guess is wrong.
|
||||
*/
|
||||
async function pageScrollToLoad(selector, containerSelector, maxScrolls, settleMs, stableRounds) {
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const count = () => document.querySelectorAll(selector).length;
|
||||
|
||||
function scroller() {
|
||||
if (containerSelector) {
|
||||
const named = document.querySelector(containerSelector);
|
||||
if (!named) return { error: `No element matches container ${containerSelector}` };
|
||||
return { el: named };
|
||||
}
|
||||
|
||||
// An ancestor that can actually scroll: overflow allows it, and there is
|
||||
// more content than fits.
|
||||
let node = document.querySelector(selector)?.parentElement ?? null;
|
||||
while (node && node !== document.body) {
|
||||
const overflow = getComputedStyle(node).overflowY;
|
||||
if (/(auto|scroll)/.test(overflow) && node.scrollHeight > node.clientHeight + 4) {
|
||||
return { el: node };
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return { el: document.scrollingElement || document.documentElement, isDocument: true };
|
||||
}
|
||||
|
||||
const found = scroller();
|
||||
if (found.error) return { error: found.error };
|
||||
const el = found.el;
|
||||
|
||||
const before = count();
|
||||
let last = before;
|
||||
let stable = 0;
|
||||
let scrolls = 0;
|
||||
|
||||
while (scrolls < maxScrolls) {
|
||||
const wasAtBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 4;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
scrolls++;
|
||||
await sleep(settleMs);
|
||||
|
||||
const now = count();
|
||||
// Nothing new AND we were already pinned to the bottom: the list is done
|
||||
// growing, not merely slow.
|
||||
stable = (now > last) ? 0 : stable + (wasAtBottom ? 1 : 0);
|
||||
last = now;
|
||||
if (stable >= stableRounds) break;
|
||||
}
|
||||
|
||||
return {
|
||||
before,
|
||||
after: last,
|
||||
scrolls,
|
||||
exhausted: stable >= stableRounds,
|
||||
container: found.isDocument ? 'document' : (el.className || el.tagName || 'element').toString().slice(0, 60),
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveLocateTab(cfg, request) {
|
||||
// Every step carries its firm's pattern; the manifest hosts are the fallback
|
||||
// for a bare request (the CLI's locate without --url).
|
||||
@@ -323,6 +388,33 @@ async function serveLocateRequest(cfg) {
|
||||
await chrome.tabs.update(tab.id, { active: true });
|
||||
await new Promise((r) => setTimeout(r, 250)); // let the OS finish raising it
|
||||
|
||||
// A scroll request is a different operation on the same channel: no
|
||||
// element is measured, the page is just walked to the bottom.
|
||||
if (request.options && request.options.op === 'scrollToLoad') {
|
||||
const [scrolled] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: pageScrollToLoad,
|
||||
args: [
|
||||
request.selector,
|
||||
request.options.containerSelector || '',
|
||||
Number(request.options.maxScrolls) || 25,
|
||||
Number(request.options.settleMs) || 800,
|
||||
Number(request.options.stableRounds) || 2,
|
||||
],
|
||||
});
|
||||
const out = scrolled?.result;
|
||||
if (!out) throw new Error('Scroll injection returned nothing');
|
||||
if (out.error) throw new Error(out.error);
|
||||
result = { ...out, tabId: tab.id, windowId: tab.windowId, browser: detectBrowser() };
|
||||
|
||||
await fetch(`${cfg.apiBase}/api/autobuyer/locate/result`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: request.id, result, error: null }),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// `complete` only means the document loaded — a React app still has to
|
||||
// mount and paint. Retry briefly rather than declaring the element missing,
|
||||
// with a longer budget when we just opened the page from cold.
|
||||
|
||||
Reference in New Issue
Block a user