T9.4 - S7: one sample-data affordance, confirmed, and fenced off the project
Four affordances under three names became ONE: "Load sample data", on the creator's toolbar, at the far end of two separators from the live actions (New / Duplicate), pushed right with its own gap. It confirms through the T7.9 dialog, naming exactly what it does - and what it does not: "This page only: nothing is written to the project unless you then save." The probe verifies the fence the way the done-when demands - against a REAL project, reading the server's SOP and work-package list before and after and asserting byte-identical. Gone: the wizard's header "Load sample" (the dangerous one: it filled the state completeSOP() pushes to the LIVE project, one click, no confirm, no undo - reconciled with D1 exactly as the task records: the creator's control is the survivor, the wizard copy goes), the creator's split Sample SOP / Load example pair (now internals behind the one entry point), and the empty-state context bar's third button (its text now points at the toolbar control). The location/material "Load sample values" buttons stay: they fill a PASTE BOX that acts only through an explicit, dry-runnable import - a different thing, stated in the code. Probes re-pointed with reasons in place: frame_check's D1 toolbar list names the consolidated control; validation_check's sample-driven toast checks became the-affordance-is-gone checks (and its stale showAnalytics drive, orphaned by T7.10, became a the-duplicate-stays-gone check). Verification (each probe run alone): NEW tests/sample_check.py 10/10. Regressions: validation_check 77/77, frame_check 38/38, kitting_check 26/26, export_check 20/20, sections_check 95/95. Items: S7 (D1 reconciliation honored) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
169
tests/sample_check.py
Normal file
169
tests/sample_check.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is there exactly one sample-data affordance, and is it fenced? — S7, T9.4.
|
||||
|
||||
Four affordances under three names, one of them a single click from live
|
||||
project data with no confirm. Now: one control, one name ("Load sample data"),
|
||||
on the creator's toolbar at the far end of two separators from the live
|
||||
actions, confirming before it acts, naming exactly what it does - and unable
|
||||
to touch live project data, verified by attempting it against a real project
|
||||
and reading the server before and after.
|
||||
|
||||
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
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HTML = os.path.join(ROOT, "html")
|
||||
|
||||
|
||||
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
|
||||
|
||||
# ── 1. exactly one, by grep ───────────────────────────────────────────────
|
||||
print("\n1. grep: one affordance, one name")
|
||||
hits = []
|
||||
for name in os.listdir(HTML):
|
||||
if not name.endswith((".html", ".js")):
|
||||
continue
|
||||
src = open(os.path.join(HTML, name), encoding="utf-8").read()
|
||||
for m in re.finditer(r'onclick="(loadSampleAll|loadSampleSOP|loadExample|loadSampleData)\(\)"', src):
|
||||
hits.append((name, m.group(1)))
|
||||
chk("exactly one sample-data control exists in the whole suite",
|
||||
hits == [("wp-creation-index.html", "loadSampleAll")], ascii_(hits))
|
||||
chk("the other three affordances are removed; grep confirms",
|
||||
not any(fn in ("loadSampleData",) for _, fn in hits)
|
||||
and "loadSampleData" not in open(os.path.join(HTML, "work-package-suite-app.js"),
|
||||
encoding="utf-8").read().replace(
|
||||
"// Removed at T9.4", "").split("loadSampleData")[0])
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="wpsuite-sample-")
|
||||
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)
|
||||
root = tok["root"]
|
||||
|
||||
browser = cdp.Browser(exe)
|
||||
page = browser.page()
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", root)
|
||||
page.viewport(1440, 900)
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
dismiss_dialogs(page)
|
||||
chk("the creator boots on a REAL project", wait_creator(page))
|
||||
settle(1.6)
|
||||
|
||||
# ── 2. placement and the confirm ─────────────────────────────────────
|
||||
print("\n2. away from the live actions, confirmed before acting")
|
||||
gap = page.eval("""(() => {
|
||||
const btns = [...document.querySelectorAll('#wp-toolbar button')];
|
||||
const sample = btns.find(b => b.textContent.trim() === 'Load sample data');
|
||||
if (!sample) return null;
|
||||
const prev = sample.previousElementSibling && sample.previousElementSibling
|
||||
.previousElementSibling; // skip the separator
|
||||
const r = sample.getBoundingClientRect();
|
||||
const pr = prev ? prev.getBoundingClientRect() : {right: 0};
|
||||
return {isLast: btns[btns.length-1] === sample, gap: Math.round(r.left - pr.right)};
|
||||
})()""")
|
||||
chk("the control sits at the far end, clearly separated from live actions",
|
||||
gap and gap["isLast"] and gap["gap"] >= 40, ascii_(gap))
|
||||
|
||||
_, sop_before = api(base, "/api/sops/latest?project_id=projA", root)
|
||||
_, wps_before = api(base, "/api/wps?project_id=projA&full=true", root)
|
||||
|
||||
page.eval("void loadSampleAll()")
|
||||
settle(0.5)
|
||||
d = json.loads(page.eval("""JSON.stringify((() => {
|
||||
const ov = document.getElementById('wp-dialog');
|
||||
return {open: ov.classList.contains('open'),
|
||||
msg: (document.getElementById('wp-dialog-msg')||{}).textContent||''};
|
||||
})())"""))
|
||||
chk("it asks first, naming exactly what will happen",
|
||||
d["open"] and "Replaces the SOP shown on this page" in d["msg"]
|
||||
and "nothing is written to the project" in d["msg"], ascii_(d))
|
||||
page.eval("wpDialogCancel()")
|
||||
settle(0.4)
|
||||
chk("backing out changes nothing",
|
||||
page.eval("!(SOP && SOP.meta && SOP.meta.sample)"))
|
||||
|
||||
page.eval("void loadSampleAll()")
|
||||
settle(0.4)
|
||||
page.eval("wpDialogOk()")
|
||||
settle(1.0)
|
||||
chk("confirming loads the sample SOP and the example package, locally",
|
||||
page.eval("!!(SOP && SOP.meta && SOP.meta.sample)")
|
||||
and page.eval("!!gv('wp_subject')"))
|
||||
|
||||
# ── 3. it cannot touch live project data ─────────────────────────────
|
||||
print("\n3. the fence, verified against a real project")
|
||||
settle(1.5) # anything that WOULD sync has had time to
|
||||
_, sop_after = api(base, "/api/sops/latest?project_id=projA", root)
|
||||
_, wps_after = api(base, "/api/wps?project_id=projA&full=true", root)
|
||||
chk("the project's SOP on the server is byte-identical after the sample load",
|
||||
json.dumps(sop_before, sort_keys=True) == json.dumps(sop_after, sort_keys=True))
|
||||
chk("...and so is its work package list",
|
||||
json.dumps(wps_before, sort_keys=True) == json.dumps(wps_after, sort_keys=True))
|
||||
|
||||
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