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