merge upstream: #24's wheel scrolling

This commit is contained in:
Ethan Stoner
2026-08-26 19:41:22 -07:00
2 changed files with 53 additions and 8 deletions
+51
View File
@@ -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
+2 -8
View File
@@ -9,7 +9,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 queries
@@ -147,12 +146,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)
@@ -168,8 +163,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,