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()