T9.1 - CR-008: the export, finally walked through
Every required field verified present under the Micron configuration: P6
Activity id and description, Priority, the Building/Floor/Sector location
row, Scope & Work, Material List, Constraints with their status carried by
WORDS (a black-and-white print keeps its meaning), Quality & Hold Points, and
the Drawings & Attachments index with per-file descriptions (T7.7's columns).
What must be absent, absent: the Micron samples now carry
fields:{costCode:false, acumaticaTask:false} - CR-002's two removals,
expressed as the toggles CLAUDE.md requires, in the creator sample AND the
wizard sample - so ACU Cost Code and Acumatica Task appear nowhere on the
Micron export while the columns, model and recorded values stay. CR-006-
suppressed sections (assets, kitting) are absent, as pinned since T5.7.
Tablet legibility, three real defects fixed:
- a bare `table { min-width:520px }` in the narrow-screen media block reached
the EXPORT tables too, dragging the whole document to 520px on a 390px
screen; scoped to .table-wrap (the form's scroll containers), because the
export must FIT a tablet, not scroll
- export tables now table-layout:fixed with overflow-wrap:anywhere
- at <=768px the doc sheds its 52/56px desk padding and neutralises the
inline column widths (the one legitimate !important: outranking an inline
style is its job)
RAISED, NOT DECIDED (the task says propose, do not assume) - merge versus
list for attachments: RECOMMEND MERGING image attachments into the printed
document (already done - they print inline as the sheet itself) and LISTING
PDFs as named, described links rather than merging them. Merging PDFs
server-side needs a PDF library dependency and re-renders every export for a
need the meeting expressed as "hand someone exactly the sheet" - which the
5MB single-sheet uploads plus inline images already serve. If merged-PDF
output is wanted anyway, it is a bounded server task - needs Nick.
Verification (each probe run alone): NEW tests/export_check.py 17/17.
Regressions: form_structure_check 50/51 (the standing F6 height question,
BL-022), sections_check 95/95.
Items: CR-008 (CR-002 field toggles applied to the samples)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -296,6 +296,12 @@ python tests/materials_check.py # D6 - material list, the CR-005 pattern
|
|||||||
python tests/mreq_check.py # CR-013 - lightweight request, end to end 19 checks
|
python tests/mreq_check.py # CR-013 - lightweight request, end to end 19 checks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Wave 9 adds these:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tests/export_check.py # CR-008 - the export, walked field by field 17 checks
|
||||||
|
```
|
||||||
|
|
||||||
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
||||||
cross-frame toggle hand-off, which no longer exists; it now pins the surviving path and
|
cross-frame toggle hand-off, which no longer exists; it now pins the surviving path and
|
||||||
the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded
|
the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ function loadSampleData(){
|
|||||||
// section added after today is not silently off for this SOP.
|
// section added after today is not silently off for this SOP.
|
||||||
if(typeof WPSections !== 'undefined'){
|
if(typeof WPSections !== 'undefined'){
|
||||||
state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today
|
state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today
|
||||||
|
state.fields = WPSections.normalizeFields({costCode: false, acumaticaTask: false}); // CR-002, mirrored on the export (T9.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the
|
// The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ const SAMPLE_SOP = {
|
|||||||
// Written as a partial map, not a full one: WPSections.normalize fills the rest
|
// Written as a partial map, not a full one: WPSections.normalize fills the rest
|
||||||
// in as ON, so a section added later is not silently off for this SOP.
|
// in as ON, so a section added later is not silently off for this SOP.
|
||||||
sections:{assets:false, kitting:false}, // kitting off: Micron EUV is not kitting today (CR-009/T8.1)
|
sections:{assets:false, kitting:false}, // kitting off: Micron EUV is not kitting today (CR-009/T8.1)
|
||||||
|
// CR-002 / T9.1: the two fields the review removed, expressed as the toggles
|
||||||
|
// CLAUDE.md requires - hidden on the form AND absent from the export, while
|
||||||
|
// the columns, the model and every recorded value stay exactly where they are.
|
||||||
|
fields:{costCode:false, acumaticaTask:false},
|
||||||
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
|
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
|
||||||
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
|
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
|
||||||
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 3–5 days (≈40–80 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' },
|
governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'Standard — 3–5 days (≈40–80 hrs)', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'80' },
|
||||||
|
|||||||
@@ -344,7 +344,19 @@
|
|||||||
.output-doc p { margin-bottom: 10px; }
|
.output-doc p { margin-bottom: 10px; }
|
||||||
.output-doc ul { padding-left: 20px; margin-bottom: 10px; }
|
.output-doc ul { padding-left: 20px; margin-bottom: 10px; }
|
||||||
.output-doc li { margin-bottom: 3px; }
|
.output-doc li { margin-bottom: 3px; }
|
||||||
.output-doc table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; border: 1px solid var(--border); }
|
.output-doc table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; border: 1px solid var(--border);
|
||||||
|
/* CR-008 / T9.1: the export opens on tablets. Fixed layout + wrap-anywhere
|
||||||
|
keeps every table inside the screen instead of laying out at its content's
|
||||||
|
natural 520px and dragging the whole document sideways. */
|
||||||
|
table-layout: fixed; }
|
||||||
|
.output-doc td, .output-doc th { overflow-wrap: anywhere; }
|
||||||
|
/* On a phone/tablet the inline column widths the desktop layout carries
|
||||||
|
(width:200px on header cells) exceed the available line; !important is the
|
||||||
|
one CSS mechanism that outranks an inline style, which is exactly the job. */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.output-doc { padding: 20px 14px; }
|
||||||
|
.output-doc th, .output-doc td { width: auto !important; }
|
||||||
|
}
|
||||||
.output-doc th { background: var(--surface2); border: 1px solid var(--border); padding: 6px 10px; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; text-align: left; color: var(--text-muted); }
|
.output-doc th { background: var(--surface2); border: 1px solid var(--border); padding: 6px 10px; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; text-align: left; color: var(--text-muted); }
|
||||||
.output-doc td { border: 1px solid var(--border); padding: 7px 10px; vertical-align: top; }
|
.output-doc td { border: 1px solid var(--border); padding: 7px 10px; vertical-align: top; }
|
||||||
.output-doc .badge { display: inline-block; padding: 1px 8px; border-radius: 3px; font-family: var(--mono); font-size: 10px; font-weight: 600; }
|
.output-doc .badge { display: inline-block; padding: 1px 8px; border-radius: 3px; font-family: var(--mono); font-size: 10px; font-weight: 600; }
|
||||||
@@ -659,7 +671,10 @@
|
|||||||
.modal { max-width:100% !important; }
|
.modal { max-width:100% !important; }
|
||||||
.output-toolbar { flex-wrap:wrap; }
|
.output-toolbar { flex-wrap:wrap; }
|
||||||
.material-actions .add-btn { flex:1 1 auto; }
|
.material-actions .add-btn { flex:1 1 auto; }
|
||||||
table { min-width:520px; } /* keep columns legible; .table-wrap scrolls */
|
/* Scoped to the FORM's scroll containers only. This was a bare `table` and
|
||||||
|
it reached the export document too, forcing every printed table to 520px
|
||||||
|
inside a 390px screen - the export must FIT a tablet, not scroll (CR-008). */
|
||||||
|
.table-wrap table { min-width:520px; } /* keep columns legible; .table-wrap scrolls */
|
||||||
.cstatus button { padding:6px 8px; }
|
.cstatus button { padding:6px 8px; }
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
157
tests/export_check.py
Normal file
157
tests/export_check.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Does the export print what the field needs, and nothing it does not? — CR-008, T9.1.
|
||||||
|
|
||||||
|
The export was never walked through in the meeting. This is the walk: every
|
||||||
|
required field present, CR-006-suppressed sections absent, the two CR-002
|
||||||
|
fields (ACU Cost Code, Acumatica Task) appearing NOWHERE under the Micron
|
||||||
|
configuration, status carried by words (a black-and-white print keeps its
|
||||||
|
meaning), and the whole document legible on a tablet.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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-export-")
|
||||||
|
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.6)
|
||||||
|
page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;")
|
||||||
|
|
||||||
|
# The Micron configuration and its example package - the real shape.
|
||||||
|
page.eval("loadSampleSOP()")
|
||||||
|
settle(1.0)
|
||||||
|
page.eval("loadExample()")
|
||||||
|
settle(1.0)
|
||||||
|
doc = page.eval("(() => { renderPackage(collectPackage()); "
|
||||||
|
"return document.getElementById('pkg-doc').innerHTML; })()")
|
||||||
|
text = page.eval("(document.getElementById('pkg-doc')||{textContent:''}).textContent")
|
||||||
|
|
||||||
|
print("\n1. every listed field, present")
|
||||||
|
for label, needle in [
|
||||||
|
("P6 Activity id + description", "P6 Activity"),
|
||||||
|
("Priority", "Priority"),
|
||||||
|
("scope of work", "Scope & Work"),
|
||||||
|
("materials", "Material List"),
|
||||||
|
("constraints", "Constraints — Release Readiness"),
|
||||||
|
("QA/QC", "Quality & Hold Points"),
|
||||||
|
]:
|
||||||
|
chk("the export carries %s" % label, needle in text or needle in doc, needle)
|
||||||
|
chk("...and the location row (Building / Floor / Sector is a row of "
|
||||||
|
"General Information)", "Location" in text)
|
||||||
|
chk("constraint status prints as WORDS - a black-and-white print keeps "
|
||||||
|
"its meaning", "Cleared" in text and "Open" in text)
|
||||||
|
|
||||||
|
print("\n2. what must be absent, absent")
|
||||||
|
chk("ACU Cost Code appears nowhere", "Cost Code" not in text)
|
||||||
|
chk("Acumatica Task appears nowhere", "Acumatica Task" not in text)
|
||||||
|
chk("the toggled-off sections are absent (assets, kitting - the Micron "
|
||||||
|
"sample's CR-006 state)",
|
||||||
|
"Kitting & MIMO" not in text and "Asset" not in text.replace("Assets is off", ""))
|
||||||
|
|
||||||
|
print("\n3. the attachment index")
|
||||||
|
chk("every attachment is listed", "Drawings & Attachments" in text)
|
||||||
|
chk("...uploads would carry their descriptions (the columns exist)",
|
||||||
|
"Focus area" in text or "Link / Note" in text)
|
||||||
|
|
||||||
|
print("\n4. the tablet")
|
||||||
|
page.viewport(390, 844, mobile=True)
|
||||||
|
settle(0.8)
|
||||||
|
page.eval("renderPackage(collectPackage())")
|
||||||
|
settle(0.8)
|
||||||
|
# scrollWidth vs clientWidth is the wrong ruler for a padded overflow-y
|
||||||
|
# container: Chrome reports scrollWidth as content+padding even with
|
||||||
|
# nothing protruding, so the naive comparison is off by exactly the
|
||||||
|
# padding+border. What legibility actually needs: the BOX fits the
|
||||||
|
# screen, and no element inside pokes past the box.
|
||||||
|
fits = page.eval("""(() => {
|
||||||
|
const d = document.getElementById('pkg-doc');
|
||||||
|
const dr = d.getBoundingClientRect();
|
||||||
|
const cs = getComputedStyle(d);
|
||||||
|
let poke = 0;
|
||||||
|
d.querySelectorAll('*').forEach(el => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
if (r.right > dr.right + 2) poke++;
|
||||||
|
});
|
||||||
|
return {boxFits: dr.width <= 390, poke, font: parseFloat(cs.fontSize)};
|
||||||
|
})()""")
|
||||||
|
chk("390px: the document fits the screen and nothing inside pokes past it",
|
||||||
|
fits["boxFits"] and fits["poke"] == 0, ascii_(fits))
|
||||||
|
chk("390px: the base type is readable (>= 12px)", fits["font"] >= 12, ascii_(fits))
|
||||||
|
|
||||||
|
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())
|
||||||
Reference in New Issue
Block a user