Files
box/bin/hatch_menu/dialog.py
T

152 lines
5.4 KiB
Python
Raw Normal View History

"""Settings dialog navigation: open, tabs, rows, back, text.
Open is retried (single-shot opens flake ~1/4 live): each attempt
re-Escapes and re-drives the dock menu from scratch. Tab clicks are
idempotent (re-clicking the active tab is a harmless no-op), so goto
always clicks and reports the click result instead of guessing which
tab is active.
"""
import time
from approvals import cdp_evaluate
from hatch_menu.mouse import escape, real_click
TAB_NAMES = ["General", "Connectors", "Wallet", "Secure store",
"Permissions", "Messaging channels", "Devices",
"Data controls", "Help & support", "Legal info"]
TABS = TAB_NAMES # legacy alias
DOCK_MORE_TESTID = "hatch-dock-more"
JS_DOCK_RECT = ("(() => { const b = document.querySelector("
"'[data-testid=\"hatch-dock-more\"]'); if (!b) return null;"
" const r = b.getBoundingClientRect();"
" return {x: r.x + r.width/2, y: r.y + r.height/2}; })()")
JS_CLICK_SETTINGS_ITEM = ("(() => { const it = Array.from(document."
"querySelectorAll('[role=\"menuitem\"]')).find(el => el.getAttribute("
"'data-pel-click') === 'settings_nav_click');"
" if (!it) return 'NO_ITEM'; it.click(); return 'CLICKED'; })()")
JS_DIALOG_PRESENT = ("(() => !!document.querySelector('[role=\"dialog\"]'))()")
JS_DIALOG_TEXT = ("(() => { const d = document.querySelector("
"'[role=\"dialog\"]'); return d ? d.innerText : null; })()")
JS_GOTO_TAB_TMPL = ("(() => { const b = Array.from(document."
"querySelectorAll('[role=\"dialog\"] button')).find(x => "
"(x.innerText||'').trim() === '%s');"
" if (!b) return 'NO_TAB'; b.click(); return 'CLICKED'; })()")
JS_CLICK_ROW_TMPL = ("((name) => {"
" const d = document.querySelector('[role=\"dialog\"]');"
" if (!d) return 'NO_DIALOG';"
" const inNav = (el) => !!el.closest("
"'nav, [role=\"tablist\"], [role=\"navigation\"]');"
" const els = Array.from(d.querySelectorAll("
"'button, [role=\"button\"], a')).filter(e => !inNav(e));"
" const t = els.find(e => (e.innerText || '').trim()"
".toLowerCase().startsWith(name.toLowerCase()));"
" if (!t) return 'NO_ROW'; t.click(); return 'CLICKED'; })('%s')")
JS_DESCRIBE_ROWS = ("(() => {"
" const d = document.querySelector('[role=\"dialog\"]');"
" if (!d) return null;"
" const inNav = (el) => !!el.closest("
"'nav, [role=\"tablist\"], [role=\"navigation\"]');"
" return Array.from(d.querySelectorAll("
"'button, [role=\"button\"], a')).filter(e => !inNav(e))"
".map(e => { const lines = (e.innerText || '').trim().split('\\n');"
" return {name: (lines[0] || '').slice(0, 80),"
" detail: lines.slice(1).join(' / ').slice(0, 120)}; }); })()")
JS_GO_BACK = ("(() => { const d = document.querySelector("
"'[role=\"dialog\"]'); if (!d) return 'NO_DIALOG';"
" const b = Array.from(d.querySelectorAll('button')).find("
"x => (x.getAttribute('aria-label') || '') === 'Go back');"
" if (!b) return 'NO_BACK'; b.click(); return 'CLICKED'; })()")
def _eval(ws, js, timeout=8.0):
try:
return cdp_evaluate(ws, js, timeout=timeout)
except Exception:
return None
def dialog_present(ws):
"""True when a dialog is currently open."""
return bool(_eval(ws, JS_DIALOG_PRESENT, timeout=5.0))
def dialog_text(ws, timeout=8.0, limit=4000):
"""Inner text of the open dialog, or None."""
try:
text = cdp_evaluate(ws, JS_DIALOG_TEXT, timeout=timeout)
except Exception:
return None
if not text:
return None
return text[:limit]
def open_settings(ws, tries=3):
"""Open the Settings dialog via the dock menu. True when open."""
for _ in range(tries):
escape(ws)
time.sleep(0.5)
rect = _eval(ws, JS_DOCK_RECT, timeout=5.0)
if not rect or "x" not in rect:
continue
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
continue
time.sleep(1.0)
if _eval(ws, JS_CLICK_SETTINGS_ITEM,
timeout=5.0) != "CLICKED":
continue
time.sleep(1.5)
if dialog_present(ws):
return True
return False
def goto_tab(ws, name, timeout=5.0):
"""Click a Settings tab by visible name. True when clicked."""
if _eval(ws, JS_GOTO_TAB_TMPL % name, timeout=timeout) != "CLICKED":
return False
time.sleep(0.8)
return True
def click_row(ws, name, tab=None, timeout=8.0):
"""Click a content row; verify the drill/expand opened. Bool."""
if tab is not None and not goto_tab(ws, tab, timeout=timeout):
return False
if _eval(ws, JS_CLICK_ROW_TMPL % name, timeout=timeout) != "CLICKED":
return False
time.sleep(1.0)
text = dialog_text(ws, timeout=timeout) or ""
return name.lower() in text.lower()
def describe_rows(ws, tab=None, timeout=8.0):
"""Inventory rows (name/detail) on a tab. [] when unreadable."""
if tab is not None and not goto_tab(ws, tab, timeout=timeout):
return []
return _eval(ws, JS_DESCRIBE_ROWS, timeout=timeout) or []
def go_back(ws):
"""Click the sub-page Go back button. True when clicked."""
if _eval(ws, JS_GO_BACK, timeout=5.0) != "CLICKED":
return False
time.sleep(0.8)
return True
def close_settings(ws):
"""Dismiss settings/popovers. Never raises."""
escape(ws)