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 1fe3a90..c5f80d1 100644 --- a/src/rewards_tasks.py +++ b/src/rewards_tasks.py @@ -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,