From bf79e188f23165922e255b9295163cb4d7c9c7fc Mon Sep 17 00:00:00 2001 From: mardausdennis <71312763+mardausdennis@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:23:13 +0200 Subject: [PATCH] address review: scroll only when needed and smoothly, tighten the visual search needle scrollIntoView re-centered every element unconditionally and instantly, which is the page jump between the daily set and explore on bing. The pointer move now scrolls only when the element is actually outside the viewport, smoothly, and polls the rect until the async scroll settles before computing the path. Visual search matches the full 'visual search streak' label. Plain 'visual search' can also hit a dashboard element that goes stale mid-interaction, and the guessed 'image search' needle is gone. --- src/element_selectors.py | 19 +++++++++---------- src/mouse_trajectory.py | 41 ++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/element_selectors.py b/src/element_selectors.py index f86e937..0dc565c 100644 --- a/src/element_selectors.py +++ b/src/element_selectors.py @@ -22,7 +22,9 @@ class Labels: CLAIM = "claim" # exact label preferred, substring as fallback DAILY_SET_STREAK = "daily set streak" CARD_COMPLETED = "completed" - VISUAL_SEARCH = ("visual search", "image search") + # The full streak label on purpose: plain "visual search" also matches an + # element on the dashboard, which can go stale mid-interaction. + VISUAL_SEARCH_STREAK = "visual search streak" class ElementSelectionUtils: @@ -175,15 +177,12 @@ class ElementSelectionUtils: # ------------------------------------------------------------------ def get_open_visual_search_sidebar(self): - for needle in Labels.VISUAL_SEARCH: - try: - return self._button_containing(needle) - except NoSuchElementException: - continue - - # Not every layout ships this entry point, and where it does the label is - # not confirmed, so fall back to the original position in streaks. - return self._streaks_button(5) + try: + return self._button_containing(Labels.VISUAL_SEARCH_STREAK) + except NoSuchElementException: + # Not every layout ships this entry point. Where it does but the + # label differs, fall back to the original position in streaks. + return self._streaks_button(5) def get_search_now_link_from_visual_search_sidebar(self): sidebar = self.get_sidebar_section() diff --git a/src/mouse_trajectory.py b/src/mouse_trajectory.py index 5f47171..0c5dcf3 100644 --- a/src/mouse_trajectory.py +++ b/src/mouse_trajectory.py @@ -314,12 +314,41 @@ class MouseUtils: 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 - # with MoveTargetOutOfBoundsException. Bring it into view first. - self.driver.execute_script( - "arguments[0].scrollIntoView({block: 'center', inline: 'center'});", - element - ) - time.sleep(0.4) + # with MoveTargetOutOfBoundsException. Bring it into view first, but only + # when it actually is out of view: unconditionally re-centering visible + # elements is what caused the page to jump between tasks. When scrolling + # is needed it is smooth, and since smooth scrolling is asynchronous, the + # rect is polled until it stops moving before the path is computed. + fully_in_view = self.driver.execute_script(""" + var r = arguments[0].getBoundingClientRect(); + return ( + r.top >= 0 && r.left >= 0 && + r.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + r.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + """, element) + + if not fully_in_view: + self.driver.execute_script( + "arguments[0].scrollIntoView({block: 'center', inline: 'center', behavior: 'smooth'});", + element + ) + + last_rect = None + + for _ in range(20): + time.sleep(0.15) + + rect = self.driver.execute_script( + "var r = arguments[0].getBoundingClientRect();" + "return [Math.round(r.top), Math.round(r.left)];", + element + ) + + if rect == last_rect: + break + + last_rect = rect current_mouse_position = self.get_current_mouse_position()