#!/usr/bin/env python3 """Is the material request the lightweight scope, wired to the list? — CR-013, T8.5. Requests ran through per-floor Teams chats and a spreadsheet, all funneling to one person. The structure that replaces it: line items (qty, unit, description), needed-by, requestor, delivery (T8.4's fields), status - riding on the package record, visible on the dashboard, filterable by status and delivery location, notifying the warehouse owner (CR-010) through the T7.6 gate. Items pick from the D6 list through a datalist, which is also exactly what keeps free text working when no list exists. 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 from qa_gate_check import SmtpSink, api, wait_for # 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) def wait_creator(page, tries=40): for _ in range(tries): if page.eval("!!window.wpCreatorReady"): return True time.sleep(0.3) return False 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-mreq-") db_path = os.path.join(tmpdir, "check.db") server = None browser = None sink = SmtpSink() sink.start() 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) root = tok["root"] api(base, "/api/projects/projA/materials/import", root, "POST", {"text": "Sample 3/4in EMT,FT,S-EMT\nSample strut,FT", "dry_run": False}) api(base, "/api/settings", root, "PUT", { "email_enabled": True, "smtp_host": "127.0.0.1", "smtp_port": sink.port, "smtp_use_tls": False, "from_addr": "suite@sink.local", "app_base_url": base}) browser = cdp.Browser(exe) page = browser.page() page.clear_cookies() page.set_cookie("wp_session", root) page.viewport(390, 844, mobile=True) # requests originate in the field page.goto(base + "/wp-creation-index.html?project=projA") dismiss_dialogs(page) chk("the creator boots at 390px", wait_creator(page)) settle(1.8) page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;") # ── 1. the request, end to end ──────────────────────────────────────── print("\n1. raise a request") page.eval("document.getElementById('wp_subject').value='mreq host'") page.eval("document.getElementById('wp_type').value='Conduit Install'") page.eval("""(() => { const sel=document.getElementById('wp_kit_owner_sel'); sel.value='user_sue'; sel.onchange.call(sel); })()""") page.eval("document.getElementById('wp_deliv_detail').value='Shark cage 7'") page.eval("void savePackage(false)") settle(1.2) chk("the description datalist offers the D6 list", page.eval("document.querySelectorAll('#mat-datalist option').length") == 2) chk("...and says so", "material list" in page.eval( "(document.getElementById('mreq-hint')||{textContent:''}).textContent")) page.eval("mreqAddItem(); mreqAddItem()") page.eval("_mreqDraft[0]={qty:'400',unit:'',desc:'Sample 3/4in EMT'};" "mreqMaybeUnit(0,'Sample 3/4in EMT')") page.eval("_mreqDraft[1]={qty:'2',unit:'EA',desc:'Left-handed flange spreader'};" "mreqRenderDraft()") chk("picking a listed material fills its unit", json.loads(page.eval("JSON.stringify(_mreqDraft[0])"))["unit"] == "FT") page.eval("document.getElementById('mreq-needed').value='2026-09-01'") page.eval("mreqSubmit()") settle(1.2) req = json.loads(page.eval("JSON.stringify(pkgMatRequests[0]||{})")) chk("the request records lines, needed-by, requestor, delivery and status", len(req.get("items", [])) == 2 and req.get("neededBy") == "2026-09-01" and req.get("requestor") and req.get("status") == "Requested" and "Shark cage 7" in (req.get("deliveryLoc") or ""), ascii_(req)) chk("...with the free-text line accepted beside the listed one", any("flange spreader" in (it.get("desc") or "") for it in req["items"])) wp_id = page.eval("editingId") _, wp = api(base, "/api/wps/" + wp_id, root) chk("...persisted on the server record, not in this browser", len((wp.get("data") or {}).get("materialRequests") or []) == 1) chk("...and the warehouse owner is notified through the T7.6 gate", wait_for(lambda: any("Material request" in m["data"] for m in sink.messages), 12)) body = next((m for m in sink.messages if "Material request" in m["data"]), {"data": "", "text": "", "to": [""]}) chk("...the mail goes to the owner, says the size, the date, the delivery " "and carries the deep link", body["to"] == ["sue@example.test"] and "2 lines" in body["text"] and "2026-09-01" in body["text"] and "Shark cage 7" in body["text"] and ("/wp-creation-index.html?project=projA&wp=" + wp_id) in body["text"], ascii_(body["text"], 300)) _, ev = api(base, "/api/audit?entity_type=wp&entity_id=%s&action=material_requested" % wp_id, root) chk("...and the audit history has it", bool(ev)) page.eval("""(() => { const sel=document.querySelector('#mreq-list select'); sel.value='Filled'; sel.dispatchEvent(new Event('change')); })()""") settle(1.0) _, wp = api(base, "/api/wps/" + wp_id, root) chk("the status set is explicit and a change persists", (wp.get("data") or {}).get("materialRequests", [{}])[0].get("status") == "Filled") chk("390px: the request block does not push the page sideways", page.eval("document.getElementById('mreq-new').scrollWidth <= 392")) # ── 2. the dashboard queue ──────────────────────────────────────────── print("\n2. the dashboard") page.viewport(1440, 900) page.eval("showDashboard()") settle(1.4) board = lambda: page.eval("(document.getElementById('dash-body')||{textContent:''}).textContent") chk("requests appear on the board", "Material requests" in board() and "flange spreader" in board()) page.eval("mreqDashFilter.status='Requested'; renderDashboard()") settle(0.6) chk("filter by status: a Filled request leaves the Requested view", "flange spreader" not in board()) page.eval("mreqDashFilter.status='Filled'; renderDashboard()") settle(0.6) chk("...and appears in the Filled one", "flange spreader" in board()) page.eval("mreqDashFilter.status=''; mreqDashFilter.loc='nowhere'; renderDashboard()") settle(0.6) chk("filter by delivery location works the same way", "flange spreader" not in board()) # ── 3. the fences ───────────────────────────────────────────────────── print("\n3. the fences") src = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "html", "wp-creation-app.js"), encoding="utf-8").read() block = src[src.find("CR-013 / T8.5"):src.find("function dashIssue")] chk("no catalog, inventory count or warehouse integration in the diff", not re.search(r"on_hand|stock_|inventory_|warehouse_api", block, re.I)) chk("the section respects CR-006 - requests live inside the materials card", page.eval("!!document.querySelector('#material-card #mreq-new')")) 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: sink.stop() 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())