Files
Project-SDE-WP-Suite/tests/sample_check.py
n.siegfried b44afa7672 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>
2026-08-19 13:05:56 -07:00

170 lines
7.0 KiB
Python

#!/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())