mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:47:03 +01:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWVUTaKFW8m7YrvWbzV
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""``scroll_to``: bring a widget that is outside the viewport on screen."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
import time
|
|
|
|
from python_pkg.android_ui._elements import ElementNotFoundError, UiElement
|
|
|
|
_SWIPE_MS = "350"
|
|
# Swipe through the middle 40% of the screen: far enough to move a page, short
|
|
# enough that Flutter does not turn it into a fling that overshoots the target.
|
|
_FROM_FRAC = 0.7
|
|
_TO_FRAC = 0.3
|
|
|
|
|
|
def _snapshot(elements: list[UiElement]) -> list[tuple[str, tuple[int, ...]]]:
|
|
"""Reduce a tree to what changes when a list scrolls."""
|
|
return [(e.label, e.bounds) for e in elements]
|
|
|
|
|
|
class ScrollMixin(ABC):
|
|
"""The scrolling part of :class:`~python_pkg.android_ui.driver.AndroidUi`."""
|
|
|
|
_settle: float
|
|
|
|
@abstractmethod
|
|
def _input(self, *args: str) -> str:
|
|
"""Supplied by the concrete driver: ``input`` on the driven display."""
|
|
|
|
@abstractmethod
|
|
def dump(self, *, retries: int = 4) -> list[UiElement]:
|
|
"""Supplied by the concrete driver: read the current UI tree."""
|
|
|
|
@abstractmethod
|
|
def find(self, query: str, *, exact: bool = False) -> UiElement:
|
|
"""Supplied by the concrete driver: the single match, or raise."""
|
|
|
|
def scroll_to(
|
|
self, query: str, *, exact: bool = False, max_swipes: int = 8
|
|
) -> UiElement:
|
|
"""Swipe up until ``query`` matches exactly one element, then return it.
|
|
|
|
A widget outside the viewport is not in the accessibility tree, so
|
|
``find`` reports it missing. Stops early, naming the query, when a swipe
|
|
leaves the screen unchanged (the end of the list) rather than spinning
|
|
for ``max_swipes`` swipes. Ambiguity raises, as it does for ``find``.
|
|
"""
|
|
elements = self.dump()
|
|
for _ in range(max_swipes):
|
|
if any(e.matches(query, exact=exact) for e in elements):
|
|
return self.find(query, exact=exact)
|
|
width = max((e.bounds[2] for e in elements), default=0)
|
|
height = max((e.bounds[3] for e in elements), default=0)
|
|
x = str(width // 2)
|
|
self._input(
|
|
"swipe",
|
|
x,
|
|
str(int(height * _FROM_FRAC)),
|
|
x,
|
|
str(int(height * _TO_FRAC)),
|
|
_SWIPE_MS,
|
|
)
|
|
time.sleep(self._settle)
|
|
after = self.dump()
|
|
if _snapshot(after) == _snapshot(elements):
|
|
break
|
|
elements = after
|
|
if any(e.matches(query, exact=exact) for e in elements):
|
|
return self.find(query, exact=exact)
|
|
msg = f"scrolled looking for {query!r} but it never appeared"
|
|
raise ElementNotFoundError(msg)
|