"""Execution of a single automation step: locate the element, then click or type. Shared by the CLI (clicker.py) and the automation runner (runner.py) so there is one implementation of the safety checks. Callers differ only in how they report progress and how they surface failures. """ import random import time import focus class StepError(Exception): """A step refused to run, or ran and could not be verified. `code` mirrors the CLI's exit codes: 3 = refused before acting, 4 = acted but verification failed, 5 = the session is signed out. """ def __init__(self, message: str, code: int = 3): super().__init__(message) self.code = code class SignedOutError(StepError): """The tab is on the login page, so the session has expired. Distinct from an ordinary failure because nothing is wrong with the automation — it just cannot proceed until a human signs in. """ def __init__(self, url: str): super().__init__( f"signed out — the tab is on {url}. Log in to the site, then run this again.", code=5, ) def check_signed_in(found: dict, signed_out: str) -> None: """Abort if the page redirected to a login screen. Worth doing before every action, not just at the start: a session can expire mid-run, and once it does, every selector resolves against a login form. A click or a keystroke aimed at the old page would land on whatever that form happens to render in the same place. """ if not signed_out: return url = found.get("url") or "" if signed_out in url: raise SignedOutError(url) # Display scaling factors that actually exist. Anything else means the two sides # are describing different things (usually a multi-monitor desktop) rather than a # scaled single display. KNOWN_SCALES = (1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0) # How long to let something covering the target get out of the way. # # Most things that cover a button are on their way out: a modal backdrop # mid-fade, a captcha overlay closing, a spinner over the button that is about # to be enabled. One measurement cannot tell those apart from a banner that is # there to stay, and failing on the first look turns a timing wobble into a # stopped run halfway through a purchase. # # This does not weaken the check — still covered when the time is up still # fails, and nothing is ever clicked while something is on top of it. COVERED_SETTLE_SECONDS = 8.0 COVERED_POLL_SECONDS = 0.75 def scale_factor(found: dict, override: float | None, report=None) -> float: """CSS pixels and desktop pixels match on macOS and unscaled Windows, but Windows display scaling breaks that. Deliberately conservative: a raw ratio is only trusted when it lands on a real scaling factor. On a multi-monitor desktop the browser reports the display it is on while pyautogui reports the primary one, and the resulting ratio is meaningless — halving every coordinate would put clicks far from the target. Fall back to 1:1 and say so, rather than silently scaling. """ if override is not None: return override try: import pyautogui except ImportError: return 1.0 css_width = (found.get("screenSize") or {}).get("width") if not css_width: return 1.0 os_width = pyautogui.size().width ratio = os_width / css_width for known in KNOWN_SCALES: if abs(ratio - known) < 0.02: return known if report: report( f"screen size disagrees (browser {css_width}px, OS {os_width}px, " f"ratio {ratio:.3f}) — assuming 1:1. If clicks land off, pass --scale." ) return 1.0 def perform( dash, action: str, selector: str, *, index: int = 0, url: str = "", open_url: str = "", signed_out: str = "", frame: str = "", text: str | None = None, clear: bool = False, scale: float | None = None, timeout: float = 20.0, rng: random.Random | None = None, activate: bool = True, verify: bool = True, force: bool = False, dry_run: bool = False, robotic: bool = False, report=None, on_located=None, ) -> dict: """Run one step. Returns the located element's data. Raises DashboardError if the extension can't find it, StepError if the step is refused or fails verification. """ import pyautogui import humanize rng = rng or random.Random() say = report or (lambda _m: None) # `frame` names the