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