T8.5 - CR-013/D6: the material request is structure, not features
The OneNote comparison from the meeting was "word vomit"; the structure that replaces it, built at the lightweight scope EXACTLY as approved Aug 14: - Line items (qty, unit, description) added, edited, removed. Descriptions offer the D6 project list through a datalist - which is also precisely what keeps free text working when no list is loaded, the state every project is in today. Picking a listed material fills its unit; nothing locks. - Needed-by date, requestor (the signed-in account), delivery location (T8.4's fields on this package, composed), and an explicit status set (Requested / Filled / Declined). The request rides on the package record (data.materialRequests) - server-persisted through the same upsert as everything else, never localStorage. - Submitting notifies the warehouse owner named on the package (CR-010) - the routing that replaces the funnel through one person - through the T7.6 gate, with the count, the needed-by, the delivery location and the deep link, in the house convention. material_requested lands in the audit history. - The dashboard grows a Material requests queue, filterable by status and by delivery location. - The block lives inside #material-card, so the CR-006 materials toggle governs it with no special casing. The whole flow is driven at 390px - requests originate in the field. - NO parts catalog, no inventory count, no warehouse integration - the probe greps the block for them. One infrastructure bug fixed in passing detection (not silently): T8.5's dashboard-panel insert matched the substring inside "async function dashIssue", splitting the async keyword from its function - the creator failed to parse and every boot died. Caught by the probe's first run; anchored fixes now restore both halves. Verification (each probe run alone): NEW tests/mreq_check.py 19/19 (request end-to-end at 390px against the SMTP sink, dashboard filters, fences). Regressions: frame_check 39/39, sections_check 95/95, kitting_check 26/26. Items: CR-013, D6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
201
tests/mreq_check.py
Normal file
201
tests/mreq_check.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/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": "", "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["data"]
|
||||
and "2026-09-01" in body["data"] and "Shark cage 7" in body["data"]
|
||||
and ("/wp-creation-index.html?project=projA&wp=" + wp_id) in body["data"],
|
||||
ascii_(body["data"], 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())
|
||||
Reference in New Issue
Block a user