#!/usr/bin/env python3 """Is Save reachable without scrolling, on every step? — B6, T7.8. The proposal put actions beside the fields; on the Constraints and Sequence steps that meant scrolling to save. The wizard's step-navigation bar is now position:sticky at the viewport bottom - the creator's pattern, adapted to the wizard's grid (sticky keeps its slot in the flow, so it cannot obscure a field at any width). The T4.4 indicator already lives in the bar and rides along. Driven at a deliberately short viewport (700px) so the tall steps genuinely overflow - a bar that is only reachable because the page happens to fit would pass a taller run and prove nothing. Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome. Exit 0 all passed, 1 a failure, 2 could not run. """ 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 stepper_check import dismiss_dialogs # noqa: E402 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) BAR_JS = """(() => JSON.stringify((() => { const bar = document.querySelector('.step-navigation'); if (!bar) return null; const r = bar.getBoundingClientRect(); const cs = getComputedStyle(bar); const btn = [...bar.querySelectorAll('button')].find(b => b.offsetParent !== null); const br = btn ? btn.getBoundingClientRect() : null; return {pos: cs.position, top: r.top, bottom: r.bottom, btnVisible: !!br && br.top >= 0 && br.bottom <= innerHeight, indicator: !!bar.querySelector('#wp-draft-status-sop')}; })()))()""" def bar(page): return json.loads(page.eval(BAR_JS)) def main(): exe = cdp.find_browser() if not exe: print("no headless-capable browser found; set WP_BROWSER.") return 2 tmpdir = tempfile.mkdtemp(prefix="wpsuite-bar-") db_path = os.path.join(tmpdir, "check.db") server = None browser = None try: tok = seed(db_path) port = cdp.free_port() base = "http://127.0.0.1:%d" % port server = start_server(port, db_path) browser = cdp.Browser(exe) page = browser.page() page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.viewport(1280, 700) page.goto(base + "/work-package-suite.html?tab=sop") dismiss_dialogs(page) settle(2.5) chk("the wizard boots", page.eval("typeof goToStep === 'function'")) print("\n1. every step, top of page, no scrolling") last = page.eval("LAST_STEP") all_ok, detail = True, [] for n in range(1, int(last) + 1): # Forward movement is gated on real input; the check is about layout, # so walk the steps the way the rail allows after marking them visited. page.eval("currentStep = %d; updateStepUI(); window.scrollTo(0,0)" % n) settle(0.25) b = bar(page) ok = bool(b) and b["btnVisible"] all_ok = all_ok and ok if not ok: detail.append((n, b)) chk("a primary action is inside the viewport on all %s steps, unscrolled" % last, all_ok, ascii_(detail)) print("\n2. the two steps that caused B6") for name, n in (("Sequence", 8), ("Constraints", 9)): page.eval("currentStep = %d; updateStepUI(); window.scrollTo(0,0)" % n) settle(0.3) tall = page.eval("document.documentElement.scrollHeight > innerHeight") b = bar(page) chk("%s: the step overflows a 700px viewport, so the check means something" % name, bool(tall)) chk("%s: the bar is stuck to the viewport bottom with its button visible" % name, b and b["pos"] == "sticky" and b["btnVisible"], ascii_(b)) page.eval("window.scrollTo(0, document.documentElement.scrollHeight)") settle(0.3) b2 = bar(page) chk("%s: still there at the bottom of the scroll" % name, b2 and b2["btnVisible"], ascii_(b2)) chk("the T4.4 save-state indicator lives in the bar", bar(page)["indicator"]) print("\n3. 390px") page.viewport(390, 844, mobile=True) settle(0.6) page.eval("currentStep = 9; updateStepUI(); window.scrollTo(0,0)") settle(0.4) b = bar(page) chk("390px: the bar is present and its button reachable without scrolling", b and b["btnVisible"], ascii_(b)) page.eval("window.scrollTo(0, document.documentElement.scrollHeight)") settle(0.4) clear = json.loads(page.eval("""JSON.stringify((() => { const bar = document.querySelector('.step-navigation').getBoundingClientRect(); const fields = [...document.querySelectorAll( '.step-content input, .step-content textarea, .step-content select, .step-content button')] .filter(el => el.offsetParent !== null); const lastEl = fields[fields.length - 1]; const r = lastEl ? lastEl.getBoundingClientRect() : null; return {barTop: bar.top, lastBottom: r ? r.bottom : null}; })())""")) chk("390px: fully scrolled, the last control sits ABOVE the bar - nothing is obscured", clear["lastBottom"] is not None and clear["lastBottom"] <= clear["barTop"] + 1, ascii_(clear)) js_errors = [e for e in page.js_errors()] 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())