#!/usr/bin/env python3 """Is every help-tip reachable by keyboard and by touch? — C1 + S8, T9.5. The badges were elements whose :focus CSS was dead code (no tabindex) and whose touch path did not exist - on tablets, Field View's surface. Now the component upgrades every badge to a button at load, and one viewport-clamped role=tooltip bubble serves them all. This probe drives a badge with REAL key events and a tap at 390px, then greps the app-wide metrics the audit document cites. Exit 0 all passed, 1 a failure, 2 could not run. """ import io import json import os import re import sys import tempfile import time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import cdp # noqa: E402 from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402 from sections_check import set_sop # noqa: E402 from stepper_check import dismiss_dialogs # noqa: E402 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HTML = os.path.join(ROOT, "html") def ascii_(v, n=280): return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n] def settle(seconds=0.5): time.sleep(seconds) def strip_js(src): src = re.sub(r"/\*.*?\*/", "", src, flags=re.S) return "\n".join(re.sub(r"(?]*\bonclick=", code)) for m in re.finditer(r"outline:\s*none", code): # The replacement can sit in the rule ABOVE (wp-chrome's search shell # rings on :focus-within, and ringing input + shell would draw two), # so the window looks both ways. ctx = code[max(0, m.start() - 400):m.start() + 260] tail = ctx[ctx.find("outline:") + 12:] if ("outline" not in tail and "box-shadow" not in ctx and "focus-within" not in ctx and "border" not in tail): outline_bad.append(name) chk("div/span click handlers app-wide: 0 (baseline 12/2)", divspan == 0, divspan) chk("outline:none without a replacement: 0", not outline_bad, outline_bad[:4]) # The glossary pill classes are injected app-wide and MUST stay scoped: # a bare .pill-hold painted the creator's Issue (hold) status radio # error-red at all times (found 2026-08-20). help_src = io.open(os.path.join(HTML, "help.js"), encoding="utf-8").read() bare = re.findall(r"(?; zero spans remain", counts["total"] > 0 and counts["spans"] == 0 and counts["buttons"] == counts["total"], ascii_(counts)) chk("...with an accessible name and a declared state", page.eval("""[...document.querySelectorAll('button.help-tip')] .every(b => b.getAttribute('aria-label') && b.getAttribute('aria-expanded') !== null)""")) # ── 3. keyboard ─────────────────────────────────────────────────────── print("\n3. keyboard") # Programmatic focus() fires no focusin unless the document HAS focus - # the exact trap form_structure_check documents. Emulate it, loudly. page.ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True}) chk("focus emulation is on, so a focus reading means something", page.eval("document.hasFocus()") is True) page.eval("""(() => { const b = [...document.querySelectorAll('button.help-tip')] .find(x => x.offsetParent !== null) || document.querySelector('button.help-tip'); b.scrollIntoView({block:'center'}); b.focus(); })()""") settle(0.4) chk("focusing a badge shows the tooltip", page.eval("!!(document.getElementById('wp-tip-bubble') && !document.getElementById('wp-tip-bubble').hidden)") and page.eval("(document.getElementById('wp-tip-bubble')||{}).textContent.length > 0")) chk("...as a role=tooltip the badge points at", page.eval("document.getElementById('wp-tip-bubble').getAttribute('role')") == "tooltip" and page.eval("document.activeElement.getAttribute('aria-describedby')") == "wp-tip-bubble") bubble = json.loads(page.eval("""JSON.stringify((() => { const r = document.getElementById('wp-tip-bubble').getBoundingClientRect(); return {left: r.left, right: r.right}; })())""")) chk("390px: the bubble is CLAMPED to the viewport (BL-001's cause, dead)", bubble["left"] >= 0 and bubble["right"] <= 390, ascii_(bubble)) # ── 4. touch ────────────────────────────────────────────────────────── print("\n4. touch") page.eval("document.activeElement.blur()") settle(0.3) page.eval("""(() => { const b = [...document.querySelectorAll('button.help-tip')] .find(x => x.offsetParent !== null); b.click(); })()""") settle(0.4) chk("tapping a badge opens the tooltip and says so with aria-expanded", page.eval("!!(document.getElementById('wp-tip-bubble') && !document.getElementById('wp-tip-bubble').hidden)") and page.eval("!!document.querySelector(%s)" % json.dumps('button.help-tip[aria-expanded="true"]'))) page.eval("document.body.click()") settle(0.3) chk("tapping elsewhere closes it", page.eval("!document.getElementById('wp-tip-bubble') || document.getElementById('wp-tip-bubble').hidden")) js_errors = [e for e in page.js_errors() if "beforeunload" not in e] chk("no JavaScript errors anywhere in this run", not js_errors, ascii_(js_errors[:2])) finally: if browser is not None: try: browser.close() except Exception: pass if server is not None: try: server.terminate() except Exception: pass print("\n" + "-" * 54) print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL))) for f in _FAIL: print(" - " + f) return 1 if _FAIL else 0 if __name__ == "__main__": sys.exit(main())