#!/usr/bin/env python3 """Can the sidebar answer the stand-up question? — A6, T7.5. The use case in the task: someone is asked in a stand-up why a package has not moved. They open it on a phone and need the answer WITHOUT scrolling or clicking — so the hold reason, captured by the hold modal since T7.3, has to be readable straight off the navigator row, along with status, priority, due date, P6 activity and the open-constraint count. Three package shapes cover the matrix: A — on hold, reason recorded (the stand-up case) B — a plain draft (no hold: the row must look sensible, not have empty slots) C — on hold the legacy way, holds:[] (the row must say the reason is missing, not render an empty red line) 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 sections_check import set_sop # noqa: E402 from stepper_check import dismiss_dialogs # noqa: E402 REASON = "Boom lift recalled for inspection" def ascii_(v, n=300): return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n] def settle(seconds=0.6): time.sleep(seconds) def wait_creator(page, tries=40): for _ in range(tries): if page.eval("!!window.wpCreatorReady"): return True time.sleep(0.3) return False def constraint_btn(page, index, which): labels = {"open": "Open", "cleared": "Cleared", "na": "N/A"} page.eval("""(() => { const tr = document.querySelectorAll('#constraint-body tr')[%d]; [...tr.querySelectorAll('.cstatus button')] .find(b => b.textContent.trim() === %s).click(); })()""" % (index, json.dumps(labels[which]))) settle(0.4) def click_status(page, val): page.eval("document.querySelector('#status-group .radio-pill[data-val=%s]').click()" % json.dumps(val)) settle(0.4) def set_field(page, fid, val): page.eval("(() => { const el = document.getElementById(%s); el.value = %s; " "el.dispatchEvent(new Event('input', {bubbles: true})); })()" % (json.dumps(fid), json.dumps(val))) def nav_row(page, number): """The navigator row for a package number, as data.""" return json.loads(page.eval("""JSON.stringify((() => { const row = [...document.querySelectorAll('.wp-nav-item')] .find(b => (b.querySelector('.wp-nav-num')||{textContent:''}).textContent === %s); if (!row) return null; const g = sel => { const e = row.querySelector(sel); return e ? e.textContent : null; }; const r = row.getBoundingClientRect(); return {triage: g('.wp-nav-triage'), hold: g('.wp-nav-hold'), state: g('.wp-nav-state'), subj: g('.wp-nav-subj'), h: r.height, scrollW: row.scrollWidth, clientW: row.clientWidth}; })())""" % json.dumps(number))) 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-triage-") db_path = os.path.join(tmpdir, "check.db") server = None browser = None try: tok = seed(db_path) set_sop(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(1440, 900) page.goto(base + "/wp-creation-index.html?project=projA") dismiss_dialogs(page) chk("the creator boots", wait_creator(page)) settle(1.5) page.eval("window.prompt=()=>null; window.alert=()=>{}; window.confirm=()=>false;") # ── build package A: on hold, with a reason ─────────────────────────── print("\n1. package A: held, reason recorded (the stand-up case)") set_field(page, "wp_subject", "Horn strobe conduit") set_field(page, "wp_type", "Conduit Install") set_field(page, "wp_priority", "High") set_field(page, "wp_due", "2026-09-01") set_field(page, "wp_p6_id", "A-1040") n = page.eval("pkgConstraints.length") for i in range(n): constraint_btn(page, i, "cleared") click_status(page, "In Progress") constraint_btn(page, 0, "open") # opens the hold modal page.eval("document.getElementById('hold-details').value=%s" % json.dumps(REASON)) page.eval("submitHold()") settle(0.4) page.eval("savePackage(false)") settle(0.8) numA = page.eval("(savedPackages[savedPackages.length-1]||{}).number||''") row = nav_row(page, numA) chk("the held package renders a row in the sidebar", row is not None, numA) chk("the hold reason is ON the row — no modal, no hover, no click", row and REASON in (row["hold"] or ""), ascii_(row)) chk("...and the row names the constraint it hangs on", row and "Construction Equipment" in (row["hold"] or "") or (row and (row["hold"] or "").count(":") >= 1), ascii_(row and row["hold"])) chk("no modal is open while all of that is readable", page.eval("!document.querySelector('#hold-modal.open')")) tri = (row or {}).get("triage") or "" chk("status, priority, due date and P6 activity are all on the row together", "Issue" in tri and "High" in tri and "2026-09-01" in tri and "A-1040" in tri, ascii_(tri)) chk("...and the open-constraint count is on the row too", row and re.search(r"1 open", (row["hold"] or "") + " " + (row["state"] or "")), ascii_(row)) # ── package B: a plain draft ────────────────────────────────────────── print("\n2. package B: nothing held, nothing weird") page.eval("newPackage()") settle(0.6) set_field(page, "wp_subject", "Wire pull") set_field(page, "wp_type", "Conduit Install") page.eval("savePackage(false)") settle(0.8) numB = page.eval("(savedPackages[savedPackages.length-1]||{}).number||''") row_b = nav_row(page, numB) chk("the no-hold package has NO hold line at all — not an empty red slot", row_b is not None and row_b["hold"] is None, ascii_(row_b)) chk("...its state still says something (ready / N open), never a blank", row_b and (row_b["state"] or "").strip() != "", ascii_(row_b)) tri_b = (row_b or {}).get("triage") or "" chk("...and its triage line shows placeholders, not gaps, for unset fields", "Draft" in tri_b and "—" in tri_b and "Normal" in tri_b, ascii_(tri_b)) # ── package C: held the legacy way, holds:[] ────────────────────────── print("\n3. package C: a legacy hold with no entry") page.eval("""(() => { const p = collectPackage(); p.id = 'wp_legacy_probe'; p.number = 'WP99-LEGACY'; p.subject = 'Legacy hold'; p.status = 'Issue'; p.holds = []; p.constraints[0].status = 'open'; savedPackages.push(p); renderWpNav(); })()""") settle(0.5) row_c = nav_row(page, "WP99-LEGACY") chk("a hold with no recorded entry says so instead of rendering nothing", row_c and "no reason recorded" in (row_c["hold"] or ""), ascii_(row_c)) # ── 390px: the phone in the stand-up ────────────────────────────────── print("\n4. 390px") page.viewport(390, 900, mobile=True) settle(0.8) page.eval("openWpNav()") settle(0.6) row = nav_row(page, numA) chk("390px: the held row renders in the opened panel", row is not None and row["h"] > 0, ascii_(row)) chk("390px: the reason is still readable there", row and REASON in (row["hold"] or ""), ascii_(row)) chk("390px: the row does not overflow its panel sideways", row and row["scrollW"] <= row["clientW"] + 2, ascii_(row)) chk("390px: the row is a comfortable tap target (>= 44px tall)", row and row["h"] >= 44, ascii_(row)) 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())