Staging is not the pain; the last fifty feet are - the correct floor lay-down,
shark cage or conduit tree instead of material picked at will by whoever is
closest. The Kitting & MIMO section gains:
- Delivery Building / Floor / Sector: the SAME dependent pickers CR-004 built,
through the same fillLocSelect (which learned an optional field-map instead
of being copied), reading the same project location lists, storing PATHS.
A parallel free-text location vocabulary is exactly what CR-004 removed;
none was added.
- A free-text detail field for the specifics ("Shark cage 7, conduit tree C"),
persisted as delivDetail.
- deliveryLoc, the composed display string (labels off the shared lists, then
the detail after a dash) - which is what the CR-011 email already reads
(kitting_body preferred deliveryLoc from day one, with mimoLoc as the
pre-CR-012 fallback) and what the package printout now carries as its own
Delivery Location row.
Verification (each probe run alone): kitting_check.py extended to 26/26 (the
delivery selects are asserted to offer the SAME option list as the CR-004
trio, values persist as paths, the printout carries the composed value);
kitting_notify_check 17/17 now asserting the mail carries CR-012's composed
value, not the fallback. Regression: locations_check 58/58.
Items: CR-012
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
259 lines
13 KiB
Python
259 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Kitting: statuses, owner, filter, delivery — CR-009/CR-010/CR-012 (T8.1/2/4).
|
|
|
|
The statuses become an explicit five (Not Started / Picking / Staged /
|
|
In Transit / Delivered) - fulfillment states, not free text. A value stored
|
|
before the set existed is kept and shown as a legacy option: renamed sets must
|
|
not orphan recorded data. The Micron EUV configuration (the sample SOP, per
|
|
CR-016/T5.7) turns the section OFF through the CR-006 toggle; any other SOP
|
|
can turn it on and everything works; nothing is deleted either way.
|
|
|
|
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
|
|
from qa_gate_check import api # noqa: E402
|
|
|
|
KIT = ["Not Started", "Picking", "Staged", "In Transit", "Delivered"]
|
|
|
|
|
|
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 kit_options(page):
|
|
return json.loads(page.eval(
|
|
"JSON.stringify([...document.getElementById('wp_kit_status').options]"
|
|
".map(o=>o.textContent))"))
|
|
|
|
|
|
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-kit-")
|
|
db_path = os.path.join(tmpdir, "check.db")
|
|
server = None
|
|
browser = None
|
|
try:
|
|
tok = seed(db_path)
|
|
set_sop(db_path, {}) # sopA: every section ON, kitting included
|
|
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.6)
|
|
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
|
|
|
|
# ── 1. the set ────────────────────────────────────────────────────────
|
|
print("\n1. an explicit set, not free text")
|
|
opts = kit_options(page)
|
|
chk("the kitting status control is a select offering exactly the five states",
|
|
opts == ["—"] + KIT, ascii_(opts))
|
|
chk("...and the set is one named constant",
|
|
json.loads(page.eval("JSON.stringify(KIT_STATUSES)")) == KIT)
|
|
|
|
# a pre-CR-009 value survives as a legacy option, selected
|
|
page.eval("""loadPackageIntoForm({subject:'legacy kit', type:'Conduit Install',
|
|
kitStatus:'Kitted', kitOwner:'Paul Coonrod', constraints:[], holds:[]})""")
|
|
settle(0.8)
|
|
chk("a stored pre-CR-009 value is kept, selected, and marked legacy",
|
|
page.eval("document.getElementById('wp_kit_status').value") == "Kitted"
|
|
and any("legacy" in o for o in kit_options(page)), ascii_(kit_options(page)))
|
|
chk("...and it round-trips through collect unchanged - nothing re-written",
|
|
page.eval("collectPackage().kitStatus") == "Kitted")
|
|
page.eval("document.getElementById('wp_kit_status').value='Staged'")
|
|
chk("...and picking a new value works from the same control",
|
|
page.eval("collectPackage().kitStatus") == "Staged")
|
|
|
|
# ── 2. this SOP (kitting ON) has the section, working ─────────────────
|
|
print("\n2. an SOP with kitting on")
|
|
vis = page.eval("""(() => { const c=document.getElementById('mimo-card');
|
|
return c && !c.hidden && getComputedStyle(c).display !== 'none'; })()""")
|
|
chk("the Kitting & MIMO section is present and rendered", bool(vis))
|
|
chk("...and appears in the section rail", page.eval(
|
|
"!!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
|
|
|
|
# ── 3. Micron EUV (the sample): kitting OFF ───────────────────────────
|
|
print("\n3. Micron EUV: off, hidden, not deleted")
|
|
page.eval("loadSampleSOP()")
|
|
settle(1.0)
|
|
chk("the sample IS the Micron configuration and turns kitting off",
|
|
page.eval("SOP.sections && SOP.sections.kitting === false"))
|
|
hidden = page.eval("""(() => { const c=document.getElementById('mimo-card');
|
|
return c && (c.hidden || getComputedStyle(c).display === 'none'); })()""")
|
|
chk("the section is gone from the form", bool(hidden))
|
|
chk("...and from the rail", page.eval(
|
|
"!document.querySelector(%s)" % json.dumps('.sec-rail-item[data-sec="mimo-card"]')))
|
|
page.eval("loadExample()")
|
|
settle(1.0)
|
|
doc = page.eval("(() => { renderPackage(collectPackage()); "
|
|
"return document.getElementById('pkg-doc').innerHTML; })()")
|
|
chk("...and from the export", "Kitting & MIMO" not in doc)
|
|
chk("the example package's kitting DATA is still on the record - hidden, "
|
|
"never deleted",
|
|
page.eval("collectPackage().kitStatus") != ""
|
|
and bool(page.eval("collectPackage().kitOwner")), ascii_(
|
|
(page.eval("collectPackage().kitStatus"),
|
|
page.eval("collectPackage().kitOwner"))))
|
|
|
|
# ── 4. CR-010: the warehouse owner (T8.2) ────────────────────────────
|
|
print(chr(10) + "4. CR-010: the warehouse owner")
|
|
page.eval("set_sop_reload = 0") # marker only
|
|
page.goto(base + "/wp-creation-index.html?project=projA")
|
|
dismiss_dialogs(page)
|
|
wait_creator(page)
|
|
settle(1.6)
|
|
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
|
|
chk("the warehouse owner is a dropdown of project members, not free text",
|
|
page.eval("document.getElementById('wp_kit_owner_sel').tagName") == "SELECT"
|
|
and page.eval("document.getElementById('wp_kit_owner_sel').options.length") > 1,
|
|
page.eval("document.getElementById('wp_kit_owner_sel').options.length"))
|
|
page.eval("""(() => {
|
|
const sel=document.getElementById('wp_kit_owner_sel');
|
|
sel.value=[...sel.options].find(o=>o.value==='user_sue').value;
|
|
sel.onchange.call(sel);
|
|
})()""")
|
|
got = json.loads(page.eval(
|
|
"JSON.stringify({name: collectPackage().kitOwner, id: collectPackage().kitOwnerId})"))
|
|
chk("picking a member persists BOTH the display name and the account id "
|
|
"(the id is what CR-011 notifications will read)",
|
|
got["id"] == "user_sue" and got["name"], ascii_(got))
|
|
|
|
page.eval("""loadPackageIntoForm({subject:'legacy owner', type:'Conduit Install',
|
|
kitOwner:'Paul Coonrod', constraints:[], holds:[]})""")
|
|
settle(0.8)
|
|
chk("a stored name with NO account renders as a kept '(no account)' option "
|
|
"- removing someone from the project breaks nothing",
|
|
page.eval("document.getElementById('wp_kit_owner_sel').value") == "__orphan__"
|
|
and "no account" in page.eval(
|
|
"document.getElementById('wp_kit_owner_sel').selectedOptions[0].textContent"))
|
|
chk("...and the stored name still round-trips",
|
|
page.eval("collectPackage().kitOwner") == "Paul Coonrod")
|
|
|
|
# the dashboard filter
|
|
page.eval("""(() => {
|
|
const mk=(id,num,owner)=>({id, number:num, subject:'kit '+num,
|
|
type:'Conduit Install', status:'Draft', kitOwner:owner,
|
|
constraints:[], holds:[], projectId:activeProjectId});
|
|
savedPackages.push(mk('wpKA','WP70-KA','Paul Coonrod'));
|
|
savedPackages.push(mk('wpKB','WP71-KB','Sue'));
|
|
saveStore(); renderSavedList();
|
|
})()""")
|
|
page.eval("showDashboard()")
|
|
settle(1.2)
|
|
page.eval("dashFilter.kitOwner='Paul Coonrod'; dashPage=0; renderDashboard()")
|
|
settle(0.8)
|
|
board = page.eval("(document.getElementById('dash-body')||{textContent:''}).textContent")
|
|
chk("the dashboard filters by warehouse owner - their package shows",
|
|
"WP70-KA" in board, ascii_(board, 120))
|
|
chk("...and the other owner's does not", "WP71-KB" not in board)
|
|
chk("...through a labelled control on the board",
|
|
page.eval("!!document.querySelector('[aria-label=%s]')"
|
|
% json.dumps("Filter by warehouse owner")))
|
|
|
|
# ── 5. CR-012: the delivery location (T8.4) ──────────────────────────
|
|
print(chr(10) + "5. CR-012: delivery on the shared lists")
|
|
code, b1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
|
|
{"level": "building", "name": "B-100"})
|
|
code, f1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
|
|
{"level": "floor", "parent_id": b1["id"], "name": "Level 3"})
|
|
code, s1 = api(base, "/api/projects/projA/locations", tok["root"], "POST",
|
|
{"level": "sector", "parent_id": f1["id"], "name": "Sector East"})
|
|
page.goto(base + "/wp-creation-index.html?project=projA")
|
|
dismiss_dialogs(page)
|
|
wait_creator(page)
|
|
settle(1.8)
|
|
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
|
|
opts = json.loads(page.eval(
|
|
"JSON.stringify([...document.getElementById('wp_deliv_building').options].map(o=>o.value))"))
|
|
wp_opts = json.loads(page.eval(
|
|
"JSON.stringify([...document.getElementById('wp_building').options].map(o=>o.value))"))
|
|
chk("the delivery building select is fed by the SAME shared list as CR-004's",
|
|
opts == wp_opts and len(opts) > 1, ascii_((opts, wp_opts)))
|
|
page.eval("document.getElementById('wp_deliv_building').value=%s; onDeliveryLocChange('building')"
|
|
% json.dumps(b1["path"]))
|
|
settle(0.3)
|
|
page.eval("document.getElementById('wp_deliv_floor').value=%s; onDeliveryLocChange('floor')"
|
|
% json.dumps(f1["path"]))
|
|
settle(0.3)
|
|
page.eval("document.getElementById('wp_deliv_sector').value=%s; onDeliveryLocChange('sector')"
|
|
% json.dumps(s1["path"]))
|
|
page.eval("document.getElementById('wp_deliv_detail').value='Shark cage 7, conduit tree C'")
|
|
got = json.loads(page.eval("""JSON.stringify((() => { const p = collectPackage();
|
|
return {b: p.delivBuilding, f: p.delivFloor, s: p.delivSector,
|
|
d: p.delivDetail, disp: p.deliveryLoc}; })())"""))
|
|
chk("the picked values persist as PATHS off the shared lists",
|
|
got["b"] == b1["path"] and got["f"] == f1["path"] and got["s"] == s1["path"],
|
|
ascii_(got))
|
|
chk("...the detail field persists", got["d"] == "Shark cage 7, conduit tree C")
|
|
chk("...and the composed display carries labels AND the detail",
|
|
"B-100" in got["disp"] and "Shark cage 7" in got["disp"], ascii_(got["disp"]))
|
|
page.eval("document.getElementById('wp_subject').value='deliv print'")
|
|
page.eval("document.getElementById('wp_type').value='Conduit Install'")
|
|
doc = page.eval("(() => { renderPackage(collectPackage()); "
|
|
"return document.getElementById('pkg-doc').innerHTML; })()")
|
|
chk("the delivery location prints on the package output",
|
|
"Delivery Location" in doc and "Shark cage 7" in doc, ascii_(doc, 120))
|
|
|
|
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())
|