From 43aadf7dedd44942d20346402aad26fa71676c7e Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:28:01 +0200 Subject: [PATCH] drive the misc cards scrolling with wheel input instead of fixed bursts The card loop fired fixed 100px scroll events back to back with no pauses, which is the jumpy scrolling, and its while-not-in-viewport loop was unbounded, so a card that never fits the viewport completely would hang the run forever. The way back up unwound a counted number of steps, which lands wrong when the page height changes while cards update. Scrolling is now wheel input with varying step sizes and short pauses, bounded, aimed at centering the target. The return reads the actual scroll position instead of counting. --- src/mouse_trajectory.py | 51 +++++++++++++++++++++++++++++++++++++++++ src/rewards_tasks.py | 10 ++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/mouse_trajectory.py b/src/mouse_trajectory.py index 0c5dcf3..92bd15a 100644 --- a/src/mouse_trajectory.py +++ b/src/mouse_trajectory.py @@ -311,6 +311,57 @@ class MouseUtils: self.driver.execute_script(f"window.moveVisualCursor({point[0]}, {point[1]});") + def wheel_scroll_element_into_view(self, element: WebElement, max_wheel_events: int = 60): + """Scroll the element into the viewport with simulated wheel input. + + Wheel steps of varying size with short pauses, the way a person scrolls, + instead of a fixed-size burst. The loop is bounded on purpose: an element + that never fits the viewport completely, for example one taller than the + window, must not hang the run forever. When the budget runs out the + caller proceeds with the element as visible as it got. + """ + for _ in range(max_wheel_events): + top, bottom, height = self.driver.execute_script( + "var r = arguments[0].getBoundingClientRect();" + "return [r.top, r.bottom, window.innerHeight];", + element + ) + + if top >= 0 and bottom <= height: + break + + # Aim the element at the middle of the viewport, one notch at a time. + distance = (top + bottom) / 2 - height / 2 + step = max(-320, min(320, distance)) + step = int(step * random.uniform(0.6, 1.0)) + + if abs(step) < 40: + step = 40 if distance > 0 else -40 + + ActionChains(self.driver).scroll_by_amount(0, step).perform() + + time.sleep(random.uniform(0.04, 0.12)) + + def wheel_scroll_to_top(self, max_wheel_events: int = 80): + """Scroll back to the top of the page with simulated wheel input. + + Reads the actual scroll position instead of unwinding a counted number + of steps, because the page height can change while cards update and a + symmetric unwind then lands in the wrong place. + """ + for _ in range(max_wheel_events): + offset = self.driver.execute_script("return window.scrollY || window.pageYOffset;") + + if offset <= 0: + break + + step = min(340, int(offset)) + step = max(60, int(step * random.uniform(0.6, 1.0))) + + ActionChains(self.driver).scroll_by_amount(0, -step).perform() + + time.sleep(random.uniform(0.04, 0.12)) + def move_to_element(self, element: WebElement, visualize: bool=True): # The pointer is moved to viewport coordinates, so an element below the # fold yields a target outside the window and the driver rejects the move diff --git a/src/rewards_tasks.py b/src/rewards_tasks.py index 55773db..15b3723 100644 --- a/src/rewards_tasks.py +++ b/src/rewards_tasks.py @@ -7,7 +7,6 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.webelement import WebElement -from selenium.webdriver.common.action_chains import ActionChains from selenium.common.exceptions import StaleElementReferenceException, TimeoutException, NoSuchElementException import tab_utils import llm_utils @@ -140,12 +139,8 @@ class RewardsTaskUtils: misc_cards: list[WebElement] = self.wait_for_element(self.elements.get_all_misc_cards) - scroll_times = 0 - for card in misc_cards: - while not self.elements.element_is_fully_in_viewport(card): # this should work for top-down iteration - ActionChains(self.driver).scroll_by_amount(0, 100).perform() - scroll_times+=1 + self.mouse.wheel_scroll_element_into_view(card) if not self.elements.card_is_complete(card) and self.elements.get_card_point_value(card) > 0: self.move_to_and_click(card) @@ -158,8 +153,7 @@ class RewardsTaskUtils: self.tab_utils.close_all_other_tabs() - for i in range(scroll_times): - ActionChains(self.driver).scroll_by_amount(0, -100).perform() # scroll back to top of page + self.mouse.wheel_scroll_to_top() def complete_required_searches(self, max_rounds: int = 6): # Points per search are not fixed. Some markets award 3 rather than 5,