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:
2026-08-19 13:05:56 -07:00
parent c2a1cc7c26
commit b44afa7672
8 changed files with 221 additions and 147 deletions

View File

@@ -247,7 +247,9 @@ def page_checks(page, base, tok):
# "Usage data" left this list at T7.10: D5 moved the report to the admin
# console. Its absence HERE is asserted, so the button cannot quietly return
# and recreate the duplicate D5 existed to remove.
for label in ("Sample SOP", "Load example", "View SOP", "Import SOP"):
# "Sample SOP" + "Load example" became the ONE "Load sample data" at T9.4
# (S7): D1's point was that the sample control is reachable, and it still is.
for label in ("Load sample data", "View SOP", "Import SOP"):
chk("%-13s is visible on the unframed page" % label, vis.get(label) is True, vis)
chk("Usage data is GONE from the creator toolbar (D5)",
"Usage data" not in vis, vis)

169
tests/sample_check.py Normal file
View 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())

View File

@@ -231,48 +231,19 @@ def run(page, base, tok):
".filter(e => (e.textContent||'').trim()).map(e => e.id))"))
chk("no error is shown on a step nobody has tried to leave", not marks, marks)
print("\n5. the wizard opens no native dialog at all")
print("\n5. the wizard opens no native dialog, and its sample affordance is gone")
open_wizard(page, base, tok)
# Drive every path that used to raise one.
page.eval("loadSampleData()")
settle(1.6)
chk("loading the sample announces instead of interrupting",
page.eval("!document.getElementById('wp-toast').hidden"))
chk("...politely, because it is a confirmation not an error",
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "status")
chk("...and says what happened",
"Sample data loaded" in (page.eval(
"(document.getElementById('wp-toast')||{}).textContent||''")),
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
chk("...and can be dismissed from the keyboard", page.eval("""(() => {
const b = document.querySelector('#wp-toast .wp-toast-close');
if (!b) return false;
b.focus();
const focused = document.activeElement === b;
b.click();
return focused && document.getElementById('wp-toast').hidden;
})()"""))
# T9.4 (S7) removed this page's Load sample - it filled the state that
# completeSOP() pushes to the LIVE project, one click, no confirm. The
# suite's one sample control is on the creator. What this section still
# owns: the wizard must not regrow the affordance, and must stay dialog-free.
chk("the wizard's Load sample control is gone (S7/T9.4)",
page.eval("!document.getElementById('load-sample-btn')")
and page.eval("typeof loadSampleData === 'undefined'"))
chk("...and no native dialog fired anywhere so far",
not json.loads(page.eval("JSON.stringify(window.__dialogs||[])")),
page.eval("JSON.stringify(window.__dialogs||[])"))
# This used to be "loading the sample on the wrong tab interrupts", because the
# wizard's Load sample was context-aware: on the WP tab it reached into the
# iframe and called the creator's own loadExample(), and complained when the
# frame was not there. B7/T7.1 made the creator a page, so there is no wrong
# tab to be on and nothing to reach into - this page's sample is the SOP
# sample, always. What is left to check is that the one remaining edge, the
# SOP gate, still says something rather than nothing.
page.eval("showCreatorGate(); loadSampleData()")
settle(1.2)
chk("from the gate, loading the sample announces rather than failing silently",
page.eval("!document.getElementById('wp-toast').hidden"))
chk("...politely, because being on the gate is not an error",
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "status",
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
chk("...and says where the work package sample actually is",
"Work Package Creation" in (page.eval(
"(document.getElementById('wp-toast')||{}).textContent||''")),
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
page.eval("switchTool('sop')")
settle(0.6)
# Through the control a person uses — the modal's text input — not through the
# library helper, which has no duplicate message to give.
@@ -304,16 +275,12 @@ def run(page, base, tok):
"no feedback to export" in (page.eval(
"(document.getElementById('wp-toast')||{}).textContent||''")).lower(),
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
page.eval("showAnalytics()")
settle(0.5)
chk("the usage summary is shown rather than crammed into a confirmation",
"Step 1:" in (page.eval("(document.getElementById('wp-toast')||{}).textContent||''")),
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''"))[:100])
chk("...with the download offered as an action beside it",
page.eval("""(() => {
const b = document.querySelector('#wp-toast .wp-toast-action');
return !!b && /download/i.test(b.textContent);
})()"""))
# showAnalytics() left this page at T7.10 (D5): the usage report lives on
# the admin console now, one implementation for the whole suite. What this
# section keeps: the duplicate must not regrow here.
chk("the wizard's usage-summary duplicate is gone (D5/T7.10)",
page.eval("typeof showAnalytics === 'undefined'")
and page.eval("typeof analyticsLoad === 'undefined'"))
fired = json.loads(page.eval("JSON.stringify(window.__dialogs||[])"))
chk("nothing anywhere in that opened alert / confirm / prompt", not fired, fired[:4])